# #297 — D2, the out-of-band capture instrument: shape, controls and ordering

**Status:** prepared, **not admitted**. Instrument specification only. **No rule creation, host
query, elevation, build or execution is authorized by this document.** Author: hertz (rig owner).
Gater: doyle. Field contract: `docs/297-OUT-OF-BAND-CAPTURE-PLAN.md` (todlando) — **that document
owns the field list and the budgets; this one owns how the fields are collected and what makes the
collection readable.** Where the two must agree on a field name, todlando's §5 is the source and this
document quotes it.

Diagnosis under investigation: `docs/297-ENFORCEMENT-CODES-DIAGNOSIS.md` (`fd87e88f`). Field contract
pin this document is written against: **`e6c2066a`** (predecessor `ecbf1708`), which already carries
doyle's four plan corrections and two checkpoint corrections. Evidence baseline: r10 field run
`20260912T231404Z`.

**The premise of this document, stated first.** The r10 run already ran a diagnostic capture before
teardown (`D1(setup-rejection)`, `findings.txt:87-88`), and it still came back unable to discriminate.
It was not a timing failure and not a preservation failure. **D1 is rendered from the product's own
`QUERY` const** (`d1_render.py`, composed blob `848a23fe`) — which is exactly what makes it valuable,
and exactly why it projects the same eight fields the product projects, and no others. The rig
inherited the blind spot of the thing it was studying. **D2 exists because the fix is a second,
differently-sourced instrument — not an edit to the first.**

---

## 1. Lineage — what is frozen and what moves

| item | value | moves? |
| --- | --- | --- |
| subject | `85f84d738fa702f35c83910f314aae17849d125c` | **frozen** |
| subject blob (`bootstrap_firewall/windows.rs`) | `848a23fe18e5ca819774f9881ef13504221992f3` | **frozen** |
| field executable | `72d309011415b419aabfdb0b4065df1183bfd0069a9f95b5779da62e43349b10` | **frozen — reused, not rebuilt** |
| driver | r10 `a78f4ba6` | **frozen as the baseline**; D2 admission produces r11 |
| `d1_render.py` / D1 output | derived byte-exact from the subject blob | **unchanged — do not edit** |
| `census.ps1`, `portfields.ps1`, `runner-census.ps1`, `cpubracket.ps1` | r10 instruments | **unchanged** |
| **`d2_capture.ps1`** | this document | **new file, additive** |

**No product code changes.** D2 reads; it does not alter what production queries. Adding a diagnostic
is not a reason to widen the product's projection, and a wider product projection would not be
evidence about the r10 pair anyway.

**This is a choice, not an impossibility — doyle's correction, recorded here so this document does not
carry the overstatement either.** A production projection *could* include associated-filter data;
nothing makes `LocalUser` structurally uncollectable by the product. What is true is narrower: it is
**not readable from the rule object**, so the documented route is a filter object either way. We are
**choosing out-of-band capture** over widening production for a diagnostic. That choice is revisitable
on evidence; "production cannot collect it" is not a fact and is not claimed.

**Instrument-set integrity is preserved the way r10 preserved it:** `d2_capture.ps1` is hashed into
`INSTRUMENTS.sha256` alongside the existing five, and its outputs into `MANIFEST.sha256`. The r10
archive is not regenerated, amended or re-verified — its MANIFEST `dfe32a59…` stands as doyle
verified it.

## 2. Shape — one process, one JSON, one instant

**All of todlando's §5 in a single PowerShell invocation**, emitting one JSON document per capture
point. Not one invocation per field, and not one per filter cmdlet.

One reason, and it is about evidence rather than cost: **a capture that is twenty processes has
twenty instants and no single one to cite.** One process yields one `utc`, recorded **inside the
document it describes**, and one elevation state for every record in it.

**A cost argument I made here is withdrawn.** An earlier revision said r10's `wall_ms` 1747–2253 legs
were *dominated by process start and module load*, and concluded the budget could absorb one process
but not twenty. **Nothing decomposed those figures** — that was an explanation of a measurement
offered as though it were the measurement, and todlando repeated it on my authority before doyle
refused it. The elapsed-time bounds in the field contract's §4 **remain binding as bounds to be met**,
and one process per capture point stands on the instant-and-cardinality argument alone.

The capture emits, per capture point:

