The machine pairing vault borrows the GitHub vault's active key and cannot survive its rotation #192
Evidence
Shipped in the release at 8c53d09, promoted 2026-08-26.
1 pushes receipt
- AtlantisPleb opened this issue 3d ago
-
AtlantisPleb
closed this as completed in
7023ed42d ago -
A Author 2d ago Closing as completed. Merged to main and pushed as 7023ed4 (WAL receipt seq 306). The pairing vault now seals with its own key (MACHINE_TOKEN_ENCRYPTION_KEY) and never borrows the GitHub key to write. Migration posture, chosen from the code rather than by habit: no rewrap, because pairing ciphertext has exactly one reader (claim_locked_pairing/1) which nulls the column in the same transaction, and expire_elapsed_pairings/0 enforces a 600-second lifetime — a rewrap-on-read branch is unreachable and an eager sweep would chase a population that turns over in ten minutes. Decrypt falls back to the whole GitHub keyring (active plus github_token_decryption_keys), so a rotation performed mid-transition still reads. Missing dedicated key fails typed from both seal and open. New invariant VAULT-001: rotating one vault's key never unreads another's records; rehearsal 4 now states each vault's rotation cost (GitHub lossless, pairing bounded at 10 minutes, voice permanent). Dev and test configs use keys distinct from the GitHub key so the suite catches re-coupling. 52 targeted tests pass. Owner action recorded in the workspace NEEDS_OWNER.md: provision openagents-staging-machine-vault-active and mount it; until then the vault runs on a recorded bridge to the GitHub key.
What happened
#180's rehearsal 4 asks that rotating each key-like secret leave already-issued records readable. For the machine pairing vault it does not, and the coupling that causes it is not recorded in any document.
OpenAgents.Machines.TokenVaultderives its key like this:It reads the GitHub vault's active key. Its envelope carries a version byte and no key id, and it consults no
github_token_decryption_keyskeyring.OpenAgents.Accounts.TokenVault, by contrast, puts the key id inside the envelope, binds it into the AAD, and keeps up to sixteen prior keys so old envelopes still open.So the documented GitHub key rotation in
docs/github-auth-plan.md— add the old key to the keyring, activate the new one, rewrap withrotate_github_tokens!/0— rewraps every GitHub token and, in the same step, makes every outstandingmachine_pairings.token_ciphertextpermanently unopenable.open/1answers{:error, :token_unsealable}and the keyring cannot help, becausekey/0never reads it.OpenAgents.Voice.RecordingVaulthas the same shape with its own key: one key, no key id, no keyring, no rewrap path.test/openagents/voice/recordings_test.exs:319already pins that a wrong key fails closed; what is missing is that nothing says rotating it is unrecoverable.Blast radius, stated honestly
Bounded rather than absent.
@pairing_lifetime_secondsis 600, and both terminal transitions null the ciphertext column — a claimed pairing atlib/openagents/machines.ex:395and an expired one at:413. So a GitHub key rotation loses at most ten minutes of unclaimed pairings, and a person retries. The long-lived machine token is a bare sha256 digest with no key under it and is unaffected.That bound is why this is a documentation and keyring defect rather than an incident. It is still a silent loss during a procedure whose written steps say nothing about it.
Acceptance criteria
OpenAgents.Machines.TokenVaultreads its own configured key rather than the GitHub vault's, or the sharing is deliberate and bothdocs/github-auth-plan.mdanddocs/security/secrets-and-log-handling.mdstate that rotatingGITHUB_TOKEN_ENCRYPTION_KEYinvalidates outstanding pairings.docs/forge-exit-rehearsals.mdrehearsal 4 records which of the three vaults can be rotated without loss. Today the answer is one of three.Found by: #180's rehearsal 4,
docs/forge-exit-rehearsals.md.