agent harness initialize repo

This commit is contained in:
Roger Oriol
2026-07-19 20:13:54 +02:00
commit 42475d3249
53 changed files with 7391 additions and 0 deletions

View File

@@ -0,0 +1,361 @@
# Sandbox Alternatives
The in-process sandbox in `tools/sandbox.py` is **application-level**: it inspects
tool arguments in Python and refuses what looks dangerous. That is convenient and
portable, but it is only as strong as the checks we remembered to write — a clever
shell command can often evade a denylist, and a path-confinement check in the same
process as the attacker offers no real boundary if the attacker can run arbitrary
code.
The alternatives below move the boundary **out of the agent process**, into the
operating system, a separate runtime, or a separate machine. They are listed
roughly from lightest to strongest isolation. None is strictly "code" in the
sense of the current implementation — most are invoked as a command, a config
file, or a one-time system setup, with the agent simply spawning its tools
inside them.
A quick legend for the tradeoff columns:
- **Strength** — how hard it is for code running inside to break out.
- **Setup** — how much one-time work is required to use it.
- **Portability** — whether it works on the host this project targets
(the harness is developed on macOS and runs against a local Ollama).
- **Fit** — a subjective rating for *this* coding-agent harness, where the
agent reads/writes files and runs shell commands in the user's project.
---
## 1. OS-level filesystem confinement (no containers)
### Landlock (Linux ≥ 5.13)
Landlock is an unprivileged, in-kernel filesystem access-control LSM. A process
calls `landlock_restrict_self()` with a ruleset describing which paths it may
read/write, and the kernel enforces it for that process and all its children —
even if they later `exec` something malicious. No root, no container, no daemon.
- **Strength**: Strong (kernel-enforced, unforgeable by the sandboxed process).
- **Setup**: Low — a few dozen lines of C or the `pylandlock` / `landlock` PyPI
binding, run once at agent startup before any tool executes.
- **Portability**: Linux only. Not available on macOS.
- **Fit**: Excellent on Linux. It does exactly what the Python path checks do,
but correctly, and it also constrains child processes spawned by `run_bash`.
This is arguably the single best drop-in replacement for the path half of
the current sandbox on a Linux host.
### `chroot`
The classic: `chroot(2)` changes the root directory for a process and its
children, so absolute paths like `/etc/passwd` resolve inside the new root.
- **Strength**: Weak. It is **not** a security boundary on its own — a root
process can escape trivially, and even non-root processes can escape in
several well-known ways (e.g. via `chroot` + `mkdir` + file descriptors).
It also does not restrict network, `/proc`, or `mknod`.
- **Setup**: Medium — you must populate the chroot with enough of a userspace
(`/bin/sh`, coreutils, libs) for `run_bash` to work.
- **Portability**: POSIX, available on macOS, but even weaker there.
- **Fit**: Poor as a primary sandbox; reasonable as a *convenience* layer
combined with something stronger (e.g. chroot + seccomp + drop privs).
### macOS Seatbelt (`sandbox-exec`)
macOS ships a kernel-enforced mandatory-access-control framework ("Seatbelt")
exposed via the `sandbox-exec` command and `.sb` policy files. You can write a
profile that permits reading/writing only under a given directory, blocks
`sudo`/`mount`/raw-disk access, and denies all network except specified hosts.
- **Strength**: Strong (kernel-enforced; used by Safari, App Store, etc.).
- **Setup**: Low — write a `.sb` profile and launch the agent under
`sandbox-exec -f profile.sb`. Apple's built-in profiles (e.g.
`no-network`) can be referenced directly.
- **Portability**: macOS only.
- **Fit**: Very good *for this project's development host*. It is the native
macOS equivalent of Landlock + a network filter, and it requires no code
changes — just a profile file and a wrapper command.
---
## 2. Namespaces and unprivileged containers
### Linux namespaces (via `bubblewrap` / `bwrap` / `unshare`)
Namespaces (`mount`, `pid`, `net`, `user`, `ipc`, `uts`) give a process its own
view of the filesystem, process list, network stack, etc. **Bubblewrap**
(`bwrap`, used by Flatpak) and **nsjail** are unprivileged wrappers that make
this practical: you declare a read-only root, a writable bind-mount for the
project, and an isolated network, then run the agent inside.
- **Strength**: Strong (kernel-enforced; the process literally cannot see the
host filesystem outside the bind-mounts).
- **Setup**: Medium — install `bwrap`, declare bind-mounts and a rootfs.
`unshare -r --net --pid --mount` is a one-liner for a quick test.
- **Portability**: Linux only (namespaces are a Linux kernel feature).
- **Fit**: Excellent on Linux. A `bwrap` invocation can replace both the path
confinement *and* the command blocklist with a real boundary, and you can
combine it with cgroups (below) for resource limits.
### systemd-nspawn
A thin container manager built around namespaces + cgroups. Think of it as
"chroot done right": it gives a near-complete OS view with proper isolation,
and integrates with `systemd` resource controls.
- **Strength**: Strong.
- **Setup**: Medium — needs a container rootfs (`debootstrap`, `dnf
--installroot`, or a tarball).
- **Portability**: Linux + systemd.
- **Fit**: Good when you already run on a systemd box and want a long-lived
project container. Heavier than `bwrap` for a single command.
### LXC / LXD
Full system containers. Overkill for a single agent process, but useful if you
want a persistent, snapshot-able "project VM" the agent always runs in.
- **Strength**: Strong.
- **Setup**: High (container image management, networking).
- **Portability**: Linux only.
- **Fit**: Low for a CLI agent; high if you want reproducible, throwaway project
environments.
---
## 3. Full containers
### Docker / OCI runtimes (`runc`, `crun`, `podman`)
Run the agent (or just the `run_bash` tool) inside a container whose root
filesystem is a project image, with the project bind-mounted read-write and
everything else read-only. Network can be disabled (`--network none`) or
proxied.
- **Strength**: Strong, assuming a non-root container and a hardened runtime.
(Docker historically had a weak default boundary for root containers;
`podman` runs rootless by default.)
- **Setup**: Medium-High — image build, volume mounts, network policy. But
tooling is mature and well-understood.
- **Portability**: Cross-platform via Docker Desktop / Podman Machine / colima.
On macOS the container runs in a Linux VM, which adds latency.
- **Fit**: Good as a *tool-level* sandbox: keep the agent loop on the host, but
route every `run_bash`/`write_file` call into a short-lived container. This
is what most hosted coding agents (SWE-agent, OpenHands) do in practice.
### `podman` (rootless)
Same UX as Docker, but daemonless and rootless by default, so a container
breakout does not immediately imply host root.
- **Fit**: Strictly better than Docker for single-user local use.
---
## 4. Kernel syscall filtering
### seccomp-bpf
A Linux kernel feature that lets a process install a BPF filter restricting
which syscalls it (and its children) may call. You can ban `ptrace`, `mount`,
`reboot`, `keyctl`, `open` of specific paths (via path-based filters with
`SECCOMP_RET_ERRNO`), etc.
- **Strength**: Strong against syscall-based attacks; weak against logic bugs
inside *allowed* syscalls (e.g. a permitted `unlink` can still delete
everything writable).
- **Setup**: Medium — a filter program (libs like `pyseccomp` or hand-rolled
BPF). Best combined with a filesystem sandbox, not used alone.
- **Portability**: Linux only.
- **Fit**: Good as a *second* layer on top of Landlock/namespaces. By itself
it doesn't confine paths well; together with Landlock it is very strong.
### AppArmor / SELinux
Mandatory access-control LSMs configured by system packages. You write a
profile that says "this binary may only read/write these paths, may not
network, may not ptrace," and the kernel enforces it.
- **Strength**: Very strong (kernel-enforced; survives `exec`).
- **Setup**: High — profile authoring is fiddly and distribution-specific.
- **Portability**: Linux only, and AppArmor vs SELinux differ by distro.
- **Fit**: Low for a portable CLI tool, high for a centrally-managed
deployment where a sysadmin owns the profile.
---
## 5. User-space kernels / VMs
### gVisor
A user-space kernel implemented in Go (`runsc`) that intercepts the sandboxed
program's syscalls and re-implements them against a restricted host API. The
sandboxed code never touches the host kernel directly. Compatible with the OCI
interface, so it drops into Docker/Podman.
- **Strength**: Very strong — defeats most kernel-exploit-based breakouts
because the guest never issues real syscalls to the host kernel.
- **Setup**: Medium — install `runsc`, set it as the Docker runtime.
- **Portability**: Linux only.
- **Fit**: Excellent when you are already containerising tool execution and
want a much harder boundary than plain `runc`. Some syscall-compatibility
gaps; fine for typical dev tooling.
### Firecracker / Cloud Hypervisor / Kata Containers (microVMs)
Full KVM-based virtual machines with a tiny footprint and millisecond boot
times. Kata integrates them into Kubernetes/container runtimes; Firecracker is
used by AWS Lambda and Fargate.
- **Strength**: Maximum practical strength — hardware-isolated; a guest kernel
exploit does not reach the host.
- **Setup**: High — needs KVM, a VM image, a network setup. Worth it only if
you run untrusted agents at scale.
- **Portability**: Linux with virtualisation extensions.
- **Fit**: Low for a local single-user harness; **the** right choice for a
multi-tenant hosted agent service.
---
## 6. Runtime / language-level confinement
### WebAssembly (WASI) runtimes
Compile tools (or the whole agent) to WASM and run them in `wasmtime`/`wasmer`
with a WASI capability-based filesystem: the runtime only sees directories you
explicitly pre-open, and there is no shell unless you implement one. `wasmtime`
also supports seccomp and per-instance resource limits.
- **Strength**: Strong — capability-based, no ambient authority, no `fork`/
`exec` by default.
- **Setup**: High for Python tools (need to compile or rewrite), low for
self-contained tools shipped as WASM.
- **Portability**: Cross-platform.
- **Fit**: Poor for the *existing* Python toolset (porting `run_bash` defeats
the point), but attractive for a *new* tool layer written in Rust/Go that
exposes safe primitives to the agent.
### RestrictedPython / sandboxed interpreters
Run agent-generated Python in a restricted interpreter that strips `open`,
`__import__`, `exec`, etc.
- **Strength**: Weak to moderate — sandboxed-Python escapes are a perennial
CTF genre; RestrictedPython explicitly disclaims being a security sandbox.
- **Fit**: Relevant only if the agent emits Python rather than shell; not our
case.
---
## 7. Resource limits (orthogonal but worth pairing)
These do not confine *what* a program can do, only *how much*. Pair them with
any of the above.
### cgroups v2 (Linux)
Limit CPU, memory, IO, and PID count for the agent process subtree. Prevents
fork bombs and runaway builds from taking down the host even when the command
blocklist is bypassed.
- **Fit**: Essential companion to any namespace/VM approach on Linux.
### `setrlimit` / `ulimit` (POSIX)
Per-process limits on file size, number of fds, CPU seconds, processes.
Available on macOS as well.
- **Fit**: Cheap baseline everywhere; weaker than cgroups but zero setup.
---
## 8. Network-level isolation
If the agent should not phone home, deny it network access entirely at the
boundary instead of trying to detect exfiltration in code.
- **Linux network namespace + `iptables`/`nftables` egress allowlist**: the
sandboxed process gets its own netns with a veth pair and a proxy that
allows only specific hosts (e.g. `localhost:11434` for Ollama).
- **macOS**: Seatbelt's `(deny network*)` or a `pfctl` rule on a dedicated
interface.
- **`bubblewrap --unshare-net`**: the process gets a loopback-only netns —
it can still reach the host via an explicit bind-mount/proxy.
For this harness, network egress should be limited to the Ollama endpoint
(`localhost:11434`) and any URL the user has allowlisted for `webfetch`.
---
## 9. Managed sandboxing services
If you do not want to run any of the above yourself, several services expose a
"sandboxed execution" API over the network:
- **E2B** — open-source microVM-based code sandboxes with an SDK; designed for
exactly this use case (agent tool execution). You ship code, they return
stdout/stderr/exit code; files live in the VM.
- **Modal / Fly Machines / Replicate** — ephemeral VMs/containers with an HTTP
API; spin one up per session, tear it down when done.
- **Daytona / envd / Devcontainer** — dev-environment-as-code; less of a
*security* boundary, more of a *reproducible workspace*, but still confines
file writes to the workspace.
- **Strength**: Strong (the provider handles isolation; you get a remote
boundary you cannot accidentally weaken).
- **Setup**: Low to medium (an SDK call), but adds a network dependency and
latency to every tool call.
- **Portability**: Anywhere with network access.
- **Fit**: Great for a hosted version of this harness; awkward for a purely
local one because every `read_file` becomes a round-trip.
---
## Comparison at a glance
| Approach | Layer | Strength | Setup | macOS | Best for this harness? |
|------------------------------|--------------|----------|-------|-------|------------------------|
| Current in-process checks | app | weak | low | yes | baseline / convenience |
| Landlock | kernel | strong | low | no | ★ on Linux |
| macOS Seatbelt (`sandbox-exec`) | kernel | strong | low | yes | ★ on macOS dev host |
| `bubblewrap` / namespaces | kernel | strong | med | no | ★★ on Linux |
| seccomp-bpf | kernel | strong* | med | no | companion layer |
| AppArmor / SELinux | kernel | very strong | high | no | server deployments |
| chroot | kernel | weak | med | yes | only with another layer|
| Docker / Podman | container | strong | med | VM | tool-level sandbox |
| gVisor (`runsc`) | user-kernel | very strong | med | no | hardened container run |
| Firecracker / Kata (microVM) | VM | max | high | no | multi-tenant hosting |
| WASI / Wasmtime | runtime | strong | high | yes | new tool layer only |
| cgroups v2 / rlimits | kernel | (resource) | low | no/yes | companion everywhere |
| Network namespace / Seatbelt net | network | strong | med | yes | companion everywhere |
| E2B / Modal / Fly | managed VM | strong | low | n/a | hosted version |
\* seccomp is strong for syscalls but does not confine file paths on its own.
---
## Recommendation for this harness
Keep the in-process checks — they are cheap, portable, and catch the obvious
mistakes before they ever reach the OS. Then layer one of the following on top,
chosen by host:
1. **On the macOS dev machine**: launch the agent under `sandbox-exec` with a
`.sb` profile that (a) restricts file writes to the project root, (b) denies
network except `localhost:11434`, and (c) blocks `mount`, `sudo`, raw disk,
and kernel-extension syscalls. Zero code changes; the Python sandbox becomes
a second line of defence rather than the only one.
2. **On a Linux host**: `bwrap --ro-bind / / --bind $PROJECT $PROJECT
--dev /dev --proc /proc --unshare-net` (with an explicit Ollama proxy) plus
a Landlock ruleset applied from Python before the tool loop starts, plus a
cgroup v2 slice for CPU/memory/PID limits. This gives kernel-enforced path,
network, and resource confinement for both the agent and any `run_bash`
children.
3. **If this ever becomes a hosted service**: run each session inside a
Firecracker microVM (or gVisor-isolated container) with the project mounted
read-write and network restricted to a allowlisted proxy. The in-process
and namespace layers stay as defence-in-depth inside the VM.
In every case, the **audit log** (`tools/audit.py`) stays as-is: it is the
mechanism that lets you reconstruct *what happened inside the sandbox*, which
matters exactly as much as the boundary itself.