# #297 — bounded out-of-band capture plan for a newly created pair

**Status:** prepared, **not admitted**. Source-only preparation. **No rule creation, host query,
elevation or execution is authorized by this document.** Author: todlando. Gater: doyle. **Rig owner:
hertz** — the capture and cleanup rig is his; §5 is the field list to coordinate with him.

**What this cannot do, stated first because it is the thing most likely to be forgotten:** the r10
pair was torn down and `TEARDOWN_VERIFIED` measured the owned names **absent**. **This plan cannot
recover the deleted instances' state.** Any evidence it produces comes from a **new pair** — a new
subject, with its own provenance, whose codes may or may not match `[5, 20]`. A different result here
would not refute the r10 observation, and a matching result would not prove the r10 instances carried
the same cause.

---

## 1. Documented semantics, established before interpreting anything

Per doyle's instruction to establish semantics first. From `New-NetFirewallRule` (NetSecurity):

**`-LocalUser`** — *"Specifies the principals to which network traffic this firewall rule applies…
The principals, represented by security identifiers (SIDs) in the security descriptor definition
language (SDDL) string… This parameter specifies that only network packets that are authenticated as
coming from or going to a principal identified in the list of accounts (SID) match this rule.
**Querying for rules with this parameter can only be performed using filter objects. See the
Get-NetFirewallSecurityFilter cmdlet.**"* — Type `String`, **Default value: `None`**.

Three consequences:

1. **`LocalUser` is not readable from the rule object**, and the documented query route is a filter
   object. So `Get-NetFirewallSecurityFilter` is **required** — that is why the r10 capture has no
   local-user field. **This does not mean production could never collect it**: a projection could
   join associated-filter data. **We are CHOOSING out-of-band capture** for a diagnostic, not
   declaring production collection impossible. (An earlier draft of mine said no projection change
   could supply it; that overstated the constraint and is corrected here.)
2. The parameter's documented shape is an **SDDL string**, and its default is `None`. `Any` is **not
   listed** among documented values for it — in contrast with `-Profile`, whose accepted values are
   enumerated explicitly.
3. **But the write was accepted.** `render_writes` passes `-ErrorAction Stop`, and the
   `reconcile-write` leg completed. So `-LocalUser Any` was **not rejected**. This is an
   accepted-argument-with-unverified-interpretation question, **not** a rejected-argument one, and
   the plan must not be written as though the cmdlet complained.

**`-Profile`** — *"Specifies one or more profiles to which the rule is assigned. **The rule is active
on the local computer only when the specified profile is currently active.** This relationship is
many-to-many… **Only one profile is applied at a time.** The acceptable values… are: Any, Domain,
Private, Public, or NotApplicable. The default value is Any."*

So profile activity is a property of **which profile the host currently has active**, and the
rule-side value alone cannot settle it. `-Profile Any` versus `InactiveProfile` remains an
**unresolved relationship** — this documentation explains what the relationship *is*, not which side
is wrong.

**What remains undocumented:** whether `Status` / `StatusCode` / `PrimaryStatus` explain any
particular `EnforcementStatus` element. The class documentation describes them as "the detailed
status of the rule" as a string and as a numeric error code, and **does not promise a per-element
mapping**. They are collected as *potentially useful diagnostics*, not as a guaranteed decoder.

## 2. Subject and mechanism — reuse, no new build

| item | value |
| --- | --- |
| executable | the **repaired** field executable already built — sha256 `72d309011415b419aabfdb0b4065df1183bfd0069a9f95b5779da62e43349b10` (per the r10 setup receipt, equal to `s5-provenance.out`) |
| source | subject `85f84d738fa702f35c83910f314aae17849d125c` |
| pair creation | the **existing scoped setup mechanism** — the same handoff-issued `serve lan --bootstrap` the r10 run used. **No new build. No new rule-writing code.** |
| teardown | the **existing scoped teardown mechanism**, plus §6 |

**The capture is out-of-band.** It is a separate read against the two owned names, **not** a change
to the product's query projection — collecting diagnostics is not a reason to alter what production
reads.

## 3. Explicit identification of the created pair

**`InstanceID` MUST NOT be assumed unique across deletion and recreation.** A recreated rule may
reuse an identifier a deleted one carried, so `InstanceID` is recorded as an observation, never as
the binding key.