- `utc`, `elevated` (the querying context's own elevation, measured not assumed), `capture_point`
  (`t1|t2|t3`), `instrument_sha256`, and the two owned rule names plus the port it was told to read;
- `rules[]` — todlando §5 per-rule fields, read from **both** stores, each record labelled with its
  store and carrying the `InstanceID` §3 of the field contract requires;
- `filters[]` — todlando §5 filter-object fields, obtained through the associated-rule route;
- `host[]` — todlando §5 host state, **as two arrays and never as a scalar** (doyle): one record per
  network interface carrying its `NetworkCategory`, and one record per firewall profile carrying its
  `Enabled` state. **No "the active profile" field is emitted.** The documentation's "only one profile
  is applied at a time" is about rule application; collapsing the host's per-interface categories into
  one value would discard the very state `InactiveProfile` has to be read against;
- `controls{}` — §3 below;
- `errors[]` — every non-terminating error, with the command that raised it.

**Values preserve type and cardinality; status is recorded separately from values.** This replaces an
earlier four-state encoding of mine (`MEASURED` / `NULL` / `ABSENT` / `DENIED`), which **collapsed a
present empty string, a present empty array and a present null into one `NULL`** — destroying exactly
the distinctions this capture exists to make. doyle refused it; the encoding below is his, and is the
one implemented.

| state | means |
| --- | --- |
| `PRESENT` | the provider exposed the property; `value`, `type`, `is_array` and `cardinality` recorded as returned |
| `ABSENT` | the provider did not expose the property at all |
| `DENIED` | the producing query raised an **explicit access-denied error** |
| `ERROR` | the producing query failed for any other reason; the error is recorded verbatim |
| `NO_RULE` | the query succeeded and returned no rule, so there was no object to have properties |

So a present empty string is `""`, a present empty array is `[]`, and a present null is `null` — three
different facts, none interchangeable, none collapsible. `EnforcementStatus` is `uint16[]`, so `[5,20]`,
`[1]` and `[]` serialize as arrays of two, one and zero elements — **never `5,20`, never `1`, never
`null`**. PowerShell unwraps a one-element array to a scalar and an empty one to `$null` if allowed
to, and the field this whole investigation turns on is the one most at risk from it.

**`DENIED` is decided by the query's own error, exit and elevation — never by a value.** A denied
firewall read on this box is measured to render as a clean zero at the value site, so inferring
denial from emptiness would relabel refusals as ordinary values and rebuild the trap the status
exists to catch. **A successful empty result stays an empty result**, and its completeness is
**unproven** wherever the provider can silently filter what it returns.

The r10 census could tell none of these apart, and every one of them read as an ordinary value. This
is the whole reason a capture is worth running twice.

### 2.1 Store qualification — preserved raw, given no meaning

The class documentation qualifies `EnforcementStatus` as describing enforcement **"if this object is
retrieved from the ActiveStore"**, and conditions `PolicyStoreSource` / `PolicyStoreSourceType` on an
ActiveStore retrieval **with `-TracePolicyStoreSource`** (todlando, and the reason a naive two-store
comparison would manufacture a finding out of a documented projection difference).

**How D2 encodes that, after doyle's correction:** the capture carries, per `(property, store)`, one
flag — **whether the documentation defines that property's meaning for that store**. That flag governs
*reading*, never *collecting*.

- Whatever the PersistentStore returns is **preserved verbatim**, in whichever of the four states it
  arrives. An absent or empty value there is **not** recorded as expected, because *undefined* does not
  establish *absent* — it establishes that the value carries no enforcement meaning, which is a
  different fact and the only one D2 asserts.
- No store-to-store difference in an undefined-for-that-store property is reported as a discrepancy,
  and none is reported as confirmation either.
- `-TracePolicyStoreSource` is used where the documentation requires it, and **whether it was used is
  recorded on the record it produced**. A source field captured without it is labelled as such.

### 2.2 Record identity — bound to the run, not to `InstanceID`

Per doyle: **`InstanceID` must not be assumed unique across deletion and recreation.** It is collected
(todlando §3 requires it and it may still discriminate two live instances), but it is **not** the key.

Every record is bound by: **run id · the measured pre-setup absence of the owned names · store · rule
name · the setup command's start/end interval**. **Creation is that interval**, unless the provider
supplies an actual creation timestamp, in which case the timestamp is recorded *and the interval is
kept beside it*. A record that cannot state all five binding components is discarded, which is the
r10 rule (`a capture that cannot say which instance it read is discarded`) with a key that survives
recreation.

## 3. Controls — without these the capture is not evidence

**A firewall read that returns nothing looks identical whether the rule is absent, the property is
unexposed, or the call was denied.** This box has already produced that failure:
`Get-NetFirewallPortFilter -All` returns Access-denied unelevated and renders as a blanket zero.
Four controls, all inside the same capture document, all mandatory:

1. **Positive control — a rule known present.** The standing 5470 guard rule is read with the **same
   property set, through the same route, in the same process**. Every property that comes back
   `MEASURED` for the pair must also come back `MEASURED` for the control; a property that is
   `ABSENT`/`DENIED` on **both** is an instrument limit, not a finding about the pair. Read-only — the
   existing `portfields`/guard-diff arms continue to assert 5470 UNCHANGED at every checkpoint,
   unmodified.
2. **Negative control — a name proven absent.** One query for a name that cannot exist
   (`spt-core-bootstrap-inbound-tcp-d2-absent-control`). It must return zero rows **and** a recorded
   non-terminating error. A negative control that returns the same clean nothing as a denied query
   means the capture cannot report absence at all, and the run says so instead of reporting an
   absence.
3. **Elevation control — two reads, not one assumption.** The rig runs unelevated
   (`ELEVATION self=False`, r10 `findings.txt:7`). Querying by name is expected to succeed there;
   **that expectation is untested on this host and is not to be assumed.** So the capture runs
   **both** unelevated from the rig **and once inside the elevated setup leg** (§4), and each document
   records its own `elevated`. If the two disagree on any property's state, the disagreement is the
   finding and the unelevated value is not cited alone.
4. **Enumeration control — no name is asserted for a code.** One `Get-CimClass` read of
   `MSFT_NetFirewallRule` in `Root/StandardCimv2`, recording whether the `EnforcementStatus`
   property's `Values` / `ValueMap` qualifiers are present on **this host's provider**, and their
   contents if so. The r10 capture found the `Values` qualifier absent, which is why the codes
   arrived nameless and why todlando's mapping is documentation-derived with provider agreement
   **unverified**. This control settles that one question, or records that the host cannot settle it.
   It does not license naming a code from the published table either way.

**Representation check, same rule as the product's.** Codes are recorded as numbers. If the transport
yields strings, the capture reports a representation fault **of its own** and does not silently
coerce — the product has a distinct refusal surface for exactly this
(`ENFORCEMENT_REPRESENTATION_FAULT`), and an instrument that launders what the product refuses is
worse than no instrument.

## 4. Ordering — the capture points, and the one that needs a handoff line

r10's chronology gap is on record and is not to be repeated: there is **no D1 `START`/`END` row in
`timeline.txt`**, so the two observations of `[5, 20]` are bounded by **artifact mtimes ~109 s apart**,
not by measured instants. D2 emits `d2_t<N>_START` / `d2_t<N>_END rc=<n>` rows into `timeline.txt` on
the same UTC clock as every other leg, and carries its own `utc` in the document.

| point | when | why it exists |
| --- | --- | --- |
| **t1** | immediately after the elevated setup writes the pair, **inside the elevated leg** | the only elevated read available; the state closest to the write |
| **t2** | at the refusal, **beside D1**, before any teardown | the state the refusal describes; D1 and D2 are then two differently-sourced views of one instant |
| **t3** | immediately before the teardown request is written | bounds "did the codes clear?" between **measured instants** instead of mtimes |

**t1 requires a line in the setup handoff request — this is the coordination point.** One nonce is one
execution; there is no adding an elevated read afterwards without a second handoff. Either the seq-001
setup request names `d2_capture.ps1` and its output path, or the elevated arm does not exist for that
run. It costs one line in the request and no extra nonce. **Recommend it be in the request.**

t1↔t3 bounds persistence and nothing more. Three observations of `[5, 20]` are three observations:
**nothing observes the intervals**, and a stable reading across them is not proof the state never
cleared — the same limit todlando records in §3 of the diagnosis, unchanged by measuring the instants
better.

## 5. Preservation

Every D2 document is written **before teardown** and preserved with the run: raw JSON, a b64 sidecar,
and a sha256 into `MANIFEST.sha256`. **Written in binary** — no text-mode write anywhere in the
instrument or its post-processing. A text-mode write on this box has already flipped a whole file's
line endings and converted lone `CR` bytes into newlines while `git diff --stat` showed a benign
count (IR-121, instance 4, mine). Diff **hunks**, never insertion counts.

A truncated capture is preserved as truncated, naming the point and the budget it got, per the field
contract's §4 — **an uncaptured field is "not captured", never "not present"**, and that string is the
one the four-state table in §2 exists to keep honest.

## 6. Cleanup mechanics — a deadline that reports, not one that finishes

Cleanup mechanics are mine; the field contract's §6 procedure (fresh identity on pid +
`CreationDate` + exact `ExecutablePath` + `CommandLine`, supervisor equals `<isolated-home>/daemon.pid`
with the brain parented to it, isolated-home association, supervisor first, `Stop-Process -Id` alone,
post-state asserted) is adopted verbatim and not restated here. Three mechanics on top of it:

