Skip to content
Merged
Changes from all commits
Commits
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
33 changes: 29 additions & 4 deletions docs/adr/0087-sandbox-subprocess-isolation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 0087 — Router/Handler subprocess isolation

- **Status:** Accepted; **Amended (2026-08-04)** — the transform-result parity rule changed shape. The child now materialises a container return with `_partition`'s **own** rule instead of reproducing its exact input container, so a tuple/set/generator **delivers** in both modes (BACKLOG #341). AC-11 and the "Result parity" bullet below are rewritten accordingly; the isolation boundary and the codec grammar are untouched. <!-- opt-in subprocess isolation built (#197, 2026-07-10) -->
- **Status:** Accepted; **Amended (2026-08-04)** — the transform-result parity rule changed shape. The child now materialises a container return with `_partition`'s **own** rule instead of reproducing its exact input container, so a tuple/set/generator **delivers** in both modes (BACKLOG #341). AC-11 and the "Result parity" bullet below are rewritten accordingly; the isolation boundary and the codec grammar are untouched. **Amended (2026-09-05)** — a 74 GiB extrapolation added under the per-worker footprint on 2026-09-04 is **retracted in place**; the measured per-child figures stand, and the surviving constraint is restated as the ADR 0052 AC-2 conflict it always was. No decision, boundary or acceptance criterion of this ADR changes. <!-- opt-in subprocess isolation built (#197, 2026-07-10) -->
- **Date:** 2026-07-10
- **Related:** [ADR 0009](0009-run-scoped-context-providers.md) (RunContext providers) · [ADR 0010](0010-handler-callable-db-lookup.md) / [ADR 0043](0043-fhir-read-lookup.md) (`db_lookup`/`fhir_lookup`) · [ADR 0072](0072-traced-dryrun-mode.md) (tracer seam it composes with) · [ADR 0036](0036-windows-config-source-trust.md) / [ADR 0041](0041-load-path-attestation-and-change-attribution.md) (config-source trust) · CLAUDE.md §2 (reliability/purity, count-and-log) · CLAUDE.md §4 (layering) · BACKLOG #197 · ASVS 15.2.5 / `docs/security/ASVS-L3-REMEDIATION-PLAN.md` WP-L3-17

Expand Down Expand Up @@ -331,12 +331,37 @@ hoisted out of the per-dispatch frame entirely). A value outside the closed gram
(`SandboxError`), never silently degrading — and the reverse is also true, so a Handler returning an
exotic object now reports a *codec* rejection rather than the pickle error text it used to.

**Resident footprint, measured 2026-09-04 (BACKLOG #1194) — this ADR did not state it.** The worker
**Per-worker footprint, measured 2026-09-04 (BACKLOG #1194) — this ADR did not state it.** The worker
is one persistent child *per inbound*, and that child costs **~50 MiB unique / ~77 MiB resident**
([the artifact](../benchmarks/results/2026-09-04-adr0087-sandbox-dispatch/README.md); a minimal
one-router one-handler graph, so it is a floor), plus a one-time spawn and config load measured in
**seconds** (1.8–2.7 s). Against the committed 1,500-connection target that is roughly **74 GiB** and 1,500 extra OS
processes. The bill attaches to the per-inbound worker *cardinality*, not to the process boundary —
**seconds** (1.8–2.7 s). Those are the measured numbers and they stand.

> **RETRACTED 2026-09-05, IN PLACE RATHER THAN DELETED.** This paragraph continued: *"Against the
> committed 1,500-connection target that is roughly **74 GiB** and 1,500 extra OS processes."*
> **Do not quote that figure.** It was refuted by an adversarial pass run by the seat that produced
> it, and each defect below was then re-checked against the files. **Three, each sufficient alone:**
> **(1) Wrong multiplier.** [ADR 0052](0052-enterprise-scale-target.md) AC-2 commits to *"1,500
> concurrent connections"* — **"inbound" does not appear**. Sandbox children exist per traffic-carrying
> *inbound*, so 1,500 is not the count to multiply by.
> **(2) Linearity was never measured.** Every result JSON in that artifact records
> `worker_tree_uss_mb`, `worker_tree_rss_mb` and `worker_tree_processes` **singular** — one tree, five
> files, no scaling series. A single-tree measurement was multiplied by 1,500 with nothing validating
> linearity.
> **(3) Resident set is bounded by installed RAM.** The tier is 16 GB and the bench box 31.7 GiB, so
> 74 GiB *resident* is not a quantity any sized host can exhibit; the observable outcome is paging and
> spawn failure. The right quantity for a demand figure is private commit charge.
> **The correction matters beyond the number:** an extrapolation was presented as a measurement, in a
> ratified ADR, where later readers cite it.

**The constraint that survives needs no extrapolation, and it is stronger.**
[ADR 0052](0052-enterprise-scale-target.md) AC-2 requires 1,500 concurrent connections *"without
per-connection-worker exhaustion (fd/socket/worker-task limits)"*. At `mode=subprocess` the engine
holds one persistent worker tree per traffic-carrying inbound — never pooled, never evicted — and each
is a process tree (**two** processes on Windows, per `worker_tree_processes` in every result file),
two parent daemon threads, three parent pipe fds and a job-object handle. **That is precisely the
resource class AC-2 names**, and ADR 0052 records its 1,500-connection axis as unvalidated with no
harness. The bill attaches to the per-inbound worker *cardinality*, not to the process boundary —
a bounded shared worker pool would decouple it from the connection count, and would keep the
property this ADR claims (a boundary to the **engine**) while dropping only one it already disclaims
in `sandbox.py` (the seam draws no line between admin functions). Not a defect of the opt-in mode;
Expand Down
Loading