#!/usr/bin/env bash
# Converge build boxes to the state declared in lib/buildbox-checks.sh — one source, N hosts.
#   buildbox audit [host...]         check every declared item, exit 1 on any drift
#   buildbox bootstrap <host...>     converge user-level items (idempotent)
#   buildbox harden <host>           apply host-config/ as root (kernel + systemd failsafes)
#   buildbox claude-parity [host...] one line per host: is ~/.claude config-identical and live
# Hosts, state and connection details come from the buildbox registry
# (~/.claude/buildbox-hosts.json); boxes are never synced from each other.
#
# harden is one host per invocation, enforced below, and never part of bootstrap: it
# arms a hardware watchdog and it partitions and formats the scratch disk. Either one
# applied to every box at once takes out the whole fleet with nobody there to stop it.
#
# harden also reboots the host it just changed, and refuses a second host until one has
# come back from that reboot. See lib/fleet-guard.sh.
set -euo pipefail

MOD="$(cd -P "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
CONFIG="${BUILDBOX_CONFIG:-$HOME/.claude/build-remote.json}"
LIB="$MOD/lib/buildbox-checks.sh"
HOSTCFG="$MOD/host-config"
CEILINGS="$MOD/../workstation/claude/systemd/user-root"
USERCFG="$MOD/user-config"
PRUNE_SRC="$USERCFG/bin/ci-scratch-prune.sh"
CACHE_PRUNE_SRC="$USERCFG/bin/buildbox-build-cache-prune.sh"
TELEMETRY_SRC="$USERCFG/bin/buildbox-telemetry.sh"
DEVTOOLS="$MOD/devtools.json"
DEADMAN_SRC="$MOD/deadman/buildbox-deadman"
DANGERLAB_SRC="$MOD/dangerlab"
# BUILDBOX_HOSTS_CONFIG is the registry reader's own override and must keep winning here:
# re-exporting a different value below would give one source of truth two env names with
# the wrong one on top.
REGISTRY="${BUILDBOX_HOSTS_CONFIG:-${BUILDBOX_REGISTRY:-$HOME/.claude/buildbox-hosts.json}}"
REPO_ROOT="$(cd -P "$MOD/../.." && pwd)"
# The desired-state items for a host are the fleet engine's to know. audit and bootstrap
# fold its verdict into their own report and exit code; no item fact is restated here.
# BUILDBOX_FLEET_ENGINE swaps the engine module — isolated tests only.
FLEET_ENGINE="${BUILDBOX_FLEET_ENGINE:-$REPO_ROOT/lib/fleet/engine.mjs}"
# BUILDBOX_FLEET_HARDEN swaps the harden module — isolated tests only.
FLEET_HARDEN="${BUILDBOX_FLEET_HARDEN:-$REPO_ROOT/lib/fleet/harden.mjs}"
GUARD="$MOD/lib/fleet-guard.sh"
PARITY="$MOD/lib/claude-parity.sh"
CLAUDE_HOME_LIB="$MOD/lib/claude-home.sh"
[ -r "$GUARD" ] || { echo "buildbox: fleet guard missing: $GUARD" >&2; exit 2; }
# shellcheck source=../lib/fleet-guard.sh
. "$GUARD"
[ -r "$PARITY" ] || { echo "buildbox: claude parity payload missing: $PARITY" >&2; exit 2; }
# shellcheck source=claude-parity.sh
. "$PARITY"
[ -r "$CLAUDE_HOME_LIB" ] || { echo "buildbox: claude-home library missing: $CLAUDE_HOME_LIB" >&2; exit 2; }
# shellcheck source=claude-home.sh
. "$CLAUDE_HOME_LIB"

# The confinement pair lives inside the `lib` and `bin` manifest entries, so its canonical
# bytes are whatever the manifest declares for those entries. Reading it from anywhere else
# gives the fleet two writers for the same two files: they overwrite each other on every
# converge and the tree digest never settles.
CONFINE_SRC="$(claude_home_source_path lib "$(claude_home_entry_source lib)")/confine.sh"
SCOPE_SRC="$(claude_home_source_path bin "$(claude_home_entry_source bin)")/_agent-build-scope"
[ -r "$LIB" ] || { echo "buildbox: checks payload missing: $LIB" >&2; exit 2; }
[ -r "$CONFIG" ] || { echo "buildbox: config missing: $CONFIG" >&2; exit 2; }
[ -r "$DEVTOOLS" ] || { echo "buildbox: devtools declaration missing: $DEVTOOLS" >&2; exit 2; }
[ -r "$FLEET_ENGINE" ] || { echo "buildbox: fleet engine missing: $FLEET_ENGINE" >&2; exit 2; }
[ -r "$FLEET_HARDEN" ] || { echo "buildbox: fleet harden module missing: $FLEET_HARDEN" >&2; exit 2; }

