feat(secrets): persist unlock across upgrade + sleep (--durable), cross-platform#1313
Conversation
The macOS secrets broker held an unlock only in RAM, so it evaporated on every daemon restart (agents-cli upgrade) and on sleep — forcing a Touch ID re-tap and breaking headless reads with "not unlocked in the secrets agent". Persist the resolved bundle+env of an unlocked bundle as a device-local, non-biometry keychain session item (the existing set-no-acl path — reads never prompt) that the broker REHYDRATES on start and that reads FALL BACK to when the broker RAM misses. Split default: survives upgrade/restart automatically; pass --durable (or set secrets.agent.durable) to survive sleep + reboot too, else the bundle re-locks on sleep as before. lock/rotate/delete clear it. - lib/secrets/session-store.ts (new): pure core (selectRehydratable, pruneOnSleep, pruneExpired) + a best-effort keychain adapter, all by KNOWN item name (never enumerated, so it round-trips under #316 hashing). - agent.ts: rehydrateStore() replaces new Map() in both broker paths; SLEEP prunes non-durable sessions; secretsAgentDurable() config reader. - bundles.ts: session read-fallback on the normal path (fixes both the interactive re-prompt and the headless throw); evict/delete also deleteSession. - index.ts: rekey keeps agents-cli.session.* no-ACL. - secrets.ts: --durable flag; off-darwin unlock/lock is a friendly no-op (Linux/ Windows already persist durably) so the command is uniform cross-platform. - types.ts: secrets.agent.durable config. Tests run on Linux CI via the KeychainBackend seam (in-memory), incl. the #316 hashed-name pass and the read-fallback resolving instead of throwing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…o-op Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code ReviewerVerdict: Ready to merge Build: ✅ Read CI status: Changes that work well
Issues that need attentionMinor — export function buildRemoteUnlockArgs(names: string[], opts: { all?: boolean; ttl?: string }): string[] {
return [
'unlock',
...(opts.all ? ['--all'] : names),
...(opts.ttl ? ['--ttl', opts.ttl] : []),
];
}But Things to verify manually
Reviewed by Code Reviewer — actually ran the build and tests on this branch. |
buildRemoteUnlockArgs forwarded --all/--ttl but not --durable, so `agents secrets unlock <b> --host <m> --durable` silently fell back to the remote's secrets.agent.durable default (off). Forward the flag so a remote unlock honors it. Completes the unreleased --durable feature (#1313); flagged by the PR review. Fails safe today (toward less-durable), so it's a gap not a regression. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
An
agents secrets unlockgrant lived only in the macOS broker's RAM, so it evaporated on every agents-cli upgrade (the postinstall bounces the daemon) and on sleep — forcing a Touch ID re-tap and breaking headless reads withnot unlocked in the secrets agent. (This bit us all through the release + fleet work in this session.)Persist the resolved bundle+env as a device-local, non-biometry keychain session item (the existing
set-no-aclpath — reads never prompt) that the broker rehydrates on start and that reads fall back to when the broker RAM misses.Split default (chosen posture): survives upgrade/daemon-restart by default; pass
--durable(or setsecrets.agent.durable: true) to survive sleep + reboot too — otherwise a bundle re-locks on sleep as before.lock/ rotate / delete clear it.Cross-platform: this is a macOS-only concern — Linux (libsecret) and Windows (Credential Manager) already resolve secrets durably from the OS store with no broker and no prompt, so
agents secrets unlockthere is now a friendly no-op and the command behaves the same on all three OSes.Design (validated; 4 review-caught corrections baked in)
session.*(breaks under#316hashing); a fixed-namesession.index+ per-bundle blobs, all read by known name.lock+ every evict site alsodeleteSession.rekeykeepsagents-cli.session.*no-ACL.Files
lib/secrets/session-store.ts(new),agent.ts(rehydrate + sleep-prune +secretsAgentDurable),bundles.ts(fallback + evict),index.ts(rekey),commands/secrets.ts(--durable+ off-darwin no-op),types.ts(secrets.agent.durable).Verified
tscclean; 114 secrets tests pass (newsession-store.test.tsincl. the#316-hashed pass, thebundles.tsread-fallback resolving instead of throwing, and the rekey no-ACL guard). Run on Linux CI via theKeychainBackendin-memory seam.exec/models, a 135s SSEservetest) — my diff touches none of those files.Ships in 1.20.71, then the fleet gets
agents devices update.Session transcript (confidential; not uploaded per public-repo convention):
zion:/Users/muqsit/.agents/.history/versions/claude/2.1.207/home/.claude/projects/-Users-muqsit-src-github-com-muqsitnawaz/f2314c23-cca7-436a-8535-3bb177497c15.jsonl