AGENTS.md loaded about 33,000 tokens into every agent session, subagent and reviewer, mostly per-feature contracts, and the agent documentation around it had accumulated duplicates, dated logs and history of removed designs. This PR makes AGENTS.md a short set of rules that apply everywhere plus an index of area documents, and leaves AGENTS.md, dev-docs and the repository skills describing only the current state. Three commits, reviewable separately: 1. **Move (lossless).** Every per-area rule moved verbatim from AGENTS.md into its area document (existing ones, or six new ones: UI_CONVENTIONS, UI_SHELL, COMPOSER, TRANSCRIPT, SETTINGS, PROJECTS). A script split the old file into 208 units, required one destination for each and checked all 199 content units verbatim at their destination. AGENTS.md went from 134,000 to 16,000 characters. 2. **Trim.** A per-area ponytail review removed rules stated two or three times (keeping the more precise copy), dated validation logs, per-version qualification sections below the current minimum, abandoned designs and self-references; seven documents whose surviving rules live elsewhere were deleted. It also fixed statements the code contradicts (locale registration, the Mission play/recovery condition, plugin package updates, cleanup progress UI, timeline index persistence, automation restart/targets, stale pins and minimums). 3. **Current state only.** The new AGENTS.md rule: no history (removed features and their names, former behavior, provenance, dated logs) and no denials; correct a contradicted statement at its source. DEVELOPER_MODE.md is merged into BROWSER_AUTOMATION.md and every Developer Mode mention is gone (including the README and the browser help page in ten locales). Names that only made sense against older designs are renamed with every reference updated, MISSIONS_ENVIRONMENT_ADMISSION is folded into SESSION_ENVIRONMENT, and INDEX lists every document. The server npm README now states the real OpenCode minimum (2.1.2). AGENTS.md is now about 14,000 characters (roughly 3,500 tokens); AGENTS.md, dev-docs and the vendor README went from about 11,200 lines to under 4,700. Validation: every relative Markdown link and every cited dev-docs path resolves; no reference to a renamed or deleted document remains; `npm run test:help` passes for all ten locales. Review: three independent GPT-6.1 Sol (high) reviews by area found binding rules lost in the trim and current-state passes; 25 were restored as present-tense facts or prohibitions and four contradicting statements corrected, each checked against code. A Fable 5.1 gatekeeper review of the whole PR passed; its follow-up routes session recovery from the index, points to dev-docs/INDEX.md and documents the family identity read.
12 KiB
Provider accounts: direct selection and opt-in rotation
Visible accounts
Choosing a dropdown option activates that account service-wide; the account label is softer than the provider name. Add reuses the native Connect flow with the provider preselected; never build a second connection mechanism. Refreshes never overwrite explicit aliases; rename drafts survive refreshes, account switches and failed writes.
Login-derived names
Native ConnectionCredentialInfo lists only ID, label and method, so only the
native fallback label default is enriched: Codex OAuth through the server-only
usage/codex-credential.ts (native credential exports, same selected-token
identity rules as the quota adapter), OpenCode Console and Anthropic OAuth from a
sanitized native metadata email. The owned account settings route returns only
the bounded display email, never tokens, account IDs, claims or credential
values; claims are display hints, never authorization. Never rename a credential
automatically. Display metadata is fetched only while Settings is mounted and is
never persisted as a catalog.
Supported integrations
provider-accounts/adapters.ts is the selection-adapter registry, keyed by
native integration ID (models.dev providers use their provider ID; plugins
declare their own). Each adapter projects one explicit, possibly inactive,
native credential (secrets stay on the server stack, identity is an irreversible
digest), reads its quota without activating it and never refreshes or
exchanges tokens.
| Integration | Credentials | Quota reader | Unknown candidate |
|---|---|---|---|
openai (ChatGPT/Codex) |
Codex browser/device OAuth | usage/native-codex.ts strict selection parser |
rejected |
opencode-go |
Go API key | usage/opencode-go-quota.ts |
accepted unless marked |
opencode (Console, opencode-go provider only) |
OAuth device, service-account key |
same, with x-org-id |
accepted unless marked |
anthropic (Claude Pro/Max) |
OpenCode Anthropic OAuth only, never Claude Code's claude-code login, even as fallback |
Usage tab Claude reader | accepted unless marked |
github-copilot |
OAuth (github.com only) | Usage tab Copilot reader (premium requests) | rejected |
zai-coding-plan, zhipuai-coding-plan |
plan key | Usage tab token-limit reader (MCP tool allowance ignored) | rejected |
kimi-for-coding |
plan key | Usage tab Kimi reader | rejected |
minimax-coding-plan, minimax-cn-coding-plan |
plan key | Usage tab MiniMax reader | rejected |
OpenAI's chatgpt-token-sharing grant cannot read Codex usage (confirmed by
OpenAI):
Usage shows the localized openai-token-sharing-unsupported explanation for that
login; its quota stays unknown, so automatic selection rejects it.
Pay-as-you-go integrations (zai, minimax, OpenRouter, DeepInfra, Kilo,
ZenMux, DeepSeek, credit providers), the Console's Zen provider and readers that
need unrelated environment cookies/tokens (Cursor, Ollama, the Go web scrape)
stay manual, outside the registry.
Usage-backed adapters reuse the Usage tab readers with a one-entry projection
(usage/native-credentials.ts projectCredential) and a small selection view
that keeps only plan windows with a positive duration. Those readers keep their
display transport (15-second timeout, no response-size cap); Codex and Go use
the bounded credential-free transport in usage/bounded-fetch.ts (64 KiB,
redirects rejected, abortable). Readers keep the upstream HTTP status, and any
definite client error (4xx except 408/429, e.g. a Zen key answering 402 or a
missing endpoint) rejects a candidate, including for reactive adapters, so an
unrelated account (or Zen billing account) is never activated. Only reads not
performed, transport failures, timeouts, rate limits and 5xx stay unknown.
Inactive Anthropic/Console OAuth tokens near or past expiry, enterprise Copilot
tokens and self-hosted Console credentials (OAuth tokens and service-account
keys alike) stay unread (unknown); never refresh them or send them to the public
endpoint. The Usage tab sends native Go credentials only for the opencode-go
provider (ID or canonical); pay-as-you-go Zen sessions use the env-cookie reader.
The Go usage endpoint (GET https://opencode.ai/zen/go/v1/usage) is
unversioned and only partly verified: the parser accepts the current
usage.{rolling,weekly,monthly} and older *Usage shapes, and treats anything
else as unknown. Console OAuth usage with x-org-id is unverified upstream.
Auto-select account
- Opt-in per integration, persisted under CodeNomad's
providerAccounts.autoSelect.<integrationID>config owner. Settings asks the server for every integration with saved credentials; the control is visible only when the server reports two eligible accounts. Environment, external and pay-as-you-go connections are never automatic fallback candidates. The route refuses writes for unregistered integrations or unsupported catalogs. - The only switch condition is a full current account: a quota read at 100% of an applicable window, or a reactive mark (below) when its read is not "available". Choose the next usable account of the same integration in stable order. Exclude exhausted, rejected, marked or needs-auth accounts; a fresh "available" read clears a mark.
- Candidates must read "available". Reactive adapters (Go, Console, Claude), whose inactive credentials are often unreadable, also accept an unknown candidate that is not marked; their next send may therefore land on an account that turns out to be full, which marks it in turn.
- If no account is acceptable, retain the current selection and let the ordinary native send report its result. Never loop, create credentials, change models or silently fall back to another provider.
- At most 20 candidates are considered within a 15-second preflight, without temporarily activating candidates. Never use the quota display cache for an activation decision. For Codex, selection rejects every malformed declared window, missing/past resets, duplicate durations, out-of-range percentages and contradictory availability flags before any normalization can omit or clamp them.
- Persist only the opt-in policy; redact failures and never expose tokens through the account catalog, browser, logs or persisted display cache.
- Apply decisions before the next prompt/custom-command admission,
independent of an open Settings window. Never replay or intercept Shell,
/btw, tools, autonomous native steps or already queued requests. Serialize decisions across this backend's workspaces, freshly verify connection/ownership/deletion fences and current credential before activation. A local manual mutation or policy change racing a quota read wins, including away-and-back selection; stale responses cannot switch it. Native selection, credential fingerprints, session model/location and ownership are reread before activation. An ambiguous activation failure blocks that send with a redacted error, without retrying another candidate or the prompt. - In-flight OpenCode steps may retain a previously acquired account; never promise mid-generation failover without a proven upstream contract.
- Native activation lacks compare-and-set, so a TUI or another backend can change the selection after the last read; the serialization fence covers only this backend's decisions.
- Validation: server policy/ownership/admission regressions, real Chromium
controls and
scripts/test-provider-account-selection-native.mjsagainst an isolated CLI/database, with synthetic OAuth and injected quota responses. The native fixture covers concurrency, exhaustion, alias preservation and a fresh read of persisted YAML. Adapter, Go parser, reactive-mark and per-integration rotation regressions (provider-accounts/*.test.ts,usage/opencode-go-quota.test.ts) use fake daemons and stubbed fetch; never use real credentials or remote generation.
Reactive quota marks
provider-accounts/quota-observer.ts listens to native events already routed to
owned workspaces (instance.event), synchronously and without blocking the
relay. A session.step.failed/session.execution.failed whose structured
error type is provider.quota is deduplicated, queued per connection (at most
8 pending, dropped beyond) and attributed off the relay: session → provider →
registered adapter with an enabled policy → the integration's active
credential. The mark is skipped when a credential switch (native
credential.switched, our own activation or a manual change) happened since the
failing step started (or within 60 seconds when its start was not observed).
Because credential.switched can arrive late through another relay queue, the
switch watermark is captured when the failure is received; after the attribution
reads the backend waits a short settle bound (500 ms), re-reads the selection
and the active credential's identity, and skips the mark if any switch was
observed after receipt, the selection or identity changed, or the selection
differs from the credential last observed active when the step started.
Marks live in memory per connection (at most 256), keyed by integration and
credential ID. Go's metadata.limitName sets the lifetime: 5 hour marks last
5 hours; weekly, monthly and unknown limits are rechecked after 24 hours, since
OpenCode keeps no reset time. A manual account change clears all marks. Failure
details are never logged: native errors may contain request bodies.
Preview variants
tests/browser/fixtures/provider-accounts-preview.tsx renders the real Settings
components with synthetic credentials/quotas: /preview (manual), ?auto
(simulated rotation), ?single (no option), ?light; its toolbar's Simulate 100%
exhausts the current mock account. Never add a simulation action to production.
Agent conventions
- Provider accounts use a native dropdown like Web search defaults; never add an Accounts disclosure or selectable account rows. Provider headers keep the name and text Manage models button on one row; a faint separator distinguishes the account controls below. Account actions are always visible. The
provider-accountcontainer stacks the label above the dropdown/actions at narrow card widths. Native account ordering supplies the current account; rename/remove drafts stay keyed by credential. Environment connections are read-only. The browser never reads credential secrets. Native labels win; onlydefaultmay display a sanitized login email from the owned server adapter. Opt-in rotation is per integration (providerAccounts.autoSelect.<integrationID>) through the selection-adapter registry inprovider-accounts/adapters.ts, for subscription accounts with quota windows only: Codex, OpenCode Go (Go key and Console sign-in, Go provider only), Claude Pro/Max (OpenCode Anthropic OAuth, never the Claude Code login), GitHub Copilot and key-based coding plans; pay-as-you-go keys/credits stay manual. It runs before prompt/command admission, reading each explicit credential's fresh quota (reusing Usage readers) without activating candidates or refreshing tokens, with local manual-change fences and no mutation/prompt replay; the only switch condition is a full current account. Nativeprovider.quotafailures add bounded in-memory per-connection marks (quota-observer.ts); reactive adapters (Go, Console, Claude) accept unknown-but-unmarked candidates, others need "available". Account styling lives instyles/components/provider-accounts.css, backend policy inprovider-accounts/. Browser preview percentages are simulated. - Provider usage providers receive credentials explicitly: the route projects each integration's active native
credential.listentry throughusage/native-credentials.ts(never host auth.json; a missing endpoint is unavailability) and caches snapshots per provider credential identity. Codex uses its selected-connection adapter. Claude quotas follow the session's login (usage/claude-credential.ts):anthropic/claudeuse only OpenCode's Anthropic OAuth, opencode-claude'sclaude-codeonly the Claude Code login the route resolves on the daemon host (session environment, WSL path translation, Claude Code's token/Keychain/file order) and injects as theclaude-codeentry, so it joins the cache identity; never cross-fall back between them. Never exchange refresh tokens or embed OAuth client secrets; Google stays unsupported for that reason. Model-scoped limits addwindow:Modelrows beside the plan windows.