# car-secrets
Cross-platform secret store for [Common Agent Runtime](https://github.com/Parslee-ai/car).
## What it does
Unifies OS-native secure storage across the three platforms CAR targets:
| 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.