arizuko

arizuko › concepts › secrets

secrets

Secrets are the credentials an agent needs to reach out, such as a GitHub token or a model API key. routd keeps them encrypted in its secrets table. The kind of key decides whether it goes into the environment of the guest or stays on the host.

The guest environment holds the five model keys. The values come from the host .env of the operator, with the rows of the triggering user on top. A shell:env:<KEY> grant adds one more key. Users set model keys at /dash/me/env and capability tokens at /dash/me/secrets. A token can also arrive through OAuth at /dash/me/connections.

A key reaches work through one of two doors

Every credential reaches work through one of two doors. Door 1 is the spawn: the key is in the environment of the guest for the whole turn. Door 2 is the tool call: routd uses the key on the host, and the agent sees only the scrubbed answer. The kind of key decides the door.

┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ env-profile key       │ │ capability credential │ │ infra anchor          │
│ the five model keys:  │ │ GITHUB_TOKEN, ...     │ │ SECRETS_KEY, bot      │
│ host .env anchors     │ │ secrets row: folder   │ │ tokens,               │
│ under the caller's    │ │ walk, the user's row  │ │ AUTHD_SERVICE_KEY:    │
│ own user row, BYOA    │ │ wins; a pasted PAT    │ │ host .env only        │
└───────────┬───────────┘ │ or surrogate OAuth    │ └───────────┬───────────┘
            │             └───────────┬───────────┘             │
            ▼                         ▼                         ▼
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ EnvProfileSecrets,    │ │ ConnectorSecrets per  │ │ daemon env: never a   │
│ mergeSecrets: the     │ │ tool call, declared   │ │ row, never in a guest │
│ user's key wins       │ │ keys only; OAuth      │ └───────────────────────┘
└───────────┬───────────┘ │ refresh near expiry   │
            │             │ or after a 401        │
            │ door 1:     └───────────┬───────────┘
            │ spawn                   │ door 2: call
            ▼                         ▼
┌───────────────────────┐ ┌───────────────────────┐    ┌──────────────┐
│ guest env, for the    │ │ routd renders the     ├───►│ upstream API │
│ whole turn            │ │ header, sends the     │◄───┤              │
└───────────────────────┘ │ request, scrubs the   │    └──────────────┘
                          │ result for the agent  │
                          └───────────────────────┘
┌───────────────────────────────────────────────────────────────────────────┐
│ door 1 with MODEL_GATEWAY_URL set: spawnSecrets drops the five model      │
│ keys, routd mints MODEL_GATEWAY_KEY for the turn, and the litellm         │
│ container keeps the credential. A shell:env:<KEY> grant adds that one     │
│ folder key to door 1.                                                     │
└───────────────────────────────────────────────────────────────────────────┘

The key name decides the type

The key name decides the type, and the type decides the rules:

Env-profile keys
The model credentials: ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_OAUTH_TOKEN, OPENAI_API_KEY and CODEX_API_KEY. These keys choose which model a spawn talks to, so they exist at user scope only, and the store refuses them at folder scope. A user sets their own on the dashboard page /dash/me/env, and that key overrides the operator default for the spawns of that user.
Capability credentials
Tokens for one service that the agent uses to reach an external API, such as GITHUB_TOKEN or a Cloudflare key. A user sets them at /dash/me/secrets, and the operator sets them at folder scope. A grant controls their use. When a connector call needs one, the broker resolves the value of the triggering user on the host.
Infra and operator anchors
Values for the whole instance that live in the host .env, not in the table: AUTHD_SERVICE_KEY, bot credentials and SECRETS_KEY itself. They belong to the operator of the host. One name can appear in both places with different values. GH_TOKEN in .env belongs to a daemon, GH_TOKEN in a folder row is what a connector sends, and a connector never reads the file.

The scope decides who owns a row

For rows in the table, which are env-profile keys and capability credentials, the scope_kind field says who owns the value:

folder
Tied to a group path such as atlas/eng. The operator owns it, and every user of that folder shares it. The store refuses env-profile keys here.
user
Tied to one user sub. The user owns it, by typing it into /dash/me/env or /dash/me/secrets, or the operator seeds it as a fallback.

The coordinate of a row is (scope_kind, scope_id, key). Keys are uppercase environment-style ids that match ^[A-Z][A-Z0-9_]*$.

Model credentials reach the guest

At spawn, the environment of the guest holds the five env-profile keys and the keys that a shell:env:<KEY> grant names. The operator values from the host .env are the base, and the rows of the triggering user for the same keys go on top. So the ANTHROPIC_API_KEY of a user replaces the operator key for that spawn, and a user without one still reaches the model.

