chore(secrets): bump secretspec to v0.20, thread an audit reason, and stage an age-capable CLI (RIG-3320) - #916
Open
rigel-mintaka wants to merge 8 commits into
Open
Conversation
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
rigel-mintaka
changed the base branch from
main
to
compass-server/ledger-id-collision
September 6, 2026 05:18
rigel-mintaka
force-pushed
the
compass-server/rig-3320-secretspec-v020
branch
from
September 6, 2026 05:18
5819d35 to
bbf59de
Compare
rigel-mintaka
added a commit
that referenced
this pull request
Sep 6, 2026
DL-330 was claimed by the merged apple-container macOS-runner record, so Record A's row was a duplicate and would have red the fail-closed design-ledger-gate on merge. DL-340 sits above every in-flight mint (#927/#916 hold DL-333..337), not merely above main's tail — a tail check cannot see a concurrently-open claim, which is how this class of collision keeps recurring. Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka
force-pushed
the
compass-server/rig-3320-secretspec-v020
branch
from
September 6, 2026 15:28
bbf59de to
e9d2d91
Compare
… stage an age-capable CLI (RIG-3320) The secrets write path shells the `secretspec` CLI (the Go SDK is read-shaped), and three things were wrong with that seam. **1. The SDK pin moved to v0.20.0.** `go build` needs no source change; the write-path contract is unchanged and re-verified against v0.20.0 source (`secrets.rs:4423-4427` for the piped-stdin branch and trim, `:4430-4433` for empty-value rejection). The intentional tripwire `TestSecretSpecVersionPin` moves with it. **2. `require_reason` (0.17+) hard-fails a reasonless `set`.** The policy defaults to `agents` and refuses a write with no reason, so the old argv broke on the new CLI. `Set` now takes a `reason` and emits `--reason` before the subcommand. An empty reason is **rejected up front** rather than omitting the flag: the CLI's policy gates on agent-environment detection, so an omitted reason makes the same write succeed on one host and fail on another, surfacing to operators as a retryable provider fault. Screening it makes the failure a deterministic caller error and makes the interface's "the reason travels with every write" promise literally true. **3. `Set` never told the CLI where the manifest was.** No `cmd.Dir`, no `--file`, and no `secretspec.toml` is committed by design (the registry is the source of truth), so every operator write failed `No secretspec.toml found in current or any parent directory`. `Set` now generates a manifest declaring exactly the name being written and passes it via the global `--file` flag — the same explicit-manifest treatment `Resolve` already gave the read path. Verified red/green against the real CLI from a manifest-less cwd: exit 1 without `--file`, exit 0 with it, and an exact readback. ## Staging an age-capable CLI The write path spawns `secretspec` by name, and nothing staged one, so `set` was unreachable from the dev shell. A bare `secretspec` is not enough: this shell's nixpkgs channel still resolves **0.14.0**, which has no `age` provider compiled in at all and fails an encrypted-at-rest write with `Provider backend 'age' not found` rather than degrading. It is therefore resolved from a second nixpkgs input pinned in `devenv.lock`, carrying **0.20.0** — the same version as the SDK pin, so the read path (SDK + native lib) and the write path (shelled CLI) move together instead of skewing across an independent seam. Consumed as a dotted attr outside the parsed `with pkgs` literal, matching `skopeo-nix2container`, because the toolchain-parity gate resolves every bare attr in that list. `age://` is proven end to end through the real dev shell: write with the value on stdin (absent from argv), a 499-byte `-----BEGIN AGE ENCRYPTED FILE-----` blob with the plaintext absent, and an exact readback. ## Drift guards `TestSecretSpecVersionPin` only ever covered the SDK half; its doc now says so. `TestSecretSpecCLIVersionFloor` covers the other half, asserting the staged binary's version floor — the CLI decides whether `--reason` is accepted, whether the policy exists, and whether `age` exists at all, and none of that is visible to a go.mod pin. It skips when no binary is on PATH so hermetic runs stay green, and it fails loudly against 0.14.0 with the provider diagnostic. ## Also Both `vendorHash` literals that consume `go/go.mod` are refreshed — `flake.nix` and `guest-image/default.nix`. Missing the second one failed `moon (nix)` on the `compass-guestd` fixed-output derivation. Three design-record sites that quote or prescribe the old 3-arg `Set` are amended to the new signature, including the T2 provisioning instruction, which now passes a concrete audit reason. Ledger-impact: none. Refs RIG-3320 Co-authored-by: Matt Wilkinson <matt@rigel.build>
rigel-mintaka
force-pushed
the
compass-server/rig-3320-secretspec-v020
branch
from
September 6, 2026 16:31
e9d2d91 to
7a331f2
Compare
rigel-mintaka
changed the base branch from
compass-server/ledger-id-collision
to
main
September 6, 2026 16:32
…g=value argv (RIG-3320) Review follow-ups on the secretspec v0.20 bump: - `setArgs` now takes the resolved profile and emits `--profile` unconditionally. The generated manifest header and the argv are fed from one `resolvedProfile()` call, so the CLI cannot act under a different profile than the manifest declares by silently falling back to its own built-in default. - Document that the joined `--flag=value` form is required rather than stylistic: the two-token form parses a leading-dash reason as the next flag and exits 2. - Widen the `//nolint:gosec` justification to cover all three argv variables (name, reason, manifest path) instead of only the name. - Drop the stale `secretspec-go v0.15.0` hashes left in `go.sum`. - Cover the both-empty guard precedence in `Set`: the value guard fires before the reason guard, which `server.SetSecret` relies on when it maps a Set failure to `CodeUnavailable`. - Refresh the design records line references onto the moved code. Refs RIG-3320 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…r (RIG-3320) setArgs used the joined --flag=value form for --reason and --file but left --provider and --profile in the two-token form the same function's comment declares unsafe. That rationale is a property of the value's shape, not of which flag carries it: ValidateProfile admits a leading dash and the provider URI is unvalidated, so a dash-leading value was parsed as the next flag and the CLI exited 2 — surfacing through SetSecret as a misleading retryable CodeUnavailable rather than a config error. Join all four, and cover the operator-configured flags in the hostile-input test that previously exercised only reason. The version-floor test's failure message and devenv.yaml both cited a 0.15 age floor, contradicting the design record's 0.17. Upstream's changelog puts age:// under 0.17.0, so the record was right and both citations are corrected rather than the record. Also refresh three stale artifacts: WithProfile's doc still promised the SDK default an explicit --profile now precludes, the record's Load citation still named the v0.15.0 pin (re-verified at v0.20.0 secretspec.go:293 — still no ctx, so the offload design stands), and the audit reason was anonymous despite the handler holding the caller ID, which is half the provenance the reason exists for. Refs RIG-3320 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…it-reason contract (RIG-3320) Round-3 review findings, all documentation/coverage — no behavior change. - Design record: the round-3 commit added 5 lines to resolver.go and silently invalidated 23 file:line citations in the record it also edits. Re-point them all to the head tree, and fix the two SDK binding citations left at their v0.15.0 offsets inside the very sentence that was refreshed to v0.20.0 (binding_cgo.go:30 -> :28, binding_purego.go:118 -> :142). - resolver.go: the Set doc comment still illustrated the two-token '--provider <p> --profile <P>' form that the next sentence declares broken and that setArgs no longer emits. Show the joined form and the widened value-shape scope. - secrets_service.go: lead the audit-reason comment with the structural guarantee (callerID is a bearer-resolved, server-minted hex account id, never a request field) and keep the CLI's JSON escaping as defense in depth. - secrets_service_pgtest_test.go: the caller-bound audit reason was a new observable contract with no test. Bind the assertion to the authenticated user's account id and pin that the reason never carries the value. Refs RIG-3320 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…tale v0.15 claims (RIG-3320) Round-4 review findings. No behavior change. - Design record: round 4 swept resolver.go citations only, but commit 1 shifted go/server/secrets_service.go by +10 lines, so four citations pointed at real-but-wrong code — including the F1 master-key guard's placement, which named an audit comment and a nil-resolver early return instead of the two resolver call sites. Re-point them, and audit EVERY citation into a file this PR touches rather than one filename: all 48 now resolve (resolver.go x34, secrets_service.go x12, go.mod x2). - resolver.go + secrets_service.go: the `Delete` no-op was rationalized as "no CLI verb upstream". True at v0.15.0, false at this pin — `secretspec delete` shipped in 0.18 and is in the staged binary. Say the verb exists and that wiring it is a deliberate deferral (RIG-3436): it makes the operation destructive against a keyspace shared by default, so it needs its own F1-guard and ordering analysis. Same correction in the record. - resolver.go: `defaultCLI` claimed the dev shell and the deployed image both stage the binary. Only the dev shell does; nothing stages it into the shipped artifact. Narrow the claim to what holds and point at RIG-3437. - secrets_service_pgtest_test.go: hoist the secret literal so the value-absence assertion cannot silently stop testing anything when the input changes. Refs RIG-3320 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…ot hand-counted (RIG-3320) Round-4 review found a high. The previous commit re-pointed the secrets_service.go citations and, in the same commit, edited resolver.go — adding a line to the defaultCLI comment and three to the Delete comment — without re-pointing the 34 resolver.go citations. 29 went stale, 7 of them onto real-but-wrong executable code: resolver.go:171 is cited four times as the uncancellable FFI `b.Load()` that anchors T2's whole bounded-offload design, and it had come to resolve to `b = b.WithProfile(profile)`, a pure builder call with no FFI and nothing to block on. An agent implementing from that reads the offload machinery as unnecessary. Same class of break on the F1 master-key guard citation: :218 landed on a comment, one line above the `resolver.Delete` call it names. The root cause is not carelessness on any one line, it is that the audit was done by hand twice and both times scoped to the file I had most recently thought about. So this pass computes it: diff each touched file against the last pushed revision, build a parent-line -> head-line map from the matching blocks, and rewrite every citation through the map. 31 refs moved. Then verify in the other direction — every cited anchor is checked to still name the construct the prose says it does (b.Load(), the Set/setArgs signatures, exec.CommandContext, the F1 pre-call sites, go.mod:22), because in-range is not the same as correct and only the second check would have caught this. Also from the review: - Two F2 passages said the repo "pins ... at v0.15.0" while citing go/go.mod:22, which this PR changes to v0.20.0 — the citation contradicted the sentence quoting it. Reframed as what it is: the pin as of writing, with the bump noted. - Dropped the value-never-in-argv loop from TestSetArgs. setArgs takes no value parameter, so the string it scans for is never in scope and no mutation could redden it; it read like the guard while asserting nothing. The comment now points at TestSetFeedsValueOnStdinNeverArgv, which spawns a real process and checks the child's actual argv. Refs RIG-3320 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…s too (RIG-3320) Round-5 review. Last round's fix computed the line map correctly and then applied it to exactly one document — the record this PR already edits. But the citations of a shifted file are not confined to the record that happens to discuss it: resolver.go moved +67 lines and devenv.nix +15, and 81 more citations of those two files live in sixteen OTHER frozen records this PR never touched. They were right at the merge base and wrong at head. Same defect as round 4, one scope level out, and the previous commit message's claim to 'rewrite every citation through the map' described the map's construction rather than where it got applied. So the unit of the audit is now the SHIFTED FILE, not the edited document: build a base->head line map for every code file this stack touches, then sweep every markdown file in docs/ for citations into any of them. 27 resolver.go refs across four records (native-client-mode, ownership-layer, linear-agent-responder, forge-poll-driver) and 54 devenv.nix refs across twelve more. The worst was the same b.Load() failure round 4 found: native-client-mode cited resolver.go:162-164 for the WithProvider/WithProfile/Load chain, which at head is a comment about temp-file cleanup. Verified by text identity rather than by range, in both directions: for all 156 citation endpoints, the line the base document pointed at and the line the head document points at are byte-identical. That check is what distinguishes a correct citation from a merely in-range one, and it is the check that would have caught both this and the round-4 high. Two more from the review, both in the record this PR edits: - The v0.15 reframing fixed the sentence quoting go.mod:22 but left the surrounding 'on the current pin' prose meaning v0.15, so each paragraph contradicted itself within four lines — stating the bump is done at v0.20.0 while enumerating the v0.15 capability matrix and calling age:// gated. An agent implementing T0/T2's provider wiring could not tell whether the record's own ruled default was available. Both paragraphs now speak of v0.15 in the past tense and state the prerequisite is met. - The record's one inline spelling of the write command was the pre-PR shape, missing the two joined global flags this PR makes mandatory — it taught the argv form this PR's own test reddens. Refs RIG-3320 Co-authored-by: Matt Wilkinson <matt@rigel.build>
…s (RIG-3320)
Round-6 review. Two gaps in the previous sweep, both places its own stated
rule was not actually applied.
The commit message said the audit unit is the shifted FILE rather than the
edited document, then the sweep narrowed to markdown under docs/ — so eight
endpoints in Go source comments were never visited, including the same
b.Load() failure mode the last two rounds each called the worst case:
serve.go cited resolver.go:135-165 for the Resolve->b.Load() span, which at
head is a temp-file-cleanup comment and a defer os.Remove. A citation lives
wherever someone wrote it, and .go comments are a place people write them.
Fixed in serve.go and internal/stack/{spec,config,deps}.go; all four files are
untouched by this PR, so those citations were correct at the merge base and are
regressions this stack introduced.
The rewriter also only re-pointed the FIRST numeric component of a
comma-joined citation, leaving four trailing components at base values — e.g.
devenv.nix:328,383, where the 383 half is the --listen line the sentence is
actually about. The text-identity check missed it by comparing per-citation
rather than per-endpoint, which is the same shape of error: verifying the unit
you happened to iterate over instead of the unit that has to be right.
Both checks are now per-ENDPOINT and prefix-aware. Prefix-awareness matters
because a bare basename match would conflate root devenv.nix with
agent-image/devenv.nix, forks/*/default.nix and guest-image/default.nix, which
this PR does not touch — I verified the previous round shifted none of those.
All 15 endpoints verified by text identity: the line each citation named at the
merge base and the line it names at head are byte-identical.
Refs RIG-3320
Co-authored-by: Matt Wilkinson <matt@rigel.build>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prerequisite for the RIG-2863 Layer-A boot-credential path, which needs an
age://-capable provider and a workingresolver.Set.The bump
secretspec-gov0.15.0 -> v0.20.0. The SDK API compass uses is unchanged, soresolver.gocompiles as-is; the version-pin drift guard moves to v0.20.0 andre-cites the write-path evidence against v0.20 source (
secrets.rs:4423-4427piped-stdin branch and trim,
:4430-4433empty-value rejection). Thestdin/trim/empty-reject contract is byte-for-byte the same behavior it was at
v0.15, verified against the new source rather than assumed.
The CLI is staged, not built
The secrets write path shells a
secretspecbinary by name, and no closurestaged one, so
age://was unavailable in practice.ageis a default-on cargofeature of the crate, so a stock package already carries it — this adds bare
secretspecto the dev/CI shell rather than building anything. The pinnedchannel ships 0.19.1, which has the
ageprovider (0.17+).Verified end to end against the staged binary: a write with the value on stdin
produces an
age-encrypted file on disk, the value reads back intact, and theplaintext is absent from the file.
The write path needed an audit reason
secretspec 0.17+ enforces a
require_reasonpolicy that defaults toagentsand refuses a
setcarrying no reason. The read path already threaded one(
Resolve->WithReason), but the write path did not, soSetwould havefailed closed against the new pin — including the master-key mint the Layer-A
path performs.
Setnow carries areasonalongside the value, mirroring the read path, andemits
--reasonbefore thesetsubcommand (it is a global flag). An emptyreason omits the flag entirely rather than passing
--reason "", leaving theCLI's own policy to accept or refuse the write. The secret value still never
appears in argv; it rides stdin only.
The argv-capture test now asserts the flag is present, positioned before the
subcommand, and that the value stays out of argv — so dropping or misordering
the flag fails the suite.
Ledger-impact: none
Refs RIG-2863
Co-authored-by: Matt Wilkinson matt@rigel.build