cmd="${1:-}"; shift || true
case "$cmd" in audit|bootstrap|harden|claude-parity) ;; *) echo "usage: buildbox audit|bootstrap|harden|claude-parity [host...]" >&2; exit 2;; esac

# Guard before the host list is defaulted below: a bare `harden` would otherwise mean
# every box, and harden partitions a disk and arms a watchdog.
if [ "$cmd" = harden ] && [ "$#" -ne 1 ]; then
  echo "buildbox: harden takes exactly one host — it partitions a disk and arms a hardware watchdog, and getting either wrong on the whole fleet at once has no undo: buildbox harden <host>" >&2
  exit 2
fi

REGISTRY_CLI="$MOD/../workstation/claude/lib/buildbox-registry.mjs"
reg() { BUILDBOX_HOSTS_CONFIG="$REGISTRY" CPU_GUARD_ACTIVE=1 node "$REGISTRY_CLI" "$@"; }

if [ "$#" -gt 0 ]; then
  if ! hosts_csv="$(reg check "$(IFS=,; echo "$*")")"; then
    echo "buildbox: buildbox registry unusable — refusing to contact any host" >&2
    exit 2
  fi
else
  if ! hosts_csv="$(reg hosts --all)"; then
    echo "buildbox: buildbox registry unusable — refusing to contact any host" >&2
    exit 2
  fi
fi
IFS=',' read -r -a hosts <<<"$hosts_csv"
[ "${#hosts[@]}" -gt 0 ] || { echo "buildbox: no usable hosts in the buildbox registry" >&2; exit 2; }

# Every door is a dial target resolved per host from the registry. Nothing here is ever
# handed to a service as a bind address.
declare -A HOST_ADDR HOST_PORT HOST_USER HOST_IDENT
for h in "${hosts[@]}"; do
  if ! access="$(reg access "$h" --path tailscale_ip)"; then
    echo "buildbox: no tailscale_ip access for $h" >&2
    exit 2
  fi
  read -r target hport hident <<<"$access"
  HOST_USER["$h"]="${target%@*}"; HOST_ADDR["$h"]="${target#*@}"
  HOST_PORT["$h"]="$hport"
  [ "$hident" = "-" ] && hident=""
  HOST_IDENT["$h"]="${hident/#\~/$HOME}"
done

sshx() {
  local h="$1" ident="${HOST_IDENT[$1]}"
  ssh -F /dev/null -p "${HOST_PORT[$h]}" ${ident:+-i "$ident"} -o BatchMode=yes -o ConnectTimeout=8 \
    "${HOST_USER[$h]}@${HOST_ADDR[$h]}" "${@:2}"
}

# Every declared door, probed from here. A host answering on one door and dark on the
# others is one config change away from being unreachable, and only a probe says so.
probe_doors() {
  local h="$1" path line target dport
  for path in lan tailscale_ip tailscale_ssh; do
    if ! line="$(reg access "$h" --path "$path" 2>/dev/null)"; then
      echo "DOOR  $path undeclared"; continue
    fi
    read -r target dport _ <<<"$line"
    if timeout 6 bash -c "exec 3<>/dev/tcp/${target#*@}/$dport" 2>/dev/null; then
      echo "DOOR  $path ${target#*@}:$dport open"
    else
      echo "DRIFT $path ${target#*@}:$dport unreachable from this workstation"
    fi
  done
}

# The box cannot know its GC script has gone stale — only the generator here can say.
gc_sha() {
  local host="$1"
  node --input-type=module -e '
import { gcScriptSha } from "'"$MOD"'/../workstation/claude/lib/remote-build-gc-install.mjs";
import { readFileSync } from "node:fs";
const cfg = JSON.parse(readFileSync(process.argv[1], "utf8"));
process.stdout.write(gcScriptSha({ ...cfg, host: process.argv[2] }));
' "$CONFIG" "$host" 2>/dev/null
}

# The agent confinement pair used to be pushed on its own, from the working tree, while
# push_claude_home ships the whole `lib` and `bin` entries from wherever the manifest
# declares. Two writers for the same two files overwrite each other on every converge and
# the tree digest never settles. push_claude_home is now the only writer; item_agent_confine
# keeps the targeted verdict, because "the box runs agents unconfined" deserves better than
# a whole-tree digest mismatch.

