From e4b546cd295325ae273ce1efb5cc3c16e20a5eb3 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Tue, 14 Jul 2026 20:30:01 +0000 Subject: [PATCH] fix: expose asks for too much, and the fake legacy box had no claude user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 18: 74/2. grok passes (reading the installer worked), migrate's retire passes. Two left, both mine: 1. box expose — read the Incus proxy docs instead of guessing again. Proxy IS supported on VMs, but NAT mode only (correct already), and crucially it does NOT need a static address: with no static IP, Incus reads the box's address off the bridge's neighbour table and keeps the NAT rules in step. My first cut pinned the lease with a device override anyway — unnecessary, and almost certainly the step that was failing before the proxy was ever added. Dropped it. Ask for less. The drill also stopped throwing incus's reason away: it swallowed stderr, then RE-RAN the command and printed only the last line ('box: expose failed') — box's own words, never incus's. It now captures the first attempt and prints all of it. 2. 'legacy box never came up' was unfixable by any timeout. wait_box uses 'box exec', which for a legacy-tagged box resolves the user to 'claude' — and the drill's synthetic legacy box was a BARE image with no claude user, so sudo -u claude could never answer, on a box that was perfectly healthy (every migration check against it passed). The fake legacy box now creates a claude user, like a real pre-0.4.0 box had. --- bin/box | 26 ++++++++++---------------- drill/drill.sh | 15 ++++++++++++--- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/bin/box b/bin/box index 6a1daf5..9e82090 100755 --- a/bin/box +++ b/bin/box @@ -1005,23 +1005,17 @@ cmd_expose() { incus network acl rule add box-isolate ingress action=allow \ "destination=$ip/32" "destination_port=$port" protocol=tcp >/dev/null 2>&1 || true - # A VM's proxy device needs NAT mode — the plain userspace forkproxy is a - # container thing. NAT mode DNATs host:port → instance:port in netfilter, and - # it REQUIRES the instance's NIC to hold a static address, so pin the lease - # the box already has. Pinning also fixes the restart caveat: the exposure - # survives a reboot instead of pointing at a lease the box no longer owns. + # A VM's proxy device must be NAT mode — Incus supports proxy on containers in + # both modes, but on VMs "NAT mode only" (the userspace forkproxy is a + # container thing). NAT mode DNATs host:port → instance:port in netfilter, and + # it needs the host to be the instance's gateway, which boxnet makes true. + # + # It does NOT need a static address: with no static IP, Incus reads the box's + # current address off the bridge's neighbour table and keeps the NAT rules in + # step. The first cut of this pinned the lease with a device override anyway — + # unnecessary, and it is what made expose fail before it ever reached the + # proxy. Ask for less; the docs said so. local err; err="$(mktemp)" - if ! incus config device override "$inst" eth0 "ipv4.address=$ip" >/dev/null 2>"$err"; then - # Already overridden (a second exposure on the same box) is fine. - grep -qi 'already' "$err" || { - echo "box: could not pin the box's address (needed for a VM proxy):" >&2 - sed 's/^/ /' "$err" >&2; rm -f "$err" - incus network acl rule remove box-isolate ingress action=allow \ - "destination=$ip/32" "destination_port=$port" protocol=tcp >/dev/null 2>&1 - die "expose failed" - } - fi - if incus config device add "$inst" "$dev" proxy \ "listen=tcp:127.0.0.1:$hport" "connect=tcp:$ip:$port" \ bind=host nat=true >/dev/null 2>"$err"; then diff --git a/drill/drill.sh b/drill/drill.sh index d80cfc5..77c8be5 100755 --- a/drill/drill.sh +++ b/drill/drill.sh @@ -714,7 +714,8 @@ if incus file push "$srv" archive/tmp/srv.js >/dev/null 2>&1; then # nothing holds its stdout (trap 2/3). The listener outlives the exec. timeout -k 5 20 incus exec archive -- sh -c 'setsid node /tmp/srv.js >/tmp/srv.log 2>&1 /dev/null 2>&1; then + xlog="$(mktemp)" + if box expose archive "$EP" "$EHP" >"$xlog" 2>&1; then ok "box expose archive $EP $EHP — the device was added" box expose archive --list 2>/dev/null | grep -q "$EP" \ && ok "expose --list shows the open door" || no "expose --list does not show the exposure" @@ -741,9 +742,12 @@ if incus file push "$srv" archive/tmp/srv.js >/dev/null 2>&1; then && no "the host still reaches the box after --remove — the door did not shut" \ || ok "after --remove, 127.0.0.1:$EHP is dead — the door shut" else - no "box expose failed to add the device — tail: $(box expose archive "$EP" "$EHP" 2>&1 | tail -1)" + no "box expose failed to add the device" + inf "what box and incus actually said:" + sed 's/^/ /' "$xlog" 2>/dev/null rm -f "$srv" 2>/dev/null fi + rm -f "$xlog" 2>/dev/null timeout -k 5 15 incus exec archive -- pkill -f srv.js /dev/null 2>&1 else rm -f "$srv" @@ -807,9 +811,14 @@ else # A minimal legacy box: no template payload, just boots and networks on the # old stack, wearing the old tag. This is what migrate has to move. printf '\n minting a faithful legacy box on the old stack…\n' + # The legacy box must carry a 'claude' user, because that is what a real + # pre-0.4.0 box had — and box_user() maps the legacy tag to it. Without the + # user, 'box exec' (sudo -u claude) can never answer and wait_box fails + # forever on a box that is perfectly healthy. Run 17/18 lost a FAIL to this. if mint_legacy=$(incus launch images:debian/13/cloud legacybox --profile claude-dev \ --config user.claudebox=1 --vm --device root,size=20GiB \ - --config security.secureboot=false 2>&1); then + --config security.secureboot=false \ + --config cloud-init.user-data="$(printf '#cloud-config\nusers:\n - name: claude\n shell: /bin/bash\n sudo: "ALL=(ALL) NOPASSWD:ALL"\n lock_passwd: true\n')" 2>&1); then wait_box legacybox && ok "legacy box up on the old stack (claudenet, user.claudebox=1)" \ || no "legacy box never came up — cannot drill migration" box list 2>/dev/null | grep -q '^legacybox' \