Skip to content

Commit 683949d

Browse files
committed
docs(auth): document credential recovery behavior
1 parent 7a00b2d commit 683949d

4 files changed

Lines changed: 48 additions & 5 deletions

File tree

‎docs/ARCHITECTURE.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ In TUI chat mode there is no completion gate — the session stays open across t
9191
- Both settings files and the project/global grant store (`.corbits/permissions.json`) are on the secret-guard denylist for path-keyed tools, so the agent cannot `read_file` its own credentials or persist standing auto-approvals. Shell commands that reference them still require explicit operator approval.
9292
- Credential-surface ownership: each auth store module enumerates its own files (`*_AUTH_FILENAME` / `MCP_AUTH_DIRNAME`), the data-only registry in `src/auth/credential-surface.ts` turns them into denylist patterns, `secret-guard-plugin.ts` owns matching (lexical plus realpath), and `@mention` resolution consumes the resolved check — never the registry directly. A new `*-auth.json` token store is denied only once its store module exports its filename and the registry lists it; the coverage test scans store sources for `*-auth.json` literals (registered dirnames get an includes-check instead) and fails the build until both exist.
9393

94+
### Inference credential recovery
95+
96+
Every inference source registers both live credential material and explicit provenance: OAuth credentials identify their provider and named profile, while API-key and keyless sources are classified separately. The harness freezes the complete source at call start, so classification, the one permitted recovery attempt, and any terminal diagnostic retain the exact provider/profile identity that actually failed even if the session changes provider concurrently.
97+
98+
On the first credential failure for an uncommitted call, the retry policy may refresh an OAuth credential from that same named profile and retry once immediately at the harness boundary. It does not substitute another profile or provider. Credential failures that survive that same-source attempt are terminal to the reactor's silent source-failover path. API-key, keyless, missing-provenance, and later credential failures do not enter OAuth refresh recovery.
99+
100+
OAuth stores serialize refresh writes and treat the value observed under the store lock as authoritative. A process that loses a compare-and-swap race adopts the concurrent winner, including its complete token set, instead of publishing or continuing with stale loser material. Credential material and refresh tokens are sanitized recursively before reactor events, run records, worker reports, terminal diagnostics, logs, or other diagnostic sinks can receive them.
101+
102+
The interactive TUI may offer an explicit alternate-provider/model selector only after the same-profile retry also ends in a terminal credential failure. That choice is generation-scoped and consumed once. It may continue the original operator message only when the failed attempt emitted no committing inference event; after any commitment it switches the live provider without replaying the message. `/connect` replaces or adds credentials and `/model` switches the live source explicitly. Exec and fleet workers use the same one-shot same-profile recovery but never open an auth or alternate-provider prompt; an exhausted failure terminates that attempt with a sanitized recovery diagnostic.
103+
94104
### TUI Runner (`src/tui/runner/`)
95105

96106
- Builds a chat-mode agent using the `ChatDirector`

