#!/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"