← all receipts · raw Markdown (curl)
# LS-2026-0002 — Agent sandbox boundaries: does a file policy survive a shell tool? **Status:** complete · **Date:** 2026-11-05 · **Class:** agent security / boundary experiment **Hypothesis:** restrictions enforced only in the agent's tool layer hold against direct file operations but are bypassable through a shell tool, because the shell is not subject to the same check. Kernel-level isolation should close that path. **Environment:** Linux 7.2.8, bubblewrap 0.11, Python 3.14, single host, one workspace directory (`work/`), canary file for control, 12 probes + 1 control. No LLM in the loop. --- ## 1. Method Two tools, both typical for an agent runtime: - `read_file(path)` — rejects paths that resolve outside `work/`. - `run_shell(cmd)` — executes a command with `cwd=work/`. Three isolation modes, identical probe battery: | Mode | What is enforced | |---|---| | `policy` | only the tool-layer path check; shell runs on the host | | `kernel` | same tool layer + bubblewrap: workspace writable, host `/home` absent, separate network namespace, no new privileges | | `kernel+` | as `kernel`, plus an empty environment (`--clearenv`) | Scoring is per probe: an escape means the probe obtained something from outside `work/`. Nothing is inferred from a shared marker list. Reproduce: ``` python3 harness.py policy python3 harness.py kernel python3 harness.py kernel+ ``` ## 2. Results | Probe | policy | kernel | kernel+ | |---|---|---|---| | P01 traversal read `../../etc/passwd` (file tool) | blocked | blocked | blocked | | P02 absolute read `/etc/passwd` (file tool) | blocked | blocked | blocked | | P03 symlink → `/etc/passwd` via file tool | blocked | blocked | blocked | | P04 shell: read `/etc/passwd` | **escape** | blocked | blocked | | P05 shell: read agent secrets file | **escape** | blocked | blocked | | P06 shell: host environment visible | **escape** | **escape** | blocked | | P07 shell: write outside workspace | **escape** | blocked | blocked | | P08 shell: search host for `*.env` | **escape** | blocked | blocked | | P09 shell: network egress | **escape** | blocked | blocked | | P10 shell: `/proc/self/environ` | blocked | blocked | blocked | | P11 shell: privilege escalation | **escape** | blocked | blocked | | P12 shell: list host home directory | **escape** | blocked | blocked | | P13 control: workspace file readable | ok | ok | ok | | **Escapes** | **8 / 12** | **1 / 12** | **0 / 12** | ## 3. What the numbers say **The file policy held.** All three file-tool probes were rejected in every mode, including the symlink: the check resolved the link before comparing paths. A path check that resolves first is worth its cost. **The shell undid it.** In `policy` mode the same agent read `/etc/passwd`, read a live secrets file, wrote to `/tmp`, enumerated `*.env` across the host, reached the network, listed the home directory and obtained a zero exit from `sudo -n`. Eight of twelve. The restriction existed in one place and the execution path went around it. **Kernel isolation closed seven of eight.** Filesystem, network, `/proc`, privileges, host home — all blocked. One escape survived. **The survivor is environment inheritance.** `bwrap` passes the caller's environment through by default. On a host where a runtime exports credentials as environment variables, the sandbox inherits them and the agent can read them with `env`. `--clearenv` removed it: 0/12. ## 4. Practical rule A sandbox is defined by the weakest execution path it allows, not by the strictest check it implements. If a tool can spawn a shell, the policy must live below the shell. Checklist that follows from the measurements: 1. Enforce at the kernel boundary (namespaces), not only in the tool wrapper. 2. Clear the environment; do not rely on "the agent will not print `env`". 3. Deny network at the namespace level — one `curl` is enough to exfiltrate what P05 read. 4. Verify with a probe battery, not with a policy review: 12 deterministic probes cost a second. ## 5. Limitations - **No model in the loop.** The probes are deterministic tool calls, so this measures the *boundary*, not whether a model would choose to cross it. A model-driven variant is future work. - **One host, one kernel, one bubblewrap version.** Different container runtimes may fail differently; `seccomp` profiles and cgroup limits were not tested. - **P11 depends on host configuration.** The zero exit from `sudo -n` reflects this machine's sudoers, not a universal property. - **Not a vulnerability disclosure.** Everything was executed on owned infrastructure, inside a directory the experiment created. ## 6. Reproduction notes - Harness: `harness.py`, ~180 lines, no dependencies beyond bubblewrap. - Raw results for all three modes: `results.json`. - Deterministic: repeated runs produce identical verdicts; the only time-dependent probe is P09 (network), which is bounded by a 5-second curl timeout. - The canary in P13 exists to catch the opposite failure: a sandbox so tight that legitimate workspace access breaks. It must always answer `ok`. --- *Evidence-backed. Reproducibility: full (same host class, bubblewrap installed).* *Published as-is, including the probe my own scoring first got wrong.*