#!/usr/bin/env bash
# Runs ON a build box (shipped over ssh by bin/buildbox). Declares the state a box
# must converge to; never copies state from another box.
#   checks.sh audit      -> print OK/DRIFT per item, exit 1 on any drift
#   checks.sh bootstrap  -> converge user-level items, report root-level ones
set -uo pipefail

MODE="${1:-audit}"
drift=0

ok()    { printf 'OK    %-14s %s\n' "$1" "$2"; }
bad()   { printf 'DRIFT %-14s %s\n' "$1" "$2"; drift=1; }
fixed() { printf 'FIXED %-14s %s\n' "$1" "$2"; }

export PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:$HOME/.cargo/bin:$PATH"

NODE_MAJOR=24
BUN_MAJOR=1

item_mise() {
  if command -v mise >/dev/null 2>&1; then ok mise "$(mise --version 2>/dev/null | head -1)"; return; fi
  if [ "$MODE" = bootstrap ]; then
    curl -fsSL https://mise.run | sh >/dev/null 2>&1 && fixed mise installed || bad mise "install failed"
  else
    bad mise missing
  fi
}

item_node() {
  local v; v=$(node --version 2>/dev/null)
  case "$v" in
    v${NODE_MAJOR}.*) ok node "$v"; return;;
  esac
  if [ "$MODE" = bootstrap ] && command -v mise >/dev/null 2>&1; then
    mise use -g "node@${NODE_MAJOR}" >/dev/null 2>&1 && fixed node "$(node --version 2>/dev/null)" || bad node "mise install failed (was: ${v:-absent})"
  else
    bad node "want v${NODE_MAJOR}.x, have ${v:-none}"
  fi
}

# local-gate, remote-runner preflight, and cursor-seat shells inherit a PATH with
# /usr/local/bin and ~/.local/bin but not the mise shims dir — mise-installed node
# alone is invisible there.
item_node_system() {
  local v node_bin
  v=$(/usr/local/bin/node --version 2>/dev/null)
  case "$v" in
    v${NODE_MAJOR}.*) ok node-system "$v"; return;;
  esac
  if [ "$MODE" = bootstrap ] && command -v mise >/dev/null 2>&1; then
    node_bin="$(mise which node 2>/dev/null)" || node_bin=""
    if [ -n "$node_bin" ] && [ -x "$node_bin" ] \
      && sudo -n install -m 0755 "$node_bin" /usr/local/bin/node 2>/dev/null; then
      fixed node-system "$(/usr/local/bin/node --version 2>/dev/null)"
    else
      bad node-system "missing (want v${NODE_MAJOR}.x in /usr/local/bin)"
    fi
  else
    bad node-system "want v${NODE_MAJOR}.x in /usr/local/bin, have ${v:-none}"
  fi
}

# remote-runner loads ~/.config/environment.d/*.conf before mise; without an explicit
# base PATH, non-login jobs can inherit /usr/bin:/bin only and miss /usr/local/bin/node.
item_runner_env() {
  local conf="$HOME/.config/environment.d/10-buildbox-base.conf"
  local want='PATH=/usr/local/bin:/usr/bin:/bin'
  if [ -f "$conf" ] && grep -qxF "$want" "$conf" 2>/dev/null; then
    ok runner-env "$want"
    return
  fi
  if [ "$MODE" = bootstrap ]; then
    mkdir -p "$HOME/.config/environment.d" \
      && printf '%s\n' "$want" >"$conf" \
      && fixed runner-env "$want" || bad runner-env "write failed"
  else
    bad runner-env "want $want"
  fi
}

item_corepack_shims() {
  if [ -x "$HOME/.local/bin/pnpm" ]; then ok corepack-shims "$HOME/.local/bin/pnpm"; return; fi
  if [ "$MODE" = bootstrap ]; then
    mkdir -p "$HOME/.local/bin" \
      && COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack enable --install-directory "$HOME/.local/bin" >/dev/null 2>&1 \
      && fixed corepack-shims "$HOME/.local/bin/pnpm" || bad corepack-shims "corepack enable failed"
  else
    bad corepack-shims "pnpm shim missing in ~/.local/bin"
  fi
}

item_bun() {
  local v; v=$(bun --version 2>/dev/null)
  case "$v" in
    ${BUN_MAJOR}.*) ok bun "$v"; return;;
  esac
  # bun.sh/install drops the binary in ~/.bun/bin, which is on no PATH any launcher here
  # builds — a box installed that way reports missing forever. mise owns the shim instead.
  if [ "$MODE" = bootstrap ] && command -v mise >/dev/null 2>&1; then
    mise use -g "bun@${BUN_MAJOR}" >/dev/null 2>&1 && fixed bun "$(bun --version 2>/dev/null)" || bad bun "mise install failed (was: ${v:-absent})"
  else
    bad bun "want ${BUN_MAJOR}.x, have ${v:-none}"
  fi
}

item_rust() {
  if command -v cargo >/dev/null 2>&1; then ok rust "$(cargo --version 2>/dev/null | cut -d' ' -f2)"; return; fi
  if [ "$MODE" = bootstrap ]; then
    curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal >/dev/null 2>&1 \
      && fixed rust "$("$HOME/.cargo/bin/cargo" --version | cut -d' ' -f2)" || bad rust "rustup install failed"
  else
    bad rust missing
  fi
}

