Files
stack/agents/researcher/work/i1-spikes/lib.sh
T

127 lines
5.9 KiB
Bash
Executable File

# Shared by the I1 spike scripts (row 86, #1580). Sourced, never run.
#
# Rules it enforces for every spike (docs/plans/2026-10-12_i1-containerized-install.md):
# - every docker call goes through D, which names the rootless endpoint;
# - the first step refuses unless that endpoint reports name=rootless;
# - everything a spike creates carries the label mosaic.i1spike=<spike>;
# - on exit, pass or fail, the spike's containers, networks, built images and
# scratch directory are removed, and the script checks that nothing is left.
#
# Exit codes: 0 the observed behavior matched the assertions, 1 it did not,
# 2 a rule refused the run (wrong daemon, leftovers). Output is also saved to
# out/<spike>.out.
EP=unix:///run/user/1000/docker.sock
# node:24-bookworm-slim, linux/amd64, pulled 2026-10-12T19:09Z. The worker's base.
NODE_IMG=node@sha256:d6aa754f16b3197301076f047b5def2f02ea1dbbc2ca920407d46d7ec7f87b20
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
REPO=$(cd "$HERE/../../../.." && pwd)
D() { docker --host "$EP" "$@"; }
FAILS=0
say() { printf '%s\n' "$*"; }
# check DESCRIPTION CONDITION-EXIT-STATUS: records one assertion.
check() {
if [ "$2" -eq 0 ]; then say "ok $1"; else say "FAIL $1"; FAILS=$((FAILS + 1)); fi
}
# spike_begin NAME: pin the endpoint, set up output, scratch and cleanup.
spike_begin() {
SPIKE=$1
mkdir -p "$HERE/out"
exec > >(tee "$HERE/out/$SPIKE.out") 2>&1
say "# $SPIKE $(date -u +%Y-%m-%dT%H:%M:%SZ)"
local sec
if ! sec=$(D info --format '{{json .SecurityOptions}}' 2>&1); then
say "refused: $EP does not answer: $sec"; trap - EXIT; exit 2
fi
case $sec in
*'"name=rootless"'*) say "endpoint $EP: name=rootless" ;;
*) say "refused: $EP does not report name=rootless: $sec"; trap - EXIT; exit 2 ;;
esac
LABEL="mosaic.i1spike=$SPIKE"
# The scratch parent is mktemp's 0700 directory; spikes mount only subdirectories.
SCRATCH=$(mktemp -d "${TMPDIR:-/tmp}/i1spike-$SPIKE.XXXXXX")
say "scratch $SCRATCH"
trap spike_end EXIT
}
# Labels and the --rm flag a spike adds to every container it starts.
RUNFLAGS() { printf '%s\n' --label "$LABEL"; }
spike_end() {
local rc=$?
trap - EXIT
set +e
local ids
ids=$(D ps -a -q --filter "label=$LABEL")
[ -n "$ids" ] && D rm -f $ids >/dev/null 2>&1
ids=$(D network ls -q --filter "label=$LABEL")
[ -n "$ids" ] && D network rm $ids >/dev/null 2>&1
ids=$(D image ls -q --filter "label=$LABEL")
[ -n "$ids" ] && D image rm -f $ids >/dev/null 2>&1
# Rootless uid 0 in a container is the host user; other container uids map into
# the subuid range and may have left files this user cannot remove. Remove them
# from inside a throwaway container as container root (host jwoltje).
rm -rf "$SCRATCH" 2>/dev/null
if [ -e "$SCRATCH" ]; then
D run --rm --label "$LABEL" -v "$SCRATCH:/s" "$NODE_IMG" sh -c 'rm -rf /s/* /s/.[!.]* 2>/dev/null' >/dev/null 2>&1
rm -rf "$SCRATCH" 2>/dev/null
fi
local left=0
[ -n "$(D ps -a -q --filter "label=$LABEL")" ] && left=1
[ -n "$(D network ls -q --filter "label=$LABEL")" ] && left=1
[ -n "$(D image ls -q --filter "label=$LABEL")" ] && left=1
[ -e "$SCRATCH" ] && left=1
if [ "$left" -eq 0 ]; then
say "cleanup: no container, network, image or scratch directory labelled $SPIKE is left"
else
say "cleanup: LEFTOVERS for $SPIKE"; rc=2
fi
if [ "$rc" -eq 0 ] && [ "$FAILS" -ne 0 ]; then rc=1; fi
if [ "$rc" -eq 0 ]; then say "result: pass"; else say "result: exit $rc ($FAILS failed assertion(s))"; fi
exit "$rc"
}
# Pull the pinned base image if the rootless daemon does not hold it. The image
# is shared by the spikes and removed by s0-clean-images.sh, not per spike.
need_node_image() {
D image inspect "$NODE_IMG" >/dev/null 2>&1 || D pull -q "$NODE_IMG" >/dev/null
}
# net_up ALLOWED-HOSTS: an internal network INT, an ordinary bridge EXT and the
# allowlisting CONNECT proxy PROXY (tools/connect-proxy.mjs on the pinned node
# image, port 443 only) on both, reachable from INT as http://proxy:3128. Sets
# FAKE, a random API-key-shaped string that no provider has issued. With no key
# at all, pi says "No API key found" (no network) and Claude Code says "Not logged
# in" (after startup connections), so neither shows a model-request answer from the provider;
# a made-up key makes the provider answer "invalid key", which is the
# authentication failure S6 wants. It is not a credential.
net_up() {
mkdir -p "$SCRATCH/tools"; cp "$HERE/tools/connect-proxy.mjs" "$SCRATCH/tools/"
INT="i1${SPIKE%%-*}-int-$$"; EXT="i1${SPIKE%%-*}-ext-$$"; PROXY="i1${SPIKE%%-*}-proxy-$$"
D network create --label "$LABEL" --internal "$INT" >/dev/null
D network create --label "$LABEL" "$EXT" >/dev/null
D run -d --name "$PROXY" --label "$LABEL" --network "$EXT" --user 0:0 \
--read-only --cap-drop ALL --security-opt no-new-privileges \
-e ALLOW="$1" -e PORTS=443 -v "$SCRATCH/tools:/t:ro" "$NODE_IMG" node /t/connect-proxy.mjs >/dev/null
D network connect --alias proxy "$INT" "$PROXY"
for _ in $(seq 50); do D logs "$PROXY" 2>&1 | grep -q listening && break; sleep 0.1; done
FAKE=$(node -e 'process.stdout.write("sk-ant-api03-FAKE" + require("crypto").randomBytes(20).toString("hex"))')
}
# plines: how many lines the proxy has logged so far; proxy_new N: the lines after N.
plines() { D logs "$PROXY" 2>&1 | wc -l; }
proxy_new() { sleep 0.3; D logs "$PROXY" 2>&1 | tail -n +"$(($1 + 1))"; }
# Build the S6/S7 image (tools/Dockerfile.clis) from an otherwise empty context
# in the spike's scratch directory, labelled so spike_end removes it. Sets CLI_IMG.
build_cli_image() {
mkdir -p "$SCRATCH/ctx"
cp "$HERE/tools/Dockerfile.clis" "$SCRATCH/ctx/Dockerfile"
CLI_IMG="mosaic-i1spike-clis:$SPIKE"
say "building $CLI_IMG from $NODE_IMG (npm install -g inside the image, exact versions)"
D build -q --label "$LABEL" --build-arg "NODE_IMG=$NODE_IMG" -t "$CLI_IMG" "$SCRATCH/ctx" >/dev/null || return 1
say "built $CLI_IMG: $(D image inspect "$CLI_IMG" --format '{{.Id}} {{.Size}} bytes')"
}