a73x

test/xos.sh

Ref:   Size: 38.9 KiB   History

#!/bin/sh
# test/xos.sh — the `make xos` gate: the wire, across the OS seam.
#
# `make vm` grades mux on Linux and `make mac` grades it on macOS, and both
# run one operating system on BOTH ends of every connection. This gate is
# the one that does not: a macOS client attaches to a Linux daemon and a
# Linux client attaches to a macOS daemon, over the ssh handoff and over
# QUIC, and a cross-architecture `mux d upgrade` is refused in both
# directions before a byte of the image moves.
#
# Four machines, taken by name (test/box_lib.sh), and each one has a job:
#
#   THIS BOX      Linux x86_64, the orchestrator and the Linux CLIENT. It
#                 drives the Linux→Mac legs with this tree's own `mux` and
#                 `ptyclient`, and it builds the static-musl image the Linux
#                 daemon box installs.
#   $MAC_BUILDER  a Mac with the toolchain and the shadow SDK. It BUILDS the
#                 arm64 image and does nothing else: no mux is installed
#                 there and nothing is scrubbed there.
#   $MAC_BOX      an Apple-silicon macOS box with no toolchain on it, which
#                 is where that image is RUN. Normally a bridged tart guest
#                 from test/provision-mac.sh.
#   $LINUX_BOX    an x86_64 Linux VM both this box and $MAC_BOX can reach.
#                 The e2e VM is on this box's private network and a Mac
#                 cannot see it, which is the whole reason a second Linux
#                 box exists.
#
# MAC_BOX and LINUX_BOX are VMs the gate MAY scrub, and box_scrub is the
# whole of what that means; it runs at the start of the run and again on the
# way out. The boxes themselves are a human action, never harness code
# (test/vm.sh's rule): the Linux VM is made once with eitri, and the Mac
# guest by `eval "$(test/provision-mac.sh)"`. What the harness owns is the
# scrub, the pairing, the install and the key.
#
# Every claim about a box is asserted by a SEPARATE ssh reading that box
# through test/os_oracle.sh — never by trusting the connection under test,
# which is the connection this gate exists to doubt ("ask the OS about the
# OS", CLAUDE.md).
set -eu

BOX_GATE=xos

MUSL_MUX="$1"   # x86_64-linux-musl ReleaseSafe: the image installed on $LINUX_BOX
MUX="$2"        # this tree's own build: the Linux client for the Linux→Mac legs
PTYCLIENT="$3"  # the real-pty fixture, driving a client the way hands do