**Records bind to a five-part identity instead:** the **run**, the **measured pre-setup absence** of
both names, the **store** the record was read from, the rule **name**, and the **setup interval**.

**Creation is recorded as the setup command's start/end INTERVAL** — its issue time and its return
time — **unless the provider supplies an actual creation timestamp**, in which case that value is
recorded as well and labelled as the provider's. No instant is invented between those two bounds.

Also carried into every artifact: both rule **names**, the **port**, and the **isolated home**. Every
captured record names which rule, which store and which run it describes. A capture that cannot state
that is discarded.

## 4. Finite bounds

| budget | value |
| --- | --- |
| setup + identification | 120 s |
| capture (all fields, §5) | 180 s |
| **experimental total, from run start** | **300 s** |
| cleanup **reporting** deadline (separate, begins when experimental activity ends **for any reason**) | **180 s** |
| **total bound on ACTIVITY AND REPORTING** | **480 s** |

**The 180 s cleanup bound is a REPORTING deadline, not a guarantee that teardown completed.** If
cleanup cannot finish within it — in particular if a **fresh pid-naming authorization is required**,
which §6 expects — then: stop experimental activity, **preserve every process identity and
artifact**, and **report CLEANUP PENDING with those identities**. **The run is NOT to be called clean
at 480 s.** A reporting deadline expiring means the report is due, never that the host is tidy.

The experimental deadline **ends activity and starts cleanup — it never skips it.** Any phase cut
short by the remaining budget is **reported as truncated**, naming the phase and the time it got; a
truncated phase's absence of a field is "not captured", never "not present".

## 5. Required fields — the list to coordinate with hertz

Captured **raw**: numbers as numbers, strings as strings, no host-rendered names substituted for
codes, no field omitted because it looked empty.

### 5.a Encoding — values keep their type and cardinality; status is recorded separately

**A single "NULL" state is REJECTED.** It collapses four facts this capture exists to tell apart. An
earlier draft of mine adopted it; that was wrong and is corrected here rather than quietly dropped.

**Values preserve type and cardinality, exactly as returned:**

| returned | recorded as | NOT to be conflated with |
| --- | --- | --- |
| a present empty string | `""` | `null`, `[]`, or ABSENT |
| a present empty array | `[]` | `""`, `null`, or ABSENT |
| a present null | `null` | `""`, `[]`, or ABSENT |
| a single numeric element | a **one-element array**, if the property is array-typed | a bare scalar |
| several elements | the array, in order | a joined string |
| the provider did not expose the property | **explicit `ABSENT`** | any of the above |

**Guard against PowerShell pipeline collapse.** The shell will unwrap a one-element array to a scalar
and an empty array to `$null` if allowed to. **The serializer must preserve the declared type and the
element count** — `EnforcementStatus` is `uint16[]`, so `[5, 20]`, `[1]` and `[]` must serialize as
arrays of two, one and zero elements respectively, and never as `5,20`, `1` or `null`.

**Status, errors and elevation are recorded SEPARATELY from values — never encoded into them:**

| status | means |
| --- | --- |
| `DENIED` | the producing query returned an **explicit access-denied error**. Nothing else earns this label. |
| `ERROR` | any other query failure, with the error recorded verbatim |
| ok | the query succeeded — and a **successful empty result is an empty result**, recorded as the value it returned |

**A successful empty result is NOT evidence of absence, and its COMPLETENESS IS UNPROVEN** wherever
the provider can silently filter what it returns. Emptiness is recorded as what it is; it licenses no
claim that nothing exists.

**`DENIED` is never inferred from a value.** On this box a denied firewall read is measured to render
as a clean zero, so deriving denial from emptiness would relabel refusals and reproduce the trap the
status exists to catch. The producing query's own error decides it.

Every record carries, alongside its value: the producing command, its exit status, any error
verbatim, whether the query was elevated, and its capture instant.

### 5.b Amendments adopted from the instrument spec

- **Route, not enumeration.** Every filter object is fetched **through its associated rule**, never
  through an `-All` enumeration — for the same clean-zero reason.
- **Provider-qualifier probe.** A `Get-CimClass` read of `MSFT_NetFirewallRule` in
  `Root/StandardCimv2`, recording whether `EnforcementStatus`'s `Values` / `ValueMap` qualifiers are
  present **on this host's provider**, and their contents if so. This settles — or records as
  unsettleable — the provider-agreement question the diagnosis marks unverified. **It licenses no
  naming either way**: the documented mapping stays documentation-derived.
