Skip to main content

Module secret_store

Module secret_store 

Source
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 keyring crate). The default, and what the desktop app always uses.
  • A file store (use_file_store): one 0600 file per entry in a 0700 directory, 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§

PendingRead 🔒
SharedReads 🔒
Keychain reads, at most one in flight per account.

Enums§

RunOutcome 🔒
Why run_with_timeout gave 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_store chose 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 in Zeroizing so 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. get collapses both into the same Err variant, 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_id in the OS keychain.
run_outcome_message 🔒
shared_reads 🔒
use_file_store
Keep every secret in files under dir instead of the OS keychain, for the rest of this process. Called once at startup, before anything reads or writes a secret. Creates dir (mode 0700) if needed.

Type Aliases§

ReadResult 🔒
What a keychain read returns: Ok(None) when there’s no entry.