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:
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/e2e/10_image.sh -- the image itself: build, cache, contents, fail-closed entrypoint.
|
||||
source "$TESTS/lib.sh"
|
||||
|
||||
PROJ="$WORK/proj"; mkdir -p "$PROJ"; cd "$PROJ" || exit 1
|
||||
|
||||
t_name "build"
|
||||
sb build
|
||||
assert_rc 0 "ai-sandbox build succeeds"
|
||||
IMG="$("$AISB" --print-policy --net none 2>/dev/null | grep '^image=' | cut -d= -f2)"
|
||||
[[ -n "$IMG" ]] && _ok "image ref resolved: $IMG" || _bad "image ref resolved"
|
||||
podman image exists "$IMG" && _ok "image exists in podman storage" || _bad "image exists"
|
||||
|
||||
t_name "build is cached and tagged by content"
|
||||
START=$SECONDS
|
||||
sb build
|
||||
assert_rc 0 "rebuild succeeds"
|
||||
(( SECONDS - START < 15 )) && _ok "second build reused the cache ($((SECONDS - START))s)" \
|
||||
|| _bad "second build reused the cache ($((SECONDS - START))s)"
|
||||
IMG2="$("$AISB" --print-policy --net none 2>/dev/null | grep '^image=' | cut -d= -f2)"
|
||||
assert_eq "$IMG" "$IMG2" "same inputs, same tag"
|
||||
sb --print-policy --net none --install hexd > /dev/null
|
||||
IMG3="$("$AISB" --print-policy --net none --install hexd | grep '^image=' | cut -d= -f2)"
|
||||
[[ "$IMG3" != "$IMG" ]] && _ok "extra packages produce a different tag" || _bad "extra packages change the tag"
|
||||
|
||||
t_name "the entrypoint fails closed without a policy"
|
||||
podman run --rm --network none --cap-drop=ALL "$IMG" true > "$WORK/fc.out" 2>&1
|
||||
assert_eq 78 "$?" "no /run/aisb policy -> exit 78, nothing ran"
|
||||
grep -q 'policy file' "$WORK/fc.out" && _ok "it says why" || _bad "it says why: $(cat "$WORK/fc.out")"
|
||||
|
||||
t_name "image contents"
|
||||
POL="$WORK/pol"; mkdir -p "$POL"
|
||||
printf 'AISB_NET=none\nAISB_UID=1000\nAISB_GID=1000\nAISB_ALLOW_ICMP=1\nAISB_ALLOW_DNS=0\n' > "$POL/policy.env"
|
||||
: > "$POL/direct4.list"; : > "$POL/direct6.list"
|
||||
# podman takes mount options as a comma list: ":ro,:z" not ":ro:z"
|
||||
SEL="${AISB_SELINUX:-:z}"; SELC="${SEL#:}"
|
||||
run_in() {
|
||||
podman run --rm --network none --cap-drop=ALL --cap-add=setuid --cap-add=setgid --cap-add=setpcap \
|
||||
--userns "keep-id:uid=1000,gid=1000" -u 0:0 \
|
||||
--security-opt no-new-privileges \
|
||||
-v "$POL:/run/aisb:ro${SELC:+,$SELC}" -v "$PROJ:/project$SEL" -w /project \
|
||||
"$IMG" bash -lc "$1" 2>&1
|
||||
}
|
||||
OUT="$(run_in 'id -u')"; assert_eq "1000" "$(printf '%s' "$OUT" | tail -1)" "drops to uid 1000"
|
||||
OUT="$(run_in 'grep CapEff /proc/self/status | cut -f2')"
|
||||
assert_eq "0000000000000000" "$(printf '%s' "$OUT" | tail -1)" "no capabilities left for the command"
|
||||
OUT="$(run_in 'node --version')"
|
||||
printf '%s' "$OUT" | grep -qE '^v2[2-9]' && _ok "node $(printf '%s' "$OUT" | tail -1)" || _bad "node 22+: $OUT"
|
||||
OUT="$(run_in 'command -v pi')"
|
||||
printf '%s' "$OUT" | grep -q '/pi' && _ok "the pi harness is installed" || _bad "pi installed: $OUT"
|
||||
OUT="$(run_in 'nft --version && setpriv --version | head -1 && git --version')"
|
||||
assert_contains "nftables" "nftables present"
|
||||
assert_contains "git version" "git present"
|
||||
OUT="$(run_in 'pwd')"
|
||||
assert_eq "/project" "$(printf '%s' "$OUT" | tail -1)" "working directory is /project"
|
||||
|
||||
t_name "the container cannot see the host filesystem"
|
||||
SECRET="$WORK/parent-secret.txt"; echo "do not read" > "$SECRET"
|
||||
OUT="$(run_in "cat '$SECRET' 2>&1 || echo NOT_VISIBLE")"
|
||||
assert_contains "NOT_VISIBLE" "files outside the mount are unreachable"
|
||||
OUT="$(run_in 'ls / | tr "\n" " "')"
|
||||
assert_absent "$(id -un)" "no host home directories in /"
|
||||
|
||||
t_name "resource caps are visible from inside"
|
||||
OUT="$(podman run --rm --network none --cap-drop=ALL --cap-add=setuid --cap-add=setgid --cap-add=setpcap --pids-limit 64 --memory 128m \
|
||||
--userns "keep-id:uid=1000,gid=1000" -u 0:0 \
|
||||
-v "$POL:/run/aisb:ro${SELC:+,$SELC}" "$IMG" \
|
||||
bash -lc 'cat /sys/fs/cgroup/pids.max; cat /sys/fs/cgroup/memory.max' 2>&1)"
|
||||
assert_contains "64" "pids.max = 64"
|
||||
assert_contains "134217728" "memory.max = 128m"
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/e2e/11_net.sh -- the promise of the project: only the allowlist is reachable.
|
||||
#
|
||||
# Needs outbound internet on the host; skips (rc 0) when there is none.
|
||||
source "$TESTS/lib.sh"
|
||||
|
||||
PROJ="$WORK/proj"; mkdir -p "$PROJ"; cp "$TESTS/fixtures/egress_probe.sh" "$PROJ/"
|
||||
cd "$PROJ" || exit 1
|
||||
|
||||
if ! curl -m8 -sS -o /dev/null http://example.com 2>/dev/null; then
|
||||
echo " (skipped: no outbound internet on this host, so egress cannot be tested)"
|
||||
summary; exit 0
|
||||
fi
|
||||
|
||||
GATE="--net allow --allow example.com"
|
||||
|
||||
# a service on the host's loopback that the sandbox must not be able to reach
|
||||
HOSTPORT="$(python3 -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1]);s.close()')"
|
||||
python3 -m http.server "$HOSTPORT" --bind 127.0.0.1 --directory "$WORK" >/dev/null 2>&1 &
|
||||
SRV=$!
|
||||
trap 'kill '"$SRV"' 2>/dev/null' EXIT
|
||||
sleep 1
|
||||
|
||||
t_name "allow mode: enforcement in one run"
|
||||
sb $GATE --env AISB_PROBE_HOSTSVC="$HOSTPORT" -- bash /project/egress_probe.sh
|
||||
assert_contains "ok_allowed_http=200" "allowlisted http works (through the gate)"
|
||||
assert_contains "ok_allowed_https=200" "allowlisted https works (CONNECT)"
|
||||
assert_contains "ok_gate_denied_domain=403" "a non-allowlisted domain is refused by the gate"
|
||||
assert_contains "ok_gate_denied_https=" "https CONNECT to a non-allowlisted host is refused"
|
||||
assert_contains "ok_direct_egress_blocked=domain" "ignoring the proxy env does not open a direct route"
|
||||
assert_contains "ok_direct_egress_blocked=rawip" "a literal IP is not routable either"
|
||||
assert_contains "ok_raw_socket_blocked" "a raw socket cannot connect out"
|
||||
assert_contains "ok_host_service_blocked" "only the gate's port is reachable on the mapped loopback address"
|
||||
assert_contains "ok_dns_blocked" "DNS is closed: names cannot be resolved behind the proxy's back"
|
||||
|
||||
t_name "the ruleset cannot be lifted from inside"
|
||||
assert_contains "uid=1000" "the harness runs unprivileged"
|
||||
assert_contains "capeff=0000000000000000" "no capabilities remain for it"
|
||||
assert_contains "ok_rules_hidden" "it cannot read the ruleset"
|
||||
assert_contains "ok_rules_locked" "it cannot delete the ruleset"
|
||||
assert_contains "ok_netns_locked" "it cannot open its own network namespace"
|
||||
assert_contains "ok_still_enforced=403" "and enforcement still holds afterwards"
|
||||
|
||||
t_name "an IP entry is allowed through the gate as well"
|
||||
sb --net allow --allow example.com --allow 1.1.1.1:80 -- bash -lc '
|
||||
echo "ip_entry_code=$(curl -m10 -sS -o /dev/null -w %{http_code} http://1.1.1.1 2>&1)"
|
||||
echo "domain_code=$(curl -m10 -sS -o /dev/null -w %{http_code} http://example.org 2>&1)"'
|
||||
assert_absent "ip_entry_code=403" "an allowlisted IP is not refused by the gate"
|
||||
assert_contains "domain_code=403" "while an unlisted domain still is"
|
||||
|
||||
t_name "--net none is dark"
|
||||
sb --net none -- bash -lc '
|
||||
curl -m5 -sS -o /dev/null http://example.com 2>/dev/null && echo BAD_open || echo ok_dark
|
||||
ip -4 -o addr show 2>/dev/null | grep -c global | sed "s/^/addrs=/"
|
||||
echo "capeff=$(grep CapEff /proc/self/status | cut -f2)"'
|
||||
assert_contains "ok_dark" "no egress at all"
|
||||
assert_contains "addrs=0" "no global addresses in the namespace"
|
||||
assert_contains "capeff=0000000000000000" "no capabilities either"
|
||||
|
||||
t_name "--no-gate: IPs resolved up front, everything else dropped"
|
||||
sb --net allow --no-gate --allow example.com -- bash -lc '
|
||||
echo "allowed=$(curl -m15 -sS -o /dev/null -w %{http_code} http://example.com 2>&1)"
|
||||
curl -m8 -sS -o /dev/null http://example.org 2>/dev/null && echo BAD_not_blocked || echo ok_blocked
|
||||
echo "dns=$(: </dev/tcp/8.8.8.8/53 2>/dev/null && echo open || echo closed)"'
|
||||
assert_contains "allowed=200" "the resolved address of an allowlisted name works"
|
||||
assert_contains "ok_blocked" "anything else is dropped by netfilter"
|
||||
|
||||
t_name "the gate does not outlive the session"
|
||||
BEFORE_GATES="$(podman ps -a --filter label=aisb.role=gate -q | wc -l | tr -d ' ')"
|
||||
sb $GATE -- true
|
||||
AFTER_GATES="$(podman ps -a --filter label=aisb.role=gate -q | wc -l | tr -d ' ')"
|
||||
assert_eq "$BEFORE_GATES" "$AFTER_GATES" "no gate container is left behind by a run"
|
||||
LEFT="$(ls -A "$AISB_STATE_DIR/sessions" 2>/dev/null | wc -l | tr -d ' ')"
|
||||
assert_eq "0" "$LEFT" "no session directory is left behind"
|
||||
|
||||
t_name "a named gate outlives a session on purpose"
|
||||
AISB_GATE_NAME=aisb-test-gate "$AISB" gate up --allow example.com > "$WORK/gate.out" 2>&1
|
||||
OUT="$(cat "$WORK/gate.out")"
|
||||
assert_rc 0 "gate up succeeds"
|
||||
assert_contains "port=" "and reports the port"
|
||||
PORT="$(printf '%s' "$OUT" | grep '^port=' | cut -d= -f2)"
|
||||
if [[ -n "$PORT" ]]; then
|
||||
code="$(curl -sS -m6 -x "http://127.0.0.1:$PORT" -o /dev/null -w '%{http_code}' http://example.com/ 2>/dev/null || echo 000)"
|
||||
assert_eq "200" "$code" "the named gate proxies an allowlisted host"
|
||||
code="$(curl -sS -m6 -x "http://127.0.0.1:$PORT" -o /dev/null -w '%{http_code}' http://example.org/ 2>/dev/null || echo 000)"
|
||||
assert_eq "403" "$code" "and refuses a non-allowlisted host"
|
||||
fi
|
||||
AISB_GATE_NAME=aisb-test-gate "$AISB" gate down > /dev/null 2>&1
|
||||
assert_eq "0" "$(podman ps -a --filter name=aisb-test-gate -q | wc -l | tr -d ' ')" "gate down removes it"
|
||||
|
||||
t_name "a sandbox can use a named gate instead of starting its own"
|
||||
AISB_GATE_NAME=aisb-test-gate2 "$AISB" gate up --allow example.com > "$WORK/gate2.out" 2>&1
|
||||
PORT2="$(grep '^port=' "$WORK/gate2.out" | cut -d= -f2)"
|
||||
sb --net allow --proxy "100.64.0.1:${PORT2}" --allow example.com -- bash -lc '
|
||||
echo "via_proxy=$(curl -m15 -sS -o /dev/null -w %{http_code} http://example.com 2>&1)"'
|
||||
assert_contains "via_proxy=200" "--proxy routes through the named gate"
|
||||
AISB_GATE_NAME=aisb-test-gate2 "$AISB" gate down > /dev/null 2>&1
|
||||
|
||||
t_name "a bad gate config fails closed rather than running wide open"
|
||||
# point the sandbox at a port where nothing answers: the run must not silently
|
||||
# proceed with a broken egress path.
|
||||
sb --net allow --proxy 100.64.0.1:1 -- bash -lc '
|
||||
code=$(curl -m8 -sS -o /dev/null -w %{http_code} http://example.com 2>/dev/null); echo "code=$code"
|
||||
exit 0'
|
||||
assert_contains "code=000" "with a dead proxy, egress simply fails (no bypass)"
|
||||
|
||||
kill "$SRV" 2>/dev/null
|
||||
@@ -0,0 +1,154 @@
|
||||
#!/usr/bin/env bash
|
||||
# tests/e2e/12_sandbox.sh -- lifecycle, filesystem and environment behaviour of a real run.
|
||||
source "$TESTS/lib.sh"
|
||||
|
||||
WORK_DIR="$WORK/project"; mkdir -p "$WORK_DIR"; cd "$WORK_DIR" || exit 1
|
||||
echo "hello from the host" > "$WORK_DIR/host-file.txt"
|
||||
export AISB_CANARY_IN=canary-value-42
|
||||
unset AISB_CANARY_OUT
|
||||
DARK="--net none"
|
||||
|
||||
t_name "the command runs and its exit code comes back"
|
||||
sb $DARK -- bash -c 'echo ran; exit 0'
|
||||
assert_rc 0 "exit 0 propagates"; assert_contains "ran" "stdout reaches the host"
|
||||
sb $DARK -- bash -c 'exit 3'
|
||||
assert_rc 3 "exit 3 propagates"
|
||||
sb $DARK -- bash -c 'exit 127'
|
||||
assert_rc 127 "exit 127 propagates"
|
||||
|
||||
t_name "default command follows the harness"
|
||||
sb $DARK --harness none -- true >/dev/null; assert_rc 0 "harness none is accepted"
|
||||
sb $DARK --harness pi -- bash -lc "command -v pi"
|
||||
assert_rc 0 "the default pi binary exists in the image"
|
||||
|
||||
t_name "the project directory is mounted, writable, and owned by the host user"
|
||||
sb $DARK -- bash -c 'pwd; touch /project/from-container.txt; echo wrote'
|
||||
assert_contains "/project" "working directory is /project"
|
||||
[[ -f "$WORK_DIR/from-container.txt" ]] && _ok "writes land on the host" || _bad "writes land on the host"
|
||||
if [[ "$(stat -c %u "$WORK_DIR/from-container.txt")" == "$(id -u)" ]]; then
|
||||
_ok "the file is owned by the host uid, not by root"
|
||||
else
|
||||
_bad "the file is owned by the host uid" "owner=$(stat -c %u "$WORK_DIR/from-container.txt") expected=$(id -u)"
|
||||
fi
|
||||
sb $DARK -- bash -c 'cat /project/host-file.txt'
|
||||
assert_contains "hello from the host" "existing project files are readable"
|
||||
|
||||
t_name "nothing else on the host is visible"
|
||||
echo secret > "$WORK/not-shared.txt"
|
||||
sb $DARK -- bash -c "cat '$WORK/not-shared.txt' 2>&1 || echo NOT_VISIBLE"
|
||||
assert_contains "NOT_VISIBLE" "the parent directory is not mounted"
|
||||
sb $DARK -- bash -c 'ls /root 2>&1 | head -1; test -e /etc/shadow && echo has_shadow'
|
||||
assert_absent "$(hostname)" "the host filesystem layout is gone"
|
||||
|
||||
t_name "environment: deny by default, allow on request"
|
||||
sb $DARK -- bash -c 'echo "canary=[${AISB_CANARY_IN:-unset}]"; echo "home=$HOME"; echo "aisb=$AISB"'
|
||||
assert_contains "canary=[unset]" "a host variable does not leak in"
|
||||
assert_contains "home=/home/sandbox" "HOME is the container's own"
|
||||
assert_contains "aisb=1" "the sandbox marks itself"
|
||||
sb $DARK --env AISB_CANARY_IN -- bash -c 'echo "canary=[$AISB_CANARY_IN]"'
|
||||
assert_contains "canary=[canary-value-42]" "--env passes exactly what was asked for"
|
||||
sb $DARK --env AISB_CANARY_OUT -- bash -c 'true'
|
||||
assert_rc 1 "an unset --env variable fails the run closed"
|
||||
|
||||
t_name "the value never appears in a command line"
|
||||
( sb $DARK --env AISB_CANARY_IN -- bash -c 'sleep 2' >/dev/null 2>&1 ) &
|
||||
RUNPID=$!
|
||||
sleep 1.5
|
||||
# Every process the value could legitimately have been handed to: the container
|
||||
# runtime. argv[0] is matched exactly so the test's own shell (whose command line
|
||||
# contains this very string) can never be the thing that matches.
|
||||
# (the bracket keeps this grep from matching its own command line)
|
||||
LEAKY="$(ps -ewwo comm=,args= | awk '{a=$1; sub(".*/","",a)} a ~ /^(podman|crun|conmon|catatonit)$/' \
|
||||
| grep -c 'canar[y]-value-42' || true)"
|
||||
assert_eq "0" "$LEAKY" "no podman/crun/conmon argv carries the secret"
|
||||
wait $RUNPID 2>/dev/null
|
||||
|
||||
t_name "the env file is 0600 while running and gone afterwards"
|
||||
( sb $DARK --env AISB_CANARY_IN -- bash -c 'sleep 2' >/dev/null 2>&1 ) &
|
||||
RUNPID2=$!
|
||||
sleep 1.5
|
||||
ENVF="$(find "$AISB_STATE_DIR/sessions" -name env -print -quit 2>/dev/null)"
|
||||
if [[ -n "$ENVF" ]]; then
|
||||
assert_file_mode "$ENVF" 600 "the env file is owner-read/write only"
|
||||
grep -q 'AISB_CANARY_IN=canary-value-42' "$ENVF" && _ok "and it holds the value we asked for" || _bad "env file content"
|
||||
else
|
||||
_bad "the env file is owner-read/write only" "no env file found while the container was running"
|
||||
fi
|
||||
wait $RUNPID2 2>/dev/null
|
||||
sb $DARK --env AISB_CANARY_IN -- true
|
||||
LEFT="$(find "$AISB_STATE_DIR/sessions" -name env 2>/dev/null | wc -l | tr -d ' ')"
|
||||
assert_eq "0" "$LEFT" "the env file is deleted when the run ends, even with --keep"
|
||||
sb $DARK --keep --env AISB_CANARY_IN -- true
|
||||
LEFT2="$(find "$AISB_STATE_DIR/sessions" -name env 2>/dev/null | wc -l | tr -d ' ')"
|
||||
assert_eq "0" "$LEFT2" "--keep keeps the container, not the secret"
|
||||
|
||||
t_name "extra mounts"
|
||||
mkdir -p "$WORK/ro-src" "$WORK/rw-src"; echo data > "$WORK/ro-src/file"
|
||||
sb $DARK --mount "$WORK/ro-src:/ro:ro" --mount "$WORK/rw-src:/rw" -- bash -c '
|
||||
cat /ro/file
|
||||
touch /rw/created && echo rw_ok
|
||||
touch /ro/writable 2>/dev/null && echo "BAD: ro mount is writable" || echo ro_locked=ok
|
||||
'
|
||||
assert_contains "data" "read-only mount is readable"
|
||||
assert_contains "rw_ok" "read-write mount is writable"
|
||||
assert_contains "ro_locked=ok" "ro really means ro"
|
||||
|
||||
t_name "the home directory"
|
||||
sb $DARK --ephemeral-home -- bash -c 'echo hi > "$HOME/scratch"; ls -A "$HOME" | tr "\n" " "'
|
||||
assert_contains "scratch" "home is writable with an ephemeral mount"
|
||||
sb $DARK --home-cache -- bash -c 'echo cached > "$HOME/cached-marker"'
|
||||
sb $DARK --home-cache -- bash -c 'cat "$HOME/cached-marker"'
|
||||
assert_contains "cached" "the home cache volume persists across runs"
|
||||
sb $DARK --ephemeral-home -- bash -c 'test -f "$HOME/cached-marker" && echo LEAK || echo isolated'
|
||||
assert_contains "isolated" "an ephemeral home does not see it"
|
||||
|
||||
t_name "read-only root filesystem"
|
||||
sb $DARK --read-only -- bash -c 'touch /usr/bin/nope 2>/dev/null && echo "BAD: wrote to rootfs" || echo rootfs_locked=ok; touch /project/rw-still-works && echo project_still_writable'
|
||||
assert_contains "rootfs_locked=ok" "--read-only blocks writes outside mounts"
|
||||
assert_contains "project_still_writable" "the project mount stays writable"
|
||||
|
||||
t_name "lifecycle"
|
||||
BEFORE="$(podman ps -a --filter label=ai-sandbox=1 -q | wc -l | tr -d ' ')"
|
||||
sb $DARK -- true
|
||||
AFTER="$(podman ps -a --filter label=ai-sandbox=1 -q | wc -l | tr -d ' ')"
|
||||
assert_eq "$BEFORE" "$AFTER" "the container is removed when the command exits"
|
||||
sb $DARK --keep --name aisb-keeptest -- true
|
||||
podman ps -a --format '{{.Names}}' | grep -qx aisb-keeptest && _ok "--keep leaves the container" || _bad "--keep leaves the container"
|
||||
SESSIONS_KEPT="$(ls -d "$AISB_STATE_DIR"/sessions/* 2>/dev/null | wc -l | tr -d ' ')"
|
||||
[[ "$SESSIONS_KEPT" -ge 1 ]] && _ok "--keep leaves the session dir for inspection" || _bad "--keep leaves the session dir"
|
||||
sb clean --all > /dev/null
|
||||
GONE="$(podman ps -a --filter name=aisb-keeptest -q | wc -l | tr -d ' ')"
|
||||
assert_eq "0" "$GONE" "clean removes leftovers"
|
||||
|
||||
t_name "clean --volumes reclaims the home cache, carefully"
|
||||
podman volume create not-aisb-decoy > /dev/null 2>&1
|
||||
podman volume create aisb-other-dangling > /dev/null 2>&1
|
||||
AISB_HOME_VOLUME=aisb-test-home sb $DARK -- true
|
||||
if podman volume exists aisb-test-home > /dev/null 2>&1; then _ok "the run created its home cache volume"; else _bad "the run created its home cache volume"; fi
|
||||
sb clean --volumes=aisb-test-home > /dev/null
|
||||
if podman volume exists aisb-test-home > /dev/null 2>&1; then _bad "clean --volumes=NAME reclaimed the unused volume"; else _ok "clean --volumes=NAME reclaimed the unused volume"; fi
|
||||
if podman volume exists aisb-other-dangling > /dev/null 2>&1; then _ok "an unmatched volume was left alone"; else _bad "an unmatched volume was left alone"; fi
|
||||
|
||||
# a live sandbox's home is never reclaimed while it runs
|
||||
AISB_HOME_VOLUME=aisb-test-home2 sb $DARK --name aisb-vollive -- bash -c 'sleep 25' > /dev/null 2>&1 &
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
||||
podman ps --filter name=aisb-vollive -q | grep -q . && break
|
||||
sleep 0.5
|
||||
done
|
||||
AISB_HOME_VOLUME=aisb-test-home2 sb clean --volumes=aisb-test-home2 > /dev/null
|
||||
if podman volume exists aisb-test-home2 > /dev/null 2>&1; then _ok "a running sandbox keeps its home volume"; else _bad "a running sandbox keeps its home volume"; fi
|
||||
podman rm -f aisb-vollive > /dev/null 2>&1
|
||||
AISB_HOME_VOLUME=aisb-test-home2 sb clean --volumes=aisb-test-home2 > /dev/null
|
||||
if podman volume exists aisb-test-home2 > /dev/null 2>&1; then _bad "it is reclaimed once nothing references it"; else _ok "it is reclaimed once nothing references it"; fi
|
||||
podman volume rm not-aisb-decoy aisb-other-dangling > /dev/null 2>&1
|
||||
|
||||
t_name "the session log records what ran, without secrets"
|
||||
LOG="$(cat "$AISB_STATE_DIR/log" 2>/dev/null || true)"
|
||||
printf '%s' "$LOG" | grep -q 'net=none' && _ok "the log records the run" || _bad "the log records the run"
|
||||
printf '%s' "$LOG" | grep -q 'canary-value-42' && _bad "secrets must never reach the log" || _ok "no secrets in the log"
|
||||
|
||||
t_name "doctor"
|
||||
sb doctor
|
||||
assert_rc 0 "doctor reports no problems"
|
||||
assert_contains "bind mount + uid mapping" "doctor verifies uid mapping"
|
||||
assert_contains "rootless" "doctor verifies rootless podman"
|
||||
Reference in New Issue
Block a user