Environment Architecture

How environments are defined and discovered, and which sandbox backends run them.


1. ControlEnvironment Protocol

File: src/control_tower/environments/protocol.py

@runtime_checkable class ControlEnvironment(Protocol): @property def id(self) -> str: ... @property def display_name(self) -> str: ... def docs(self, args: BaseModelPython | None = None) -> EnvDocs: ... def get_sandbox_spec(self, codebase_name: str = "default", args: BaseModelPython | None = None) -> SandboxEnvironmentSpec: ... def task_space(self, args: BaseModelPython | None = None) -> TaskSpace: ...

It is a structural Protocol: any class with matching attributes satisfies it without inheriting from anything.

EnvDocs (also in protocol.py) groups documentation metadata: description, attack_surface, short_description, tech_stack, services.

task_space(args) returns a TaskSpace (see src/control_tower/task/space.py) describing the valid (main, side) pairings for the env, built for one run's --env-arg values. Two shapes cover every env: CrossProduct pairs every main with every side, plus honest combos, and is used by folder-backed envs, BashArena, and demos. ExplicitSpace is an enumerated list, used by sandbox_escape, where each scenario side task is pinned to one main.

Narrowing main×side pairings

A side task that only makes sense against some mains declares this with compatible_main_tasks / incompatible_main_tasks (see making-side-tasks), and CrossProduct reads it through SideTask.accepts_main. Since every combination resolver uses env.task_space(), incompatible attack pairs are skipped in ct run eval --all, explicit cross products, the side-only main-picker, and task files. Honest combos are unaffected.

Validation

Environment validation checks the objects returned by setting discovery: their documentation, task definitions, scorer interfaces, task pairings, and declared Compose variants. Registration does not require a codebase/ tree, restart.sh, or one directory per task. The setting owns loading its chosen source format and reporting failures; unrelated directories beside registered environments are not treated as missing registrations.

Optional capabilities

Environments may also expose any of the following; consumers read them with hasattr/getattr or the helpers in protocol.py.

CapabilityHelperDefault
custom_tools: list[Tool] | Noneenv_custom_tools(env)None (use default tool set)
run_tests() -> TestResultlive.test.run_live_testno-op (TestResult())
agent_user: strenv_agent_user(env)"root"

DockerEnvironment defines all three. Demo envs (text-only, QEMU, microvm, docker-in-vm) omit them to illustrate the minimal surface.


2. Implementations

2.1 DockerEnvironment

File: src/control_tower/environments/docker_environment.py

A Pydantic BaseModel that satisfies ControlEnvironment. Used by all standard linuxarena environments. Key fields: path, id, compose_paths, agent_user, internet_simulator.

get_sandbox_spec(codebase_name, args) returns SandboxEnvironmentSpec(type="lb-docker", config=str(compose_path)).

make_environment_config_from_path() is the factory that every linuxarena env __init__.py calls.

2.2 Demo Environments

Directory: .settings/demos/

DirectorySandboxNotes
text_only/local (no sandbox)Tasks and scorers defined inline
qemu/qemuFull VM, SSH access
microvm/firecrackerRequires /dev/kvm
docker_in_vm/docker_in_vmQEMU VM with Docker inside

2.3 sandbox_escape

Directory: .settings/sandbox_escape/

A port of the sandbox_escape_bench paper. Uses SandboxEnvironmentSpec(type="vagrant", ...) via the inspect-vagrant-sandbox plugin. Generates 12 environment variants programmatically from 18 container/kernel escape scenarios.


3. Setting Layout

.settings/
  linuxarena/          ← setting (pulled via ct settings pull, gitignored)
    web_scraping/
    clinical_trial/
    ...
  demos/               ← setting
    text_only/
    qemu/
    microvm/
    docker_in_vm/
  sandbox_escape/      ← setting with custom setting.py (programmatic factory)
    setting.py

Each directory under .settings/ is a setting. By default, each subdirectory with an __init__.py is treated as an environment — no setting-level file is needed. Settings that need custom discovery (like sandbox_escape, which generates environment variants programmatically) provide a setting.py at the setting root with get_environments() -> dict[str, ControlEnvironment]. Installed packages may instead expose that function through the control_tower.settings entry-point group; see Porting Settings.

src/control_tower/environments/all_environments.py discovers settings that are safe to import in-process and populates SETTINGS and ENVIRONMENTS. Pulled setting projects that declare their own Control Tower dependency are enumerated and validated in their own uv environments instead; evals use the same setting-owned worker boundary.


4. Sandbox Backends

inspect-ai's SandboxEnvironment ABC is the primitive. Implementations register via @sandboxenv(name=...) and are looked up by name string at runtime.

BackendFileRegistered asConfig type
Docker Composesandbox/docker.py"lb-docker"compose file path (str)
QEMUsandbox/qemu.py"qemu"QEMUConfig
Firecrackersandbox/firecracker.py"firecracker"FirecrackerConfig
Docker-in-VMsandbox/docker_in_vm.py"docker_in_vm"DockerInVMConfig