item_builds_dir() {
  if [ -d "$HOME/builds" ]; then ok builds-dir "$HOME/builds"; return; fi
  if [ "$MODE" = bootstrap ]; then mkdir -p "$HOME/builds" && fixed builds-dir "$HOME/builds"; else bad builds-dir missing; fi
}

item_linger() {
  if [ "$(loginctl show-user "$USER" -p Linger --value 2>/dev/null)" = yes ]; then ok linger yes; return; fi
  if [ "$MODE" = bootstrap ] && loginctl enable-linger "$USER" 2>/dev/null; then fixed linger enabled; else
    bad linger "disabled — systemd-run --user jobs die on disconnect (needs root: loginctl enable-linger $USER)"
  fi
}

item_npmrc_token() {
  if grep -q _authToken "$HOME/.npmrc" 2>/dev/null; then ok npmrc-token present; return; fi
  # secret comes from the workstation; bin/buildbox appends it before invoking bootstrap
  bad npmrc-token "no _authToken in ~/.npmrc (private registry installs will 401)"
}

# root-installed by host-config/apply.sh (buildbox harden), report-only here.
# Every item below is a layer of the "no reset button" failsafe; drift means an
# unattended box can wedge with nobody to power-cycle it.
item_failsafe() {
  local want got key prevent
  # read /proc/sys directly: sysctl(8) lives in /usr/sbin, off a plain user's PATH
  while read -r key want; do
    got=$(cat "/proc/sys/${key//.//}" 2>/dev/null)
    [ "$got" = "$want" ] && ok "sysctl" "$key=$got" || bad "sysctl" "$key=${got:-unset} want $want (root: buildbox harden)"
  done <<'SYSCTLS'
kernel.panic 20
kernel.panic_on_oops 1
kernel.sysrq 1
vm.panic_on_oom 0
vm.swappiness 10
net.ipv4.ip_nonlocal_bind 1
net.ipv6.ip_nonlocal_bind 1
SYSCTLS

  got=$(systemctl show -p RuntimeWatchdogUSec --value 2>/dev/null)
  case "$got" in 0|"") bad watchdog "RuntimeWatchdogUSec=0 — a wedged PID1 never resets the box (root: buildbox harden)";;
    *) ok watchdog "RuntimeWatchdogUSec=$got state=$(cat /sys/class/watchdog/watchdog0/state 2>/dev/null)";; esac

  # A cold boot races sshd's ListenAddress against tailscale0. ip_nonlocal_bind above wins
  # that race; these recover the box if anything else fails the bind. Restart=on-failure is
  # the packaged default and proves nothing on its own — sshd exits 255 on a failed bind and
  # Debian's unit lists 255 as unrecoverable, so the cleared RestartPreventExitStatus is what
  # makes the policy reach this failure at all.
  got=$(systemctl show ssh.service -p Restart --value 2>/dev/null)
  prevent=$(systemctl show ssh.service -p RestartPreventExitStatus --value 2>/dev/null)
  [ "$got" = on-failure ] && [ -z "$prevent" ] && ok sshd-restart "Restart=$got, no prevented exit status" \
    || bad sshd-restart "ssh.service Restart=${got:-?} RestartPreventExitStatus=${prevent:-} — a failed bind leaves no rescue path (root: buildbox harden)"

  # The restart policy cannot help a box whose hardened ListenAddress no longer belongs to
  # this node: every attempt fails the same way. The timer re-derives it from tailscaled.
  got=$(systemctl is-active buildbox-sshd-access.timer 2>/dev/null)
  [ "$got" = active ] && ok sshd-access-timer "active" \
    || bad sshd-access-timer "buildbox-sshd-access.timer ${got:-absent} — a stale ListenAddress would never be repaired (root: buildbox harden)"

  # ssh.socket's ListenStream would govern over the Port/ListenAddress drop-in.
  got=$(systemctl is-enabled ssh.socket 2>/dev/null)
  [ "$got" = masked ] && ok sshd-socket "ssh.socket masked" \
    || bad sshd-socket "ssh.socket is ${got:-absent}, not masked — activating it overrides the 2222 drop-in (root: buildbox harden)"

  got=$(cat /sys/fs/cgroup/system.slice/ssh.service/memory.min 2>/dev/null)
  [ "${got:-0}" -gt 0 ] 2>/dev/null && ok ssh-memmin "memory.min=$got" \
    || bad ssh-memmin "memory.min=${got:-?} — sshd unprotected under reclaim (root: buildbox harden)"

  got=$(systemctl show "user@$(id -u).service" -p ManagedOOMSwap --value 2>/dev/null)
  [ "$got" = kill ] && ok oomd-swap "user@$(id -u) ManagedOOMSwap=kill" \
    || bad oomd-swap "user@$(id -u) ManagedOOMSwap=${got:-?} (root: buildbox harden)"

  # The outcome that matters: oomd is watching the slices the agents run in. Both are on
  # memory pressure only — zram makes swap fullness the steady state here, so a swap kill
  # cannot tell heavy agent work from a runaway, and the slice drop-ins leave
  # ManagedOOMSwap at auto. oomd is told about a slice when the user manager loads it, so
  # an idle one is absent.
  for slice in agent.slice:1 build.slice:1; do
    want="${slice#*:}"; slice="${slice%:*}"
    if [ "$(systemctl --user is-active "$slice" 2>/dev/null)" != active ]; then
      ok "oomd-monitor" "$slice idle — registers when a seat starts"
      continue
    fi
    got=$(oomctl 2>/dev/null | grep -c "/user@$(id -u).service/$slice\$")
    [ "${got:-0}" = "$want" ] && ok oomd-monitor "$slice on $want monitored list(s)" \
      || bad oomd-monitor "$slice on ${got:-0}/$want oomd lists (root: buildbox harden)"
  done

  for f in /etc/systemd/user/agent.slice.d/90-ceiling.conf /etc/systemd/user/build.slice.d/90-ceiling.conf; do
    [ -r "$f" ] && ok ceiling "$f" || bad ceiling "$f missing (root: buildbox harden)"
  done
}