1. **The 180 s cleanup bound is a reporting deadline, not a guarantee of completed teardown** (doyle).
   The rig therefore carries `CLEANUP=PENDING` as a **reported terminal value**, alongside the existing
   verified-clean value, and never reports a run clean at 480 s on the strength of the clock. The r10
   teardown value that *does* mean clean was earned by measurement —
   `TEARDOWN_CHECK receipt_exit=0 census_exit=0 clean_verdict=1 named_rules_absent=1 census_valid=1` —
   and only that shape earns it again.
2. **Fresh pid-naming authorization is a stop, not a step.** If disposal needs one, the rig **stops
   experimental activity, preserves the identities, and reports cleanup pending**. It does not wait on
   an authorization mid-run and does not proceed on the assumption one will arrive.
3. **Residue is reported as an identity the next consumer can act on** — pid, `ExecutablePath`,
   `CommandLine` — because it lands in a shared build pool and is paid for by whoever builds next.
   `DAEMON_STOP_REFUSED` is expected here (IR-122: the guard keys on the caller's `OWL_SESSION_ID`,
   regardless of target `SPT_HOME`, and no flag overrides it). It is reported, never worked around, and
   never converted into a pid-only automation.

**Grants are spent, separately.** The r10 setup and teardown grants were **nonce-bound** (`…-001-39393`,
`…-002-39393`, both consumed) and the later residual-cleanup grant was **PID-specific** (34436). Fresh
elevation and fresh residual-process authorization are **two prerequisites, not one**, and neither is
requested by this document.

