Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# Allowed Sandboxes Plan (`AGENTA_RUNNER_SANDBOXES_ALLOWLIST`)

Date: 2026-07-13

## Problem

The sandbox axis (`local`, `daytona`, planned `e2b`, proposed `docker` — see the
[docker-sandbox plan](../docker-sandbox/docker-sandbox-plan.md)) is currently gated by a
single boolean: `AGENTA_SANDBOX_LOCAL_ALLOWED`. That flag answers exactly one question
("may requests run on the runner host?") and cannot express the deployments we actually
have:

- **Self-hosted default**: everything allowed, zero config.
- **Hardened self-host**: `docker,daytona` but not `local`.
- **Cloud**: remote providers only — `local` and `docker` both off, at every layer, so
the UI never offers them, the API never accepts them, and the runner never runs them.

Each new provider would otherwise need its own `_ALLOWED` boolean and its own five-layer
plumbing. One comma-separated allowlist replaces the family.

## The variable

```text
AGENTA_RUNNER_SANDBOXES_ALLOWLIST=local,docker,daytona,e2b
```

- **Semantics**: the set of sandbox ids this deployment may provision. Unset or empty
means "all known providers" — self-host stays zero-config and permissive, matching
the posture of `AGENTA_SANDBOX_LOCAL_ALLOWED` defaulting to true.
- **Format**: comma-separated ids, whitespace-trimmed, case-insensitive, unknown ids
rejected at parse time with a warning naming the known set (parsing follows the
existing CSV conventions in `api/oss/src/utils/env.py` — `_load_csv_env_list` /
`_comma_set`).
- **Naming**: `_ALLOWLIST` matches `AGENTA_AGENT_MCPS_HOST_ALLOWLIST` and
`AGENTA_REDACTED_ALLOWLIST`; `AGENTA_RUNNER_*` matches the api-side `RunnerConfig`
namespace (`AGENTA_RUNNER_CONCURRENCY_LIMIT`), which is where the canonical
declaration lives. Considered and rejected: `SANDBOX_AGENT_PROVIDERS` (the
`SANDBOX_AGENT_*` prefix is the vendored daemon's namespace, and "provider" is
already taken by LLM providers across the codebase).
- **One name, all layers**: api, services, runner, and the web entrypoint all read the
same variable. No per-layer spellings; the browser-facing derivation
(`NEXT_PUBLIC_AGENTA_SANDBOXES_ALLOWLIST` in `__env.js`) is generated by
`web/entrypoint.sh`, never set by hand.

### Interplay with existing knobs

- **`AGENTA_SANDBOX_LOCAL_ALLOWED`** becomes a deprecated alias: the effective set is
the allowlist with `local` removed when the boolean is false. Both set and
conflicting → the more restrictive wins, with a boot warning. The boolean's
hardening warning in `RunnerConfig._warn_local_sandbox` moves to the allowlist
("allowlist includes `local` (default): local sandbox is not a tenant boundary…"),
and gains a sibling for `docker` once that provider lands.
- **`SANDBOX_AGENT_PROVIDER`** (singular) keeps its one job: the default sandbox id
when a request omits `sandbox`. It must be a member of the effective allowlist;
the runner fails requests (and warns at boot) when it is not — a deployment that
says "no local" with a default of `local` is a misconfiguration, not a fallback.
- **`AGENTA_SERVICES_CODE_SANDBOX_RUNNER`** (the code-evaluator selector,
`local`/`restricted`/`daytona`) is a separate axis and stays out of scope. Drive-by
found while auditing and already fixed on `big-agents`: the SDK registry (the actual
executor, `sdks/python/agenta/sdk/engines/running/runners/registry.py`) still
defaulted to `restricted` after the permissive-by-default pass set the documented
default to `local` everywhere else; the registry now matches.

## Enforcement: every layer, one policy

Defense in depth with a clear authority order. The runner is the hard gate (it is the
thing that executes); everything above it exists so users see honest options and fail
early with good errors.

### 1. Runner (authoritative, fail-closed)

