# model authentication model authentication is supplied by model-provider extensions rather than the daemon configuration loader. ## credentials Every secret albedo keeps lives in `$ALBEDO_HOME/creds.json`, mode `0600`, and only the daemon reads or writes it. It has three sections: `accounts` holds OAuth accounts (and an optional `alibaba` key) by store key, one object or an array; `providers` holds each profile's `apiKey` by profile name; `mcp` holds each MCP server's `bearerToken`, `headers` and `env` by server name. All credential and settings mutations share one per-home daemon lock. OAuth refreshes retain their re-read under that lock and preserve other sections. `config.json` keeps no keys: an `apiKey` still written there by hand is used and wins over the saved one until the next daemon start moves it. At start the daemon moves secrets still kept elsewhere into `creds.json`: `auth.json`'s accounts, `mcp-credentials.json`'s servers, and each `config.json` profile's `apiKey`. It writes `creds.json` first, then moves the old files into `backups/` as `-before-creds-` with every mode bit cleared, so an interrupted start loses nothing and the next one finishes. Not even their owner can read the backups without `chmod 600` first, so tools and agents running as the user never read them by accident; `rm -f` deletes them. Clients save provider profiles and MCP configuration with credentials through the [settings API](settings.md). They only learn which credentials are saved. The existing authentication routes remain available. `GET /auth/credentials` answers `{"providers": [profile...], "mcp": {server: {"bearerToken": bool, "headers": [name...], "env": [name...]}}}`. `POST /auth/credentials/migration` answers `{"moved": [file...]}` with the files this daemon's start moved secrets out of, to the first client that asks and `[]` after; the CLI asks before its TUI starts and, after a migration, prints where the secrets went and the `rm -f` for the backups, then waits for enter. `PUT /auth/credentials/providers/` with `{"apiKey"}` saves a key and `DELETE` removes it. `PATCH /auth/credentials/mcp/` changes one server's secrets: an absent field keeps its value, `null` removes it, and `headers` and `env` map a name to a value or `null`. It answers `{"undo": token}`; `POST /auth/credentials/mcp//undo` with `{"token"}` puts back what the server held before, for callers of the credential-only API. The CLI uses daemon-owned settings save/reload/restoration instead. ## openai-compatible api keys The enabled-by-default `openai` extension requires `models`. It handles existing named OpenAI-compatible profiles and both Responses and Chat Completions. Model discovery comes from the models.dev cache, not the configured API endpoint. Older `config.json` profiles without an `extension` field remain OpenAI profiles; newly saved profiles include `"extension": "openai"`. ## chatgpt codex oauth The enabled-by-default `codex` extension requires `openai`, so it reuses the OpenAI Responses encoder, SSE transport, replay format, and model metadata. Its picker shows the models the ChatGPT backend offers the signed-in account (see [codex models](models.md#codex-models)); API-key OpenAI profiles still show the full OpenAI catalog. `/login` can add ChatGPT Plus/Pro accounts. A fresh install can enter `codex` as the provider name; an existing install can choose **add chatgpt codex account**. The daemon runs the login (see [sign-in api](#sign-in-api)) with the Codex CLI browser flow: PKCE S256, the allowlisted `http://localhost:1455/auth/callback`, state validation, and a manual callback-url/code input that races the local callback. The token exchange uses OpenAI's public Codex client id and stores access, rotating refresh, expiry, ChatGPT workspace id, seat id when present, and email when present. Credentials live in `creds.json`'s accounts under the `openai-codex` key. One account uses the legacy object shape; two or more use an array. A login replaces the same seat/account identity and appends a different account. `/login` lists saved providers and each stored account; `d` (or delete) on either row asks to remove it, and enter on an account does the same. Removing the active provider makes the first remaining provider by name active. A Codex provider with no accounts left reads `signed out` and starts a sign-in when chosen. Access tokens refresh under the credential lock 60 seconds before expiry, with a re-read under the lock so concurrent sessions do not overwrite a newer refresh. Each session hashes its id into the account pool and stays on that account. If that account is invalid or cannot refresh, selection continues through its siblings. When the Codex backend answers a request with 401 (for example `token_revoked`), that account is removed from `creds.json` and the turn fails with an error ending in `run /login`; the TUI opens `/login` when that error follows a turn sent from the open chat. Requests use `https://chatgpt.com/backend-api/codex/responses` with Bearer auth, `chatgpt-account-id`, Codex experimental Responses headers, and the session request id. Codex request bodies force `store: false`, automatic parallel tools, low text verbosity, encrypted reasoning replay, and nullable tool strictness. The oauth callback binds only to `127.0.0.1`. Port 1455 is fixed because OpenAI allowlists the exact redirect URI; a busy port fails instead of silently changing redirects. ## sign-in api Browser sign-ins run in the daemon, so every client shares one OAuth implementation. An extension contributes `extension.LoginPlugin(oauth.Login(...))`: the profile extension it serves, a chooser label and detail, the protocol its profiles use, its `creds.json` accounts key, its loopback callback (host, port, path, and whether the port is fixed), and three functions. `authorize` builds the provider url from a `Grant` (redirect, state, PKCE verifier and challenge). `exchange` trades the code for the credential object to store and may report progress. `account` names a stored credential; equal ids are the same account. `albedo_oauth` owns the rest: the callback listener, the race with a pasted code, a 5-minute timeout, and locked writes. A sign-in replaces the stored account with the same id and appends a new one. One account is stored as an object, two or more as an array. Clients use these daemon routes. All of them require the daemon token. - `GET /auth` returns `{logins: [{provider, label, detail, protocol}], accounts: [{provider, id, label, detail, selected}]}`. Accounts whose labels collide get the start of their id appended. - `POST /auth/{provider}` starts a sign-in and returns `201 {id, url}`. The client opens `url` in a browser unless `ALBEDO_NO_BROWSER` is set, and shows it for copying. - `GET /auth/logins/{id}` returns `{state, message}`. `state` is `waiting`, `exchanging`, `done`, or `failed`; `message` is progress, the new account's label when done, or the failure reason. Clients poll it. A settled sign-in is forgotten after 10 minutes. - `POST /auth/logins/{id}` with `{input}` delivers a pasted callback url, `code#state`, query string, or bare code. - `DELETE /auth/logins/{id}` cancels the sign-in and closes its callback listener. - `POST /auth/{provider}/accounts/{id}` selects that account; `DELETE` on the same path removes it. After a sign-in finishes, the client lists models with `GET /models/{provider}` and saves the profile `{extension: provider, model, protocol}` itself. Profiles stay client-owned; credentials are daemon-owned. ## claude code oauth The enabled-by-default `claude` extension adds Claude Pro/Max accounts through `/login` using Anthropic’s Claude Code OAuth client. It stores rotating access and refresh tokens in `creds.json`'s accounts under `anthropic`, refreshed under the credential lock. An account stored without a refresh token (a legacy or hand-edited one) is never refreshed: the request path skips it, and the quota poll keeps it so its auth failure is recorded. The fixed `http://localhost:53692/callback` loopback redirect must be available; a pasted callback URL also works. Choose a Claude model after signing in; profiles use `extension: "claude"` and `protocol: "chat_completions"`. The picker shows Opus 5.5, Fable 5.1, Sonnet 5, Opus 5, Opus 4.8, and Haiku 4.5 only when the cached models.dev Anthropic catalog contains them. `/reload models` refreshes this availability and the models’ limits. New model families still require a picker-policy update. Requests go to Anthropic’s Messages endpoint with Claude Code’s OAuth beta, client headers, billing attribution block, identity system block, and tool names. The billing marker is text in `system[0]`, not an HTTP header; its CCH attestation hashes the exact outgoing JSON body. Top-level tool-schema combiners are flattened into an object with their property alternatives and common required fields; nested schemas remain intact because Anthropic rejects top-level `oneOf`/`allOf`/`anyOf`. This transformation lives beside the request encoder in `harness/extensions/claude/schema.gleam` and preserves unrelated schema fields, including definitions and custom metadata. Replay is saved as provider-neutral chat messages so changing models retains conversation and tool history. This is subscription access, not an Anthropic API-key profile. Anthropic can change or restrict this unofficial client behavior at any time. ## google antigravity oauth The enabled-by-default `antigravity` extension signs in with the Antigravity IDE's Google OAuth client and sends requests to Cloud Code Assist (`https://daily-cloudcode-pa.googleapis.com`) for Gemini 3, Claude, and GPT-OSS models. `/login` offers **add google antigravity account**; a fresh install can also enter `antigravity` as the provider name. The daemon runs the sign-in through the [sign-in api](#sign-in-api). Its callback is `127.0.0.1:51121/oauth-callback`, which moves to an ephemeral port when 51121 is busy. After the token exchange it finds or provisions the account's Cloud Code Assist project: `loadCodeAssist`, a free-tier eligibility check, and `onboardUser` when the account has no tier yet, with progress shown in the client. An account that Google wants verified fails with the verification url. Credentials live in `creds.json`'s accounts under `google-antigravity`, in the one-object-or-array shape: `{type: "oauth", access, refresh, expires, projectId, email?, selected?}`. The daemon tries the selected account first, then the others in order. Access tokens refresh under the credential lock 60 seconds before they expire. A 401 ends the turn with an error ending in `run /login` and marks that access token expired, so the next turn refreshes it; if the refresh token was revoked too, the account is skipped and reads as signed out. Antigravity profiles use `extension: "antigravity"` and must use `protocol: "chat_completions"`. The extension's upstream translates albedo requests into the Cloud Code Assist envelope, sends it through `openai_api.exchange` (the shared HTTP and SSE runner), and reduces Gemini's stream back into albedo events. Each assistant turn is stored as a Chat Completions message, so history, projection, and other providers read it unchanged. The exact Gemini parts, including thought signatures, travel in `reasoning_details` as an `antigravity.parts` entry and are replayed only to the model that produced them. Output from other models replays as plain text and tool calls; Gemini function calls without a signature get Google's documented `skip_thought_signature_validator` placeholder. Requests use the `antigravity/hub/` user agent. The version comes from the Antigravity update manifest, or from `ALBEDO_ANTIGRAVITY_VERSION` when it is set. Tool schemas are flattened to the subset Cloud Code Assist accepts: references are resolved, unions keep their first non-null member, `const` and enums become string enums, and dropped validation keywords move into the description.