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.
| Capability | Helper | Default |
|---|---|---|
custom_tools: list[Tool] | None | env_custom_tools(env) | None (use default tool set) |
run_tests() -> TestResult | live.test.run_live_test | no-op (TestResult()) |
agent_user: str | env_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/
| Directory | Sandbox | Notes |
|---|---|---|
text_only/ | local (no sandbox) | Tasks and scorers defined inline |
qemu/ | qemu | Full VM, SSH access |
microvm/ | firecracker | Requires /dev/kvm |
docker_in_vm/ | docker_in_vm | QEMU 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.
| Backend | File | Registered as | Config type |
|---|---|---|---|
| Docker Compose | sandbox/docker.py | "lb-docker" | compose file path (str) |
| QEMU | sandbox/qemu.py | "qemu" | QEMUConfig |
| Firecracker | sandbox/firecracker.py | "firecracker" | FirecrackerConfig |
| Docker-in-VM | sandbox/docker_in_vm.py | "docker_in_vm" | DockerInVMConfig |