Meerkat’s auth story has two layers. The fast path is env vars
(ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, or
AZURE_OPENAI_API_KEY plus AZURE_OPENAI_ENDPOINT) — set them and
rkat run works. The powerful path is realm-scoped bindings:
declare a realm in your config, register credentials via rkat auth login or the REST/RPC surfaces, and pass --auth-binding <realm>:<binding> to scope a session or mob member to that specific
binding.
Today, CLI rkat auth login is primarily a convenience bootstrap flow for the default dev:* credential namespace. If you need explicit realm/profile-targeted auth persistence such as prod:openai, use the REST/RPC auth surfaces or manage the realm config entries directly.
What this guide is for
Use this guide when you need more than the env-var fast path:
- explicit realm-scoped bindings
- OAuth or cloud-IAM-backed auth
- hot-swapping provider identity
- per-mob-member credential overrides
Both paths route through the same ProviderRuntimeRegistry. There’s no
hidden magic path: the env fallback is a synthesized realm with
CredentialSourceSpec::Env, resolved by the same code that handles
your configured realms.
The fast path: env vars
export ANTHROPIC_API_KEY=sk-ant-...
rkat run "What's the weather like in 2026?"
That’s it. If the env var is set, Meerkat synthesizes a default realm
(env_default) with a binding that reads the key via
CredentialSourceSpec::Env, then resolves normally. RKAT_*-prefixed
env vars (e.g., RKAT_ANTHROPIC_API_KEY) take precedence over the
provider-native names.
| Provider | Primary env var | Fallback |
|---|
| Anthropic | ANTHROPIC_API_KEY | — |
| OpenAI | OPENAI_API_KEY | — |
| OpenAI on Azure | AZURE_OPENAI_API_KEY plus AZURE_OPENAI_ENDPOINT | Optional AZURE_OPENAI_IMAGE_GENERATION_DEPLOYMENT, AZURE_OPENAI_IMAGE_GENERATION_API_VERSION |
| Gemini | GEMINI_API_KEY | GOOGLE_API_KEY |
If both public OpenAI and unprefixed Azure OpenAI env vars are present, the
public OpenAI env default wins. Use RKAT_AZURE_OPENAI_API_KEY plus
RKAT_AZURE_OPENAI_ENDPOINT, or an explicit realm binding, to make Azure
OpenAI the env default in that case.
The powerful path: realms + bindings
A realm is a named collection of backend profiles, auth profiles,
and provider bindings. It lets you:
- Run multiple sessions against different accounts or tenants
- Mix OAuth (Claude.ai Pro/Max), api_key (keyed by CI), and cloud IAM
(Bedrock/Vertex/Foundry) in one process
- Hot-swap a session’s active binding mid-conversation
- Give different mob members different credentials
Realm shape
# ~/.rkat/config.toml (or project-local .rkat/config.toml)
[realm.prod.backend.anthropic_primary]
provider = "anthropic"
backend_kind = "anthropic_api"
[realm.prod.backend.openai_primary]
provider = "openai"
backend_kind = "openai_api"
[realm.prod.auth.anthropic_oauth]
provider = "anthropic"
auth_method = "claude_ai_oauth"
source = { kind = "managed_store" }
[realm.prod.auth.openai_apikey]
provider = "openai"
auth_method = "api_key"
source = { kind = "managed_store" }
[realm.prod.binding.default]
backend_profile = "anthropic_primary"
auth_profile = "anthropic_oauth"
[realm.prod.binding.openai]
backend_profile = "openai_primary"
auth_profile = "openai_apikey"
Azure OpenAI uses the same provider id (openai) with an Azure-specific
backend/auth pair:
[realm.prod.backend.azure_openai]
provider = "openai"
backend_kind = "azure_openai"
base_url = "https://my-resource.cognitiveservices.azure.com"
options = { image_generation_deployment = "gpt-image-2", image_generation_api_version = "preview" }
[realm.prod.auth.azure_openai_key]
provider = "openai"
auth_method = "azure_api_key"
source = { kind = "managed_store" }
[realm.prod.binding.azure_openai]
backend_profile = "azure_openai"
auth_profile = "azure_openai_key"
default_model = "gpt-5.5" # Azure deployment name
Log in
# Interactive OAuth (opens a browser; stores refresh token in the
# local TokenStore credentials file, 0600 on Unix).
rkat auth login anthropic
# Scripted (api_key):
rkat auth login openai --non-interactive --secret "$OPENAI_KEY"
The current CLI login flow is convenient for local setup, but it does not yet let you choose an arbitrary target realm/profile name at login time. It seeds the runtime-managed default dev:* identities and is best thought of as the easiest way to get persisted credentials into Meerkat quickly.
Use the binding
rkat run --auth-binding prod:default "hello"
rkat run --auth-binding prod:openai "hello"
Subcommands
Login and profile management
rkat auth realms # list realms defined in config
rkat auth profiles --realm prod # list profiles/backends/bindings
rkat auth profile --realm prod <id> # inspect one profile
rkat auth login <provider> # start OAuth or api_key flow
rkat auth logout <profile_id> # clear persisted credentials
Status and refresh
rkat auth status --realm prod <profile_id> # TokenStore state + expiry
rkat auth test --realm prod <binding_id> # dry-run the binding
rkat auth refresh --realm prod <profile_id> # force-refresh OAuth tokens
rkat auth refresh is a no-op for api_key / azure_api_key / static_bearer
auth methods (nothing to refresh). For OAuth-backed methods it
exchanges the persisted refresh token for a fresh access token and
writes the new bundle back to the TokenStore.Delete
rkat auth profile-delete --realm prod <profile_id> -y
Release auth smoke
make e2e-auth is the CI-safe auth smoke lane. It drives Meerkat’s
browser OAuth plumbing against a local OAuth fixture, verifies token-store
and AuthMachine lifecycle behavior across login, status, refresh, restart,
resolve, and logout, and skips optional live-provider canaries when their
seeded credentials are absent.
Real third-party browser login pages remain a manual pre-release check.
Do not automate provider-hosted OpenAI, Anthropic, or Google login pages in
CI; those pages are intentionally subject to MFA, CAPTCHA, account policy,
and provider UI changes.
Manual browser-login release checklist:
- Start login for the target provider.
- Confirm the system browser opens the provider-hosted login or consent page.
- Complete the callback and confirm Meerkat reports the login complete.
- Check auth status and confirm the target binding is
valid.
- Run one tiny call through that binding.
- Logout and confirm auth status is cleared and the credential is gone.
Auth methods
Each provider’s runtime accepts a matrix of (backend_kind, auth_method) pairs. The table below summarizes what’s wired today.
| Provider | backend_kind | auth_method | Notes |
|---|
| Anthropic | anthropic_api | api_key, static_bearer | Direct to api.anthropic.com |
| Anthropic | anthropic_api | claude_ai_oauth | Claude Pro/Max OAuth (PKCE, S256) |
| Anthropic | anthropic_api | oauth_to_api_key | Console OAuth → provisions api_key |
| Anthropic | anthropic_api | external_authorizer | Host-resolved auth injection |
| Anthropic | bedrock | bedrock_bearer, bedrock_aws_sigv4 | AWS SigV4 / env-bearer |
| Anthropic | vertex | vertex_google_auth | GoogleAuth / Vertex AI |
| Anthropic | foundry | foundry_api_key, foundry_azure_ad | Azure AI Foundry |
| OpenAI | openai_api | api_key, static_bearer, external_authorizer | Direct to api.openai.com or host-resolved auth |
| OpenAI | azure_openai | azure_api_key | Azure OpenAI Responses API; base_url required; session model is the Azure deployment name |
| OpenAI | chatgpt_backend | managed_chatgpt_oauth | ChatGPT Plus/Pro OAuth |
| OpenAI | chatgpt_backend | external_chatgpt_tokens | Host-supplied tokens |
| Google | google_genai | api_key, bearer_api_key, external_authorizer | Direct to Google GenAI or host-resolved auth |
| Google | vertex_ai | api_key_express | Vertex API Express key |
| Google | vertex_ai | adc, compute_adc | ADC / compute metadata |
| Google | google_code_assist | google_oauth | Gemini Code Assist OAuth |
| Self-hosted | self_hosted, openai_compatible | none, api_key, static_bearer | Realm-bound credentials for configured self_hosted servers |
External commands (CredentialSourceSpec::Command) and file
descriptors (CredentialSourceSpec::FileDescriptor) let host
applications inject tokens without going through OAuth.
Credential sources
The source field on an auth profile declares where credentials
come from.
| Kind | Description |
|---|
InlineSecret { secret } | Secret stored inline in the TOML (use only for dev/local) |
Env { env, fallback } | Read env var at resolve time via ResolverEnvironment::env_lookup |
ManagedStore | Resolve via the TokenStore; the storage key is the resolved typed binding identity (realm, binding) |
PlatformDefault | Use the host platform’s default credential chain where supported |
ExternalResolver { handle } | Host-registered JS callback (WASM) or Rust resolver (embedded) |
Command { program, args, ... } | Run an external binary; cache stdout for refresh_interval_ms |
FileDescriptor { fd, scope_override } | Read credentials from a host-supplied file descriptor |
Storing api keys inline in config files works but shouldn’t be
checked into git. Prefer ManagedStore (which writes to the OS
keychain or a 0o600 file) or Env.
FileDescriptor is a host-integration seam rather than a plain standalone resolver path. It requires a host-scoped reader/injection path, so treat it as an advanced embedding surface rather than a generic CLI config trick.
Hot-swap mid-session
When a session is created with --auth-binding prod:default, its
persisted SessionMetadata.auth_binding carries that binding through
resume and hot-swap. Changing the model/provider mid-turn via the RPC
turn/start override keeps the binding by default; pass an explicit
auth_binding on the turn request to scope the swap to a different
realm or binding.
// RPC turn/start — hot-swap to openai binding in the same realm
{
"method": "turn/start",
"params": {
"session_id": "...",
"prompt": "...",
"model": "gpt-5.5",
"auth_binding": {"realm": "prod", "binding": "openai"}
}
}
The factory re-enters ProviderRuntimeRegistry::resolve with the new
binding — no env-default fallback, no cross-realm credential bleed.
Per-mob-member override
Mob members default to env-default / config-realm fallback credentials.
Pass auth_binding on the host-side mob spawn surface to scope a member to a
specific binding:
{
"method": "mob/spawn",
"params": {
"mob_id": "research",
"profile": "analyst",
"agent_identity": "a1",
"auth_binding": {"realm": "prod", "binding": "openai"}
}
}
Members in the same mob can use different providers or tenants
without cross-contamination. Public wire surfaces accept the structural
auth_binding object only; the colon-joined realm:binding[:profile]
form is a CLI input convenience and is converted before it crosses the
wire boundary.
Refresh coordination
Refresh semantics are owned by the AuthMachine DSL (one instance
per <realm>:<binding>). On resolve, the runtime:
- Checks
expires_at. If present and within the refresh window
(AUTH_LEASE_TTL_REFRESH_WINDOW_SECS, 60 seconds before expiry),
marks the lease Expiring via the DSL.
- Under
Expiring, the next resolve triggers BeginRefresh.
In-process dedup ensures only one refresh HTTP call per binding
even under concurrent resolves; cross-process dedup via filesystem
lockfile when refresh-file-lock is enabled.
- On successful refresh, fires
CompleteRefresh and updates
expires_at. On failure, RefreshFailedTransient (retry)
or RefreshFailedPermanent (routes to ReauthRequired +
emits an [AUTH_REAUTH_REQUIRED] system notice on the session).
The lifecycle is TLC-verified in specs/machines/auth/.
Audit logging
Every accepted lifecycle transition emits a structured tracing event:
target = "meerkat::auth::audit"
- Fields:
binding_key, action, from_phase, to_phase
REST and RPC surfaces also emit audit events for user-initiated
actions: create_profile, delete_profile, login_oauth_complete,
login_device_complete, logout. Filter any tracing::Subscriber
on the meerkat::auth::audit target to build a persistent audit log.
Feature flags
The meerkat-providers crate gates optional auth methods behind
features so you only pay for what you use:
| Feature | Enables |
|---|
oauth | OAuth flows (Claude.ai, ChatGPT, Code Assist) |
native-keyring | OS keychain storage for tokens |
refresh-file-lock | Cross-process refresh dedup |
aws-auth | Bedrock backend + SigV4 signing |
google-oauth | Vertex + Code Assist + ADC |
azure-auth | Foundry backend + Azure AD |
The rkat CLI enables anthropic, openai, gemini, and oauth
by default; add cloud-specific features at compile time for
Bedrock/Vertex/Foundry.
See also
- Hooks — observe auth events or gate turns on
credential state
- Mobs — per-member
auth_binding overrides
- Machine Authority — rationale behind single-owner runtime state