car-secrets
Cross-platform secret store for Common Agent Runtime.
What it does
Unifies OS-native secure storage across the three platforms CAR targets:
| Platform | Backend |
|---|---|
| macOS | /usr/bin/security over Keychain Services |
| Windows | Credential Manager (DPAPI) |
| Linux | Secret Service — GNOME Keyring / KWallet / KeePassXC / anything speaking org.freedesktop.secrets |
The API is intentionally small: put, publish, get, delete, status,
list. Callers choose a service (namespace) and an account (key); values are
UTF-8 strings. JSON helpers are provided for structured values.
publish is reserved for an already-composed authoritative value that must
replace its predecessor without a visible delete gap. The debug file backend
uses an owner-private staging file plus rename; macOS updates the Keychain item
in place. Windows uses two deterministic Credential Manager generations:
- each chunk carries a publication revision and the root sentinel is written only after every inactive-generation chunk exists;
- the previous generation is retained so a reader that already captured its root can finish, while a revision mismatch or missing retired chunk causes a bounded retry against the current root;
- a non-secret high-water manifest is written before staging, so a crash at any phase leaves a fixed, recoverable set of entries instead of a fresh unreachable nonce namespace; and
- cleanup after the root commit is best-effort and retryable. A cleanup error does not report the already-committed publication as failed, and metadata errors before commit leave the only good generation untouched.
publish calls for the same ref must be serialized by the caller (Parslee auth
uses its cross-process coordinator lock); readers may run concurrently with a
publisher.
macOS approval persistence
Every macOS operation uses the Apple-signed /usr/bin/security helper. The
in-process keyring/Security.framework backend is not compiled into
car-secrets on macOS, so CAR has one stable Keychain identity across app,
daemon, CLI, agent, release, and debug builds.
put and publish use add-generic-password -U -A. A new item grants the
stable helper access; an existing item is updated in place so its ACL, partition
list, and every approval the user already granted remain intact. A read that
shows a password or access dialog is read-only even after the user approves it:
CAR does not automatically delete, recreate, or rewrite that item, and does not
run set-generic-password-partition-list from an unattended process. The grant
therefore remains attached instead of being discarded by CAR's next repair.
Only an explicit caller operation mutates an existing item: put/publish
updates its value and delete removes it. The internal availability probe may
delete only the unique non-secret sentinel it just wrote. Automatic recovery
never rewrites an item, including one created by another application.
Availability
On headless Linux without a Secret Service daemon, put / get / delete return SecretError::Unavailable. There is no silent plaintext fallback. Callers should probe is_available() before relying on the store, or handle Unavailable with their own fallback.
Security boundary
Secrets never enter CAR memory, state, or prompt context unless a caller explicitly reads them and passes them into one of those systems. The store treats a missing backend as a hard error so misconfigured environments are loud, not silently insecure.
Where it fits
Surfaced via the WebSocket secret.* methods. Used by car-integrations for OAuth token storage and by any user app that needs to persist credentials gathered through its own auth flow.
On macOS, car-secrets uses the Apple-signed /usr/bin/security helper for
reads, writes, status checks, and deletes. Every child has a bounded runtime and
is killed and reaped at the deadline, so a Keychain authorization prompt cannot
wedge an async CAR service indefinitely. Reads parse the helper's
byte-preserving -g output so valid UTF-8 values, including trailing newlines,
round-trip without repeated Keychain prompts when rebuilt CAR helper binaries
get new CDHashes.