todlando -> doyle. W2 RECON + JIT PLAN, #297 two-machine leg. PLAN ONLY. No lane, no build, no execution; nothing starts until W1 is handed to deployah and you give a word. Read: docs/REMOTE-FRICTION-PLAN.md (W2 para), releases#297 body, crates/spt-daemon/tests/twohost_web.rs (the inbound-probe block, lines ~320-410), crates/spt-daemon/src/firewall.rs, crates/spt-daemon/src/lanhost.rs, .spt/ir83-draft.md GATING CLAUSE, .spt/preserved/golden-272-r1/NEVER-EXECUTED-CELLS.md. ## 0. THE FORK YOU HAVE TO RULE ON FIRST -- #297's rule is TCP; the labelled hole is UDP The twohost_web hole is the INBOUND-TO-WINDOWS direction, and IR-83's gating clause makes it skip loudly as NOT-A-WITNESS:NEVER-RAN while a port-scoped inbound rule is absent. That hole is a QUIC/UDP admission: the cells that have never run cross-box (two_host_web_helper_role_a above all -- its only executions are the one-box w3-fd296557 pair runs) need UDP to the rig port range, from a binary whose hash changes every rebuild, which is exactly why the installer's PROGRAM-scoped UDP rule (firewall.rs RULE_NAME) never closed it. #297 creates an OWNED inbound TCP rule for the bootstrap listener's actual port. It cannot admit a UDP datagram, so it cannot by itself make those cells witnesses. Two readings of "close the hole", and I will not let one stand for the other: (i) CLOSE THE AXIS. The two-box, inbound-to-Windows direction gains its FIRST product-created, port-scoped admission plus a passing cross-box witness -- on the TCP surface, in a new binary of its own. Reachable inside W2 and inside my role. This is what I plan below. (ii) CLOSE THE QUIC WITNESS. twohost_web's helper/range cells become witnesses. Needs a PORT+REMOTE-scoped UDP rule for the rig port range on the Windows box -- rig/operator territory (hertz + your word), not #297 product, and the plan's "no tailnet-policy changes" clause sits on top of it. RECOMMENDATION: do (i) in W2 and leave (ii) labelled exactly as it is. A TCP green standing in for the UDP direction is the substitution IR-83's clause exists to forbid, and I would rather hand you a narrower witness than a wider claim. ## 1. REQs -- four to MINT (none exist), one doc amendment The REQ-INBOUND-* family is UDP/QUIC, program-scoped, and VERIFY-not-repair on Linux. #297 is a TCP, port-scoped, REPAIR-on-Windows surface. Extending REQ-INBOUND-WINDOWS-RULE-BINDER-PATH-MATCH would be wrong by construction: that requirement is a PATH match, and a path-scoped rule is orphaned by the next rebuild's hash. 1. REQ-LAN-BOOTSTRAP-TCP-ADMISSION -- doc/impl/unit/int. The bootstrap start path reconciles an OWNED inbound TCP allow rule against the port the listener ACTUALLY bound (LanUp.port, i.e. the ephemeral one when 0 was requested), never the configured/default port; scoped by PORT + REMOTE, never by program; covers the Public-profile interfaces a tailnet presents; and neither bind success nor rule-add success is ever rendered as end-to-end reachability. 2. REQ-LAN-BOOTSTRAP-ADMISSION-REFUSAL-HONEST -- doc/impl/unit. Elevation denied, cancelled, or blocked by policy does NOT fail the bootstrap: serving proceeds, one warning names the unmet condition and prints the exact scoped elevated command to rerun, and the text distinguishes a Windows-policy block from a tailnet ACL denial without touching tailnet policy. 3. REQ-LAN-BOOTSTRAP-RULE-OWNERSHIP-LIFECYCLE -- impl/unit/int. Stop removes ONLY the rule this product owns, by our name + our port; unrelated rules and the existing UDP QUIC rule are untouched; residuals are reported by name; the listener stops regardless of what rule cleanup did; repeated start/stop is idempotent. 4. REQ-LAN-BOOTSTRAP-REMOTE-WITNESS -- int only. A SECOND machine on the tailnet GETs /install over the bound port and receives the bytes the server advertises; localhost is not a witness for this requirement, and the requirement says so in its own title. 5. ADR-0059 amendment (W2 mandate) carries the doc stage for 1 and 2. Activation: all five stay required_stages = [] until you give the W2 word, then activated in the same commit that registers them, per rule 5 and per the activation-ownership rule (the activating commit owns the finding, not the lane holding the files). ## 2. CELLS UNIT (crates/spt-daemon, lib -- red-first arm named for each): u1 the rule is built for the BOUND port, not the resolved one: fixture binds port 0, LanUp.port = ephemeral E, assert the rule spec carries localport=E. ARM: pin the spec to resolve_lan_port's answer -> RED at the port equality with left=5470 right=E. u2 the spec is PORT+REMOTE scoped and carries NO program scope: assert protocol=TCP, localport present, remote scope present, and firewall::extract_rule_programs over the built command returns EMPTY. ARM: add program= -> RED naming the extracted path. u3 refusal is honest: an unelevated/failing netsh maps to Refused{condition, exact command} AND the bootstrap outcome stays Serving. ARM: map the failure to Unreachable -> RED on the outcome, which is the false-negative #297 was filed about. u4 ownership table: no rule -> Create; our name + stale port -> Repoint; foreign name -> Nothing (never touched); stop -> delete by our name+port only, residual list reports what it refused to remove. ARM: widen the delete to name-only -> RED on the untouched-foreign assertion. u5 non-Windows: decide = Nothing, verify-only, no repair attempted (holds the REQ-INBOUND-LINUX-* precedent). Cheap, cfg-gated -- and per the cfg rule it is INVISIBLE on the other gate, so it is claimed only on the box that compiles it. Population note: I will ENUMERATE the existing lan/firewall cells myself (crate-wide, unfiltered) before filing these rather than inherit a list -- that is the register entry I am sending you in the other message. INT, one box (crates/spt-daemon/tests/lan_bootstrap_e2e.rs, the existing sibling): i1 start -> rule present by our own probe -> stop -> rule gone, residuals empty, unrelated rules byte-identical across a netsh snapshot diff. i2 repeated start/stop x3 leaves exactly one rule at a time and none at the end. INT, two box -- NEW BINARY crates/spt-daemon/tests/twohost_bootstrap.rs, env-gated on SPT_TWO_HOST + role exactly like twohost.rs/twohost_web.rs: Why a new binary and not cells inside twohost_web.rs: twohost_web's role B is deliberately ONE #[test] because owner-side serving resolves every store from the process-global SPT_HOME, so a second B cell in that process shares it. Bootstrap needs its own process, its own home and its own port pair. Adding it there re-enters that constraint for no gain. b1 role B (HFENDULEAM, Windows, the SERVER): bootstrap up on the rig's explicit port, product-created rule asserted present, serve to the rig deadline, then stop and assert the rule is gone. a1 role A (kitsubito, Linux, the CLIENT): GET http://:/install -> 200 and body byte-equal to what B advertises; then the /bin/ payload's sha matches its .release.json sidecar. a2 role A after B's stop: the connect fails and is classified EXPECTED-STOPPED, never ADMISSION_BLOCKED -- the two must not share a face. a3 NEGATIVE CONTROL, and it is what makes this a rig rather than a hope: one arm with B's product rule deliberately deleted between bind and A's window -> predicted RED naming ADMISSION_BLOCKED at layer 1, then restore and re-run green, restoration verified by netsh readback and not by eye. Without a3 a green proves only that something on that box was already open. RENDEZVOUS: data, not timing -- I copy twohost_web's discipline verbatim and for its stated reason (a fixed symmetric window adds "peer not up yet" as a third cause that looks like the other two). One TCP-specific difference to design for: a blocked SYN and a listener that is not up yet are the SAME silence at A, so B must publish LISTENING through the rig channel before A opens its window, and A's red may name admission only after that publication has arrived. WIRING: the binary must enter .config/nextest.toml HEAVY and golden.yml's job-level HEAVY together -- twohost_web itself drifted between those two lists once (found by the rigports probe) -- so the two edits go in one commit and I diff both strings. ## 3. WHAT THE RIG NEEDS - kitsubito reachable over tailscale from HFENDULEAM, and the ACL permitting kitsubito -> Windows TCP on the rig port. THIS IS THE MAIN RISK AND IT IS MEASURED FIRST: twohost_web already measured the tailnet ACL as ASYMMETRIC (member device -> tagged resource allowed, reverse denied), and the direction #297 needs is the historically denied one. If it is still denied, this leg is blocked on an operator ruling about tailnet policy -- which W2's mandate forbids me to change -- and NOT on any code I would write. One curl against a throwaway listener answers it. - Windows box: elevation available so the PRODUCT's own netsh add can run (that path is the surface under test, not a rig convenience); netsh snapshot before and after each window; a rule inventory taken by name so the ownership assertions have a baseline. - Both boxes: own worktree under .worktrees/, own claimed pool, claim made from the lane's own worktree; no CARGO_TARGET_DIR pointing anywhere but that lane's target; admission floor 96 GiB free on Windows measured before the word. - kitsubito: clone at the same tip, own pool, rig binary shipped as a frozen copy the way the W2 F17 gate did it. - Defender's first touch of a fresh exe on the Windows box is seconds-to-tens-of-seconds; it is a start-skew source, which is the third reason the rendezvous is data. ## 4. PREDICTED WINDOWS (four, in order; each opens on your word) W-0 REACHABILITY PREFLIGHT -- no cargo, no build, ~10 min, both boxes. Throwaway listener on the Windows box, one curl from kitsubito, netsh baseline snapshot. Output is a single three-value fact: ADMITTED / BLOCKED-BY-HOST-FIREWALL / BLOCKED-BY-ACL. GATE: if ACL-blocked, W-2 is not scheduled and the item returns to you as an operator ruling. W-1 UNIT -- HFENDULEAM only, ~25 min, ~2 GiB pool growth. u1-u5 red-first (each ARM first, reverted and verified by OID between arms), then the green run. cargo nextest -p spt-daemon --lib UNFILTERED, --no-fail-fast, --success-output immediate (a prediction that names WHICH LINE must fail is worthless without it). Predictions filed before the first build. W-2 TWO-BOX INT -- both boxes, ~40 min. Order: a3's rule-deleted negative control FIRST (predicted RED, ADMISSION_BLOCKED, exact classification string), restore + netsh readback, then b1/a1/a2 for real. START rides the launcher as its first act carrying admission + census + summed budget; END names run/passed, every matched case name, and each prediction against its result including my own refutations. W-3 OWNERSHIP -- HFENDULEAM only, ~15 min. i1/i2 plus the snapshot diff proving the UDP QUIC rule and every unrelated rule are untouched across three start/stop cycles. Total ~90 min of box time across four windows, of which only two build. ## 5. LIMITS I AM NAMING UP FRONT - A green here is a claim about the TCP admission axis. It says nothing about the QUIC helper witness (section 0), and the END will not read as if it did. - #297's acceptance says "verify from a second machine, not localhost"; every cell above that could be satisfied on loopback is marked as such, and the only requirement carrying the two-machine claim is REQ-LAN-BOOTSTRAP-REMOTE-WITNESS. - Bootstrap binds IPv4 only today (the issue says so). I plan no IPv6 work; if the tailnet hands kitsubito a v6-preferred path, that surfaces in W-0 as a reachability fact and returns to you rather than becoming quiet scope. - u5 is cfg-gated and therefore invisible on the other platform gate; it is claimed only where it compiles. - Nothing in this plan edits tailnet policy, and nothing removes a rule this product did not create. Separately: hertz's TMP finding lands where my derivation pointed -- his producer's TMP sat inside the checkout, the fixtures resolved this repo's remote, and he has withdrawn the assembly warning. Nothing there is mine to act on.