Files
ai-sandbox/tests/unit/08_pi_config.sh
T
ducoterra 8ee535f6ae bridge the host pi config, with the credential by env only
The image ships pi with no provider and no preferences, so a sandboxed run
meant configuring the model twice. ai-sandbox now reads the host
~/.pi/agent/{models,settings}.json and rewrites them instead of copying them:
every provider apiKey becomes "$AI_API_TOKEN" (pi resolves an env reference
itself, so no value is written to disk), '!command' header values are dropped
rather than executed, settings that point into $HOME are dropped because that
path is not mounted, and auth.json / models-store.json / sessions / extensions
are never read.

The rewritten pair is seeded into /home/sandbox/.pi/agent at container start,
owned by uid 1000 and mode 600, and is treated as managed: a host change is
re-seeded, an edit made inside the sandbox is kept and reported, a deletion
takes the host version again.

The token follows the existing environment rules -- one variable, passed by
name through the 0600 env file, announced when auto-passed. A bridged run
whose token variable is unset or empty is refused instead of starting a pi
that cannot reach its model, and the bridge never widens the allowlist: it
warns with the exact --allow to add.

Docs and examples now use a self-hosted gateway (https://aipi.reeseapps.com)
instead of vendor API endpoints, since that is the only thing this tool is
used against. 391 assertions pass (make lint, ./tests/run.sh --e2e).

LLM-Generated: true
2026-10-27 17:29:17 -04:00

225 lines
10 KiB
Bash

#!/usr/bin/env bash
# tests/unit/08_pi_config.sh -- the pi configuration bridge.
#
# The bridge rewrites the host agent directory instead of copying it, so what matters
# most here is what does NOT cross the boundary: the credential, host commands, host
# paths, auth.json. Everything is preview (`--print-policy` / `--dry-run`) plus the
# refusal paths, so no container is started and no token is real.
source "$TESTS/lib.sh"
AGENT="$WORK/agent"; mkdir -p "$AGENT"
export AISB_PI_AGENT_DIR="$AGENT" AISB_PI_TOKEN_AUTO=1
export AI_API_TOKEN=sandbox-only-token-4242
CANARY_SECRET='REALSECRET-nev3r-lef7s-the-host'
CANARY_CMD='pass show services/gateway | head -1'
cat > "$AGENT/models.json" <<EOF
{
"providers": {
"gateway": {
"baseUrl": "https://aipi.reeseapps.com/v1",
"api": "openai-completions",
"apiKey": "!$CANARY_CMD",
"headers": { "X-Trace": "!date +%s", "X-Team": "self" },
"models": [
{ "id": "juggernaut", "reasoning": true, "contextWindow": 131072 },
{ "id": "turbo", "reasoning": true, "contextWindow": 262144 }
]
},
"lab-ollama": {
"baseUrl": "http://10.20.0.7:11434/v1",
"api": "openai-completions",
"models": [ { "id": "small" } ]
}
}
}
EOF
# sessionDir points at the *real* host home on purpose: that is the case the bridge
# has to drop, and HOME must stay the real home for podman's sake.
cat > "$AGENT/settings.json" <<EOF
{
"defaultProvider": "gateway",
"defaultModel": "turbo",
"theme": "dark/dark",
"defaultThinkingLevel": "xhigh",
"enableInstallTelemetry": false,
"thinkingBudgets": { "xhigh": 65536 },
"sessionDir": "$HOME/pi-sessions"
}
EOF
printf '{ "gateway": "%s" }' "$CANARY_SECRET" > "$AGENT/auth.json"
printf '{}' > "$AGENT/models-store.json"
clear_sessions() { rm -rf "${AISB_STATE_DIR:?}"/sessions/*; }
gen_file() { find "$AISB_STATE_DIR/sessions" -path "*/pi/$1" -print -quit 2>/dev/null; }
restore_models() { cat > "$AGENT/models.json" <<EOF
{ "providers": { "gateway": { "baseUrl": "https://aipi.reeseapps.com/v1",
"api": "openai-completions", "apiKey": "literal-key-never-copied",
"models": [ { "id": "turbo" } ] } } }
EOF
}
t_name "the credential becomes an environment reference, not a copy"
clear_sessions
sb --keep --print-policy --net none
assert_contains "pi_config=bridged from $AGENT" "the bridge reports where the config came from"
assert_contains "pi_token_env=AI_API_TOKEN (set)" "and whether the token is in reach"
M="$(gen_file models.json)"
[[ -n "$M" ]] && _ok "models.json was generated" || _bad "models.json was generated"
if [[ -n "$M" ]]; then
grep -qF '"apiKey": "$AI_API_TOKEN"' "$M" && _ok "apiKey is the variable; pi resolves it at request time" || _bad "apiKey is the variable; pi resolves it at request time"
grep -qF "$CANARY_CMD" "$M" && _bad "the host '!command' is gone" || _ok "the host '!command' is gone"
grep -qF "$CANARY_SECRET" "$M" && _bad "the literal key is gone" || _ok "the literal key is gone"
grep -qF 'literal-key-never-copied' "$M" && _bad "no literal key from the host file" || _ok "no literal key from the host file"
grep -qF '"X-Team": "self"' "$M" && _ok "an ordinary header survives" || _bad "an ordinary header survives"
grep -qF 'X-Trace' "$M" && _bad "a header built by a host command is dropped" || _ok "a header built by a host command is dropped"
fi
assert_contains "dropped header(s) X-Trace" "and it says which one it dropped"
if printf '%s' "$OUT" | grep -qF "$AI_API_TOKEN"; then
_bad "the token value never appears in the policy output"
else
_ok "the token value never appears in the policy output"
fi
t_name "providers and models are carried over untouched"
if [[ -n "$M" ]]; then
grep -qF '"baseUrl": "https://aipi.reeseapps.com/v1"' "$M" && _ok "the gateway endpoint" || _bad "the gateway endpoint"
grep -qF '"id": "juggernaut"' "$M" && grep -qF '"id": "turbo"' "$M" && _ok "both models" || _bad "both models"
grep -qF '"contextWindow": 262144' "$M" && _ok "with their limits" || _bad "with their limits"
grep -qF '"lab-ollama"' "$M" && _ok "and the second provider" || _bad "and the second provider"
fi
JSON_CHECK="$(python3 - "${M:-/nonexistent}" <<'PY'
import json, sys
try:
prov = json.load(open(sys.argv[1]))["providers"]
except Exception as e:
print(f"invalid:{e}"); raise SystemExit
if "apiKey" in prov.get("lab-ollama", {}):
print("gained-a-key")
elif prov["gateway"]["apiKey"] != "$AI_API_TOKEN":
print("wrong-key-ref")
else:
print("json-valid")
PY
)"
assert_eq "json-valid" "$JSON_CHECK" "valid JSON, and a provider without a key does not gain one"
assert_contains "pi_endpoints=10.20.0.7 aipi.reeseapps.com" "both endpoints are reported"
t_name "settings travel, host paths do not"
S="$(gen_file settings.json)"
if [[ -n "$S" ]]; then
for pair in '"defaultProvider": "gateway"' '"defaultModel": "turbo"' '"theme": "dark/dark"' \
'"defaultThinkingLevel": "xhigh"' '"xhigh": 65536' '"enableInstallTelemetry": false'; do
grep -qF "$pair" "$S" && _ok "kept $pair" || _bad "kept $pair"
done
grep -qF 'sessionDir' "$S" && _bad "a value pointing at the host home is dropped" || _ok "a value pointing at the host home is dropped"
fi
assert_contains "dropped settings sessionDir" "and it says so"
t_name "auth.json and the credential store are never bridged"
if find "$AISB_STATE_DIR/sessions" \( -name auth.json -o -name models-store.json \) | grep -q .; then
_bad "only models.json and settings.json are bridged"
else
_ok "only models.json and settings.json are bridged"
fi
if grep -rlF "$CANARY_SECRET" "$AISB_STATE_DIR" "$AISB_CACHE_DIR" > /dev/null 2>&1; then
_bad "the host credential appears nowhere in ai-sandbox state"
else
_ok "the host credential appears nowhere in ai-sandbox state"
fi
t_name "the token is passed by name, once, and announced"
assert_contains "passing \$AI_API_TOKEN from the host" "the auto-pass is announced"
assert_eq "env_names=AI_API_TOKEN" "$(printf '%s' "$OUT" | grep '^env_names=' || true)" "exactly one variable is passed"
assert_absent "$AI_API_TOKEN" "and only its name is reported"
t_name "--no-pi-token-env leaves it out and says so"
clear_sessions
sb --keep --no-pi-token-env --print-policy --net none
assert_eq "env_names=" "$(printf '%s' "$OUT" | grep '^env_names=' || true)" "nothing is passed"
assert_contains "will not reach the container" "preview warns instead of launching a pi with no credential"
t_name "a bridged provider with no credential in reach is refused, not attempted"
unset AI_API_TOKEN
sb --net none --harness pi
assert_rc 1 "the run refuses to start"
assert_contains 'asks for $AI_API_TOKEN, but it is not going to reach the container' "it names the variable and why"
assert_contains "env = AI_API_TOKEN" "it shows the config-file way"
assert_contains "--no-pi-config" "and the way to opt out"
assert_eq "0" "$([[ -n "$(podman ps -q --filter label=ai-sandbox=1)" ]] && echo 1 || echo 0)" "and nothing was started"
t_name "an empty token is treated as missing"
sb_env AI_API_TOKEN= --net none --harness pi # set, but empty
assert_rc 1 "an empty credential is refused, not sent"
assert_contains "unset or empty" "and it says which"
sb_env AI_API_TOKEN= --net none --print-policy
assert_contains "pi_token_env=AI_API_TOKEN (set but empty)" "preview distinguishes the two states"
t_name "but only when pi is the thing being launched"
export AI_API_TOKEN=x
sb --net none --dry-run -- make test
assert_absent "not going to reach the container" "a replaced command needs no credential"
sb --net none --dry-run --shell
assert_rc 0 "--shell works without a token"
assert_absent "not going to reach the container" "and does not demand one"
sb --net none --dry-run --harness codex
assert_absent "not going to reach the container" "codex does not read pi's models.json"
t_name "--no-pi-config opts out cleanly"
sb --no-pi-config --net none --print-policy
assert_absent "pi_config=" "no bridge is reported"
assert_absent "not going to reach the container" "and nothing is required"
clear_sessions
sb --keep --no-pi-config --net none -- true
if [[ -n "$(gen_file models.json)" ]]; then _bad "nothing was generated"; else _ok "nothing was generated"; fi
t_name "the token variable name is yours to choose"
clear_sessions
sb --keep --pi-token-env GW_TOKEN --print-policy --net none
assert_contains "pi_token_env=GW_TOKEN (NOT SET)" "reported under the new name"
clear_sessions
sb_env GW_TOKEN=whatever-gw --keep --pi-token-env GW_TOKEN --print-policy --net none
assert_contains "pi_token_env=GW_TOKEN (set)" "it is recognised as in reach"
assert_contains "passing \$GW_TOKEN" "and announced under that name"
M2="$(gen_file models.json)"
grep -qF '"apiKey": "$GW_TOKEN"' "${M2:-/nonexistent}" && _ok "the generated config references it" || _bad "the generated config references it"
sb --net none --harness pi --pi-token-env GW_TOKEN # GW_TOKEN is not set
assert_rc 1 "and it is enforced under that name too"
t_name "a missing agent directory is a skip, not an error"
sb_env AISB_PI_AGENT_DIR="$WORK/nope" --net none --print-policy
assert_rc 0 "nothing to bridge, nothing demanded"
assert_absent "pi_config=" "and no bridge is claimed"
sb_env AISB_PI_AGENT_DIR="$WORK/nope" --net none --harness pi
assert_rc 0 "pi may still be launched, with its own defaults"
t_name "broken JSON is reported, not propagated"
printf '{ "providers": oops' > "$AGENT/models.json"
clear_sessions
sb --keep --net none --print-policy
assert_contains "models.json is not valid JSON" "it says the file is broken"
if grep -qF 'oops' "${AISB_STATE_DIR}/sessions"/*/mnt/pi/models.json 2>/dev/null; then
_bad "no invalid file is placed in front of pi"
else
_ok "no invalid file is placed in front of pi"
fi
restore_models
t_name "the model endpoint has to be allowed, and says when it is not"
sb --net allow --allow registry.npmjs.org --print-policy
assert_contains "the model endpoint is not in the allowlist" "a pi that cannot reach its gateway is a warning, not a mystery"
assert_contains "add --allow aipi.reeseapps.com" "and it names the host to add"
sb --net allow --allow aipi.reeseapps.com --allow 10.20.0.7 --print-policy
assert_absent "not in the allowlist" "silent once both endpoints are allowed"
t_name "the bridge never widens the allowlist by itself"
sb --net allow --allow aipi.reeseapps.com --print-policy
assert_contains "gate_domains=aipi.reeseapps.com" "what you asked for is what you got"
assert_absent "10.20.0.7 80,443" "the discovered endpoint was not added on your behalf"
t_name "an invalid token variable name is rejected"
sb --net none --pi-token-env '9bad name' --print-policy
assert_rc 1 "rejected"
assert_contains "not a valid identifier" "with a clear reason"