From 1cef6ed7510903f6ea5219329615e1d9e93afba9 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sat, 18 Jul 2026 20:18:15 +0000 Subject: [PATCH] review r1: staging tolerates only the workload guest; dockerd must answer; one CLI capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The staging marker tolerance now says what the docs meant: class=server with host=no only. A non-server machine (class=human via custom) refuses with its own message instead of dying later inside harden_sshd with server-specific advice. Fixture pins the refusal. - The docker converge asserts the DAEMON answers (docker info, bounded 30s settle), not just the client binary — grep-pinned. - The agent-CLI version check is one capture serving assert and log; emptiness is the failure signal (head exits 0, a pipeline status can't be). - Harness gains the codex login-flow context grep alongside claude/grok. Verified: test/cli.sh 244/0, shellcheck -x clean, live container e2e (staging round 1 + convergence round 2, dockerd answering). Co-Authored-By: Claude Fable 5 --- commands/bootstrap-tenant.sh | 32 ++++++++++++++++++++++++++------ test/cli.sh | 13 ++++++++++++- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/commands/bootstrap-tenant.sh b/commands/bootstrap-tenant.sh index b28878a..a09f23b 100755 --- a/commands/bootstrap-tenant.sh +++ b/commands/bootstrap-tenant.sh @@ -100,9 +100,12 @@ done # Names the staging rename out loud — before #31, `staging` was the VM-host # PRESET; that shape is now spelled through the traits. # - class= (agent tenants) → refuse: an agent box is never a tailnet machine. -# - class= with host=no (staging only) → PROCEED, and leave the marker alone: -# that is the staging guest AFTER its operator-run workload join, and +# - class=server with host=no (staging only) → PROCEED, and leave the marker +# alone: that is the staging guest AFTER its operator-run workload join, and # re-converging docker+hardening on it is exactly what convergence is for. +# ONLY that shape — any other class (say class=human, via `custom`) is a +# machine rig built on purpose, and staging hardening it with server rules +# would die with server-specific messaging on a box that was never one. MARKER_PATH="${RIG_ROLE_MARKER:-/etc/rig/role}" EXISTING_MARKER="$(read_role_marker "$MARKER_PATH")" case "$EXISTING_MARKER" in @@ -111,7 +114,12 @@ case "$EXISTING_MARKER" in *class=*) if [ "$ROLE" != "staging" ]; then die "this box already carries a machine role (${EXISTING_MARKER}) — the agent tenants converge box guests, never tailnet machines. If this really is a guest, remove ${MARKER_PATH} and re-run." - fi ;; + fi + case "$EXISTING_MARKER" in + *class=server*) ;; + *) + die "this box carries a non-server machine role (${EXISTING_MARKER}) — staging tolerates only the workload-joined guest (class=server host=no). If this really is a staging guest, remove ${MARKER_PATH} and re-run." ;; + esac ;; esac [ "$(id -u)" -eq 0 ] || die "must run as root" @@ -185,6 +193,16 @@ else log "docker already installed" fi docker --version >/dev/null 2>&1 || die "docker installed but 'docker --version' does not answer" +# The client answering is not the effective state — a dead dockerd would still +# pass it. Ask the daemon, with a bounded settle for the freshly-installed case +# (get.docker.com starts it, but not instantaneously on a slow guest). +docker_up="" +for _ in 1 2 3 4 5 6; do + if docker info >/dev/null 2>&1; then docker_up=1; break; fi + sleep 5 +done +[ -n "$docker_up" ] || die "dockerd does not answer 'docker info' after 30s — the daemon is not running; check 'systemctl status docker' (or the container's init) before re-running" +log "dockerd answering" if getent group docker >/dev/null 2>&1; then if id -nG "$TENANT_USER" | tr ' ' '\n' | grep -qx docker; then log "${TENANT_USER} already in the docker group" @@ -260,9 +278,11 @@ esac if [ -n "$CLI" ]; then [ -e "$CLI_SRC" ] || die "the ${CLI} installer produced no ${CLI_SRC} — upstream layout changed?" ln -sf "$CLI_SRC" "/usr/local/bin/$CLI" - runuser -l "$TENANT_USER" -c "$CLI --version" >/dev/null 2>&1 \ - || die "'$CLI --version' does not answer for ${TENANT_USER} — the CLI landed but cannot run; check /usr/local/bin/$CLI and its target" - log "${CLI} CLI on the system PATH and answering ($(runuser -l "$TENANT_USER" -c "$CLI --version" 2>/dev/null | head -n1))" + # One capture serves both the assert and the log line; emptiness IS the + # failure signal (head exits 0 regardless, so a pipeline status can't be). + CLI_VER="$(runuser -l "$TENANT_USER" -c "$CLI --version" 2>/dev/null | head -n1)" + [ -n "$CLI_VER" ] || die "'$CLI --version' does not answer for ${TENANT_USER} — the CLI landed but cannot run; check /usr/local/bin/$CLI and its target" + log "${CLI} CLI on the system PATH and answering (${CLI_VER})" # The interactive-shell PATH exports the templates carried, converged as # literal rc lines (written once, never duplicated). Single quotes are the diff --git a/test/cli.sh b/test/cli.sh index af96aa7..bfef353 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -209,6 +209,11 @@ check "tenant: unknown role exits 2" 2 "unknown tenant role" "$ROOT/commands/ check "tenant: unknown flag exits 2" 2 "unknown flag" "$ROOT/commands/bootstrap-tenant.sh" claude --nope check "tenant: --user needs value" 2 "needs a value" "$ROOT/commands/bootstrap-tenant.sh" claude --user check "tenant: bad --user charset exits 2" 2 "invalid user" "$ROOT/commands/bootstrap-tenant.sh" claude --user 'fo|o' +# The docker converge asserts the DAEMON answers, not just the client binary — +# a dead dockerd passing `docker --version` is the "linked but cannot run" +# scar in daemon form. Grep-pinned so the assert cannot ship deleted. +check "tenant: dockerd effective-state assert is present" 0 "" \ + grep -qF "docker info" "$ROOT/commands/bootstrap-tenant.sh" # The machine-role traits die with the tenant story, never "unknown flag" — an # operator reaching for --hostname must learn where the trait family went. check "tenant: trait flags die with the tenant story" 2 "have no traits" \ @@ -224,12 +229,17 @@ check "bootstrap: tenant roles dispatch through bootstrap.sh" 0 "claude|codex|gr # VM host (host=yes) refuses for every tenant — and names the staging rename, # because a pre-#31 staging HOST re-running its old command is exactly who # lands here. An agent tenant refuses ANY machine-role box; staging tolerates -# a class= marker with host=no — that is the staging guest after its +# ONLY class=server with host=no — that is the staging guest after its # operator-run workload join, and re-converging it is what convergence is for. +# A non-server machine (class=human via custom) is NOT that guest, and server +# hardening would die at it with server-specific messaging — refuse instead. TEN_FIX="$(mktemp -d)" printf 'role=dev class=human host=yes join=authkey\n' > "$TEN_FIX/host" printf 'role=workload class=server host=no join=authkey\n' > "$TEN_FIX/machine" +printf 'role=custom class=human host=no join=login\n' > "$TEN_FIX/human" printf 'role=claude tenant=yes host=no\n' > "$TEN_FIX/tenant" +check "tenant: staging refuses a non-server machine box" 1 "non-server machine role" \ + env RIG_ROLE_MARKER="$TEN_FIX/human" "$ROOT/commands/bootstrap-tenant.sh" staging check "tenant: refuses a host=yes box (a VM host is never a guest)" 1 "hosts VMs" \ env RIG_ROLE_MARKER="$TEN_FIX/host" "$ROOT/commands/bootstrap-tenant.sh" claude check "tenant: the host refusal names the old staging preset's new spelling" 1 "custom --class server --host yes" \ @@ -279,6 +289,7 @@ check "tenant context: the guard says whose host this is not" 0 "not a host you check "tenant context: the guard cites box#80" 0 "box#80" tctx claude check "tenant context: the creds-free contract is stated" 0 "Creds-free by default" tctx claude check "tenant context: claude names /login as the operator's flow" 0 "/login" tctx claude +check "tenant context: codex names its login flow" 0 "login flow (\`codex\`)" tctx codex check "tenant context: grok names its login flow" 0 "grok login" tctx grok check "tenant context: staging renders nothing (no agent lives there)" 1 "" tctx staging # Creds-free BY CONSTRUCTION, provable by absence (box#69's grep-refusal