car-secrets 0.54.0

Cross-platform secret store for Common Agent Runtime
Documentation

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.