# root-installed by host-config/apply.sh (buildbox harden), report-only here.
# bin/buildbox and remote-build reach every box with `ssh -F /dev/null` on the single
# port in build-remote.json; port 22 belongs to tailscaled's own SSH server, and an
# OpenSSH listener there shadows nothing but does expose the box off the tailnet.
item_sshd_port() {
  local on off bound want
  on=$(ss -H -lnt 'sport = :2222' 2>/dev/null | awk '{print $4}' | paste -sd' ' -)
  off=$(ss -H -lnt 'sport = :22' 2>/dev/null | awk '{print $4}' | paste -sd' ' -)
  [ -n "$on" ] && ok sshd-port "2222 on $on" \
    || bad sshd-port "nothing listening on 2222 — every launcher reaches this box there (root: buildbox harden)"

  # A half-won bind race leaves one address family up: reachable today, dark for anything
  # that resolves the other family.
  bound=$(ss -H -lnt 'sport = :2222' 2>/dev/null | awk '{print $4}' | sed 's/:2222$//; s/^\[//; s/\]$//' | sort -u)
  want=$(printf '0.0.0.0\n::\n' | sort -u)
  [ "$bound" = "$want" ] && ok sshd-bound "both wildcards bound" \
    || bad sshd-bound "bound [$(echo $bound)] want [$(echo $want)] — a bind that can fail is a box that cannot be reached without a console (root: buildbox harden)"
  [ -z "$off" ] && ok sshd-port22 "unused" \
    || bad sshd-port22 "OpenSSH also on 22 ($off) — undeclared, off-tailnet reachable (root: buildbox harden)"
}

# The two doors that survive a converge of the one above. Reported per box because a
# box with one door is one bad change away from the state debian2 and debian3 are in.
item_recovery_doors() {
  local bound want got
  bound=$(ss -H -lnt 'sport = :2223' 2>/dev/null | awk '{print $4}' | sed 's/:2223$//; s/^\[//; s/\]$//' | sort -u)
  want=$(printf '0.0.0.0\n::\n' | sort -u)
  [ "$bound" = "$want" ] && ok rescue-door "2223 on both wildcards" \
    || bad rescue-door "bound [$(echo $bound)] want [$(echo $want)] — no way in that a converge of /etc/ssh cannot take down (root: buildbox harden)"

  # tailscaled serves this itself, so no sshd config can remove it — but it lives in
  # tailscaled's prefs, which re-registering a node resets.
  got=$(tailscale debug prefs 2>/dev/null | sed -n 's/.*"RunSSH": *\([a-z]*\).*/\1/p')
  [ "$got" = true ] && ok tailscale-ssh "RunSSH=true" \
    || bad tailscale-ssh "RunSSH=${got:-unreadable} — the door that kept debian1 reachable is off (root: buildbox harden)"

  got=$(systemctl is-active buildbox-tailscale-ssh.timer 2>/dev/null)
  [ "$got" = active ] && ok tailscale-ssh-timer "active" \
    || bad tailscale-ssh-timer "buildbox-tailscale-ssh.timer ${got:-absent} — a node re-registration would silently drop Tailscale SSH (root: buildbox harden)"
}

JOURNAL_CAP=4G
JOURNAL_STORAGE=persistent
# Overridable so the suite can point the check at a fixture tree.
JOURNALD_CONF_ROOT="${JOURNALD_CONF_ROOT:-}"

