Run model-generated Python in a locked-down container
Offline tests: Python 3.11+, standard library.
Live tests: Docker with Linux containers (Docker Desktop or Docker Engine) and internet access
once, to pull the pinned Python image (about 45 MB compressed).

Reference solution: not included here, so the exercise does not hand you the answer.
Attempt the repair first. Then download agent-code-sandbox-solution.zip from the project card
on https://softwaresecurity.com/labs/#project-agent-code-sandbox and extract it into this
folder; it adds reference.py without changing your files, and the reference commands below
need it.

Goal
An agent has a "run Python" tool. The model writes code to analyse files the user attached,
and any page, email or document the model read may have steered what it wrote, so the code
is untrusted. exercise.py runs each piece of code in a fresh container and returns stdout,
stderr and output files to the model. runner.py runs inside the container and does not need
changes. The starter already pins the image by digest and runs as a non-root user with a
read-only root, no capabilities and no-new-privileges, but:
- run_generated() runs the code on the host when Docker is missing, where it can read the
  agent's environment (API keys) and files;
- sandbox_argv() shares the job folder with the host, and collect_outputs() follows any link
  the code leaves in out/, so the code can hand back any file the agent can read;
- the container is on the network, so the code can send what it finds anywhere;
- there is no limit on memory, swap, CPU or processes;
- run_with_deadline() kills the docker client at the deadline, but the container keeps running.

1. See the gaps offline [OFFLINE: Python only]
   python3 -m unittest -v test_sandbox.py
   Expected starter: 6 failures in 13 tests. fake_docker.py stands in for the docker CLI
   and never runs the job's code. Like real Docker, it runs the "container" as a separate
   process, so killing the client does not stop it.
   Reference: EXERCISE_MODULE=reference python3 -m unittest -v test_sandbox.py

2. Repair exercise.py
   Raise SandboxUnavailable instead of falling back to the host. Mount nothing: send the
   input files in on stdin (build_job already does) and let the runner return output files
   in its envelope ("collect_outputs": true), then check every name and size in
   parse_result(), since the code can forge the envelope. Add --network=none, --pids-limit,
   --memory with an equal --memory-swap and --cpus, and kill the container by name at the
   deadline. Run step 1 again.

3. Pull the pinned image [LIVE: Docker and internet access]
   docker pull python:3.14.8-slim-trixie@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170
   The digest is the multi-platform index, so the same reference works on amd64 and arm64.
   Move it to a newer Python patch release when one ships; the tests do not depend on it.

4. Run the live tests [LIVE: Docker; no internet needed]
   EXERCISE_MODULE=reference python3 -m unittest -v test_live.py
   Then the same with your exercise.py (EXERCISE_MODULE=exercise, the default).
   Legitimate code must still work:
   - code that reads an attached CSV, prints JSON and writes out/summary.csv;
   - three million loop iterations and a 32 MB buffer;
   - a bug (ZeroDivisionError) comes back as exit status 1 with the traceback.
   Escape attempts must fail:
   - a TCP connection to a canary server on the host: nothing arrives. A control run of
     the same code with networking on does reach the canary, so the test can fail;
   - reading a variable planted in the host environment, and looking for the Docker
     socket, cloud and SSH credentials;
   - a link in out/ to a host file holding a fake API key: the key never comes back;
   - a fork bomb stops at the process limit. The test refuses to start it at all when
     --pids-limit is missing, and the bomb itself stops at 300 processes;
   - 1 GiB of memory: the kernel's out-of-memory killer ends the code (SIGKILL, reported
     as exit status -9) at the 256 MB limit, with no swap to spill into;
   - four processes spinning for 2 s get about 2 s of CPU in total, not 8;
   - an endless loop is killed at a 3-second deadline and its container is gone;
   - 32 MB of output: the code's writes stop at the 16 MB file-size limit, and the model
     gets the first 64 KB, marked as truncated.
   The live tests take about 10 seconds with the reference.
   On a Linux Docker Engine, set CANARY_BIND=0.0.0.0 and CANARY_HOST to the docker0 address
   (usually 172.17.0.1) so the control run can reach the canary; Docker Desktop needs nothing
   extra. CONTAINER_PREFIX changes the container names (default: sandbox).

Execution status
The offline tests run in the site's test suite. The live tests were last run on
11 October 2026 on Docker 29.4.3 (Docker Desktop, Apple silicon, runc 1.3.5, cgroup v2)
with python:3.14.8-slim-trixie at the digest above, driven by Python 3.14.8 on the host.
The reference passed all 12 live tests in about 10 seconds; the starter failed 6 (network,
host-file link, fork bomb, memory, CPU share and deadline) while all three legitimate cases
passed.

Limits
Containers share the host kernel, and the Docker daemon is not part of the sandbox: run the
tool on a host that holds no credentials and does nothing else. For a stronger boundary run
the same flags under gVisor (docker run --runtime=runsc) or put each job in a microVM such
as Firecracker; neither was tested here. The network block also stops pip: give the code a
fixed, pre-installed set of packages instead. Nothing here decides whether the code's
result is correct, or whether an action it proposes should happen; keep side effects
outside the sandbox and behind an approval.

Dev container
.devcontainer/devcontainer.json opens this folder in VS Code Dev Containers or GitHub
Codespaces using the Dev Containers Python 3.13 image, pinned by digest, for the offline
steps. The live steps need Docker on the host, so run them outside the container.
