Vault Integration¶
Problem¶
terok bind-mounts vendor config directories into task containers. A prompt-injected or supply-chain-compromised agent can read and exfiltrate API keys, OAuth tokens, or SSH private keys from these shared mounts.
Solution: Token Broker¶
No real secret enters a task container. Instead:
- Credential DB (host-side) stores API keys and OAuth tokens
- Token broker (terok-sandbox) resolves phantom tokens to real credentials and forwards requests upstream. It runs inside each container's supervisor (spawned by the terok-sandbox OCI hook) and listens on a Unix socket (default) or a per-container host TCP port (legacy)
- Per-provider phantom tokens (per-task, per-provider) are what containers see
- SSH signer (terok-sandbox)
lets containers sign with host-side SSH keys without the private keys
entering the container — via the bind-mounted signer socket
(
TEROK_SSH_SIGNER_SOCKET) or, in TCP mode, a per-container port (TEROK_SSH_SIGNER_PORT)
Architecture¶
HOST CONTAINER
------------------------------- -----------------------------------
Credential DB (encrypted SQLite, Per-provider phantom tokens (env vars)
SQLCipher) ANTHROPIC_API_KEY=<anthropic-phantom>
credentials table MISTRAL_API_KEY=<mistral-phantom>
proxy_tokens table GH_TOKEN=<github-phantom>
(token, scope, subject, GITLAB_TOKEN=<gitlab-phantom>
credential_set, provider)
Agent / tool makes API request
Token Broker (terok-sandbox, with phantom token in auth header
per-container supervisor)
Unix socket (default) or TCP listener Routing is by token, not URL path.
Resolves phantom → real credential Token encodes which provider it's for.
Injects auth header, forwards
to upstream over TLS
SELinux considerations¶
SELinux blocks connect() on host Unix sockets mounted into rootless
Podman containers (container_t -> unconfined_t denied). terok-sandbox
ships a policy module (terok_socket.te, installed by setup) that
allows exactly this connect. Where the module cannot be installed, the
containers reach the token broker via TCP
(host.containers.internal:<port>) instead; terok-shield
allows the broker port through the nftables firewall via its per-container
loopback_ports state.
Per-provider phantom token routing¶
Each routed provider gets its own phantom token, minted at container launch:
tokens = {
name: db.create_token(scope, task_id, credential_set, name)
for name in routed_providers
}
The token broker resolves the route from the token's provider field, not from
the URL path. This is essential because some SDKs (Vibe's Mistral SDK,
gh CLI) strip or ignore URL path components.
Per-Agent Traffic Routing¶
Inside the container, HTTP clients always reach the vault at
http://localhost:9419 — an in-container loopback bridge that runs in
both transports and forwards to the host socket (socket mode) or the
per-container broker port (TCP mode). Different agents are pointed at
it in different ways, depending on what their SDK supports:
| Agent | How it reaches the token broker | Notes |
|---|---|---|
| Claude | ANTHROPIC_BASE_URL=http://localhost:9419 (+ ANTHROPIC_UNIX_SOCKET pointing at the vault socket) |
Anthropic SDK respects these env vars |
| Codex | Shared ~/.codex/config.toml rewrite (openai_base_url, chatgpt_base_url) |
Codex's built-in first-party auth is file/config based, so terok patches the shared Codex config instead of relying on env vars. openai_base_url is keyed by stored credential type: OAuth → {vault_url}/backend-api/codex, API key → {vault_url}/v1 |
| Vibe | config.toml with api_base (+ api_key_env_var) in shared ~/.vibe mount |
Mistral SDK ignores the URL path in api_base, only uses host:port. Written by the provider.config_patch in YAML |
| Blablador / KISSKI / OpenRouter | TEROK_OC_<NAME>_BASE_URL env var override |
The OpenCode wrapper reads this; computed at launch as the vault URL plus the provider's served path (e.g. /v1, /api/v1 for OpenRouter) |
| gh | http_unix_socket in ~/.config/gh/config.yml |
gh routes ALL API traffic through a Unix socket. See below. |
| glab | GITLAB_API_HOST + API_PROTOCOL=http env vars |
glab sends to http://<api_host>/api/v4/...; the host is localhost:9419 in socket mode, host.containers.internal:<port> in TCP mode |
| CodeRabbit | Real API key via sidecar env_map |
CLI has no base URL override, so token broker routing is not possible. The sidecar receives the real key directly from the credential DB. |
| SonarCloud | SONAR_HOST_URL + SONAR_TOKEN phantom env |
Tool; scanner uses the host URL override |
In addition, every routed provider with a serves: protocol map is
materialized as a generic handle — TEROK_PROVIDER_<NAME>_TOKEN plus
TEROK_PROVIDER_<NAME>_BASE_<PROTOCOL> — so harnesses (OpenCode, Pi)
can select any authenticated, protocol-compatible provider at runtime
via --provider.
gh: Unix-socket pattern¶
gh has no env var for base URL. It supports http_unix_socket in its
config file, which routes all API traffic through a Unix socket.
The config-patch mechanism writes the vault socket path into
~/.config/gh/config.yml after terok-executor auth gh — the
{vault_socket} template token, resolved per transport mode:
- socket mode:
/run/terok/vault.sock— the supervisor's vault socket, bind-mounted into the container. - TCP mode:
/tmp/terok-vault.sock— a socat bridge started by terok-sandbox'sensure-bridges.sh, forwarding tohost.containers.internal:${TEROK_TOKEN_BROKER_PORT}.
Either way gh's API traffic reaches the vault token broker, which substitutes the phantom token before forwarding upstream.
YAML-driven config patches¶
Agents that need config file changes (not just env vars) declare a
config_patch under their provider: binding:
# Vibe: TOML patch
provider:
config_patch:
file: config.toml
toml_table: providers
toml_match: {name: mistral}
toml_set:
api_base: "{vault_url}/v1"
api_key_env_var: MISTRAL_API_KEY
# gh: YAML patch
provider:
config_patch:
file: config.yml
yaml_set: {http_unix_socket: "{vault_socket}"}
The patch is applied after auth and reconciled on every task launch. Only
non-secret values (URLs, socket paths) are written to shared mounts. The
patcher also writes a .terok-managed-config.json sidecar beside the config
file so callers can later disable a provider and remove only values still
owned by terok; user-edited values are preserved.
YAML Registry: Providers and Bindings¶
Vault integration is split across two YAML files. The endpoint
half lives in resources/providers/<name>.yaml — where requests go
and how the real credential is attached:
# resources/providers/anthropic.yaml
upstream: https://api.anthropic.com
auth: # wire auth per credential type
oauth:
header: Authorization
prefix: "Bearer "
extra_headers: {anthropic-beta: oauth-2025-04-20}
api_key:
header: x-api-key
prefix: ""
oauth_refresh: {token_url: ..., client_id: ...} # optional
serves: # protocol → served path (harness selection)
anthropic-messages: ""
The delivery half is the agent's provider: binding in
resources/agents/<name>.yaml:
# resources/agents/claude.yaml
provider:
default: anthropic # binds the agent to one provider
token_env: # phantom-token env var, keyed by credential type
oauth: CLAUDE_CODE_OAUTH_TOKEN
_default: ANTHROPIC_API_KEY # fallback for any non-OAuth credential
base_url_env: ANTHROPIC_BASE_URL # optional: env var for the vault URL
socket_env: ANTHROPIC_UNIX_SOCKET # optional: env var for the vault socket
credential_file: .credentials.json
credential_type: oauth
config_patch: ... # optional: file patch for the vault address
A provider maps to exactly one vault route: the roster loader rejects
a provider bound by more than one agent. routes.json (regenerated
by terok-executor vault routes and by setup) carries one entry per
provider, keyed by its clean name — which is also what keeps a
provider routable for harnesses even when no agent binds it (the
curated OpenCode providers declare their endpoint plus an opencode:
block and have no agent YAML at all).
Agent-specific settings not in YAML¶
-
OpenCode base URL override: For Blablador, KISSKI, and OpenRouter, the environment builder sets
TEROK_OC_<NAME>_BASE_URLwhen the vault is active. This is computed at container launch, not declared in YAML. -
glab env vars:
GITLAB_API_HOSTandAPI_PROTOCOL=httpare injected by the environment builder for glab specifically. glab has no YAML field for this because it's a routing concern, not a credential concern. -
socat bridges: terok-sandbox's
ensure-bridges.shruns in-container. In socket mode the broker socket is bind-mounted at/run/terok/vault.sockand the bridge fronts it as TCP onlocalhost:9419for HTTP-only clients. In TCP mode the bridge is reversed: it presents/tmp/terok-vault.sockfor socket-only clients and forwardslocalhost:9419to the per-container host port.
Auth Flow¶
Three auth paths¶
1. OAuth / interactive login (Claude, Codex, gh):
Launches a container with the vendor CLI and a temporary config
directory. After exit, the extractor captures the OAuth token to the
DB. Codex additionally offers a headless device-code variant
(--device-auth).
2. API key -- interactive prompt (Vibe, Blablador, KISSKI, OpenRouter, glab, CodeRabbit, SonarCloud): Prompts for an API key on the terminal. No container needed.
3. API key -- non-interactive (any provider with an auth flow):
terok-executor auth <provider> --api-key <key>
Credentials are stored keyed by the entry's provider (claude →
anthropic, gh → github), matching the vault route names.
Post-auth config patching¶
After storing credentials, write_vault_config() applies any
provider.config_patch from the YAML registry. This writes vault
addresses (not secrets) to the entry's shared config mount. Task launch
re-applies enabled patches and removes disabled provider patches using
the managed sidecar (.terok-managed-config.json), which keeps global
shared config directories from retaining stale vault routing after a
feature mode changes.
Per-Provider Credential Extractors¶
| Entry | File | Key fields |
|---|---|---|
| Claude | .credentials.json (fallback: config.json) |
access_token, refresh_token (fallback: api_key) |
| Codex | auth.json |
access_token, refresh_token, id_token, account_id |
| Vibe | .env |
key (MISTRAL_API_KEY) |
| Blablador | config.json |
key (api_key) |
| KISSKI | config.json |
key (api_key) |
| gh | hosts.yml |
token (oauth_token) |
| glab | config.yml |
token (per-host) |
| OpenRouter | — | API key via prompt/--api-key |
| CodeRabbit | — | API key via prompt/--api-key |
| SonarCloud | — | API key via prompt/--api-key |
Limitations¶
- Claude: OAuth logins do not support full routing. You can either expose the real token to the agent to get the full Claude CLI user experience, or route what's officially routable, but miss out on some subscription features.
- Codex: ChatGPT/backend-api and realtime websocket traffic are routed through the token broker, but any newly added Codex-specific upstream surfaces still need explicit vault route coverage.
- Copilot: Not proxied yet — no
provider:binding in its YAML, so no vault route and noauthflow.
Package Boundaries¶
- terok-sandbox: Credential DB, token broker (HTTP/WebSocket forwarding, phantom token resolution, OAuth token refresh, SSH signer), socket/TCP listeners, per-container supervisor lifecycle
- terok-executor (this package): YAML agent + provider registry, credential extractors, auth CLI, canonical container environment assembly (phantom env vars, base URL overrides, config patches)
- terok: Multi-task orchestration on top — delegates env assembly here and layers per-project credential sets and OAuth exposure tiers