# The prune keeps /tmp and ~/builds off their inode and byte floors, so a box without it
# ENOSPCs CI rather than degrading. The two boxes that had it were hand-installed and had
# already drifted apart (-mtime +1 prunes at 48h, not the 24h the unit describes).
push_scratch_prune() {
  local host="$1"
  tar -C "$USERCFG" -cf - bin/ci-scratch-prune.sh systemd/ci-scratch-prune.service systemd/ci-scratch-prune.timer \
    | sshx "$host" 'set -e
        mkdir -p ~/.local/bin ~/.config/systemd/user
        d=$(mktemp -d); trap "rm -rf $d" EXIT
        tar -xf - -C "$d"
        install -m 755 "$d/bin/ci-scratch-prune.sh" ~/.local/bin/ci-scratch-prune.sh
        install -m 644 "$d/systemd/ci-scratch-prune.service" ~/.config/systemd/user/ci-scratch-prune.service
        install -m 644 "$d/systemd/ci-scratch-prune.timer" ~/.config/systemd/user/ci-scratch-prune.timer
        systemctl --user daemon-reload
        systemctl --user enable --now ci-scratch-prune.timer >/dev/null' \
    && echo "PUSH  ci-scratch-prune from workstation"
}

# The build cache is bounded separately from /tmp. Its script declines to remove an
# entry until it is both old enough and the cache is over its size cap.
push_build_cache_prune() {
  local host="$1"
  tar -C "$USERCFG" -cf - bin/buildbox-build-cache-prune.sh systemd/buildbox-build-cache-prune.service systemd/buildbox-build-cache-prune.timer \
    | sshx "$host" 'set -e
        mkdir -p ~/.local/bin ~/.config/systemd/user
        d=$(mktemp -d); trap "rm -rf $d" EXIT
        tar -xf - -C "$d"
        install -m 755 "$d/bin/buildbox-build-cache-prune.sh" ~/.local/bin/buildbox-build-cache-prune.sh
        install -m 644 "$d/systemd/buildbox-build-cache-prune.service" ~/.config/systemd/user/buildbox-build-cache-prune.service
        install -m 644 "$d/systemd/buildbox-build-cache-prune.timer" ~/.config/systemd/user/buildbox-build-cache-prune.timer
        systemctl --user daemon-reload
        systemctl --user enable --now buildbox-build-cache-prune.timer >/dev/null' \
    && echo "PUSH  buildbox-build-cache-prune from workstation"
}

# The box samples its own kernel facts on a timer; the controller reads the published
# summary. The unit is started once here so a summary exists before the first check runs.
push_telemetry_sampler() {
  local host="$1"
  tar -C "$USERCFG" -cf - bin/buildbox-telemetry.sh systemd/buildbox-telemetry.service systemd/buildbox-telemetry.timer \
    | sshx "$host" 'set -e
        mkdir -p ~/.local/bin ~/.config/systemd/user
        d=$(mktemp -d); trap "rm -rf $d" EXIT
        tar -xf - -C "$d"
        install -m 755 "$d/bin/buildbox-telemetry.sh" ~/.local/bin/buildbox-telemetry.sh
        install -m 644 "$d/systemd/buildbox-telemetry.service" ~/.config/systemd/user/buildbox-telemetry.service
        install -m 644 "$d/systemd/buildbox-telemetry.timer" ~/.config/systemd/user/buildbox-telemetry.timer
        systemctl --user daemon-reload
        systemctl --user enable --now buildbox-telemetry.timer >/dev/null
        systemctl --user start buildbox-telemetry.service' \
    && echo "PUSH  buildbox-telemetry from workstation"
}

# The lab lives on whichever box the registry gives the dangerlab role — never a host
# name written into this file.
is_lab_host() {
  [ -r "$REGISTRY" ] || return 1
  python3 -c '
import json,sys
r=json.load(open(sys.argv[1]))
sys.exit(0 if any(h.get("name")==sys.argv[2] and "dangerlab" in h.get("roles",[]) for h in r.get("hosts",[])) else 1)
' "$REGISTRY" "$1"
}

