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 2 commits into
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 |
|
Compass engineering docs preview: https://compass-server-rig-3320-secr.compass-eng-docs.pages.dev Deployed from Changed pages: |
`main` is red on `design-ledger-gate:check`: the delivery→EventFabric cutover record's ledger block (merged as #905) claimed DL-327..333, but four of those ids were already taken by records that merged first, so the ledger carries four duplicates and the gate fails on every branch rebased onto current `main`. Because the pre-push gate runs `moon ci`, this blocks every lane's submit — and a stale clone still reads green, so it is easy to miss. The four cutover rows move to the free tail; the earlier claimants keep their ids because already-merged records cite them and other lanes implement from those citations. | id | kept by | cutover row moved to | | --- | --- | --- | | DL-327 | SubjectService token principal (#868) | DL-334 (OQ-1 ack timing) | | DL-328 | gateway-credentials at-rest encryption (#859) | DL-335 (OQ-2 callback) | | DL-329 | compass-stack supervision | DL-336 (OQ-3 p1 DB role) | | DL-330 | apple-container macOS runner | DL-337 (OQ-3 p2 recovery) | DL-331/332/333 were already unique and keep their ids, so the cutover block is now contiguous at DL-331..337. Its record's `Ledger-impact` line and the four in-prose references are updated to match, including the DL-333 row's own citation of the reconnect trigger it rests on (DL-330 → DL-337). No ruling is altered — this is an identifier reconciliation. Same shape as the earlier DL-319/DL-320 → DL-322/DL-323 reconciliation. Verified: `moon run design-ledger-gate:check` passes, and the ledger parses to 304 rows with zero duplicate ids (it previously reported four). Ledger-impact: renumbers DL-327..330 to DL-334..337; no ruling changes. Refs RIG-3107 Co-authored-by: Matt Wilkinson <matt@rigel.build>
… 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
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>
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.
This PR is part of a stack containing 2 PRs:
mainPrerequisite 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