feat: add agenix secret add/delete support - #656
feat: add agenix secret add/delete support#656Scott McMaster (scottmcmaster) wants to merge 3 commits into
Conversation
|
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.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
🎨 Storybook previewUpdated for 85fd686
|
📋 PR Overview
🔬 Coverage
|
There was a problem hiding this comment.
Caution
The agenix diff preview hardcodes ../../secrets/${slug}.age as the file path, which is only correct when the declaration module lives at exactly modules/darwin/agenix-secrets.nix (two directorie...
apps/native/src/components/widget/secrets/add-secret-view.tsx:24
1 finding(s) posted as inline comments.
There was a problem hiding this comment.
Pull request overview
This PR implements agenix secret add and delete support in nixmac, bringing the agenix backend to feature parity with the existing SOPS flow. Both flows encrypt/remove a per-secret .age file, edit the classic agenix rules file and the age.secrets declaration module, verify with a darwin-rebuild dry build, and commit — with bounded rollback on failure. The frontend gains a backend toggle (sops-nix / agenix) in the add-secret form and backend-aware copy across the apply and delete UIs. The change also includes DRY refactoring: shared helpers ensure_clean_repo, restore_repo_files_on_failure, and verify_dry_build_for_secret_edit; extracted sops_config_path / discover_agenix_rules_path / match_agenix_secret_entries in recipients.rs; and a new remove_attrpath_in_file plus quote-aware split_attrpath_for_match in nix_file_editor.rs.
Changes:
- Implement
add_age_secret/delete_age_secretwith agenix rules + declaration editing, age encryption over stdin, dry-build verification, and rollback. - Refactor shared secret-edit safety helpers and agenix rule discovery/matching; add quote-aware attrpath splitting so keys like
"api.token.age"resolve correctly. - Frontend: backend selector in the add-secret form, backend-aware recipient filtering/empty-state, and agenix-aware copy in the apply sheet and delete dialog.
Reviewed changes
Copilot reviewed 8 out of 8 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 | Core agenix add/delete logic, shared clean-repo/verify/rollback helpers, agenix rule/declaration writers and tests. |
| apps/native/src-tauri/src/secrets/recipients.rs | Extracts sops_config_path, discover_agenix_rules_path, and match_agenix_secret_entries; adjusts debug logging. |
| apps/native/src-tauri/src/evolve/nix_file_editor.rs | Adds quote-aware split_attrpath_for_match and filesystem remove_attrpath_in_file, with tests. |
| apps/native/src/components/widget/secrets/add-secret-view.tsx | Backend toggle, backend-specific paths/copy, registration-based recipient filtering, empty-state message. |
| apps/native/src/components/widget/secrets/secret-detail-view.tsx | canDelete allows agenix regardless of local decrypt capability; backend-aware delete dialog copy. |
| apps/native/src/components/widget/secrets/apply-sheet.tsx | Derives backend-specific encryption label ("age"/"SOPS"/fallback) for review and applying states. |
| apps/native/src/components/widget/secrets/types.ts | Adds optional backend field to ApplyRequest. |
| apps/native/src/components/widget/secrets/secrets-management.tsx | Generalizes the add-secret failure message to be backend-agnostic. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
f5d297c to
b76a140
Compare
f4ee943 to
6593ba3
Compare
b76a140 to
5b2306b
Compare
6593ba3 to
11665de
Compare
5b2306b to
f117d3d
Compare
11665de to
0ee0c75
Compare
652ca35 to
0ad03b4
Compare
darkmatteragent
left a comment
There was a problem hiding this comment.
Review — REQUEST CHANGES
0ad03b47165f · 5 findings
Requesting changes: the new agenix flows can verify one tree and commit another. - The declaration-module finder discards its validated canonical path, so with a symlinked standard module the edit lands on the target while commit and rollback touch the symlink. - Two majors: suffix-colliding rule basenames make deletion falsely refuse, and encrypt_age_secret can deadlock on large values. - Two nits: a wrong doc comment and a missed DRY migration.
Findings
Caution
blocker · correctness — find_secret_declaration_file discards the validated canonical path and returns the unvalidated lexical one
apps/native/src-tauri/src/secrets/secrets_management.rs:617
Gutters 615-616 canonicalize and scope-check the standard module via resolve_existing_path_in_dir(base, standard_relative), then discard the result, and gutter 617 returns Ok(standard) — base.join(standard_relative) (gutter 609), the raw lexical path. When the standard module (or an ancestor like a symlinked modules/darwin) is a tracked symlink to another file inside the repo, is_file()
Warning
major · correctness — matching_agenix_rule_key basename fallback string-suffix over-matches and blocks deletion
apps/native/src-tauri/src/secrets/secrets_management.rs:264
The exact branch (gutter 254, evaluated_path.ends_with(Path::new(key))) compares whole path components, so for declarations written via builtins.path — which this patch itself produces (gutter 746, nix_builtins_path_meta_value) — the evaluated secret.file is /nix/store/<hash>-<basename>.age, whose hash-prefixed component never equals the rule key's components, and every such secret falls
Warning
major · correctness — encrypt_age_secret can deadlock on large plaintexts (stdin write before draining stdout)
apps/native/src-tauri/src/secrets/secrets_management.rs:674
encrypt_age_secret pipes stdin, stdout, and stderr (gutters 666-668) and writes the entire plaintext with write_all (gutter 674) before any output is drained (wait_with_output only runs at gutter 676). age encrypts as a stream: once the child has emitted ~64KiB of ciphertext its stdout pipe fills, it stops reading stdin, and the parent blocks in write_all with the stdin pipe still full — a mut
Tip
nit · documentation — remove_agenix_declaration doc says "rules file" but edits the declaration module
apps/native/src-tauri/src/secrets/secrets_management.rs:293
The doc comment at gutter 293 reads "Remove an agenix declaration for a specific secret from the rules file", but the function removes age.secrets."{secret_id}" via remove_attrpath_in_file on the caller-provided relative_file, which delete_age_secret passes as declaration_rel (gutter 147) — the declaration module, not the rules file. Same-family function remove_agenix_rule (gutter 286) is
Tip
nit · maintainability — New find_secret_declaration_file duplicates find_sops_declaration_file instead of replacing it
apps/native/src-tauri/src/secrets/secrets_management.rs:600
The new generic find_secret_declaration_file (gutters 600-654) reproduces the gitignore-filtered walkdir scan, visibility check for the standard module, and single-candidate match of the untouched find_sops_declaration_file (pre-existing, near-verbatim); only the error strings differ. The same applies to the $RULES/secrets.nix/secrets/secrets.nix discovery, now written twice (find_agenix_rules
Caution blocker · correctness — find_secret_declaration_file discards the validated canonical path and returns the unvalidated lexical one Gutters 615-616 canonicalize and scope-check the standard module via Warning major · correctness — matching_agenix_rule_key basename fallback string-suffix over-matches and blocks deletion The exact branch (gutter 254, Warning major · correctness — encrypt_age_secret can deadlock on large plaintexts (stdin write before draining stdout) encrypt_age_secret pipes stdin, stdout, and stderr (gutters 666-668) and writes the entire plaintext with Tip nit · documentation — remove_agenix_declaration doc says "rules file" but edits the declaration module The doc comment at gutter 293 reads "Remove an agenix declaration for a specific secret from the rules file", but the function removes Tip nit · maintainability — New find_secret_declaration_file duplicates find_sops_declaration_file instead of replacing it The new generic find_secret_declaration_file (gutters 600-654) reproduces the gitignore-filtered walkdir scan, visibility check for the standard module, and single-candidate match of the untouched find_sops_declaration_file (pre-existing, near-verbatim); only the error strings differ. The same applies to the $RULES/ reviewed: 0ad03b4 — 2026-09-01T00:03Z verdict: request_changes findings: 5
Run details
|
||||||||||||||||||||||||||||
ac9ad52 to
6d33048
Compare
0ad03b4 to
12ae9b2
Compare
12ae9b2 to
d9c860f
Compare
6d33048 to
9cbf1b4
Compare
There was a problem hiding this comment.
Caution
encrypt_age_secret calls write_all(plaintext) on the child's stdin and only then calls wait_with_output to drain stdout.
apps/native/src-tauri/src/secrets/secrets_management.rs:674
Caution
find_secret_declaration_file calls resolve_existing_path_in_dir(base, standard_relative) for scope validation but discards the returned canonical path, returning Ok(standard) — the raw lexical `...
apps/native/src-tauri/src/secrets/secrets_management.rs:617
Warning
The basename fallback in matching_agenix_rule_key uses secret.file.ends_with(basename) — a string-suffix check, not a path-component check.
apps/native/src-tauri/src/secrets/secrets_management.rs:264
3 finding(s) posted as inline comments.
Resolved findings (1)
The agenix diff preview hardcodes../../secrets/${slug}.ageas thefilepath, which is only correct when the declaration module lives at exactlymodules/darwin/agenix-secrets.nix(two directorie...apps/native/src/components/widget/secrets/add-secret-view.tsx:24
d9c860f to
90b8230
Compare
9cbf1b4 to
03f4a59
Compare
Prelint Review SummaryActive findings (3)Caution
Caution
Warning The basename fallback in
Resolved findings (1)
3 active, 1 resolved |
90b8230 to
5ef9bbe
Compare
|
Prelint reached the review limit for this pull request (5 reviews of each kind). New pushes do not start a review.
|






Summary
Implement the agenix secret add/delete analogous to what we did for SOPS. (Not including screenshots since there's no practical difference in the UI.)
Also includes some related DRY refactoring in secrets_management.rs and recipients.rs.
Test Plan
Some new unit tests where possible/appropriate, plus manual e2e testing with my test config repo.
Docs