# Every lab file the box must carry, as "<box path><TAB><sha256>". Root-owned slice
# units are audit-only, exactly like the rest of host-config.
lab_manifest() {
  local f
  for f in dangerlab dangerlab-run dangerlab-reap dangerlab-lib.sh provision.sh; do
    printf '$HOME/dangerlab/%s\t%s\n' "$f" "$(sha256sum "$DANGERLAB_SRC/$f" | cut -d' ' -f1)"
  done
  for f in "$USERCFG"/systemd/dangerlab-reaper.*; do
    printf '$HOME/.config/systemd/user/%s\t%s\n' "$(basename "$f")" "$(sha256sum "$f" | cut -d' ' -f1)"
  done
  for f in "$HOSTCFG"/systemd-system/machine-dangerlab*.slice; do
    printf '/etc/systemd/system/%s\t%s\n' "$(basename "$f")" "$(sha256sum "$f" | cut -d' ' -f1)"
  done
}

# The template tooling, its slot slices and the reaper are declared here; the box holds
# only copies. A hand-edit on the box is drift, and item_dangerlab reports it.
push_dangerlab() {
  local host="$1"
  is_lab_host "$host" || return 0
  tar -C "$MOD" -cf - dangerlab user-config/systemd/dangerlab-reaper.service \
      user-config/systemd/dangerlab-reaper.timer \
    | sshx "$host" 'set -e
        mkdir -p ~/dangerlab ~/.config/systemd/user
        d=$(mktemp -d); trap "rm -rf $d" EXIT
        tar -xf - -C "$d"
        for f in dangerlab dangerlab-run dangerlab-reap provision.sh; do
          install -m 755 "$d/dangerlab/$f" ~/dangerlab/$f
        done
        install -m 644 "$d/dangerlab/dangerlab-lib.sh" ~/dangerlab/dangerlab-lib.sh
        install -m 644 "$d/user-config/systemd/dangerlab-reaper.service" ~/.config/systemd/user/
        install -m 644 "$d/user-config/systemd/dangerlab-reaper.timer" ~/.config/systemd/user/
        systemctl --user daemon-reload
        systemctl --user enable --now dangerlab-reaper.timer >/dev/null' \
    && echo "PUSH  dangerlab from workstation"
}

push_npmrc_token() {
  local host="$1" line
  line=$(grep -m1 '_authToken' "$HOME/.npmrc" 2>/dev/null) || return 0
  sshx "$host" 'grep -q _authToken ~/.npmrc 2>/dev/null' && return 0
  registry=$(grep -m1 ':registry=' "$HOME/.npmrc" 2>/dev/null || true)
  printf '%s\n%s\n' "$registry" "$line" | sshx "$host" 'cat >> ~/.npmrc && chmod 600 ~/.npmrc' \
    && echo "PUSH  npmrc-token from workstation"
}

# The devtools repos are private https clones. A box whose gh token is invalid fails them
# as "could not read Username for 'https://github.com'", which names neither gh nor auth.
push_gh_token() {
  local host="$1"
  command -v gh >/dev/null 2>&1 || return 0
  gh auth token >/dev/null 2>&1 || return 0
  sshx "$host" 'gh auth status --hostname github.com >/dev/null 2>&1' && return 0
  gh auth token 2>/dev/null \
    | sshx "$host" 'gh auth login --hostname github.com --git-protocol https --with-token && gh auth setup-git' >/dev/null 2>&1 \
    && echo "PUSH  gh-token from workstation"
}

# The slice ceilings are the workstation's files, not a buildbox copy: a fork of them
# drifted once and dropped the ManagedOOM keys, which silently unregisters the boxes
# from systemd-oomd on the next harden.
boot_id_of() { sshx "$1" 'cat /proc/sys/kernel/random/boot_id' 2>/dev/null | tr -d '\r\n'; }

# sshd keeps accepting for a few seconds into a reboot, so an answer alone means
# nothing: wait for a boot id that differs from the one the change was applied under.
wait_for_reboot() { # host timeout-sec boot-id-before — prints the new boot id
  local host="$1" deadline=$(( $(date +%s) + $2 )) before="$3" bid
  while [ "$(date +%s)" -lt "$deadline" ]; do
    bid="$(boot_id_of "$host")"
    [ -n "$bid" ] && [ "$bid" != "$before" ] && { printf '%s' "$bid"; return 0; }
    sleep 5
  done
  return 1
}

# The exact tree a harden ships. audit hashes the same staging so the id it expects is
# the id an apply would write, never a hash of some other view of the repo.
stage_payload() {
  local payload
  payload="$(mktemp -d)"
  cp -a "$HOSTCFG/." "$payload/"
  mkdir -p "$payload/lib"
  cp -a "$MOD/lib/disk-admission.sh" "$payload/lib/"
  mkdir -p "$payload/systemd-user-root"
  cp -a "$CEILINGS/." "$payload/systemd-user-root/"
  printf '%s' "$payload"
}

