secrets
Scopes showed the folder tree that gates who can see whom. Secrets live in that same tree — the credentials an agent needs to reach out (a GitHub token, an API key), folder- or user-scoped, with one hard rule: the agent uses them without ever seeing the plaintext in its prompt. arizuko stores secrets in the secrets table in routd.db and hands them to the right caller at the right moment — either as container env at spawn, or as a tool-call argument on the host. The split between those two delivery paths is the whole story, and it’s what keeps a leaked transcript from leaking a key.
secrets table, the scope model, and the operator CLI are shipped. Values are encrypted at rest — routd requires a SECRETS_KEY (see SECRETS_KEY). The two delivery paths below describe the design, not current behavior: today the container spawn injects only ANTHROPIC_API_KEY and CLAUDE_CODE_OAUTH_TOKEN from host env, and the tool-call broker is wired but resolves no secrets yet (spec 7/Y).two scopes
Every secret row carries a scope_kind:
- folder — tied to a group path like
atlas/eng. Owned by the operator; shared by every user that interacts with that folder. - user — tied to one
auth_users.sub. Owned by the user (typed into/dash/me/secrets) or seeded by the operator as a fallback.
The primary key is (scope_kind, scope_id, key). Keys are uppercase env-style ids: ^[A-Z][A-Z0-9_]*$.
folder secrets reach the container
At spawn time, container/runner.go resolves the folder's secrets, merges them with the base env, and writes the map into the container as env vars. The agent process inside reads them like any other env var.
Resolution walks from the folder up to a synthetic root row, deepest wins. So atlas/eng/sre sees its own keys overlaid on atlas/eng, on atlas, and on root. A key set at atlas reaches every child folder under atlas/ unless a child overrides it.
user secrets stay on the host
User-scoped secrets never enter the container. They are resolved at tool-call time inside the host MCP dispatch chain. When a tool declares requires_secrets: ["GITHUB_TOKEN"], routd looks up the calling user’s value and passes it as an argument to the handler running in the host process. The handler makes the outbound HTTP call. The agent sees only the handler’s response.
This is why a leaked agent transcript can’t leak a user’s GitHub token — the token was never in the prompt and never in the container env.
operator CLI
# folder-scoped
arizuko secret <instance> set <folder> KEY --value V
arizuko secret <instance> list <folder>
arizuko secret <instance> delete <folder> KEY
# user-scoped (fallback for users who haven't logged in yet)
arizuko user-secret <instance> set <user_sub> KEY --value V
arizuko user-secret <instance> list <user_sub>
arizuko user-secret <instance> delete <user_sub> KEY
Logged-in users manage their own user secrets through the dashboard at /dash/me/secrets — the CLI is the operator fallback for seeded values.
storage at rest
Secret values are encrypted at rest in the secrets table (AES-256-GCM). routd requires a SECRETS_KEY to seal and unseal them. The key, the keyring, and rotation are documented in SECRETS_KEY.
rotation
A second set with the same key upserts the value and bumps created_at. The next container spawn picks up the new folder value; the next broker call picks up the new user value. There is no restart-the-world step — the old value lives in the env of running containers until the agent process restarts.
what doesn’t happen
- No plaintext secret in audit logs. The
secret_use_logtable records key, scope, status, and latency — never the value. - No user secret in container env. Per-user values reach only the host-side broker.
- No skill or agent prompt can enumerate the table. Tools declare the keys they need; the broker resolves only those.
go deeper
Full broker design, threat model, and the tool-descriptor field: specs/7/Y. Where secrets sit in the trust map: SECURITY.md. The folder hierarchy that drives resolution: scopes.