# 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.*