harden_host() {
  local host="$1" payload rc=0 change bid_before bid_pre_reboot bid_after armed_at harden_wait
  # The wait has to end before the deadman fires, or a slow but healthy boot gets
  # reverted with nobody watching.
  local deadman_sec="${HARDEN_DEADMAN_SEC:-1200}" reboot_wait
  reboot_wait=$(( deadman_sec - 300 ))
  [ "$reboot_wait" -ge 60 ] || { echo "buildbox: HARDEN_DEADMAN_SEC=$deadman_sec leaves no room to wait out a reboot (need >= 360)" >&2; return 2; }
  [ -x "$HOSTCFG/apply.sh" ] || { echo "buildbox: host-config/apply.sh missing or not executable" >&2; return 2; }
  [ -d "$CEILINGS" ] || { echo "buildbox: slice ceilings missing: $CEILINGS" >&2; return 2; }
  [ -r "$DEADMAN_SRC" ] || { echo "buildbox: deadman missing: $DEADMAN_SRC" >&2; return 2; }
  payload="$(stage_payload)"

  change="$(fleet_change_id "$payload")"
  if ! fleet_gate_fanout "$change" "$host" || ! fleet_second_door "$host" "${HOST_USER[$host]}" "${HOST_PORT[$host]}"; then
    rm -rf "$payload"; return 1
  fi

  bid_before="$(boot_id_of "$host")"
  [ -n "$bid_before" ] || { echo "buildbox: $host is not reachable" >&2; rm -rf "$payload"; return 1; }

  sshx "$host" 'cat >/tmp/buildbox-deadman.$$ && sudo -n install -m 0755 /tmp/buildbox-deadman.$$ /usr/local/sbin/buildbox-deadman && rm -f /tmp/buildbox-deadman.$$' <"$DEADMAN_SRC" \
    || { echo "buildbox: could not install the deadman on $host" >&2; rm -rf "$payload"; return 1; }
  sshx "$host" "sudo -n /usr/local/sbin/buildbox-deadman arm --id $change --deadline-sec $deadman_sec" \
    || { echo "buildbox: could not arm the deadman on $host — refusing to apply unprotected" >&2; rm -rf "$payload"; return 1; }
  armed_at="$(date +%s)"

  tar -C "$payload" -czf - . \
    | sshx "$host" 'set -e
        d=$(mktemp -d /run/user/$(id -u)/buildbox-harden.XXXXXX)
        trap "rm -rf $d" EXIT
        tar -xzf - -C "$d"
        sudo -n bash "$d/apply.sh"' || rc=$?
  rm -rf "$payload"
  [ "$rc" = 0 ] || return "$rc"
  fleet_ledger_record "$change" "$host" applied
  # The engine pass reboots on its own when it writes a root file, so its wait gets only
  # what is left of the armed window after the reboot below has been reserved its full
  # share. Without the reservation two 900s waits run under one 1200s deadline and the
  # deadman reverts a healthy change mid-boot.
  harden_wait=$(( armed_at + deadman_sec - $(date +%s) - reboot_wait - 30 ))
  [ "$harden_wait" -ge 0 ] || harden_wait=0
  fleet_harden_pass "$host" "$harden_wait" || return 1

  echo "== $host rebooting to prove the change survives a boot (deadman restores last-known-good otherwise)"
  # The engine pass reboots the host when it wrote a root file, so the id this reboot is
  # proven against is read here rather than reused from before the change was applied.
  bid_pre_reboot="$(boot_id_of "$host")"
  [ -n "$bid_pre_reboot" ] || bid_pre_reboot="$bid_before"
  sshx "$host" 'sudo -n systemctl reboot' >/dev/null 2>&1 || true
  if ! bid_after="$(wait_for_reboot "$host" "$reboot_wait" "$bid_pre_reboot")"; then
    echo "buildbox: $host did not come back under a new boot id within ${reboot_wait}s. The deadman on the box restores last-known-good and restarts ssh at its deadline; do not apply this change anywhere else." >&2
    return 1
  fi
  sshx "$host" "sudo -n /usr/local/sbin/buildbox-deadman disarm $change" || return 1
  fleet_ledger_record "$change" "$host" reboot-proven "{\"bootIdBefore\":\"$bid_before\",\"bootIdAfter\":\"$bid_after\"}"
  echo "HARDEN $host change=$change reboot-proven"
}