MAC_BOX=${MAC_BOX:-}
LINUX_BOX=${LINUX_BOX:-}
# The one Mac that is both is the common case, so the builder follows the
# box under test unless it is named.
MAC_BUILDER=${MAC_BUILDER:-$MAC_BOX}
# The host halves are what the QUIC legs dial: QUIC has no ssh config to
# read, so a MAC_BOX or LINUX_BOX spelled with a name rather than an address
# must be a name this box resolves.
MAC_IP=${MAC_BOX#*@}
LAN_IP=${LINUX_BOX#*@}
# Relative to the BUILDER's $HOME, because that is how ssh's scp-like remote
# spelling (`HOST:code/rad/mux`) resolves a path.
REPO="${MAC_BUILDER_REPO:-code/rad/mux}"
BRANCH=mac-journey
QUIC_PORT=4433
# The port as /proc/net/udp writes a wildcard bind, which is the spelling
# os_oracle.sh's udp_local_bound takes on BOTH arms — the Darwin arm converts
# it, so a leg never learns which OS it is asking. Derived rather than written
# out, because a hand-typed hex goes stale the moment somebody edits the port
# above it; leg 7 spells the same derivation for the port a Mac announces.
QUIC_HEX=$(printf '00000000:%04X' "$QUIC_PORT")
WANT_MAC_ARCH=aarch64
WANT_LAN_ARCH=x86_64

# now_ms for the leg clock, and sha256_of for the installed-image hashes.
# The pid_*, file_* and udp_* names are sourced by the REMOTE legs from the
# copy beside the binary on each box, which is what keeps a leg from having to
# know which OS is answering it.
. "$(dirname "$0")/os_oracle.sh"
. "$(dirname "$0")/box_lib.sh"

# A provisioned Mac guest has a new host key every clone, so the harness's
# own ssh to it keeps none. mux's ssh is the exception handled below.
BOX_EPHEMERAL=$MAC_BOX

OK_COUNT=0
LEG_T0=0

leg() { LEG_T0=$(now_ms); }

ok() {
    OK_COUNT=$((OK_COUNT + 1))
    echo "$1 ok [$(( $(now_ms) - LEG_T0 ))ms]${2:+ ($2)}"
}

fail() {
    echo "xos FAIL: $1: $2"
    exit 1
}

# What every mux the HARNESS runs on a remote box runs under.
#
# XDG_STATE_HOME is isolated because every mux that attaches records its
# daemon in the hosts file under it (CLAUDE.md), and this gate must not
# write a throwaway host onto somebody's real wall. XDG_CACHE_HOME is
# isolated for the handoff's per-host endpoint cache and XDG_CONFIG_HOME so
# that no default ~/.config/mux/key can be picked up by accident — the key
# this run uses is named explicitly on every command line.
#
# XDG_RUNTIME_DIR is left exactly as each box's ssh login leaves it: set on
# Ubuntu (/run/user/1000) and UNSET on macOS, where the daemon falls back to
# /tmp/mux-<uid>. That fallback is what the Linux→Mac legs exercise, and the
# entry dial's own `mux d endpoint --start` will look nowhere else.
#
# The builder gets the same prelude, where its only effect is a scratch
# directory the cleanup removes: the build legs cd into the repo themselves,
# and no mux ever runs there.
PRELUDE='
export XDG_STATE_HOME=$HOME/xos/state
export XDG_CACHE_HOME=$HOME/xos/cache
export XDG_CONFIG_HOME=$HOME/xos/config
mkdir -p $HOME/xos
cd $HOME
'

# ---- preflight: every box answers, before anything is written ----------
box_preflight MAC_BOX "$MAC_BOX" "$WANT_MAC_ARCH" \
    'Provision one with: eval "$(test/provision-mac.sh)"'
box_preflight LINUX_BOX "$LINUX_BOX" "$WANT_LAN_ARCH" \
    'An x86_64 Linux VM this box and MAC_BOX can both ssh to.'
box_preflight MAC_BUILDER "$MAC_BUILDER" "$WANT_MAC_ARCH" \
    'A Mac holding this repo with zig and deps/mac-sdk. Defaults to $MAC_BOX.'

# The entry dial in leg 7 spawns a PLAIN `ssh $MAC_BOX` — mux's own, with
# mux's own options — and that one reads the real ~/.ssh/known_hosts. A
# freshly provisioned guest has a host key nobody has seen, at an address a
# previous guest may have held under a different one, so the stale entry is
# forgotten and the new key learned once, here, where a failure is a
# preflight and not a leg timing out with nothing to say.
ssh-keygen -R "$MAC_IP" >/dev/null 2>&1 || true
timeout 20 ssh -n -o BatchMode=yes -o ConnectTimeout=10 \
    -o StrictHostKeyChecking=accept-new "$MAC_BOX" true ||
    fail preflight "cannot record $MAC_BOX's host key for the plain ssh mux spawns"

# Both directions of this gate dial the other box from INSIDE a box: the
# macOS client's entry dial spawns ssh at the Linux daemon, and leg 9's
# `mux d upgrade` spawns ssh back the other way.
box_pair "$MAC_BOX" "$LINUX_BOX"
box_pair "$LINUX_BOX" "$MAC_BOX"

# ---- the local side is a hermetic laptop ------------------------------
# Every XDG dir mux reads, isolated. Short root because a unix socket path
# caps at 107 bytes.
SCRATCH=$(mktemp -d /tmp/muxxos.XXXXXX)
export XDG_STATE_HOME="$SCRATCH/st" XDG_RUNTIME_DIR="$SCRATCH/rt"
export XDG_CONFIG_HOME="$SCRATCH/cf" XDG_CACHE_HOME="$SCRATCH/ca"
mkdir -p "$XDG_STATE_HOME" "$XDG_RUNTIME_DIR" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME"

# Both boxes back to the mux-less state a new user has, on every exit. The
# builder is not scrubbed — it is not this gate's machine to reset — so its
# scratch directory is all there is to take back there.
cleanup() {
    _rc=$?
    box_scrub "$LINUX_BOX" >/dev/null 2>&1 || true
    box_scrub "$MAC_BOX" >/dev/null 2>&1 || true
    box_ssh 60 "$MAC_BUILDER" >/dev/null 2>&1 <<'B' || true
rm -rf "$HOME/xos"
B
    rm -rf "$SCRATCH"
    exit "$_rc"
}
trap cleanup EXIT

# ---- leg 1: push — the builder builds THIS commit -----------------------
leg
HEAD_SHA=$(git rev-parse HEAD)
PUSHED=no
HAVE=$(box_ssh 60 "$MAC_BUILDER" <<H || true
cd \$HOME/$REPO 2>/dev/null || exit 0
git rev-parse -q --verify refs/heads/$BRANCH 2>/dev/null || true
H
)
if [ "$HAVE" != "$HEAD_SHA" ]; then
    # A push cannot update the branch the remote has checked out, and the
    # run before this one left it checked out. Detaching first is what makes
    # a re-run work; it changes no commit and no file.
    box_ssh 60 "$MAC_BUILDER" >/dev/null <<H || fail push "cannot detach $MAC_BUILDER:$REPO before the push"
set -e
cd \$HOME/$REPO
if [ "\$(git rev-parse --abbrev-ref HEAD)" = "$BRANCH" ]; then
    git checkout -q --detach
fi
H
    # --force because this branch is a scratch mirror of whatever this tree
    # is and never shared history: a local amend or rebase leaves the
    # builder's ref on a commit the new HEAD does not descend from, and a
    # plain push would be rejected as a non-fast-forward for the rest of the
    # branch's life.
    git push --force "$MAC_BUILDER:$REPO" "HEAD:refs/heads/$BRANCH" >/dev/null 2>&1 ||
        fail push "git push $MAC_BUILDER:$REPO HEAD:refs/heads/$BRANCH was refused"
    PUSHED=yes
fi
box_ssh 60 "$MAC_BUILDER" >/dev/null <<H || fail push "the builder could not check out $BRANCH"
set -e
cd \$HOME/$REPO
git checkout -q $BRANCH
git reset -q --hard $BRANCH
H
BUILDER_SHA_GIT=$(box_ssh 60 "$MAC_BUILDER" <<H
cd \$HOME/$REPO && git rev-parse HEAD
H
)
[ "$BUILDER_SHA_GIT" = "$HEAD_SHA" ] ||
    fail push "the builder is at $BUILDER_SHA_GIT, this tree is at $HEAD_SHA"
ok push "$MAC_BUILDER:$REPO on $BRANCH at $(git rev-parse --short HEAD), pushed=$PUSHED"

# ---- leg 2: build — one image per architecture -------------------------
# The builder builds its own arm64 binary and leg 3 puts it on the box that
# has no toolchain. The shim PATH is required for the link and Homebrew is
# not on a non-interactive PATH, so both are spelled here.
leg
# XDG_CACHE_HOME back to the builder's own for this one: zig's GLOBAL package
# cache lives under it, and building into a fresh one re-fetches ghostty and
# the rest of the package set on every run — minutes, for isolation this leg
# does not need. What the prelude isolates is mux's own cache directory, and
# no mux runs here.
BOUT=$(box_ssh 2400 "$MAC_BUILDER" <<H
cd \$HOME/$REPO || { echo "RC_BUILD=90"; exit 0; }
unset XDG_CACHE_HOME
export PATH=\$HOME/$REPO/deps/mac-sdk/bin:/opt/homebrew/bin:\$PATH
make build >/tmp/xos-build.log 2>&1; echo "RC_BUILD=\$?"
H
)
BRC=$(printf '%s\n' "$BOUT" | sed -n 's/^RC_BUILD=//p')
[ "$BRC" = "0" ] || {
    # The verdict first and the reading under it, test/vm.sh's shape: a
    # reader scrolling back finds the sentence that says what broke, not the
    # tail of somebody else's build log.
    echo "xos FAIL: build: make build on $MAC_BUILDER exited ${BRC:-<no answer>}"
    box_ssh 60 "$MAC_BUILDER" <<'H' | sed 's/^/          /' || true
tail -30 /tmp/xos-build.log 2>/dev/null || true
H
    exit 1; }
BINFO=$(box_ssh 120 "$MAC_BUILDER" <<H
set -e
cd \$HOME/$REPO
. ./test/os_oracle.sh
echo "ver=\$(./zig-out/bin/mux --version)"
echo "sha=\$(sha256_of ./zig-out/bin/mux)"
H
)
BUILT_VER=$(printf '%s\n' "$BINFO" | sed -n 's/^ver=//p')
MAC_BIN_SHA=$(printf '%s\n' "$BINFO" | sed -n 's/^sha=//p')
WANT_VER=$("$MUX" --version)
[ -n "$BUILT_VER" ] && [ "$BUILT_VER" = "$WANT_VER" ] ||
    fail build "$MAC_BUILDER built '$BUILT_VER', this tree built '$WANT_VER'"
[ -n "$MAC_BIN_SHA" ] || fail build "$MAC_BUILDER would not hash the binary it built"
ok build "$MAC_BUILDER built an $WANT_MAC_ARCH '$BUILT_VER'"

# ---- leg 3: reset — two mux-less boxes, each given one image -----------
# The scrub is the hermeticity: each run starts from the mux-less box a new
# user has, and box_scrub is the one place that says what that means. It
# writes no shell rc file on either box — the product carries its own PATH
# prefix on every remote word it sends — so what the legs below meet is a
# box this gate has configured in no way at all.
leg
box_scrub "$LINUX_BOX"
box_scrub "$MAC_BOX"
box_ssh 60 "$LINUX_BOX" >/dev/null <<'L' || fail reset "could not make the scratch directories on $LINUX_BOX"
set -e
mkdir -p "$HOME/.local/bin" "$HOME/xos"
L
box_ssh 60 "$MAC_BOX" >/dev/null <<'M' || fail reset "could not make the scratch directories on $MAC_BOX"
set -e
mkdir -p "$HOME/.local/bin" "$HOME/xos"
M
# The oracle goes beside the binary on each box rather than being read out of
# a repo, so both boxes answer through the same path and a leg never has to
# know which one it is talking to.
box_scp "$LINUX_BOX" "$MUSL_MUX" .local/bin/mux ||
    fail reset "could not copy the musl build to $LINUX_BOX"
box_scp "$LINUX_BOX" "$(dirname "$0")/os_oracle.sh" xos/os_oracle.sh ||
    fail reset "could not copy the oracle to $LINUX_BOX"
box_scp "$MAC_BOX" "$(dirname "$0")/os_oracle.sh" xos/os_oracle.sh ||
    fail reset "could not copy the oracle to $MAC_BOX"
# Streamed through this box rather than copied between the two Macs: neither
# needs a key of the other's for that, and only this box can reach both.
box_stream "$MAC_BUILDER" "\$HOME/$REPO/zig-out/bin/mux" "$MAC_BOX" "\$HOME/.local/bin/mux" ||
    fail reset "could not stream the arm64 mux from $MAC_BUILDER to $MAC_BOX"
box_stream "$MAC_BUILDER" "\$HOME/$REPO/zig-out/bin/ptyclient" "$MAC_BOX" "\$HOME/xos/ptyclient" ||
    fail reset "could not stream ptyclient from $MAC_BUILDER to $MAC_BOX"

LAN_BIN_SHA=$(sha256_of "$MUSL_MUX")
LINFO=$(box_ssh 60 "$LINUX_BOX" <<'L'
. ~/xos/os_oracle.sh
echo "ver=$(~/.local/bin/mux --version)"
echo "sha=$(sha256_of ~/.local/bin/mux)"
echo "exe=$(real_path ~/.local/bin/mux)"
echo "rtd=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
L
)
LAN_VER=$(printf '%s\n' "$LINFO" | sed -n 's/^ver=//p')
LAN_GOT_SHA=$(printf '%s\n' "$LINFO" | sed -n 's/^sha=//p')
LAN_EXE=$(printf '%s\n' "$LINFO" | sed -n 's/^exe=//p')
LAN_RTD=$(printf '%s\n' "$LINFO" | sed -n 's/^rtd=//p')
[ -n "$LAN_EXE" ] || fail reset "$LINUX_BOX would not resolve the path of its installed mux"
[ "$LAN_VER" = "$WANT_VER" ] ||
    fail reset "$LINUX_BOX answers '$LAN_VER', this tree built '$WANT_VER'"
[ "$LAN_GOT_SHA" = "$LAN_BIN_SHA" ] ||
    fail reset "$LINUX_BOX holds a mux hashing $LAN_GOT_SHA, the pushed image is $LAN_BIN_SHA"
LAN_SOCK="$LAN_RTD/muxd.sock"

MINFO=$(box_ssh 60 "$MAC_BOX" <<'M'
. $HOME/xos/os_oracle.sh
echo "uid=$(id -u)"
echo "ver=$($HOME/.local/bin/mux --version)"
echo "sha=$(sha256_of $HOME/.local/bin/mux)"
echo "exe=$(real_path $HOME/.local/bin/mux)"
M
)
MAC_UID=$(printf '%s\n' "$MINFO" | sed -n 's/^uid=//p')
MAC_VER=$(printf '%s\n' "$MINFO" | sed -n 's/^ver=//p')
MAC_GOT_SHA=$(printf '%s\n' "$MINFO" | sed -n 's/^sha=//p')
MAC_EXE=$(printf '%s\n' "$MINFO" | sed -n 's/^exe=//p')
[ -n "$MAC_UID" ] || fail reset "$MAC_BOX would not say its uid"
[ -n "$MAC_EXE" ] || fail reset "$MAC_BOX would not resolve the path of its installed mux"
[ "$MAC_VER" = "$WANT_VER" ] ||
    fail reset "$MAC_BOX answers '$MAC_VER', this tree built '$WANT_VER'"
# The bytes that RUN are the bytes the builder produced: `cat` down one pipe
# and up another is a stream nothing else checks.
[ "$MAC_GOT_SHA" = "$MAC_BIN_SHA" ] ||
    fail reset "$MAC_BOX holds a mux hashing $MAC_GOT_SHA, the builder's is $MAC_BIN_SHA"
MAC_SOCK="/tmp/mux-$MAC_UID/muxd.sock"

# What an ssh handoff will find, asked in the handoff's own words. sshd runs
# a NON-LOGIN shell, so nothing on either box puts ~/.local/bin on that PATH
# and the product does not expect it to: every remote word mux sends is
# prefixed with `PATH="$PATH:$HOME/.local/bin"` — `handoff.local_bin_append`,
# carried by the entry dial, the upgrade preflight and the upgrade's push.
# The prefix below is that string character for character, so what this
# grades is the installed binary being findable the way mux finds it, and
# not a shell rc file the harness wrote. A bare `command -v mux` here would
# be asking a question the handoff never asks.
for _box in "$LINUX_BOX" "$MAC_BOX"; do
    _opts=$(box_opts_for "$_box")
    # shellcheck disable=SC2086,SC2016 # the options are words; $PATH is the REMOTE shell's
    _which=$(timeout 20 ssh -n $_opts "$_box" 'PATH="$PATH:$HOME/.local/bin"; command -v mux' 2>/dev/null) || _which=""
    case "$_which" in
        */.local/bin/mux) ;;
        *) fail reset "with the handoff's own PATH prefix, $_box finds mux at '${_which:-nothing}'; every remote dial would fail" ;;
    esac
