# Section D, grounded in source (replaces v3 D; not a new plan)

hertz 2026-09-12. SOURCE-ONLY. No grant, A7 still todlando's window. Every path below is read at
b6bbaf22 (.worktrees/hertz-twohost-gate) unless a line says 921aa68f (.worktrees/asm-304-w2).

## D0. What the registered product path IS (this is what v3 D got wrong)

- Start:  `spt serve lan --bootstrap [--port N]`  — crates/spt/src/serveverb.rs:36-42, :179-186
- Stop:   `spt serve lan --stop`                   — same arms
- Ask:    `spt serve lan` (NEITHER flag) is a QUESTION, not a mutation — serveverb.rs:184-187
- Daemon: `spt node stop` (alias `spt daemon stop`) — cli.rs:416-423, :2543-2546

The LISTENER IS IN THE DAEMON, not in the CLI process: `run` calls `daemon::ensure_running()` then
`servehost::call(seed_socket_name(), request)` (serveverb.rs:190-200). So `Stop-Process -Force` on
the port's OwningProcess kills the DAEMON, not a listener the CLI owns. It is removed below.

`--bootstrap` is ALSO the admission-repair trigger: the LanUp arm calls
`report_lan_admission(binder, port, retry = bootstrap)` (serveverb.rs:247, :352-372), which
verifies, and only if absent requests the elevated helper. `--stop` calls `report_lan_cleanup()`
(serveverb.rs:253-256, :375-391), which verifies clean, and only if not requests the helper. The
helper is `serve lan-firewall --broker <seed socket>` (serveverb.rs:326-348) — the broker address is
passed BY VALUE precisely because elevation changes env and home (the comment says so at :326-328),
so the elevated half needs no SPT_HOME of its own.

## D1. The already-running case — VERIFIED in source, not assumed

`lanhost::start_at_inner` (lanhost.rs:667-673): if a listener is held it returns
`LanUp::from(listener, true)` IMMEDIATELY — no second bind, NO GATE RE-RUN, **and the requested port
is discarded**. So a second `--bootstrap --port 29470` against a daemon already serving 5470 answers
5470 and then reconciles admission FOR 5470. On this box that daemon exists right now (the resident
node is LAN-EXPOSED on 5470), which makes SPT_HOME isolation load-bearing: the seed socket is
`spt-daemon-seed-<hash of spt_home()>.sock` (endpoint.rs:23-34), so a rig home is a different daemon.
The arm-point assertion is therefore not optional: the port the product PRINTS
(`LAN_BOOTSTRAP_UP: <url> (port N)`, serveverb.rs:236) must equal $PORT, and the binder the product
prints on `LAN_FIREWALL_RECONCILED: ... for <binder> port <port>` (serveverb.rs:298-302) must equal
$EXE. If either differs, the rig is driving the fleet daemon and the arm is UNRUNNABLE.

Also verified: a SECOND `--stop` is not an error — `lanhost::stop()` returns `Ok(false)` → NOT_UP
(servehost.rs:261-266; lan_bootstrap_e2e.rs:348) and `report_lan_cleanup()` still runs, because the
CLI keys it on the `--stop` FLAG, not on whether a listener was up (serveverb.rs:253-256).

## D2. The four transitions, as four product invocations and nothing else

