Files
ai-sandbox/tests/lib.sh
T
ducoterra 03ab9c0cb1 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
2026-10-27 15:11:29 -04:00

76 lines
2.6 KiB
Bash

#!/usr/bin/env bash
# tests/lib.sh -- tiny assertion helpers for the ai-sandbox test suite.
#
# Every test file gets: AISB (path to the script), WORK (scratch dir), and uses
# sb() to run the sandbox with fully redirected XDG dirs so nothing touches the
# real machine.
PASS=0
FAIL=0
FAILED_NAMES=()
_current=""
t_name() { _current="$1"; printf '\n\033[1m# %s\033[0m\n' "$1"; }
_ok() { PASS=$((PASS + 1)); printf ' \033[32mok\033[0m %s\n' "$1"; }
_bad() { FAIL=$((FAIL + 1)); FAILED_NAMES+=("$_current :: $1"); printf ' \033[31mFAIL\033[0m %s\n' "$1"; shift || true;
[[ -n "${1:-}" ]] && printf ' %s\n' "$1"; return 0; }
# sb [args...] -> sets RC, OUT (stdout+stderr)
sb() {
OUT="$("$AISB" "$@" 2>&1)" && RC=0 || RC=$?
return 0
}
# sb_env VAR=val ... -- args (env prefix, without leaking into the test shell)
sb_env() {
local -a envs=()
while (($#)) && [[ "$1" != -* && "$1" == *=* ]]; do envs+=("$1"); shift; done
OUT="$(env "${envs[@]}" "$AISB" "$@" 2>&1)" && RC=0 || RC=$?
return 0
}
assert_rc() { # assert_rc <expected> <label>
if [[ "$RC" == "$1" ]]; then _ok "$2"
else _bad "$2" "expected rc=$1 got rc=$RC; output: $(printf '%s' "$OUT" | head -3 | tr '\n' ' ')"; fi
}
assert_contains() { # assert_contains <needle> <label>
if printf '%s' "$OUT" | grep -qF -- "$1"; then _ok "$2"
else _bad "$2" "missing: '$1' in: $(printf '%s' "$OUT" | head -5 | tr '\n' ' ')"; fi
}
assert_absent() { # assert_absent <needle> <label>
if printf '%s' "$OUT" | grep -qF -- "$1"; then
_bad "$2" "unexpectedly present: '$1'"
else _ok "$2"; fi
}
assert_eq() { # assert_eq <expected> <actual> <label>
if [[ "$1" == "$2" ]]; then _ok "$3"
else _bad "$3" "expected '$1' got '$2'"; fi
}
assert_count() { # assert_count <expected> <needle> <label>
local n
n="$(printf '%s' "$OUT" | grep -oF -- "$2" | wc -l | tr -d ' ')"
if [[ "$n" == "$1" ]]; then _ok "$3"
else _bad "$3" "expected $1 occurrence(s) of '$2', got $n"; fi
}
assert_file_mode() { # assert_file_mode <path> <mode> <label>
local m; m="$(stat -c '%a' "$1" 2>/dev/null || echo missing)"
if [[ "$m" == "$2" ]]; then _ok "$3"; else _bad "$3" "expected mode $2 got $m"; fi
}
summary() {
printf '\n------------------------------------------------------------\n'
[[ -n "${AISB_TEST_COUNTS:-}" ]] && printf '%s %s\n' "$PASS" "$FAIL" >> "$AISB_TEST_COUNTS"
if (( FAIL )); then
printf '\033[31m%d passed, %d FAILED\033[0m\n' "$PASS" "$FAIL"
local f; for f in "${FAILED_NAMES[@]}"; do printf ' - %s\n' "$f"; done
return 1
fi
printf '\033[32m%d passed, 0 failed\033[0m\n' "$PASS"
}