Store named Pi credential tokens with per-token ciphertexts #14

Merged
vnprc merged 2 commits from agent/named-tokens-registry into master 2026-08-24 02:42:27 +01:00
Member

A Pi credential can now hold several named tokens instead of exactly one, and each token gets its own encrypted file.

Nothing changes for any running machine today. The public credential registry is empty, so this generates no secrets, no recipients, and no per-VM settings — same as before.

What stays identical: every non-Pi secret, every recipient list, every other flake output, and the empty public registry itself.

What changes is the contract that a private deployment fills in. A credential record is now exactly providers, targets, tokens, and defaultToken; the old rotationStrategy field is gone and a record still carrying it is rejected. Each token stores its bearer at secrets/pi-credentials/<credential>/<token>.age, all tokens of one credential are encrypted to the same machines, and each VM is told only the token names and file paths — never a bearer or an endpoint.

What proves it: both repository checks build green, and each rejection rule has a deliberately broken fixture that the check must reject.

If it is wrong, revert this commit straight. Nothing downstream pins the new shape until the deploy slice lands, and no live credential is touched either way.

Risk

Residual risk is R3 High, matching the plan's slice table entry for the secrets contract. The scrutiny is on the exact schema, the per-token recipient sets and paths, and the guarantee that no secret data reaches a projection. Those three are pinned by exact-equality assertions against a synthetic two-credential fixture plus per-rule sabotage, but a synthetic fixture cannot attest that a private registry migrated cleanly or that a real ciphertext decrypts on its target: this change alters the ciphertext layout that a private fork will have to move files into, and the correctness of that move is owned by the Nexus slice and the private rollout, not by anything provable here. The public blast radius is bounded to zero by the empty registry.

Validation

From the change worktree:

nix build .#checks.x86_64-linux.credential-inventory .#checks.x86_64-linux.pi-credential-registry

Both derivations built successfully (exit 0).

Additionally run from the same worktree:

nix flake check

All three checks (external-ssh-trust-targets, pi-credential-registry, credential-inventory) evaluated and built (exit 0).

The pi-credential-registry check was proved non-vacuous two ways before the final build. Corrupting one expected recipient key in the fixture expectation made the build fail with error: pi-credential-registry: recipient derivation drifted; pointing the rejects helper at the valid fixture instead of a sabotage made it fail with error: pi-credential-registry: retired rotationStrategy field was accepted. Both edits were reverted before committing.

Each schema sabotage was also confirmed to fail for the intended reason by reading the contract's non-throwing diagnostics output: a leftover rotationStrategy and a removed tokens field report entries have missing or unknown fields; empty, duplicated, and invalid-name token lists report tokens must be non-empty unique token-name lists; an unlisted and a non-string default report defaultToken must be null or one listed token; and hiding a single token's ciphertext reports missing ciphertexts: secrets/pi-credentials/shared/secondary.age, which pins per-token path derivation rather than the credential root alone. Token-name length was checked at the boundary: 63 characters is accepted and 64 is rejected.

The empty public outputs were verified directly: pi-credentials.json is {}, and lib.piCredentialRecipients, lib.piCredentialCiphertextPaths, and the derived credential inventory are all empty, with secrets.nix still listing exactly its six pre-existing paths.

Refs allod/strategy#36

