Expand description
srv’s secret storage: account API keys, OAuth accounts, MuxBus credentials, browser-pane logins.
Backs SecretRef::Keychain { service, account }, which names an entry in
this store, wherever it keeps secrets; the DB holds just the pointer plus
non-secret metadata + a masked tail. Reads return a Zeroizing<String> so
the plaintext is wiped from memory on drop.
Two backends, chosen once at startup:
- The OS keychain (macOS Keychain / Windows Credential Manager / Linux
Secret Service via the
keyringcrate). The default, and what the desktop app always uses. - A file store (
use_file_store): one0600file per entry in a0700directory, for machines with no keychain — a container, a server, CI. Headless srv uses it unless told otherwise (docs/specs/SPEC_SRV_HEADLESS_MODE_2026_09_26.md §3.6). Secrets are protected by file permissions (and the volume’s own encryption), not encrypted by srv: a key kept beside them would add nothing.
There is no silent downgrade from one to the other: failures surface as errors. See docs/specs/archive/SPEC_TRUST_CENTER_2026_06_15.md §7 and §12.2.
The rest of this comment is about the keychain backend.
Reads are bounded by TIMEOUT; writes are not — see below. Every
operation here can require interactive OS consent (“App wants to access
your confidential information…”) the first time a given code
signature touches a given entry, and that consent call has no
cancellation mechanism: an unanswered prompt (headless process, dialog
on another Space, no attached display session) blocks the underlying
platform call indefinitely. Confirmed live — see
docs/retro/retro-macos-muxbus-keychain-prompt-storm-2026-08-19.md §5.
get/get_optional run the real platform call on a detached thread and
give up waiting after TIMEOUT, so a stuck prompt bounds how long the
CALLER waits (and any lock it’s holding) instead of hanging it forever.
This is safe specifically because a read has no side effect: if the
timeout fires, the detached thread’s eventual (possibly much later)
result is just discarded — nothing acts on a stale read. At most one
read per account is in flight (SharedReads): a retry while one is
still waiting on a dialog joins it rather than queueing another dialog
behind it.
put/delete deliberately do NOT use this timeout (reagent + Codex,
PR #2679 round 2): the same “detached thread keeps running after the
caller gives up” behavior is unsafe for a MUTATION. A caller that treats
a timed-out write as failure and proceeds — e.g. skipping a dependent DB
write, or a rollback in muxbus.rs writing a different value to the
same entry — can have the orphaned original write land afterward and
silently clobber whatever the caller did next, with no ordering
guarantee between them. There is no cancellation-equivalent compensation
available (the platform call truly cannot be cancelled), so until one
exists, an unbounded wait — the pre-existing, safe-if-slow behavior — is
the correct tradeoff for anything that mutates state. A stuck consent
prompt on a write still blocks its caller indefinitely; only reads are
protected today.
Modules§
- file 🔒
- The file store. Each entry is one file named by the SHA-256 of its account key, so no key text (ids, hostnames) reaches a file name and no key can escape the directory. No timeouts: file operations don’t wait on a user.
Structs§
- Pending
Read 🔒 - Shared
Reads 🔒 - Keychain reads, at most one in flight per account.
Enums§
- RunOutcome 🔒
- Why
run_with_timeoutgave up waiting — reagent P2: collapsing these into one outcome made a closure PANIC (sender dropped without sending,mpsc::RecvTimeoutError::Disconnected) misreport as “timed out after 15s” — actively misleading, since the operation actually failed immediately, not after waiting the full deadline.
Constants§
- SERVICE
- Keychain service string — constant across all AgentMux secrets.
- TIMEOUT 🔒
- How long a caller waits for a keychain operation before giving up. Long
enough for a real user to notice and answer a genuine first-time consent
prompt if they’re looking at their screen; short enough that an
unanswered/unanswerable one doesn’t wedge whatever the caller is holding
(e.g.
muxbus_save_lock) indefinitely. See this module’s doc comment.
Statics§
- FILE_
STORE 🔒 - Set when
use_file_storechose the file store; unset means the keychain.
Functions§
- account_
key - Build the keychain account string for an identity-account id.
- backend_
name - Which backend this process uses, for logs:
"keychain"or"file". - delete
- Delete the secret for
account_id. A missing entry is treated as success (idempotent delete). - entry 🔒
- get
- Read the secret for
account_id. Returned wrapped inZeroizingso it is wiped on drop. Resolved at agent spawn time when injecting env vars. - get_
optional - Read the secret for
account_id, distinguishing “no entry stored yet” (Ok(None)) from a real storage failure (Err) — a locked keychain, no Secret Service daemon running, permission denied, etc.getcollapses both into the sameErrvariant, which is correct for its own callers (a spawn-time credential resolve should fail either way), but is the wrong shape for a caller that needs to tell “genuinely never logged in” apart from “storage is transiently broken” — treating a transient failure as “no credential” can silently present as a full logout. Use this variant when that distinction matters. - get_
optional_ 🔒now - lock 🔒
- put
- Store (or overwrite) the secret for
account_idin the OS keychain. - run_
outcome_ 🔒message - shared_
reads 🔒 - use_
file_ store - Keep every secret in files under
dirinstead of the OS keychain, for the rest of this process. Called once at startup, before anything reads or writes a secret. Createsdir(mode0700) if needed.
Type Aliases§
- Read
Result 🔒 - What a keychain read returns:
Ok(None)when there’s no entry.