## 6a. The instruments themselves — where they live and what they are pinned by

Written, and exercised under the harmless-control authorization. They are **not in git** (`.spt/` is
gitignored), so they are **pinned by content hash**, not by commit — committing them merely to
manufacture identity is the wrong reason to commit. Path:
`.spt/preserved/hertz-fp-driver-review/d2/`, verified by `D2-MANIFEST.sha256`.

| instrument | what it is |
| --- | --- |
| `d2_capture.ps1` | the capture; read-only by construction |
| `d2_wrapper.ps1` | the executable setup/capture/teardown wrapper — consumes the elevated legs' receipts, issues no handoff |
| `d2_serializer_check.ps1` | leaf value encoding, both PowerShell editions |
| `d2_stub_case.ps1` + `d2_document_check.ps1` | a real capture document produced against stubbed host commands |
| `d2_wrapper_check.ps1` | the wrapper's four scenarios |
| `d2-control-receipt.md` | **the authority for the current hashes, results and limits** |

The receipt is where results live; this document does not restate them, so the two cannot drift.

## 7. Exercise before admission — the instrument is unproven

`INSTRUMENT_EXERCISE_SCOPE` in r10 records which instruments were live-exercised and which were not.
D2 is **entirely unexercised**. Before it is admitted to a field run it needs, as its own read-only
step with its own grant:

1. a run against the **positive control alone**, on a host with no owned pair present — proving each
   property's state resolves and the four-state encoding renders;
2. a **deliberate red**: the negative-control name, and one property name known to be wrong, both
   producing `ABSENT`/error rather than a clean zero. **A rig that has never been red on purpose has
   not been shown capable of reporting a failure.**

Both are read-only host probes and neither is authorized here.

## 8. What D2 still does not establish

- **Nothing about reachability.** `REACHABILITY=NOT_TESTED` stands. No connection is attempted from
  any box, and the evidence ceiling of `docs/304-W2-FIELD-PLAN.md` §5 is unchanged in every word.
- **It is a separate invocation from any timed product call.** Like D1, it cannot say what the
  product's own query saw — `snapshot()` keeps nothing. It bounds the host state *around* the call.
- **It names a disagreement; it does not explain one.** A stored `LocalUser` reading as written while
  the ActiveStore reports `LocalUserEmpty` is a well-posed question, not a mechanism. todlando's §7
  table is where a capture outcome gets read, and none of its rows is adopted as an interpretation.
- **It says nothing about the r10 instances.** They were torn down and measured absent. Any D2
  evidence describes a **new pair**.
- **Strict `[1]` acceptance is unchanged**, and changing it is not mine.
