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 credential item:
put/publish updates its value and delete removes it. The internal
availability probe writes or updates one fixed non-secret sentinel to verify
that a non-interactive process can really write; it retains that sentinel and
never deletes it. Automatic recovery never rewrites a credential item,
including one created by another application.
Supervised-agent read broker
resolve_env_or_keychain keeps its established order: a non-empty process
environment variable always wins. When both CAR_AGENT_ID and
CAR_AGENT_TOKEN identify a supervised process and the environment is empty,
the resolver asks car-server's existing authenticated secret.get RPC instead
of opening the OS store in the child. car do --serve installs the daemon
connection it already owns before model credential discovery; other supervised
Rust executables lazily create one persistent broker connection with the same
agent credentials. Node and Python inference already runs through each binding
runtime's daemon connection, so its provider-key read is daemon-side as well.
The daemon derives the agent identity from session.auth, evaluates a synthetic
secret.get tool action through that session's policy engine, and appends a
redacted result to its event log. Audit rows contain the service, key name,
authenticated agent id, and outcome, never the value. Every supervised broker
outcome is authoritative. A policy or store refusal cannot bypass enforcement,
and a transport failure cannot make the child open
the OS credential store that the broker keeps daemon-owned. An unsupervised,
personally-run command still uses the local OS store.
The native car CLI also installs a broker reader when its daemon URL is
loopback and it can read the owner-only host-token file for that same
CAR_HOME. The ordinary token is available to loopback dashboard clients, so
it does not prove the caller shares the daemon user's filesystem identity; the
cross-host CAR_AUTH_TOKEN override is likewise never used for this role. This
routes car keys discovery, provider values, and Parslee auth state through the
daemon process that already has credential-store approval. Environment values
still win first. The daemon accepts an operator request only on an
authenticated host session with no supervised-agent identity and records service,
key, caller_role: "operator", and outcome — never the value. A daemon refusal
is final; only no response within the two-second broker bound uses the CLI's
direct-store fallback. Writes and deletes retain their existing direct or
coordinated paths.
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, deletes, and the availability probe. Before each
helper starts, a read-only Security.framework status check rejects a locked or
insufficiently accessible keychain without accessing an item or displaying a
prompt. Once started, the helper owns its lifetime: a process-owned worker waits
for its natural result and never kills it at a deadline, while callers stop
waiting after 15 seconds and a retry cannot spawn a second helper. The
availability probe then performs a
real write or update of its retained non-secret sentinel, because status bits
alone do not prove that a non-interactive process may write. 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.