Skip to content

feat: edit existing secrets in-place for sops-nix and agenix - #658

Open
Scott McMaster (scottmcmaster) wants to merge 2 commits into
08-12-scott-age-secret-add-delfrom
08-13-scott-edit-secrets
Open

feat: edit existing secrets in-place for sops-nix and agenix#658
Scott McMaster (scottmcmaster) wants to merge 2 commits into
08-12-scott-age-secret-add-delfrom
08-13-scott-edit-secrets

Conversation

@scottmcmaster

@scottmcmaster Scott McMaster (scottmcmaster) commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Hook up the edit functionality for secrets for both SOPS and agenix.

ATTENTION

  1. Please note the pecularity specific to SOPS where we need to deduce the repo path from the nix store path for the secret, and if you have any better ideas, please let me know.
  2. There is a subtle difference in how recipients are handled which you should double-check, this is reflected in the UI:
    a. For age, the updated value keeps the same recipients.
    b. For SOPS, we use what's currently in the repo's .sops.yaml.

Also did some refactoring of the YAML-related utility methods, and enhanced some of the unit tests.

Screenshots (note that I hid all of the extra controls from the "add" flow since they were distracting and not directly relevant to the "edit" flow but per prelint comments recipients are included):

Screenshot 2026-08-13 at 3 35 18 PM Screenshot 2026-08-13 at 3 35 32 PM

Test Plan

New unit tests in Rust and TypeScript, manually tested the SOPS and age paths in the UI.

  • No test plan needed