A Pi credential can now hold several named tokens instead of exactly one, and each token gets its own encrypted file. Nothing changes for any running machine today. The public credential registry is empty, so this generates no secrets, no recipients, and no per-VM settings — same as before. What stays identical: every non-Pi secret, every recipient list, every other flake output, and the empty public registry itself. What changes is the contract that a private deployment fills in. A credential record is now exactly providers, targets, tokens, and defaultToken; the old rotationStrategy field is gone and a record still carrying it is rejected. Each token stores its bearer at `secrets/pi-credentials/<credential>/<token>.age`, all tokens of one credential are encrypted to the same machines, and each VM is told only the token names and file paths — never a bearer or an endpoint. What proves it: both repository checks build green, and each rejection rule has a deliberately broken fixture that the check must reject. If it is wrong, revert this commit straight. Nothing downstream pins the new shape until the deploy slice lands, and no live credential is touched either way. ## Risk Residual risk is R3 High, matching the plan's slice table entry for the secrets contract. The scrutiny is on the exact schema, the per-token recipient sets and paths, and the guarantee that no secret data reaches a projection. Those three are pinned by exact-equality assertions against a synthetic two-credential fixture plus per-rule sabotage, but a synthetic fixture cannot attest that a private registry migrated cleanly or that a real ciphertext decrypts on its target: this change alters the ciphertext layout that a private fork will have to move files into, and the correctness of that move is owned by the Nexus slice and the private rollout, not by anything provable here. The public blast radius is bounded to zero by the empty registry. ## Validation From the change worktree: ``` nix build .#checks.x86_64-linux.credential-inventory .#checks.x86_64-linux.pi-credential-registry ``` Both derivations built successfully (exit 0). Additionally run from the same worktree: ``` nix flake check ``` All three checks (`external-ssh-trust-targets`, `pi-credential-registry`, `credential-inventory`) evaluated and built (exit 0). The `pi-credential-registry` check was proved non-vacuous two ways before the final build. Corrupting one expected recipient key in the fixture expectation made the build fail with `error: pi-credential-registry: recipient derivation drifted`; pointing the `rejects` helper at the valid fixture instead of a sabotage made it fail with `error: pi-credential-registry: retired rotationStrategy field was accepted`. Both edits were reverted before committing. Each schema sabotage was also confirmed to fail for the intended reason by reading the contract's non-throwing `diagnostics` output: a leftover `rotationStrategy` and a removed `tokens` field report `entries have missing or unknown fields`; empty, duplicated, and invalid-name token lists report `tokens must be non-empty unique token-name lists`; an unlisted and a non-string default report `defaultToken must be null or one listed token`; and hiding a single token's ciphertext reports `missing ciphertexts: secrets/pi-credentials/shared/secondary.age`, which pins per-token path derivation rather than the credential root alone. Token-name length was checked at the boundary: 63 characters is accepted and 64 is rejected. The empty public outputs were verified directly: `pi-credentials.json` is `{}`, and `lib.piCredentialRecipients`, `lib.piCredentialCiphertextPaths`, and the derived credential inventory are all empty, with `secrets.nix` still listing exactly its six pre-existing paths. Refs allod/strategy#36
A Pi credential record is now exactly providers, targets, tokens, and
defaultToken. The vestigial rotationStrategy field is gone, so a record
still carrying it fails as an unknown field.

Every named token derives its own ciphertext at
secrets/pi-credentials/<credential>/<token>.age, all tokens of one
credential share that credential's Nexus-plus-target active/staged
recipient set, and the derived inventory emits one agenix consumer per
ciphertext. The per-VM projection carries names and paths only:
credentials.<credential> = { providers; tokens.<token>.file;
defaultToken; }.

The public registry stays empty and still generates nothing.

Model: claude-opus-5
Author
Member

Review pass (required post-open review): read-only diff review by gpt-5.6-terra at reasoning xhigh against the plan contract in allod/strategy dev-plans/pi-named-tokens.md. Scope: full master...HEAD diff, all in-repo consumers of the changed lib shapes (including the piCredentialCiphertextPaths credential->token->path change), sabotage non-vacuity, and every PR-body claim. Verdict: no findings. The reviewer sandbox could not reach the Nix daemon, so the managing agent independently re-ran the two acceptance checks from the PR worktree: nix build .#checks.x86_64-linux.credential-inventory .#checks.x86_64-linux.pi-credential-registry — both green.

Review pass (required post-open review): read-only diff review by gpt-5.6-terra at reasoning xhigh against the plan contract in allod/strategy dev-plans/pi-named-tokens.md. Scope: full master...HEAD diff, all in-repo consumers of the changed lib shapes (including the piCredentialCiphertextPaths credential->token->path change), sabotage non-vacuity, and every PR-body claim. Verdict: no findings. The reviewer sandbox could not reach the Nix daemon, so the managing agent independently re-ran the two acceptance checks from the PR worktree: nix build .#checks.x86_64-linux.credential-inventory .#checks.x86_64-linux.pi-credential-registry — both green.
The plan at 46d074e excludes the literal name none from valid token names; it is the pi-provider token-default clearing word. Schema validation now rejects it and a sabotage fixture proves the rejection; the README schema section records the reservation.

Model: gpt-5.6-terra
vnprc approved these changes 2026-08-24 02:42:21 +01:00
vnprc merged commit 9d637b1f93 into master 2026-08-24 02:42:27 +01:00
vnprc deleted branch agent/named-tokens-registry 2026-08-24 02:42:28 +01:00
Sign in to join this conversation.
No description provided.