Skip to content

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
compass-server/ledger-id-collisionfrom
compass-server/rig-3320-secretspec-v020
Open

chore(secrets): bump secretspec to v0.20, thread an audit reason, and stage an age-capable CLI (RIG-3320)#916
rigel-mintaka wants to merge 2 commits into
compass-server/ledger-id-collisionfrom
compass-server/rig-3320-secretspec-v020

Conversation

@rigel-mintaka

@rigel-mintaka rigel-mintaka commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

This PR is part of a stack containing 2 PRs:

  1. main
  2. fix(design): renumber colliding ledger ids DL-327..330 to DL-334..337 #926
  3. "chore(secrets): bump secretspec to v0.20, thread an audit reason, and stage an age-capable CLI (RIG-3320)" (this PR)

Prerequisite for the RIG-2863 Layer-A boot-credential path, which needs an
age://-capable provider and a working resolver.Set.

The bump

secretspec-go v0.15.0 -> v0.20.0. The SDK API compass uses is unchanged, so
resolver.go compiles as-is; the version-pin drift guard moves to v0.20.0 and
re-cites the write-path evidence against v0.20 source (secrets.rs:4423-4427
piped-stdin branch and trim, :4430-4433 empty-value rejection). The
stdin/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 secretspec binary by name, and no closure
staged one, so age:// was unavailable in practice. age is a default-on cargo
feature of the crate, so a stock package already carries it — this adds bare
secretspec to the dev/CI shell rather than building anything. The pinned
channel ships 0.19.1, which has the age provider (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 the
plaintext is absent from the file.

The write path needed an audit reason

secretspec 0.17+ enforces a require_reason policy that defaults to agents
and refuses a set carrying no reason. The read path already threaded one
(Resolve -> WithReason), but the write path did not, so Set would have
failed closed against the new pin — including the master-key mint the Layer-A
path performs.

Set now carries a reason alongside the value, mirroring the read path, and
emits --reason before the set subcommand (it is a global flag). An empty
reason omits the flag entirely rather than passing --reason "", leaving the
CLI'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

@trunk-io

trunk-io Bot commented Sep 6, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

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

@linear-code

linear-code Bot commented Sep 6, 2026

Copy link
Copy Markdown

RIG-3320

RIG-2863

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

@rigel-mintaka rigel-mintaka changed the title chore(secrets): bump secretspec to v0.20 and thread an audit reason through the write path (RIG-3320) chore(secrets): bump secretspec to v0.20, thread an audit reason, and stage an age-capable CLI (RIG-3320) Sep 6, 2026
rigel-mintaka and others added 2 commits September 6, 2026 01:12
`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
rigel-mintaka changed the base branch from main to compass-server/ledger-id-collision September 6, 2026 05:18
@rigel-mintaka
rigel-mintaka force-pushed the compass-server/rig-3320-secretspec-v020 branch from 5819d35 to bbf59de Compare September 6, 2026 05:18
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant