You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -35,9 +35,9 @@ JSON is the default; exit codes: 0 success / 1 any-provider-failed / 2 usage err
35
35
-[internal/providers/usagecache/](internal/providers/usagecache/) — provider-neutral 90 s file-backed usage cache. `New(provider, nowFn, warnFn)` validates the provider char-set and writes `$CACHE/aistat/usage/<provider>-v1.json` with `<provider>.cache.lock`. Warn strings include the provider name.
36
36
-[internal/providers/multiaccount/](internal/providers/multiaccount/) — provider-neutral helpers consumed by both Claude and Codex multi-account `Fetch`: `SortAccountResults`, `RecordFetchOutcome`, `RecomputeResetAfter`, `Budget(base, perAccount, count)`.
-[internal/providers/codex/](internal/providers/codex/) — Codex's multi-account machinery, structurally a mirror of Claude's: [codex.go](internal/providers/codex/codex.go) (Fetch + FetchForSwitch + cache wiring + `rotateRawBlob`), [refresh.go](internal/providers/codex/refresh.go) (OAuth refresh against `https://auth.openai.com/oauth/token` with client_id confirmed via Codex binary inspection), [reconcile.go](internal/providers/codex/reconcile.go) (pure decision tree: byte-match → JWT `sub` lookup → live-unstored), [account.go](internal/providers/codex/account.go) (Codex-shaped `Stored*` helpers). Identity is the `sub` claim of the OIDC `id_token` — no network endpoint, JWT-payload decode only. Slot-vs-duration window labelling is by `limit_window_seconds`, NOT by slot position, so free-account weekly windows that land in the primary slot are not mislabelled as `five_hour`.
38
+
- [internal/providers/codex/](internal/providers/codex/) — Codex's multi-account machinery, structurally a mirror of Claude's: [codex.go](internal/providers/codex/codex.go) (Fetch + FetchForSwitch + cache wiring + `rotateRawBlob`), [refresh.go](internal/providers/codex/refresh.go) (OAuth refresh against `https://auth.openai.com/oauth/token` with client_id confirmed via Codex binary inspection), [reconcile.go](internal/providers/codex/reconcile.go) (pure decision tree: byte-match → JWT `sub` lookup → live-unstored), [account.go](internal/providers/codex/account.go) (Codex-shaped `Stored*` helpers). `StoredExpiresAt` decodes expiry from the **access_token** JWT's `exp` (via `cred.ParseJWTExp`) — the long-lived API credential — NOT the short-lived OIDC `id_token`, which is identity-only (gating on the id_token's ~1 h `exp` made the refresh gate fire on every run). Identity is the `sub` claim of the OIDC `id_token` — no network endpoint, JWT-payload decode only. Slot-vs-duration window labelling is by `limit_window_seconds`, NOT by slot position, so free-account weekly windows that land in the primary slot are not mislabelled as `five_hour`.
39
39
-[internal/render/](internal/render/) — `json` and `text` renderers. The JSON shape is the public contract; the text renderer is a thin presentation layer over the same model. Provider-agnostic: when `len(result.Accounts) > 0` for ANY provider the renderer emits the nested per-account view, otherwise the legacy flat form (still in use for Copilot, and as a Claude/Codex fallback).
40
-
-[internal/cred/](internal/cred/) — credential read/write and JWT decoding. `Credential.Raw []byte` preserves the verbatim provider blob (every byte the upstream CLI wrote) so a switch re-publishes byte-for-byte. `ReadClaudeCredential` / `WriteClaudeLiveBlob` (macOS Keychain item `Claude Code-credentials` via a `runSecurity` test seam; Linux `~/.claude/.credentials.json`). `ReadCodexCredential` / `WriteCodexLiveBlob` (file-only on both OSes: `~/.codex/auth.json`, mode 0600, atomic rename + fsync). `ParseCodexIDToken` (exported here to avoid a `cred` ↔ `providers/codex` import cycle) decodes the OIDC `id_token` payload for `sub` / `email` / `exp` without signature verification.
40
+
-[internal/cred/](internal/cred/) — credential read/write and JWT decoding. `Credential.Raw []byte` preserves the verbatim provider blob (every byte the upstream CLI wrote) so a switch re-publishes byte-for-byte. `ReadClaudeCredential` / `WriteClaudeLiveBlob` (macOS Keychain item `Claude Code-credentials` via a `runSecurity` test seam; Linux `~/.claude/.credentials.json`). `ReadCodexCredential` / `WriteCodexLiveBlob` (file-only on both OSes: `~/.codex/auth.json`, mode 0600, atomic rename + fsync). `ParseCodexIDToken` (exported here to avoid a `cred` ↔ `providers/codex` import cycle) decodes the OIDC `id_token` payload for `sub` / `email` / `exp` without signature verification; `ParseJWTExp` similarly decodes just the `exp` claim of any JWT (no signature check) and is used by `codex.StoredExpiresAt` to read the access-token expiry.
41
41
-[internal/httpx/](internal/httpx/) — shared HTTP transport: `Doer.GetJSON` (Authorization-reserved) + `Doer.PostForm` (no Authorization by default — used by both refresh clients) sharing an unexported `setCommonHeaders` / `do` split. `do` runs a bounded retry loop (max 3 attempts) that honors `Retry-After` (capped at 10 s) on transient classifications, falling back to exponential backoff with ±20 % jitter. `Classifier` takes `*http.Response` so callers can inspect headers.
42
42
-[internal/orchestrate/](internal/orchestrate/) — parallel fan-out across providers; one failing provider does not block the others. Preserves per-account rows on provider-level error (D8 contract).
43
43
-[internal/testutil/](internal/testutil/) — shared test helpers.
@@ -76,7 +76,7 @@ These are the principles every change should respect. When in doubt, optimize fo
76
76
77
77
These are upstream OAuth-provider behaviors aistat cannot work around without re-authentication. Both fail closed with actionable errors. **`codex login --device-auth` is the right flow on remote / headless machines** (no browser required on the host), but per upstream [code](https://github.com/openai/codex/blob/main/codex-rs/login/src/device_code_auth.rs) and [docs](https://developers.openai.com/codex/auth) device-auth uses the same persistence path and the same single-cached-login model as the browser flow — the revocation semantics below apply equally.
78
78
79
-
-**Refresh-token rotation race.** OpenAI's `/oauth/token` endpoint single-uses each refresh_token; the Codex CLI rotates it on every refresh and writes the new value to `~/.codex/auth.json`. If the Codex CLI runs between aistat reading the file and aistat sending its refresh request, aistat's in-memory copy is stale and the server returns a 401 whose body reads `Your refresh token has already been used to generate a new access token.` aistat tightens this to `stale refresh token (codex CLI rotated it); retry or run codex login to recover` (matched on `already been used` in `refreshErrorMessage`). Recovery: `codex login` (or just wait for the next aistat run — the cache will catch up next pass).
79
+
-**Refresh-token rotation race.** OpenAI's `/oauth/token` endpoint single-uses each refresh_token; the Codex CLI rotates it on every refresh and writes the new value to `~/.codex/auth.json`. If the Codex CLI runs between aistat reading the file and aistat sending its refresh request, aistat's in-memory copy is stale and the server returns a 401 whose body reads `Your refresh token has already been used to generate a new access token.` aistat tightens this to `stale refresh token (codex CLI rotated it); retry or run codex login to recover` (matched on `already been used` in `refreshErrorMessage`). Recovery: `codex login` (or just wait for the next aistat run — the cache will catch up next pass). Note: since `StoredExpiresAt` now gates on the long-lived access_token's `exp`, routine reporting no longer triggers a refresh on every run — only when the access token is genuinely near expiry — so this race is hit far less often.
80
80
-**Switch-side token revocation.** When a new account logs in on the same OAuth client via the browser flow (`codex logout && codex login`), OpenAI's server invalidates the previous account's tokens. aistat's `switch` re-publishes the stored blob byte-for-byte, but server-revoked tokens stay revoked — the next usage call returns a 401 whose body carries either `token_revoked` or `token_invalidated` (OpenAI uses both interchangeably for this condition). aistat tightens both to `tokens revoked by upstream (likely a codex login for another account); run codex login to recover` (matched by `isRevokedTokenErr`). Recovery: `codex login` for the now-active account.
0 commit comments