From 16a4c129fcb18f29442f24de2d81bee9ef6f51f0 Mon Sep 17 00:00:00 2001 From: claude-hdb Date: Tue, 14 Jul 2026 13:13:18 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20'claudebox=20doctor'=20=E2=80=94=20the?= =?UTF-8?q?=20host-health=20checks=20as=20a=20first-class=20verb?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every fault the drill's doctor diagnoses is a user's fault first: a wedged Incus daemon, a dnsmasq that silently isn't serving, a VPN resolver boxes inherit, isolation claimed by config but off in the kernel — each has killed a cold mint or weakened a boundary, with a cloud-init error that names none of them. The CLI half-admitted it: cmd_new's failure path hand-pointed at issue #33, doing one special case of a doctor's job inline. The verb delegates to the installed drill/doctor.sh (the tree ships whole) — one hardened script, two audiences. Its flags pass through; the verdict now reads 'fit to mint boxes (and to drill)'; the mint- failure hint ends with 'claudebox doctor' instead of the hand-rolled diagnosis. Closes #46 --- README.md | 1 + bin/claudebox | 38 ++++++++++++++++++++++++++++++++++++-- drill/doctor.sh | 7 ++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e56ba59..eed4849 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ claudebox down # stop (state kept; `start` resumes) claudebox start # start a stopped box claudebox rm [--force] # delete the box + its snapshots (asks first) claudebox incus -- # escape hatch: any incus command, box resolved +claudebox doctor [--fix|--pin-dns] # is this host fit to mint boxes? diagnose from ground truth claudebox status # deprecated alias for `list` claudebox help [] # full help, or one command's page ``` diff --git a/bin/claudebox b/bin/claudebox index de9a320..daccf1d 100755 --- a/bin/claudebox +++ b/bin/claudebox @@ -49,6 +49,7 @@ CMDS=( "start^^box^Start a stopped box^incus:start^started {}" "rm^ [--force]^box,confirm^Delete a box and its snapshots — irreversible, and it asks first^incus:delete -f^removed {}" "incus^ -- ^box^Escape hatch: run any incus command against a box^fn:cmd_incus^" + "doctor^[--fix | --pin-dns]^^Is this host fit to mint boxes? Diagnose the daemon, network, DNS, isolation^fn:cmd_doctor^" "status^^^Deprecated alias for 'list'^fn:cmd_status^" "help^[]^^This help, or 'claudebox help ' for one command^fn:cmd_help^" ) @@ -282,6 +283,25 @@ Changing the profile, the network, a device or a security.* key can take a box outside the isolation stack. claudebox warns and then does as you asked — from there, the trust boundary is yours to keep. EOF +;; + doctor) cat <<'EOF' +Answer "is this host fit to mint boxes?" from ground truth, not config claims: +is the Incus daemon answering, is a dnsmasq actually serving claudenet, does +the kernel's bridge port say 'isolated on', is the resolver pinned or is a +host VPN's DNS leaking into boxes, can a box actually resolve names. Every +check exists because its fault has happened — most kill a cold mint with a +cloud-init error that names none of them. + + --fix also revert what a drill run may have left behind + --pin-dns pin claudenet's resolver to public upstreams and re-test + (setup-host.sh now pins by default; this is the quick test) + + claudebox doctor + claudebox doctor --fix + +Exit 0 = clean; 1 = problems found (each printed with its fix). Read-only +unless --fix or --pin-dns is given. +EOF ;; status) cat <<'EOF' Deprecated alias for 'claudebox list'. It ignored the argument it @@ -330,6 +350,8 @@ while [ $# -gt 0 ]; do # An unrecognized flag used to be swallowed as a positional — so a typo'd # --labl silently became a snapshot's label. Say so instead. -*) + # doctor's flags belong to the doctor script, not to claudebox + if [ "$cmd" = doctor ]; then args+=("$1"); shift; continue; fi if [ "$cmd" = exec ] || [ "$cmd" = incus ]; then usage_error "unknown option: $1 — a command's own flags go after --, as in '$(synopsis_of "$cmd")'" fi @@ -473,8 +495,8 @@ cmd_new() { echo "claudebox: '$name' is still up — inspect it, then delete it:" >&2 echo " claudebox incus $name -- exec {} -- tail -50 /var/log/cloud-init-output.log" >&2 echo " claudebox rm $name" >&2 - echo "claudebox: a box that cannot resolve DNS is usually the HOST's resolver" >&2 - echo " (a VPN/Tailscale resolver the box inherits — see issue #33)." >&2 + echo "claudebox: a failed mint is usually the HOST's fault (a wedged daemon, a dnsmasq" >&2 + echo " not serving, a VPN resolver the box inherits). Diagnose it: claudebox doctor" >&2 die "cloud-init failed — the box is incomplete, so refusing to hand it over" fi fi @@ -583,6 +605,18 @@ cmd_status() { list_all } +# The host-health checks live in drill/doctor.sh — grown by the drill, but +# every fault they diagnose (a wedged daemon, a dnsmasq that isn't serving, +# a VPN resolver boxes inherit, isolation off in the kernel) is a USER's +# fault first: each one has killed a cold mint or silently weakened a +# boundary. The install tree ships the whole repo, so delegate — one +# hardened script, two audiences. +cmd_doctor() { + local script="$root/drill/doctor.sh" + [ -f "$script" ] || die "doctor script not found at $script — re-run install.sh" + exec bash "$script" "${args[@]}" +} + cmd_help() { show_help "${args[0]:-}"; } # The escape hatch. The box is resolved and tag-checked; everything else is diff --git a/drill/doctor.sh b/drill/doctor.sh index 0dfced1..cf91737 100755 --- a/drill/doctor.sh +++ b/drill/doctor.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# doctor.sh — is this host in a fit state to drill, and if not, what is wrong? +# doctor.sh — is this host fit to mint boxes (and to drill), and if not, what +# is wrong? Users reach it as 'claudebox doctor'; the drill runs it directly. # # bash drill/doctor.sh # report # bash drill/doctor.sh --fix # report, then revert what the drill left behind @@ -243,10 +244,10 @@ fi head_ "Verdict" if [ "$bad" -eq 0 ]; then - printf ' \033[32mclean\033[0m — this host is fit to drill.\n\n' + printf ' \033[32mclean\033[0m — this host is fit to mint boxes (and to drill).\n\n' exit 0 fi -printf ' \033[31m%s problem(s)\033[0m — this host is NOT fit to drill.\n' "$bad" +printf ' \033[31m%s problem(s)\033[0m — this host is NOT fit to mint boxes (or to drill).\n' "$bad" if [ "$FIX" = 1 ]; then printf ' reverted what could be reverted; re-run doctor to confirm.\n\n' else -- 2.45.2