- **Controls are mandatory:** a positive control (a standing rule, read-only, same property set, same
  route, same process — **a property `ABSENT` on both is an instrument limit, not a finding about the
  pair**), a negative-control name proven absent, the elevation pair, and the qualifier probe.
- **One process, one JSON per capture point** — a reasonable shape, adopted. **The elapsed-time
  bounds in §4 remain binding regardless.** r10's product query legs measured 1747–2253 ms each, but
  **that does NOT establish that process start and module load dominated them** — nothing decomposed
  those figures, and an earlier draft of mine repeated that attribution without challenge. The budget
  is a bound to be met, not a quantity explained away.

**Per rule, from the rule object:**
- `Name`, `DisplayName`, `InstanceID`, `Group`
- `Enabled`, `Direction`, `Action`, `EdgeTraversalPolicy`
- **`Profiles`** — the raw `uint16` bitmask, **not** the rendered string
- **`EnforcementStatus`** — the raw `uint16[]`, verbatim
- **`PrimaryStatus`** (`uint16`), **`Status`** (`string`), **`StatusCode`** (`uint32`)
- `PolicyStoreSourceType`, `PolicyStoreSource`, `Owner`
- read from **both** the ActiveStore **and** the PersistentStore, **each labelled with the store it
  came from**. The write targeted PersistentStore while `EnforcementStatus` is documented against an
  ActiveStore retrieval, so the two reads are not interchangeable and a difference between them is
  not by itself a defect.
  **PRESERVE WHATEVER PERSISTENTSTORE RETURNS, AND ASSIGN IT NO ENFORCEMENT MEANING.** The
  documentation qualifying the field to an ActiveStore retrieval does **not** establish that an
  absent or empty value is the *expected* PersistentStore result — an earlier note of mine said
  exactly that and it was wrong. Record the returned value, whatever it is, without interpreting it.