The registered opt-out `SPT_INSTALL_NO_FIREWALL` (bootstrap_firewall.rs:19-25) is checked in the
UNELEVATED CLI process BEFORE elevation is requested (serveverb.rs:330-333), so it deterministically
produces the two "no mutation now" arm points without a human declining a UAC prompt and without any
assumption about env inheritance through `try_elevate_args` (which I have NOT read). The daemon
never mutates the firewall on its own (bootstrap_firewall.rs:39-41).

    # T1 -> A0: listener UP, admission ABSENT
    $env:SPT_HOME = $HOME_DIR
    $env:SPT_INSTALL_NO_FIREWALL = '1'
    & $EXE serve lan --bootstrap --port $PORT
    #  expect, from the product: LAN_BOOTSTRAP_UP ... (port $PORT)
    #                            sha256 <triple> <hex>          <- one per served triple
    #                            LAN_FIREWALL_MUTATION_DISABLED  (serveverb.rs:330-333)
    #                            LAN_FIREWALL_UNVERIFIED: owned admission is absent ...
    #  That warning IS A0's precondition stated by the product's own verify() (serveverb.rs:357-362).
    #  PRE-REGISTERED BRANCH: if verify() returns Err instead (unelevated NetSecurity query failure),
    #  the same line carries a decode/query error rather than "absent" — then A0 is NOT established
    #  by the product and needs the elevated census of D4 before the arm runs. Not a surprise, a fork.

    # T2 -> A1: the PRODUCT writes the pair (elevated helper, launched by the product)
    Remove-Item Env:SPT_INSTALL_NO_FIREWALL
    & $EXE serve lan --bootstrap --port $PORT
    #  expect: LAN_BOOTSTRAP_ALREADY_UP, then a UAC prompt, then from the helper
    #          LAN_FIREWALL_RECONCILED: ... for <binder> port <port>
    #  `decide` requires BOTH halves matched name-wise in PersistentStore AND ActiveStore AND
    #  EnforcementStatus == Full (windows.rs:422-446), so that line is stronger than a rule census.

    # T3 -> A3: listener DOWN, pair PRESERVED — still no raw rule edit
    $env:SPT_INSTALL_NO_FIREWALL = '1'
    & $EXE serve lan --stop
    #  expect: LAN_BOOTSTRAP_DOWN, LAN_FIREWALL_MUTATION_DISABLED,
    #          LAN_FIREWALL_CLEANUP_UNVERIFIED: owned firewall resources remain ...
    #  is_clean() == Ok(false) (serveverb.rs:377-380) is the product asserting the pair SURVIVED the
    #  stop — which is exactly what A3 needs and what my v3 T3 was going to assert by hand.

    # T4 -> A4: the PRODUCT removes the pair
    Remove-Item Env:SPT_INSTALL_NO_FIREWALL
    & $EXE serve lan --stop
    #  expect: LAN_BOOTSTRAP_NOT_UP, UAC prompt, LAN_FIREWALL_CLEAN: no bootstrap-owned firewall
    #          resource remains
    #  The helper's removal is REMOVE_AND_VERIFY (windows.rs:64-70, :141-150): Assert-Owned on every
    #  rule (group + TCP) BEFORE deleting, then a throw if either store still holds a name. My v3
    #  `Remove-NetFirewallRule -Name ...` had neither guard nor verification. Fallback, if and only
    #  if elevation is unavailable: the product PRINTS a paste-able encoded form of that same guarded
    #  script (cleanup_command(), windows.rs:523-529) — use that, not a hand-written delete.

    # rig teardown
    & $EXE node stop            # service-aware stop; not Stop-Process
    Remove-Item -Recurse $HOME_DIR

So: four product commands, zero `New-`/`Remove-NetFirewallRule` of mine, zero `Stop-Process -Force`.

## D3. The signed set — the preparation exists, and it is NOT on my candidate

The daemon gates with the PRODUCTION policy (`lanhost::start_at` -> `VerifyPolicy::production(0,0)`,
lanhost.rs:634-638), so an empty rig home refuses by name: no staged set -> REFUSED_UNSIGNED_EXE,
staged-but-unapplied -> REFUSED_SET_NOT_APPLIED (lanhost.rs:184-220). `production()` reads
`<SPT_HOME>/identity/release-keys.json` as an OVERLAY on the builtin keys (release.rs:359-385), which
is the seam the four maintainer verbs use:

    xtask debug-keygen  <key-id>
    xtask debug-pin     --key-id <id> --public-key <hex> [--home <dir>]   # writes identity/release-keys.json, channel=debug (main.rs:2220-2259)
    xtask debug-rollout --key-id <id> --artifact <triple>=<path to $EXE>  # signs + stages into <home>/releases (main.rs:2380-2510)
    xtask debug-mark-applied --version <u64> [--home <dir>]               # the AppliedRecord no other verb writes (main.rs:2262-2295)

Two consequences, both load-bearing:

1. EXPECTED_SHA is recorded BEFORE any fetch and NOT from the served response: debug-rollout computes
   `artifact_sha256` from the bytes of the file named by `--artifact` (main.rs:2447-2452). The
   `--artifact` file MUST BE $EXE ITSELF — the gate compares the signed host-triple digest against
   the sha of the DAEMON's t=0 captured exe (lanhost.rs:88-110, :690-694). One file, one sha, staged
   and run.