Docs

  • Docs updated (companion PR in darkmatter/nixmac-web: #___)
  • No docs update needed

Copy link
Copy Markdown
Collaborator Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@scottmcmaster Scott McMaster (scottmcmaster) changed the title scott-edit-secrets feat: edit existing secrets in-place for sops-nix and agenix Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

No Linear issue ID found in this PR's title, description, or branch name (expected something like ENG-123). Add one so this work is traceable in Linear, or add #no-linear to the PR description to acknowledge it's intentionally untracked.

Messages
📖 No docs update needed — acknowledged.

📋 PR Overview

Lines changed 1251 (+1059 / -192)
Files 3 added, 12 modified, 0 deleted
Draft / WIP no
Has Test Plan yes
Linear issue no
No Test Plan Needed no
New UI components no
New Storybook stories no
New Rust modules yes (1)
New TS source files no
New tests yes (2)
package.json touched no
Cargo.toml touched no
Infra / CI touched no

🔬 Coverage

Report Lines Statements Functions Branches
apps/native/coverage/coverage-summary.json 36.1% 35.7% 31.4% 30.4%

Generated by 🚫 dangerJS against c78dc70

@darkmatter

darkmatter Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

🎨 Storybook preview

Open Storybook preview

Updated for c78dc70


🧭 Story changes

Compared to 08-12-scott-age-secret-add-del (snapshot diff at story level):

✏️ Changed stories (8)

💡 Update snapshots ↗ to regenerate baselines and open a PR.


⚠️ Detected UI changes (5)

These stories' HTML snapshots changed. I've added screenshots + links to the changed stories below. Review them carefully then accept the changes to regenerate baselines and include them in this PR:

Flows/Evolve › Playground

Flows/Evolve › Playground

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › Evolving With Error Event

Flows/Evolve › Evolving With Error Event

Flows/Evolve › 3. Review (changes generated)

Flows/Evolve › 3. Review (changes generated)


Accept UI changes

  • Click here to accept these changes

Alternatively, you can run bun run test:update-snapshots locally to re-generate the baselines and then push the changes to this PR.

What does this do?

The screenshots above show UI changes detected by the Storybook
snapshot tests run on this PR. Each image is the rendered output of
a Storybook story from the code in this PR branch; the snapshot
test compared it against the committed baseline in
__snapshots__/ and flagged the difference.

Checking the box tells the darkmatter[bot] to regenerate the
baselines from this PR's current code and commit them directly to
this branch. The new baselines become the source of truth for
future runs — only accept after confirming the visual changes are
intentional.

Comparison baseline: the committed __snapshots__/ files on this
PR branch (carried forward from develop). Accept updates them in
place on this branch.

@prelint

prelint Bot commented Aug 13, 2026

Copy link
Copy Markdown

Ship it Secret edit flow: recipients visible, agenix edit gate enforced

Product decisions in this change

Agree 1. Unregistered agenix recipients appear in the edit form by their raw public key with an "Unclassified recipient" label, rather than being hidden or blocking the edit.

A user who replaces a compromised secret needs to confirm every identity that can decrypt the new value. Hiding an unregistered key creates a blind spot at the moment the user cares most about access control. Showing the raw key with a neutral label is honest and lets the user decide whether to proceed. Blocking the edit when any unregistered recipient is present would be too conservative. It would also force vault housekeeping before a time-sensitive rotation.

Agree with concerns 2. A SOPS edit adopts the current `.sops.yaml` recipient list at the time of the edit, which may differ from the recipients on the file when it was originally encrypted.

SOPS is designed so that .sops.yaml is the single authority for recipients. Re-encrypting a SOPS file applies the current rules. This is the expected SOPS behavior, and it differs correctly from agenix, where each file has a distinct recipient list. The UI now shows the current recipients before submission.

The concern is that a user who intends to replace only the value may also change who can decrypt the secret. If a new machine key was added to .sops.yaml after the original encryption, the edit grants that machine access. The recipients panel gives the user the data to notice this. It does not flag the change as a difference from the recipients on the original file.

Option What it gives users What it costs Effort to change later
Current: always use .sops.yaml Consistent with SOPS design Silent recipient change if .sops.yaml was updated Low
Warn when recipients differ from original file Explicit alert at the moment of change Requires reading the existing file's SOPS metadata Medium

The current approach is correct for SOPS-native workflows. A "recipients changed since last encryption" warning is a meaningful follow-up for teams that manage recipient rotation carefully.

Agree with concerns 3. The edit UI reuses the add-secret form with fields hidden or repurposed based on an `editing` flag, rather than using a dedicated edit component.

This was noted as acceptable in the prior review, and no new mode branches have been added. The shared form still serves two divergent models: configuring a new secret versus replacing an existing value. The threshold for splitting into a dedicated edit component is a third mode branch, which has not appeared. The current approach is acceptable for this iteration.

Agree with concerns 4. The SOPS encrypted file is located by matching its filename and byte-level contents against repository files when the evaluated Nix path points to the Nix store.

The matching is conservative. Zero matches and multiple matches both produce a refusal, so no wrong file is ever overwritten. The fragility is that re-encryption of the source file between Nix evaluations will cause the match to fail. This happens even when the file clearly exists in the repository. No improved error message has been added to guide the user in this state. A follow-up should add a message that tells the user to run a rebuild and retry the edit.

Agree with concerns 5. A dry build check runs before the edited encrypted file is committed, even though editing only the encrypted content changes no Nix declarations.

The check can catch unrelated configuration drift but cannot validate the encrypted output itself. This was noted in the prior review. No documentation of this distinction has been added. A brief comment explaining what the check does and does not verify would help future contributors. They would then know whether to extend or skip the check as the secret edit path evolves.

Since the last review

  • Addressed in code: The edit button is always visible for agenix secrets with unresolved or empty recipients, causing a late failure after the user has entered a new value. (secret-detail-view.tsx: agenixEditUnavailable hides the button with explanation, tested in secret-detail-view.test.tsx for both the unresolved and empty-recipient cases.)
  • Still open: The edit UI reuses the add-secret form with mode branches, risking divergence as the two flows evolve. (add-secret-view.tsx still uses a single component with an editing flag. No third branch has appeared, so the prior acceptable assessment still holds.)
  • Addressed in code: The edit flow shows no recipient information, removing the security confirmation a user replacing a compromised secret would most want. (add-secret-view.tsx: encryptionRecipients renders recipients for both backends, tested in add-secret-view.ui.test.tsx for agenix, SOPS, and unregistered-key cases.)
  • Still open: The SOPS file source is located by filename and byte-level content match, which fails when the file was re-encrypted since the last Nix evaluation, with no recovery guidance. (match_repo_file_by_name_and_contents in secrets_management.rs is unchanged. No improved error message guides the user to run a rebuild and retry.)
  • Still open: The dry build before committing an encrypted edit cannot validate the encrypted output, and its rationale is undocumented. (verify_dry_build_for_secret_edit is called in both edit_age_secret and edit_sops_secret with no comment explaining what the check does and does not verify.)
  • Still open: A user editing a SOPS secret whose file was re-encrypted in the repository after the last Nix evaluation will see a confusing failure with no path to recovery. (No error-message improvement or rebuild hint was added to match_repo_file_by_name_and_contents.)
  • Addressed in code: The edit button should be disabled with a tooltip for agenix secrets with unresolved recipients rather than producing a late commit-time failure. (secret-detail-view.tsx hides the button entirely when agenixEditUnavailable is true and renders an inline explanation, which prevents the failure earlier than a tooltip approach would.)
  • Addressed in code: Recipient information should be shown as read-only in the edit flow so users can confirm who gains access before committing. (add-secret-view.tsx renders encryptionRecipients in the edit form for both backends, including unregistered keys shown by raw public key.)
  • Still open: The dry build intent is undocumented and it cannot validate the encrypted output itself. (No comment or documentation added to verify_dry_build_for_secret_edit or its callers in secrets_management.rs.)
  • Still open: For a SOPS secret in a shared YAML file, a concurrent edit to a sibling key between decrypt and re-encrypt could be silently overwritten. (No guard added beyond the existing ensure_clean_repo check, which covers only local working-tree state.)

Open questions

  • If .sops.yaml has been updated since a SOPS secret was last encrypted, the edit silently changes who can decrypt the new value. Should the UI compare the current .sops.yaml recipients against the existing file's SOPS metadata and warn the user when they differ?

  • For a SOPS secret in a shared YAML file: can a concurrent edit to a sibling key between decrypt and re-encrypt be silently overwritten? The clean-repo check covers only the local working tree, not remote changes.

Recommendation

Ship it
Both gaps from the prior review are resolved. Recipients are now visible in the edit form, and the edit button is hidden with a clear explanation when agenix recipients cannot be resolved. The remaining concerns about SOPS file matching, the dry build scope, and the shared form structure were each assessed as acceptable in the prior review and none has worsened.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires up the previously stubbed "edit secret value" flow for both the sops-nix and agenix backends. On the backend it adds an edit_secret command that decrypts the existing encrypted file, replaces the requested value in place (preserving the declaration, file location, backend, and recipients), re-encrypts, runs a dry darwin-rebuild check, and commits — with best-effort rollback on failure. On the frontend it reuses AddSecretView in an "edit" mode that hides the add-only controls, and replaces the "not implemented" edit button with a real one. It also extracts the YAML helpers (replace_yaml_path, remove_yaml_path, validate_yaml_syntax) into a new shared yaml_utils module.

Changes:

  • New Rust edit_secret backend (SOPS + agenix), including conservative recovery of a SOPS repo source file from its evaluated /nix/store path, plus the editSecret orpc route and EditSecretResult shared type.
  • Frontend edit flow: AddSecretView gains an optional secret prop / buildEditRequest, SecretDetailView exposes onEdit, and SecretsManagement routes the new edit view to editSecret.
  • Refactor: YAML utilities moved to yaml_utils.rs with expanded unit tests.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
apps/native/src-tauri/src/secrets/secrets_management.rs Adds edit_secret/edit_sops_secret/edit_age_secret, SOPS store-path source recovery, and tests
apps/native/src-tauri/src/yaml_utils.rs New module hosting YAML path replace/remove/validate helpers + tests
apps/native/src-tauri/src/evolve/file_ops.rs Moves validate_yaml_syntax (and its tests) into yaml_utils
apps/native/src-tauri/src/main.rs Declares the new yaml_utils top-level module
apps/native/src-tauri/src/orpc/secrets.rs Adds editSecret input type, handler, and route
apps/native/src-tauri/src/shared_types/secrets_management.rs Adds EditSecretResult
apps/native/src/ipc/orpc-bindings.ts Generated EditSecretInput/EditSecretResult + editSecret client
apps/native/src/components/widget/secrets/add-secret-view.tsx Edit mode support and buildEditRequest
apps/native/src/components/widget/secrets/add-secret-view.test.ts Tests for buildEditRequest
apps/native/src/components/widget/secrets/secret-detail-view.tsx Replaces stub edit button with real onEdit; removes canEdit
apps/native/src/components/widget/secrets/secrets-management.tsx Routes edit view and calls editSecret
apps/native/src/components/widget/secrets/types.ts Adds edit view kind and edit apply origin

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/native/src/components/widget/secrets/secret-detail-view.tsx Outdated
Comment thread apps/native/src/components/widget/secrets/add-secret-view.tsx Outdated
@scottmcmaster

Copy link
Copy Markdown
Collaborator Author

Ship with changes Secret edit flow: in-place value replacement for SOPS and agenix

Product decisions in this change

Agree 1. Editing a secret value is a first-class operation with its own endpoint and commit, separate from adding a new secret.

Separating edit from add is correct. The two operations have different preconditions. Add creates a declaration and an encrypted file. Edit replaces only the encrypted content. Combining them would force the add path to handle cases it was not designed for, such as preserving existing recipients. A dedicated commit message also makes the history legible.

Agree with concerns 2. The "Edit value" button is always visible on the secret detail view, regardless of whether the edit can succeed for that secret.

The always-visible button keeps the UI uniform, but a user with an agenix secret that has unresolved recipients starts the edit flow and sees a failure only at the build-and-commit step. A user who has typed a new credential value and submitted the form receives a backend error. The error provides no guidance on how to fix the underlying problem.

Option What it gives users What it costs Effort to change later
Always visible (current) Uniform UI Late failure with no guidance for unresolved recipients Low
Conditionally visible Early prevention and a clear message Requires checking recipient resolution state at render time Low
The current approach works if unresolved recipients are rare. It becomes harmful if users regularly encounter this state with no explanation.

Agree with concerns 3. The edit UI reuses the add-secret form with the name, backend selector, runtime path, and recipient sections hidden when in edit mode.

The shared component avoids duplicating code for a first implementation. But it now serves two divergent models: configure a new secret versus replace an existing value. Mode branches already appear in several places inside the component. Every future change to the add flow must consider whether it also applies to the edit flow. The current approach is acceptable. A dedicated edit component becomes the right choice when a third branch appears.

Agree 4. The review screen shows a symbolic placeholder diff representing the replaced value, rather than the actual encrypted bytes.

Encrypted file diffs are ciphertext and carry no useful information for a human reviewer. The placeholder honestly describes what changed without pretending the user can verify the raw bytes. This is consistent with how secrets management tools generally handle the review problem.

Agree 5. Editing a secret preserves the exact recipients already recorded on the encrypted file, and does not fall back to the currently registered recipient list.

This is the correct security model. A fallback to the current recipient list could silently add or remove identities that were not on the original file. For agenix, each file can have a distinct recipient set, so the current-registry fallback would be a silent security change. Refusing the edit when recipients cannot be resolved is also correct. A conservative failure is safer than a guess.

Agree with concerns 6. The edit flow does not show users which recipients can decrypt the new value.

Recipients do not change during an edit, so displaying them is not strictly required. But a user who is replacing a compromised secret wants confirmation that the new value is encrypted to the right identities and that no extra recipients have gained access. The review screen currently offers no security-relevant information beyond the file path and a symbolic placeholder.

Option What it gives users What it costs Effort to change later
No recipient display (current) Simpler edit form No confirmation of access scope Low
Read-only recipient display Explicit confirmation of who can decrypt the new value Minor added UI Low
Recipient data is already available in SecretEntry. The list component already exists in the add flow. A read-only display requires no backend changes.

Agree with concerns 7. The app locates the editable SOPS file by matching its filename and encrypted contents when the Nix configuration points to an internal store location rather than the repository directly.

When Nix copies a SOPS file into the store, it severs the link to the repository source. The PR recovers it by matching the store artifact name and bytes against files in the repository. Refusing on zero or multiple matches is the correct safety boundary. No wrong guess is ever made.

The fragility is in the content match. If the repository SOPS file has been re-encrypted since the last Nix evaluation, its bytes no longer match the store artifact. Recipient rotation is one common cause of this state. The edit then fails with a message saying the source cannot be found, even though the file clearly exists. The user has no obvious path forward other than running a rebuild to refresh the store artifact first. The PR author acknowledges this limitation. A follow-up should surface a clearer error message that explains the mismatch and tells the user what to do.

Agree with concerns 8. A dry build check runs before the edited encrypted file is committed.

Editing only the encrypted content does not change any Nix declarations. A dry build cannot detect a bad SOPS encryption or a corrupted agenix file. If the intent is to confirm the Nix configuration still evaluates cleanly after any recent unrelated changes, the check provides that assurance. If the intent is to validate the edited secret itself, it does not. The check is harmless but its rationale should be documented so future contributors understand what it verifies and what it does not.

Agree 9. An agenix edit fails explicitly with an error when the existing recipient set cannot be resolved, rather than proceeding with the currently registered recipients.

The agenix model allows per-file recipient lists that differ from the global registry. Re-encrypting with the current registry would be a silent security change. Refusing and surfacing a clear error is the correct behavior. Tests cover both the unresolved and empty-recipient cases.

Open questions

  • What happens when a user tries to edit a SOPS secret whose file was re-encrypted in the repository after the last Nix evaluation? The content-matching recovery will fail with a confusing message. Is there a planned path forward, or must the user run a rebuild first?
  • Should the edit button be disabled with a tooltip when an agenix secret has unresolved recipients, rather than failing at commit time after the user has already entered a new value?
  • Is there a plan to show recipient information as read-only during the edit flow, so users can confirm who gains access to the new value before committing?
  • The dry build before committing cannot validate the encrypted output itself. Is the intent to catch unrelated configuration drift, or is a separate validation of the encrypted file planned?
  • For a SOPS secret in a shared YAML file with multiple keys: if another key in the same file was changed in the repository after the last Nix evaluation, does the edit operate on the current repository version or on the Nix store version? Could a concurrent change by another contributor be silently overwritten?

Recommendation

Ship with changes The security decisions are correct: recipients are preserved exactly, refusals are conservative, and the SOPS source recovery is honest about ambiguity. Two gaps are worth addressing before merge. The edit review screen shows no recipient information, removing the one confirmation a user replacing a compromised secret would most want. The always-visible edit button also produces a late failure for agenix secrets with unresolved recipients, with no early guidance to the user.

I addressed the two gaps:

  1. Bring back recipients view in the Edit flow.
  2. Hide the Edit button proactively when age public recipients can't be resolved on the backend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants