diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e54df1a..29617492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,8 +52,12 @@ flag, or page that no existing user can encounter without opting into it. where a redundant query hit the three-second limit before either rule was written. Elevated admission now verifies and derives any repair from one pre-write census; NetSecurity operations have a bounded 30-second allowance rather than the short - probe budget. Equivalent rule spellings and enforcement evidence remain supported, - and a failed post-write verification still says the rules may already be present. + probe budget. The sixth window then exposed a separate defect: explicitly writing + `-Package Any` left both rules inactive. Bootstrap now omits that package argument + and recognizes enforcement on active profiles without rejecting an inactive + companion profile. Non-OK rules, missing success evidence, and NoLocalUser + remain unverified. A failed post-write verification still says the rules may + already be present. - Unlisted-peer presence checks in endpoint listings use a 2.5-second timeout diff --git a/crates/spt-daemon/src/bootstrap_firewall/windows.rs b/crates/spt-daemon/src/bootstrap_firewall/windows.rs index 56ff9176..c7cc6786 100644 --- a/crates/spt-daemon/src/bootstrap_firewall/windows.rs +++ b/crates/spt-daemon/src/bootstrap_firewall/windows.rs @@ -29,7 +29,7 @@ const RULE_GROUP: &str = "spt-core bootstrap TCP"; /// and `desired_specs` all derive from these constants instead of repeating them. const RULE_NAMES: &[&str] = &[RULE_NAME_TAILNET, RULE_NAME_LAN]; -/// The one ActiveStore enforcement code that certifies a rule is in force. +/// The positive ActiveStore code, combined with PrimaryStatus and the code-20 veto. /// /// A NUMBER, NEVER A SPELLING. The host renders this code as a display string, and /// which string is a property of the host rather than of the rule: measured on @@ -41,19 +41,12 @@ const RULE_NAMES: &[&str] = &[RULE_NAME_TAILNET, RULE_NAME_LAN]; // [impl->REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES] const ENFORCEMENT_SUCCESS: u16 = 1; -/// The COMPLETE certified evidence: exactly one element, the success code. -/// -/// STRICTNESS IS THE REQUIREMENT, not an implementation detail. This is compared with -/// `!=` against the whole slice, so `[1, 1]` and `[1, 5]` both refuse. Equality must -/// never become membership: a success code sitting beside another code means the host -/// reported something else as well. What an array of several codes MEANS is not -/// resolved -- the documented values describe individual codes and say nothing about -/// how a multi-element array should be read -- so this policy refuses rather than -/// interpreting. Empty, unknown and additional values all refuse here, and -/// anything the query could not transport as a number never reaches this comparison at -/// all -- it fails deserialization and surfaces as a query error instead. +/// Run-02 on HFENDULEAM measured OK/[5,1] without a package binding and +/// Inactive/[5,20] with `-Package Any`. A multi-profile rule need not have a +/// singleton enforcement array: the inactive Domain profile contributes 5. // [impl->REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES] -const ENFORCEMENT_CERTIFIED: [u16; 1] = [ENFORCEMENT_SUCCESS]; +const ENFORCEMENT_NO_LOCAL_USER: u16 = 20; +const PRIMARY_STATUS_OK: u16 = 1; // Enumerate then compare the exact immutable name: a failed query must not be // confused with NetSecurity's non-terminating 'named object not found' error. @@ -74,11 +67,11 @@ function Value($object, $name) { # verdict compared against "Full" -- so the success value itself was refused and no host # state could satisfy the arm. The repair is this ACCESSOR, not a cast: casting the # adapted value throws on "Enforced". CimInstanceProperties is the unadapted CIM view. -function RawValue($object, $name) { +function RawValue($object, $name, $expectedType = 'UInt16Array') { $property = $object.CimInstanceProperties[$name] if ($null -eq $property) { throw "ENFORCEMENT_REPRESENTATION_FAULT: NetSecurity omitted required CIM property $name" } - if ([string]$property.CimType -ne 'UInt16Array') { - throw "ENFORCEMENT_REPRESENTATION_FAULT: CIM property $name has type $($property.CimType), expected UInt16Array" + if ([string]$property.CimType -ne $expectedType) { + throw "ENFORCEMENT_REPRESENTATION_FAULT: CIM property $name has type $($property.CimType), expected $expectedType" } $property.Value } @@ -142,12 +135,14 @@ function Describe($rule) { # NOT: it is reported below and decided in Rust, because a policy expressed # only as text handed to another interpreter cannot be unit-tested, which is # how two contradictory amendments coexisted with nothing red (releases#304). + # Run-02 measured an empty Package when New-NetFirewallRule omitted -Package. + # Literal Any is a binding here, unlike the Program filter's unrestricted spelling. $hygiene = (Is $rule 'Enabled' 'True') -and (Is $rule 'Direction' 'Inbound') -and (Is $rule 'Action' 'Allow') -and (Empty $rule 'Platform') -and (Empty $rule 'Owner') -and (Is $port 'RemotePort' 'Any') -and (Is $port 'DynamicTarget' 'Any') -and (Is $address 'LocalAddress' 'Any') -and - (Is $app 'Package' 'Any') -and (Is $service 'Service' 'Any') -and + (Empty $app 'Package') -and (Is $service 'Service' 'Any') -and (Is $interface 'InterfaceAlias' 'Any') -and (Is $type 'InterfaceType' 'Any') -and (Is $security 'Authentication' 'NotRequired') -and (Is $security 'Encryption' 'NotRequired') -and (Is $security 'OverrideBlockRules' 'False') -and @@ -185,6 +180,10 @@ function Describe($rule) { } [int]$_ }) + $primaryStatusRaw = RawValue $rule 'PrimaryStatus' 'UInt16' + if ($null -eq $primaryStatusRaw -or $primaryStatusRaw -isnot [uint16]) { + throw "ENFORCEMENT_REPRESENTATION_FAULT: invalid raw PrimaryStatus for $name, expected System.UInt16" + } # WHERE THE EFFECTIVE RULE CAME FROM. Reported, never judged here: the Rust # side decides what counts as persistent. 'Local' names the local persistent # store as this effective rule's source, which is how ONE ActiveStore pass @@ -199,6 +198,7 @@ function Describe($rule) { remotes = @(Value $address 'RemoteAddress' | ForEach-Object { [string]$_ }) hygiene = [bool]$hygiene enforcement = $enforcement + primaryStatus = [int]$primaryStatusRaw } } # ONE STORE PASS, NOT TWO. The old query enumerated PersistentStore and @@ -259,14 +259,12 @@ struct Rule { /// anything the query cannot transport as a number -- a null, a display /// string, a negative -- fails deserialization and surfaces as a query error /// rather than arriving as a code the verdict would then judge. - /// - /// NO NAME IS ATTACHED TO ANY CODE, here or anywhere downstream. The captured - /// class on the measuring host exposes ValueMap (0..25) with the Values - /// qualifier ABSENT, so no code-to-name mapping is derivable from the host at - /// all -- including for the success code. Naming one would be documentation - /// smuggled in as an observation. + /// PrimaryStatus is transported separately so a success code cannot override + /// the provider reporting the rule inactive. // [impl->REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES] enforcement: Vec, + #[serde(rename = "primaryStatus")] + primary_status: u16, /// Which store this EFFECTIVE rule came from, as NetSecurity reports it: /// `Local` for the local persistent store, `GroupPolicy` for domain policy, /// `Dynamic` for a rule that exists only until reboot. Reported here and @@ -456,6 +454,8 @@ fn desired_specs(binder_normalized: &str, bound_port: u16, lan: &LanScope) -> Ve /// the emitted rule text can be compared against the spec WITHOUT a live /// NetSecurity, and an effector whose output nothing can read is how a scope /// policy expressed as script text went unchecked in the first place. +/// Run-02 isolated `-Package Any` as the argument making the rule inactive. +/// Omit the binding; Get-NetFirewallApplicationFilter reports an empty Package. // [impl->REQ-WEB-LAN-BOOTSTRAP-FIREWALL] fn render_writes(want: &[RuleSpec]) -> String { want.iter() @@ -465,7 +465,7 @@ fn render_writes(want: &[RuleSpec]) -> String { r#" New-NetFirewallRule -PolicyStore PersistentStore -Name '{name}' -DisplayName '{name}' -Group $ruleGroup ` -Enabled True -Direction Inbound -Action Allow -Profile {profile} -Protocol TCP -LocalPort {port} ` - -RemotePort Any -LocalAddress Any -RemoteAddress {remotes} {program_arg}-Package Any -Service Any ` + -RemotePort Any -LocalAddress Any -RemoteAddress {remotes} {program_arg}-Service Any ` -InterfaceAlias Any -InterfaceType Any -Authentication NotRequired -Encryption NotRequired ` -LocalUser Any -RemoteUser Any -RemoteMachine Any -OverrideBlockRules $false -DynamicTarget Any ` -EdgeTraversalPolicy Block -ErrorAction Stop | Out-Null @@ -832,11 +832,15 @@ fn decide(state: &Snapshot, expected_program: &str, port: u16) -> ResultREQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES] - if rule.enforcement.as_slice() != ENFORCEMENT_CERTIFIED { + if rule.primary_status != PRIMARY_STATUS_OK + || !rule.enforcement.contains(&ENFORCEMENT_SUCCESS) + || rule.enforcement.contains(&ENFORCEMENT_NO_LOCAL_USER) + { return Err(format!( - "Bootstrap rule {} is configured but ActiveStore enforcement codes are {:?}, \ - not exactly [{}]", - rule.name, rule.enforcement, ENFORCEMENT_SUCCESS + "Bootstrap rule {} is configured but ActiveStore PrimaryStatus is {} and \ + enforcement codes are {:?}; expected PrimaryStatus {} with code {} present and code {} absent", + rule.name, rule.primary_status, rule.enforcement, PRIMARY_STATUS_OK, + ENFORCEMENT_SUCCESS, ENFORCEMENT_NO_LOCAL_USER )); } } @@ -1029,10 +1033,10 @@ mod tests { "active": [ {"name": RULE_NAME_TAILNET, "program": "Any", "ports": ["29470"], "profile": "Any", "remotes": ["100.64.0.0/10"], "hygiene": true, - "enforcement": [1], "sourceType": "Local"}, + "enforcement": [1], "primaryStatus": 1, "sourceType": "Local"}, {"name": RULE_NAME_LAN, "program": "Any", "ports": ["29470"], "profile": "Private,Domain", "remotes": ["192.168.1.0/24"], - "hygiene": true, "enforcement": [1], "sourceType": "Local"} + "hygiene": true, "enforcement": [5, 1], "primaryStatus": 1, "sourceType": "Local"} ], "addresses": addresses }).to_string(); @@ -1157,6 +1161,7 @@ mod tests { remotes: remotes.iter().map(|r| r.to_string()).collect(), hygiene: true, enforcement: vec![ENFORCEMENT_SUCCESS], + primary_status: PRIMARY_STATUS_OK, source_type: "Local".to_string(), } } @@ -1658,6 +1663,33 @@ mod tests { ); } + /// The run-02 scratch control isolates `-Package Any`: adding it to an + /// otherwise identical rule changes OK/[5,1] to Inactive/[5,20]. + /// An unrestricted bootstrap rule must omit that package binding entirely. + // [unit->REQ-WEB-LAN-BOOTSTRAP-FIREWALL] + #[test] + fn unrestricted_rules_omit_the_package_parameter() { + let want = desired_specs("c:/spt/spt.exe", 5470, &one_lan()); + let rendered = render_writes(&want); + let mut calls = rendered.split("New-NetFirewallRule").skip(1); + for spec in &want { + let call = calls + .next() + .expect("each admission half must have a create call"); + assert!( + call.contains(spec.name), + "the assertion must inspect the named half" + ); + assert!( + !call + .split_ascii_whitespace() + .any(|argument| argument == "-Package"), + "{} must not acquire the package restriction measured to disable admission: {call}", + spec.name + ); + } + } + // ---- observed-spelling regression cells (hertz, releases#304 W2) ---- // // Appended onto FOLD-4 (10d18b7f). Boundary agreed with todlando: everything @@ -1719,23 +1751,46 @@ mod tests { } } - /// H2 — AN UNENFORCED RULE IS STILL REFUSED, AFTER THE PAIR MATCHES. - /// - /// Enforcement is decided at its own `decide` arm, on a field the FOLD-4 - /// normalizers never see, and it returns `Err` rather than `Ok(false)`. No - /// sibling asserts it: the source-store arm has :1136, this one had nothing. - /// It exists so a comparison repair cannot fold enforcement into a WEAKER - /// comparison — a rule that is configured but not in force must stay a LOUD - /// refusal and not a quiet non-match. That intent is unchanged; only its - /// mechanism moved. It once guarded against folding enforcement into STRING - /// handling, because the field was Vec; the field is now Vec and - /// the live risk is equality silently becoming MEMBERSHIP, so the cases below - /// include a repeated success code and a success code beside a non-success one. - /// - /// No case here asserts a NAME for any code. The host that produced the A7 - /// capture rendered its codes as display text and exposed no Values qualifier, - /// so no code-to-name mapping is derivable from it; what makes 1 the success - /// code is the documented specification, asserted nowhere but in the constant. + // [unit->REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES] + #[test] + fn an_ok_multi_profile_rule_with_codes_5_and_1_is_enforced() { + let want = desired_specs("c:/spt/spt.exe", 29470, &one_lan()); + let mut active = reconciled_store(&want); + active[1].enforcement = vec![5, 1]; + let state = Snapshot { + active, + addresses: one_lan_census(), + }; + assert_eq!(decide(&state, "c:/spt/spt.exe", 29470), Ok(true)); + } + + // [unit->REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES] + #[test] + fn an_inactive_rule_with_codes_5_and_20_is_refused() { + let want = desired_specs("c:/spt/spt.exe", 29470, &one_lan()); + let mut active = reconciled_store(&want); + active[1].primary_status = 2; + active[1].enforcement = vec![5, 20]; + let state = Snapshot { + active, + addresses: one_lan_census(), + }; + assert!(decide(&state, "c:/spt/spt.exe", 29470).is_err()); + } + + // [unit->REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES] + #[test] + fn an_ok_rule_with_code_1_is_enforced() { + let want = desired_specs("c:/spt/spt.exe", 29470, &one_lan()); + let state = Snapshot { + active: reconciled_store(&want), + addresses: one_lan_census(), + }; + assert_eq!(decide(&state, "c:/spt/spt.exe", 29470), Ok(true)); + } + + /// A matching rule still needs positive enforcement and no code-20 veto. + /// Null/type faults remain a separate query/deserialization failure. // [unit->REQ-WEB-LAN-BOOTSTRAP-FIREWALL] #[test] fn an_unenforced_rule_is_refused_loudly_after_the_pair_matches() { @@ -1751,28 +1806,15 @@ mod tests { "control: the unperturbed snapshot decides reconciled" ); - // Every case is a REFUSAL, and the list is the requirement's acceptance - // table written as literals: [0], [2], [5], [20], [1,1], [1,5] and []. - // [5,20] is carried beyond that list because two non-success codes is a - // distinct shape from one. The arms that matter after the repair are the - // REPEATED success code and the success code accompanied by a non-success - // one — they fail only if `as_slice() != ENFORCEMENT_CERTIFIED` is ever - // softened into "contains the success code". - // - // WHAT THIS FILE CANNOT COVER, so that it is not read as covered: the - // requirement's null arms and its distinguishable REPRESENTATION FAULT - // live in the PowerShell query, before any JSON exists. Their Rust-side - // shadow is the deserialization cell below — a null element and a null - // field both refuse there; the faults themselves are the - // extraction-boundary exercise's subject. + // An empty or non-success array cannot certify admission. Code 20 vetoes + // even an otherwise positive array; inactive-profile code 5 alone is not success. // [unit->REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES] for status in [ vec![0u16], vec![2u16], vec![5u16], vec![20u16], - vec![ENFORCEMENT_SUCCESS, ENFORCEMENT_SUCCESS], - vec![ENFORCEMENT_SUCCESS, 5u16], + vec![ENFORCEMENT_SUCCESS, ENFORCEMENT_NO_LOCAL_USER], vec![5u16, 20u16], Vec::new(), ] { @@ -1787,6 +1829,16 @@ mod tests { (status {status:?}): {refusal}" ); } + let mut inactive = effective; + inactive[0].primary_status = 2; + let state = Snapshot { + active: inactive, + addresses: one_lan_census(), + }; + assert!( + decide(&state, "c:/spt/spt.exe", 29470).is_err(), + "a success code cannot override non-OK PrimaryStatus" + ); } @@ -1807,7 +1859,7 @@ mod tests { format!( r#"{{"name":"n","program":"Any","ports":["29470"],"profile":"Any", "remotes":["Any"],"hygiene":true,"enforcement":{enforcement}, - "sourceType":"Local"}}"# + "primaryStatus":1,"sourceType":"Local"}}"# ) } @@ -1859,14 +1911,12 @@ mod tests { // filters plus DynamicTarget and the Platform/Owner emptiness checks. Those // cmdlets were never run, so the inputs do not exist. `true` here ISOLATES // the representation axes; it does not assert what the host would report. - // - `enforcement`: SYNTHETIC. The capture reads {ProfileInactive, NoLocalUser} in - // ActiveStore. Those are the host’s ADAPTED DISPLAY NAMES, not codes, and THE - // BOOTSTRAP PAIR’S NUMERIC VALUES REMAIN UNKNOWN: the raw UInt16 1 was measured - // on a SEPARATE, pre-existing 5470 rule, not on either bootstrap rule. So nothing - // here may be read as "the pair really carried 1 and only rendered badly". - // ENFORCEMENT_SUCCESS is used because it keeps these cells about the COMPARISON - // axes; it asserts nothing about what that host would have reported. The captured - // value is a SEPARATE FINDING, reported to doyle, not smuggled into a fixture. + // - `enforcement` and `primary_status`: the September 12 display-name capture + // was not a successful pair. Window 6 subsequently measured its raw [5,20] + // refusal. These comparison fixtures now use OK/[5,1], measured on the + // Private+Domain scratch rule without -Package in run-02 (2026-09-14). + // It is deliberately COMPOSED evidence: not a new capture of these named + // bootstrap rules, and not a measured all-profile/tailnet array. // - the port: SYNTHETIC. Its captured value is not in hand. The port axis is not an // equivalence axis and R5 covers its drift, so the spec's port is used. @@ -1880,7 +1930,8 @@ mod tests { profile: "Any".to_string(), // captured remotes: vec!["100.64.0.0/255.192.0.0".to_string()], // captured — MASK form hygiene: true, // SYNTHETIC, not captured — see above - enforcement: vec![ENFORCEMENT_SUCCESS], // SYNTHETIC, not captured — see above + enforcement: vec![5, ENFORCEMENT_SUCCESS], // run-02 enforced-shape composition + primary_status: PRIMARY_STATUS_OK, source_type: "Local".to_string(), // captured } } @@ -1895,7 +1946,8 @@ mod tests { profile: "Domain, Private".to_string(), // captured remotes: vec!["192.168.1.0/255.255.255.0".to_string()], // captured — MASK form hygiene: true, // SYNTHETIC, not captured — see above - enforcement: vec![ENFORCEMENT_SUCCESS], // SYNTHETIC, not captured — see above + enforcement: vec![5, ENFORCEMENT_SUCCESS], // run-02 enforced-shape composition + primary_status: PRIMARY_STATUS_OK, source_type: "Local".to_string(), // captured } } diff --git a/docs-site/src/changelog.md b/docs-site/src/changelog.md index fe786e88..11c33765 100644 --- a/docs-site/src/changelog.md +++ b/docs-site/src/changelog.md @@ -46,8 +46,12 @@ flag, or page that no existing user can encounter without opting into it. where a redundant query hit the three-second limit before either rule was written. Elevated admission now verifies and derives any repair from one pre-write census; NetSecurity operations have a bounded 30-second allowance rather than the short - probe budget. Equivalent rule spellings and enforcement evidence remain supported, - and a failed post-write verification still says the rules may already be present. + probe budget. The sixth window then exposed a separate defect: explicitly writing + `-Package Any` left both rules inactive. Bootstrap now omits that package argument + and recognizes enforcement on active profiles without rejecting an inactive + companion profile. Non-OK rules, missing success evidence, and NoLocalUser + remain unverified. A failed post-write verification still says the rules may + already be present. - Unlisted-peer presence checks in endpoint listings use a 2.5-second timeout instead of 10 seconds. A peer that does not answer remains unknown, not offline. diff --git a/docs/297-ENFORCEMENT-CODES-DIAGNOSIS.md b/docs/297-ENFORCEMENT-CODES-DIAGNOSIS.md index cd104cb8..13523a72 100644 --- a/docs/297-ENFORCEMENT-CODES-DIAGNOSIS.md +++ b/docs/297-ENFORCEMENT-CODES-DIAGNOSIS.md @@ -3,8 +3,32 @@ **Subject:** `85f84d738fa702f35c83910f314aae17849d125c` **Evidence:** r10 field run `20260912T231404Z`, archived at `.spt/preserved/hertz-fp-driver-review/v9-exercises/r10-field-run-20260912T231404Z` -**Scope:** documentation and source reading only. No host probe, no rule recreation, no policy -change. **Strict `[1]` acceptance is unchanged.** +**Historical scope:** sections 1–5 record the September 12 documentation/source +investigation, without a host probe or policy change at that time. Its strict +`[1]` policy and unresolved hypotheses are superseded by the measured update below. +The original captures and quotations remain historical evidence. + + +## September 14 update — measured construction defect and corrected verdict + +Window 6 on S2 `527cd8e8` proved that the query-budget repair writes the pair, +but the pair remained `Inactive/[5,20]` and did not admit the LAN GET. +The 19-variant scratch run at +`.spt/preserved/297-enforcement-shape/scratch-probe/run-02/variants.json` +isolated **`-Package Any`**, not LocalUser: the four Package-bearing variants +were `Inactive/[5,20]`; all fifteen others were `OK/[5,1]`. Adding only Package +to the minimal policy reproduced the inactive state. Every variant was removed; +final and independent cleanup censuses found neither store retained the scratch rule. + +The corrected writer omits only that Package argument. Its omitted value reads +as empty through `Get-NetFirewallApplicationFilter`; hygiene now requires that +empty value instead of literal `Any`. The profiles do not change: on this Private-only host, +the Domain portion contributes inactive-profile code `5`. +Doyle's ruling replaces singleton equality with **raw PrimaryStatus=1 (OK), +code 1 present, and code 20 absent**. Null/malformed raw evidence, non-OK status, +missing success and any code 20 remain refusals. This supersedes the old array +interpretation, not the captured historical `[5,20]` refusal or the requirement +for a separate end-to-end field check. --- diff --git a/docs/adr/0059-lan-exposure-only-for-the-bootstrap-surface.md b/docs/adr/0059-lan-exposure-only-for-the-bootstrap-surface.md index 5f0a1283..00cb1dd2 100644 --- a/docs/adr/0059-lan-exposure-only-for-the-bootstrap-surface.md +++ b/docs/adr/0059-lan-exposure-only-for-the-bootstrap-surface.md @@ -171,6 +171,28 @@ budgets are not a strict end-to-end CLI deadline. The listener is bound and `LAN_BOOTSTRAP_UP` is printed before admission begins: slow policy work delays the admission verdict, not listener startup, and still does not prove remote reachability. + +**Package binding and multi-profile enforcement (2026-09-14, sixth-window repair).** +Window 6 completed the previously killed query and wrote both rules, but they +remained inactive (`PrimaryStatus=Inactive`, raw enforcement `[5,20]`); a remote +LAN GET failed while program-scoped positive-control ports admitted traffic. +The subsequent 19-variant scratch experiment isolated the explicit `-Package Any` +argument: adding only that argument to a minimal rule changed `OK/[5,1]` to +`Inactive/[5,20]`. All four Package-bearing variants were inactive; all fifteen +others were OK. The write now omits only `-Package Any`; other arguments and both +profile/remote scopes remain unchanged. NetSecurity reports the omitted application +filter's Package as empty, so read-side hygiene now requires an empty Package, +not the literal `Any` binding. + +The earlier exact-`[1]` requirement incorrectly refused a multi-profile rule on +a host with only Private interfaces. Per doyle's measured ruling, enforcement +requires raw `PrimaryStatus=1` (OK), code `1` present, and code `20` absent. +Inactive-profile code `5` beside success is allowed; the Domain profile is not +removed to hide it. Both fields retain typed raw-CIM transport; absent, null or +malformed evidence remains a query fault. A non-OK status or missing success +remains unverified, as does code `20` even beside success. This verdict is still +not an end-to-end reachability guarantee. + **Firewalld provisioning boundary (doyle, 2026-09-11):** a fresh named policy can only be created permanently; activating it requires reload, which discards unrelated runtime-only configuration. Bootstrap does not reload, silently create pending permanent diff --git a/traceable-reqs.toml b/traceable-reqs.toml index cbde0eda..fac977a3 100644 --- a/traceable-reqs.toml +++ b/traceable-reqs.toml @@ -7688,8 +7688,8 @@ required_stages = ["impl", "unit"] # ACTIVATED #304 W2 representation repair (t [[requirements]] id = "REQ-BOOTSTRAP-FIREWALL-ENFORCEMENT-CODES" -title = "THE ENFORCEMENT VERDICT READS RAW NUMERIC EVIDENCE AND NEVER A DISPLAY SPELLING (releases#297 / #304 W2, doyle ruled 2026-09-12 from a measured capture). MEASURED ON HFENDULEAM, Windows 11 Pro 26200, UNDER THE PRODUCT'S OWN INVOCATION MODE (powershell.exe 5.1.26100.8875, -NoLogo -NoProfile -NonInteractive -EncodedCommand): the ActiveStore rule's EnforcementStatus raw element is System.UInt16 1, and EVERY adapted accessor -- $rule.EnforcementStatus, and the product's own `Value` helper reading $object.PSObject.Properties[name].Value which is what the query used -- renders that element as the String \"Enforced\". The gate compared adapted strings against [\"Full\"], so THE SUCCESS VALUE ITSELF WAS REFUSED and no firewall state on this host could satisfy the arm. THE REPAIR IS THE ACCESSOR, NOT A CAST: casting the adapted value throws on \"Enforced\", so the query must transport the raw CIM element from $rule.CimInstanceProperties[name].Value. WHAT THE EVIDENCE DOES NOT ESTABLISH, and no diagnostic or test may assert it: the code-to-NAME mapping. The captured class exposes ValueMap (0..25) with the Values qualifier ABSENT, so no name is derivable from this host for ANY code, 1 included -- \"Full\" is a published-documentation name, not an observed one, and the codes seen on the bootstrap pair remain unresolved. STRICT ACCEPTANCE IS THE REQUIREMENT, NOT A DETAIL: the evidence certifies ONLY the exact single-element array [1]. An empty array, a missing or null property, a non-numeric element, an unknown code, a DUPLICATED success code ([1,1]) and any mixed array carrying 1 beside another value ([1,5]) ALL REFUSE. Equality does not become membership: a success code among others is not a success. What a MULTI-ELEMENT array means is UNRESOLVED -- the documented values describe individual codes and say nothing about how several together should be read -- so the policy refuses rather than interpreting, and no reading of array semantics is asserted here. NULL IS AN EXPLICIT ARM (doyle, same day): direct CIM access MAY YIELD NULL RATHER THAN THROW, and a null -- as the property, inside the array, or beside a code -- must never serialize as valid success evidence. A REPRESENTATION FAULT REFUSES DISTINGUISHABLY FROM A WRONG CODE, so a regression is recognisable on sight rather than inferred. Narrow to REQ-WEB-LAN-BOOTSTRAP-FIREWALL, which owns the admission pair and the scope policy; this row owns only HOW enforcement evidence is transported and judged, and it does not touch the spec match, the source store or the LAN scope arms. Gate: impl -- the raw-element accessor in the query, the numeric field on the effective rule, the exact-[1] comparison, and a refusal naming the observed codes without naming any code. unit -- the strict acceptance table written as literals ([1] certifies; [], [0], [2], [5], [20], [1,1], [1,5] refuse), the four null arms, malformed transport surfacing as refusal and never as success, and a representation fault refusing distinguishably." -required_stages = ["impl", "unit"] # ACTIVATED #304 W2 representation repair (todlando 2026-09-12) on doyle's KLPE4J6K contract, linked to the parent REQ-WEB-LAN-BOOTSTRAP-FIREWALL rather than folded into it -- filing enforcement-code semantics under the spelling row would put it beneath a title about spelling, which is the framing that produced the defect. MINTED AND ACTIVATED IN ONE STEP because this lane delivers it. EVIDENCE: .spt/preserved/304-w2-attempt2-20260912/repr-capture/ (subject.out, accessor.out) -- raw UInt16 1 rendering \"Enforced\" through all three accessors, with a RULE_MISSING negative control proving the query can fail. SPLIT OWNERSHIP (doyle): impl is todlando's, unit is hertz's evidence-derived suite landing on top of this tip, so the unit stage is RED BETWEEN THE TWO COMMITS BY CONSTRUCTION and that redness is the coordination, not a regression. SEPARATE AND NOT CLEARED BY THIS ROW: the bootstrap pair's own {ProfileInactive, NoLocalUser} codes are unexplained and unresolved; this row makes the success value readable and says nothing about why those appeared. +title = "THE ENFORCEMENT VERDICT READS RAW NUMERIC EVIDENCE, NEVER DISPLAY SPELLINGS (releases#297 / #304 W2). Raw UInt16 EnforcementStatus 1 renders as Enforced on HFENDULEAM, not the documentation's Full; read CimInstanceProperties, not adapted values or coercions. AMENDED BY DOYLE 2026-09-14, run-02 scratch matrix: a Private+Domain rule without an explicit Package argument reports PrimaryStatus OK and EnforcementStatus [5,1] on this Private-only host; Package Any changes it to Inactive/[5,20]. Exact-[1] acceptance is superseded. ENFORCED iff raw PrimaryStatus is 1 (OK), EnforcementStatus contains 1, and EnforcementStatus does not contain 20 (NoLocalUser). Inactive-profile code 5 alongside success is not a refusal; profile scope is unchanged. Empty arrays, arrays without 1, any array containing 20, and non-OK PrimaryStatus refuse. Missing/null properties, null elements, non-numeric values, wrong CIM types and out-of-range transport remain distinguishable representation faults; never coerce them into success. This row owns transport and enforcement interpretation only: scope, hygiene, persistent source, ownership and LAN derivation remain the parent REQ-WEB-LAN-BOOTSTRAP-FIREWALL contract. Unit evidence includes OK/[5,1] and OK/[1] accepted, Inactive/[5,20] refused, code-20 veto even beside 1, non-OK status despite 1, and malformed numeric transport refused." +required_stages = ["impl", "unit"] # Amended fix-2 (todlando, doyle 73J2YTOV/F4DPMCQ5). Evidence: .spt/preserved/297-enforcement-shape/scratch-probe/run-02/variants.json; historical raw-accessor capture remains under .spt/preserved/304-w2-attempt2-20260912/repr-capture/. [[requirements]] id = "REQ-ATTACH-CLIENT-STALE"