# The fleet declaration names nodes; this file names registry hosts. The node is the one
# whose host_ref dials this host, and a host no node claims is drift the engine cannot
# audit, never a host to pass silently.
fleet_engine_pass() { # audit|converge host — ENGINE-prefixed report lines, nonzero on drift
  local verb="$1" host="$2"
  (
    cd "$REPO_ROOT"
    BUILDBOX_HOSTS_CONFIG="$REGISTRY" BUILDBOX_FLEET_ENGINE="$FLEET_ENGINE" \
    BUILDBOX_REPO_ROOT="$REPO_ROOT" CPU_GUARD_ACTIVE=1 \
    node --input-type=module -e '
import { loadFleet } from "./lib/fleet/loader.mjs";
import { expandNode } from "./lib/fleet/expand.mjs";
import { createLocalTransport } from "./lib/fleet/transport-local.mjs";
import { createSshTransport } from "./lib/fleet/transport-ssh.mjs";
import { loadRegistry } from "./modules/workstation/claude/lib/buildbox-registry.mjs";

const engine = await import(process.env.BUILDBOX_FLEET_ENGINE);
const [verb, host] = process.argv.slice(1);

function fail(detail) {
  process.stdout.write(`ENGINE FAIL ${host} ${detail}\n`);
  process.exit(1);
}

try {
  const fleet = loadFleet(process.env.DECKCTL_FLEET_FILE || undefined);
  const nodeName = fleet.nodeNames().find((name) => fleet.node(name).host_ref === host);
  if (!nodeName) {
    fail(`no fleet node declares host_ref ${JSON.stringify(host)}`);
  }
  const node = fleet.node(nodeName);
  const items = expandNode(fleet, nodeName);
  const registry = loadRegistry();
  const transport =
    node.transport === "local"
      ? createLocalTransport()
      : createSshTransport({ hostRef: node.host_ref, registry });

  const options = { repoRoot: process.env.BUILDBOX_REPO_ROOT };
  const report =
    verb === "converge"
      ? await engine.convergeNode(nodeName, items, transport, options)
      : await engine.auditNode(nodeName, items, transport, options);

  if (verb === "converge") {
    for (const id of report.changed) {
      process.stdout.write(`ENGINE APPLY ${nodeName} ${id}\n`);
    }
    for (const entry of report.failed) {
      process.stdout.write(`ENGINE FAIL ${nodeName} ${entry.id} ${entry.error}\n`);
    }
  }
  for (const line of engine.formatAuditReport(verb === "converge" ? report.finalAudit : report)) {
    process.stdout.write(`ENGINE ${line}\n`);
  }
  process.exit(engine.aggregateExitCode([report]));
} catch (err) {
  fail(String(err?.message ?? err));
}
' -- "$verb" "$host"
  )
}

