Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
04f956f
docs(adr): ADR 0166 -- sandbox child stderr captured and relayed, con…
wshallwshall Aug 14, 2026
2eed70c
feat(worktree): install receipts for the machine-global gate, and ref…
wshallwshall Aug 14, 2026
b3ecfc7
feat(security): structural estate-identifier backstop for the leak ga…
wshallwshall Aug 14, 2026
a113717
feat(sandbox): capture the child's stderr and relay it attributed, co…
wshallwshall Aug 15, 2026
5d91331
fix(sandbox): two confirmed findings from the #343 adversarial review…
wshallwshall Aug 15, 2026
a956293
feat(store): monotonic-prefix audit comparator beside the exact seal,…
wshallwshall Aug 15, 2026
bb32089
feat(store): wire the monotonic-prefix comparator into Postgres, SQL …
wshallwshall Aug 15, 2026
d143e4d
fix(security): the estate-shape skip reasoning expired, so name the o…
Aug 24, 2026
459fb95
test(tooling): classify test_gate_install_receipt so the partition st…
Aug 24, 2026
0201491
fix(security): allowlist the nine wordlist collisions instead of skip…
Aug 24, 2026
a85f7ac
fix(ci): name a native crash in the engine suite as a crash, not a te…
wshallwshall Aug 15, 2026
919da04
test(ci): the engine-step locator must tolerate a wrapped invocation
Aug 24, 2026
54eb293
docs(coord): correct three false deployment-state claims about the ma…
wshallwshall Aug 15, 2026
6133ddd
backlog: five items this PR touches, verified against the landed diff…
Aug 26, 2026
18fb8aa
backlog: #1215's note was stale before it even merged -- PR 604 lande…
Aug 26, 2026
ca59e64
Merge branch 'main' into lander/433-partb-engine
wshallwshall Aug 26, 2026
7379bf9
Merge branch 'main' into lander/433-partb-engine
wshallwshall Aug 26, 2026
bb51b85
Merge branch 'main' into lander/433-partb-engine
wshallwshall Aug 26, 2026
3dde372
Merge branch 'main' into lander/433-partb-engine
wshallwshall Aug 26, 2026
6caa988
docs(adr): retire 30 stale ADR 0166 citations this branch added; the …
Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 57 additions & 3 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3123,6 +3123,13 @@ Wall time (the cleaner signal — all three still ingest everything up to 300/s)

## 321. Leak gate is blind to the ported-estate site-code and partner-product token class

> **PARTIAL 2026-08-26 (lander), NOT A CLOSURE -- this item stays OPEN.** Ships Proposed 3, the
> prefix-free estate-identifier shape backstop (`_ESTATE_ID_SHAPE` in `scan_forbidden.py`), scanning
> for the six-digit-run-inside-an-identifier shape independent of any loaded prefix. **What is NOT
> done: Proposed 1, the owner-run token data** across the private file and the Actions + Dependabot
> secret stores -- that half is owner-only and cannot be verified from any checkout. Proposed 2
> (detector coverage against the loaded token set, not a monkeypatched one) is a separate PR.

> 🔢 **Re-scored 2026-08-20 -> P2.** Value **7/10** · Difficulty **3/10** · _quick win_. Both halves the 2026-08-03 amendment left standing are still standing: no prefix-free shape backstop exists (scan_forbidden.py:568-570 derives every site-code detector from loaded prefixes and degrades to _NEVER at :574-576), and the token data is owner-run and unverifiable from this checkout. A required merge context that is blind to a live token class is a real gap, and the remainder is one structural regex plus a negative test plus an owner data edit. _(was 7/10 · 3/10.)_
>
> **Filed 2026-08-01 — not started.** A required merge context exited 0 on content carrying a real site code and a partner product name, with no compensating control (`scan_forbidden.py:10-12` is explicit that gitleaks finds secrets, not this class) and nothing stopping the next estate-derived identifier landing the same way; `.md` is not in `_SITE_SKIP_SUFFIXES` (`scan_forbidden.py:119`, `{".lock", ".svg"}`) so the file was scanned — the fix is owner-run token data across the private file plus the Actions *and* Dependabot secret stores, a negative test per class, and optionally a structural shape backstop.
Expand Down Expand Up @@ -3166,6 +3173,15 @@ Note the item is **not** "the scanner is broken" — it is that the token *sourc

