refactor(bootstrap_firewall): move the scope policy out of PowerShell into a testable seam FOLD-1 of doyle's ruling on releases#304 W2 / #297. Two implementations of one surface existed — `REQ-WEB-LAN-BOOTSTRAP-FIREWALL` here and four `REQ-LAN-BOOTSTRAP-*` ids I minted on a lane off main after grepping only main for prior art. The ids survive here; the duplicate set never reached main and is abandoned with its lane kept as the record of W-1. THE DEFECT THIS FIXES IS NOT THE DUPLICATION. It is that the scope policy — profile, remote address, program — lived inside a PowerShell string constant, so no Rust cell could reach it. That is why the six existing cells cover gating, argument guards, binder-path text and UTF-16LE encoding and NONE of them touches scope, and it is how two amendments that contradict each other on that exact policy coexisted with nothing going red. THE SPLIT: hygiene stays in the script, policy comes to Rust. HYGIENE (stays): enabled, inbound, allow, no platform/owner scope, RemotePort and DynamicTarget Any, LocalAddress Any, package/service/interface Any, no user/machine restriction, no dynamic keyword. These must hold under ANY admission shape, so they are a precondition rather than a policy. POLICY (leaves): profile, remote address, program. QUERY now REPORTS these three as data and decides nothing about them. New, pure, and the first assertable form the policy has ever had: RuleSpec { port, profile, remotes, program: Option } desired_spec(binder_normalized, bound_port) -> RuleSpec spec_satisfied_by(observed: &Rule, want: &RuleSpec) -> bool `decide()` — already split out at c468e9df — now calls the predicate instead of inlining the comparison, so its duplicate/ambiguity and enforcement arms are untouched. TWO BEHAVIOUR CHANGES, both deliberate, both required by the seam: 1. THE QUERY NO LONGER THROWS ON A RULE WITHOUT A PROGRAM. An absent application filter is a FACT about a port+remote-scoped rule, not an error; throwing made the query unable to describe the very shape the scope ruling may select. 2. `reconcile` EMITS THE SPEC. When the spec carries no program the `-Program` argument is absent entirely rather than passed as `Any` — that would be a different rule which merely reads similar. Its failure message now names the spec it wanted instead of the old "unrestricted scope" prose. SCOPE VALUES ARE NOT RULED YET. `DESIRED_PROFILE`, `DESIRED_REMOTES` and `DESIRED_PROGRAM` carry `// FOLD-2: scope policy pending W-2a arm C` and hold todlando's W2 shape as the working default. The ADR-0059 amendment prose, the requirement title's scope sentences and the docs-site scope section are deliberately NOT written here; they land in FOLD-2 with the measurement cited. `DESIRED_PROGRAM = false` records what W-0 measured on HFENDULEAM: program-scoped rules naming the listening binary, enabled and ActiveStore-Enforced on a Private-classified tailnet interface, admitted nothing over TCP. FOUR CELLS ATTACH WHERE NONE COULD BEFORE: the spec is built for the BOUND port and a stale-port rule does not satisfy it; a program-bearing rule does NOT satisfy a spec that wants none (the direct contradiction of the old throw), while the spec can still express a program so this is a policy choice and not a lost capability; an unrestricted `RemoteAddress Any` does not satisfy a narrowed spec, with profile asserted separately — a right RemoteIP on a wrong profile is the trap a RemoteIP-only assertion cannot catch — and remote comparison is set-like and case-insensitive so a reordered render does not decide the verdict; hygiene is a precondition: a rule failing it is refused even when its scope is exactly right. The retired REMOTE-WITNESS text folds into this requirement's INT clause, still deactivated, reactivated by the twohost_bootstrap lane and no one else: a second machine witnesses the admission, loopback is not a witness for that claim, and ADMISSION-BLOCKED must not share a face with STOPPED. The Linux arm makes no claim here: linux.rs carries 676 lines and zero cells, and is invisible to this box's gate. Registered as a labelled hole (IR-110) for a kitsubito test lane, not silently covered. Build only — no test executed; the execution window follows this compile-gate. releases#304 releases#297 Co-authored by: todlando Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V8rmw2TwXUSA88iFjYV3ii