5db1d50b
docs: step 3 plan gains Task 9, the cross-OS journeys gate
a73x 2026-09-04 10:16
Commit message
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. |