done
ok reset "$LINUX_BOX and $MAC_BOX scrubbed and each given '$WANT_VER' on its PATH"

# ---- leg 4: keys — one PSK for the run, never the user's ---------------
# `mux d keygen` always writes the default path, so the isolated
# XDG_CONFIG_HOME above is what puts it in the scratch tree. It is copied to
# ~/xos/key on both boxes and named on every QUIC command line.
#
# On the Linux box the same bytes ALSO go to that box's default key path,
# and leg 5's daemon is started with no `--key` at all, which is how a
# person starts one after `mux d keygen`. That is not decoration. `mux d
# endpoint` resolves the key it ANNOUNCES from $MUX_KEY_FILE or the default
# path — it never asks the daemon which key its listener actually holds — so
# a daemon started with `--quic ... --key SOMEWHERE_ELSE` announces a key
# that does not authenticate against itself, creating the default file on
# the spot if it has to. Every ssh handoff to such a daemon then dies in
# silence and falls back to the pipe. Measured on this fixture 2026-09-04
# and reported; it is not a macOS difference, and the two spellings agree
# again the moment the listener's key IS the default one.
leg
"$MUX" d keygen >/dev/null 2>&1 || fail keys "mux d keygen refused under $XDG_CONFIG_HOME"
cp "$XDG_CONFIG_HOME/mux/key" "$SCRATCH/key"
chmod 600 "$SCRATCH/key"
[ "$(file_size "$SCRATCH/key")" = "32" ] ||
    fail keys "the generated key is $(file_size "$SCRATCH/key") bytes, want 32"
