car-auth 0.52.1

Shared Parslee OAuth2 PKCE + token/keychain logic for the CAR CLI and daemon
Documentation

car-auth

Shared Parslee OAuth2 PKCE + token/keychain core for Common Agent Runtime.

What it does

One implementation of Parslee sign-in, used by two surfaces:

  • car-clicar auth login parslee (loopback redirect flow).
  • car-server — the auth.* JSON-RPC surface that CAR Host.app's signup/sign-in GUI drives.
Function Purpose
pkce_verifier / pkce_challenge / new_state PKCE S256 + CSRF state
authorize_url Build the /connect/authorize URL
exchange_code Redeem code + verifier at /connect/token
commit_login / logout Atomically publish a complete login or signed-out tombstone
access_token / access_token_refreshing / force_refresh Read or refresh the active credential through the shared coordinator
api_base Resolve API base (override → process env → authoritative V2 record → default)
credential_authority_hint Read non-secret passive state without touching the credential store
fetch_status / fetch_status_with_access /connect/session for the stored token, or for an explicit uncommitted token
reserve_login_attempt / claim_login_attempt Durable browser reservation and one-worker redemption fence
local_auth_snapshot / auth_completion_status Non-networked baseline and one-snapshot typed attempt result
list_accounts / switch_account / remove_account / switch_org Multi-login and organization mutations through the same coordinator

Keychain contract

Parslee auth has one authoritative record in the default "car" keychain service: PARSLEE_AUTH_STATE_V2. It contains the active credentials, held accounts, revision, identity generation, and one bounded latest-attempt lifecycle (awaiting_callback, worker-owned redeeming, terminal failure, or completion). One car-secrets::SecretStore::publish call is the commit point, so readers do not observe token/account/generation/lifecycle fields from different mutations.

For isolated release proof on macOS only, CAR_KEYCHAIN_PATH selects an existing real Keychain without changing the user's default Keychain or search list. It is accepted only together with CAR_KEYCHAIN_PROOF_ROOT; both paths must be absolute and owned by the current user, the root must be an owner-private non-symlink directory, and the Keychain must be an owner-private regular non-symlink file canonically contained by that root. The validated path is appended to every /usr/bin/security read, availability/status, write, and delete operation. With no selector, normal Keychain behavior is unchanged.

car-secrets also exposes process-lifetime operation counters for get, status, availability, write, and delete attempts. They are aggregate diagnostics only: no service, key, credential identity, filesystem path, or value is retained.

Passive credential authority

After the authoritative V2 record commits, car-auth atomically writes $CAR_HOME/parslee-auth-authority.json at Unix mode 0600. It contains only state (unknown, signed_out, or configured), credential generation, and an update timestamp. It never contains a token, account or organization identity, email, API base, credential reference, or Keychain path. Missing, malformed, oversized, symlinked, or field-expanded files load as unknown.

The hint is presentation and routing metadata, not authentication evidence. Every authenticated operation must still resolve the authoritative credential and fail closed. A hint publication failure is logged and passive state is degraded to unknown; it never rolls back an already-committed credential mutation.

The older fixed slots (PARSLEE_ACCESS_TOKEN, PARSLEE_REFRESH_TOKEN, PARSLEE_ACCESS_TOKEN_EXPIRES_AT, PARSLEE_API_BASE), account registry, per-account stashes, generation, completion, and active marker are import-only. The first coordinated read migrates an attributable legacy session and then best-effort removes those slots. If legacy tokens carry an empty or __unknown__ active marker, disagree with registry.active, or name an account absent from the registry, CAR discards the fixed slot rather than relabelling it onto a guessed account: the migration keeps whatever logins the registry can account for, publishes a signed-out record, and the user signs in again. It degrades rather than failing because the import runs ahead of every read — including the one that starts a fresh sign-in — so a fatal import would make the sign-in that clears the orphan unreachable. Malformed legacy data is still a hard error. Explicit logout remains available and publishes a signed-out tombstone before cleanup.

All auth slots, including PARSLEE_AUTH_STATE_V2, are daemon-private: generic CLI/FFI/RPC secret wrappers cannot read, replace, or delete them. The explicit PARSLEE_ACCESS_TOKEN environment override remains available for test/CI injection, but it cannot masquerade as a durable completion proof.

Multiple logins, and switching org

The V2 record contains every held login and exactly one optional active credential. Every request-time Parslee consumer reads that same active value: the CLI, daemon status and refresh paths, car-parslee, and inference do not maintain separate writers.

Surface Purpose
auth.start with prompt=select_account Add another login (car auth login --add)
auth.accounts / auth.switch_account / auth.remove_account List, activate, drop a held login (car auth accounts / switch-account)
auth.switch_org Silent active-org switch via the refresh grant's organization_id override — no browser (car auth orgs / switch-org)

Login completion, account switch/removal, logout, and org-switch refresh each publish one replacement record. A refresh performs network I/O before taking the credential lock, then compares the captured revision and account ID; a stale refresh cannot overwrite a newer login, logout, account switch, or org switch.

Trust boundary

The crate is deliberately headless: no browser-open, no loopback listener. Those are surface-specific — the CLI runs a localhost listener; the daemon hands the authorize URL to the in-process GUI, which carries the PKCE verifier + state between auth.start and auth.complete (same trust boundary as the keychain the token lands in). Mutations are serialized by a process-global Tokio mutex and a per-user cross-process file lock before entering the keychain; blocking keychain work runs off the async executor, the in-process coordinator queue has an enforced 30-second bound, and no network call occurs while either lock is held. Once an operation acquires the coordinator guard, its daemon-owned caller retains the guard until the blocking storage task joins; a response timeout or socket close cannot expose an overlapping mutation while keychain work continues. A successful browser completion records its random attempt ID in the same publication as the credential generation, allowing a host to reconcile a lost reply without replaying the one-time authorization code. auth.start first publishes an ownerless reservation valid beyond the 300-second browser window; any daemon instance may atomically claim it once. The daemon returns state:"accepted" only after that claim is durable and the redemption task is spawned. The claim persists a daemon/worker owner and a derived 210-second completion expiry before /connect/token. The lease is one numeric composition: 15 seconds for claim publication + 90 seconds for aggregate network work + 30 seconds for the coordinator queue + 30 seconds for the process lock + 15 seconds for the strict-expiry state read + 30 seconds of positive scheduling margin. It remains inside the host's 480-second reconciliation horizon, including one explicitly safe pre-redemption retry through both contention layers. AuthOperationError keeps coordinator/process-lock deadlines typed separately from terminal state, store, and worker failures, so the server can return retryable -32004 without treating corruption as transient. Duplicates fail before exchange, worker errors close terminally, a restart while awaiting the callback remains resumable, and a restart while redeeming fails instead of replaying a possibly-consumed code. auth_completion_status reads the already-published V2 completion, generation, and lifecycle from one lock acquisition and returns pending, complete, failed, or stale; it never imports legacy slots, and an expired or old-daemon worker is terminally closed by that same operation. Any later start, switch, or logout supersedes the older attempt. Onboarding/consent for brand-new accounts is handled entirely by Parslee's existing hosted web flow during the browser hand-off — this crate (and CAR generally) never touches consent, by design.