# Environments

A named execution context binding one sandbox + a house-secret scoping policy into something a thread can reference, durable and inspectable within a house. `threads.environment_id` carries an FK with `on delete set null`, and the thread row keeps a snapshot of the environment — so later env edits or teardown don't rewrite a thread's history.

```
environments {
  id, house_id (FK cascade), author_id, name, sandbox_id (required at row level),
  secrets: jsonb "all" | {only: [names]}               # which house secrets the box sees; default "all"
}
```

`POST /api/environments` with `sandbox_id` omitted **auto-provisions** a box on the env's `runtime`. Daytona creates a fresh house-scoped sandbox via `DAYTONA_API_KEY`; `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](../access/secrets.md) for resolution. Sandbox pi defaults to `DEFAULT_BOT_MODEL` ([llm-keys](../access/llm-keys.md)) — 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 `sandbox_id` omitted |
| `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`):

```sh
arbe env list [--json]
arbe env view <name-or-id>                 # name (case-insensitive) or UUID
arbe env create <name> [--sandbox <id>] [--only-secret NAME]   # default: all house secrets
arbe env secrets <name|id>                                     # show the policy
arbe env secrets <name|id> --only NAME [--only NAME2]          # restrict to a subset (replaces the set)
arbe env secrets <name|id> --all                               # back to the default
arbe env delete <name|id>
```

`--only-secret` / `--only` are repeatable; omitting them means every house secret is available. Dispatch with `--env`: `arbe --env work thread entries create <ref> "fix the bug"` — `--env` is a root-level option that resolves the environment by name or ID and uses its `sandbox_id`, overriding any `-s` flag. 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/threads.ts` owns re-resolution.

Code: `packages/core/schemas/environment.ts` (`EnvironmentRowSchema`), `packages/core/environments.ts`, `apps/www/src/routes/api/environments/`.<br>
See [daytona runtime](sandbox-daytona.md), [system/secrets](../access/secrets.md), [system/dispatch](../chat/dispatch.md), [runtime](runtime.md).
