test/box_lib.sh
Ref: Size: 12.6 KiB History
# shellcheck shell=sh
# test/box_lib.sh — the boxes the macOS gates take by name, sourced never run.
#
# `make mac` and `make xos` used to spell one developer's machines in their
# own source: a Mac called squirtle, and a Linux VM at a literal LAN
# address. Nobody else could run either gate, and neither gate could say
# what it was allowed to do to the machines it found — one scrubbed a box to
# bare metal, the other tiptoed around a laptop it refused to touch. Both
# gates take their boxes as ssh targets in the environment now, and the
# arrangements they share live here rather than in three copies.
#
# LINUX_BOX an x86_64 Linux VM this box can ssh to with key auth. The
# gate installs a static-musl mux at ~/.local/bin/mux there.
# MAC_BOX an Apple-silicon macOS box this box can ssh to with key
# auth, with NO toolchain on it — normally a bridged tart
# guest from test/provision-mac.sh. It only RUNS what the
# builder built, which is the stronger claim: a bare Mac and
# not the machine that compiled the binary.
# MAC_BUILDER a Mac holding a checkout with zig and deps/mac-sdk in it.
# It BUILDS and does nothing else, so no gate installs a mux
# there and none scrubs it. It defaults to $MAC_BOX, which is
# right only when the one Mac is both.
#
# LINUX_BOX and MAC_BOX are VMs the gate MAY scrub, and box_scrub is the
# whole of what that means. Neither name has a default: a gate that guessed
# a box would eventually guess somebody's laptop.
#
# Sourced after test/os_oracle.sh, whose now_ms and file/pid helpers these
# functions leave alone.
# The word a refusal opens with, so a failure line reads as its own gate's.
BOX_GATE=${BOX_GATE:-box}
# The refusals in here are the shared ones, and they end the run: a box that
# will not take a key or will not reset is not a box the gate can grade.
box_die() {
echo "$BOX_GATE FAIL: $*"
exit 1
}
# BatchMode so a broken auth fails in seconds rather than prompting a
# harness nobody sits in front of.
BOX_SSH_BASE="-o BatchMode=yes -o ConnectTimeout=10"
# The one target whose host key is a fresh clone's, and so is a different key
# every run: a MAC_BOX that test/provision-mac.sh just made. A gate's own ssh
# to that box therefore keeps no host key at all — recording it would fill
# known_hosts with keys no later run ever sees again, and checking it would
# refuse every run after the first. LogLevel=ERROR because a throwaway
# known-hosts file makes ssh announce the new key on stderr every single
# time, and that line would be most of what a green run prints.
#
# mux's OWN ssh is a different matter: the entry dial spawns a plain
# `ssh HOST` that reads the real ~/.ssh/known_hosts and cannot be handed
# these options, which is why box_preflight forgets and re-learns that box's
# key there. A gate that dials MAC_BOX with mux sets this and does that.
BOX_EPHEMERAL=${BOX_EPHEMERAL:-}
# The ssh options for one target: loose for the ephemeral box above,
# accept-new for every other, because the first run against a real box
# should record its host key where plain ssh will find it.
box_opts_for() {
if [ -n "$BOX_EPHEMERAL" ] && [ "$1" = "$BOX_EPHEMERAL" ]; then
printf '%s' "$BOX_SSH_BASE -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
else
printf '%s' "$BOX_SSH_BASE -o StrictHostKeyChecking=accept-new"
fi
}
# box_ssh SECONDS TARGET — run the script on stdin on TARGET, under /bin/sh,
# with $PRELUDE in front of it.
#
# /bin/sh and not the box's login shell (zsh on a Mac): `$VAR` holding
# several words does not word-split in zsh, so an options string pasted into
# a command line there arrives as ONE argument and ssh answers "keyword
# batchmode extra arguments at end of line".
#
# The rule for a script handed to this: anything INSIDE it that reads stdin
# gets it closed — `-n` for an ssh, `</dev/null` for the rest. The remote sh
# is reading its own script off stdin, and a command that drains stdin
# swallows the lines after it, silently and with a 0 exit, so they simply
# never run. `mux d endpoint` is one of those: it announces on stdout and
# then PROXIES, and it ate the two assertions after it the first time the
# cross-OS gate ran its entry-dial leg.
#
# The quoting rule is that there is NO quoting: the script rides this
# shell's stdin all the way to the remote `sh -s` and no shell in between
# parses it as a command line. The one thing a caller owes it is a heredoc;
# called with no redirect it would read the enclosing script's stdin.
box_ssh() {
_bx_t=$1
_bx_target=$2
_bx_opts=$(box_opts_for "$_bx_target")
# shellcheck disable=SC2086 # the options are words on purpose
{ printf '%s\n' "${PRELUDE:-}"; cat; } |
timeout "$_bx_t" ssh $_bx_opts "$_bx_target" /bin/sh -s
}
# box_scp TARGET LOCAL REMOTE — one local file onto TARGET, at a path
# relative to that box's $HOME unless it starts with a slash.
box_scp() {
_bc_target=$1
_bc_opts=$(box_opts_for "$_bc_target")
# shellcheck disable=SC2086 # the options are words on purpose
scp -q $_bc_opts "$2" "$_bc_target:$3"
}
# box_stream FROM SRCPATH TO DESTPATH — one file from one box to another,
# through this one, mode 755 at the far end.
#
# A `cat` and not an scp, because the two boxes need no key of each other's
# for this: the builder holds a binary and the box under test must run it,
# and the only machine that can reach both is the one driving the gate.
# `cat` carries no mode, so the chmod is explicit — and a caller who cares
# that the bytes arrived intact compares the two hashes rather than trusting
# this.
box_stream() {
_bm_from=$1
_bm_src=$2
_bm_to=$3
_bm_dest=$4
_bm_fopts=$(box_opts_for "$_bm_from")
_bm_topts=$(box_opts_for "$_bm_to")
# shellcheck disable=SC2086 # both option strings are words on purpose
ssh -n $_bm_fopts "$_bm_from" "cat $_bm_src" |
ssh $_bm_topts "$_bm_to" "cat > $_bm_dest.part && chmod 755 $_bm_dest.part && mv $_bm_dest.part $_bm_dest"
}
# box_preflight NAME TARGET WANT_ARCH [HINT] — the box answers, and it is the
# machine this gate is for. Runs for every box BEFORE the gate writes
# anything anywhere, so a run that cannot finish has not started either.
#
# `-n` is load-bearing. `timeout` runs its child in a background process
# group and an ssh with no -n reads its stdin to forward it; run from a
# terminal that is a background read of the tty, so the kernel stops ssh
# with SIGTTIN and the SIGTERM at 20 s cannot land on a stopped process. The
# gate sat for 17 minutes on `uname -m` before this was fixed (2026-09-04).
#
# `uname -m` on macOS prints `arm64` and zig's word for that machine is
# `aarch64`, the same one-way normalisation src/cli/main.zig's archMatches
# does for `mux d upgrade`'s preflight. Callers therefore spell WANT_ARCH
# the zig way for both machines.
box_preflight() {
_pf_name=$1
_pf_target=$2
_pf_want=$3
_pf_hint=${4:-}
if [ -z "$_pf_target" ]; then
echo "$BOX_GATE FAIL: $_pf_name is unset, and this gate has no default for it."
echo " A box named by hand is a box somebody chose; a guessed one is"
echo " eventually somebody's laptop."
[ -n "$_pf_hint" ] && printf ' %s\n' "$_pf_hint"
exit 2
fi
_pf_opts=$(box_opts_for "$_pf_target")
# shellcheck disable=SC2086 # the options are words on purpose
_pf_arch=$(timeout 20 ssh -n $_pf_opts "$_pf_target" 'uname -m' 2>"${BOX_ERRFILE:-/tmp/box_preflight.err}") || {
echo "$BOX_GATE FAIL: cannot ssh to $_pf_name ($_pf_target). It said:"
sed 's/^/ /' "${BOX_ERRFILE:-/tmp/box_preflight.err}" 2>/dev/null || true
[ -n "$_pf_hint" ] && printf ' %s\n' "$_pf_hint"
exit 2
}
[ "$_pf_arch" = "arm64" ] && _pf_arch=aarch64
[ "$_pf_arch" = "$_pf_want" ] || {
echo "$BOX_GATE FAIL: $_pf_name ($_pf_target) is $_pf_arch; this gate needs $_pf_want there."
exit 2
}
echo "preflight $_pf_name $_pf_target $_pf_arch"
}
# box_pair A B — A can ssh to B unattended, and can still after a re-run.
#
# Both directions of the cross-OS gate dial the other box FROM inside a box:
# a macOS client's entry dial spawns ssh at the Linux daemon, and a `mux d
# upgrade` in the other direction spawns ssh back. Neither box has ever seen
# the other on a first run, and a tart guest has never seen anything at all.
#
# Three steps, each idempotent: A gets a key if it has none, B accepts that
# key, and then A actually dials B — which both proves the pairing and
# records B's host key on A, where the plain ssh mux spawns will read it.
# Writing another machine's authorized_keys is in scope precisely because
# both boxes are VMs the gate may scrub.
box_pair() {
_pr_a=$1
_pr_b=$2
_pr_pub=$(box_ssh 60 "$_pr_a" <<'A' | grep '^ssh-' | head -1
set -eu
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
[ -f "$HOME/.ssh/id_ed25519" ] ||
ssh-keygen -q -t ed25519 -N "" -f "$HOME/.ssh/id_ed25519" </dev/null
cat "$HOME/.ssh/id_ed25519.pub"
A
)
[ -n "$_pr_pub" ] ||
box_die "pair: $_pr_a would not hand over an ssh public key"
box_ssh 60 "$_pr_b" >/dev/null <<B || box_die "pair: $_pr_b would not take $_pr_a's key"
set -eu
mkdir -p "\$HOME/.ssh"
chmod 700 "\$HOME/.ssh"
touch "\$HOME/.ssh/authorized_keys"
chmod 600 "\$HOME/.ssh/authorized_keys"
grep -qxF '$_pr_pub' "\$HOME/.ssh/authorized_keys" ||
printf '%s\n' '$_pr_pub' >> "\$HOME/.ssh/authorized_keys"
B
box_ssh 60 "$_pr_a" >/dev/null <<B || box_die "pair: $_pr_a still cannot ssh to $_pr_b with its own key"
ssh -n -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new $_pr_b true
B
}
# box_scrub TARGET — the mux-less box a new user has, on a box the gate is
# allowed to scrub. Run at the START of a run and again at the END, so a
# failed run leaves nothing behind and the run after it starts from the same
# place whatever happened.
#
# It writes NO shell rc file, which is a deliberate reversal of what the
# earlier reset did. Every remote spelling the PRODUCT sends already carries
# `PATH="$PATH:$HOME/.local/bin"` in front of it — that is
# `handoff.local_bin_append`, on the entry dial's word, on the upgrade
# preflight and on the upgrade's push — so a `~/.bashrc` or `~/.zshenv` line
# put there by the harness tests nothing the product needs. Worse, it is the
# fixture configuring the machine: a bare box that finds mux only because
# the gate edited its shell rc is exactly the configured developer machine a
# pristine guest exists to rule out. The gate's OWN remote scripts therefore
# spell `$HOME/.local/bin/mux` in full, or carry a PATH in the prelude.
#
# The one arrangement that is (re)made is linger, and only on Linux: without
# it logind tears down /run/user/<uid> — the daemon's socket directory —
# when the ssh session that started the daemon ends, and reaps the detached
# daemon with it. It is separated from the rest by its own exit code, so a
# box without passwordless sudo is told what is missing rather than being
# handed "the reset was refused".
#
# Both gates' scratch directory names are removed here, not just the calling
# gate's: a box may have run either, and neither should outlive a run.
box_scrub() {
_sc_rc=0
box_ssh 120 "$1" >/dev/null <<'S' || _sc_rc=$?
# Best-effort down to the mkdir: a box with no mux on it answers "no such
# process" to half of these, and that is the state this is aiming for.
set -u
for _m in "$HOME/.local/bin/mux" "$HOME/xos/mux" "$HOME/mux-e2e/mux"; do
[ -x "$_m" ] && "$_m" d stop >/dev/null 2>&1
done
sleep 0.3
pkill -x mux >/dev/null 2>&1
sleep 0.3
pkill -9 -x mux >/dev/null 2>&1
rm -rf "$HOME/.local/bin/mux" "$HOME/.local/state/mux" "$HOME/.cache/mux" \
"$HOME/.config/mux" "$HOME/xos" "$HOME/mux-e2e"
[ -n "${XDG_RUNTIME_DIR:-}" ] && rm -rf "$XDG_RUNTIME_DIR"/mux*
case "$(uname)" in
Darwin) rm -rf "/tmp/mux-$(id -u)" ;;
*) rm -rf "/run/user/$(id -u)"/mux* ;;
esac
# The install legs copy into this, so its absence is the run's problem.
mkdir -p "$HOME/.local/bin" || exit 1
# Its own code, because "no passwordless sudo on this box" is a different
# thing for a reader to fix than "the reset was refused".
case "$(uname)" in
Linux) sudo -n loginctl enable-linger "$(id -un)" >/dev/null 2>&1 || exit 3 ;;
esac
exit 0
S
case "$_sc_rc" in
0) ;;
3) box_die "scrub: $1 refused \`sudo -n loginctl enable-linger\`. Without linger,
logind takes /run/user/<uid> down with the ssh session that started
the daemon. Give that user passwordless sudo for loginctl." ;;
*) box_die "scrub: $1 refused the reset (rc $_sc_rc)" ;;
esac
}