add ai-sandbox: rootless podman sandbox for agentic development
A single bash script that runs a coding harness (pi by default) inside a rootless Ubuntu 24.04 container with only the current directory mounted at /project, an explicit egress allowlist and an explicit environment: nothing is passed in that was not named on the command line or in a config file. Network enforcement here is empirical rather than assumed (tests/spike_network.sh, docs/networking.md). The original design -- a sidecar proxy on an internal network -- does not work rootless on this host, so allow mode runs pasta with --map-host-loopback, installs an inet aisb ruleset inside the container that drops every egress path except the gate's, and relays through a squid gate bound to the host's loopback that resolves names itself and refuses anything unlisted. The entrypoint checks that the default-drop rule really exists before setpriv drops to uid 1000 with an empty capability set, and exits 77/78 rather than run unenforced. The Containerfile, the entrypoint, the squid config generator and the ruleset renderer all live in bin/ai-sandbox, and the image is tagged by content hash, so a policy change can never silently reuse a stale image. Tests are pure bash: 161 assertions start no container and touch no network, 256 run real ones, including proof that a raw socket, a stripped proxy env and DNS all stay blocked while an allowlisted host works over both http and CONNECT. LLM-Generated: true
This commit is contained in:
Executable
+86
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/unit/03_argv.sh -- the policy actually reaches the podman invocation.
|
||||
source "$TESTS/lib.sh"
|
||||
|
||||
PROJ="$WORK/proj"; mkdir -p "$PROJ"; cd "$PROJ" || exit 1
|
||||
export AISB_CONFIG_DIR="$WORK/cfg-empty"; mkdir -p "$AISB_CONFIG_DIR"
|
||||
A="--allow example.com"
|
||||
|
||||
t_name "isolation flags are always on"
|
||||
sb --dry-run $A
|
||||
assert_contains "--cap-drop=ALL" "all capabilities dropped"
|
||||
assert_contains "no-new-privileges" "no new privileges"
|
||||
assert_contains "--userns keep-id" "uid mapped back to the host user"
|
||||
assert_contains "--pids-limit 512" "pid limit"
|
||||
assert_contains "--memory 4g" "memory limit"
|
||||
assert_contains "--cpus 4" "cpu limit"
|
||||
assert_contains "/tmp:rw,nosuid,nodev" "/tmp is a nosuid tmpfs"
|
||||
assert_contains "--hostname aisb" "hostname"
|
||||
assert_contains "--rm" "container is removed on exit"
|
||||
assert_contains "--stop-signal SIGTERM" "clean shutdown signal"
|
||||
|
||||
t_name "network modes"
|
||||
sb --dry-run --net none
|
||||
assert_contains "--network none" "none"
|
||||
sb --dry-run --net full
|
||||
assert_contains "--network pasta" "full uses pasta with no filter"
|
||||
assert_absent "--cap-add=NET_ADMIN" "full mode needs no NET_ADMIN"
|
||||
sb --dry-run $A
|
||||
assert_contains "--network pasta:--map-host-loopback,100.64.0.1" "allow mode maps a host-loopback door for the gate"
|
||||
assert_contains "--cap-add=NET_ADMIN" "allow mode grants NET_ADMIN to the entrypoint only"
|
||||
assert_contains "HTTP_PROXY=http://100.64.0.1:18081" "proxy env points at the gate"
|
||||
assert_contains "NO_PROXY=localhost" "localhost is never proxied"
|
||||
sb --dry-run $A --proxy 10.9.8.7:3128
|
||||
assert_contains "HTTP_PROXY=http://10.9.8.7:3128" "--proxy overrides the gate"
|
||||
sb --dry-run $A --no-gate
|
||||
assert_absent "HTTP_PROXY=" "--no-gate sets no proxy"
|
||||
|
||||
t_name "filesystem"
|
||||
sb --dry-run $A
|
||||
assert_contains "$PROJ:/project" "cwd is mounted at /project"
|
||||
assert_contains "--workdir /project" "cwd is the working directory"
|
||||
assert_contains "/run/aisb:ro" "policy dir is mounted read-only"
|
||||
sb --dry-run $A --no-cwd
|
||||
assert_absent "$PROJ:/project" "--no-cwd mounts nothing"
|
||||
sb --dry-run $A --workdir /src
|
||||
assert_contains "$PROJ:/src" "--workdir changes the mount point"
|
||||
mkdir -p "$WORK/extra"
|
||||
sb --dry-run $A --mount "$WORK/extra"
|
||||
assert_contains "$WORK/extra:/mnt/extra" "--mount defaults to /mnt/<name>"
|
||||
sb --dry-run $A --mount "$WORK/extra:/ex:ro"
|
||||
assert_contains "$WORK/extra:/ex" "--mount with explicit destination"
|
||||
assert_contains "ro" " ... and ro option present"
|
||||
sb --dry-run $A --read-only
|
||||
assert_contains "--read-only" "--read-only"
|
||||
sb --dry-run $A
|
||||
assert_contains "type=volume,source=aisb-home,target=/home/sandbox" "home cache volume by default"
|
||||
sb --dry-run $A --ephemeral-home
|
||||
assert_contains "/home/sandbox:rw,nosuid,nodev" "--ephemeral-home uses a tmpfs home"
|
||||
assert_absent "type=volume,source=aisb-home" "--ephemeral-home drops the volume"
|
||||
|
||||
t_name "labels, limits, extra args"
|
||||
sb --dry-run $A --label team=x
|
||||
assert_contains "--label team=x" "--label"
|
||||
assert_contains "aisb.net=allow" "session label"
|
||||
sb --dry-run $A --cpus 12 --memory 8g --pids 99
|
||||
assert_contains "--cpus 12" "custom cpus"
|
||||
assert_contains "--memory 8g" "custom memory"
|
||||
assert_contains "--pids-limit 99" "custom pids"
|
||||
sb_env "AISB_EXTRA_ARGS=--label from=extra" --dry-run $A
|
||||
assert_contains "--label from=extra" "AISB_EXTRA_ARGS is appended"
|
||||
|
||||
t_name "everything lands before the image; the command comes last"
|
||||
sb --dry-run $A --label zzz=1 -- echo hello
|
||||
line="$(printf '%s' "$OUT" | tail -1)"
|
||||
pos_img="$(printf '%s' "$line" | tr ' ' '\n' | grep -n -m1 'localhost/ai-sandbox' | cut -d: -f1)"
|
||||
pos_lbl="$(printf '%s' "$line" | tr ' ' '\n' | grep -n -m1 'zzz=1' | cut -d: -f1)"
|
||||
pos_cmd="$(printf '%s' "$line" | tr ' ' '\n' | grep -n -m1 '^hello$' | cut -d: -f1)"
|
||||
if [[ -n "$pos_img" && -n "$pos_lbl" && $pos_lbl -lt $pos_img ]]; then _ok "extra flags precede the image"; else _bad "extra flags precede the image" "img=$pos_img label=$pos_lbl"; fi
|
||||
if [[ -n "$pos_cmd" && -n "$pos_img" && $pos_img -lt $pos_cmd ]]; then _ok "the command follows the image"; else _bad "the command follows the image" "img=$pos_img cmd=$pos_cmd"; fi
|
||||
|
||||
t_name "dry run is side-effect free"
|
||||
sb --dry-run $A
|
||||
RUNNING="$(podman ps --format '{{.Names}}' | grep -c 'aisb' || true)"
|
||||
DIRS="$(ls -A "$AISB_STATE_DIR/sessions" 2>/dev/null | wc -l | tr -d ' ')"
|
||||
assert_eq "$RUNNING" 0 "dry run started no container"
|
||||
assert_eq "$DIRS" 0 "dry run left no session directory behind"
|
||||
Reference in New Issue
Block a user