2. **`debug-mark-applied` is commit 4f3f370f, which is NOT an ancestor of the twohost candidate
   b6bbaf22** (b6bbaf22's parent is 00c4dad9; 4f3f370f is its sibling). The rig cannot be prepared
   from the candidate's own tree. Either role B's product is the assembly tip (921aa68f, which has
   it) and the candidate is assembled onto the repair tip before execution, or the preparation runs
   from a tree that carries the verb while role B runs the candidate's build. That is a lane
   decision, not mine to take — but the execution plan cannot be granted without it settled.

Related and NOT cosmetic: **b6bbaf22 and 921aa68f disagree in exactly the column the 2026-09-12
failure turned on.** At b6bbaf22 the LAN half is `DESIRED_LAN_REMOTES = ["LocalSubnet"]`
(windows.rs:184-194); at 921aa68f (FOLD-3) that constant is gone and the remotes are derived per host
from connected IPv4 prefixes (`lan_scope`, windows.rs:289-336). Which sha role B runs decides what
the admitted arm witnesses. Name it and I pin it once, before the first arm.

## D4. Absence proofs — no `-ErrorAction SilentlyContinue` anywhere

Taken from the product's own mechanism and its stated reason (windows.rs:33-38): "Enumerate then
compare the exact immutable name: a failed query must not be confused with NetSecurity's
non-terminating 'named object not found' error."

    # rules: enumerate with -ErrorAction Stop, filter in the client. Empty = absent. A throw = a
    # failed query, and it stops the rig instead of reading as absence.
    $ruleNames = @('spt-core-bootstrap-inbound-tcp','spt-core-bootstrap-inbound-tcp-lan')
    $owned = @(Get-NetFirewallRule -PolicyStore ActiveStore -ErrorAction Stop |
               Where-Object { $ruleNames -ccontains $_.Name })
    # listener: same shape — no -LocalPort filter, which errors when nothing matches
    $listen = @(Get-NetTCPConnection -State Listen -ErrorAction Stop |
                Where-Object { $_.LocalPort -eq $PORT })

## D5. The effective-Allow conjunction — implementation, not prose

Captured ONCE into `$D`; every predicate and every control then runs over `$D`, so a control cannot
be answered by a second, differently-scoped query. Rows are classified COVERS / NO / UNDECIDED — a
shape the matcher cannot decide (a keyword remote such as Internet/Intranet/DNS, a dynamic keyword)
is never silently a NO.

    $sentinel = "hz-absent-sentinel-$([guid]::NewGuid())"      # minted, never created
    $D = @(Get-NetFirewallRule -PolicyStore ActiveStore -Direction Inbound -ErrorAction Stop |
      ForEach-Object {
        $p = @($_ | Get-NetFirewallPortFilter -ErrorAction Stop)[0]
        $a = @($_ | Get-NetFirewallApplicationFilter -ErrorAction Stop)[0]
        $r = @($_ | Get-NetFirewallAddressFilter -ErrorAction Stop)[0]
        [pscustomobject]@{ name=[string]$_.Name; enabled=[string]$_.Enabled; action=[string]$_.Action
          profile=[string]$_.Profile; enforce=[string]$_.EnforcementStatus
          proto=[string]$p.Protocol; ports=@($p.LocalPort | % { [string]$_ })
          program=[string]$a.Program
          local=@($r.LocalAddress | % { [string]$_ }); remotes=@($r.RemoteAddress | % { [string]$_ }) } })

    function Ip2I($s){ $b=([net.ipaddress]$s).GetAddressBytes(); [array]::Reverse($b); [BitConverter]::ToUInt32($b,0) }
    function InCidr($cidr,$ip){ $n,$l = $cidr -split '/'; $m = if([int]$l -eq 0){0}else{[uint32]::MaxValue -shl (32-[int]$l)}
                                ((Ip2I $n) -band $m) -eq ((Ip2I $ip) -band $m) }
    function PortCovers($ports,$p){ foreach($e in $ports){ if($e -eq 'Any'){return $true}
        if($e -match '^\d+$'){ if([int]$e -eq $p){return $true} }
        elseif($e -match '^(\d+)-(\d+)$'){ if($p -ge [int]$Matches[1] -and $p -le [int]$Matches[2]){return $true} } }
      $false }
    # returns 'yes'/'no'/'undecided'
    function RemoteCovers($remotes,$peer,$prefixes){ $u=$false
      foreach($e in $remotes){ if($e -eq 'Any'){return 'yes'}
        elseif($e -match '^\d+\.\d+\.\d+\.\d+/\d+$'){ if(InCidr $e $peer){return 'yes'} }
        elseif($e -match '^\d+\.\d+\.\d+\.\d+$'){ if($e -eq $peer){return 'yes'} }
        elseif($e -match '^(\d+\.\d+\.\d+\.\d+)-(\d+\.\d+\.\d+\.\d+)$'){
          $i=Ip2I $peer; if($i -ge (Ip2I $Matches[1]) -and $i -le (Ip2I $Matches[2])){return 'yes'} }
        elseif($e -eq 'LocalSubnet'){ foreach($c in $prefixes){ if(InCidr $c $peer){return 'yes'} } }
        else { $u=$true } }                      # keyword / dynamic keyword: not decidable here
      if($u){'undecided'}else{'no'} }

    # the serving interface's own facts, read once
    $prefixes = @(Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop |
                  Where-Object { $_.AddressState -eq 'Preferred' -and $_.PrefixLength -ge 8 } |
                  ForEach-Object { $n=(Ip2I $_.IPAddress) -band ([uint32]::MaxValue -shl (32-$_.PrefixLength))
                                   "$([net.ipaddress]([BitConverter]::GetBytes($n)[3..0] -join '.'))/$($_.PrefixLength)" })
    $cat = [string](Get-NetConnectionProfile -ErrorAction Stop | ? { $_.IPv4Connectivity -ne 'Disconnected' } | % NetworkCategory)

    $rows = $D | ForEach-Object {
      $hy = ($_.enabled -eq 'True') -and ($_.action -eq 'Allow') -and
            ($_.proto -in @('TCP','6','Any')) -and ($_.enforce -eq 'Full')
      $rc = RemoteCovers $_.remotes $PEER $prefixes
      $pf = ($_.profile -eq 'Any') -or ($_.profile -split ',' -contains $cat) -or
            ($cat -eq 'Private' -and $_.profile -match 'Private')
      $pg = [string]::IsNullOrEmpty($_.program) -or ($_.program -ieq $EXE)
      $lc = ($_.local -contains 'Any')
      [pscustomobject]@{ name=$_.name; verdict = if(-not $hy){'no'}
        elseif(-not (PortCovers $_.ports $PORT)){'no'} elseif(-not $pf){'no'}
        elseif(-not $pg){'no'} elseif(-not $lc){'no'} else{$rc} } }
    $allow     = @($rows | ? verdict -eq 'yes')
    $undecided = @($rows | ? verdict -eq 'undecided')

    # CONTROLS, all over $D / over the predicate itself, creating nothing:
    #  port column, positive : @($D | ? { PortCovers $_.ports 5470 }).Count  -gt 0   (the live 5470 rule)
    #  program column, positive: @($D | ? { $_.program }).Count              -gt 0
    #  dump filter, negative : @($D | ? { $_.name -ceq $sentinel }).Count    -eq 0
    #  predicate, own control: PortCovers @('29470') 29470 -eq $true  AND
    #                          PortCovers @('29470') 29471 -eq $false        (a filter that cannot
    #                          return false is the same clean zero as a dead one)
    # Any control that fails VOIDS the census; a blanket count is not reported from a denied read.

    # the other two conjuncts of the precondition
    Get-NetFirewallProfile -ErrorAction Stop | Select-Object Name,Enabled,DefaultInboundAction
    $blocks = @($D | ? { $_.action -eq 'Block' -and $_.enabled -eq 'True' })   # enumerated, not assumed empty

BLOCKED PRECONDITION = `$allow` EMPTY **and** `$undecided` EMPTY **and** the serving profile's
DefaultInboundAction recorded **and** `$blocks` enumerated. A non-empty `$undecided` is reported and
the arm does not claim a blocked precondition.

## D6. The local 200 — a sample, stated as one

v3 said a local 200 proves the app answers. It proves it AT THAT INSTANT. Corrected: each arm takes
the responsiveness sample TWICE, bracketing the remote attempt series, and both samples are timestamped
against the series window; the verdict says "answering at t0 and t1 bracketing a window of N s",
never "throughout". And the sample is the OWN-LAN-ADDRESS probe, not loopback, because the rig file
itself rules loopback insufficient (twohost_bootstrap.rs:55-70: 127.0.0.1 answers 200 whether or not
the listener is on that interface):

    curl.exe -sS -o NUL -w "%{http_code} %{time_total}\n" --max-time 10 http://<this host's LAN ip>:$PORT/install

## D7. Corrections to my own earlier pinning (section B)

"$EXE at a path no program rule names" was chosen for the wrong reason. `DESIRED_PROGRAM = false`
(windows.rs, FOLD-2 block): the product's pair carries NO program filter at all, so the exe path
cannot make the OWNED rules match or not match. The precondition that matters is PORT + REMOTE
coverage, which is what D5 computes. $EXE must still be pinned once and never moved — because it is
the gate's sha subject (D3.1) and the binder the product prints — but not for the reason I gave.