box_scp "$LINUX_BOX" "$SCRATCH/key" xos/key || fail keys "could not install the key on $LINUX_BOX"
box_scp "$MAC_BOX" "$SCRATCH/key" xos/key || fail keys "could not install the key on $MAC_BOX"
# BOTH default key paths on the Linux box, because two different
# environments resolve "the default key" there and they must not disagree:
# everything the harness runs sees the prelude's isolated XDG_CONFIG_HOME,
# while the `mux d endpoint` at the far end of MAC_BOX's entry dial is a
# fresh ssh with the box's own environment and resolves ~/.config/mux/key.
# Same bytes at both, so whichever one a process picks is this run's key.
box_ssh 30 "$LINUX_BOX" >/dev/null <<'L' || fail keys "could not install the key at the default paths on $LINUX_BOX"
set -e
for d in "$HOME/.config/mux" "${XDG_CONFIG_HOME:-$HOME/.config}/mux"; do
    mkdir -p "$d"
    chmod 700 "$d"
    cp "$HOME/xos/key" "$d/key"
    chmod 600 "$d/key"
done
L
# 0600 on both, because quic.Key.load refuses a group- or other-readable key
# exactly as ssh refuses a private key, and scp does not carry the mode.
KMODES=$(
    box_ssh 30 "$LINUX_BOX" <<'L'
. ~/xos/os_oracle.sh
chmod 600 ~/xos/key
echo "lan=$(file_mode ~/xos/key)"
L
    box_ssh 30 "$MAC_BOX" <<'M'
. $HOME/xos/os_oracle.sh
chmod 600 $HOME/xos/key
echo "mac=$(file_mode $HOME/xos/key)"
M
)
printf '%s\n' "$KMODES" | grep -qx 'lan=600' ||
    fail keys "the key on $LINUX_BOX is not mode 600: $KMODES"
printf '%s\n' "$KMODES" | grep -qx 'mac=600' ||
    fail keys "the key on $MAC_BOX is not mode 600: $KMODES"
ok keys "one 32-byte PSK at ~/xos/key, mode 600 on both boxes"

# ---- leg 5: mac-to-linux — a Mac client on a Linux daemon --------------
# The daemon carries a QUIC arm, so the Mac's entry dial is the whole
# product path: plain ssh to fetch the announce, then the session moves onto
# QUIC. MUX_SHELL_INTEGRATION=1 because leg 6 reads a real exit code off
# this same daemon and marks are opt-in.
leg
box_ssh 90 "$LINUX_BOX" >/dev/null <<L || fail mac-to-linux "mux d start -d --quic was refused on $LINUX_BOX"
MUX_SHELL_INTEGRATION=1 ~/.local/bin/mux d start -d --quic 0.0.0.0:$QUIC_PORT
L
# The listener's key and the key `mux d endpoint` announces, side by side.
# They are the same file here by construction (leg 4), and this reads them
# back rather than trusting that: an announce that names a key the listener
# does not hold is invisible from the client — the handshake simply never
# answers — and leg 5 would then be grading the ssh fallback while reporting
# QUIC.
#
# XDG_CONFIG_HOME is UNSET for this one read. The announce that matters is the
# one at the far end of the MAC's entry dial, which is a fresh ssh carrying
# this box's own environment and so resolves ~/.config/mux/key; asking under
# the prelude's isolated config dir would check a sibling file that happens to
# hold the same bytes and guard nothing.
ANN=$(box_ssh 30 "$LINUX_BOX" <<'L'
unset XDG_CONFIG_HOME
~/.local/bin/mux d endpoint </dev/null
L
)
case "$ANN" in
    "endpoint $QUIC_PORT $(od -An -tx1 -v < "$SCRATCH/key" | tr -d ' \n')") ;;
    *) fail mac-to-linux "$LINUX_BOX announces '$ANN', which is not this run's key at port $QUIC_PORT" ;;
