a73x

5db1d50b

docs: step 3 plan gains Task 9, the cross-OS journeys gate

a73x   2026-09-04 10:16

Commit message
docs: step 3 plan gains Task 9, the cross-OS journeys gate

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

docs/superpowers/plans/2026-09-03-macos-port-step3-darwin-arm.md
Old New
@@ -711,6 +711,168 @@ Then per group: `for g in $(ls test/e2e_[0-9]*_*.sh | sed 's,test/e2e_,,;s,\.sh,
711 711
712 --- 712 ---
713 713
714 ### Task 9: `test/xos.sh` — cross-OS journeys: a Mac client on a Linux daemon, a Linux client on a Mac daemon
715
716 **Ruling (user, 2026-09-04):** "once macos builds we'll also need to test
717 macos client to linux daemon and vice versa." The macOS port is a second
718 ARM of one product, and the wire is the claim step 3 has not graded: every
719 pin so far runs one OS on both ends. This task attaches across the seam in
720 both directions, over both transports, and pins that a cross-arch
721 `mux d upgrade HOST` is refused before a byte moves.
722
723 **Execution order:** run this task BEFORE Task 8, so Task 8's
724 `docs/decisions.md` entry records the cross-OS verdict beside the Mac e2e
725 table. The number is 9 because it was added after the plan was cut.
726
727 **The fixture (a human/eitri action, never harness code, the `vm.sh` rule):**
728
729 - The MAC is the HOST `squirtle` (`$MUX_MAC`, arm64, macOS 26): it holds
730 the toolchain and builds; `make install` there puts an aarch64 `mux` in
731 `~/.local/bin`, which is where `client.handoff`'s appended PATH finds it
732 for a non-login ssh (`local_bin_append`). The macOS GUEST is not used:
733 it holds no key for any Linux box and a pristine guest must stay that way.
734 - The LINUX daemon box is `mux-lan` (`$MUX_LAN`, default
735 `ubuntu@192.168.0.37`): an eitri VM on host `onyx` with its `lan`
736 network, x86_64 Ubuntu, reachable by plain ssh from BOTH this box and
737 the Mac (squirtle's `~/.ssh/id_ed25519.pub` was authorized by cloud-init,
738 this box's key appended after; the `mux-vm-gate` cert is refused there).
739 Made 2026-09-04 with eitri `vm_create {name: mux-lan, host: onyx,
740 network: lan}`; the DHCP lease can move, `vm_info` re-reads it.
741 - This box is the Linux CLIENT and the orchestrator: `zig-out/bin/mux`
742 and `zig-out/bin/ptyclient` from this tree drive the Linux→Mac legs.
743 - Reachability measured 2026-09-04: squirtle cannot ssh INTO this box
744 (publickey denied) or the e2e VM (charizard's private network), which is
745 why a LAN box exists. The Mac's non-interactive ssh PATH is
746 `~/.cargo/bin:/usr/bin:/bin:/usr/sbin:/sbin` — no brew, no
747 `~/.local/bin` — so every harness ssh onto the Mac spells
748 `$HOME/.local/bin/mux` and the repo's shim PATH itself.
749
750 **Files:**
751 - Create: `test/xos.sh`
752 - Modify: `Makefile` (`xos` target, beside `vm` and `mac`, never inside `ci`), `README.md` (one paragraph under the macOS section naming the gate and its two boxes), `CLAUDE.md` (the toolchain block's `make vm` line gains `xos`)
753
754 **Interfaces:**
755 - Consumes: `MUX_MAC` (default `squirtle`), `MUX_LAN` (default `ubuntu@192.168.0.37`), the repo at `~/code/rad/mux` on the Mac, `deps/mac-sdk/bin` on the Mac PATH, `test/os_oracle.sh` (its Linux arm runs on `mux-lan`, its Darwin arm on the Mac), `test/vm.sh`'s reset block and `test/mac.sh`'s `hssh`/push legs as the shapes to copy, `mux a`'s `--quic HOST --key PATH` spelling, `main.zig`'s refusal line `mux d upgrade: HOST is ARCH and this image is ARCH; refusing to push a binary that cannot run there`.
756 - Produces: `make xos`, exit 0 with `xos OK (N legs ...)`; the verdict Task 8 records.
757
758 - [ ] **Step 1: The script**, shaped like `test/mac.sh` (`set -eu`; BatchMode
759 everywhere the harness itself sshes; `ok`/`fail` with a literal leg-count
760 pin; every claim about a box asserted by a SEPARATE ssh reading that box
761 through the oracle, never by trusting the connection under test). Two
762 helpers, `mssh SECONDS` (script on stdin, on the Mac, under `/bin/sh -s`,
763 every inner ssh takes `-n` — `mac.sh`'s rule) and `lssh SECONDS` (the
764 same onto `$MUX_LAN`). Legs, each named:
765
766 1. `push` — `mac.sh`'s push leg verbatim (`--force` to `mac-journey`,
767 detach first, `HOST_SHA` equals `HEAD_SHA`).
768 2. `build` — on the Mac: `make build` and `make install` under the shim
769 PATH (`export PATH=$HOME/code/rad/mux/deps/mac-sdk/bin:/opt/homebrew/bin:$PATH`);
770 `$HOME/.local/bin/mux --version` equals this tree's `zig-out/bin/mux --version`.
771 Here: `$(ZIG) build -Dtarget=x86_64-linux-musl -Doptimize=ReleaseSafe -p dist/xosgate`
772 (the Makefile does it, `vm`'s spelling — static musl because `mux-lan`
773 is a VM on a host whose CPU this box did not build for; the laptop was
774 bricked by a native push once, `docs/decisions.md` 2026-09-01), and
775 this box's own `zig build` for the Debug client and `ptyclient`.
776 3. `reset` — on `mux-lan`, `vm.sh`'s scorched-earth block verbatim
777 (kill every mux, remove `~/.local/bin/mux`, state, cache, config and
778 `$XDG_RUNTIME_DIR/mux*`, `enable-linger`, the bashrc PATH line); then
779 `scp` the musl `mux` to `~/.local/bin/mux` and `test/os_oracle.sh` to
780 `~/xos/`; `~/.local/bin/mux --version` equals the tree's. On the MAC
781 nothing is killed and nothing under `~` is removed — it is the user's
782 machine, not a throwaway: the leg REFUSES with a clear line if
783 `pgrep -x mux` finds a mux there already ("stop it or run on another
784 Mac"), and asserts `/tmp/mux-$(id -u)` is absent, so the daemon the
785 Linux→Mac legs meet is the one they start. Both boxes: an isolated
786 `XDG_STATE_HOME` for every mux the HARNESS runs (`~/xos/state` there,
787 `$SCRATCH/state` here), because every attaching mux writes its daemon
788 into the hosts file (CLAUDE.md) — the daemons themselves run under
789 each box's DEFAULT runtime dir, since that is where the entry dial's
790 `mux d endpoint --start` will look.
791 4. `keys` — one 32-byte QUIC key for the run: `mux d keygen` into
792 `$SCRATCH/key` here, then installed 0600 at `~/xos/key` on both remote
793 boxes. Never `~/.config/mux/key` on the Mac (the user's). Every daemon
794 below starts with `--key ~/xos/key`, every QUIC client dials with
795 `--key`/`MUX_KEY_FILE`.
796 5. `mac-to-linux` — on `mux-lan`:
797 `MUX_SHELL_INTEGRATION=1 ~/.local/bin/mux d start -d --quic 0.0.0.0 --key ~/xos/key`;
798 read `LPID` (`pgrep -x mux`). On the Mac, `zig-out/bin/ptyclient` (built by
799 leg 2) drives `$HOME/.local/bin/mux $MUX_LAN` on a real pty with
800 `XDG_STATE_HOME=$HOME/xos/state`: `expect \x1b[?1049h`, settle,
801 `send printf "XOS-M2L-%s-%s\\n" $(uname -m) $$\n`, `expect XOS-M2L-x86_64-`,
802 detach with `\x1cd`, `waitexit`. The marker carries the SESSION shell's
803 `uname -m`, so `x86_64` on a Mac's screen is the cross-OS fact itself.
804 Read the shell pid off the capture; assert on `mux-lan` via the oracle
805 that `pid_children LPID` lists it; assert the capture holds NO
806 `landed over ssh` line (the entry dial fetched the daemon's QUIC
807 coordinates over ssh and moved onto QUIC — inbound UDP to a LAN VM
808 works, `mux-test-boxes`' rule), and via the oracle
809 `udp_local_bound LPID 4433` on `mux-lan`. `mux d dump` on `mux-lan`
810 still holds the marker after the detach.
811 6. `mac-to-linux-quic` — on the Mac, no ssh in the path:
812 `MUX_KEY_FILE=$HOME/xos/key $HOME/.local/bin/mux a run --quic 192.168.0.37 --key $HOME/xos/key --timeout 20000 'echo m2lq-$$'`
813 (`mux a` joins the session leg 5 made; `--quic HOST --key PATH` is its
814 own spelling): reply carries `"mechanism":"marks"`, `"exit_code":0`,
815 and a pid that `pid_children LPID` on `mux-lan` lists. This is the
816 Darwin wolfSSL build's PSK handshake against the musl build's, in one
817 round trip, with no ssh to fall back to.
818 7. `linux-to-mac` — from HERE: `zig-out/bin/ptyclient` drives
819 `zig-out/bin/mux $MUX_MAC` with `XDG_STATE_HOME=$SCRATCH/state`; the
820 entry dial runs `ssh squirtle 'PATH=...; mux d endpoint --start'`, which
821 is the daemon's ONLY starter on the Mac in this gate (`asked` is the
822 entry dial's alone). Marker `XOS-L2M-%s-%s` with `$(uname -m)` → expect
823 `XOS-L2M-arm64-`. Then on the Mac via the oracle's Darwin arm (the repo
824 checkout at `mac-journey` has it): `MPID` is `pgrep -x mux`,
825 `pid_holds_unix_sock MPID /tmp/mux-$(id -u)/muxd.sock` (no
826 `XDG_RUNTIME_DIR` on an ssh login — the Task 2 fallback, exercised by a
827 REAL `mux HOST`), `pid_exe MPID` equals `$HOME/.local/bin/mux` resolved
828 with `cd -P`/`pwd -P` (mac.sh's spelling), `pid_children MPID` lists the
829 shell pid. The daemon has no `--quic` here because the entry dial
830 started it bare, so this attach is the ssh-handoff transport: assert
831 the capture DOES say the attach landed over ssh, or — if the daemon
832 announces a QUIC arm anyway — record which and assert that one; the
833 implementer reads the announce and pins what it says, with the reason in
834 the leg's comment.
835 8. `linux-to-mac-quic` — on the Mac, stop the bare daemon
836 (`$HOME/.local/bin/mux d stop`, `pid_alive MPID` false within 3 s) and
837 start `MUX_SHELL_INTEGRATION=1 $HOME/.local/bin/mux d start -d --quic 0.0.0.0 --key $HOME/xos/key`;
838 from HERE `MUX_KEY_FILE=$SCRATCH/key zig-out/bin/mux a run --quic 192.168.0.47 --key $SCRATCH/key --timeout 20000 'echo l2mq-$$'`
839 after a pty attach `zig-out/bin/mux quic://192.168.0.47` (with the key
840 in `MUX_KEY_FILE`) has made a session: the reply's pid is a child of
841 the new `MPID`. **This is the leg that asks whether macOS delivers
842 inbound UDP 4433 to a daemon a non-GUI ssh session started.** If the
843 application firewall eats it, the leg fails with the dial's own words;
844 record that in the report as a product/platform finding, do NOT loosen
845 the leg — a Mac daemon nobody can dial over QUIC is a fact the user
846 needs, not a harness problem.
847 9. `upgrade-refused` — both ways, with the daemons of legs 5 and 8 up.
848 From HERE: `zig-out/bin/mux d upgrade $MUX_MAC --allow-same-version`
849 exits nonzero and stderr matches
850 `is arm64 and this image is x86_64; refusing to push a binary that cannot run there`;
851 on the Mac, `pgrep -x mux` is still `MPID` and `pid_exe` unchanged.
852 From the MAC: `$HOME/.local/bin/mux d upgrade $MUX_LAN --allow-same-version`
853 exits nonzero, stderr `is x86_64 and this image is arm64; refusing …`;
854 on `mux-lan`, `LPID` and its `pid_exe` unchanged. The preflight's
855 `uname -m` is the gate and nothing is streamed: assert the installed
856 binary's sha on each box (`shasum -a 256` / `sha256sum`, through the
857 oracle if it has a helper, else the two spellings in a `case`) equals
858 what leg 3 / leg 2 installed.
859 10. `stop` — `mux d stop` on both boxes with the isolated key/state;
860 `pid_alive` false within 3 s each; the Mac's `/tmp/mux-$(id -u)`
861 socket gone; `mux-lan`'s `$XDG_RUNTIME_DIR/muxd.sock` gone.
862 11. Cleanup trap: stop any daemon the run started on either box, always;
863 remove `~/xos` on `mux-lan`; on the Mac remove `~/xos` only (never
864 `~/.local/bin/mux`, which `make install` owns and the user may want).
865
866 - [ ] **Step 2: Run it** — `make xos` from this box, twice; every leg
867 prints its name and `ok`; exit 0. Record per-leg wall-clock and which
868 transport each attach landed on in the report. A leg that fails on a
869 PRODUCT difference (leg 8's firewall question is the expected one) is
870 reported with the box's own words and left RED — the user decides.
871
872 - [ ] **Step 3: Commit** — `test: make xos attaches a Mac client to a Linux daemon and a Linux client to a Mac daemon, over ssh and QUIC`
873
874 ---
875
714 ## Self-review 876 ## Self-review
715 877
716 - **Spec coverage:** Step 3's two arm files (Tasks 3, 4), harness `case "$(uname)"` arms (Task 6), the dep-script arm (Task 1, already in tree), the two hardware probes (answered, recorded in rulings and Task 8), the three named Darwin differences (Task 3 doc header + Task 8), the `runtimeDir` spelling (Task 2), the Testing section's gate — `make ci` on the Mac is Task 8's run, the oracle's own test is Task 6 Step 4, the `make vm`-shaped journey is Task 7. The Deferred build-host decision is ruling 1. 878 - **Spec coverage:** Step 3's two arm files (Tasks 3, 4), harness `case "$(uname)"` arms (Task 6), the dep-script arm (Task 1, already in tree), the two hardware probes (answered, recorded in rulings and Task 8), the three named Darwin differences (Task 3 doc header + Task 8), the `runtimeDir` spelling (Task 2), the Testing section's gate — `make ci` on the Mac is Task 8's run, the oracle's own test is Task 6 Step 4, the `make vm`-shaped journey is Task 7. The Deferred build-host decision is ruling 1.