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-cli—car auth login parslee(loopback redirect flow).car-server— theauth.*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.