esac
LPID=$(box_ssh 30 "$LINUX_BOX" <<'L'
pgrep -x mux | head -1
L
)
[ -n "$LPID" ] || fail mac-to-linux "no mux process on $LINUX_BOX after mux d start -d"
# The image this daemon is running, recorded before anything is pushed at it.
# Leg 9 reads it again after the refused push: on Linux an upgrade keeps the
# pid across the exec, so the pid alone cannot tell an untouched daemon from
# one that swallowed a new image, and the exe is what can.
LIS=$(box_ssh 30 "$LINUX_BOX" <<L
. ~/xos/os_oracle.sh
pid_exe $LPID
L
)
[ "$LIS" = "$LAN_EXE" ] ||
    fail mac-to-linux "daemon $LPID on $LINUX_BOX execs '$LIS', not the installed '$LAN_EXE'"
# Bound BEFORE the dial, asked of /proc and not of the daemon: a leg that
# read "no answer" from the Mac could otherwise be blaming the network for a
# listener that was never up.
box_ssh 60 "$LINUX_BOX" >/dev/null <<L || fail mac-to-linux "daemon $LPID binds no UDP $QUIC_PORT on $LINUX_BOX"
. ~/xos/os_oracle.sh
_i=0
while [ \$_i -lt 50 ]; do
    udp_local_bound $QUIC_HEX && exit 0
    sleep 0.2; _i=\$((_i + 1))
done
exit 1
L

# The marker carries the SESSION shell's own `uname -m` and its own \$\$, so
# `XOS-M2L-x86_64-` painted on a Mac's screen is the cross-OS fact itself:
# the bytes were produced by a shell only the Linux box's process table can
# account for.
M2L=$(box_ssh 240 "$MAC_BOX" <<M
cd \$HOME/xos
\$HOME/xos/ptyclient --cols 80 --rows 24 \
    --out \$HOME/xos/m2l.cap --err \$HOME/xos/m2l.err -- \
    \$HOME/.local/bin/mux $LINUX_BOX <<'PC'
expect \\x1b[?1049h 30000
settle 900 20000
send echo XOS-M2L-\$(uname -m)-\$\$\n
expect XOS-M2L-x86_64- 25000
settle 800 15000
send \\x1cd
waitexit 15000
PC
echo "PTYRC=\$?"
M
) || fail mac-to-linux "$MAC_BOX would not run ptyclient"
printf '%s\n' "$M2L" | grep -qx 'PTYRC=0' || {
    echo "xos FAIL: mac-to-linux: the Mac's client never painted XOS-M2L-x86_64-: $M2L"
    box_ssh 60 "$MAC_BOX" <<'M' | sed 's/^/          /' || true
cat $HOME/xos/m2l.err 2>/dev/null
M
    exit 1; }
M2L_READ=$(box_ssh 60 "$MAC_BOX" <<'M'
echo "pid=$(sed -n 's/.*XOS-M2L-x86_64-\([0-9][0-9]*\).*/\1/p' $HOME/xos/m2l.cap | head -1)"
grep -c 'attaching over ssh' $HOME/xos/m2l.err 2>/dev/null || true
M
)
M2L_PID=$(printf '%s\n' "$M2L_READ" | sed -n 's/^pid=//p')
[ -n "$M2L_PID" ] || fail mac-to-linux "the Mac's capture carries no shell pid: $M2L_READ"
# The handoff prints this one line and only when QUIC was tried and lost, so
# its ABSENCE is the pin that the session rode QUIC. The daemon's bound port
# above says the arm existed to move onto.
printf '%s\n' "$M2L_READ" | grep -qx '0' ||
    fail mac-to-linux "the Mac's dial fell back to the ssh pipe; it said: $M2L_READ"
box_ssh 60 "$LINUX_BOX" >/dev/null <<L || fail mac-to-linux "shell $M2L_PID is not a child of daemon $LPID on $LINUX_BOX"
. ~/xos/os_oracle.sh
pid_children $LPID | grep -qx $M2L_PID
L
# The detach was a goodbye and not a tear: what the Mac typed is still on
# the Linux box's grid.
box_ssh 60 "$LINUX_BOX" >/dev/null <<'L' || fail mac-to-linux "the session did not survive the Mac's detach"
~/.local/bin/mux d dump | grep -q XOS-M2L-x86_64-
L
ok mac-to-linux "a macOS client typed into shell $M2L_PID under daemon $LPID on $LINUX_BOX, over QUIC"

# ---- leg 6: mac-to-linux-quic — no ssh anywhere in the path ------------
# The Darwin wolfSSL build's PSK handshake against the musl build's, in one
# round trip, joining the session leg 5 made. `mux a` never creates.
#
# The exit code is read on the box and stderr kept there, because a `mux a`
# that dies by signal writes its JSON to neither stream and the shell that
# ran it is the only witness. One run on 2026-09-04 ended this leg with an
# empty stdout, nothing on stderr and a message that could not say what had
# exited how; every verb failure in muxa.zig prints `{"error":...}` before
# it exits, so an empty capture is a death and the code says which signal.
leg
AQ=$(box_ssh 120 "$MAC_BOX" <<M
MUX_KEY_FILE=\$HOME/xos/key \$HOME/.local/bin/mux a run --quic $LAN_IP:$QUIC_PORT \
    --key \$HOME/xos/key --timeout 20000 'echo m2lq-\$\$' 2>\$HOME/xos/aq.err
echo "AQRC=\$?"
M
) || fail mac-to-linux-quic "$MAC_BOX would not run mux a: $AQ"
AQ_RC=$(printf '%s\n' "$AQ" | sed -n 's/^AQRC=//p')
[ "$AQ_RC" = 0 ] || {
    echo "xos FAIL: mac-to-linux-quic: mux a run --quic exited ${AQ_RC:-with no code} on $MAC_BOX (past 128 is a signal: 141 SIGPIPE, 139 SIGSEGV) and said: $AQ"
    box_ssh 60 "$MAC_BOX" <<'M' | sed 's/^/          /' || true
cat $HOME/xos/aq.err 2>/dev/null
M
    exit 1; }
case "$AQ" in
    *'"mechanism":"marks"'*) ;;
    *) fail mac-to-linux-quic "no marks mechanism in the reply: $AQ" ;;
