0010 — Providers
Status: accepted · 2026-09-29
- Status: accepted
- Date: 2026-09-29
Context
ADR 0004 reserved the engine ids and built the Engine
interface, but only Claude Code was ever wired up, and it showed: the Settings
section was called Claude Code, the engine was chosen by an environment
variable (CONCH_ENGINE) most people never set, and the first-run flow said "let's connect
to Claude" as if there were nothing else.
What people actually have on their machines (research, Sept 2026) is more varied, and each of these is a different kind of connection:
- an agent already installed and signed in — Claude Code, Codex;
- a subscription they already pay for (Claude, ChatGPT);
- a key for a gateway with every model behind it (OpenRouter) or for one lab (the Anthropic API);
- keys they keep in a password manager, not in dotfiles.
The recurring complaints are the same shape as the integrations ones (ADR 0009): setup is JSON editing, keys end up in plaintext config, switching means editing a file and restarting, and nothing tells you which provider a reply came from.
Decision
Providers are the engines you can connect, presented as one screen with live state, one button each, and the one in use wearing a ring.
The choice is yours, and it's remembered
GET /api/providersreturns every provider: what it is in plain words, its liveEngineStatus, whether a key is saved and what connecting would take.- The active provider is
preferences.engineinsettings.json— a preference, not an environment variable.POST /api/providers/:id/usechanges it, and every turn after that goes through the new engine (Services.engine()reads it, so nothing needs restarting). CONCH_ENGINEstill wins, but its meaning is now a pin: the UI says "Conch was started with CONCH_ENGINE=…, so the provider is fixed" instead of offering a switch that wouldn't take. That keepspnpm dev:mockand the E2E runs honest.- Product copy never says "Claude Code" where it means "your provider". Settings → Providers; first run asks "Choose what powers me".
Three ways to connect, one dialog
- A program on this computer (
connect: 'program'): install hints with copy buttons while Conch watches for the binary to appear, then the CLI's own sign-in driven throughEngine.login(). - A key (
connect: 'key'): a field that also accepts a 1Password reference — see below. - One click, where the provider will mint a key for you: OpenRouter's PKCE
flow (
providers/oauth.ts). No key is ever copied by hand.
Keys: one place, two homes
ProviderKeys (providers/keys.ts) is the only code that writes, reads or
describes a provider's key. SecretVault (secrets/vault.ts) decides where it
lives:
- Conch —
~/.conch/secrets.json, mode 0600, like every other file there. - 1Password — Conch stores only the reference (
op://Vault/Item/field) and asks theopcommand for the value when a turn needs it.
Rules that hold everywhere:
- The browser learns that a key exists, where it lives and its last four
characters. Never the value. (
SavedSecretcarries no secret.) - A key is checked before it's kept: the shape is matched, a 1Password reference is resolved once, and the engine is asked to re-detect. If any of that fails, the previous key is restored and the dialog says why. "Saved" means it works.
- Reading a secret can involve a person (a fingerprint), so it only happens
when a turn needs it. Drawing Settings, or checking whether a provider is
connected, uses
peek— memory only. Nobody gets a biometric prompt for opening a settings panel. - Resolved values are cached in memory for five minutes, which also keeps us well inside 1Password's read quota (1,000/hour on Individual and Families).
- The older top-level
anthropicApiKeyis still read, and retired the next time a key is saved.
Why the op CLI and not @1password/sdk. The SDK is a new runtime
dependency, still on 0.x with breaking minors, and it only supports service
accounts or explicit desktop auth. The CLI is already how developers use
1Password, works with whatever auth they have (desktop unlock, a service
account token we pass through, or Connect), and op read takes only the
reference as an argument — the value comes back on stdout, so nothing
sensitive is visible in ps. (1Password's own docs warn about argv for
writes, not reads.) Two caveats we surface rather than hide: a service account
can never read the built-in Private/Employee vaults, and a locked app revokes
prior authorisation, so a turn may need an unlock.
OpenRouter's one-click key, done by the book
- PKCE S256, no client secret — a self-hosted app can't keep one, which is exactly the case OpenRouter's flow is designed for. Localhost callbacks on any port are supported.
- OpenRouter's flow has no
stateparameter, so the flow id lives in the callback path (GET /oauth/provider/:flowId) and does the same job: 256 random bits, in memory only, single use, ten minutes, constant-time lookup. Without it a code is worthless, and the code is itself single-use and bound to our verifier. - The callback sits outside
/apibecause it's a cross-site top-level navigation (same reasoning as the integrations callback), spends the code at once and 303-redirects, so the code leaves the address bar. - The exchange goes to a fixed https address we chose, so there's no SSRF
surface; the response is parsed with Zod and only a
keyis accepted.
Honesty about what each provider can do
Features ask the engine, never the name (AGENTS.md rule 9), and the UI says what the answer means:
- Claude Code —
integrations.mode: 'native', full permission modes, the provider account's own connectors. - Codex —
native, butcodex execforces approvals off and has no channel to ask a supervisor, so Conch cannot prompt per action. It declares only the modes Codex can honour, maps them to--sandbox, and the card and dialog say "Codex works inside its own sandbox, so Conch can't ask you before each step." Marked early support rather than presented as finished. - OpenRouter / the Anthropic API —
bridgeengines: Conch holds the MCP connections and runs the permission rules itself, so integrations behave the same. They have no filesystem or shell tools at all, and declarepermissionModes: ['default']instead of pretending.
Engine.hostTools is the other declared capability this added: an engine that
can't run Conch's own tools (Codex, whose exec takes no tool definitions) is
never told about them, so the prompt stops promising a memory it can't save. The
system prompt no longer claims to be "Claude, working through Claude Code"
either — what a provider can actually do is the provider's own sentence.
1Password as an integration, too
The catalog gains 1Password: the Environments MCP server (1password-mcp,
shipped with the desktop app, stdio only; on Windows too, as an app alias. See
ADR 0016, which also installs the app when it's missing). It manages
Environments and variable names; by explicit 1Password design it never returns
a secret value to a client, and the entry says so. Enabling it takes two
toggles in the 1Password app, which the dialog lists as steps.
Consequences
- New data:
secrets.jsongainsproviders(aStoredSecretper engine id), andapi-sessions/<id>.jsonholds the transcript a plain model API needs — it keeps no session of its own, so Conch sends the conversation each time. Both are 0600 like everything in~/.conch. - New routes:
GET /api/providers,POST /api/providers/:id/use|check|login|signin,PUT|DELETE /api/providers/:id/key, andGET /oauth/provider/:flowId.GET /api/engineandPUT /api/engine/api-keystill work and now act on the active provider. CONCH_ENGINEchanges meaning from "the engine" to "the engine, fixed".- Settings → Claude Code is gone;
EngineConnectwent with it, replaced byProviderSetupand oneConnectProviderDialogthat covers every path. - Conch now makes outbound requests to whichever providers you connect, and runs
opwhen a key lives in 1Password. - Adding a provider is still one folder under
engines/plus a registry entry — now with one entry inproviders/catalog.tsfor the words.
Sources
- OpenRouter OAuth PKCE,
GET /api/v1/key,/api/v1/models/user, streaming and error semantics — openrouter.ai/docs (Sept 2026). - Codex CLI
exec --jsonevent schema, forcedapproval_policy: never, no approval channel outsideapp-server,codex login --with-api-keyon stdin — openai/codex source and learn.chatgpt.com/docs (Sept 2026). - 1Password secret references,
op read, desktop-app session limits, service account scoping and rate limits, and the Environments MCP server — 1password.dev (Sept 2026), plus "Where MCP fits, and where it doesn't" (1Password, 2025) for why an MCP server must not hand over credentials. - Anthropic API
/v1/messagesstreaming,thinking: {type:'adaptive'}withoutput_config.effort,GET /v1/models— platform.claude.com (Sept 2026). - OWASP ASVS 5.0 (secret storage, session binding) and the OAuth 2.0 Security Best Current Practice for the PKCE and single-use-state reasoning.