39bf2478
docs: the macOS gates name their boxes, and the guest is provisioned
a73x 2026-09-04 12:50
Commit message
CLAUDE.md
| Old | New | ||
|---|---|---|---|
| @@ -17,7 +17,15 @@ make check # fmt + unit tests + shell syntax + comment-claim | |||
| 17 | make ci # check + e2e + agent + throughput — the delivery gate | 17 | make ci # check + e2e + agent + throughput — the delivery gate |
| 18 | make agent soak bench throughput | 18 | make agent soak bench throughput |
| 19 | make vm # real user journeys against the mux-e2e VM (test/vm.sh) | 19 | make vm # real user journeys against the mux-e2e VM (test/vm.sh) |
| 20 | make mac xos # macOS journeys (test/mac.sh); the cross-OS gate (test/xos.sh) | 20 | make mac xos # macOS journeys (test/mac.sh); the cross-OS gate (test/xos.sh). |
| 21 | # Both take their boxes BY NAME and have no default | ||
| 22 | # for one: MAC_BOX (macOS, no toolchain, SCRUBBED), | ||
| 23 | # MAC_BUILDER (a Mac with zig + deps/mac-sdk, builds | ||
| 24 | # only, never scrubbed, defaults to $MAC_BOX), and | ||
| 25 | # for xos also LINUX_BOX (x86_64 Linux VM, scrubbed). | ||
| 26 | make provision-mac # clone, boot and key a macOS guest on a tart host and | ||
| 27 | # print one line; `eval "$(test/provision-mac.sh)"` | ||
| 28 | # is how MAC_BOX gets set. --down takes it away. | ||
| 21 | make install # the one user binary -> ~/.local/bin, ReleaseSafe | 29 | make install # the one user binary -> ~/.local/bin, ReleaseSafe |
| 22 | # MUX_TARGET follows the HOST: a Mac installs a Mac | 30 | # MUX_TARGET follows the HOST: a Mac installs a Mac |
| 23 | # binary, because that is the only one it can link | 31 | # binary, because that is the only one it can link |
| @@ -36,7 +44,7 @@ costs ~800k tokens; every token stays in context and is re-billed each turn. | |||
| 36 | siblings in `src/server/`, none over 1.8k; the agent relay and the | 44 | siblings in `src/server/`, none over 1.8k; the agent relay and the |
| 37 | session table are `src/server/server_agent.zig` and `src/server/server_sessions.zig`), | 45 | session table are `src/server/server_agent.zig` and `src/server/server_sessions.zig`), |
| 38 | `src/tui/interact.zig` (4.8k), | 46 | `src/tui/interact.zig` (4.8k), |
| 39 | `docs/decisions.md` (8.7k). `test/e2e.sh` is a 188-line runner now; the | 47 | `docs/decisions.md` (8.8k). `test/e2e.sh` is a 188-line runner now; the |
| 40 | bodies are `test/e2e_NN_*.sh` (`E2E_ONLY=<group>` runs one). | 48 | bodies are `test/e2e_NN_*.sh` (`E2E_ONLY=<group>` runs one). |
| 41 | Use `grep -n` for the symbol, then `sed -n 'A,Bp'` for a window. | 49 | Use `grep -n` for the symbol, then `sed -n 'A,Bp'` for a window. |
| 42 | `src/client/client.zig` is 3.3k now — cheap enough to read, and so is | 50 | `src/client/client.zig` is 3.3k now — cheap enough to read, and so is |
| @@ -485,7 +493,7 @@ own. Test fixtures in `test/`: | |||
| 485 | ## Where the answers live | 493 | ## Where the answers live |
| 486 | 494 | ||
| 487 | `docs/roadmap.md` (~660 ln) the ranked queue, but stale past 2026-08-16 — | 495 | `docs/roadmap.md` (~660 ln) the ranked queue, but stale past 2026-08-16 — |
| 488 | `git-collab issue list` is the live order · `docs/decisions.md` (8.7k ln, grep | 496 | `git-collab issue list` is the live order · `docs/decisions.md` (8.8k ln, grep |
| 489 | only) every decision + measurement · `README.md` user-facing usage. Design | 497 | only) every decision + measurement · `README.md` user-facing usage. Design |
| 490 | history (specs, plans, the original handoff) lives in git: `git log -- docs/superpowers`. | 498 | history (specs, plans, the original handoff) lives in git: `git log -- docs/superpowers`. |
| 491 | 499 | ||
README.md
| Old | New | ||
|---|---|---|---|
| @@ -755,13 +755,32 @@ so a `make e2e` on a Mac stops at the first of the two. The other gates — | |||
| 755 | `make agent`, `make soak`, `make throughput`, `make xversion` — have not | 755 | `make agent`, `make soak`, `make throughput`, `make xversion` — have not |
| 756 | been run on a Mac at all. | 756 | been run on a Mac at all. |
| 757 | 757 | ||
| 758 | The gate for macOS is `make mac`, | 758 | The gate for macOS is `make mac`, the macOS half of what `make vm` is for |
| 759 | which is the macOS half of what `make vm` is for Linux: it pushes this | 759 | Linux. Both macOS gates take the machines they use BY NAME, as ssh targets |
| 760 | commit to the Mac named by `MUX_MAC` (default `squirtle`), builds it there, | 760 | in the environment, and neither has a default for one: |
| 761 | and then runs the user journeys against a *pristine* macOS virtual machine | 761 | |
| 762 | with no developer tooling on it, so what is graded is the product on a bare | 762 | | Name | What it is | |
| 763 | Mac rather than on the machine that compiled it. The VM is Cirrus Labs | 763 | |---|---| |
| 764 | `tart`, and the base image is made once by hand, on the Mac: | 764 | | `MAC_BOX` | the macOS box under test, with no developer tooling on it. Scrubbed to a mux-less state at the start of a run and again at the end, so make it a VM. | |
| 765 | | `MAC_BUILDER` | a Mac holding this repo with zig and the shadow SDK. It builds, and nothing else: no mux is installed there and nothing is reset there. Defaults to `MAC_BOX`. | | ||
| 766 | | `LINUX_BOX` | `make xos` only: an x86_64 Linux VM that both this box and `MAC_BOX` can reach. Scrubbed the same way. | | ||
| 767 | |||
| 768 | `make mac` pushes this commit to the builder, builds and runs the unit suite | ||
| 769 | there, streams the binary onto `MAC_BOX`, and then runs the user journeys — | ||
| 770 | so what is graded is the product on a bare Mac rather than on the machine | ||
| 771 | that compiled it. A run looks like this: | ||
| 772 | |||
| 773 | ```sh | ||
| 774 | eval "$(test/provision-mac.sh)" # prints: export MAC_BOX=admin@192.168.0.x | ||
| 775 | MAC_BUILDER=squirtle make mac | ||
| 776 | test/provision-mac.sh --down # when you are done with the guest | ||
| 777 | ``` | ||
| 778 | |||
| 779 | `test/provision-mac.sh` is where the virtual machine's life is spelled: it | ||
| 780 | clones a pristine base image on a Mac that has Cirrus Labs `tart`, boots the | ||
| 781 | clone bridged onto the LAN so this box can ssh to it directly, and installs | ||
| 782 | this box's public key on it. The base image itself is made once, by hand, on | ||
| 783 | that Mac: | ||
| 765 | 784 | ||
| 766 | ```sh | 785 | ```sh |
| 767 | brew trust cirruslabs/cli && brew install cirruslabs/cli/tart | 786 | brew trust cirruslabs/cli && brew install cirruslabs/cli/tart |
| @@ -771,21 +790,17 @@ tart exec mux-mac-base sh -c 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo "PUBKE | |||
| 771 | tart stop mux-mac-base | 790 | tart stop mux-mac-base |
| 772 | ``` | 791 | ``` |
| 773 | 792 | ||
| 774 | `PUBKEY` is that Mac's own `~/.ssh/id_ed25519.pub`. Every `make mac` clones | 793 | `PUBKEY` is that Mac's own `~/.ssh/id_ed25519.pub`. Nothing stops you |
| 775 | `mux-mac-base` to `mux-mac`, boots the clone, and stops it on the way out, | 794 | pointing `MAC_BOX` at some other spare Mac instead, as long as you mean the |
| 776 | so no run can see the run before it. | 795 | word scrubbed. |
| 777 | 796 | ||
| 778 | `make mac` and `make vm` each run one operating system on both ends of every | 797 | `make mac` and `make vm` each run one operating system on both ends of every |
| 779 | connection. `make xos` is the gate that does not: it attaches a macOS client | 798 | connection. `make xos` is the gate that does not: it attaches a macOS client |
| 780 | to a Linux daemon and a Linux client to a macOS daemon, over the ssh handoff | 799 | to a Linux daemon and a Linux client to a macOS daemon, over the ssh handoff |
| 781 | and over QUIC, and checks that a cross-architecture `mux d upgrade` is | 800 | and over QUIC, and checks that a cross-architecture `mux d upgrade` is |
| 782 | refused in both directions before any of the image is streamed. It needs two | 801 | refused in both directions before any of the image is streamed. It uses all |
| 783 | other machines — the Mac named by `MUX_MAC`, which builds and installs its | 802 | three names above, and pairs the two boxes' ssh keys itself, because each |
| 784 | own arm64 binary, and a Linux box named by `MUX_LAN` (default | 803 | one dials the other from the inside. |
| 785 | `ubuntu@192.168.0.37`) that the Mac can reach by plain ssh. The Linux box is | ||
| 786 | a throwaway and is reset to a mux-less state at the start of every run; the | ||
| 787 | Mac is somebody's machine, so the gate starts nothing there that it does not | ||
| 788 | stop and refuses to run at all if a mux is already going. | ||
| 789 | 804 | ||
| 790 | ## How it works | 805 | ## How it works |
| 791 | 806 | ||
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -8740,3 +8740,96 @@ e2e suite did before this branch and defines its own `now_ms` through | |||
| 8740 | be expected to find the same class of thing the e2e suite just did. | 8740 | be expected to find the same class of thing the e2e suite just did. |
| 8741 | 8741 | ||
| 8742 | Spec: `docs/superpowers/specs/2026-09-03-macos-port-design.md`. | 8742 | Spec: `docs/superpowers/specs/2026-09-03-macos-port-design.md`. |
| 8743 | |||
| 8744 | ## 2026-09-04 — the box gates take their boxes by name | ||
| 8745 | |||
| 8746 | `make mac` and `make xos` were written against one developer's machines and | ||
| 8747 | said so in their own source: a Mac called `squirtle`, a Linux VM at | ||
| 8748 | `ubuntu@192.168.0.37`, and a `MUX_MAC_IP` literal for the QUIC legs. Nobody | ||
| 8749 | else could run either gate. Worse, the two disagreed about what a gate may | ||
| 8750 | do to a machine it finds: `xos` scrubbed the Linux box to bare metal and | ||
| 8751 | tiptoed around the Mac — refusing to run at all if a mux was already going | ||
| 8752 | there, and carrying a `MAC_CFG_MADE` flag so it could put back a | ||
| 8753 | `~/.config/mux` the product had created — while `mac` cloned a fresh tart | ||
| 8754 | guest per run and reached it only through the host's NAT, over an | ||
| 8755 | ssh-through-ssh string in every command. | ||
| 8756 | |||
| 8757 | Both gates now take three ssh targets, and neither has a default for one: | ||
| 8758 | `MAC_BOX` (macOS, no toolchain, the box under test), `LINUX_BOX` (x86_64 | ||
| 8759 | Linux, `xos` only) and `MAC_BUILDER` (a Mac with zig and the shadow SDK, | ||
| 8760 | which BUILDS and is neither installed onto nor scrubbed; it defaults to | ||
| 8761 | `MAC_BOX`). A missing name is a preflight refusal with rc 2 that names the | ||
| 8762 | variable and the line to run, before anything is written anywhere. There is | ||
| 8763 | no default because a gate that guesses a box eventually guesses somebody's | ||
| 8764 | laptop — which is the same reason the "it is a person's real machine" | ||
| 8765 | branches are gone rather than kept: MAC_BOX and LINUX_BOX are VMs the gate | ||
| 8766 | MAY scrub, stated once, and `box_scrub` is the whole of what that means. | ||
| 8767 | |||
| 8768 | The shared pieces live in `test/box_lib.sh`, sourced by both gates after | ||
| 8769 | `test/os_oracle.sh`: `box_preflight` (one `ssh -n` per box, `arm64` | ||
| 8770 | normalised to `aarch64` exactly as `main.archMatches` does for `mux d | ||
| 8771 | upgrade`), `box_pair` (idempotent key install, then a real dial from A to B | ||
| 8772 | so the pairing is proved and B's host key recorded where the plain ssh mux | ||
| 8773 | spawns will read it), `box_scrub`, and `box_ssh`/`box_scp`/`box_stream`, | ||
| 8774 | which replaced the three copies of the script-on-stdin runner the two files | ||
| 8775 | had between them. | ||
| 8776 | |||
| 8777 | The scrub runs at the START of a run and again at the END, so a failed run | ||
| 8778 | leaves nothing behind and the next one starts from the same place whatever | ||
| 8779 | happened. It writes NO shell rc file, which reverses what the old reset did. | ||
| 8780 | The old one inserted a `~/.local/bin` PATH line into the Linux box's | ||
| 8781 | `.bashrc`, and the first draft of this work added the Darwin mirror of it, a | ||
| 8782 | `~/.zshenv` line, on the reasoning that the entry dial's `~/.local/bin` | ||
| 8783 | lookup is part of what these gates test. That reasoning was wrong. Every | ||
| 8784 | remote spelling the product sends already carries the prefix itself — | ||
| 8785 | `handoff.local_bin_append`, `PATH="$PATH:$HOME/.local/bin"`, on the entry | ||
| 8786 | dial's word, on the upgrade preflight and on the upgrade's push — so a line | ||
| 8787 | in a box's shell rc tests nothing mux needs, and it is the fixture | ||
| 8788 | configuring the machine. A bare box that finds mux only because the gate | ||
| 8789 | edited its shell rc is exactly the configured developer machine the pristine | ||
| 8790 | guest exists to rule out. The gate's own remote scripts spell | ||
| 8791 | `$HOME/.local/bin/mux` in full instead, checked leg by leg, and `xos`'s | ||
| 8792 | install leg asserts the product's actual question: a plain | ||
| 8793 | `ssh BOX 'PATH="$PATH:$HOME/.local/bin"; command -v mux'` — that prefix | ||
| 8794 | character for character — answers `~/.local/bin/mux` on both boxes. | ||
| 8795 | |||
| 8796 | The one arrangement the scrub still makes is `loginctl enable-linger` on the | ||
| 8797 | Linux arm, because without it logind takes `/run/user/<uid>` down with the | ||
| 8798 | ssh session that started the daemon and reaps the detached daemon with it. | ||
| 8799 | It exits with its own code so a box without passwordless sudo is told that, | ||
| 8800 | rather than being handed "the reset was refused". | ||
| 8801 | |||
| 8802 | The Mac guest's lifecycle moved out of `test/mac.sh` and into | ||
| 8803 | `test/provision-mac.sh`, run from the Linux box: it clones the hand-made | ||
| 8804 | `mux-mac-base` on a tart host, boots the clone with `--net-bridged`, polls | ||
| 8805 | `tart ip --resolver=arp` (6 s when measured), installs this box's public key | ||
| 8806 | through the host's own, verifies a DIRECT ssh from here, and prints one line | ||
| 8807 | — `export MAC_BOX=admin@IP` — with everything else on stderr, so | ||
| 8808 | `eval "$(test/provision-mac.sh)"` works. Bridged and not NAT is what removes | ||
| 8809 | the host hop: the guest took `192.168.0.170` on the LAN, this box reaches it | ||
| 8810 | directly, and the Linux VM can see its port 22, which is what lets the two | ||
| 8811 | boxes dial each other for the cross-OS legs. | ||
| 8812 | |||
| 8813 | One consequence has to be handled rather than configured away. The guest's | ||
| 8814 | host key is new on every clone, so the harness's own ssh to `MAC_BOX` keeps | ||
| 8815 | none (`StrictHostKeyChecking=no`, `UserKnownHostsFile=/dev/null`) — but | ||
| 8816 | mux's OWN entry dial spawns a plain `ssh HOST` that reads the real | ||
| 8817 | `~/.ssh/known_hosts` and cannot be handed those options. `xos`'s preflight | ||
| 8818 | therefore does `ssh-keygen -R IP` and then one accept-new dial, so leg 7 | ||
| 8819 | finds the key where the product looks for it; `box_pair` does the same | ||
| 8820 | forget-and-relearn on the far box, because a re-provisioned guest at the | ||
| 8821 | same address would otherwise be refused by a stale entry for the rest of | ||
| 8822 | that VM's life. | ||
| 8823 | |||
| 8824 | The binary reaches `MAC_BOX` as a stream — `ssh BUILDER cat` into | ||
| 8825 | `ssh MAC_BOX cat` — because neither Mac needs a key of the other's for that | ||
| 8826 | and only the orchestrating box can reach both. `cat` carries no mode, so | ||
| 8827 | `box_stream` chmods 755 and both gates compare the builder's sha256 against | ||
| 8828 | the far side's: the assertion that the bytes arrived is the hash, not the | ||
| 8829 | copy tool. | ||
| 8830 | |||
| 8831 | Leg counts: `xos` is still ten legs. `mac` is seven, not eight — the | ||
| 8832 | clone-and-boot leg left with the VM lifecycle, and what replaced it is the | ||
| 8833 | preflight line, which is not an `ok`. The user's real Mac is no longer a | ||
| 8834 | gate target at all; it can still be `MAC_BUILDER`, which is the one role | ||
| 8835 | that touches nothing. | ||