Add Defense in Depth
Compile-time sandboxing gives generated programs fine-grained permissions through typed capabilities. However, compile-time sandboxing does not limit CPU and memory usage. You can enforce those limits with OS-level mechanisms. You may also add file and network rules for defense in depth.
What Harpe already applies
runCode always:
- compiles and runs each program in an isolated temporary directory
- enforces wall-clock timeouts
- kills the whole process group on timeout
- limits captured output
- starts the guest with a minimal environment containing only the executable search path, audit-log path, and variables explicitly supplied by the application
Enable the wrapper
From a generated template project:
cp sandbox/run.sh.example sandbox/run.sh
chmod +x sandbox/run.sh
When this executable exists, Harpe runs:
sandbox/run.sh <generated-program.py>
The wrapper must finish by executing a Python interpreter with "$@". Start
with the example unchanged and confirm the agent can still run a calculation.
1. Limit resources
Add limits before the final exec:
ulimit -v 1048576 # about 1 GiB of address space
ulimit -t 30 # 30 CPU-seconds
exec python3 "$@"
The wall-clock timeout catches hangs. These limits constrain memory, CPU time, and CPU consumption.
2. Restrict the filesystem
Give the process read access to its runtime dependencies. These may include the Python interpreter, standard library, installed packages, shared libraries, and files required by trusted capability implementations. Give it write access only to the run directory and locations explicitly managed by capabilities.
The following allowlists are starting points for a system Python installation.
With landrun:
exec landrun \
--ro /usr /lib /lib64 /etc \
--rw "$(dirname "$1")" \
-- python3 "$@"
With bubblewrap:
exec bwrap \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind /lib64 /lib64 \
--ro-bind /etc /etc \
--proc /proc \
--dev /dev \
--tmpfs /tmp \
--bind "$(dirname "$1")" "$(dirname "$1")" \
-- python3 "$@"
Paths differ across deployments. Add the specific package, virtual-environment,
and capability data paths your application needs. Do not expose the whole
project directory as a shortcut. Keep .env, SSH keys, and other sessions’ data
outside the allowlist.
3. Restrict the network
If guest programs need no network, add a private network namespace:
exec bwrap \
--ro-bind /usr /usr \
--ro-bind /lib /lib \
--ro-bind /lib64 /lib64 \
--ro-bind /etc /etc \
--proc /proc \
--dev /dev \
--tmpfs /tmp \
--bind "$(dirname "$1")" "$(dirname "$1")" \
--unshare-net \
-- python3 "$@"
If a capability needs network access, prefer keeping that access in trusted runtime code. Do not give the generated guest general network access merely because one capability calls an API.
For host-level filtering, run guests under a dedicated account and apply
nftables rules or your platform’s equivalent to that account. Switching users
requires carefully scoped operator privileges. Configure it in the service
manager rather than giving the agent unrestricted sudo.
Use an existing isolation stack
run.sh is an integration seam. It can hand the generated program to Docker,
Podman, gVisor, Kata, or a microVM instead of invoking Python directly.
For example:
exec docker run --rm \
--network none \
--memory 1g \
--cpus 1 \
--read-only \
-v "$(dirname "$1"):$(dirname "$1"):ro" \
python:3.12-slim \
python3 "$@"
Use the isolation system your team already operates and monitors.
Verify before deployment
Exercise each boundary explicitly:
- Run a normal calculation.
- Trigger a long loop and confirm the timeout stops it.
- Try to read a file outside the allowed directory.
- Try an outbound connection when networking should be disabled.
- Confirm the guest environment contains only
PATH, the audit path, and the values your application intentionally grants. - Review the session log for the recorded tool result.