# The root file installs apply.sh does not own. It runs after apply.sh and before the
# reboot, inside the armed window, so the same deadman reverts a bad write. The deadman
# refuses a second arm while one is armed, so this pass answers its own arm and disarm
# calls itself and leaves the outer window's pair to harden_host.
fleet_harden_pass() { # host reboot-wait-sec — HARDEN-ENGINE lines, nonzero on drift or failure
  local host="$1" reboot_wait="$2"
  (
    cd "$REPO_ROOT"
    BUILDBOX_HOSTS_CONFIG="$REGISTRY" BUILDBOX_FLEET_HARDEN="$FLEET_HARDEN" \
    BUILDBOX_REPO_ROOT="$REPO_ROOT" BUILDBOX_HARDEN_REBOOT_WAIT="$reboot_wait" \
    CPU_GUARD_ACTIVE=1 \
    node --input-type=module -e '
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { loadFleet } from "./lib/fleet/loader.mjs";
import { expandNode } from "./lib/fleet/expand.mjs";
import { createSshTransport } from "./lib/fleet/transport-ssh.mjs";
import { loadRegistry } from "./modules/workstation/claude/lib/buildbox-registry.mjs";

const harden = await import(process.env.BUILDBOX_FLEET_HARDEN);
const [host] = process.argv.slice(1);
const execFileAsync = promisify(execFile);

function fail(detail) {
  process.stdout.write(`HARDEN-ENGINE FAIL ${host} ${detail}\n`);
  process.exit(1);
}

const OUTER_DEADMAN = /buildbox-deadman (arm|disarm)\b/;

// A NaN here reaches waitForReboot as a deadline that is never in the future, which
// reports a reboot that never finished rather than a bad budget.
function rebootBudgetSec() {
  const raw = Number(process.env.BUILDBOX_HARDEN_REBOOT_WAIT);
  if (!Number.isFinite(raw)) fail("BUILDBOX_HARDEN_REBOOT_WAIT is not a number");
  return Math.max(0, Math.floor(raw));
}

async function sshExec(argv) {
  if (OUTER_DEADMAN.test(argv[argv.length - 1] ?? "")) {
    return { stdout: "", stderr: "", code: 0 };
  }
  try {
    const { stdout, stderr } = await execFileAsync(argv[0], argv.slice(1), { encoding: "utf8" });
    return { stdout: stdout ?? "", stderr: stderr ?? "", code: 0 };
  } catch (err) {
    return {
      stdout: err?.stdout ?? "",
      stderr: err?.stderr ?? "",
      code: typeof err?.code === "number" ? err.code : 1,
    };
  }
}

try {
  const fleet = loadFleet(process.env.DECKCTL_FLEET_FILE || undefined);
  const nodeName = fleet.nodeNames().find((name) => fleet.node(name).host_ref === host);
  if (!nodeName) {
    fail(`no fleet node declares host_ref ${JSON.stringify(host)}`);
  }
  const node = fleet.node(nodeName);
  const items = expandNode(fleet, nodeName);
  const selected = items.filter((item) => item.mode === "harden");
  if (selected.length === 0) {
    fail(`${nodeName} selects no harden item — it no longer declares the buildbox-root profile`);
  }
  const transport = createSshTransport({ hostRef: node.host_ref, registry: loadRegistry() });
  const report = await harden.hardenNode(nodeName, items, transport, {
    repoRoot: process.env.BUILDBOX_REPO_ROOT,
    rebootTimeoutSec: rebootBudgetSec(),
    sshExec,
  });
  for (const id of report.changed) {
    process.stdout.write(`HARDEN-ENGINE APPLY ${nodeName} ${id}\n`);
  }
  process.stdout.write(
    `HARDEN-ENGINE ${report.finalAudit.ok ? "OK" : "DRIFT"} ${nodeName}` +
      ` items=${selected.length} changed=${report.changed.length}` +
      ` rebooted=${report.rebooted ? "yes" : "no"}\n`,
  );
  process.exit(report.finalAudit.ok && !report.finalAudit.unreachable ? 0 : 1);
} catch (err) {
  fail(String(err?.message ?? err));
}
' -- "$host"
  )
}

check_host() {
  local host="$1" rc=0
  echo "== $host ($cmd)"
  probe_doors "$host"
  if [ "$cmd" = bootstrap ]; then
    push_npmrc_token "$host"
    push_gh_token "$host"
    push_claude_home "$host" "$CLAUDE_STAGE"
    push_scratch_prune "$host"
    push_build_cache_prune "$host"
    push_telemetry_sampler "$host"
    push_dangerlab "$host"
    fleet_engine_pass converge "$host" || rc=1
  fi
  local lab=0; is_lab_host "$host" && lab=1
  sshx "$host" "EXPECT_HOSTCFG_CHANGE=$EXPECT_HOSTCFG_CHANGE \
    EXPECT_CLAUDE_HOME=$EXPECT_CLAUDE_HOME \
    CLAUDE_HOME_ENTRIES=$CLAUDE_HOME_ENTRIES \
    EXPECT_BUN_VERSION=$EXPECT_BUN_VERSION \
    EXPECT_LAB_HOST=$lab \
    EXPECT_LAB_MANIFEST=$(lab_manifest | base64 -w0) \
    EXPECT_GC_SHA=$(gc_sha "$host") \
    EXPECT_CONFINE_SHA=$(sha256sum "$CONFINE_SRC" | cut -d' ' -f1) \
    EXPECT_SCOPE_SHA=$(sha256sum "$SCOPE_SRC" | cut -d' ' -f1) \
    EXPECT_PRUNE_SHA=$(sha256sum "$PRUNE_SRC" | cut -d' ' -f1) \
    EXPECT_CACHE_PRUNE_SHA=$(sha256sum "$CACHE_PRUNE_SRC" | cut -d' ' -f1) \
    EXPECT_TELEMETRY_SHA=$(sha256sum "$TELEMETRY_SRC" | cut -d' ' -f1) \
    DEVTOOLS_B64=$(base64 -w0 "$DEVTOOLS") \
    EXPECT_GIT_NAME=$(printf %q "$(git config --get user.name)") \
    EXPECT_GIT_EMAIL=$(printf %q "$(git config --get user.email)") \
    bash -s $cmd" < <(cat "$PARITY" "$LIB") || rc=$?
  fleet_engine_pass audit "$host" || rc=1
  return "$rc"
}

