#!/bin/sh
# runit service: the in-VM bridge (terminal protocol + /agent-key, and it spawns
# the harness via agent-shell). It reads /proc/cmdline itself for
# tribes.bridge_token / tribes.agent_env, so it needs no env threading — it
# inherits HOME/PATH from /sbin/init (via tini → runsvdir). Foreground exec, as
# runit requires. A bridge crash is respawned in ~1s; ssh stays an independent
# path in the meantime, and /root/workspace is on disk.
#
# CRASH-GATE for the in-place upgrade path (scripts/sandbox-upgrade-stage.sh):
# runit re-execs this script ~1s after ANY microvmd exit, so a swapped-in bundle
# that fails to start (corrupt/incompatible) would crash-loop bun FOREVER and
# leave the box bridge-dark. Guard it — mirror the caddy/run .good/.rejected
# revert. The "upgrade unconfirmed" flag is the PERSISTENT file microvmd.js.prev
# on the rootfs (NOT a /run marker — /run is tmpfs, so a reboot mid-upgrade would
# drop the marker and DISARM the gate). The respawn counter lives in /run
# (per-boot, self-clearing — exactly right). If the new bundle crash-loops (>=3
# respawns within a 120s window while .prev exists) revert to .prev, set the bad
# bundle aside as .rejected, and clear the counter — the box is back on the last
# working bridge in ~5s, no host round-trip. sandboxd finalizes a confirmed-good
# upgrade by removing .prev (host-driven), which disarms the gate. The OFFLINE heal
# (sandboxd MicrovmdHeal) arms the same .prev, so a healed box is revertible too.
BIN=/usr/local/bin
PREV="$BIN/microvmd.js.prev"
COUNTER=/run/microvmd-respawns
SVC_PID=/etc/sandbox-service/microvmd/supervise/pid

# Readiness watchdog (invoked by the gate below as `run __watchdog &`). A bundle that
# STARTS but never serves /healthz (a bind failure / hang) produces NO exit, so the
# respawn counter never advances and the crash-gate never fires — a silent brick.
# While an upgrade is unconfirmed (.prev present), this kills microvmd if it isn't
# serving within the grace window, feeding the counter so a hung upgrade still reverts.
# NEVER-LOCK-OUT: it MUST NOT kill a confirmed-good daemon — a slow/saturated guest
# could miss one probe, and after finalize removed .prev the respawn would come up with
# an empty in-memory owner env (silent owner lock-out). So: (1) if finalize already
# removed .prev, DISARM (never kill); (2) retry the probe a few times before killing.
# A separate subcommand (not an inline subshell) so it's unit-testable in isolation.
if [ "${1:-}" = "__watchdog" ]; then
  sleep "${MICROVMD_WATCHDOG_GRACE_S:-20}"
  # Finalize (host-confirmed upgrade) may have removed .prev meanwhile → disarm.
  [ -s "$PREV" ] || exit 0
  i=0
  tries="${MICROVMD_WATCHDOG_TRIES:-3}"
  while [ "$i" -lt "$tries" ]; do
    if curl -sf -m 3 http://127.0.0.1:7681/healthz >/dev/null 2>&1; then
      exit 0
    fi
    # Re-check the disarm between retries — a confirm can land mid-probe.
    [ -s "$PREV" ] || exit 0
    i=$((i + 1))
    [ "$i" -lt "$tries" ] && sleep "${MICROVMD_WATCHDOG_RETRY_S:-2}"
  done
  # Still not serving after every retry and the gate is still armed → kill so the
  # respawn advances the counter toward a revert.
  kill "$(cat "$SVC_PID" 2>/dev/null)" 2>/dev/null
  exit 0
fi

if [ -s "$PREV" ]; then
  now=$(date +%s)
  # An orphaned .prev (a host confirmation that never landed) must not arm the
  # gate forever — a much-later, unrelated triple-crash would false-revert to an
  # ancient bundle. Disarm after 24h; a genuinely bad upgrade reverts in ~5s.
  prev_age=$((now - $(stat -c %Y "$PREV" 2>/dev/null || echo "$now")))
  if [ "$prev_age" -gt 86400 ]; then
    rm -f "$PREV" "$PREV.sha256" "$COUNTER"
  else
    count=0
    last=0
    if [ -f "$COUNTER" ]; then
      read count last <"$COUNTER" 2>/dev/null || {
        count=0
        last=0
      }
    fi
    # Reset the window if the last respawn was long ago: a bundle that ran fine
    # for minutes then crashed once is a normal runtime crash (runit just
    # respawns it), not a broken upgrade — it must not accumulate toward a revert.
    [ $((now - last)) -gt 120 ] && count=0
    count=$((count + 1))
    echo "$count $now" >"$COUNTER"
    if [ "$count" -ge 3 ]; then
      # Revert: restore the last-good bundle, set the bad one aside as evidence,
      # clear the counter. Guard on a non-empty .prev (checked above) AND, when the
      # stage script stamped one, a matching sha256 — never install a bundle that
      # was truncated mid-copy (e.g. ENOSPC). If .prev is corrupt, DISARM instead of
      # installing garbage: the (bad) new bundle keeps crash-looping, which is no
      # worse than a bricked revert and leaves .rejected evidence for a human.
      if [ ! -f "$PREV.sha256" ] || (cd "$BIN" && sha256sum -c microvmd.js.prev.sha256 >/dev/null 2>&1); then
        mv -f "$BIN/microvmd.js" "$BIN/microvmd.js.rejected"
        mv -f "$PREV" "$BIN/microvmd.js"
        rm -f "$PREV.sha256" "$COUNTER"
        # The stage script stamped the NEW version marker post-swap; after a revert
        # the box runs the OLD bundle, so drop the marker's claim (the live push
        # re-derives the true version from /healthz).
        rm -f "$BIN/microvmd.version"
      else
        rm -f "$PREV" "$PREV.sha256" "$COUNTER"
      fi
    else
      # Arm the readiness watchdog (see the __watchdog subcommand above). Backgrounded
      # before exec, stdio detached so it never holds the runit log pipe; runsv reaps
      # it. `sh "$0"` re-execs THIS script's watchdog branch regardless of exec bit.
      sh "$0" __watchdog </dev/null >/dev/null 2>&1 &
    fi
  fi
fi

exec /usr/local/bin/bun /usr/local/bin/microvmd.js
