---
title: "Auth"
description: "Realm-scoped credential resolution: from the env-var happy path to OAuth, cloud IAM, and per-member overrides"
icon: "key"
---

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.

<Note>
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.
</Note>

## 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

```bash
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

```toml
# ~/.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:

```toml
[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

```bash
# 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

```bash
rkat run --auth-binding prod:default "hello"
rkat run --auth-binding prod:openai   "hello"
```

## Subcommands

<Steps>
  <Step title="Login and profile management">
    ```bash
    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
    ```
  </Step>
  <Step title="Status and refresh">
    ```bash
    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.
  </Step>
  <Step title="Delete">
    ```bash
    rkat auth profile-delete --realm prod <profile_id> -y
    ```
  </Step>
</Steps>

## 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 |

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 { profile }` | Resolve via the `TokenStore`; in current provider runtimes, persisted lookup is tied to the active auth-profile identity/binding path |
| `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 |

<Warning>
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`.
</Warning>

<Note>
`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.
</Note>

## 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.

```json
// 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:

```json
{
  "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:

1. Checks `expires_at`. If present and within 80% of TTL, marks the
   lease `Expiring` via the DSL.
2. 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.
3. 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](/guides/hooks) — observe auth events or gate turns on
  credential state
- [Mobs](/guides/mobs) — per-member `auth_binding` overrides
- [Machine Authority](/reference/machine-authority) — rationale behind single-owner runtime state