routd limits the set before it leaves the host. routd.DB.EnvProfileSecrets reads scope_kind='user' rows and keeps only the env-profile names, so no other row can reach the environment by mistake. mergeSecrets in runed/secrets.go merges that map over the anchors it reads from .env.

The litellm package keeps even the model credential out of the guest. The gateway container reads the credential from .env and forwards each request upstream with it. The guest gets MODEL_GATEWAY_URL and MODEL_GATEWAY_KEY instead: a virtual key that only the gateway accepts. A leaked transcript or a process in the guest holds nothing that works anywhere else. routd creates one key per turn, valid for the run ceiling, and deletes it when the turn ends. The package brings its own PostgreSQL for keys and spend. With a gateway, runed drops the own model key of a user and logs a warning, because no code converts it into a gateway key.

A shell grant adds a capability credential to the guest

Capability credentials do not enter the guest, at either scope, with one exception. A shell:env:<KEY> grant adds that folder key to the environment of the guest, so a shell tool such as gh can read it. shell:env:* names every key. Without that grant, the host resolves them at tool-call time, inside the MCP dispatch chain. When a connector declares secrets = ["GITHUB_TOKEN"], routd looks up the value and passes it as an argument to the handler in the host process. The handler makes the outbound HTTP call, limited to the keys that the call declared. The agent sees only the response of the handler, with the value scrubbed.

Folder resolution walks from the folder up to a synthetic root row, and the deepest row wins. The own row of the calling user then goes on top. So atlas/eng/sre sees its own keys over those of atlas/eng, then atlas, then root. A key set at atlas reaches every folder under atlas/ unless a child overrides it.

OAuth gets a capability token without a paste

A GITHUB_TOKEN pasted at /dash/me/secrets works, but you must create a long-lived personal access token and copy it by hand. Connect GitHub at /dash/me/connections runs the OAuth flow instead. You approve the arizuko OAuth app on GitHub. The access and refresh tokens from GitHub go into the same secrets row as a pasted token, with the same key, broker and delivery path. The broker refreshes the token before it expires, so you do not handle it again after the first approval.

This is surrogate OAuth: arizuko authenticates as you to the GitHub API. It is different from the OAuth login you use to sign in to arizuko. The provider registry holds GitHub, and another provider registers in the same way.

The broker refreshes the token when less than a minute is left, and once more after a 401. A provider can report a token as valid after it has expired. If the provider refuses the refresh token, the call fails with a message that tells you to connect again.

A key is not a grant

A stored key grants nothing, and a GitHub connection grants nothing either. The agent still needs a grant for the tool, such as mcp:github:* or ext:<service>:*. The tool appears in its list only when that grant exists. The reverse is also true: a tool grant to a folder gives it nobody’s key. Your row stays yours, and the broker uses it only on a turn that you started.

A scheduled task runs as service:routd, not as you, so it finds no user row. A cron job that needs a capability credential needs a key at folder scope.

The operator manages secrets from the CLI

# folder-scoped
arizuko secret -i <instance> set <folder> SERVICE_TOKEN --value V
arizuko group -i <instance> grant folder:<folder> <folder> shell:env:SERVICE_TOKEN
arizuko secret -i <instance> list <folder>
arizuko group -i <instance> grants folder:<folder>
arizuko secret -i <instance> delete <folder> SERVICE_TOKEN

# user-scoped (fallback for users who haven't logged in yet)
arizuko user-secret -i <instance> set <user_sub> KEY --value V
arizuko user-secret -i <instance> list <user_sub>
arizuko user-secret -i <instance> delete <user_sub> KEY

Replace the placeholders with your instance, folder and value. The list commands print key names and creation times, never values. The grants command shows the action and scope.

Logged-in users manage their own user secrets on the dashboard at /dash/me/secrets. The CLI is the fallback for values that the operator seeds.

Values are encrypted at rest

The secrets table stores values encrypted with AES-256-GCM. routd requires a SECRETS_KEY to seal and unseal them. SECRETS_KEY documents the key, the keyring and rotation.

A new value applies on the next use

A second set with the same key replaces the value and updates created_at. The next broker call uses the new value. The next turn receives new model keys and capability keys with explicit shell grants. Value and grant changes need no restart. A running turn keeps its existing environment until the turn ends.

Two things never happen

SECURITY.md has the full detail

[SECURITY.md § Secret injection] covers the doors, the audit rows and key rotation in full. [EXTENDING.md] shows how to add an OAuth provider. scopes shows the folder tree that drives resolution. MCP reference covers external REST tools that inject a capability token.