- `PolicyStoreSource` / `PolicyStoreSourceType` are the **returned property names**, and they are
  populated only on an ActiveStore retrieval with the trace switch set. **THE CMDLET PARAMETER IS
  `-TracePolicyStore`** — a `SwitchParameter` on `Get-NetFirewallRule`, documented as *"Specifies
  that the name of the source GPO is queried and set to the PolicyStoreSource parameter value."*
  (The class reference describes the same thing in prose as "the TracePolicyStoreSource option"; that
  is not the parameter's spelling, and an earlier draft of mine used the prose form as if it were.)
  **Use `-TracePolicyStore` where those fields are wanted, and RECORD that it was used** — a value
  collected without it and one collected with it are different observations.

**Per rule, from filter objects** (the documented route):
- **`Get-NetFirewallSecurityFilter`** → `LocalUser`, `RemoteUser`, `RemoteMachine` — **raw SDDL
  strings**, exactly as returned, including empty or null
- `Get-NetFirewallAddressFilter` → `LocalAddress`, `RemoteAddress`
- `Get-NetFirewallPortFilter` → `Protocol`, `LocalPort`, `RemotePort`
- `Get-NetFirewallApplicationFilter` → `Program`, `Package`
- `Get-NetFirewallInterfaceFilter` / `-InterfaceTypeFilter`, `Get-NetFirewallServiceFilter`

**Host state, captured in the same window:**
- **every firewall profile's state, preserved separately** — `Enabled` and settings per profile
  (Domain, Private, Public), recorded as three values, **never collapsed into one host value**
- **every interface's network category**, per interface, for **all** interfaces — not only the ones
  carrying `192.168.1.x` and the tailnet address
- **Do NOT require, or record, a single "currently active profile".** The host does not have one
  global answer: network category is per-interface and firewall state is per-profile. Collapsing them
  is how a per-interface fact would be lost and a false host-wide claim manufactured.

**Provenance for every record:** the capture instant, the querying command, its exit status, and
whether the query was elevated. A field with no recorded query is not evidence.

### 5.0 The elevated capture must ride the setup request — one nonce is one execution

**Accepted from hertz's instrument spec, and recorded here because it is a property of the REQUEST,
not of the rig.** The capture point taken immediately after the pair is written is the **only
elevated read available**, and **one nonce is one execution**: an elevated read cannot be added after
the setup handoff has been answered without a *second* handoff and a second nonce.

**Therefore the seq-001 setup request MUST name the elevated capture and its output path, in the same
request that creates the pair.** It costs one line in the request and **no extra nonce**.

**If it is omitted, the elevated capture is UNAVAILABLE — and that is an EVIDENCE GAP to be reported
explicitly, never a licence to report the capture contract as satisfied.** A run missing that point
did not collect the contract's elevated observation; it collected a smaller set. The report names
which point was unavailable and why, and **no downstream conclusion may rest on the missing
observation.** Omission is a hole with a label, not a pass.

**The two exit statuses stay separate.** The elevated setup's **native exit** and the capture's
**native exit** are recorded as **two independent values**. Neither overwrites the other and neither
stands in for the other: a capture that fails inside a setup that succeeded must not read as a failed
setup, and a setup that fails must not be masked by a capture that ran. Each is reported with its own
command and its own status.

### 5.1 Prerequisites — three separate grants, all currently spent

**These are distinct authorizations and none substitutes for another:**

| grant | r10's form | status |
| --- | --- | --- |
| elevated **setup** | nonce-bound | **SPENT** |
| elevated **teardown** | nonce-bound | **SPENT** |
| **residual-process** cleanup | pid-specific | **SPENT** |

A fresh **elevation** authorization and a fresh **residual-process** authorization are **separate
prerequisites**, requested and granted independently. **No launch until the new plan, this rig and
the cleanup path are all admitted.** liam's availability is not to be assumed, and no step proceeds
on the expectation that a handoff will be answered.

## 6. Cleanup — IR-122's procedure, not a pid-file kill

**A bare pid-file kill is not approved as sufficient.** `<isolated-home>/daemon.pid` is the pid
**source**, never the authorization. At the moment of acting, and re-read **fresh**:

1. Identify each process on **pid + `CreationDate` + exact `ExecutablePath` + `CommandLine`** — a
   bare pid match is unsound; pid reuse on this box is measured.
2. Confirm the **supervisor still equals** `<isolated-home>/daemon.pid`, and the **brain's parent is
   that supervisor**.
3. Confirm **isolated-home association** — never the resident node daemon, which is a **different
   binary** under `AppData\Local\spt-core\bin\`.
4. A **fresh authorization naming the new pids** — the r10 authorization was pid-specific and is
   **spent**.
5. **Supervisor first**, so no child respawns. `Stop-Process -Id` alone — **never** a tree kill,
   **never** an image-name kill.
6. **Assert** the post-state; do not announce it.
7. **Any mismatch or unreadable identity STOPS the action** and is reported.

**Expect the known refusal.** Per IR-122, the rig's isolated-daemon stop is structurally unrunnable
from an spt-hosted session: `DAEMON_STOP_REFUSED` fires on the caller's `OWL_SESSION_ID`, regardless
of which `SPT_HOME` is targeted, and no flag overrides it. That refusal is **reported, never worked
around**. If it leaves a live daemon, the residue is reported **as an identity the next consumer can
act on — pid, executable path, command line** — not merely as a failed step, because the residue
lands in a shared build pool and is paid for by whoever builds next.

**Rules:** the created pair is removed by the existing scoped teardown, and its absence **measured**,
exactly as r10 did. **Evidence is retained, never deleted.**

## 7. What this can and cannot discriminate

| capture outcome | reads as |
| --- | --- |
| security filter shows `LocalUser` as written, ActiveStore still reports `LocalUserEmpty` | the write did what it said — **provider/host state or an unmodelled semantic** |
| security filter shows `LocalUser` empty/null | **the stored value differs from the argument** — *not by itself* a construction failure; empty may be the provider's representation of unrestricted access, which §1 has not settled |
| host's applied profile excludes the rule's `Profiles` bitmask | `InactiveProfile` is **descriptive of host state** |
| `Profiles` bitmask disagrees with the written `-Profile` | **the stored value differs from the argument** |
| ActiveStore and PersistentStore disagree on a field | a **store-projection** difference, not a defect |

**None of these rows is adopted as an interpretation.** Each is what the *capture* would show; what
it *means* needs the semantics in §1 completed — in particular, **whether an empty stored `LocalUser`
represents unrestricted access is not yet established**, and until it is, an empty value proves
nothing about construction.

**Strict `[1]` acceptance is unchanged.** The settled network scope is unchanged. Nothing in this
plan proposes a policy change, and its most likely useful outcome is a better-posed question rather
than a verdict.
