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
This commit is contained in:
2026-10-27 17:29:17 -04:00
parent 0242ab5b5e
commit 8ee535f6ae
14 changed files with 740 additions and 49 deletions
+60
View File
@@ -147,6 +147,66 @@ 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 "the pi config bridge lands inside the container"
AGENT="$WORK/agent"; mkdir -p "$AGENT"
cat > "$AGENT/models.json" <<EOF
{ "providers": { "gateway": { "baseUrl": "https://aipi.reeseapps.com/v1",
"api": "openai-completions", "apiKey": "!pass show gateway",
"models": [ { "id": "turbo", "contextWindow": 262144 } ] } } }
EOF
printf '{ "defaultProvider": "gateway", "defaultModel": "turbo", "theme": "dark/dark" }\n' > "$AGENT/settings.json"
printf '{ "gateway": "HOSTSECRET-must-not-travel" }' > "$AGENT/auth.json"
export AISB_PI_AGENT_DIR="$AGENT" AISB_PI_TOKEN_ENV=AI_API_TOKEN AISB_PI_TOKEN_AUTO=1
export AI_API_TOKEN=container-token-777
export AISB_HOME_VOLUME=aisb-test-pihome
sb $DARK --harness pi -- bash -lc '
f="$HOME/.pi/agent/models.json"
[ -f "$f" ] && echo seeded=yes || echo seeded=no
[ "$(id -u)" = 1000 ] || echo uid=wrong
echo "mode=$(stat -c %a "$f" 2>/dev/null)"
grep -q "\$AI_API_TOKEN" "$f" && echo ref=env || echo ref=other
grep -q "pass show" "$f" && echo LEAKED_CMD || echo cmd=gone
grep -q "HOSTSECRET" "$f" && echo LEAKED_KEY || echo key=gone
[ -f "$HOME/.pi/agent/auth.json" ] && echo LEAKED_AUTH || echo auth=absent
grep -q "turbo" "$HOME/.pi/agent/settings.json" && echo settings=yes
[ -n "$AI_API_TOKEN" ] && echo token=present || echo token=missing'
assert_contains "seeded=yes" "pi's config is seeded into the sandbox home"
assert_contains "mode=600" "and only the sandbox user can read it"
assert_contains "ref=env" "the credential stays an environment reference"
assert_contains "cmd=gone" "the host '!command' did not cross"
assert_contains "key=gone" "nor did the literal key"
assert_contains "auth=absent" "auth.json was not bridged"
assert_contains "settings=yes" "settings.json is in place"
assert_contains "token=present" "and the token reaches pi by env"
assert_absent "container-token-777" "while its value is never printed"
assert_absent "uid=wrong" "seeded files belong to the sandbox user"
# an edit made inside the sandbox survives; a change on the host does not get lost
sb $DARK --harness pi -- bash -c 'sed -i "s/turbo/juggernaut/" "$HOME/.pi/agent/models.json"; echo edited' >/dev/null
sb $DARK --harness pi -- bash -lc 'grep -q juggernaut "$HOME/.pi/agent/models.json" && echo edit_kept || echo edit_lost'
assert_contains "edit_kept" "a change made inside the sandbox is not clobbered"
sb $DARK --harness pi -- bash -lc 'grep -q juggernaut "$HOME/.pi/agent/models.json" && echo still_here' 2>/dev/null
sed -i 's/262144/524288/' "$AGENT/models.json"
sb $DARK --harness pi -- bash -lc '
grep -q 524288 "$HOME/.pi/agent/models.json" && echo host_change_ignored_fully || echo host_change_partly_ignored
grep -q juggernaut "$HOME/.pi/agent/models.json" && echo edit_still_kept'
assert_contains "edit_still_kept" "the sandbox edit still wins over the bridged file"
sb $DARK --harness pi -- bash -c 'rm -f "$HOME/.pi/agent/models.json"; echo reset' >/dev/null
sb $DARK --harness pi -- bash -lc 'grep -q 524288 "$HOME/.pi/agent/models.json" && echo reseeded || echo not_reseeded'
assert_contains "reseeded" "deleting it inside the sandbox takes the host version again"
unset AI_API_TOKEN
sb_timeout 90 $DARK --harness pi
assert_rc 1 "a run without the credential is refused before anything starts"
assert_contains "not going to reach the container" "and it says why"
sb_env AI_API_TOKEN= --net none --harness pi
assert_rc 1 "an empty token is refused too -- pi would send an empty credential"
unset AISB_PI_AGENT_DIR AISB_PI_TOKEN_ENV AISB_PI_TOKEN_AUTO AISB_HOME_VOLUME
sb clean --all --volumes=aisb-test-pihome > /dev/null
t_name "doctor"
sb doctor
assert_rc 0 "doctor reports no problems"