a73x

9e1ab77e

test: one library for the boxes the macOS gates share

a73x   2026-09-04 12:50

Commit message
test: one library for the boxes the macOS gates share

`make mac` and `make xos` had three copies of the script-on-stdin ssh
runner between them, two spellings of a preflight, and two opposite
answers to "what may a gate do to a machine it finds": one scrubbed a
Linux box to bare metal, the other tiptoed around a Mac it refused to
touch. test/box_lib.sh is the one place that answers all of it.

The boxes are named, never guessed — MAC_BOX, LINUX_BOX, MAC_BUILDER —
and box_preflight refuses with rc 2 on a name that is unset, before
anything is written anywhere. A gate with a default for a box eventually
guesses somebody's laptop.

box_pair is the piece that had no copy to be a third of: both directions
of the cross-OS gate dial the other box from INSIDE a box, and a fresh
guest has never seen anything at all. It installs the key, then makes A
actually dial B, which proves the pairing and records B's host key where
the plain ssh mux spawns will read it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

test/box_lib.sh
Old New
@@ -0,0 +1,272 @@
1 # shellcheck shell=sh
2 # test/box_lib.sh — the boxes the macOS gates take by name, sourced never run.
3 #
4 # `make mac` and `make xos` used to spell one developer's machines in their
5 # own source: a Mac called squirtle, and a Linux VM at a literal LAN
6 # address. Nobody else could run either gate, and neither gate could say
7 # what it was allowed to do to the machines it found — one scrubbed a box to
8 # bare metal, the other tiptoed around a laptop it refused to touch. Both
9 # gates take their boxes as ssh targets in the environment now, and the
10 # arrangements they share live here rather than in three copies.
11 #
12 # LINUX_BOX an x86_64 Linux VM this box can ssh to with key auth. The
13 # gate installs a static-musl mux at ~/.local/bin/mux there.
14 # MAC_BOX an Apple-silicon macOS box this box can ssh to with key
15 # auth, with NO toolchain on it — normally a bridged tart
16 # guest from test/provision-mac.sh. It only RUNS what the
17 # builder built, which is the stronger claim: a bare Mac and
18 # not the machine that compiled the binary.
19 # MAC_BUILDER a Mac holding a checkout with zig and deps/mac-sdk in it.
20 # It BUILDS and does nothing else, so no gate installs a mux
21 # there and none scrubs it. It defaults to $MAC_BOX, which is
22 # right only when the one Mac is both.
23 #
24 # LINUX_BOX and MAC_BOX are VMs the gate MAY scrub, and box_scrub is the
25 # whole of what that means. Neither name has a default: a gate that guessed
26 # a box would eventually guess somebody's laptop.
27 #
28 # Sourced after test/os_oracle.sh, whose now_ms and file/pid helpers these
29 # functions leave alone.
30
31 # The word a refusal opens with, so a failure line reads as its own gate's.
32 BOX_GATE=${BOX_GATE:-box}
33
34 # The refusals in here are the shared ones, and they end the run: a box that
35 # will not take a key or will not reset is not a box the gate can grade.
36 box_die() {
37 echo "$BOX_GATE FAIL: $*"
38 exit 1
39 }
40
41 # BatchMode so a broken auth fails in seconds rather than prompting a
42 # harness nobody sits in front of.
43 BOX_SSH_BASE="-o BatchMode=yes -o ConnectTimeout=10"
44
45 # The one target whose host key is a fresh clone's, and so is a different key
46 # every run: a MAC_BOX that test/provision-mac.sh just made. A gate's own ssh
47 # to that box therefore keeps no host key at all — recording it would fill
48 # known_hosts with keys no later run ever sees again, and checking it would
49 # refuse every run after the first. LogLevel=ERROR because a throwaway
50 # known-hosts file makes ssh announce the new key on stderr every single
51 # time, and that line would be most of what a green run prints.
52 #
53 # mux's OWN ssh is a different matter: the entry dial spawns a plain
54 # `ssh HOST` that reads the real ~/.ssh/known_hosts and cannot be handed
55 # these options, which is why box_preflight forgets and re-learns that box's
56 # key there. A gate that dials MAC_BOX with mux sets this and does that.
57 BOX_EPHEMERAL=${BOX_EPHEMERAL:-}
58
59 # The ssh options for one target: loose for the ephemeral box above,
60 # accept-new for every other, because the first run against a real box
61 # should record its host key where plain ssh will find it.
62 box_opts_for() {
63 if [ -n "$BOX_EPHEMERAL" ] && [ "$1" = "$BOX_EPHEMERAL" ]; then
64 printf '%s' "$BOX_SSH_BASE -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
65 else
66 printf '%s' "$BOX_SSH_BASE -o StrictHostKeyChecking=accept-new"
67 fi
68 }
69
70 # box_ssh SECONDS TARGET — run the script on stdin on TARGET, under /bin/sh,
71 # with $PRELUDE in front of it.
72 #
73 # /bin/sh and not the box's login shell (zsh on a Mac): `$VAR` holding
74 # several words does not word-split in zsh, so an options string pasted into
75 # a command line there arrives as ONE argument and ssh answers "keyword
76 # batchmode extra arguments at end of line".
77 #
78 # The rule for a script handed to this: anything INSIDE it that reads stdin
79 # gets it closed — `-n` for an ssh, `</dev/null` for the rest. The remote sh
80 # is reading its own script off stdin, and a command that drains stdin
81 # swallows the lines after it, silently and with a 0 exit, so they simply
82 # never run. `mux d endpoint` is one of those: it announces on stdout and
83 # then PROXIES, and it ate the two assertions after it the first time the
84 # cross-OS gate ran its entry-dial leg.
85 #
86 # The quoting rule is that there is NO quoting: the script rides this
87 # shell's stdin all the way to the remote `sh -s` and no shell in between
88 # parses it as a command line. The one thing a caller owes it is a heredoc;
89 # called with no redirect it would read the enclosing script's stdin.
90 box_ssh() {
91 _bx_t=$1
92 _bx_target=$2
93 _bx_opts=$(box_opts_for "$_bx_target")
94 # shellcheck disable=SC2086 # the options are words on purpose
95 { printf '%s\n' "${PRELUDE:-}"; cat; } |
96 timeout "$_bx_t" ssh $_bx_opts "$_bx_target" /bin/sh -s
97 }
98
99 # box_scp TARGET LOCAL REMOTE — one local file onto TARGET, at a path
100 # relative to that box's $HOME unless it starts with a slash.
101 box_scp() {
102 _bc_target=$1
103 _bc_opts=$(box_opts_for "$_bc_target")
104 # shellcheck disable=SC2086 # the options are words on purpose
105 scp -q $_bc_opts "$2" "$_bc_target:$3"
106 }
107
108 # box_stream FROM SRCPATH TO DESTPATH — one file from one box to another,
109 # through this one, mode 755 at the far end.
110 #
111 # A `cat` and not an scp, because the two boxes need no key of each other's
112 # for this: the builder holds a binary and the box under test must run it,
113 # and the only machine that can reach both is the one driving the gate.
114 # `cat` carries no mode, so the chmod is explicit — and a caller who cares
115 # that the bytes arrived intact compares the two hashes rather than trusting
116 # this.
117 box_stream() {
118 _bm_from=$1
119 _bm_src=$2
120 _bm_to=$3
121 _bm_dest=$4
122 _bm_fopts=$(box_opts_for "$_bm_from")
123 _bm_topts=$(box_opts_for "$_bm_to")
124 # shellcheck disable=SC2086 # both option strings are words on purpose
125 ssh -n $_bm_fopts "$_bm_from" "cat $_bm_src" |
126 ssh $_bm_topts "$_bm_to" "cat > $_bm_dest.part && chmod 755 $_bm_dest.part && mv $_bm_dest.part $_bm_dest"
127 }
128
129 # box_preflight NAME TARGET WANT_ARCH [HINT] — the box answers, and it is the
130 # machine this gate is for. Runs for every box BEFORE the gate writes
131 # anything anywhere, so a run that cannot finish has not started either.
132 #
133 # `-n` is load-bearing. `timeout` runs its child in a background process
134 # group and an ssh with no -n reads its stdin to forward it; run from a
135 # terminal that is a background read of the tty, so the kernel stops ssh
136 # with SIGTTIN and the SIGTERM at 20 s cannot land on a stopped process. The
137 # gate sat for 17 minutes on `uname -m` before this was fixed (2026-09-04).
138 #
139 # `uname -m` on macOS prints `arm64` and zig's word for that machine is
140 # `aarch64`, the same one-way normalisation src/cli/main.zig's archMatches
141 # does for `mux d upgrade`'s preflight. Callers therefore spell WANT_ARCH
142 # the zig way for both machines.
143 box_preflight() {
144 _pf_name=$1
145 _pf_target=$2
146 _pf_want=$3
147 _pf_hint=${4:-}
148 if [ -z "$_pf_target" ]; then
149 echo "$BOX_GATE FAIL: $_pf_name is unset, and this gate has no default for it."
150 echo " A box named by hand is a box somebody chose; a guessed one is"
151 echo " eventually somebody's laptop."
152 [ -n "$_pf_hint" ] && printf ' %s\n' "$_pf_hint"
153 exit 2
154 fi
155 _pf_opts=$(box_opts_for "$_pf_target")
156 # shellcheck disable=SC2086 # the options are words on purpose
157 _pf_arch=$(timeout 20 ssh -n $_pf_opts "$_pf_target" 'uname -m' 2>"${BOX_ERRFILE:-/tmp/box_preflight.err}") || {
158 echo "$BOX_GATE FAIL: cannot ssh to $_pf_name ($_pf_target). It said:"
159 sed 's/^/ /' "${BOX_ERRFILE:-/tmp/box_preflight.err}" 2>/dev/null || true
160 [ -n "$_pf_hint" ] && printf ' %s\n' "$_pf_hint"
161 exit 2
162 }
163 [ "$_pf_arch" = "arm64" ] && _pf_arch=aarch64
164 [ "$_pf_arch" = "$_pf_want" ] || {
165 echo "$BOX_GATE FAIL: $_pf_name ($_pf_target) is $_pf_arch; this gate needs $_pf_want there."
166 exit 2
167 }
168 echo "preflight $_pf_name $_pf_target $_pf_arch"
169 }
170
171 # box_pair A B — A can ssh to B unattended, and can still after a re-run.
172 #
173 # Both directions of the cross-OS gate dial the other box FROM inside a box:
174 # a macOS client's entry dial spawns ssh at the Linux daemon, and a `mux d
175 # upgrade` in the other direction spawns ssh back. Neither box has ever seen
176 # the other on a first run, and a tart guest has never seen anything at all.
177 #
178 # Three steps, each idempotent: A gets a key if it has none, B accepts that
179 # key, and then A actually dials B — which both proves the pairing and
180 # records B's host key on A, where the plain ssh mux spawns will read it.
181 # Writing another machine's authorized_keys is in scope precisely because
182 # both boxes are VMs the gate may scrub.
183 box_pair() {
184 _pr_a=$1
185 _pr_b=$2
186 _pr_pub=$(box_ssh 60 "$_pr_a" <<'A' | grep '^ssh-' | head -1
187 set -eu
188 mkdir -p "$HOME/.ssh"
189 chmod 700 "$HOME/.ssh"
190 [ -f "$HOME/.ssh/id_ed25519" ] ||
191 ssh-keygen -q -t ed25519 -N "" -f "$HOME/.ssh/id_ed25519" </dev/null
192 cat "$HOME/.ssh/id_ed25519.pub"
193 A
194 )
195 [ -n "$_pr_pub" ] ||
196 box_die "pair: $_pr_a would not hand over an ssh public key"
197 box_ssh 60 "$_pr_b" >/dev/null <<B || box_die "pair: $_pr_b would not take $_pr_a's key"
198 set -eu
199 mkdir -p "\$HOME/.ssh"
200 chmod 700 "\$HOME/.ssh"
201 touch "\$HOME/.ssh/authorized_keys"
202 chmod 600 "\$HOME/.ssh/authorized_keys"
203 grep -qxF '$_pr_pub' "\$HOME/.ssh/authorized_keys" ||
204 printf '%s\n' '$_pr_pub' >> "\$HOME/.ssh/authorized_keys"
205 B
206 box_ssh 60 "$_pr_a" >/dev/null <<B || box_die "pair: $_pr_a still cannot ssh to $_pr_b with its own key"
207 ssh -n -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new $_pr_b true
208 B
209 }
210
211 # box_scrub TARGET — the mux-less box a new user has, on a box the gate is
212 # allowed to scrub. Run at the START of a run and again at the END, so a
213 # failed run leaves nothing behind and the run after it starts from the same
214 # place whatever happened.
215 #
216 # It writes NO shell rc file, which is a deliberate reversal of what the
217 # earlier reset did. Every remote spelling the PRODUCT sends already carries
218 # `PATH="$PATH:$HOME/.local/bin"` in front of it — that is
219 # `handoff.local_bin_append`, on the entry dial's word, on the upgrade
220 # preflight and on the upgrade's push — so a `~/.bashrc` or `~/.zshenv` line
221 # put there by the harness tests nothing the product needs. Worse, it is the
222 # fixture configuring the machine: a bare box that finds mux only because
223 # the gate edited its shell rc is exactly the configured developer machine a
224 # pristine guest exists to rule out. The gate's OWN remote scripts therefore
225 # spell `$HOME/.local/bin/mux` in full, or carry a PATH in the prelude.
226 #
227 # The one arrangement that is (re)made is linger, and only on Linux: without
228 # it logind tears down /run/user/<uid> — the daemon's socket directory —
229 # when the ssh session that started the daemon ends, and reaps the detached
230 # daemon with it. It is separated from the rest by its own exit code, so a
231 # box without passwordless sudo is told what is missing rather than being
232 # handed "the reset was refused".
233 #
234 # Both gates' scratch directory names are removed here, not just the calling
235 # gate's: a box may have run either, and neither should outlive a run.
236 box_scrub() {
237 _sc_rc=0
238 box_ssh 120 "$1" >/dev/null <<'S' || _sc_rc=$?
239 # Best-effort down to the mkdir: a box with no mux on it answers "no such
240 # process" to half of these, and that is the state this is aiming for.
241 set -u
242 for _m in "$HOME/.local/bin/mux" "$HOME/xos/mux" "$HOME/mux-e2e/mux"; do
243 [ -x "$_m" ] && "$_m" d stop >/dev/null 2>&1
244 done
245 sleep 0.3
246 pkill -x mux >/dev/null 2>&1
247 sleep 0.3
248 pkill -9 -x mux >/dev/null 2>&1
249 rm -rf "$HOME/.local/bin/mux" "$HOME/.local/state/mux" "$HOME/.cache/mux" \
250 "$HOME/.config/mux" "$HOME/xos" "$HOME/mux-e2e"
251 [ -n "${XDG_RUNTIME_DIR:-}" ] && rm -rf "$XDG_RUNTIME_DIR"/mux*
252 case "$(uname)" in
253 Darwin) rm -rf "/tmp/mux-$(id -u)" ;;
254 *) rm -rf "/run/user/$(id -u)"/mux* ;;
255 esac
256 # The install legs copy into this, so its absence is the run's problem.
257 mkdir -p "$HOME/.local/bin" || exit 1
258 # Its own code, because "no passwordless sudo on this box" is a different
259 # thing for a reader to fix than "the reset was refused".
260 case "$(uname)" in
261 Linux) sudo -n loginctl enable-linger "$(id -un)" >/dev/null 2>&1 || exit 3 ;;
262 esac
263 exit 0
264 S
265 case "$_sc_rc" in
266 0) ;;
267 3) box_die "scrub: $1 refused \`sudo -n loginctl enable-linger\`. Without linger,
268 logind takes /run/user/<uid> down with the ssh session that started
269 the daemon. Give that user passwordless sudo for loginctl." ;;
270 *) box_die "scrub: $1 refused the reset (rc $_sc_rc)" ;;
271 esac
272 }