#!/usr/bin/env bash # tests/unit/02_config.sh -- config files, allowlists and precedence. source "$TESTS/lib.sh" CFG="$WORK/cfg" REPO="$WORK/repo" mkdir -p "$CFG" "$REPO/.ai-sandbox" export AISB_CONFIG_DIR="$CFG" cd "$REPO" || exit 1 t_name "allow files are merged" cat > "$CFG/allow" <<'EOF' # user-wide allowlist api.openai.com registry.npmjs.org # trailing comment EOF cat > "$REPO/.ai-sandbox/allow" <<'EOF' pypi.org EOF sb --print-policy --net allow assert_rc 0 "print-policy runs" assert_contains "api.openai.com" "user allowlist entry present" assert_contains "registry.npmjs.org" "user entry with trailing comment present" assert_contains "pypi.org" "repo allowlist entry present" t_name "dedup" sb --print-policy --net allow --allow pypi.org --allow pypi.org ALLOW_LINE="$(printf '%s' "$OUT" | grep '^allow=')" N="$(printf '%s' "$ALLOW_LINE" | grep -o 'pypi.org' | wc -l | tr -d ' ')" assert_eq "1" "$N" "duplicate allow entries collapse into one" t_name "key = value config" cat > "$CFG/config" <<'EOF' net = none harness = none cpus = 7 memory = 1536m pids = 77 selinux = :Z EOF sb --print-policy assert_contains "net=none" "config file sets net" assert_contains "harness=none" "config file sets harness" assert_contains "cpus:7" "config file sets cpus" assert_contains "memory:1536m" "config file sets memory" assert_contains "pids:77" "config file sets pids" assert_contains "selinux=:Z" "config file sets selinux" t_name "precedence: flag > env > config file" sb --print-policy --net allow assert_contains "net=allow" "flag beats config file" sb_env AISB_NET=full --print-policy assert_contains "net=full" "AISB_* env beats config file" sb_env AISB_NET=full --print-policy --net none assert_contains "net=none" "flag beats AISB_* env" t_name "AISB_ALLOW / AISB_ENV lists" sb_env AISB_ALLOW=a.example.com,b.example.com:8443 --print-policy --net none assert_contains "a.example.com" "AISB_ALLOW entry 1" assert_contains "b.example.com:8443" "AISB_ALLOW entry 2 with port" sb_env AISB_ENV=HOME_X,TMPDIR --print-policy --net none assert_contains "env_names=HOME_X TMPDIR" "AISB_ENV populates env names" assert_contains "env_missing=HOME_X" "unset names are reported by --print-policy" t_name "repo config wins per-directory" cat > "$REPO/.ai-sandbox/config" <<'EOF' pids = 42 EOF sb --print-policy --net none assert_contains "pids:42" "repo config overrides user config" t_name "unknown keys warn but do not break" echo 'bogus_key = 1' >> "$CFG/config" sb --print-policy --net none assert_contains "unknown key: bogus_key" "unknown key warns" assert_rc 0 "unknown key is not fatal" t_name "an allow file may end on a comment or a blank line" # Regression: cfg_read_allowlist() ended on its `[[ -n "$line" ]]`, so a file whose last # line yields no target returned 1. load_config() calls it in a non-final position, so under # `set -e` every run died with exit 1 and *zero* bytes of output -- --dry-run and # --print-policy included. The test above ends its file on "host # trailing comment", which # yields a target and so never exercised this. for last in '# a trailing comment' '' ' '; do printf '# user-wide allowlist\napi.openai.com\n%s\n' "$last" > "$CFG/allow" printf 'pypi.org\n%s\n' "$last" > "$REPO/.ai-sandbox/allow" sb --print-policy --net allow assert_rc 0 "user allow file ending on [$last] is not fatal" assert_contains "api.openai.com" "user target survives a file ending on [$last]" assert_contains "pypi.org" "repo target survives a file ending on [$last]" done t_name "publish accumulates like the other list keys" cat > "$CFG/config" <<'EOF' publish = 8000 EOF cat > "$REPO/.ai-sandbox/config" <<'EOF' publish = 18080:8000 EOF sb --print-policy --net allow --allow example.com assert_contains "publish=127.0.0.1:8000:8000" "user config publishes" assert_contains "127.0.0.1:18080:8000" "repo config adds to it rather than replacing it" sb_env AISB_PUBLISH=9000,9100:80 --print-policy --net allow --allow example.com assert_contains "127.0.0.1:9000:9000" "AISB_PUBLISH is a comma list of the same shapes" assert_contains "127.0.0.1:9100:80" " ... including host:container" sb --print-policy --net allow --allow example.com --publish 9200 assert_contains "127.0.0.1:9200:9200" "a flag adds to both files" sb --print-policy --net allow --allow example.com --publish 8000 N="$(printf '%s' "$OUT" | grep -o '127.0.0.1:8000:8000' | wc -l | tr -d ' ')" assert_eq "1" "$N" "the same port from two sources collapses to one" t_name "publish_addr is the default bind address, and only that" cat > "$CFG/config" <<'EOF' publish_addr = 127.0.0.2 EOF rm -f "$REPO/.ai-sandbox/config" sb --print-policy --net allow --allow example.com --publish 8000 assert_contains "publish=127.0.0.2:8000:8000" "config file sets the address a bare port binds" sb_env AISB_PUBLISH_ADDR=127.0.0.3 --print-policy --net allow --allow example.com --publish 8000 assert_contains "127.0.0.3:8000:8000" "AISB_PUBLISH_ADDR beats the config file" sb_env AISB_PUBLISH_ADDR=localhost --print-policy --net allow --allow example.com --publish 8000 assert_contains "publish=localhost:8000:8000" "a name is accepted where an address goes" printf '%s' "$OUT" | grep -qF 'route to this machine' \ && _bad "localhost warned about as if it were the LAN" || _ok "localhost counts as loopback: no LAN warning" sb --print-policy --net allow --allow example.com --publish 0.0.0.0:8000:8000 assert_contains "publish=0.0.0.0:8000:8000" "an address written in the spec is the address used" printf '%s' "$OUT" | grep '^publish=' | grep -qF '127.0.0.2' \ && _bad "publish_addr overwrote an explicit address" || _ok "publish_addr only fills in a bare port" # The fix must not trade one silence for another: with no target anywhere, the run still # refuses, and says why. printf '# nothing but a header here\n' > "$CFG/allow" printf '# nothing but a header here\n' > "$REPO/.ai-sandbox/allow" sb_timeout 20 --print-policy --net allow assert_rc 1 "a header-only allow file still refuses to start" assert_contains "no egress allowlist configured" "and refuses out loud"