fix: three first-run drill defects — pipefail false FAILs, exec-pty hang, DHCP race

The first live run (issue #15) hit all three: (1) 'set -o pipefail'
turned every 'refusal 2>&1 | grep -q' check into a false FAIL — the
refusals exit 1/2 by design, and 'grep -q' SIGPIPEs the left side on
early match; the pipeline verdict must be grep's alone, so pipefail is
gone with a comment explaining why. (2) The in-box background listener
inherited the exec pty, so 'incus exec' waited on it forever — the run
hung at C7 for 10+ minutes; the listener is now a helper with </dev/null
on the child and a timeout on the client. (3) eth0_ip raced DHCP — the
agent answers before the address exists — so the sibling probe never
ran; it now retries for 30s.

Also: the claude-in-box check now distinguishes 'installed but not on
exec's PATH' (a repo bug — the help promises that exact invocation)
from 'not installed', and dumps cloud-init status when it is the latter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-hdb 2026-07-13 22:05:32 +00:00
parent de671b0731
commit 4ea758fa81

View file

@ -25,7 +25,12 @@
# The file is one long 'probe && ok "..." || no "..."'. ok/no always return 0, so # The file is one long 'probe && ok "..." || no "..."'. ok/no always return 0, so
# the C-may-run-when-A-is-true trap SC2015 warns about cannot fire here. # the C-may-run-when-A-is-true trap SC2015 warns about cannot fire here.
# shellcheck disable=SC2015 # shellcheck disable=SC2015
set -uo pipefail # NOT -e: a failing check is data, not a crash #
# NOT -e: a failing check is data, not a crash. NOT pipefail: half the checks
# are 'refusal 2>&1 | grep -q text' where the refusal exits 1/2 BY DESIGN, and
# 'grep -q' SIGPIPEs the left side on early match — pipefail turned both into
# false FAILs on the first live run. The pipeline verdict must be grep's alone.
set -u
REPO="${CLAUDEBOX_REPO:-heavy-duty/claudebox}" REPO="${CLAUDEBOX_REPO:-heavy-duty/claudebox}"
REF="${CLAUDEBOX_REF:-main}" REF="${CLAUDEBOX_REF:-main}"
@ -62,9 +67,25 @@ wait_box() { # poll until exec answers (the VM agent can take a while), ~2 min
} }
eth0_ip() { # the box's address on claudenet — eth0 exactly; a box running eth0_ip() { # the box's address on claudenet — eth0 exactly; a box running
# docker reports several addresses across quoted CSV lines # docker reports several addresses across quoted CSV lines.
incus list "^$1\$" --format csv --columns 4 | tr -d '"' | tr ',' '\n' \ # Retries: the agent answers before DHCP hands out the address.
| awk '/\(eth0\)$/ { print $1; exit }' local b="$1" ip _i
for _i in $(seq 1 15); do
ip="$(incus list "^$b\$" --format csv --columns 4 | tr -d '"' | tr ',' '\n' \
| awk '/\(eth0\)$/ { print $1; exit }')"
[ -n "$ip" ] && { printf '%s\n' "$ip"; return 0; }
sleep 2
done
return 1
}
box_listen() { # start a throwaway HTTP listener INSIDE a box, detached.
# </dev/null is load-bearing: a child holding the exec pty makes
# 'incus exec' wait forever (the first live run hung here).
timeout 20 claudebox exec "$1" -- bash -c \
"command -v python3 >/dev/null && { nohup python3 -m http.server $2 --bind 0.0.0.0 >/dev/null 2>&1 </dev/null & }" \
>/dev/null 2>&1
sleep 1
} }
# --- stage 1: consent, install, then re-enter inside the incus-admin group --- # --- stage 1: consent, install, then re-enter inside the incus-admin group ---
@ -233,8 +254,16 @@ fi
claudebox info drill | grep -q '^IPV4' && ok "info shows an IPv4" || no "info has no IPV4 row" claudebox info drill | grep -q '^IPV4' && ok "info shows an IPv4" || no "info has no IPV4 row"
claudebox info drill | grep -q 'SNAPSHOTS (none)' && ok "info: no snapshots yet, offers to take one" || no "info snapshot-empty state wrong" claudebox info drill | grep -q 'SNAPSHOTS (none)' && ok "info: no snapshots yet, offers to take one" || no "info snapshot-empty state wrong"
claudebox exec drill -- claude --version >/dev/null 2>&1 \ if claudebox exec drill -- claude --version >/dev/null 2>&1; then
&& ok "Claude Code is installed in the box" || no "'claude --version' failed inside the box" ok "Claude Code is installed in the box"
elif timeout 30 claudebox exec drill -- bash -lc 'claude --version' >/dev/null 2>&1; then
no "'claude' is installed but NOT on exec's PATH — repo bug: the help promises 'claudebox exec work -- claude --version'"
inf "PATH as exec sees it: $(timeout 30 claudebox exec drill -- printenv PATH 2>/dev/null)"
else
no "'claude --version' failed inside the box"
inf "cloud-init: $(timeout 30 claudebox incus drill -- exec {} -- cloud-init status 2>&1 | head -1)"
inf "claude's ~/.local/bin holds: $(timeout 30 claudebox incus drill -- exec {} -- ls /home/claude/.local/bin 2>&1 | tr '\n' ' ' | cut -c1-120)"
fi
claudebox exec drill -- gh --version >/dev/null 2>&1 \ claudebox exec drill -- gh --version >/dev/null 2>&1 \
&& ok "the GitHub CLI is installed in the box (PR #5)" || no "'gh --version' failed inside the box" && ok "the GitHub CLI is installed in the box (PR #5)" || no "'gh --version' failed inside the box"
@ -327,7 +356,7 @@ claudebox exec archive -- curl -sS -m 5 -o /dev/null http://192.168.1.1 2>/dev/n
# egress drop is not covering siblings) · timeout = dropped, as designed. # egress drop is not covering siblings) · timeout = dropped, as designed.
PEER_IP="$(eth0_ip peer)" PEER_IP="$(eth0_ip peer)"
if [ -n "$PEER_IP" ]; then if [ -n "$PEER_IP" ]; then
claudebox exec peer -- bash -c 'command -v python3 >/dev/null && nohup python3 -m http.server 8088 --bind 0.0.0.0 >/dev/null 2>&1 & sleep 1' >/dev/null 2>&1 box_listen peer 8088
claudebox exec archive -- curl -sS -m 5 -o /dev/null "http://$PEER_IP:8088" 2>/dev/null claudebox exec archive -- curl -sS -m 5 -o /dev/null "http://$PEER_IP:8088" 2>/dev/null
rc=$? rc=$?
case "$rc" in case "$rc" in
@ -362,7 +391,7 @@ fi
# C7 — inbound, host → box (#15 A7): the ACL's default ingress drop # C7 — inbound, host → box (#15 A7): the ACL's default ingress drop
ARCH_IP="$(eth0_ip archive)" ARCH_IP="$(eth0_ip archive)"
claudebox exec archive -- bash -c 'command -v python3 >/dev/null && nohup python3 -m http.server 8087 --bind 0.0.0.0 >/dev/null 2>&1 & sleep 1' >/dev/null 2>&1 box_listen archive 8087
if [ -n "$ARCH_IP" ] && curl -sS -m 5 -o /dev/null "http://$ARCH_IP:8087" 2>/dev/null; then if [ -n "$ARCH_IP" ] && curl -sS -m 5 -o /dev/null "http://$ARCH_IP:8087" 2>/dev/null; then
no "the HOST connected to a listener inside the box — the default ingress drop is not holding" no "the HOST connected to a listener inside the box — the default ingress drop is not holding"
aud "A7 inbound host→box: FAIL — reached a box listener" aud "A7 inbound host→box: FAIL — reached a box listener"