esac
case "$AQ" in
    *'"exit_code":0'*) ;;
    *) fail mac-to-linux-quic "no clean exit code in the reply: $AQ" ;;
esac
AQ_PID=$(printf '%s\n' "$AQ" | sed -n 's/.*m2lq-\([0-9][0-9]*\).*/\1/p')
[ -n "$AQ_PID" ] || fail mac-to-linux-quic "the reply carries no shell pid: $AQ"
box_ssh 60 "$LINUX_BOX" >/dev/null <<L || fail mac-to-linux-quic "shell $AQ_PID is not a child of daemon $LPID"
. ~/xos/os_oracle.sh
pid_children $LPID | grep -qx $AQ_PID
L
ok mac-to-linux-quic "mux a over QUIC ran shell $AQ_PID under $LPID, marks, exit 0"

# ---- leg 7: linux-to-mac — the entry dial starts the Mac's daemon ------
# `mux $MAC_BOX` from here is the ONLY thing in this gate allowed to start a
# daemon on the Mac: `mux d endpoint --start` is the asked dial's alone. The
# Mac's ssh login has no XDG_RUNTIME_DIR, so the daemon it starts binds the
# /tmp/mux-<uid> fallback — the Darwin path, exercised by a real `mux HOST`
# rather than by a test that sets a variable.
leg
set +e
timeout 120 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$SCRATCH/l2m.cap" --err "$SCRATCH/l2m.err" -- "$MUX" "$MAC_BOX" \
    > "$SCRATCH/l2m.log" 2>&1 <<'EOF'
expect \x1b[?1049h 30000
settle 900 20000
send echo XOS-L2M-$(uname -m)-$$\n
expect XOS-L2M-arm64- 25000
settle 800 15000
send \x1cd
waitexit 15000
EOF
RC=$?
set -e
[ "$RC" -eq 0 ] || {
    echo "xos FAIL: linux-to-mac: ptyclient exited $RC (did the dial or the remote start refuse?):"
    sed 's/^/          /' "$SCRATCH/l2m.log" 2>/dev/null || true
    sed 's/^/          /' "$SCRATCH/l2m.err" 2>/dev/null || true
    exit 1; }
L2M_PID=$(sed -n 's/.*XOS-L2M-arm64-\([0-9][0-9]*\).*/\1/p' "$SCRATCH/l2m.cap" | head -1)
[ -n "$L2M_PID" ] || fail linux-to-mac "the capture carries no shell pid"

MSTATE=$(box_ssh 90 "$MAC_BOX" <<M
. \$HOME/xos/os_oracle.sh
echo "pid=\$(pgrep -x mux | head -1)"
echo "n=\$(pgrep -x mux | wc -l | tr -d ' ')"
echo "announce=\$(\$HOME/.local/bin/mux d endpoint </dev/null 2>&1 | head -1)"
udp_local_bound $QUIC_HEX && echo "udp=bound" || echo "udp=none"
M
)
MPID=$(printf '%s\n' "$MSTATE" | sed -n 's/^pid=//p')
MNPROC=$(printf '%s\n' "$MSTATE" | sed -n 's/^n=//p')
MANN=$(printf '%s\n' "$MSTATE" | sed -n 's/^announce=//p')
[ -n "$MPID" ] || fail linux-to-mac "no mux on $MAC_BOX after the entry dial: $MSTATE"
[ "$MNPROC" = "1" ] ||
    fail linux-to-mac "$MAC_BOX runs $MNPROC mux processes after the detach, want 1 (the daemon): $MSTATE"
box_ssh 60 "$MAC_BOX" >/dev/null <<M || fail linux-to-mac "daemon $MPID does not hold $MAC_SOCK"
. \$HOME/xos/os_oracle.sh
pid_holds_unix_sock $MPID $MAC_SOCK
M
# Both sides canonical, and both through the oracle: pid_exe answers what
# lsof reports, which resolves every symlink on the way, so a $HOME reached
# through one would fail a correct exec unless the expected path is resolved
# the same way.
MIS=$(box_ssh 60 "$MAC_BOX" <<M
. \$HOME/xos/os_oracle.sh
pid_exe $MPID
M
)
[ -n "$MAC_EXE" ] && [ "$MIS" = "$MAC_EXE" ] ||
    fail linux-to-mac "daemon $MPID execs '$MIS', not the installed '$MAC_EXE'"
box_ssh 60 "$MAC_BOX" >/dev/null <<M || fail linux-to-mac "shell $L2M_PID is not a child of daemon $MPID"
. \$HOME/xos/os_oracle.sh
pid_children $MPID | grep -qx $L2M_PID
M
# WHICH transport carried it, read off the announce rather than assumed.
# The entry dial starts this daemon BARE — no --quic and no --key on any
# command line — and what the Mac does with that is the product's own
# behaviour, not this gate's to arrange: `mux d endpoint` creates the
# default key file when the box has none and the daemon then binds a QUIC
# listener lazily, on an EPHEMERAL port rather than 4433, so the announce
# names a port nothing here could have predicted. Whichever of the two
# answers comes back, the leg asserts the matching OS fact and reports what
# carried the session; what it will not do is grade one transport while
# reporting the other.
#
# An `if` and not `grep ... && fail`: an AND-OR list whose first command
# fails is the ORDINARY case here, and reading that as the leg's verdict is
# the wrong way round.
case "$MANN" in
    "endpoint none")
        # No arm to move onto, so the pipe that carried the announce is the
        # session. The daemon's own word is what says so — there is no
        # positive line on the client side, which only speaks up when QUIC
        # was tried and lost — and the OS fact beside it is that nothing is
        # bound at the port this gate would otherwise expect.
        printf '%s\n' "$MSTATE" | grep -qx 'udp=none' ||
            fail linux-to-mac "$MAC_BOX announces no endpoint yet binds UDP $QUIC_PORT: $MSTATE"
        if grep -q 'attaching over ssh' "$SCRATCH/l2m.err"; then
            fail linux-to-mac "the client fell back from a QUIC arm the daemon says it has not got"
        fi
        L2M_WIRE="ssh handoff (the daemon announced 'endpoint none')" ;;
    'endpoint '*)
        MPORT=${MANN#endpoint }
        MPORT=${MPORT%% *}
        case "$MPORT" in
            ''|*[!0-9]*) fail linux-to-mac "$MAC_BOX announces no readable port: '$MANN'" ;;
        esac
        # The announced port, as /proc/net/udp writes a wildcard bind — the
        # spelling both arms of the oracle take. Asked of lsof on the Mac,
        # so the claim "the daemon is listening there" is the OS's and not
        # the daemon's own.
        box_ssh 60 "$MAC_BOX" >/dev/null <<M || fail linux-to-mac "$MAC_BOX announces '$MANN' but binds no UDP $MPORT"
