Add recoverable Pi provider lifecycle command #26

Merged
vnprc merged 10 commits from agent/pi-provider-lifecycle into master 2026-08-22 04:16:29 +01:00
Member

This adds a Nexus-only pi-provider command for safely adding, sharing, retargeting, rotating, retiring, and recovering Pi provider credentials.
Bearer values enter through a hidden prompt, flow only to Age over stdin, and never enter Git, argv, the environment, logs, or persistent plaintext files.
The command edits only the profiles/secrets registries and Age ciphertext; it does not commit, rebuild, test a real endpoint, or revoke a remote token.
Dry runs need no token and change nothing; interrupted writes retain a path-scoped recovery journal and refuse unsafe recovery.
Existing Nexus provisioning behavior is unchanged until the command is deliberately run.
If this is wrong, leave the PR unmerged; no private deployment pin consumes it yet.

Refs allod/strategy#34

Depends on the data contracts in allod/profiles#6, allod/secrets#13, and the rebuilt-host workspace in allod/inventory#12.

Risk

R3 High. This command accepts authentication material and coordinates recoverable writes across two repositories. Review should focus on the stdin-only secret boundary, recipient derivation, shared-credential impact, and recovery confinement.

Validation

  • nix build .#checks.x86_64-linux.provisioning-contract -L
  • The Pi witness passes 82 assertions covering dry-run, invalid inputs, add/share/retarget/rotate/retire, ciphertext recipients and mode, ordinary rollback, each registry/ciphertext install phase, both sides of orphan deletion, post-validation death, unsafe-recovery refusal, and secret non-persistence.
  • shellcheck, bash -n, and git diff --check pass through the aggregate check.

Real inference, private migration, VM rebuilds, and provider-side token revocation remain gated to the later private rollout.

This adds a Nexus-only `pi-provider` command for safely adding, sharing, retargeting, rotating, retiring, and recovering Pi provider credentials. Bearer values enter through a hidden prompt, flow only to Age over stdin, and never enter Git, argv, the environment, logs, or persistent plaintext files. The command edits only the profiles/secrets registries and Age ciphertext; it does not commit, rebuild, test a real endpoint, or revoke a remote token. Dry runs need no token and change nothing; interrupted writes retain a path-scoped recovery journal and refuse unsafe recovery. Existing Nexus provisioning behavior is unchanged until the command is deliberately run. If this is wrong, leave the PR unmerged; no private deployment pin consumes it yet. Refs allod/strategy#34 Depends on the data contracts in allod/profiles#6, allod/secrets#13, and the rebuilt-host workspace in allod/inventory#12. ## Risk R3 High. This command accepts authentication material and coordinates recoverable writes across two repositories. Review should focus on the stdin-only secret boundary, recipient derivation, shared-credential impact, and recovery confinement. ## Validation - `nix build .#checks.x86_64-linux.provisioning-contract -L` - The Pi witness passes 82 assertions covering dry-run, invalid inputs, add/share/retarget/rotate/retire, ciphertext recipients and mode, ordinary rollback, each registry/ciphertext install phase, both sides of orphan deletion, post-validation death, unsafe-recovery refusal, and secret non-persistence. - `shellcheck`, `bash -n`, and `git diff --check` pass through the aggregate check. Real inference, private migration, VM rebuilds, and provider-side token revocation remain gated to the later private rollout.
Author
Member

Independent read-only review of HEAD 042e215 found four concrete gaps:

  • Medium — recovery mutates an unrecorded fixed path and can follow an ignored symlink. restore_journal writes tracked bytes through .pi-provider-recover-<basename> (scripts/pi-provider:604-612), but that path is neither preflighted nor included in TOUCHED_SPECS. An ignored pre-existing symlink there is absent from assert_dirt_confined, so shell redirection overwrites its external target and mv then installs the symlink as the registry/ciphertext path. This contradicts path-confined exact recovery. Record and preflight the recovery temp alongside each transaction temp (and allow it in safe_journal_path), or create a guaranteed-new regular file with mktemp and safely clean/refuse leftovers.
  • Medium — retarget omits the approved per-VM impact report. Its output names all providers and the replacement target list (scripts/pi-provider:677-680) but does not compute old/new target differences or say which shared providers are added to and removed from each VM. The witness only matches Shared providers (tests/pi-provider.sh:257-260). Print and test the provider set for every added/removed VM before secret input.
  • Medium — --credential can silently replace the positional retarget credential. Parsing retarget expected --credential shared --target dev-b overwrites CREDENTIAL_ID at lines 93-104, while retarget validation cannot distinguish the flag and proceeds against shared. Track whether --credential was supplied and reject it outside add. The same strictness check should reject --api/--rotation-strategy on recover, which currently ignores them.
  • Low — the PR's “every crash boundary” validation claim is broader than the witness. The matrix (tests/pi-provider.sh:302-328) covers before only the first add mv, after each add destination, and post-validation; it does not leave the second/third transaction temp before their moves, exercise retirement deletion, or prove unrelated-dirt refusal (only changed-HEAD refusal is tested). Expand the boundary/unsafe-state matrix, including unrelated dirt with journal preservation, or narrow the PR claim.

I independently reran nix build .#checks.x86_64-linux.provisioning-contract --no-link -L, bash tests/pi-provider.sh (53 passing assertions), and git diff --check. The current checks pass; these findings are contract/coverage gaps not reported test failures.

