ProviderAuthManager stores and retrieves credentials for all LLM providers. It supports two credential types — OAuth tokens and plain API keys — and three credential sources: stored in auth.json, set at runtime, or read from environment variables.
@dataclass
class OAuthCredential:
access: str # access token
refresh: str # refresh token
expires: int # Unix timestamp in milliseconds
account_id: str | None = None
@dataclass
class APICredential:
key: str # plain API keyAuthCredential = OAuthCredential | APICredential
Credentials are stored in auth.json in the platform config directory (~/.operator/auth/providers.json by default). The file is created with mode 0600 (owner read/write only) and the parent directory with mode 0700.
The JSON format is a flat object keyed by provider ID:
{
"anthropic": {
"type": "oauth",
"access": "...",
"refresh": "...",
"expires": 1234567890000,
"account_id": "user@example.com"
},
"openai": {
"type": "api_key",
"key": "sk-..."
}
}All reads and writes go through a FileLock to prevent race conditions between concurrent processes (e.g., multiple agent instances refreshing the same token simultaneously).
| Backend | When to use |
|---|---|
FileAuthStorage |
Production — persists to disk with FileLock |
InMemoryAuthStorage |
Testing — no disk I/O, no locking |
ProviderAuthManager.create(registry) creates a FileAuthStorage at the default path. ProviderAuthManager.in_memory(registry, initial) creates an in-memory store pre-seeded with initial data.
ProviderAuthManager.get_api_key(provider) resolves a usable API key in priority order:
- Runtime override — set via
set_runtime_api_key(provider, key). Takes priority over everything else. Not persisted. - Stored API key —
APICredentialfromauth.json. - Stored OAuth token —
OAuthCredentialfromauth.json. If expired, triggers a token refresh (with file locking to prevent duplicate refreshes). Derives a usable API key viaoauth_provider.get_api_key(credential). - Environment variable —
{PROVIDER_ID_UPPER}_API_KEY(e.g.,ANTHROPIC_API_KEY).
Returns None if no credential is found at any level.
get_auth_status(provider) returns an AuthStatus without exposing credential values:
@dataclass
class AuthStatus:
configured: bool
source: Literal["stored", "runtime", "env"] | None = None
label: str | None = None # env var name for env-sourced keysUsed by /auth and /login to show which providers are configured.
OAuth tokens expire. When get_api_key() detects an expired token it calls _refresh_oauth_token_with_lock():
- Acquire the file lock.
- Re-read
auth.json— another process may have already refreshed the token. - If the token is still expired, call
oauth_provider.refresh_token(credential). - Write the new credential back to
auth.jsonatomically under the lock. - Update
self.datain-memory.
If the refresh fails (network error, revoked token), None is returned and the caller must re-authenticate.
await auth_manager.login(provider_id, callbacks)
await auth_manager.logout(provider_id)login: Delegates to OAuthProvider.login(callbacks). The callbacks object receives the authorization URL and handles user interaction (browser open, device code prompt). After the exchange, the credential is stored in-memory and persisted.
logout: Calls OAuthProvider.logout(credential) to revoke the token server-side if supported. Then removes the credential from in-memory storage and persists the deletion.
LLM._auth_store is a class-level ProviderAuthManager. All LLM instances share it. A credential stored by /login is immediately visible to any LLM constructed afterward.
class OAuthLoginCallbacks:
on_url: Callable[[str], None] # receive the auth URL
on_device_code: Callable[[str], None] # receive device code (if needed)
on_complete: Callable[[], None] # called after login completesThe /login command provides callbacks that open the browser (webbrowser.open(url)) and prompt for confirmation on the terminal.
Every provider supports a {PROVIDER_ID_UPPER}_API_KEY environment variable. This is checked last — after stored credentials and runtime overrides. It is never persisted.
Examples:
ANTHROPIC_API_KEYOPENAI_API_KEYMISTRAL_API_KEYGOOGLE_API_KEY
The operator command-line tool provides a unified auth CLI suite to manage provider credentials (both API keys and interactive OAuth logins).
operator author
operator auth listDisplays a list of all registered providers (both OAuth and API-key), along with their current authentication status and configuration source.
To view detailed status for a single provider:
operator auth list <provider-id>or
operator auth status <provider-id>To set an API key for a key-based provider:
operator auth set <provider-id> --api-key <key>Example:
operator auth set openai --api-key sk-...To initiate the OAuth login flow for an OAuth provider:
operator auth set <provider-id> --oauthExample:
operator auth set claude --oauthThis launches a browser window to authenticate with the provider and guides you through the process in the terminal.
To remove stored credentials (either deleting an API key or logging out of an OAuth session):
operator auth unset <provider-id>Storage errors are non-fatal. ProviderAuthManager._load() catches exceptions from the file lock and records them. If the initial load fails, self.data is empty and self._load_error is set. Subsequent _persist_provider_change() calls are no-ops when _load_error is set, so the in-memory state is not silently lost to disk when the storage is broken.
drain_errors() returns and clears the accumulated error list, allowing the caller to surface any storage failures.
Channel bot tokens are stored separately from LLM provider credentials. They are never mixed with auth.json.
Storage: ~/.operator/auth/channels.json, mode 0600.
{
"telegram": { "bot_token": "..." },
"discord": { "bot_token": "..." },
"slack": { "bot_token": "xoxb-...", "app_token": "xapp-..." },
"twitch": { "token": "..." },
"email": { "username": "user@example.com", "password": "..." }
}For each field, ChannelAuthManager checks in order:
channels.json— stored value (if non-empty).- Environment variable — see table below.
Environment variables take effect when the JSON field is blank:
| Channel | Field | Env var |
|---|---|---|
telegram |
bot_token |
TELEGRAM_BOT_TOKEN |
discord |
bot_token |
DISCORD_BOT_TOKEN |
slack |
bot_token |
SLACK_BOT_TOKEN |
slack |
app_token |
SLACK_APP_TOKEN |
twitch |
token |
TWITCH_TOKEN |
email |
username |
EMAIL_USERNAME |
email |
password |
EMAIL_PASSWORD |
from operator_use.auth.channels import ChannelAuthManager
from operator_use.settings.paths import get_channels_auth_path
auth = ChannelAuthManager(get_channels_auth_path())
# Read
token = auth.telegram.bot_token
bot_token, app_token = auth.slack.bot_token, auth.slack.app_token
# Write (persists immediately to channels.json)
auth.set_telegram(bot_token="...")
auth.set_slack(bot_token="xoxb-...", app_token="xapp-...")
auth.set_email(username="bot@example.com", password="...")ChannelAuthManager is created by the Runtime alongside the GatewayManager. If channels.json cannot be parsed, it logs a warning and starts with empty credentials (channels that require tokens will log their own warning and be skipped at startup).
- inference.md — How
LLMusesProviderAuthManagerat construction time - commands.md —
/login,/logout,/authcommands - gateway.md — How channel credentials are used at channel startup