car-auth 0.52.0

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](https://github.com/Parslee-ai/car).

## What it does

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

- **`car-cli`**`car 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.