- Parse once at module scope next to `KNOWN_SANDBOX_IDS`
(`services/runner/src/engines/sandbox_agent/provider.ts`): effective set =
`KNOWN_SANDBOX_IDS ∩ allowlist` (unknown allowlist entries warn and drop).
- Enforce in `buildRunPlan` (`run-plan.ts`), immediately after
`sandboxId = request.sandbox || SANDBOX_AGENT_PROVIDER || "local"` is resolved.
This is the earliest point that sees the fully-resolved id and it already returns
structured `{ok:false, error}` refusals (the filesystem/network/code-tool
"unsupported" gates are exact siblings) — rejecting here costs nothing and happens
before any side effects (temp dirs, secrets merge, mount signing). Enforcing only in
`buildSandboxProvider` would be too late: `acquireEnvironment` has already done
partial work that then needs rollback.
- `buildSandboxProvider` keeps its unconditional unknown-id refusal as the last line
of defense; it additionally checks the allowlist so the policy holds even for
callers that bypass `buildRunPlan` (tests, future engines).
- Note the resolution logic is duplicated in `session-identity.ts`
(`resolvesToLocalProvider`) and `server.ts` (`resolveKeepaliveProvider`); the
keep-alive dispatch must not park sessions for a disallowed provider — enforcing at
`buildRunPlan` covers this because a rejected run never reaches the pool, but a
regression test should pin it.

### 2. services/oss (producer gate, good errors)

`select_backend` (`services/oss/src/agent/app.py`) today special-cases
`sandbox == "local" and not sandbox_local_allowed()` → `LocalSandboxNotAllowedError`.
Generalize: resolve the effective allowlist (same env var, read directly in
`services/oss/src/agent/config.py`, which already mirrors the api env module because
services cannot import `api`) and raise a `SandboxNotAllowedError(sandbox_id)`
carrying the id and the allowed set. `LocalSandboxNotAllowedError` remains as a
subclass for compatibility with existing handlers
(`sdks/python/agenta/sdk/agents/errors.py`, re-raised in `handler.py`).

### 3. API (canonical config + commit-time validation)

- `RunnerConfig` (`api/oss/src/utils/env.py`) gains
`sandboxes_allowlist: set[str]` (CSV parse, default = all known ids), the
deprecation shim for `sandbox_local_allowed`, and the relocated hardening warning.
This stays the canonical declaration other layers mirror.
- Workflow revisions are not validated against the sandbox axis today (`sandbox` only
appears in the build-kit overlay, `api/oss/src/core/workflows/build_kit.py`).
Optional second slice: reject committing an agent revision whose `sandbox.kind` is
outside the allowlist, so users find out at save time, not first-run time. Runtime
enforcement does not depend on this.

### 4. Web (honest options)

- `web/entrypoint.sh` passes the allowlist through into `__env.js` (same mechanism as
the existing `NEXT_PUBLIC_AGENTA_SANDBOX_LOCAL_ENABLED` derivation — the value is
injected at container start, not baked at build time, because one image serves all
deployments).
- `web/packages/agenta-shared/src/api/env.ts` replaces `isSandboxLocalEnabled()` with
`enabledSandboxes(): Set<string>` (keeping the old helper as a wrapper during the
transition; note the duplicate implementation in
`web/oss/src/lib/helpers/dynamicEnv.ts` must move in the same change).
- The sandbox dropdown filter
(`web/packages/agenta-entity-ui/src/DrillInView/SchemaControls/agentTemplate/useModelHarness.tsx`)
swaps its `local`-only exclusion for set membership: options come from the SDK JSON
schema enum as today, filtered to the allowlist. A disabled provider is invisible,
not greyed out.

### 5. SDK (schema, not policy)

`_SandboxSchema.kind` (`sdks/python/agenta/sdk/utils/types.py`) stays the full
`Literal` of ids that exist (`local`, `daytona`, plus `docker`/`e2b` as they land) —
the schema describes what the platform knows, the allowlist describes what a
deployment permits. The SDK performs no allowlist enforcement of its own: it cannot
know the deployment's policy, and the services/runner gates give the real answer with
a proper error. (Deployments that want the schema to shrink too can revisit this once
the schema endpoint can consult `env`; not needed for correctness.)

### 6. Hosting

