Provider authentication
Status: 2026-08-22.
Provider authentication has three separate roles. A feature is complete only when all three are present:
AuthStorepersists a typed credential without knowing how it was obtained or used.LoginMethodRegistrydeclares the user-selectable ways a provider can obtain a credential.- The provider implementation consumes that credential, including provider-specific refresh, endpoint, and header behavior.
Keeping these roles separate prevents the existence of an oauth JSON variant from being mistaken for an implemented OAuth integration.
Identity, transport, and authentication
A provider id is the configuration and credential identity, for example openai or myopenai. provider.<id>.transport selects a native Rust wire implementation. It does not change the provider id and does not select authentication.
Consequently, a custom provider using transport: "openai" gets OpenAI request and stream encoding but does not inherit the official openai provider's ChatGPT login. The same transport can safely serve multiple provider ids with different credentials and endpoints.
Credential resolution
A turn resolves one credential in this order:
provider.<id>.options.apiKey;- the provider-id entry in
AuthStore; - the first non-empty variable named by
provider.<id>.env; - no credential.
An explicitly empty options.apiKey is intentional and prevents fallback. This lets a local endpoint declare that it accepts no key without accidentally receiving a stored vendor credential.
Environment credentials remain process inputs. They are not copied into the credential file. Stored credentials live at $XDG_DATA_HOME/zuno/auth.json, normally ~/.local/share/zuno/auth.json. ZUNO_AUTH_CONTENT supplies an in-memory read override for managed environments.
Method registration and catalog availability
Every login method is an explicit registration. The shipped registry gives the official openai id api-key, chatgpt-browser, and chatgpt-device. Configured provider instances receive api-key only when their resolved models use a native transport that consumes stored API keys. Ambient-credential transports such as Bedrock and Vertex do not advertise a login method.
Catalog resolution receives the same registry. A stored OAuth credential makes a provider selectable only when that exact provider id has a native OAuth method. This joins the interface, provider, and consumer at the composition boundary:
openaiplus a ChatGPT OAuth credential is selectable;myopenaiplus the same OAuth-shaped credential is not granted OpenAI OAuth behavior;myopenaiwith a config block, API key, or declared environment key remains selectable normally.
A future custom OAuth component must register its methods, implement authorization and refresh, and consume the resulting credential in its provider. Adding only a config value or credential shape is insufficient.
CLI selection
zuno auth login owns a short-lived terminal picker rather than borrowing the resident TUI or putting selection policy in the provider registry. The official openai id is always present. Other rows require both a configured, selectable model route and an explicitly registered login method. A catalog entry or a stored credential alone never creates a row, and there is no Other escape hatch that stores an unusable credential. enabled_providers and disabled_providers are applied before the list is rendered.
When the selected provider has several registered methods, a second picker selects the method. Both pickers support arrows, paging, type-to-filter, Enter, and Escape/Ctrl+C cancellation. They are entered only when standard input and standard error are terminals. A redirected invocation remains deterministic: the provider must be explicit, and piped standard input selects its registered API-key method. An unsupported or unconfigured id fails before standard input is read or the credential file is changed.
OpenAI authentication
The official openai provider supports two independent families:
- Platform API key: read from config, storage, or environment and sent to the configured OpenAI API endpoint.
- ChatGPT OAuth: browser authorization with loopback PKCE or device-code authorization for headless hosts.
Browser authorization binds the allowlisted loopback ports 1455 and 1457, verifies the callback state, and exchanges the code with its PKCE verifier. Device authorization prints the verification URL and one-time code, then polls until authorization completes or the bounded deadline expires.
The stored OAuth credential contains access, refresh, expiry, and optional account metadata. Before each request, the OpenAI provider refreshes a token within the expiry skew. A successful rotation updates the in-memory credential and AuthStore; an active ZUNO_AUTH_CONTENT override is never rewritten.
ChatGPT OAuth is accepted only for the Responses surface. Its request uses the ChatGPT Codex backend, includes ChatGPT-Account-Id when the token identifies an account, and forwards a constrained compute-residency claim when one is present. Custom OpenAI-wire providers treat a manually supplied OAuth access value as an ordinary bearer and do not receive ChatGPT endpoint or refresh semantics.
Product-agent authentication is separate
Configured Codex and Claude Code subagents are not model providers and never participate in zuno auth login. Zuno starts the host-installed command and inherits that product's own configuration and login state. It does not read, translate, refresh, or copy those tokens into AuthStore, and it does not choose a product model from the Zuno catalog.
This boundary is intentional: openai provider OAuth authenticates Zuno's in-process OpenAI request implementation, while a Codex product agent authenticates the separate Codex installation and app-server process. See Codex and Claude Code product agents.
Security properties
- Credential files are created with mode
0600on Unix and repaired to that mode on later writes. - Secret fields use a redacted
DebugandDisplay; access requires the explicitSecret::exposeboundary. - Browser state and PKCE verifier values are omitted from debug output.
- API-key login disables terminal echo interactively and reads standard input in pipelines, avoiding command-line arguments and shell history.
- OAuth transport and protocol failures remain typed so retry policy does not depend on rendered text.
Windows uses inherited filesystem ACLs because Unix mode bits are unavailable. An OS keyring is not currently an authentication backend; the durable backend is the protected Zuno credential file.