Files
6krrt/scripts/sandbox.sh
adlee-was-taken a9e8e75882 feat(scripts): shared agent mechanics for the sandbox and PR flow
Both scripts wrap something every agent on this repo ends up doing by hand,
and each encodes a constraint that is easy to get wrong once and expensive
to get wrong twice. Agent-agnostic on purpose -- plain scripts, plain text
output -- so opencode agents use the same ones rather than each
hand-rolling the equivalent command and getting a different detail wrong.
Indexed from AGENTS.md; the reasoning is in scripts/README.md.

scripts/sandbox.sh -- throwaway router on 8081. Encodes the port convention
(8080 is production and this cannot bind it), the kill discipline (only a
pid it recorded itself; never kill-by-name, Restart=always will fight you
and on this machine that process is the operator's model access), and the
cwd-vs-PYTHONPATH split that decides which config.yaml, which router.db and
which admin/frontend get used.

Writing it found two bugs in itself, both only visible by running it:

  - `sandbox.sh start | tail` hung until the SERVER exited, because the
    child inherited the script's stdout and held the pipe open. Fixed with
    </dev/null and all three fds redirected.
  - $! recorded a pid one off from the real uvicorn, so `stop` reported
    success while the server kept running and `status` said "not running"
    against a live :8081 answering 200. Fixed by backgrounding the command
    directly instead of inside `( ... & echo $! )`, plus a post-start check
    that the recorded pid is the one actually holding the port -- without
    that check both bugs printed success.

scripts/mkpr.sh -- cut a gitea PR with a long markdown body. `tea pr
create` has no --description-file, so the body must arrive via $(cat ...),
which a worktree-pinned agent session refuses as too complex to verify;
running it from a script file sidesteps that without weakening it. Passes
--remote origin as well as --repo because AGENTS.md notes the `alee` login
is not tea's default. Refuses a head branch that is unpushed, diverged from
its remote, or not ahead of the base -- tea will cheerfully do all three.

Deliberately not scripted: merging to main (needs main checked out, which
conflicts with any agent in a worktree), Seed Energy and Restart Service
(one spends real credit, the other restarts production), and anything that
writes config.local.yaml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
2026-09-08 21:14:28 -04:00

94 lines
3.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Throwaway 6krrt router instance on 8081, for exercising the admin portal
# against real data without touching production.
#
# sandbox.sh start [worktree] start it, wait for /health, print the pid
# sandbox.sh stop kill the instance this script started
# sandbox.sh status is it up, and on what pid
#
# 8080 is production and 8081 is the throwaway, per CLAUDE.md. This script
# refuses to bind 8080 at all, and only ever kills a pid it recorded itself
# -- never one matched by name or port, because Restart=always will fight
# you and on this machine that process is the user's model access.
set -euo pipefail
REPO=/home/alee/Sources/6krrt
PORT=8081
PIDFILE="${TMPDIR:-/tmp}/6krrt-sandbox-${PORT}.pid"
LOGFILE="${TMPDIR:-/tmp}/6krrt-sandbox-${PORT}.log"
usage() { sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'; exit 2; }
start() {
local worktree="${1:-$PWD}"
[ -d "$worktree/src" ] || { echo "no src/ under $worktree" >&2; exit 1; }
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "already running, pid $(cat "$PIDFILE")"; exit 0
fi
# cwd decides config/config.yaml and the relative router.db; PYTHONPATH
# decides which src/ and which admin/frontend/*.html get served. Pointing
# both at the worktree keeps every write inside it.
#
# Backgrounded directly rather than inside `( ... & echo $! )`. Two bugs
# came out of the subshell form, both found by running it:
# - `sandbox.sh start | tail` hung until the SERVER exited, because the
# child inherited the script's stdout and held the pipe open. Hence
# </dev/null and all three fds redirected.
# - $! recorded the wrong pid (off by one from the real uvicorn), so
# `stop` reported success while the server kept running and `status`
# said "not running" against a live :8081. Backgrounding a simple
# command in this shell makes $! exactly the process we started.
cd "$worktree"
PYTHONPATH="$worktree/src" \
HF_HOME="$REPO/.hf-cache" \
"$REPO/.venv/bin/uvicorn" dispatcher:app \
--host 127.0.0.1 --port "$PORT" --timeout-graceful-shutdown 5 \
</dev/null >"$LOGFILE" 2>&1 &
local pid=$!
echo "$pid" >"$PIDFILE"
if ! curl -s -m 30 --retry 20 --retry-delay 1 --retry-connrefused \
-o /dev/null "http://127.0.0.1:$PORT/health"; then
echo "failed to come up; last log lines:" >&2
tail -20 "$LOGFILE" >&2
exit 1
fi
# Prove the recorded pid is the process actually holding the port. Without
# this the two bugs above were silent: everything printed success while
# stop/status operated on a pid that was never the server.
local owner
owner="$(ss -ltnpH "sport = :$PORT" 2>/dev/null | grep -o 'pid=[0-9]*' | head -1 | cut -d= -f2)"
if [ -n "$owner" ] && [ "$owner" != "$pid" ]; then
echo "warning: recorded pid $pid but :$PORT is held by $owner; recording $owner" >&2
pid="$owner"
echo "$pid" >"$PIDFILE"
fi
echo "up on $PORT, pid $pid, log $LOGFILE"
}
stop() {
[ -f "$PIDFILE" ] || { echo "no pidfile; nothing this script started"; exit 0; }
local pid; pid="$(cat "$PIDFILE")"
kill "$pid" 2>/dev/null && echo "stopped pid $pid" || echo "pid $pid already gone"
rm -f "$PIDFILE"
}
status() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "running, pid $(cat "$PIDFILE")"
else
echo "not running"
fi
curl -s -m 5 -o /dev/null -w "health on $PORT: %{http_code}\n" \
"http://127.0.0.1:$PORT/health" || true
}
case "${1:-}" in
start) shift; start "$@" ;;
stop) stop ;;
status) status ;;
*) usage ;;
esac