# One line per host, no door probes and no devtools checks: the seat stack gates a launch
# on this and cannot pay for a 28-item audit.
parity_host() {
  local host="$1" line rc=0
  line=$(sshx "$host" "EXPECT_CLAUDE_HOME=$EXPECT_CLAUDE_HOME \
    CLAUDE_HOME_ENTRIES=$CLAUDE_HOME_ENTRIES \
    EXPECT_BUN_VERSION=$EXPECT_BUN_VERSION \
    bash -s" < <(cat "$PARITY"; echo 'claude_parity_report') 2>/dev/null) || rc=$?
  if [ -z "$line" ]; then
    printf '%-10s unreachable (registry marks it reachable — that is itself drift)\n' "$host"
    return 1
  fi
  printf '%-10s %s\n' "$host" "$line"
  return "$rc"
}

EXPECT_HOSTCFG_CHANGE=""
if [ "$cmd" != harden ] && [ "$cmd" != claude-parity ]; then
  _payload="$(stage_payload)"
  EXPECT_HOSTCFG_CHANGE="$(fleet_change_id "$_payload")"
  rm -rf "$_payload"
fi

# One staging for the whole run: the payload is the workstation's, not a per-host view of
# it, and re-staging per host would let two boxes converge to two different trees.
EXPECT_CLAUDE_HOME=""; CLAUDE_HOME_ENTRIES=""; EXPECT_BUN_VERSION=""
CLAUDE_STAGE=""
if [ "$cmd" != harden ]; then
  CLAUDE_STAGE="$(claude_home_stage)" || exit 1
  trap 'rm -rf "$CLAUDE_STAGE"' EXIT
  readarray -t _entries <"$CLAUDE_STAGE/entries"
  EXPECT_CLAUDE_HOME="$(claude_home_digest "$CLAUDE_STAGE/tree" "${_entries[@]}")"
  CLAUDE_HOME_ENTRIES="$(base64 -w0 <"$CLAUDE_STAGE/entries")"
  EXPECT_BUN_VERSION="$("$CLAUDE_STAGE/tree/bin/bun" --version 2>/dev/null | tr -d '\r\n')"
  [ -n "$EXPECT_BUN_VERSION" ] || { echo "buildbox: the workstation's own ~/.claude/bin/bun wrapper does not run — refusing to assert a version no box can match" >&2; exit 2; }
fi

rc=0
# Fanned out and buffered like audit: three serial ssh round-trips over a tailnet is long
# enough that a seat launch would rather skip the check than wait for it.
if [ "$cmd" = claude-parity ]; then
  pbuf="$(mktemp -d)"
  trap 'rm -rf "$pbuf" "$CLAUDE_STAGE"' EXIT
  ppids=()
  for i in "${!hosts[@]}"; do
    parity_host "${hosts[$i]}" >"$pbuf/$i" 2>&1 &
    ppids+=("$!")
  done
  for i in "${!hosts[@]}"; do wait "${ppids[$i]}" || rc=1; done
  for i in "${!hosts[@]}"; do cat "$pbuf/$i"; done
  exit "$rc"
fi

if [ "$cmd" = harden ]; then
  echo "== ${hosts[0]} (harden)"
  harden_host "${hosts[0]}" || rc=1
  exit "$rc"
fi

# audit and bootstrap are read-mostly, idempotent and have no cross-host coupling, so
# they fan out. Each host's output is buffered to its own file and printed in declared
# order afterwards: interleaved OK/DRIFT lines from three boxes are unattributable, and
# a drift line pinned on the wrong box is worse than a slow audit.
buf="$(mktemp -d)"
trap 'rm -rf "$buf" "$CLAUDE_STAGE"' EXIT
pids=()
for i in "${!hosts[@]}"; do
  check_host "${hosts[$i]}" >"$buf/$i" 2>&1 &
  pids+=("$!")
done
for i in "${!hosts[@]}"; do
  wait "${pids[$i]}" || rc=1
done
for i in "${!hosts[@]}"; do
  cat "$buf/$i"
done
exit "$rc"
