You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(adr): the 0087 index row claimed a residual closure the pipe did not deliver
`docs/adr/README.md:121` said ADR 0087 "Closes the WP-L3-17 residual
(residual-closure)" while `:175` (ADR 0144) said "15.2.5 stays **Partial**". They
contradicted, and :175 was right.
Two separate reasons the closure claim was wrong, and only one of them is fixed:
1. Until BACKLOG #339 the IPC pipe pickled, so a Handler's `__reduce__` executed in
the engine parent and the boundary was bypassable outright. The row asserted a
closure the transport did not deliver. That part is now fixed.
2. Even with the codec, ADR 0087 confines the ADDRESS SPACE, not the host. Verified
rather than inferred: `DEFAULT_FORBIDDEN_MODULES` blocks socket/ssl/asyncio/
multiprocessing and the engine's secret-bearing packages, but NOT `os` or
`subprocess` — a sandboxed Handler still reaches host command execution. OS-level
default-deny is ADR 0147, still Proposed with no code.
So the row now records the MFW2 amendment, states 15.2.5 stays Partial in agreement
with the 0144 row, names the address-space limit explicitly, and adds the
address-space-only and #342 (grandchild not reaped) residuals. Module list gains
`_sandbox_codec.py`.
This edit was blocked earlier by the worktree guard while
`claude/adr-asvs-scorecard-as-data` held the file; it merged as `8f01cef8` (ADR 0156,
PR #120) and the guard released. Their merge did not touch the contradiction.
NOT included: BACKLOG #339's OPEN item 2 still reads "the edit was NOT made", and
#197's banner carries the same original closure claim. `docs/BACKLOG.md` is currently
held by two live sessions (adr-0154-sync-reply-handoff, stuck-cis) — I handed that
file to the first of them earlier and am not overriding the guard to take it back.
Both are text-accuracy follow-ups, not correctness ones.
Copy file name to clipboardExpand all lines: docs/adr/README.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -118,7 +118,7 @@ what is withheld and what you can request.
118
118
| [0083](0083-mtls-client-certificate-identity.md) | mTLS client-certificate identity (BACKLOG #200) — a **verified** peer cert's subject/SAN maps to a MessageFoundry principal via an explicit deny-by-default allow-list (`[api].tls_client_cert_identities`), rooted in `CERT_REQUIRED` and namespace-qualified against spoofing. An **attested service-to-service** identity (no MFA/session/step-up). **Activated** (PLAN-9 Wave 3): a fork-free scope-populating shim (`api/tls_client_cert.py`) surfaces the verified peer cert post-handshake, and a fenced cert-only dependency `require_service_cert` gates one non-interactive route (`GET /service/identity`) — never a bearer/PHI/step-up route (refuses PHI-view perms at build), so a cert-identity can never bypass step-up | Accepted (2026-07-10) — owner ratified; model+resolver built (#200), **activated** PLAN-9 Wave 3 |
119
119
| [0084](0084-accepts-router-seam.md) | `accepts=` Router-stage seam — let a Handler declare a **pure** router-time applicability predicate (`Callable[[Message \| RawMessage], bool]`) so the Router declines it **before** a routed row is materialized, recovering the `2` transactions per self-filtering handler the ADR 0051 `txn/msg = 3 + 2H + 2N` model charges (the ADT hub's `wasted == 32`, ~63% of its durable writes). Purity enforced by construction (router stage already makes `db_lookup`/`fhir_lookup` raise — ADR 0010/0043); additive + default-identical (`accepts=None` = today). **Crux:** an all-declined message finalizes **`UNROUTED`** not `FILTERED` — count-and-log intact (still `RECEIVED` at ingress, still a final logged disposition, never accept-and-drop), only per-destination FILTERED granularity lost (optional `message_events` declined-handler mitigation). Ships an **advisory** `accepts-candidate` lint (flags a `@handler` opening with a guard-filter `if …: return []`). Spec + lint stub only, no engine build (build = BACKLOG #213) | **Accepted (2026-07-11, owner-ratified)** — `FILTERED → UNROUTED` accepted; `message_events` declined-handler mitigation deferred from v1, gated behind the #63 verbosity gate when built |
120
120
| [0085](0085-direct-hisp-smime-connector.md) | Direct-Project S/MIME-over-SMTP outbound connector (DIRECT-HISP, BACKLOG #157) — PR1 **outbound send only**: a new `ConnectorType.DIRECT` + `DirectDestination` that **SIGNs then ENCRYPTs** the Handler body via core `cryptography` `serialization.pkcs7` (no new dep — `endesive` rejected, `dnspython` deferred) and submits it as `application/pkcs7-mime; smime-type=enveloped-data` over the reused EMAIL STARTTLS/`refuse_cleartext_credentials` posture. All signing key+cert / per-partner recipient cert / trust anchor loaded + cross-validated at construction (fail loud): key↔cert public-key match + one-level `verify_directly_issued_by` chain check. New fail-closed `[egress].allowed_direct` host gate (kept separate from `allowed_smtp`). Inbound Direct mail / MDN / DNS-CERT discovery / IHE XDR **deferred** to later phases | Accepted (2026-07-10) — PR1 outbound-only, later phases deferred |
121
-
| [0087](0087-sandbox-subprocess-isolation.md) | Router/Handler subprocess isolation (SANDBOX, BACKLOG #197, ASVS 15.2.5 / WP-L3-17) — an **opt-in** `[sandbox]` section: `mode=off` (default) runs Routers/Handlers in-process **byte-identically, zero overhead**; `mode=subprocess` runs each inbound's Router/Handler in a **persistent per-inbound worker child** (`pipeline/sandbox.py` + `_sandbox_worker.py`; stdlib-only, no new dep — RestrictedPython rejected), never a per-message fork. The OS-process boundary denies admin code reach to the parent's DEK/audit-chain/sockets (the child loads only the message *graph*); on top: a forbidden-import guard (socket/store/crypto), a parent-enforced wall-clock cap (+ POSIX `RLIMIT_CPU`/`RLIMIT_AS`), and a **fail-closed** refusal of the live `db_lookup`/`fhir_lookup` bridges. Interposed at the `route_only`/`transform_one` seam (the in-process `mode=off` path composes with the ADR 0072 tracer; `mode=subprocess` bypasses it); engine-side handler/outbound-name validation stays engine-side; a denial → `ERROR`/dead-letter **post-ACK** (no NAK, never dropped). Closes the WP-L3-17 residual (residual-closure). **Residuals:** default-off; live-lookup forward-over-IPC deferred; load-time top-level exec not sandboxed (unchanged safe-source DACL gate) | Accepted (2026-07-10) — opt-in subprocess isolation built (#197) |
121
+
| [0087](0087-sandbox-subprocess-isolation.md) | Router/Handler subprocess isolation (SANDBOX, BACKLOG #197, ASVS 15.2.5 / WP-L3-17) — an **opt-in** `[sandbox]` section: `mode=off` (default) runs Routers/Handlers in-process **byte-identically, zero overhead**; `mode=subprocess` runs each inbound's Router/Handler in a **persistent per-inbound worker child** (`pipeline/sandbox.py` + `_sandbox_worker.py` + `_sandbox_codec.py`; stdlib-only, no new dep — RestrictedPython rejected), never a per-message fork. The OS-process boundary denies admin code reach to the parent's DEK/audit-chain/sockets (the child loads only the message *graph*); on top: a forbidden-import guard (socket/store/crypto), a parent-enforced wall-clock cap (+ POSIX `RLIMIT_CPU`/`RLIMIT_AS`), and a **fail-closed** refusal of the live `db_lookup`/`fhir_lookup` bridges. Interposed at the `route_only`/`transform_one` seam (the in-process `mode=off` path composes with the ADR 0072 tracer; `mode=subprocess` bypasses it); engine-side handler/outbound-name validation stays engine-side; a denial → `ERROR`/dead-letter **post-ACK** (no NAK, never dropped). **MFW2 amendment (BACKLOG #339):** the IPC pipe originally pickled, so a Handler's `__reduce__` executed in the engine parent and the boundary was bypassable outright; both legs now use a closed-tag, non-executing codec. Consistent with the [0144](0144-security-lint-gate-over-admin-authored-router-handler-config.md) row below, **15.2.5 stays Partial** — this is the *address-space* half (the child still reaches `os`/`subprocess` and the host); OS-level default-deny confinement is [ADR 0147](0147-hardened-runtime-isolation-for-router-handler-code-ipc-brokered-sandbox-extends-adr-0087.md), still Proposed. **Residuals:** default-off; live-lookup forward-over-IPC deferred; load-time top-level exec not sandboxed (unchanged safe-source DACL gate); confinement is address-space only; worker kill does not reap a grandchild (#342) | Accepted (2026-07-10) — opt-in subprocess isolation built (#197) |
122
122
| [0088](0088-apiclient-service-cli-extraction.md) | Extract a Qt-free / FastAPI-free `apiclient/` engine-client library + a `messagefoundry service {install,start,stop,status}` CLI (BACKLOG #103, reusable-core half) — `apiclient/client.py` is the verbatim former `console/client.py` body (deps: `httpx` + lazy `truststore` + the pure `api/` pydantic models), the canonical client shared by the console, the headless load/acceptance harness, and future clients; `service.py` is the verbatim former `console/service_control.py` (stdlib-only Windows SCM control), surfaced on the CLI. `console/client.py` + `console/service_control.py` become thin re-export shims (no behaviour change); harness client imports repoint to `messagefoundry.apiclient`, Qt-widget imports stay on `console/`. Explicitly the reusable-core half of #103 — the console is **kept**; deleting `console/`, rehoming the Qt widgets, and dropping the `[console]` extra remain **deferred**. Does **not** supersede [ADR 0032](0032-console-desktop-launch.md); no new dependency | Accepted (2026-07-10) — built (PLAN-9 Wave 3) |
123
123
| [0086](0086-deterministic-corepoint-import.md) | Deterministic Corepoint action-list import → code-first Handlers (BACKLOG #105) — the **inverse** of ADR 0076's typed vocabulary + lens: a pure, stdlib-only engine importer (`messagefoundry/corepoint_import.py`) + a `messagefoundry import corepoint <export> --out <dir>` CLI that parses a Corepoint action-list export and emits one code-first `@router`/`@handler` module per channel, calling the ADR 0076 vocabulary + `Send`. The `.py` stays the only artifact/execution path (#26). **Amended 2026-07-24 (§2(a′)/(b′)/(c′)): the input schema is VALIDATED and it is XML** — root `<Package>`, logic at `<ActionList>/<List>`, a recursive control-flow tree whose `@Data` is rich-text markup that must be stripped before any statement classifies; `<Block>` is a section label (never an action) and `@Disabled` is preserved as a comment, never live. Parsed through defusedxml (`forbid_dtd`/`forbid_entities`/`forbid_external`); `parse_any()` sniffs XML vs. the superseded synthetic JSON model. The mapping is the inverse of ADR 0076 §2 and deliberately narrow — a field path is never guessed, and control flow is emitted as real Python with inert placeholder conditions. Unmapped verbs emit in-place `# TODO: Corepoint …` + best-effort `msg.set` stub (count-and-log — never dropped). Untrusted export values ride across as `json.dumps`-escaped literals (no code injection). Correctness gate: emitted modules pass `messagefoundry check` + round-trip through `lens parse`. Optional `ide/` wrapper deferred | Accepted (2026-07-10), amended 2026-07-24 — owner ratified; engine importer + CLI built, schema validated (#105) |
124
124
| [0089](0089-recognition-first-lens-native-idioms.md) | Recognition-first lens (Phase A, BACKLOG #222) -- recognize the native Message API (msg.set / msg.field-copy / msg.delete_segments) as editable set_field/copy_field/delete_segment rows, so a Handler authored in the native API (not the ADR 0076 actions.py wrappers) renders as editable Steps without being rewritten. Extends the byte-stable rewrite to EDIT and INSERT those native forms import-free (insert_row exempt from the editable-kind guard; inserts indent to the anchor code line). Roadmap phases A-E; a production-estate scan measured ~66% opaque rows before, ~42% editable after Phase A | Accepted (2026-07-13) — owner-ratified; Phase A built + adopted |
0 commit comments