pi-provider: give one machine its own bearer on a provider #95

Merged
vnprc merged 1 commit from agent/per-provider-target-cli into master 2026-10-21 22:53:00 +01:00
Member

pi-provider grant <provider> <machine> [token-name] gives one machine a bearer only it holds on a provider whose credential is otherwise shared: one hidden prompt, one new ciphertext encrypted to that machine's host keys and the hypervisor's, every other target's ciphertext and recipient set untouched. pi-provider rotate <provider> <machine> [new-token-name] replaces that bearer in place, or moves the claim to a fresh named token and retires the old one once nothing claims or defaults to it. Both write the new ciphertext before the registry repoints, and both refuse to land if any ciphertext the operation did not name changes recipients. Registry validation matches the per-target contract clause for clause, including per-(provider, target) uniqueness; provider-wide commands refuse a split provider loudly instead of picking one. retarget, token remove, and discovery handle mixed shared/claimed credentials, and shared-credential behavior is unchanged. docs/credentials.md documents the composed, resumable migration from one shared credential to per-target bearers; its last hop, emptying the shared target list, is tracked in allod/nexus#83.

Rebased onto master after allod/nexus#94's add --models-file. The seam needed two fixes: master's classify_add called provider_credential with one argument after this branch gave it a required command label, which the suite caught as an unbound variable; and add --models-file refused a provider split across credentials, which this branch makes contract-valid and which a models update does not need resolved to one credential. It now names every credential serving the provider and a new case covers it.

Closes allod/nexus#36
Closes allod/secrets#16
Refs allod/nexus#83

Risk

R3. Secret custody: the tool writes Age ciphertexts and the registry that drives per-machine decryption across two repositories. Narrowness is the contract's job (lib.piCredentialRecipients, witnessed by the secrets pi-credential-registry check), the tool's pre-write assertion refuses unintended recipient changes, and every path is fixture-tested, but a mistake here reaches real bearers on real machines. Rollback is a revert plus pi-provider recover for an interrupted run.

Validation

bash tests/pi-provider.sh: 443 assertions, exit 0 (master before allod/nexus#94: 336; the delivered branch: 410). nix build .#checks.x86_64-linux.provisioning-contract on the rebased commit: exit 0, which runs shellcheck over the tool and the suite. Two cold cross-family review rounds: codex gpt-5.6-sol on the delivered branch found three defects, all reproduced and fixed before relay; codex gpt-5.6-sol again on the rebased diff found the split-provider add --models-file refusal, fixed with a test, and six comments to trim, trimmed. Its one other finding, that retarget installs the registry before the shared ciphertexts it re-encrypts, is pre-existing master behavior unchanged here and covered by the recovery journal, so it is noted and not acted on.

A third pass after the fix round, correctness only, on two seats: gemma4-31b over nullsink returned no findings; codex gpt-5.6-sol returned two, both pre-existing and not changed here. The bearer is visible under bash -x in prompt_and_encrypt, exactly as on master's path, since xtrace prints every expansion. And the suite stubs nix, so the bash-ported registry validator's agreement with lib/pi-credential-schema.nix is witnessed by the live run's post-install build of the secrets pi-credential-registry check and by the deployment build, not by the suite, which was already the suite's design.

`pi-provider grant <provider> <machine> [token-name]` gives one machine a bearer only it holds on a provider whose credential is otherwise shared: one hidden prompt, one new ciphertext encrypted to that machine's host keys and the hypervisor's, every other target's ciphertext and recipient set untouched. `pi-provider rotate <provider> <machine> [new-token-name]` replaces that bearer in place, or moves the claim to a fresh named token and retires the old one once nothing claims or defaults to it. Both write the new ciphertext before the registry repoints, and both refuse to land if any ciphertext the operation did not name changes recipients. Registry validation matches the per-target contract clause for clause, including per-(provider, target) uniqueness; provider-wide commands refuse a split provider loudly instead of picking one. `retarget`, `token remove`, and discovery handle mixed shared/claimed credentials, and shared-credential behavior is unchanged. `docs/credentials.md` documents the composed, resumable migration from one shared credential to per-target bearers; its last hop, emptying the shared target list, is tracked in allod/nexus#83. Rebased onto master after allod/nexus#94's `add --models-file`. The seam needed two fixes: master's `classify_add` called `provider_credential` with one argument after this branch gave it a required command label, which the suite caught as an unbound variable; and `add --models-file` refused a provider split across credentials, which this branch makes contract-valid and which a models update does not need resolved to one credential. It now names every credential serving the provider and a new case covers it. Closes allod/nexus#36 Closes allod/secrets#16 Refs allod/nexus#83 ## Risk R3. Secret custody: the tool writes Age ciphertexts and the registry that drives per-machine decryption across two repositories. Narrowness is the contract's job (`lib.piCredentialRecipients`, witnessed by the secrets `pi-credential-registry` check), the tool's pre-write assertion refuses unintended recipient changes, and every path is fixture-tested, but a mistake here reaches real bearers on real machines. Rollback is a revert plus `pi-provider recover` for an interrupted run. ## Validation `bash tests/pi-provider.sh`: 443 assertions, exit 0 (master before allod/nexus#94: 336; the delivered branch: 410). `nix build .#checks.x86_64-linux.provisioning-contract` on the rebased commit: exit 0, which runs shellcheck over the tool and the suite. Two cold cross-family review rounds: codex `gpt-5.6-sol` on the delivered branch found three defects, all reproduced and fixed before relay; codex `gpt-5.6-sol` again on the rebased diff found the split-provider `add --models-file` refusal, fixed with a test, and six comments to trim, trimmed. Its one other finding, that `retarget` installs the registry before the shared ciphertexts it re-encrypts, is pre-existing master behavior unchanged here and covered by the recovery journal, so it is noted and not acted on. A third pass after the fix round, correctness only, on two seats: gemma4-31b over nullsink returned no findings; codex `gpt-5.6-sol` returned two, both pre-existing and not changed here. The bearer is visible under `bash -x` in `prompt_and_encrypt`, exactly as on master's path, since xtrace prints every expansion. And the suite stubs `nix`, so the bash-ported registry validator's agreement with `lib/pi-credential-schema.nix` is witnessed by the live run's post-install build of the secrets `pi-credential-registry` check and by the deployment build, not by the suite, which was already the suite's design.
`grant <provider> <machine> [token-name]` resolves the provider's credential and claims the machine a bearer of its own, defaulting the token name to the machine and refusing a name already in use; `rotate <provider> <machine> [new-token-name]` replaces that bearer in place with no new name, or moves the claim to a fresh named token -- retiring the old one -- when one is given. Both write the new ciphertext before the registry and refuse to land if any ciphertext they did not name changes recipients. Registry validation, retarget, token remove, and docs/credentials.md now speak the provider-keyed verbs, and the `target` noun group is gone from the CLI.
vnprc approved these changes 2026-10-21 22:52:55 +01:00
vnprc merged commit 9280ac4351 into master 2026-10-21 22:53:00 +01:00
vnprc deleted branch agent/per-provider-target-cli 2026-10-21 22:53:01 +01:00
Sign in to join this conversation.
No description provided.