item_journal_cap() {
  local conf="$JOURNALD_CONF_ROOT/etc/systemd/journald.conf"
  local dir="$JOURNALD_CONF_ROOT/etc/systemd/journald.conf.d"
  local declared_cap declared_storage
  # tail -1 mirrors systemd precedence: the last drop-in read wins.
  declared_cap=$(grep -hs '^SystemMaxUse=' "$conf" "$dir"/*.conf 2>/dev/null | tail -1)
  declared_storage=$(grep -hs '^Storage=' "$conf" "$dir"/*.conf 2>/dev/null | tail -1)
  if [ "$declared_cap" = "SystemMaxUse=$JOURNAL_CAP" ] && [ "$declared_storage" = "Storage=$JOURNAL_STORAGE" ]; then
    ok journal-cap "$JOURNAL_CAP, $JOURNAL_STORAGE"
    return
  fi
  bad journal-cap "want SystemMaxUse=$JOURNAL_CAP + Storage=$JOURNAL_STORAGE, have ${declared_cap:-unset} / ${declared_storage:-unset} (root: printf '[Journal]\nStorage=$JOURNAL_STORAGE\nSystemMaxUse=$JOURNAL_CAP\n' > /etc/systemd/journald.conf.d/90-cap.conf && systemctl restart systemd-journald)"
}

# Which host-config this box actually received, and when. The stamp is written by
# host-config/apply.sh after a successful apply, so a box that was never hardened, or was
# fixed by hand, cannot look current.
item_host_config() {
  local stamp=/etc/buildbox/host-config.stamp have applied
  if [ -z "${EXPECT_HOSTCFG_CHANGE:-}" ]; then
    bad host-config "expected change id not supplied (run audit via bin/buildbox, not by hand)"
    return
  fi
  if [ ! -r "$stamp" ]; then
    bad host-config "never applied: no $stamp (fix: buildbox harden <host>)"
    return
  fi
  have=$(sed -n 's/^change=//p' "$stamp")
  applied=$(sed -n 's/^applied=//p' "$stamp")
  if [ "$have" != "$EXPECT_HOSTCFG_CHANGE" ]; then
    bad host-config "box $have applied $applied, canonical $EXPECT_HOSTCFG_CHANGE (fix: buildbox harden <host>)"
  else
    ok host-config "$have applied $applied"
  fi
}

# The box GC script is a materialization of boxGcScript(); nothing on the box can tell it has
# gone stale, so bin/buildbox passes the generator's current hash in and the box compares.
item_gc_script() {
  local have
  if [ -z "${EXPECT_GC_SHA:-}" ]; then
    bad gc-script "expected hash not supplied (run audit via bin/buildbox, not by hand)"
    return
  fi
  have=$(sha256sum "$HOME/.local/bin/rb-mirror-gc" 2>/dev/null | cut -d' ' -f1)
  if [ -z "$have" ]; then
    bad gc-script "not installed (fix: rb-gc-install)"
  elif [ "$have" != "$EXPECT_GC_SHA" ]; then
    bad gc-script "stale: box ${have:0:12} want ${EXPECT_GC_SHA:0:12} (fix: rb-gc-install)"
  elif [ "$(systemctl --user is-enabled rb-mirror-gc.timer 2>/dev/null)" != enabled ]; then
    bad gc-script "current but timer not enabled (fix: rb-gc-install)"
  else
    ok gc-script "${have:0:12} timer enabled"
  fi
}

# The build PATH carries ~/.local/bin but not the mise shims dir, so the shim alone is not
# enough — the check is on the resolved binary in ~/.local/bin.
item_gitleaks() {
  if [ -x "$HOME/.local/bin/gitleaks" ]; then ok gitleaks "$("$HOME/.local/bin/gitleaks" version 2>/dev/null)"; return; fi
  if [ "$MODE" = bootstrap ] && command -v mise >/dev/null 2>&1; then
    mkdir -p "$HOME/.local/bin" \
      && mise use -g gitleaks@latest >/dev/null 2>&1 \
      && ln -sfn "$(mise which gitleaks)" "$HOME/.local/bin/gitleaks" \
      && fixed gitleaks "$("$HOME/.local/bin/gitleaks" version 2>/dev/null)" || bad gitleaks "mise install failed"
  else
    bad gitleaks "missing in ~/.local/bin"
  fi
}

# Gated runs reach gitleaks through the system PATH, and $HOME/.local/bin is not on it for
# every launcher — bin/import-module.sh aborts with "gitleaks not installed" when only the
# home copy exists.
item_gitleaks_system() {
  if [ -x /usr/local/bin/gitleaks ]; then ok gitleaks-system "$(/usr/local/bin/gitleaks version 2>/dev/null)"; return; fi
  if [ "$MODE" = bootstrap ] && [ -x "$HOME/.local/bin/gitleaks" ] \
    && sudo -n install -m 0755 "$(readlink -f "$HOME/.local/bin/gitleaks")" /usr/local/bin/gitleaks 2>/dev/null; then
    fixed gitleaks-system "$(/usr/local/bin/gitleaks version 2>/dev/null)"
  else
    bad gitleaks-system "missing (root: install gitleaks in /usr/local/bin)"
  fi
}

# A box with no committer identity fails every test that builds a commit; git reports it as
# an auto-detect failure on user@<host>.(none), not as missing configuration.
item_git_identity() {
  local name email
  name=$(git config --global --get user.name 2>/dev/null)
  email=$(git config --global --get user.email 2>/dev/null)
  if [ -n "$name" ] && [ -n "$email" ]; then ok git-identity "$name <$email>"; return; fi
  if [ "$MODE" = bootstrap ] && [ -n "${EXPECT_GIT_NAME:-}" ] && [ -n "${EXPECT_GIT_EMAIL:-}" ]; then
    git config --global user.name "$EXPECT_GIT_NAME" \
      && git config --global user.email "$EXPECT_GIT_EMAIL" \
      && fixed git-identity "$EXPECT_GIT_NAME <$EXPECT_GIT_EMAIL>" || bad git-identity "git config failed"
  else
    bad git-identity "unset (fix: buildbox bootstrap)"
  fi
}

# Both halves are required before a private https clone works: a valid token, and the git
# credential helper that hands it to git. Either one missing surfaces downstream only as
# item_devtools "clone failed", which names neither gh nor auth.
item_gh_auth() {
  local acct
  command -v gh >/dev/null 2>&1 || { bad gh-auth "gh missing (root: apt install gh)"; return; }
  if ! gh auth status --hostname github.com >/dev/null 2>&1; then
    bad gh-auth "token invalid or absent (fix: buildbox bootstrap)"; return
  fi
  if ! git config --global --get-regexp '^credential\.https://github\.com\.helper$' >/dev/null 2>&1; then
    if [ "$MODE" = bootstrap ] && gh auth setup-git 2>/dev/null; then
      fixed gh-auth "credential helper installed"; return
    fi
    bad gh-auth "no git credential helper for github.com (fix: buildbox bootstrap)"; return
  fi
  acct=$(gh auth status --hostname github.com 2>&1 | awk '/Logged in to/ {print $(NF-1); exit}')
  ok gh-auth "${acct:-authenticated}"
}

# An agent CLI on this box runs under ~/.claude/bin/_agent-build-scope, which execs
# ~/.claude/lib/confine.sh. Both are workstation files; a box carrying an older copy runs
# agents unconfined and cannot detect that by itself.
item_agent_confine() {
  local pair have want
  for pair in "confine:$HOME/.claude/lib/confine.sh:${EXPECT_CONFINE_SHA:-}" \
              "build-scope:$HOME/.claude/bin/_agent-build-scope:${EXPECT_SCOPE_SHA:-}"; do
    IFS=: read -r name path want <<<"$pair"
    if [ -z "$want" ]; then
      bad "$name" "expected hash not supplied (run via bin/buildbox, not by hand)"
      continue
    fi
    have=$(sha256sum "$path" 2>/dev/null | cut -d' ' -f1)
    if [ -z "$have" ]; then
      bad "$name" "not installed (fix: buildbox bootstrap)"
    elif [ "$have" != "$want" ]; then
      bad "$name" "stale: box ${have:0:12} want ${want:0:12} (fix: buildbox bootstrap)"
    else
      ok "$name" "${have:0:12}"
    fi
  done
}

# The prune script is the workstation's file; a box cannot tell its copy has drifted, and the
# two boxes that carried a hand-installed copy had already diverged on the age cutoff.
item_scratch_prune() {
  local have
  if [ -z "${EXPECT_PRUNE_SHA:-}" ]; then
    bad scratch-prune "expected hash not supplied (run via bin/buildbox, not by hand)"
    return
  fi
  have=$(sha256sum "$HOME/.local/bin/ci-scratch-prune.sh" 2>/dev/null | cut -d' ' -f1)
  if [ -z "$have" ]; then
    bad scratch-prune "not installed (fix: buildbox bootstrap)"
  elif [ "$have" != "$EXPECT_PRUNE_SHA" ]; then
    bad scratch-prune "stale: box ${have:0:12} want ${EXPECT_PRUNE_SHA:0:12} (fix: buildbox bootstrap)"
  elif [ "$(systemctl --user is-enabled ci-scratch-prune.timer 2>/dev/null)" != enabled ]; then
    bad scratch-prune "current but timer not enabled (fix: buildbox bootstrap)"
  else
    ok scratch-prune "${have:0:12} timer enabled"
  fi
}

# The cache pruner is independent of the /tmp prune. It must be installed from this
# module and armed as a user timer, otherwise every build box slowly accumulates mirrors.
item_build_cache_prune() {
  local have
  if [ -z "${EXPECT_CACHE_PRUNE_SHA:-}" ]; then
    bad build-cache-prune "expected hash not supplied (run via bin/buildbox, not by hand)"
    return
  fi
  have=$(sha256sum "$HOME/.local/bin/buildbox-build-cache-prune.sh" 2>/dev/null | cut -d' ' -f1)
  if [ -z "$have" ]; then
    bad build-cache-prune "not installed (fix: buildbox bootstrap)"
  elif [ "$have" != "$EXPECT_CACHE_PRUNE_SHA" ]; then
    bad build-cache-prune "stale: box ${have:0:12} want ${EXPECT_CACHE_PRUNE_SHA:0:12} (fix: buildbox bootstrap)"
  elif [ "$(systemctl --user is-enabled buildbox-build-cache-prune.timer 2>/dev/null)" != enabled ]; then
    bad build-cache-prune "current but timer not enabled (fix: buildbox bootstrap)"
  elif [ "$(systemctl --user is-active buildbox-build-cache-prune.timer 2>/dev/null)" != active ]; then
    bad build-cache-prune "timer enabled but not active (fix: buildbox bootstrap)"
  else
    ok build-cache-prune "${have:0:12} timer active"
  fi
}

# An enabled timer is not the fact worth checking. The controller drops a box whose summary
# has aged out, so a box that publishes nothing is invisible in the fleet panel rather than
# loud; this check is what says so out loud.
item_telemetry_sampler() {
  local have summary age
  if [ -z "${EXPECT_TELEMETRY_SHA:-}" ]; then
    bad telemetry-sampler "expected hash not supplied (run via bin/buildbox, not by hand)"
    return
  fi
  have=$(sha256sum "$HOME/.local/bin/buildbox-telemetry.sh" 2>/dev/null | cut -d' ' -f1)
  summary="$HOME/.local/state/overdeck/buildbox-telemetry.json"
  age=$(( $(date +%s) - $(stat -c %Y "$summary" 2>/dev/null || echo 0) ))
  if [ -z "$have" ]; then
    bad telemetry-sampler "not installed (fix: buildbox bootstrap)"
  elif [ "$have" != "$EXPECT_TELEMETRY_SHA" ]; then
    bad telemetry-sampler "stale: box ${have:0:12} want ${EXPECT_TELEMETRY_SHA:0:12} (fix: buildbox bootstrap)"
  elif [ "$(systemctl --user is-enabled buildbox-telemetry.timer 2>/dev/null)" != enabled ]; then
    bad telemetry-sampler "current but timer not enabled (fix: buildbox bootstrap)"
  elif [ "$(systemctl --user is-active buildbox-telemetry.timer 2>/dev/null)" != active ]; then
    bad telemetry-sampler "timer enabled but not running (fix: buildbox bootstrap)"
  elif [ ! -s "$summary" ]; then
    bad telemetry-sampler "timer running but no summary published (fix: buildbox bootstrap)"
  elif [ "$age" -gt 90 ]; then
    bad telemetry-sampler "summary ${age}s old, controller drops it past 90s (fix: journalctl --user -u buildbox-telemetry.service)"
  else
    ok telemetry-sampler "${have:0:12} timer active, summary ${age}s old"
  fi
}

# Tools whose code lives in its own repo. devtools.json declares the ref; the box holds a
# checkout, never a hand-copied binary. A build stamp records which ref the artifacts in the
# tree were produced from, so a ref bump rebuilds and a re-run does not.
item_devtools() {
  if [ -z "${DEVTOOLS_B64:-}" ]; then
    bad devtools "declaration not supplied (run via bin/buildbox, not by hand)"
    return
  fi
  local spec; spec=$(printf %s "$DEVTOOLS_B64" | base64 -d) || { bad devtools "declaration undecodable"; return; }

  local name repo ref dest install bins
  while IFS=$'\t' read -r name repo ref dest install bins; do
    [ -n "$name" ] || continue
    dest="${dest/#\~/$HOME}"
    local head stamp
    head=$(git -C "$dest" rev-parse HEAD 2>/dev/null || true)
    if [ "$head" != "$ref" ]; then
      if [ "$MODE" != bootstrap ]; then
        bad "$name" "${head:-absent} want ${ref:0:12} (fix: buildbox bootstrap)"
        continue
      fi
      if [ -z "$head" ]; then
        rm -rf "$dest"
        mkdir -p "$(dirname "$dest")"
        git clone --quiet "$repo" "$dest" 2>/dev/null || { bad "$name" "clone failed: $repo"; continue; }
      else
        git -C "$dest" fetch --quiet origin 2>/dev/null || { bad "$name" "fetch failed: $repo"; continue; }
      fi
      git -C "$dest" checkout --quiet --detach "$ref" 2>/dev/null || { bad "$name" "ref not on origin: ${ref:0:12}"; continue; }
    fi

    stamp=$(cat "$dest/.buildbox-built" 2>/dev/null || true)
    if [ -n "$install" ] && [ "$stamp" != "$ref" ]; then
      if [ "$MODE" != bootstrap ]; then
        bad "$name" "${ref:0:12} checked out but built from ${stamp:-nothing} (fix: buildbox bootstrap)"
        continue
      fi
      if ( cd "$dest" && eval "$install" ) >/dev/null 2>&1; then
        printf %s "$ref" > "$dest/.buildbox-built"
      else
        bad "$name" "install failed: $install"
        continue
      fi
    fi

    local missing="" b target
    for b in ${bins//,/ }; do
      target="$dest/${b#*=}"; b="${b%%=*}"
      if [ ! -x "$target" ]; then missing="$missing $b"; continue; fi
      if [ "$(readlink "$HOME/.local/bin/$b")" != "$target" ]; then
        if [ "$MODE" = bootstrap ]; then
          mkdir -p "$HOME/.local/bin" && ln -sfn "$target" "$HOME/.local/bin/$b"
        else
          missing="$missing $b"
        fi
      fi
    done
    if [ -n "$missing" ]; then bad "$name" "bins not linked:$missing (fix: buildbox bootstrap)"; continue; fi
    ok "$name" "${ref:0:12}"
  done < <(printf %s "$spec" | python3 -c '
import json,sys
d=json.load(sys.stdin)
root=d.get("root","~/.dev-tools")
for t in d["tools"]:
    dest = t.get("dest") or (root + "/" + t["name"])
    bins=",".join(f"{k}={v}" for k,v in (t.get("bins") or {}).items())
    print("\t".join([t["name"],t["repo"],t["ref"],dest,t.get("install",""),bins]))
')
}

# Tools shipped as a vendor release, not a source repo: there is nothing to clone or build,
# only a pinned artifact to place. The version the box actually runs is the assertion — a
# link left pointing at an older release answers `command -v` and reports the old version.
item_vendor() {
  if [ -z "${DEVTOOLS_B64:-}" ]; then
    bad vendor "declaration not supplied (run via bin/buildbox, not by hand)"
    return
  fi

  local name kind version version_cmd url tool dest bins
  while IFS=$'\t' read -r name kind version version_cmd url tool dest bins; do
    [ -n "$name" ] || continue
    [ "$url" = - ] && url=""
    [ "$tool" = - ] && tool=""
    [ "$dest" = - ] && dest=""
    dest="${dest/#\~/$HOME}"

    if ! vendor_at_version "$version_cmd" "$version"; then
      if [ "$MODE" != bootstrap ]; then
        bad "$name" "want $version, have $(vendor_version "$version_cmd") (fix: buildbox bootstrap)"
        continue
      fi
      case "$kind" in
        tarball) vendor_fetch_tarball "$name" "$url" "$dest" || continue ;;
        mise)
          command -v mise >/dev/null 2>&1 || { bad "$name" "mise missing"; continue; }
          mise use -g "$tool@$version" >/dev/null 2>&1 || { bad "$name" "mise use -g $tool@$version failed"; continue; }
          ;;
        *) bad "$name" "unknown vendor kind: $kind"; continue ;;
      esac
    fi

    local missing="" b target
    for b in ${bins//,/ }; do
      target="${b#*=}"; b="${b%%=*}"
      target="${target//\{dest\}/$dest}"; target="${target/#\~/$HOME}"
      if [ ! -x "$target" ]; then missing="$missing $b"; continue; fi
      if [ "$(readlink "$HOME/.local/bin/$b")" != "$target" ]; then
        if [ "$MODE" = bootstrap ]; then
          mkdir -p "$HOME/.local/bin" && ln -sfn "$target" "$HOME/.local/bin/$b"
        else
          missing="$missing $b"
        fi
      fi
    done
    if [ -n "$missing" ]; then bad "$name" "bins not linked:$missing (fix: buildbox bootstrap)"; continue; fi

    if vendor_at_version "$version_cmd" "$version"; then ok "$name" "$version"
    else bad "$name" "installed but reports $(vendor_version "$version_cmd"), want $version"; fi
  done < <(printf %s "$DEVTOOLS_B64" | base64 -d | python3 -c '
import json,sys
for v in json.load(sys.stdin).get("vendor") or []:
    # tab is IFS-whitespace, so an empty field would collapse and shift every field after
    # it; "-" holds the column and the reader turns it back into empty.
    f = lambda s: (s or "").replace("{version}", v["version"]) or "-"
    bins=",".join(f"{k}={f(p)}" for k,p in (v.get("bins") or {}).items())
    print("\t".join([v["name"],v["kind"],v["version"],v["version_cmd"],
                     f(v.get("url")),f(v.get("tool")),f(v.get("dest")),bins]))
')
}

vendor_version() { ( eval "$1" ) 2>/dev/null | tr -d '\r' | head -1 || true; }

vendor_at_version() { case "$(vendor_version "$1")" in *"$2"*) [ -n "$2" ];; *) false;; esac; }

vendor_fetch_tarball() { # $1=name $2=url $3=dest -> extract atomically, never over a live dir
  local name="$1" url="$2" dest="$3" tmp
  tmp="$(dirname "$dest")/.tmp-$(basename "$dest").$$"
  mkdir -p "$(dirname "$dest")" && rm -rf "$tmp" && mkdir -p "$tmp" || { bad "$name" "cannot stage $tmp"; return 1; }
  if ! curl -fsSL "$url" | tar --strip-components=1 -xzf - -C "$tmp"; then
    rm -rf "$tmp"; bad "$name" "download failed: $url"; return 1
  fi
  rm -rf "$dest" && mv "$tmp" "$dest" || { rm -rf "$tmp"; bad "$name" "cannot install $dest"; return 1; }
}

# A box that carries an artifact nothing declares is not identical to its siblings, and the
# artifact answers `command -v` while resolving to a path that exists on no box.
item_stale_artifacts() {
  if [ -z "${DEVTOOLS_B64:-}" ]; then
    bad stale-artifacts "declaration not supplied (run via bin/buildbox, not by hand)"
    return
  fi
  local found="" p
  while read -r p; do
    [ -n "$p" ] || continue
    p="${p/#\~/$HOME}"
    [ -e "$p" ] || continue
    if [ "$MODE" = bootstrap ] && rm -rf "$p"; then fixed stale-artifacts "removed $p"; else found="$found $p"; fi
  done < <(printf %s "$DEVTOOLS_B64" | base64 -d | python3 -c '
import json,sys
[print(p) for p in json.load(sys.stdin).get("forbidden",{}).get("paths",[])]
')
  [ -n "$found" ] && bad stale-artifacts "undeclared:$found (fix: buildbox bootstrap)" || ok stale-artifacts none
}

SCRATCH_MNT=/var/lib/buildbox

# root-installed by host-config/apply.sh (buildbox harden), report-only here.
# A bind that never mounted leaves the target as an ordinary directory on root, which
# reads as healthy from every tool that only looks at the path — so the check is on the
# device the target lives on, not on the path.
item_scratch_disk() {
  local src fstype rota size avail scratch_dev root_dev sub rel target
  if ! findmnt -no TARGET --mountpoint "$SCRATCH_MNT" >/dev/null 2>&1; then
    bad scratch-disk "$SCRATCH_MNT not mounted (root: buildbox harden)"
    return
  fi
  src=$(findmnt -no SOURCE --mountpoint "$SCRATCH_MNT")
  fstype=$(findmnt -no FSTYPE --mountpoint "$SCRATCH_MNT")
  rota=$(lsblk -no ROTA "$src" 2>/dev/null | awk 'NR == 1 { print $1 }')
  size=$(df -BG --output=size "$SCRATCH_MNT" | tail -1 | tr -dc '0-9')
  avail=$(df -BG --output=avail "$SCRATCH_MNT" | tail -1 | tr -dc '0-9')
  ok scratch-disk "$fstype on $SCRATCH_MNT size=${size}G free=${avail}G rotational=$([ "$rota" = 0 ] && echo no || echo yes)"

  scratch_dev=$(stat -c %d "$SCRATCH_MNT")
  root_dev=$(stat -c %d /)
  while read -r sub rel; do
    target="$HOME/$rel"
    if [ "$(stat -c %d "$target" 2>/dev/null)" = "$scratch_dev" ]; then
      ok scratch-bind "$rel"
    elif [ "$(stat -c %d "$target" 2>/dev/null)" = "$root_dev" ]; then
      bad scratch-bind "$rel is on the root filesystem — $SCRATCH_MNT/$sub never bound (root: buildbox harden)"
    else
      bad scratch-bind "$rel missing (root: buildbox harden)"
    fi
  done <<'BINDS'
builds builds
pnpm-store .local/share/pnpm
npm .npm
cargo-registry .cargo/registry
dev-tools .dev-tools
BINDS
}

# root-installed, report-only: never mutated from here
# The danger lab: every file the box must carry, plus the reaper that collects clones
# whose owner died. Root-owned slice units are reported, never written, like the rest of
# host-config.
item_dangerlab() {
  if [ "${EXPECT_LAB_HOST:-0}" != 1 ]; then
    ok dangerlab "not the lab host"
    return
  fi
  if [ -z "${EXPECT_LAB_MANIFEST:-}" ]; then
    bad dangerlab "manifest not supplied (run via bin/buildbox, not by hand)"
    return
  fi
  local path want have drift_here=0 root_hint
  while IFS=$'\t' read -r path want; do
    [ -n "$path" ] || continue
    eval "path=$path"
    have=$(sha256sum "$path" 2>/dev/null | cut -d' ' -f1)
    case "$path" in /etc/*) root_hint=" (root: buildbox harden)";; *) root_hint=" (fix: buildbox bootstrap)";; esac
    if [ -z "$have" ]; then
      bad dangerlab "missing $path$root_hint"; drift_here=1
    elif [ "$have" != "$want" ]; then
      bad dangerlab "stale $path: box ${have:0:12} want ${want:0:12}$root_hint"; drift_here=1
    fi
  done <<<"$(printf %s "$EXPECT_LAB_MANIFEST" | base64 -d)"

  if [ "$(systemctl --user is-enabled dangerlab-reaper.timer 2>/dev/null)" != enabled ]; then
    bad dangerlab "reaper timer not enabled — a clone whose owner dies would leak (fix: buildbox bootstrap)"
    drift_here=1
  fi
  if ! sudo -n sha256sum -c "$HOME/dangerlab/base.qcow2.sha256" >/dev/null 2>&1; then
    bad dangerlab "frozen template does not match its sha256 pin (fix: dangerlab/provision.sh)"
    drift_here=1
  fi
  [ "$drift_here" = 0 ] && ok dangerlab "tooling current, reaper armed, template pinned"
}

item_system() {
  command -v git >/dev/null 2>&1 && ok git "$(git --version | cut -d' ' -f3)" || bad git "missing (root: apt install git)"
  command -v rsync >/dev/null 2>&1 && ok rsync "$(rsync --version | awk 'NR==1{print $3}')" || bad rsync "missing (root: apt install rsync)"
  command -v flock >/dev/null 2>&1 && ok flock present || bad flock "missing (root: apt install util-linux)"
  command -v git-filter-repo >/dev/null 2>&1 && ok filter-repo present || bad filter-repo "missing (root: apt install git-filter-repo)"
}

item_mise
item_node
item_node_system
item_runner_env
item_corepack_shims
item_bun
item_rust
item_builds_dir
item_linger
item_npmrc_token
item_failsafe
item_sshd_port
item_recovery_doors
item_journal_cap
item_host_config
item_gc_script
item_gitleaks
item_gitleaks_system
item_git_identity
item_gh_auth
item_agent_confine
item_claude_home
item_claude_hooks
item_claude_behavior
item_scratch_prune
item_build_cache_prune
item_telemetry_sampler
item_devtools
item_vendor
item_stale_artifacts
item_scratch_disk
item_dangerlab
item_system

exit "$drift"