. \$HOME/xos/os_oracle.sh
udp_local_bound $(printf '00000000:%04X' "$MPORT")
M
        if grep -q 'attaching over ssh' "$SCRATCH/l2m.err"; then
            fail linux-to-mac "$MAC_BOX offered QUIC on $MPORT and the dial fell back to ssh"
        fi
        L2M_WIRE="QUIC on the lazily-bound port $MPORT" ;;
    *)
        fail linux-to-mac "$MAC_BOX's daemon answers no announce this leg can read: '$MANN'" ;;
esac
ok linux-to-mac "shell $L2M_PID under daemon $MPID on $MAC_SOCK, over $L2M_WIRE"

# ---- leg 8: linux-to-mac-quic — inbound UDP to a Mac daemon ------------
# The question this leg asks and no other does: does macOS deliver inbound
# UDP $QUIC_PORT to a daemon a NON-GUI ssh session started? If the
# application firewall eats it, the dial times out in silence and this leg
# fails with the client's own words — which is a fact about the platform,
# not a harness problem to loosen away.
leg
box_ssh 60 "$MAC_BOX" >/dev/null <<'M' || fail linux-to-mac-quic "mux d stop was refused on $MAC_BOX"
$HOME/.local/bin/mux d stop
M
box_ssh 60 "$MAC_BOX" >/dev/null <<M || fail linux-to-mac-quic "the bare daemon $MPID survives mux d stop by 3 s"
. \$HOME/xos/os_oracle.sh
_i=0
while [ \$_i -lt 30 ]; do
    pid_alive $MPID || exit 0
    sleep 0.1; _i=\$((_i + 1))
done
exit 1
M
box_ssh 90 "$MAC_BOX" >/dev/null <<M || fail linux-to-mac-quic "mux d start -d --quic was refused on $MAC_BOX"
MUX_SHELL_INTEGRATION=1 \$HOME/.local/bin/mux d start -d --quic 0.0.0.0:$QUIC_PORT --key \$HOME/xos/key
M
MPID2=$(box_ssh 30 "$MAC_BOX" <<'M'
pgrep -x mux | head -1
M
)
[ -n "$MPID2" ] || fail linux-to-mac-quic "no mux on $MAC_BOX after the QUIC start"
[ "$MPID2" != "$MPID" ] ||
    fail linux-to-mac-quic "$MAC_BOX still runs the pre-stop daemon $MPID; the restart did not happen"
box_ssh 60 "$MAC_BOX" >/dev/null <<M || fail linux-to-mac-quic "daemon $MPID2 binds no UDP $QUIC_PORT on $MAC_BOX"
. \$HOME/xos/os_oracle.sh
_i=0
while [ \$_i -lt 50 ]; do
    udp_local_bound $QUIC_HEX && exit 0
    sleep 0.2; _i=\$((_i + 1))
done
exit 1
M

# A real pty attach first, because `mux a` joins and never creates.
export MUX_KEY_FILE="$SCRATCH/key"
set +e
timeout 120 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$SCRATCH/l2mq.cap" --err "$SCRATCH/l2mq.err" -- \
    "$MUX" "quic://$MAC_IP:$QUIC_PORT" \
    > "$SCRATCH/l2mq.log" 2>&1 <<'EOF'
expect \x1b[?1049h 30000
settle 900 20000
send echo XOS-L2MQ-$(uname -m)-$$\n
expect XOS-L2MQ-arm64- 25000
settle 800 15000
send \x1cd
waitexit 15000
EOF
RC=$?
set -e
[ "$RC" -eq 0 ] || {
    echo "xos FAIL: linux-to-mac-quic: the QUIC attach to $MAC_IP:$QUIC_PORT exited $RC."
    echo "          This is the leg that asks whether macOS delivers inbound UDP"
    echo "          $QUIC_PORT to a daemon a non-GUI ssh session started. The dial said:"
    sed 's/^/          /' "$SCRATCH/l2mq.log" 2>/dev/null || true
    sed 's/^/          /' "$SCRATCH/l2mq.err" 2>/dev/null || true
    exit 1; }
L2MQ_PID=$(sed -n 's/.*XOS-L2MQ-arm64-\([0-9][0-9]*\).*/\1/p' "$SCRATCH/l2mq.cap" | head -1)
[ -n "$L2MQ_PID" ] || fail linux-to-mac-quic "the QUIC capture carries no shell pid"
box_ssh 60 "$MAC_BOX" >/dev/null <<M || fail linux-to-mac-quic "shell $L2MQ_PID is not a child of daemon $MPID2"
. \$HOME/xos/os_oracle.sh
pid_children $MPID2 | grep -qx $L2MQ_PID
M
AQ2=$(set +e; timeout 60 "$MUX" a run --quic "$MAC_IP:$QUIC_PORT" --key "$SCRATCH/key" \
    --timeout 20000 'echo l2mq-$$'; echo "RC=$?")
case "$AQ2" in
    *RC=0*) ;;
    *) fail linux-to-mac-quic "mux a run --quic to $MAC_BOX did not answer: $AQ2" ;;
esac
case "$AQ2" in
    *'"mechanism":"marks"'*) ;;
    *) fail linux-to-mac-quic "no marks mechanism in the reply from $MAC_BOX: $AQ2" ;;
esac
AQ2_PID=$(printf '%s\n' "$AQ2" | sed -n 's/.*l2mq-\([0-9][0-9]*\).*/\1/p')
[ -n "$AQ2_PID" ] || fail linux-to-mac-quic "the reply carries no shell pid: $AQ2"
box_ssh 60 "$MAC_BOX" >/dev/null <<M || fail linux-to-mac-quic "shell $AQ2_PID is not a child of daemon $MPID2"
. \$HOME/xos/os_oracle.sh
pid_children $MPID2 | grep -qx $AQ2_PID
M
unset MUX_KEY_FILE
ok linux-to-mac-quic "macOS delivered inbound UDP $QUIC_PORT: shells $L2MQ_PID and $AQ2_PID under $MPID2"