Independent read-only review of HEAD `042e215` found four concrete gaps: - **Medium — recovery mutates an unrecorded fixed path and can follow an ignored symlink.** `restore_journal` writes tracked bytes through `.pi-provider-recover-<basename>` (`scripts/pi-provider:604-612`), but that path is neither preflighted nor included in `TOUCHED_SPECS`. An ignored pre-existing symlink there is absent from `assert_dirt_confined`, so shell redirection overwrites its external target and `mv` then installs the symlink as the registry/ciphertext path. This contradicts path-confined exact recovery. Record and preflight the recovery temp alongside each transaction temp (and allow it in `safe_journal_path`), or create a guaranteed-new regular file with `mktemp` and safely clean/refuse leftovers. - **Medium — `retarget` omits the approved per-VM impact report.** Its output names all providers and the replacement target list (`scripts/pi-provider:677-680`) but does not compute old/new target differences or say which shared providers are added to and removed from each VM. The witness only matches `Shared providers` (`tests/pi-provider.sh:257-260`). Print and test the provider set for every added/removed VM before secret input. - **Medium — `--credential` can silently replace the positional retarget credential.** Parsing `retarget expected --credential shared --target dev-b` overwrites `CREDENTIAL_ID` at lines 93-104, while retarget validation cannot distinguish the flag and proceeds against `shared`. Track whether `--credential` was supplied and reject it outside `add`. The same strictness check should reject `--api`/`--rotation-strategy` on `recover`, which currently ignores them. - **Low — the PR's “every crash boundary” validation claim is broader than the witness.** The matrix (`tests/pi-provider.sh:302-328`) covers before only the first add `mv`, after each add destination, and post-validation; it does not leave the second/third transaction temp before their moves, exercise retirement deletion, or prove unrelated-dirt refusal (only changed-HEAD refusal is tested). Expand the boundary/unsafe-state matrix, including unrelated dirt with journal preservation, or narrow the PR claim. I independently reran `nix build .#checks.x86_64-linux.provisioning-contract --no-link -L`, `bash tests/pi-provider.sh` (53 passing assertions), and `git diff --check`. The current checks pass; these findings are contract/coverage gaps not reported test failures.
Author
Member

Follow-up verification of the four findings against HEAD dc503fa: all are resolved.

  • Recovery temp paths are now planned, preflighted, allowlisted, and journaled as non-pre-existing managed paths; restore requires the matching journal entry and refuses an ignored recovery-path symlink before redirection. The witness proves the outside file and journal remain intact.
  • retarget computes the old/new target difference before secret input and names every shared provider added to or removed from each VM.
  • A supplied --credential can no longer replace the positional retarget ID, and recover refuses provider-definition flags.
  • The witness now covers install/start and both sides of every registry/ciphertext move, both sides of orphan ciphertext deletion, post-validation death, ignored recovery symlinks, unrelated dirt, and changed HEAD. The PR wording accurately describes that scope.

Independent reruns passed: bash tests/pi-provider.sh (82 assertions), the aggregate provisioning check, and git diff --check. No findings remain from this review.

Follow-up verification of the [four findings](https://forge.anarch.diy/allod/nexus/pulls/26#issuecomment-5187) against HEAD `dc503fa`: all are resolved. - Recovery temp paths are now planned, preflighted, allowlisted, and journaled as non-pre-existing managed paths; restore requires the matching journal entry and refuses an ignored recovery-path symlink before redirection. The witness proves the outside file and journal remain intact. - `retarget` computes the old/new target difference before secret input and names every shared provider added to or removed from each VM. - A supplied `--credential` can no longer replace the positional retarget ID, and `recover` refuses provider-definition flags. - The witness now covers install/start and both sides of every registry/ciphertext move, both sides of orphan ciphertext deletion, post-validation death, ignored recovery symlinks, unrelated dirt, and changed HEAD. The PR wording accurately describes that scope. Independent reruns passed: `bash tests/pi-provider.sh` (82 assertions), the aggregate provisioning check, and `git diff --check`. No findings remain from this review.
Author
Member

The owner requested that this PR be rebased after a batch of changes landed on master. This branch is one base commit behind. I will rebase it first because archetypes and deploy pin its exact feature head, rerun the aggregate lifecycle gate, then update those downstream pins to the rewritten commit.

The owner requested that this PR be rebased after a batch of changes landed on `master`. This branch is one base commit behind. I will rebase it first because archetypes and deploy pin its exact feature head, rerun the aggregate lifecycle gate, then update those downstream pins to the rewritten commit.
Author
Member

The repository's public-history policy rejected the force-push required by a literal rebase, so this branch was refreshed with a merge from current master instead. Merge head: 18c64d059688928694e8ff71c25cd17656efd230.

The merged tree is byte-identical to the clean local rebase result. The aggregate provisioning contract passed, the standalone Pi lifecycle witness passed all 82 assertions, and git diff --check is clean. Archetypes and deploy will pin this new reachable head.

The repository's public-history policy rejected the force-push required by a literal rebase, so this branch was refreshed with a merge from current `master` instead. Merge head: `18c64d059688928694e8ff71c25cd17656efd230`. The merged tree is byte-identical to the clean local rebase result. The aggregate provisioning contract passed, the standalone Pi lifecycle witness passed all 82 assertions, and `git diff --check` is clean. Archetypes and deploy will pin this new reachable head.
vnprc approved these changes 2026-08-22 04:16:23 +01:00
vnprc merged commit 18c64d0596 into master 2026-08-22 04:16:29 +01:00
vnprc deleted branch agent/pi-provider-lifecycle 2026-08-22 04:16:29 +01:00
Sign in to join this conversation.
No description provided.