Secrets
Encrypted credentials stored in Supabase Vault, scoped to a house. Storing one is the whole job: at dispatch every house secret is decrypted and exported as an env var into the sandbox, for run_command and for the coding agent behind delegate_task. An environment can restrict its boxes to a named subset (secrets: { only: [...] }, see environments).
secrets { house_id, name, # UNIQUE(house_id, name) where deleted_at is null author_id, # who created it; an owner or this author may rotate or delete vault_secret_id, # encrypted value lives in Vault, never in the table funded_by, provider_key_hash # 'arbe' + key hash when arbe minted it (LLM keys) deleted_at # soft delete}Secrets belong to the house, not the person. Any member can use any secret in the house, and a name means one value, whoever created it. author_id gates nothing but rotation and deletion. Names are immutable: set on an existing name rotates the value, and renaming is delete and recreate.
What reaches a sandbox. A bot with run_command can read the box’s entire environment with a bare env, so the injected set is the blast radius. buildBoxEnv (packages/core/dispatch/sandbox-shared.ts) is the one place deciding it, with two rules:
- Every house secret by default, or the environment’s
onlysubset. - House tier only, including arbe-funded keys. Operator infrastructure keys never enter a box.
That means the house’s model key, BYOK or arbe-funded, is readable inside the sandbox by design; the funded key is capped, so this is accepted rather than proxied (LLM keys). A house without a model key gets No model key for this house. Add OPENROUTER_API_KEY under house secrets. A name in an only list with no secret behind it is left out and reported by arbe env diagnose; dispatch never refuses over it.
arbe secret list # names, rotation age, funding provenancearbe secret set <name> # upsert from stdin; same name = rotatearbe secret delete <name> # soft deleteecho "sk-or-..." | arbe secret set OPENROUTER_API_KEYGitHub. You don’t need to know the secret name. arbe connection create github (or Settings → Runtime → Connections) stores the token as GITHUB_TOKEN and also signs git and gh in inside every box (packages/core/dispatch/connection-env.ts).
Nothing ever returns a value. Values are read from stdin, never argv. The API (GET/POST /api/secrets, GET/DELETE /api/secrets/:id, PUT /api/secrets/:id/value) and arbe secret view are metadata only. Decryption happens only in resolve_secrets_for_scope, a service-role Postgres function the backstage calls in-process during a turn it already holds the fence for.
Storage. Supabase Vault (pgsodium AEAD, Supabase-managed key). Soft-delete keeps the metadata row and hard-deletes the Vault entry; a trigger cascades when a house is soft-deleted.
Code: packages/core/schemas/secret.ts, apps/www/src/routes/api/secrets/. See environments, dispatch, auth.