allod secret: declare, create, rotate, and migrate on credential value templates #208
No reviewers
Labels
No labels
blocked
bug
decision
duplicate
enhancement
help wanted
invalid
landed?
question
ready-to-merge
stale
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/tools!208
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/secret-value-templates"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Nothing changes on any machine until nexus moves its
allod-toolspin; dev VMs never gain the host-only commands because they compile only behind thesecrettag. On the host,allod secretstops carrying a copy of the credential vocabulary:declarewrites a credential's non-secret half as avalue.template(or novaluefor a plain secret) with an optionalvalue.encodechecked against the secrets flake'slib.credentialEncodingsexport and oneverifycommand per target;createandrotaterender the candidate secret through one shared renderer and never decrypt an existing value; the newmigrate <name>translates one legacy container entry to the declared shape exactly once, proving the decrypted bytes round-trip through the proposed template before touching the registry.rotatekeeps the whole reviewed landing gate from allod/tools#207 (clean branch, in-memory encryption, atomic writes, checks, branch re-check before commit, printed deploy, verify, and revocation steps even when the push fails) and refuses a legacy entry by namingmigrate. The proof is the tagged and untagged test suites over fakeage,nix,git, andrclone, with golden bytes for every rendering and every registry edit. This PR lands last, after the archetypes and secrets PRs. If it is wrong, revert it and move the pin back;rotate-tokenstill rotates every legacy entry and no ciphertext this PR wrote exists until the owner runs a command.What changes
allod secret declare(untagged):--formatand the probe-type flags (--verify <probe>,--verify-repo-url,--verify-context,--verify-user) are gone. A credential's value is declared with--value-template-file <path>or--value-template-stdin(mutually exclusive, bytes stored untrimmed, exactly one{secret}), optionally--value-encode <name>checked against the checkout'slib.credentialEncodings, and--verify <command>is one non-empty line per target. The registry encoder no longer HTML-escapes, so<in a command stays readable.createlooks the credential up in the registry, refuses a legacy or mixed-shape entry by namingmigrate, validates the declared value against the exported encodings, and renders the candidate secret through the same rendererrotateuses.rotateis allod/tools#207's reviewed command with the format switch replaced by one rendering per group member from the one value read, and the enum validation replaced by the new contract. It never decrypts, refuses a legacy selected credential or group member by namingmigrate, checksrcloneis onPATHbefore reading the value when any member encodes, keeps the dry run, the branch re-checks, atomic writes, restore-all-then-report, group summary, deploy steps (including therotate-token refresh-local-authstep), verification, and revocation gate. Verification prints the bare command for the local host andssh '<system>' '<command>'with POSIX single-quote escaping otherwise.migrate <name>(tagged): checks everything it can without plaintext first (branch, clean tree, registry lookup, still-legacy, not mixed, not alocal_auth_refreshsource, one of the two containers, every target's verify object convertible, no unmodelled registry field), then decrypts once into memory, parses only the canonical bytes rotate-token writes, proves the extracted secret renders back to the original bytes and appears nowhere else in the template, splices only that credential object's bytes inforgejo-token-groups.json, runs the checks, commits, pushes. The rclone container's migrated declaration carriesencode: "rclone-obscure"; the ciphertext is never rewritten. Verify objects become the inner commands rotate-token printed (git-ls-remote,forge-token-verify,site-check,tailscale-status); the legacy targetuseris baked into the command and dropped.secret_value.go: the renderer, value validation, the one encoder implementation (stdin only, output held to one base64url token), verify-string reading, and the registry lookup.landCommitregains the branch re-verification immediately beforegit addand the report-push variantrotateuses to print its steps before a push failure is fatal.createandrotatefor plain, URL, and encoded multiline values with and without a trailing newline, every new refusal, migration goldens and refusals with a whole-checkout scan proving plaintext reached no file, remote verification with embedded single quotes, and the untagged surface (declarepresent;create,rekey,migrate,rotateunknown).docs/allod-secret.md,README.md, and theflake.nixtag comment describe the new command set and the verbatim-secret rule.declaredispatcher; the second is the value-template change; the third fixes the review findings. None is stacked on an open PR.Risk
R3 in the plan (host-only decryption and authoritative repository writes). Residual after the tests:
migrate's strict legacy parsers refuse a real ciphertext whose bytes are not the canonical shape rotate-token wrote, which is a refusal, not a wrong write; and a verbatim secret piped with a trailing newline lands inside a URL template, which the netrc activation and git credential store then reject loudly at the next rebuild. The first live run of each command should be--dry-runwhere it exists and be verified with the printed command before any old value is revoked. Rollback: revert this PR and the pin; a landed rotation or migration is a commit on a branch nobody merged.Validation
All run by the overseer in the worktree on the committed tree (
834ee26), each command's last line and exit status:nix develop -c gofmt -l .nix develop -c go vet ./...nix develop -c go test ./...ok forge.anarch.diy/allod/tools/internal/gitremote 0.170snix develop -c go test -tags site ./...ok forge.anarch.diy/allod/tools/internal/gitremote 0.140snix develop -c go test -tags secret ./...ok forge.anarch.diy/allod/tools/internal/gitremote 0.145snix develop -c go test -tags site,secret ./...ok forge.anarch.diy/allod/tools/internal/gitremote 0.168snix build --no-link .#checks.x86_64-linux.go-checksbuilding '/nix/store/vky3ixr643aqgly5599i6djfrcg8di1n-allod-tools-go-checks.drv'...git diff --checkThe worker also ran the seven
nix developcommands on the first commit alone (1bb3406) and on the second (931ab6d) and reported them green; the overseer ran the full table above on931ab6das well before the review round.Cold reviews of this diff before the third commit, each reading a
.git-free snapshot and the diff against master: through thepirunner on providernullsink,gpt-5.6-terraat thinkinghigh(1 blocker, 3 high, 1 medium: encoder stderr echoed into a fatal error,migratedecrypting before checking the encoder export, unquoted legacyuser/repo_url/deployed_pathbecoming shell text in a verify command, same-group duplicate entries passing the one-entry check, invalid UTF-8 templates silently altered by the JSON encoder) and, becausegpt-5.6-solatxhighrefused with a router balance error and codex was out of quota, a second pass byclaude-fable-5-1as a fresh Claude Code subagent (5 gaps, 1 simplify, 1 question:createreading stdin before proving rclone is on PATH, unknown verify fields anduserdropped silently, the written template never validated,rotatemisdirecting plain legacy entries tomigrate, stale create help, non-atomic create/rekey writes). Every finding was verified against the source and fixed in the third commit (834ee26), each with a test; the golden for the bakeduseralso caught the verify command being HTML-escaped on write, now fixed. The second pass is same-family with the fix author, recorded here as a deviation forced by runner availability. The fixes were authored by a Claude model, so the GPT pass is cross-family. A coverage audit of the previous attempt's deleted tests (70 tests deleted, none re-homed) drove the restoration; the audit and the worker brief are recorded in the overseer's session notes.Known limit, deliberately left:
ageencrypt and decrypt errors still quoteage's own stderr, the same class as the encoder finding.agedoes not echo its input, so no test can make it leak here; tightening it is a one-line follow-up if the owner wants the same rule for every subprocess handed a secret.Not run: a live
create,rotate, ormigrateagainst realage,nix, andrcloneon the host, because no agent machine holds an age identity. The first live run of each should be--dry-runwhere it exists, and be verified with the printed command before any old value is revoked.Closes allod/secrets#23
Closes allod/tools#197
Refs allod/tools#182, allod/tools#207
Replaces the three hand-named credential formats and the verify-probe enum with one declared shape. A credential's non-secret text is a template around one {secret} placeholder, an optional value.encode names a transformation applied to the secret before substitution, and each target's verify is one command string printed verbatim. Adding a kind of credential is now a registry edit rather than a matching code change in three repositories. declare takes --value-template-file or --value-template-stdin (bytes stored without trimming), --value-encode, and --verify <command> in place of --format and the structured verify flags. create and rotate render through one shared renderer, so both produce the same bytes for the same declaration, and rotate no longer decrypts anything: the non-secret half is declared, not recovered. The encoder list comes from the checkout's own lib.credentialEncodings; this build carries an implementation per exported name and refuses one it cannot perform. Adds the host-only secret-tagged migrate, which translates one legacy container credential exactly once. It decrypts in memory after every check that does not need the plaintext, accepts only the canonical bytes rotate-token writes, proves the extracted secret substituted back reproduces those bytes and occurs nowhere else in the template, and rewrites only that credential's registry entry; the ciphertext is untouched. It refuses a local_auth_refresh source until that consumer is migrated. During coexistence a credential is wholly legacy or wholly new; the mix is refused. rotate names migrate for a legacy container, and for a legacy member of the group it was asked to rotate. Refs allod/secrets#23834ee2643btof48399c5e5