diff --git a/bin/ai-sandbox b/bin/ai-sandbox index 02e8ffc..d004c88 100755 --- a/bin/ai-sandbox +++ b/bin/ai-sandbox @@ -348,6 +348,13 @@ cfg_read_allowlist() { # one target per line line="$(trim "${line%%#*}")" [[ -n "$line" ]] && CFG_allow+=("$line") done < "$file" + # A loop returns the status of the last command its body ran, and here that is the `&&` + # above: a file whose last line is a comment or blank would return 1. load_config() calls + # this twice in a non-final position, so under `set -e` every run would die with exit 1 + # and no output at all -- previews included. A blank last line is not an error, so reading a + # file always succeeds; an allowlist with nothing in it is validate_opts()'s call to + # refuse, with a message, rather than a silent death here. + return 0 } load_config() { diff --git a/tests/unit/02_config.sh b/tests/unit/02_config.sh index a2fa6d4..5c0d162 100755 --- a/tests/unit/02_config.sh +++ b/tests/unit/02_config.sh @@ -76,3 +76,25 @@ 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 +# 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"