Pass a custom template and mixin kits through to sbx

Sandboxes ignore the host ~/.claude by design: the agent runs as a separate
user with HOME elsewhere, so even a read-only mount is not picked up. Skills
can be shared with sbx skills import, but plugins carry commands, hooks and
MCP servers that only a custom image can deliver.

Adds --template, --stock-template and a repeatable --kit, persisted per
repository so run and refresh reuse them. AI_SBX_TEMPLATE supplies the default
image, so one custom template can be declared once in the user's mise config
and apply to every repository, with --template overriding it per repository
and --stock-template opting out.

save_config now packs two arrays into one argument list separated by a count,
so it ships with a round-trip test covering empty arrays, values containing
spaces, and the boundary between kits and AWS profiles.
This commit is contained in:
2026-07-30 16:29:25 -05:00
parent 4af8c1c153
commit ace4e81f97
3 changed files with 184 additions and 3 deletions
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
# shellcheck source-path=SCRIPTDIR
# shellcheck source=tasks/ai/sbx
source "$(dirname "${BASH_SOURCE[0]}")/../tasks/ai/sbx"
failures=0
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
failures=$((failures + 1))
}
REPOSITORY="CareEvolution/api-portal"
SANDBOX_NAME="ai-test"
CONFIG_ROOT="$work/config"
REPO_CONFIG_DIR="$CONFIG_ROOT/repos/deadbeef"
REPO_CONFIG_FILE="$REPO_CONFIG_DIR/config"
roundtrip() {
local agent="$1" mode="$2" template="$3" kit_count="$4"
shift 4
rm -rf "$CONFIG_ROOT"
save_config "$agent" "$mode" "$template" "$kit_count" "$@"
unset CONFIG_KITS CONFIG_AWS_PROFILES CONFIG_TEMPLATE
load_config
}
roundtrip codex clone "" 0
[[ "$CONFIG_TEMPLATE" == "" ]] || fail "empty template did not survive: $CONFIG_TEMPLATE"
((${#CONFIG_KITS[@]} == 0)) || fail "expected no kits, got ${#CONFIG_KITS[@]}"
((${#CONFIG_AWS_PROFILES[@]} == 0)) || fail "expected no profiles, got ${#CONFIG_AWS_PROFILES[@]}"
roundtrip claude direct ghcr.io/me/img:v1 0 dev-readonly prod-readonly
[[ "$CONFIG_TEMPLATE" == "ghcr.io/me/img:v1" ]] || fail "template lost: $CONFIG_TEMPLATE"
((${#CONFIG_KITS[@]} == 0)) || fail "profiles leaked into kits: ${CONFIG_KITS[*]}"
[[ "${CONFIG_AWS_PROFILES[*]}" == "dev-readonly prod-readonly" ]] ||
fail "profiles wrong: ${CONFIG_AWS_PROFILES[*]}"
roundtrip claude clone img:v2 2 /kits/a /kits/b api-portal
((${#CONFIG_KITS[@]} == 2)) || fail "expected 2 kits, got ${#CONFIG_KITS[@]}"
[[ "${CONFIG_KITS[*]}" == "/kits/a /kits/b" ]] || fail "kits wrong: ${CONFIG_KITS[*]}"
[[ "${CONFIG_AWS_PROFILES[*]}" == "api-portal" ]] ||
fail "kits leaked into profiles: ${CONFIG_AWS_PROFILES[*]}"
roundtrip claude clone "reg/img:v3" 1 "/kits/with space" "profile one"
[[ "${CONFIG_KITS[0]}" == "/kits/with space" ]] || fail "kit with space mangled: ${CONFIG_KITS[0]}"
[[ "${CONFIG_AWS_PROFILES[0]}" == "profile one" ]] ||
fail "profile with space mangled: ${CONFIG_AWS_PROFILES[0]}"
[[ "$(stat -c '%a' "$REPO_CONFIG_FILE")" == "600" ]] ||
fail "config file is not mode 600"
grep -q 'CONFIG_TEMPLATE=' "$REPO_CONFIG_FILE" || fail "template not persisted"
if ((failures)); then
printf '%d assertion(s) failed\n' "$failures" >&2
exit 1
fi
printf 'All config round-trip assertions passed.\n'