# ---- leg 9: upgrade-refused — the preflight is uname, both ways --------
# `uname -m` is the FIRST line of the preflight and the whole gate: the
# image is never opened and nothing is streamed. The pin is therefore both
# the refusal's own words and the installed binary's hash on the far side,
# unchanged from what leg 3 put there.
leg
set +e
timeout 120 "$MUX" d upgrade "$MAC_BOX" --allow-same-version > "$SCRATCH/up_mac.out" 2>&1
RC=$?
set -e
[ "$RC" -ne 0 ] ||
    fail upgrade-refused "mux d upgrade $MAC_BOX exited 0; an x86_64 image must not reach an arm64 box"
grep -q "is arm64 and this image is x86_64; refusing to push a binary that cannot run there" \
    "$SCRATCH/up_mac.out" || {
    echo "xos FAIL: upgrade-refused: the Linux→Mac refusal is not the arch line; it said:"
    sed 's/^/          /' "$SCRATCH/up_mac.out"
    exit 1; }

# From the Mac, the mirror. The image there is aarch64 and the far side says
# x86_64, so the two words differ in BOTH spelling and machine — which is
# what makes this the mirror image and not the same test twice.
UPL=$(box_ssh 180 "$MAC_BOX" <<M
\$HOME/.local/bin/mux d upgrade $LINUX_BOX --allow-same-version 2>&1
echo "RC=\$?"
M
)
case "$UPL" in
    *RC=0*) fail upgrade-refused "mux d upgrade $LINUX_BOX from $MAC_BOX exited 0; an aarch64 image must not reach an x86_64 box" ;;
esac
case "$UPL" in
    *"is x86_64 and this image is aarch64; refusing to push a binary that cannot run there"*) ;;
    *) fail upgrade-refused "the Mac→Linux refusal is not the arch line; it said: $UPL" ;;
esac

POST=$(
    box_ssh 60 "$MAC_BOX" <<M
. \$HOME/xos/os_oracle.sh
echo "macsha=\$(sha256_of \$HOME/.local/bin/mux)"
echo "macpid=\$(pgrep -x mux | head -1)"
echo "macexe=\$(pid_exe \$(pgrep -x mux | head -1))"
M
    box_ssh 60 "$LINUX_BOX" <<L
. ~/xos/os_oracle.sh
echo "lansha=\$(sha256_of ~/.local/bin/mux)"
echo "lanpid=\$(pgrep -x mux | head -1)"
echo "lanexe=\$(pid_exe $LPID)"
L
)
printf '%s\n' "$POST" | grep -qx "macsha=$MAC_BIN_SHA" ||
    fail upgrade-refused "$MAC_BOX's installed mux changed across the refusal: $POST"
printf '%s\n' "$POST" | grep -qx "lansha=$LAN_BIN_SHA" ||
    fail upgrade-refused "$LINUX_BOX's installed mux changed across the refusal: $POST"
printf '%s\n' "$POST" | grep -qx "macpid=$MPID2" ||
    fail upgrade-refused "$MAC_BOX's daemon is no longer $MPID2: $POST"
printf '%s\n' "$POST" | grep -qx "lanpid=$LPID" ||
    fail upgrade-refused "$LINUX_BOX's daemon is no longer $LPID: $POST"
printf '%s\n' "$POST" | grep -qx "macexe=$MAC_EXE" ||
    fail upgrade-refused "$MAC_BOX's daemon execs something else now: $POST"
printf '%s\n' "$POST" | grep -qx "lanexe=$LAN_EXE" ||
    fail upgrade-refused "$LINUX_BOX's daemon execs something else now: $POST"
ok upgrade-refused "both pushes refused on uname alone; both images and both daemons untouched"

# ---- leg 10: stop — and each box's socket goes with its daemon ---------
leg
box_ssh 60 "$MAC_BOX" >/dev/null <<'M' || fail stop "mux d stop was refused on $MAC_BOX"
$HOME/.local/bin/mux d stop
M
box_ssh 60 "$LINUX_BOX" >/dev/null <<'L' || fail stop "mux d stop was refused on $LINUX_BOX"
~/.local/bin/mux d stop
L
MEND=$(box_ssh 60 "$MAC_BOX" <<M
. \$HOME/xos/os_oracle.sh
_i=0
while [ \$_i -lt 30 ]; do
    pid_alive $MPID2 || break
    sleep 0.1; _i=\$((_i + 1))
done
pid_alive $MPID2 && echo "alive=yes" || echo "alive=no"
[ -e $MAC_SOCK ] && echo "sock=present" || echo "sock=gone"
M
)
printf '%s\n' "$MEND" | grep -qx 'alive=no' ||
    fail stop "$MAC_BOX's daemon $MPID2 survives mux d stop by 3 s: $MEND"
printf '%s\n' "$MEND" | grep -qx 'sock=gone' ||
    fail stop "$MAC_SOCK outlives the daemon that bound it: $MEND"
LEND=$(box_ssh 60 "$LINUX_BOX" <<L
. ~/xos/os_oracle.sh
_i=0
while [ \$_i -lt 30 ]; do
    pid_alive $LPID || break
    sleep 0.1; _i=\$((_i + 1))
done
pid_alive $LPID && echo "alive=yes" || echo "alive=no"
[ -e $LAN_SOCK ] && echo "sock=present" || echo "sock=gone"
L
)
printf '%s\n' "$LEND" | grep -qx 'alive=no' ||
    fail stop "$LINUX_BOX's daemon $LPID survives mux d stop by 3 s: $LEND"
printf '%s\n' "$LEND" | grep -qx 'sock=gone' ||
    fail stop "$LAN_SOCK outlives the daemon that bound it: $LEND"
ok stop "both daemons gone within 3 s, each taking its own socket with it"

# ---- the pin ----------------------------------------------------------
# A literal, test/vm.sh's and test/mac.sh's style: adding a leg means
# editing this by hand, so a leg that stops running cannot pass by not being
# counted.
[ "$OK_COUNT" = "10" ] || {
    echo "xos FAIL: $OK_COUNT legs ran, the pin says 10 —"
    echo "         a leg was added (update the pin) or silently lost"
    exit 1
}
echo "xos OK ($OK_COUNT legs: $MAC_BOX ($WANT_MAC_ARCH) and $LINUX_BOX ($WANT_LAN_ARCH), each the other's client)"