## 328. `audit-verify` cannot detect a truncated audit tail

> **PARTIAL 2026-08-26 (lander), NOT A CLOSURE -- this item stays OPEN.** Ships the monotonic-prefix
> comparator this item names as the prerequisite ("a seal-on-stop / check-on-start design (or a
> monotonic-prefix comparator) before it is worth wiring"), on all three store backends -- SQLite,
> Postgres and SQL Server -- plus the Store protocol. **What is NOT done: the three-file startup
> wiring** (`config/settings.py`, `pipeline/engine.py`, `api/app.py`'s `create_managed_app`) that
> would actually consume it as an `[integrity]` anchor key. Verified before this note: `store.py`,
> `base.py`, `postgres.py` and `sqlserver.py` all carry the comparator; none of the three named
> wiring files is touched by the commits this note rides with.

> 🚧 **Re-scored 2026-08-20 -> P2.** Value **5/10** · Difficulty **4/10** · _fill-in_. The operator-runnable half landed, so a compliance job can snapshot and compare an anchor, but the automatic startup check at pipeline/engine.py:860 remains truncation-blind because no [integrity] anchor key exists at config/settings.py:3285-3309. Value 5 because the CLI path is a workable substitute for the automatic one; difficulty 4 because the exact point-in-time seal has to be replaced by a seal-on-stop or monotonic-prefix comparator before the three-file plumb through settings, engine and create_managed_app is worth wiring, and a prefix comparator lands on all three store backends. _(previously unscored.)_
>
> **Status OPEN — Proposed 1-2 SHIPPED 2026-08-04, Proposed 3 DEFERRED.** `messagefoundry audit-anchor` (`--service-config` / `--db` / `--json`, with the same SQLite missing-DB refusal as its verify twin, so a typo'd path cannot mint an empty database and print an anchor OF NOTHING) prints `COUNT:HEAD`, and `audit-verify --expected-anchor COUNT:HEAD` / `--expected-anchor-file PATH` feeds it into the already-present `expected_anchor=` keyword — no comparison-logic change and no store migration, as filed. `docs/FEATURE-MAP.md`'s hand-maintained CLI count moved 30 to 31 with it. **Proposed 3 — the `[integrity]` startup-anchor key — is NOT built, which is why this stays OPEN.** The reason is measured, and pinned by `test_an_anchor_goes_stale_on_the_next_appended_row`: the shipped comparator is an EXACT point-in-time seal (row count *and* head hash), so a stored anchor consumed by the startup auto-verify would fire a false `integrity_drift` on essentially every restart, because any running instance writes audit rows. It needs a seal-on-stop / check-on-start design (or a monotonic-prefix comparator) before it is worth wiring, and the plumbing is a THREE-file edit — `config/settings.py`, `pipeline/engine.py`, and `api/app.py`'s `create_managed_app`, which is the only route an `[integrity]` key reaches the Engine by, and which the multi-session plan had scope-dropped. `[integrity].audit_verify_on_start` therefore remains a bare walk and still cannot see a truncated tail; that limit is now stated on its own `docs/CONFIGURATION.md` row and in ADR 0014 §16.4.2. The SQL Server and Postgres `audit_anchor` CLI tests are written and collect cleanly but have **never executed locally** (no Docker daemon) — they are CI-verified only. _(was 5/10 · 3/10.)_
Expand Down Expand Up @@ -3454,9 +3470,24 @@ What is NOT settled is the mechanism. Two independent passes reached different a

## 343. Sandbox child stderr is inherited unframed into the engine log stream

> 🚧 **Re-scored 2026-08-20 -> P3.** Value **4/10** · Difficulty **3/10** · _fill-in_. Both named problems survive: attribution, because stderr=None (sandbox.py:446) leaves child lines indistinguishable from engine lines, and the print() PHI path, because the #1054 filter installed at _sandbox_worker.py:49 is a property of the logging handler only. Value 4 given the same-admin threat model and no product-facing surface; difficulty 3 because the fix is stderr=subprocess.PIPE plus a relay thread mirroring the existing stdout reader, plus a bootstrap redirect of the child's sys.stdout away from the frame fd. _(was 4/10 · 3/10.)_
>
> **Status OPEN (filed 2026-08-01).** The worker is spawned with `stderr=None` ([pipeline/sandbox.py:266](../messagefoundry/pipeline/sandbox.py)), so the child's stderr is the **engine's own stderr**, unframed and unattributed. fd 1 is the IPC channel and is strictly framed; fd 2 has no such discipline. Admin-authored Handler code can therefore write arbitrary bytes straight into the engine's log stream — including forged log lines, ANSI control sequences, or content that breaks whatever consumes those logs (NSSM captures stdout/stderr to files; see [docs/SERVICE.md](SERVICE.md)).
> ✅ **SHIPPED 2026-08-26 (lander).** Matches the item's own "Fix direction" exactly: `stderr=PIPE`
> plus a dedicated reader thread relaying through the engine's stdlib logger, attributed to the
> inbound + worker generation and rate-limited; content at DEBUG only, an attributed rate-limited
> NOTICE at INFO+ with no content, satisfying CLAUDE.md section 9 by construction rather than
> operator discipline. The adjacent stdout-landmine the item names is closed in the same change:
> the worker rebinds `sys.stdout` to fd 2 at bootstrap, after the frame writer captures its raw
> handle and before `load_config()` runs untrusted code. ADR 0176 records the decision and the
> rejected alternatives (a per-line byte cap -- rejected because it preserves precisely the most
> identifying part of an HL7 message, MSH and PID). Verified before this note: `pipeline/sandbox.py`
> carries `stderr=subprocess.PIPE` and the reader thread; `tests/test_sandbox.py` passes.

Filed 2026-08-01, re-scored 2026-08-20 (value 4/10, difficulty 3/10) while open. The worker was
spawned with `stderr=None` ([pipeline/sandbox.py:266](../messagefoundry/pipeline/sandbox.py)), so
the child's stderr was the **engine's own stderr**, unframed and unattributed. fd 1 is the IPC
channel and is strictly framed; fd 2 had no such discipline. Admin-authored Handler code could
therefore write arbitrary bytes straight into the engine's log stream — including forged log lines,
ANSI control sequences, or content that breaks whatever consumes those logs (NSSM captures
stdout/stderr to files; see [docs/SERVICE.md](SERVICE.md)).
> Verdict: build
> Closing-act: code

Expand Down Expand Up @@ -10876,6 +10907,20 @@ gate is the wrong shape, validation of the walk is the right one.

## 1215. ADR 0161 and `mail-drain.ps1` describe the pre-wiring channel, and the script contradicts itself about markers

> **PARTIAL 2026-08-26 (lander), NOT A CLOSURE -- this item stays OPEN, one limb of at least four.**
> The item names four defects. **Two land here**, in `mail-drain.ps1`: the "THIS DOES NOT WIRE
> ANYTHING" paragraph (the same commit that added it also introduced the
> `install-coordination.ps1` rows wiring both events, so the claim was false in the commit that made
> it) and the marker-gates-a-consume paragraph (inverted twice; the shipped guard skips the marker
> check entirely when consuming, so a marker suppresses a re-display and nothing else). **A third
> already landed separately, on `main` before this PR**: PR #604 (`871f146ae`) fixed ADR 0161's own
> status line, its "Status and what gates wiring" section, and the line-258 inline comment --
> coordinated live with the builder holding that half, confirmed disjoint before either landed.
> **A fourth is still open and neither PR touches it**: `docs/adr/0161-*.md` still carries one
> warning-glyph (⚠️) at the line the item cites, verified present on `main` directly before writing
> this note. Do not read the item as closed once this PR and #604 are both counted -- one named
> defect survives both.

> 🔢 **Re-scored 2026-08-20 -> P3.** Value **4/10** · Difficulty **1/10** · _fill-in_. The inverted marker model is rewritten, leaving two false status claims and one glyph. Value stays 4 because the remainder is the same trap class rather than a milder one: ADR 0161's Status line and its Status section agree with mail-drain.ps1's header that nothing is wired, so a reader who re-reads either document has the error confirmed, and only reading install-coordination.ps1:278-279 or the ADR's own contradicting checklist at :405-408 falsifies it. Difficulty 1 because the remainder is edits to two documentation surfaces with no code and no test. _(was 4/10 · 2/10.)_
>
> **Filed 2026-08-11 -- found by Session C at HEAD while verifying #1028; reported, not fixed.** Three defects in one record. (1) ADR 0161's Status line and its "Status and what gates wiring" section still call the code an **unwired prototype** with *"nothing live in any session"* -- **false at HEAD on both counts**. (2) `scripts/hooks/mail-drain.ps1:71-73` still says *"THIS DOES NOT WIRE ANYTHING"*, but commit `fdec72ca` introduced both hook rows itself, so **the sentence was false in the commit that added it**. (3) The same file **CONTRADICTS ITSELF ABOUT MARKERS**: `:37-42` and `:57-64` assert a marker gates a consume; the shipped code at `:802`, `:809` and `:875-886` says the opposite.
Expand Down Expand Up @@ -12144,6 +12189,15 @@ location rather than on subject -- the same shape as a commit that CITES an item
BUILDS it.*
## 1247. installing the machine-global worktree gate leaves no record: no backup, no receipt, no log line, and Copy-Item preserves the source mtime

> **CONTESTED 2026-08-26 (lander), NOT A CLOSURE -- this item stays OPEN.** Two independent
> implementations exist, PR #613 and PR #607, both adding a backup + install-receipt mechanism to
> the same two files with the SAME shape but DIFFERENT default security postures on a receipt
> mismatch: #607 warns and overwrites by default (`-RefuseOnMismatch` to stop); #613 refuses by
> default (`-OverwriteUnverifiedGate` to proceed) -- fail-open versus fail-closed on a machine-global
> safety control. Flagged by the Dispatcher before either could land; #613's auto-merge is disarmed
> pending an owner ruling on which posture to keep. Do not read a SHIPPED banner here as settled
> until that ruling lands.

> 🔢 **Re-scored 2026-08-20 -> P2.** Value **5/10** · Difficulty **3/10** · _fill-in_. A write to a shared machine-global safety control leaves no attributable record, and the inherited mtime is worse than absent because it once carried a true finding into retraction; the only workaround is a hash baseline captured in advance by luck. Difficulty 3: a receipt file plus a refuse-on-mismatch flag at one install site, with tests, and no product code touched. _(was 6/10 · 3/10.)_
>
> **Filed 2026-08-13 -- found the hard way. The installed gate's CONTENT changed on this box while three sessions were running against it, and after all three looked, NOBODY CAN SAY WHO WROTE IT.** The change itself was benign and correct -- it moved the gate FORWARD, from `590b68f6` to `dd90232e` -- so this is a governance defect, not an incident. An unattributable write to a shared safety control is the same class of event whether it upgrades or downgrades; only the outcome differed.
Expand Down
15 changes: 13 additions & 2 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -856,8 +856,19 @@ now reaps its whole process tree (a Windows kill-on-close job object / a POSIX p
a grandchild no longer outlives the kill (BACKLOG #342). That reap is best-effort process hygiene: what
makes a stray frame *harmless* is still the codec plus the request-answer binding (a live grandchild can
force a respawn, i.e. dead-letter messages on that inbound, but nothing more), not the process teardown.
The child's **stderr is inherited by the engine**, so a
Handler that prints goes into the engine's log unparsed and un-redacted. ADR 0072 Router/Handler
The child's **stderr is captured by the engine, not inherited** (ADR 0176): a Handler that prints is
relayed into the engine's log attributed to the inbound, the child pid and the worker generation, with
**the content itself only at `DEBUG`**. At `INFO` and above you get a rate-limited `WARNING` naming the
inbound and counting the lines, and no content — that is deliberate, and it is how the never-log-bodies
rule is kept when a Handler prints a message body. To read what a Handler actually wrote, set
`[logging].level = "DEBUG"`, and treat that log as PHI-bearing while you do. Two things that will
surprise you otherwise: raising the level shows every `print` and raw write **plus** the child's own
`WARNING`+ records, but never the child's own `DEBUG`/`INFO` records — the worker's root logger is
pinned at `WARNING` when it starts and no knob plumbs a level into it (an unfiled follow-up, named by
subject rather than by a number that does not exist yet). And because that one relay thread is also
what keeps the child's stderr pipe from filling, a slow log handler — an off-box `[logging].forward_*`
collector that has stalled, say — becomes back-pressure on a `DEBUG`-level child rather than lost
output. ADR 0072 Router/Handler
tracing does not compose with `mode=subprocess` (the sandbox branch precedes the tracer branch), and a
`mode=subprocess` graph cannot use the ADR-0071 fused thread-hop path (it is hard-disabled).

Expand Down
33 changes: 27 additions & 6 deletions docs/PHI.md
Original file line number Diff line number Diff line change
Expand Up @@ -1029,12 +1029,33 @@ of the filters above. It is **out of scope for this section** — but treat a
[§2](#2-where-phi-lives--data-at-rest-inventory).

The **opt-in ADR 0087 sandbox worker** (`[sandbox].mode = "subprocess"`, default `"off"`) is **not** an
exclusion. The child is spawned with `stderr=None`, so it **inherits the engine's stderr** — stream 1's
own sink — and it installs the three filters above on that stream itself, via `configure_stderr_logging`
(BACKLOG #1054). A `WARNING`+ record emitted in the child by admin-authored Router/Handler code, or by a
library it pulls, is therefore redacted and CR/LF-scrubbed on the same terms as an engine record.
Redaction is a property of the **handler**, so this is a second installation of the chain rather than
something the child inherits along with the file descriptor.
exclusion, and this paragraph is the single statement of how its output reaches stream 1 — the code
docstrings link here rather than restate it. Two independent mechanisms cover it:

- **Inside the child.** It calls `configure_stderr_logging`, which installs the same three filters on
its own stderr handler (BACKLOG #1054), so a `WARNING`+ record emitted there by admin-authored
Router/Handler code, or by a library it pulls, is redacted and CR/LF-scrubbed at the source.
Redaction is a property of the **handler**, so this is a second installation of the chain rather
than something the child inherits along with a file descriptor.
- **In the engine parent (ADR 0176, BACKLOG #343).** The child is spawned with
`stderr=subprocess.PIPE` — it no longer *inherits* stream 1's sink — and a per-worker drain thread
turns those bytes into engine log records attributed to the inbound, the child pid and the worker
generation. **Content is relayed at `DEBUG` and only at `DEBUG`.** At `INFO` and above the engine
emits an attributed, rate-limited `WARNING` notice carrying the identity and a line **count** and no
content, so the never-log-bodies rule holds **by construction**: a Handler that `print()`s a message
body cannot put that body on a default-level log, because no call site above `DEBUG` carries child
stderr content at all. Suppressed lines are counted and reported by the next notice, never dropped
silently. Relayed records ride stream 1's own handlers, so they are redacted and scrubbed on
stream 1's terms; the relay additionally scrubs control characters itself, because "one child write
is one log record" is the drain's own framing contract and cannot depend on the host process's
logging configuration. **Residuals, stated rather than implied — at least these:** raising the service to `DEBUG`
to read that content puts full Handler output on stream 1, at stream 1's PHI class — the same
posture as any `DEBUG` run; and the child's own root logger is pinned at `WARNING` when the worker
starts, so `DEBUG` shows every `print`/raw write plus the child's `WARNING`+ records, and never the
child's own `DEBUG`/`INFO` records, which the child never emitted. **A byte-cap truncation was
rejected, not overlooked:** truncating an HL7 v2 message to its first N bytes keeps MSH and PID and
discards the clinically bulky remainder, so it preserves precisely the most identifying part of the
record (ADR 0176).

---

Expand Down
Loading
Loading