- Add `AGENTA_RUNNER_SANDBOXES_ALLOWLIST=` to the "Agenta - Sandbox (agent runner)"
section of all four env example files
(`hosting/docker-compose/{oss,ee}/env.*.example`), bare line, no comment (per
env-example conventions; explanation goes in the self-host configuration docs).
- Pass it explicitly in the runner service `environment:` block (next to
`SANDBOX_AGENT_PROVIDER`) in every compose file that enumerates runner vars; api,
services, and web pick it up via their existing `env_file`/enumerated blocks.
- Helm: `agenta.sandboxesAllowlist` in `values.schema.json` (string, comma-separated,
description documenting known ids) + `values.yaml` default + a `hasKey`-guarded
emission in `templates/_helpers.tpl` next to the existing `sandboxLocalAllowed`
block — emitted for the api, services, runner, and web containers.
- Cloud deployments set `AGENTA_RUNNER_SANDBOXES_ALLOWLIST=daytona` (later
`daytona,e2b`); self-host leaves it unset.

The full deployment-surface checklist — none of these are optional, and the
permissive-by-default hardening pass for `AGENTA_SANDBOX_LOCAL_ALLOWED` is the file-set
template (it touched exactly this set):

- env example files: `hosting/docker-compose/{oss,ee}/env.oss.dev.example`,
`env.oss.gh.example`, `env.ee.dev.example`, `env.ee.gh.example`
- the compose files that enumerate runner env vars
(`hosting/docker-compose/{oss,ee}/docker-compose.*.yml`)
- helm: `hosting/kubernetes/helm/values.schema.json`, `values.yaml`,
`templates/_helpers.tpl`, and the values example
`hosting/kubernetes/ee/values.ee.example.yaml`
- self-host configuration MDX: `docs/docs/self-host/02-configuration.mdx` — both the
env-var ↔ compose ↔ helm mapping table and the "Agent sandbox" warning block (which
documents `AGENTA_SANDBOX_LOCAL_ALLOWED` today and must present the allowlist as its
successor, including the alias/precedence rules)

## Failure modes and messages

- Request for a disallowed id → 4xx-shaped structured error naming the id and the
allowed set, from services/oss when it gates first, from the runner's
`buildRunPlan` refusal otherwise. Never a silent fallback to another provider.
- Default provider not in the allowlist → boot warning at runner start + per-request
refusal for requests that relied on the default (explicit `sandbox` values in the
allowlist keep working).
- Allowlist contains only unknown ids → effective set is empty; the runner refuses
every run with a message pointing at the variable. Loud beats guessing.
- Existing revisions referencing a now-disallowed provider keep failing at run time
with the structured error (same behavior `AGENTA_SANDBOX_LOCAL_ALLOWED=false` has
today for local-pinned revisions).

## Testing

- Runner unit: allowlist parse (empty/unset/garbage/mixed-case), `buildRunPlan`
refusal, default-provider-not-allowed, keep-alive never parks a disallowed id.
- services/oss unit: `select_backend` generalized gate + error subclass compat.
- API unit: `RunnerConfig` parse, boolean-alias precedence (restrictive wins), warning
relocation.
- Web: dropdown filter against injected `__env.js` values (acceptance covers the
cloud shape: `daytona`-only shows no local/docker options).
- Acceptance in both editions (ungated-endpoint convention): request a disallowed
sandbox → structured error; allowed → runs.

## Slices

1. API `RunnerConfig` allowlist + deprecation shim + services/oss generalized gate.
2. Runner `buildRunPlan` enforcement + `buildSandboxProvider` backstop + tests.
3. Web entrypoint/env helper/dropdown filter.
4. Hosting + docs: the full deployment-surface checklist in "6. Hosting" above (env
examples, compose blocks, helm schema/values/helpers/values example, self-host
configuration MDX).
5. (Optional) commit-time revision validation in the API.

## Non-goals

- Not a per-project or per-plan entitlement — this is a deployment-level operator
switch. Plan-gated sandbox access (cloud tiers) would be an entitlements concern
layered separately, not mixed into this permission-style check.
- Does not touch the code-evaluator runner selector
(`AGENTA_SERVICES_CODE_SANDBOX_RUNNER`).
- Does not add or remove providers; it only gates ids that
`KNOWN_SANDBOX_IDS` / the SDK schema already define.
Loading
Loading