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:

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:

  1. Run a normal calculation.
  2. Trigger a long loop and confirm the timeout stops it.
  3. Try to read a file outside the allowed directory.
  4. Try an outbound connection when networking should be disabled.
  5. Confirm the guest environment contains only PATH, the audit path, and the values your application intentionally grants.
  6. Review the session log for the recorded tool result.