‎docs/IMPLEMENTATION.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,22 @@ Providers and credentials are read exclusively from settings files: the global `
382382

383383
**Models-first connect.** There is no standalone `/login` command. `/model` opens on a flat **models-only** list (Recent, Favorites, then connected provider/model rows) built by `buildModelsFirstList` (`src/tui/model-picker.ts`); type-to-filter owns printable keys. Selecting a row runs `applyLiveModelSwitch` (`src/session/live-model-switch.ts`) so inference sources, permission-gate identity, grant persistence identity, and advertised tool schemas cut over together. **Alt+A** or `/connect` opens Connect via `addProviderSelectorChoices` (`src/tui/provider-setup.ts`), which lists every first-class kind including Custom — never bare `c` / Ctrl+A, and never in-list “connect →” rows. First-class API-key rows use a named-instance + auth-only form (instance name, key; catalog base URL is display-only); Custom keeps the full manual form. **Alt+F** toggles favorites; recent/favorite pairs live in global settings (`recentModels` / `favoriteModels`). **Alt+D** sets the default via `setDefaultModel` (global `defaultProvider` + that provider's `defaultModel`) plus `persistConnectedSelection` without switching the live session. First-class providers ship from `packages/first-class-providers` (corbits-agnostic defs) and `packages/opencode-go` (Go catalog, auth validate, multi-protocol endpoints, usage). OAuth providers open the existing browser login modal with a named account step; API-key providers share the same multi-instance naming and pre-seed models on save so selection works without restart. Both OAuth and API-key (including Custom) connects share `persistConnectedSelection` in `provider-setup-submit.ts` so project-local provider/model selection is written alongside global credentials. OpenCode Go forces `OPENCODE_GO_BASE_URL` when `opencodeGo` is set so subscription traffic is not billed as Zen PAYG.
384384

385+
### Inference authentication recovery
386+
387+
`src/config/source-credentials.ts` stores each source's live `CredentialMaterial` beside `SourceCredentialProvenance`: `{ kind: "oauth", provider, profile }`, `{ kind: "api-key" }`, or `{ kind: "keyless" }`. Codex and xAI source builders register the exact named OAuth profile plus any credential-derived identity header in the same material record. The vendored harness resolves that record at send time and shallow-freezes the complete call-start source; retries therefore keep the original source identity rather than consulting mutable session selection.
388+
389+
`createCorbitsRetryPolicy` normalizes the attempt error before counting credential failures. On ordinal 1 only, an OAuth source calls `refreshSourceCredentialByProvenance` for that exact provider/profile and retries immediately. A refresh error is replaced with a profile-specific `/connect` diagnostic. Non-OAuth credentials, unknown provenance, later credential failures, and a failed post-refresh retry abort. The vendored harness treats the resulting `credential_failure` as terminal instead of feeding it to automatic source failover.
390+
391+
Codex and xAI refresh sessions use the auth store's locked compare-and-swap update. `updateTokens` returns the authoritative profile observed under the lock; callers replace the complete mutable token object with that winner. If a refresh request loses to a concurrent process, the valid stored winner is adopted rather than overwritten, including removal of optional fields absent from the winner. The in-memory source credential cell also rotates only if the record used to start refresh is still current.
392+
393+
`sanitizeDiagnosticText` strips terminal controls, replaces exact configured credentials, and scrubs secret-shaped text; `sanitizeDiagnosticValue` applies the same rules recursively. Refresh failures sanitize the refresh token through the error's message, stack, detail, and cause chain before it leaves the auth boundary. TUI, exec, and worker event paths sanitize before run sinks, persistent error records, terminal/UI notices, logs, and parent-facing worker output.
394+
395+
TUI recovery state lives in `src/tui/runner/credential-recovery.ts`. It arms only for an operator-originated send that observed both the automatic credential retry and a terminal credential failure, and only when another configured provider/model can be assembled. A monotonically increasing generation makes stale accept/cancel actions inert, and acceptance consumes the generation once. The selector excludes the failed provider. It switches the live provider/model and sends a content-less, generation-correlated director continuation only when no committing inference event occurred; after text, tool, or other committed output it switches without replay. Provider/model option IDs are opaque internal identities used to preserve arbitrary provider and model strings; only their labels are UI contract.
396+
397+
Exec and fleet workers install the same retry policy and sanitization but no recovery selector or credential prompt, including TTY exec. Their exhausted credential failure is terminal and reports how to repair the profile for a later run. In the TUI, `/connect` reauthorizes or adds a profile and refreshes the live catalog, while `/model` switches to a connected provider/model; either is available after a terminal failure.
398+
399+
This release changes no settings, OAuth-store, or session persistence format and runs no migration. New sessions and resumed sessions resolve provider selection and credentials from the stores as they exist when the process starts or rebuilds; persisted transcripts do not pin old credential material. A Corbits process already running during upgrade still has the old in-memory harness and credential cells and must be restarted to acquire this recovery behavior.
400+
385401
**OpenCode Go multi-protocol.** Selectable ids come from live `GET /zen/go/v1/models` (process-cached; packaged seed when cold or the fetch fails). Byte and model-count caps fail closed as malformed — never a truncated prefix. Protocol routing stays on the local map; unknown live ids use Chat Completions. `buildGoSource` / `resolveGoEndpoint` pick the adapter and base URL per model (not a single provider-wide OpenAI route). When Go is the active provider, subscription usage is fetched for the status bar and omitted on auth/network failure.
386402

387403
### CLI Verbs and Flags

‎docs/TUI.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,12 @@ pair as the default (global `defaultProvider` + that provider's `defaultModel`
572572
active, bare `j`/`k` type into the filter rather than moving the highlight —
573573
use arrow keys (or the filtered list's navigation) to move.
574574

575+
After an operator-originated send fails authentication, Corbits first attempts one silent OAuth refresh and retry against the same named profile. If that retry also ends in a terminal credential failure, the shell waits until it is idle and may open a dedicated model/provider picker containing only assemblable models from providers other than the failed provider. It does not appear for API-key failures, the first failure before recovery is attempted, non-credential terminal errors, sends not originated by the operator, or when no alternate is available. Escape consumes the recovery offer without changing provider or replaying input.
576+
577+
Each recovery offer belongs to the failed send's generation. Enter accepts a displayed provider/model identity once; stale, malformed, duplicate, interrupted, cleared, or superseded acceptance is inert. The selected provider/model becomes live. If the failed attempt emitted no assistant text, tool activity, or other committing inference event, Corbits continues the preserved original operator message once on the selected provider. If anything committed, it **never replays the message**: selection only switches the live provider for the next operator action. The internal option IDs that preserve arbitrary provider/model names are opaque implementation details and are never shown as user-facing syntax.
578+
579+
The terminal failure remains visible and names `/connect` as the path to reauthorize a profile. `/connect` refreshes the provider catalog after successful authorization; `/model` remains available to switch explicitly to any connected provider/model. Neither command retroactively replays committed work.
580+
575581
`/mcp` uses the same longest-first overlay-hint footer as the model picker:
576582
**Alt+A** add (omitted while local MCP settings shadow global), **Alt+D**
577583
disable, **Alt+R** remove — never bare letters. A remove confirm drops those

‎docs/VENDORING.md‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,22 @@ verbatim (including the two upstream deletions,
108108
a ledger entry), and the second re-applies each `PATCHES.md` entry with an
109109
as-is / adapt / subsumed triage recorded in the ledgers. No entry was
110110
subsumed upstream. Upstream replaced inline provider `apiKey` plumbing
111-
with a `credentialId` + credential-cell model; no ledger entry touches
112-
auth so the vendored trees needed no migration, but first-party callers
113-
were migrated to the new model (each built source registers its secret
114-
in `src/config/source-credentials.ts`, handed to the vendored trees as
115-
their resolver).
111+
with a `credentialId` + credential-cell model. At sync time no existing
112+
ledger entry touched auth, so the pristine trees needed no auth-patch
113+
migration; first-party callers were migrated to the new model (each built
114+
source registers its secret in `src/config/source-credentials.ts`, handed
115+
to the vendored trees as their resolver).
116+
117+
CL-9347 subsequently adds the coupled `harness-ts-auth-recovery` entry in
118+
`vendor/intx-inference/PATCHES.md` and `runtime-ts-auth-recovery-context` in
119+
`vendor/intx-types/PATCHES.md`; it does not change the upstream pin or
120+
retrieval metadata. Together they atomically resolve bearer material and
121+
credential-derived identity headers, freeze the exact call-start source,
122+
expose per-call credential-failure history to the retry policy, preserve a
123+
classified refresh diagnostic on abort, and make an exhausted credential
124+
failure terminal to automatic source failover. Re-syncs must carry or replace
125+
both entries together so retry identity cannot drift from the credential
126+
material used by the attempt.
116127

117128
`vendor/intx-workflow-host/workflow-definition-loader.ts` is new in this
118129
sync: a second partial-tree path alongside `adapters/`, carrying

0 commit comments

Comments
 (0)