> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rkat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth

> Realm-scoped credential resolution: from the env-var happy path to OAuth, cloud IAM, and per-member overrides

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>
  `rkat auth login` provisions the reserved `global` realm in the home-rooted doc
  at `~/.rkat/config.toml`. Because `global` is the universal tail of every realm
  chain, a single sign-in is inherited by every workspace realm (credential reads
  inherit down the chain). For a binding owned by a specific realm such as
  `prod:openai`, manage that realm's config entries directly; credential *writes*
  are strict-owner and land only in the realm that defines the binding. See
  [Realm inheritance](/concepts/realm-inheritance).
</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 theme={null}
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 theme={null}
# ~/.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 theme={null}
[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 theme={null}
# 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"
```

`rkat auth login` persists credentials and the matching `[realm.global]`
binding section into the home-rooted `global` realm doc (`~/.rkat/config.toml`),
so the sign-in is inherited by every workspace realm via the chain tail. To use
a binding owned by another realm (for example `prod:openai`), declare that
binding in the owning realm's config; credential writes are strict-owner and
land only in the realm that defines the binding.

### Use the binding

```bash theme={null}
rkat run --auth-binding prod:default "hello"
rkat run --auth-binding prod:openai   "hello"
```

## Subcommands

<Steps>
  <Step title="Login and profile management">
    ```bash theme={null}
    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 theme={null}
    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 theme={null}
    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                                                                    |
| 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                                                     |

<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 theme={null}
// 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 theme={null}
{
  "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.

## Native desktop hosts

Native applications should use `HostAuthService` for the complete interactive
transaction instead of recreating the CLI or RPC flow. The service owns strict
binding-owner resolution, PKCE and one-time state, exchange, coordinated
TokenStore persistence, AuthMachine lifecycle publication, rollback, status,
and logout. The application owns only its loopback listener, browser launch,
and UI.

Open one persistence capability and share that exact capability between
`AgentFactory` and `HostAuthService`:

```rust theme={null}
use std::sync::Arc;

use meerkat::{
    AgentFactory, HostAuthService, TokenStoreBackend,
};
use meerkat_runtime::MeerkatMachine;

fn desktop_auth(
    sessions_root: std::path::PathBuf,
) -> Result<
    (AgentFactory, HostAuthService, Arc<MeerkatMachine>),
    Box<dyn std::error::Error>,
> {
    // This is the CLI-compatible profile: the desktop and `rkat auth login`
    // read and write the same file-owned credential for a binding.
    let persistence = TokenStoreBackend::default_auto()?
        .open_with_refresh_authority()?;
    let runtime = Arc::new(MeerkatMachine::ephemeral());

    let factory = AgentFactory::new(sessions_root)
        .with_provider_auth_persistence(persistence.clone());
    let auth = HostAuthService::new(
        persistence,
        runtime.provider_auth_runtime_authority(),
    );
    Ok((factory, auth, runtime))
}
```

Keep the `MeerkatMachine` alive for as long as the factory and auth service;
the snippet returns it in `Arc`, while a real host normally stores all three in
one application state object. A TokenStore open
failure is returned as a fault and is never collapsed into
`interactive_login_required`.

Managed-store resolution also requires per-session AuthMachine bindings. Do
not use the factory's standalone build mode for a runtime-backed desktop
session:

```rust theme={null}
use meerkat::{AgentBuildConfig, RuntimeBuildMode, Session, SessionId};

let session = Session::new();
let bindings = runtime.prepare_bindings(session.id().clone()).await?;
let mut build = AgentBuildConfig::new("gpt-5.4");
build.resume_session = Some(session);
build.runtime_build_mode = RuntimeBuildMode::SessionOwned(bindings);
let agent = factory.build_agent(build, &config).await?;
```

For a keyring-preferred desktop, enable Meerkat's `native-keyring` feature and
replace `default_auto()` with `default_keyring_auto()`. That profile preserves
an existing CLI file credential as the per-key authority, but a brand-new key
is written to the OS keyring. Use the same keyring-aware backend in every
process that must see desktop-first credentials; the stock CLI intentionally
uses the file profile to avoid surprise OS keychain prompts.

The browser flow is split for native UI:

1. Bind the loopback listener and choose its redirect URI.
2. Call `login_start(config, target, redirect_uri)`, then open the returned
   `authorize_url`.
3. Pass the callback's `state` and `code` to `login_complete`.
4. Use `status` and `logout` for secret-free UI projections and sign-out.

## 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 the refresh window
   (`AUTH_LEASE_TTL_REFRESH_WINDOW_SECS`, 60 seconds before expiry),
   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
