Environments
A named execution context — a runtime plus a house-secret scoping policy — that a thread can reference, durable and inspectable within a house. The sandbox is provisioned from it and recorded in sandboxes; a thread points at the environment with threads.environment_id, an FK with on delete set null. The environment is not copied onto the thread, so editing or deleting it leaves the thread’s stream — the history — untouched.
environments { id, house_id (FK cascade), author_id, name, config: jsonb, # optional coding-agent instructions/model secrets: jsonb "all" | {only: [names]}, # which house secrets the box sees; default "all" runtime, deleted_at, created_at, updated_at}
sandboxes { id, house_id, environment_id, # the env→box link lives here, not on environments provider, provider_ref, status, ephemeral, ...}POST /api/environments takes an optional sandbox_id in the request body — an external box ref to adopt instead of provisioning one. With it omitted, the route auto-provisions a box on the env’s runtime: Daytona creates a fresh house-scoped sandbox via DAYTONA_API_KEY and the row is written to sandboxes with environment_id pointing back. delegate_task can run in that box or create one and attach it to the parent thread for later work. The secrets policy stores names at most, never values — see secrets for resolution. Sandbox pi defaults to DEFAULT_BOT_MODEL (llm-keys) — a house OPENROUTER_API_KEY reaches the box by default. Provider-specific keys (ANTHROPIC_API_KEY, OPENAI_API_KEY, …) still work paired with a matching thread.config.model override.
| Endpoint | Auth | Notes |
|---|---|---|
GET /api/environments?house_id=<uuid> | house member | list |
GET /api/environments/<id> | house member | fetch one |
POST /api/environments | member-write | auto-provisions on the runtime (daytona default) when the body omits sandbox_id |
GET /api/environments/<id>/diagnose | house member | readiness gaps for dispatch (arbe env diagnose) |
PATCH /api/environments/<id> | member-write | partial update |
DELETE /api/environments/<id> | owner-only | soft delete |
Every house secret injects into the box by default (secrets: 'all'); a restricted env (secrets: { only: [...] }) injects that subset and nothing else. buildBoxEnv (packages/core/dispatch/sandbox-shared.ts) resolves names to values and the launch path exports them as env vars. A private repo needs the house connected to GitHub (Settings → Runtime → Connections, or arbe connection create github); there is no GitHub App yet.
A GitHub connection is the GITHUB_TOKEN house secret, and buildBoxEnv turns it into a signed-in box (packages/core/dispatch/connection-env.ts): it derives GH_TOKEN (for gh, which prefers it) and GIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0 — an env-only credential helper (git >= 2.31) scoped to https://github.com, so the token is offered to that host and no other, and read from $GITHUB_TOKEN at call time rather than baked into a config string. The box also gets a git identity: GIT_AUTHOR_*/GIT_COMMITTER_* name the agent by handle with a non-routable <handle>@agents.arbe.invalid email, so commits show the agent as author and the token’s owner as pusher; the prompt asks the agent to add a Co-authored-by trailer for the human who set it to work. Nothing is persisted on the box, so rotating or removing the connection takes effect on the next run. Private git clone works in both run_command and delegate_task. A house that sets GH_TOKEN, a GIT_CONFIG_* or a GIT_AUTHOR_*/GIT_COMMITTER_* name itself keeps its own value.
CLI (all commands operate on the active house, set via arbe house select):
arbe env list [--json]arbe env view <name-or-id> # name (case-insensitive) or UUIDarbe env create <name> [--sandbox <id>] [--only-secret NAME] # default: all house secretsarbe env use <name-or-id> # set the active env for this house (--clear to unset)arbe env secrets <name|id> # show the policyarbe env secrets <name|id> --only NAME [--only NAME2] # restrict to a subset (replaces the set)arbe env secrets <name|id> --all # back to the defaultarbe env diagnose <name-or-id> [--model provider/model] # is this env ready to dispatch?arbe env delete <name|id>--only-secret / --only are repeatable; omitting them means every house secret is available. Bind a thread to an environment at creation: arbe thread create <parent> --environment <env-id> (it takes an id, and falls back to the active env from arbe env use when that env belongs to the same house), then arbe thread entries create <ref> "fix the bug" to dispatch. To run a one-off command in an environment’s live box without hunting for its provider id, arbe x -e <environment> -- <cmd>. Sandboxes are created once and reused across dispatches.
Stale thread sandbox pointers
A thread may keep reaching an old sandbox after its environment changes or is deleted. threads.environment_id is the current binding; threads.sandbox_id caches the resolved box. Compare that sandbox row’s environment_id with the thread’s. A mismatch confirms a stale pointer.
Clearing threads.sandbox_id makes the next turn resolve the current environment’s live sandbox. This is a direct production write: do it freely only in a test house; otherwise ask first. ensureThreadSandbox in packages/core/dispatch/sandbox-access.ts owns re-resolution.
Code: packages/core/schemas/environment.ts (EnvironmentRowSchema), packages/core/environments.ts, apps/www/src/routes/api/environments/.
See daytona runtime, system/secrets, system/dispatch, runtime.