Skip to content

Commit a09e46d

Browse files
committed
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.
1 parent 994c91f commit a09e46d

1 file changed

Lines changed: 1 addition & 1 deletion

File tree

docs/adr/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ what is withheld and what you can request.
118118
| [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 |
119119
| [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 |
120120
| [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) |
122122
| [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) |
123123
| [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) |
124124
| [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

Comments
 (0)