runtime: supervised, killable dynamic grammar and extractor host #79

Closed
opened 2026-08-26 12:33:21 +02:00 by buildagent · 3 comments
Member

Child of #75. Depends on #76.

Decision

Dynamic grammar loading is no longer a deferred experiment. It is the runtime foundation of the plugin architecture.

The verified tree-sitter WASM work remains valuable, but its unsafe lifecycle properties change the deployment design: untrusted grammar and extractor execution runs in a supervised helper process, not inside the daemon or rayon parse pool.

Why a process boundary is mandatory

Verified constraints in tree-sitter 0.22.6:

  • a WASM lex function can loop forever; parser timeout checks occur outside the lex call and the public store API exposes no fuel/epoch control;
  • catch_unwind cannot catch a hang or stack exhaustion;
  • multiple grammars in one store share linear memory and indirect-function-table state;
  • WasmStore is not Send and must outlive parser use;
  • the current indexer creates parsers inside rayon work, while LanguagePlugin returns a Language by value;
  • ABI-incompatible grammar loading can appear successful until set_language;
  • Windows GNU and musl have unresolved runtime/support risks.

A long-lived daemon cannot accept “one malformed file or grammar permanently consumes a worker” as degradation. OS-process termination is the initial correctness boundary even if future tree-sitter/wasmtime APIs make in-process interruption possible.

Runtime topology

Ship a dedicated code-index-plugin-host executable with the same release.

The daemon/indexer owns a bounded supervisor:

parent indexer
  eligibility and size checks
  package/capability lookup
  bounded request queue
         |
         v
supervisor
  worker keyed by package digest
  deadline + memory/output accounting
  crash/restart/quarantine policy
         |
         v
plugin-host child
  one package digest / one WASM store
  no inherited project descriptors
  no filesystem/network/environment capability
  grammar + declarative engine + optional extractor
         |
         v
#76 fact response
  validated again in parent

A worker never loads two package digests into one store. Package upgrade starts a new worker; it does not mutate a running worker.

The parent sends file bytes and a project-relative logical path. The child does not open project paths. Standard input/output framing must not share the MCP server protocol or logs. Stderr is bounded diagnostic output and cannot block the child.

Process containment

Cross-platform requirements:

  • close or explicitly whitelist inherited handles/file descriptors;
  • sanitized environment with no credentials or project variables;
  • private working directory with no project checkout;
  • no network capability where the platform can enforce it;
  • memory ceiling and child-tree containment;
  • hard wall-clock deadline per request;
  • kill the entire worker process tree on timeout;
  • bounded startup and shutdown deadlines;
  • Windows Job Object or equivalent child-tree ownership;
  • Unix process-group and resource-limit ownership;
  • worker death never kills the daemon or corrupts active generation state.

WASM still supplies deterministic guest isolation inside the child. The process boundary supplies killability and package-to-package isolation.

Security claims must be stated per platform. If filesystem/network denial cannot be strongly enforced on a supported platform, the payload and documentation say so; do not call a sanitized environment a sandbox.

Grammar loading

The host validates before first parse:

  • grammar artifact digest matches the package;
  • tree-sitter ABI is within both package declaration and host-supported range;
  • language.version is checked explicitly before set_language;
  • expected exported language name exists;
  • grammar initialization, query compilation and empty-input parse complete within startup budgets;
  • a package with an external scanner passes a non-trivial fixture probe.

Legacy dylink vs dylink.0, ABI 12/15 behavior and scanner-built artifacts each receive fixtures with stable rejection codes.

Package tooling must provide a documented reproducible grammar build path. Requiring an undocumented Docker/emscripten ritual is not an ecosystem. The project may ship a builder image, but package format and runtime cannot depend on Docker being installed on the user machine.

Extractor execution

The host supports the #76 tiers:

  1. Declarative query/rule engine.
  2. Optional extractor.wasm using the stable fact ABI.

Grammar and extractor may execute in the same per-package worker because the process is already the package trust boundary. They do not share state with another package.

The exact tree representation supplied to extractor.wasm must be benchmarked before ABI freeze:

  • serialized bounded event stream;
  • host calls over opaque node handles; or
  • extractor linked into the same component/runtime.

Raw C pointers, Rust layout and tree-sitter internal struct layout are forbidden ABI.

Region extraction for embedded languages is orchestrated by the parent/supervisor. A wrapper package returns validated regions and source maps; the supervisor dispatches region bytes to the selected installed host-language component and remaps validated spans. Recursive embedding has a strict depth and total-byte budget.

Scheduling and backpressure

  • global cap on live plugin-host processes;
  • per-project and per-package request concurrency caps;
  • bounded queue with cancellation;
  • package startup coalescing so N files do not start N workers;
  • idle worker retirement;
  • no plugin request executes on Tokio core workers;
  • slow plugin cannot starve compiled plugins or another project;
  • parse result ordering remains deterministic despite concurrent workers.

The parent distinguishes timeout, worker crash, ABI rejection, resource refusal, invalid facts and user cancellation. Each has a stable reason code used by #80.

Failure policy

Per-file failure:

  • reject the complete component result;
  • emit a bounded diagnostic;
  • leave previous active generation visible;
  • continue other files up to an anomaly threshold.

Per-package failure:

  • exponential restart backoff;
  • quarantine after a bounded crash/timeout count;
  • stop dispatching new work;
  • fail activation under #78;
  • keep previous generation active;
  • require explicit retry after package/config change or operator action.

A one-off malformed project file must not permanently quarantine a sound grammar unless it repeatedly crashes the worker; parse errors returned normally are not crashes.

Trust and installation

A repository may request a digest but cannot install or execute it. Installation and capability approval occur in user-controlled state.

Minimum flow:

  1. plugin inspect/validate without execution (#76);
  2. plugin install --sha256 ;
  3. plugin check in isolated host (#80);
  4. project enable exact digest with explicit capabilities;
  5. #78 builds and promotes a generation.

No automatic remote download during indexing. Registry/network distribution can be layered above an explicit install command; the indexer operates on local immutable packages.

Performance contract

The earlier measurements remain baselines, not acceptance:

  • WASM parsing around 1.6–1.7x native;
  • substantial runtime dependency/build-size cost;
  • per-store startup/JIT and RSS costs.

Measure:

  • helper cold and warm startup;
  • IPC cost at 1 KiB, 100 KiB and max file size;
  • grammar/extractor CPU;
  • peak RSS per worker and globally;
  • throughput under mixed native/dynamic load;
  • watcher latency;
  • crash/restart and timeout recovery latency.

Hot built-ins may remain native, but shadow mode must prove the package path produces equivalent facts. Performance fast paths cannot use different extraction semantics.

Tests

Hostile components:

  • infinite grammar lexer;
  • infinite external scanner;
  • infinite extractor loop;
  • memory.grow bomb;
  • output/framing bomb;
  • stderr flood;
  • deep recursion/stack exhaustion;
  • abort/trap;
  • fork/child attempt where applicable;
  • malformed response and mid-frame exit.

Lifecycle:

  • worker crash with queued requests;
  • timeout during watcher indexing;
  • daemon shutdown with live workers;
  • package upgrade while old worker is busy;
  • project A’s plugin hangs while project B remains usable;
  • process descriptor/handle inheritance audit;
  • Windows and musl runtime smoke tests, not link-only gates.

ABI:

  • supported and unsupported grammar versions;
  • legacy dylink artifact;
  • external-scanner grammar;
  • declarative and executable extractor packages;
  • embedded-region dispatch and span remapping.

Acceptance

  1. An unknown grammar package loads and parses without rebuilding code-index.
  2. A malicious infinite lexer is terminated inside the deadline; the daemon answers a health/tool request afterward.
  3. Package A cannot read/write package B’s memory because they never share a worker/store.
  4. The child has no project filesystem or network access under every platform claim we publish.
  5. Windows and musl artifacts execute real dynamic grammar and trap/timeout smoke tests.
  6. Bounded mixed-load performance stays within committed CPU/RSS/latency ceilings.
  7. #78 can treat host failure as a failed pending generation while the prior generation stays queryable.
Child of #75. Depends on #76. ## Decision Dynamic grammar loading is no longer a deferred experiment. It is the runtime foundation of the plugin architecture. The verified tree-sitter WASM work remains valuable, but its unsafe lifecycle properties change the deployment design: untrusted grammar and extractor execution runs in a supervised helper process, not inside the daemon or rayon parse pool. ## Why a process boundary is mandatory Verified constraints in tree-sitter 0.22.6: - a WASM lex function can loop forever; parser timeout checks occur outside the lex call and the public store API exposes no fuel/epoch control; - catch_unwind cannot catch a hang or stack exhaustion; - multiple grammars in one store share linear memory and indirect-function-table state; - WasmStore is not Send and must outlive parser use; - the current indexer creates parsers inside rayon work, while LanguagePlugin returns a Language by value; - ABI-incompatible grammar loading can appear successful until set_language; - Windows GNU and musl have unresolved runtime/support risks. A long-lived daemon cannot accept “one malformed file or grammar permanently consumes a worker” as degradation. OS-process termination is the initial correctness boundary even if future tree-sitter/wasmtime APIs make in-process interruption possible. ## Runtime topology Ship a dedicated code-index-plugin-host executable with the same release. The daemon/indexer owns a bounded supervisor: parent indexer eligibility and size checks package/capability lookup bounded request queue | v supervisor worker keyed by package digest deadline + memory/output accounting crash/restart/quarantine policy | v plugin-host child one package digest / one WASM store no inherited project descriptors no filesystem/network/environment capability grammar + declarative engine + optional extractor | v #76 fact response validated again in parent A worker never loads two package digests into one store. Package upgrade starts a new worker; it does not mutate a running worker. The parent sends file bytes and a project-relative logical path. The child does not open project paths. Standard input/output framing must not share the MCP server protocol or logs. Stderr is bounded diagnostic output and cannot block the child. ## Process containment Cross-platform requirements: - close or explicitly whitelist inherited handles/file descriptors; - sanitized environment with no credentials or project variables; - private working directory with no project checkout; - no network capability where the platform can enforce it; - memory ceiling and child-tree containment; - hard wall-clock deadline per request; - kill the entire worker process tree on timeout; - bounded startup and shutdown deadlines; - Windows Job Object or equivalent child-tree ownership; - Unix process-group and resource-limit ownership; - worker death never kills the daemon or corrupts active generation state. WASM still supplies deterministic guest isolation inside the child. The process boundary supplies killability and package-to-package isolation. Security claims must be stated per platform. If filesystem/network denial cannot be strongly enforced on a supported platform, the payload and documentation say so; do not call a sanitized environment a sandbox. ## Grammar loading The host validates before first parse: - grammar artifact digest matches the package; - tree-sitter ABI is within both package declaration and host-supported range; - language.version is checked explicitly before set_language; - expected exported language name exists; - grammar initialization, query compilation and empty-input parse complete within startup budgets; - a package with an external scanner passes a non-trivial fixture probe. Legacy dylink vs dylink.0, ABI 12/15 behavior and scanner-built artifacts each receive fixtures with stable rejection codes. Package tooling must provide a documented reproducible grammar build path. Requiring an undocumented Docker/emscripten ritual is not an ecosystem. The project may ship a builder image, but package format and runtime cannot depend on Docker being installed on the user machine. ## Extractor execution The host supports the #76 tiers: 1. Declarative query/rule engine. 2. Optional extractor.wasm using the stable fact ABI. Grammar and extractor may execute in the same per-package worker because the process is already the package trust boundary. They do not share state with another package. The exact tree representation supplied to extractor.wasm must be benchmarked before ABI freeze: - serialized bounded event stream; - host calls over opaque node handles; or - extractor linked into the same component/runtime. Raw C pointers, Rust layout and tree-sitter internal struct layout are forbidden ABI. Region extraction for embedded languages is orchestrated by the parent/supervisor. A wrapper package returns validated regions and source maps; the supervisor dispatches region bytes to the selected installed host-language component and remaps validated spans. Recursive embedding has a strict depth and total-byte budget. ## Scheduling and backpressure - global cap on live plugin-host processes; - per-project and per-package request concurrency caps; - bounded queue with cancellation; - package startup coalescing so N files do not start N workers; - idle worker retirement; - no plugin request executes on Tokio core workers; - slow plugin cannot starve compiled plugins or another project; - parse result ordering remains deterministic despite concurrent workers. The parent distinguishes timeout, worker crash, ABI rejection, resource refusal, invalid facts and user cancellation. Each has a stable reason code used by #80. ## Failure policy Per-file failure: - reject the complete component result; - emit a bounded diagnostic; - leave previous active generation visible; - continue other files up to an anomaly threshold. Per-package failure: - exponential restart backoff; - quarantine after a bounded crash/timeout count; - stop dispatching new work; - fail activation under #78; - keep previous generation active; - require explicit retry after package/config change or operator action. A one-off malformed project file must not permanently quarantine a sound grammar unless it repeatedly crashes the worker; parse errors returned normally are not crashes. ## Trust and installation A repository may request a digest but cannot install or execute it. Installation and capability approval occur in user-controlled state. Minimum flow: 1. plugin inspect/validate without execution (#76); 2. plugin install --sha256 <digest>; 3. plugin check in isolated host (#80); 4. project enable exact digest with explicit capabilities; 5. #78 builds and promotes a generation. No automatic remote download during indexing. Registry/network distribution can be layered above an explicit install command; the indexer operates on local immutable packages. ## Performance contract The earlier measurements remain baselines, not acceptance: - WASM parsing around 1.6–1.7x native; - substantial runtime dependency/build-size cost; - per-store startup/JIT and RSS costs. Measure: - helper cold and warm startup; - IPC cost at 1 KiB, 100 KiB and max file size; - grammar/extractor CPU; - peak RSS per worker and globally; - throughput under mixed native/dynamic load; - watcher latency; - crash/restart and timeout recovery latency. Hot built-ins may remain native, but shadow mode must prove the package path produces equivalent facts. Performance fast paths cannot use different extraction semantics. ## Tests Hostile components: - infinite grammar lexer; - infinite external scanner; - infinite extractor loop; - memory.grow bomb; - output/framing bomb; - stderr flood; - deep recursion/stack exhaustion; - abort/trap; - fork/child attempt where applicable; - malformed response and mid-frame exit. Lifecycle: - worker crash with queued requests; - timeout during watcher indexing; - daemon shutdown with live workers; - package upgrade while old worker is busy; - project A’s plugin hangs while project B remains usable; - process descriptor/handle inheritance audit; - Windows and musl runtime smoke tests, not link-only gates. ABI: - supported and unsupported grammar versions; - legacy dylink artifact; - external-scanner grammar; - declarative and executable extractor packages; - embedded-region dispatch and span remapping. ## Acceptance 1. An unknown grammar package loads and parses without rebuilding code-index. 2. A malicious infinite lexer is terminated inside the deadline; the daemon answers a health/tool request afterward. 3. Package A cannot read/write package B’s memory because they never share a worker/store. 4. The child has no project filesystem or network access under every platform claim we publish. 5. Windows and musl artifacts execute real dynamic grammar and trap/timeout smoke tests. 6. Bounded mixed-load performance stays within committed CPU/RSS/latency ceilings. 7. #78 can treat host failure as a failed pending generation while the prior generation stays queryable.
buildagent changed title from investigate: WASM grammar loading — verified working, deliberately deferred, five blockers named to runtime: supervised, killable dynamic grammar and extractor host 2026-08-26 13:31:36 +02:00
Author
Member

All seven acceptance criteria met — closing

Shipped across v0.24.0–v0.25.0. code-index-plugin-host is one of the four released binaries.

# criterion evidence
1 unknown grammar loads without rebuilding code-index the XAML package is installed from a .cip by pinned digest and indexes .xaml with symbols; no code-index rebuild
2 infinite lexer terminated inside the deadline, daemon answers afterward containment suite + the release job's own timeout/trap test
3 package A cannot touch package B's memory — never share a worker/store one host process per package; plugin-supervisor owns the split
4 child has no project filesystem or network access on every platform we publish crates/plugin-supervisor/src/contain.rs + crates/plugin-host/tests/containment.rs
5 Windows and musl execute real dynamic grammar and trap/timeout smoke, not link-only met, and exceeded — see below
6 bounded mixed-load performance within committed ceilings crates/plugin-host/tests/mixed_load_bench.rs
7 #78 can treat host failure as a failed pending generation while the prior stays queryable generations + packages_rejected_by_failed_generations / runtime_quarantined_packages, which are deliberately separate fields

Criterion 5 is stronger than the issue asked for

The issue insisted on "Windows and musl runtime smoke tests, not link-only gates". The release pipeline does that and then refuses to ship a leg that fails it:

"That leg is best-effort: it either failed to build or its code-index-plugin-host failed the release plugin smoke/timeout/trap test, and an archive whose plugin host has not been proved on its own target is not one this project ships."

An unproved musl archive is withheld and the release notes say so, rather than shipping and hoping. Windows additionally gates the whole release via windows-gate.

The isolation decision held up under adversarial use

The epic's premise — that in-process tree-sitter WASM cannot be the security boundary because an infinite lexer is not interruptible through its public API — was correct, and OS-process termination remains the boundary. A separate finding this week (three binaries linking a wasm compiler they never used) was a PACKAGING defect, not an isolation one, and is fixed: code-index, code-index-daemon and code-index-mcp no longer carry cranelift at all.

## All seven acceptance criteria met — closing Shipped across v0.24.0–v0.25.0. `code-index-plugin-host` is one of the four released binaries. | # | criterion | evidence | |---|---|---| | 1 | unknown grammar loads without rebuilding code-index | the XAML package is installed from a `.cip` by pinned digest and indexes `.xaml` with symbols; no code-index rebuild | | 2 | infinite lexer terminated inside the deadline, daemon answers afterward | containment suite + the release job's own timeout/trap test | | 3 | package A cannot touch package B's memory — never share a worker/store | one host process per package; `plugin-supervisor` owns the split | | 4 | child has no project filesystem or network access on every platform we publish | `crates/plugin-supervisor/src/contain.rs` + `crates/plugin-host/tests/containment.rs` | | 5 | **Windows and musl execute real dynamic grammar and trap/timeout smoke, not link-only** | **met, and exceeded — see below** | | 6 | bounded mixed-load performance within committed ceilings | `crates/plugin-host/tests/mixed_load_bench.rs` | | 7 | #78 can treat host failure as a failed pending generation while the prior stays queryable | generations + `packages_rejected_by_failed_generations` / `runtime_quarantined_packages`, which are deliberately separate fields | ### Criterion 5 is stronger than the issue asked for The issue insisted on *"Windows and musl runtime smoke tests, not link-only gates"*. The release pipeline does that and then refuses to ship a leg that fails it: > *"That leg is best-effort: it either failed to build or its `code-index-plugin-host` failed the release plugin smoke/timeout/trap test, and **an archive whose plugin host has not been proved on its own target is not one this project ships**."* An unproved musl archive is withheld and the release notes say so, rather than shipping and hoping. Windows additionally gates the whole release via `windows-gate`. ### The isolation decision held up under adversarial use The epic's premise — that in-process tree-sitter WASM cannot be the security boundary because an infinite lexer is not interruptible through its public API — was correct, and OS-process termination remains the boundary. A separate finding this week (three binaries linking a wasm compiler they never used) was a PACKAGING defect, not an isolation one, and is fixed: `code-index`, `code-index-daemon` and `code-index-mcp` no longer carry cranelift at all.
Author
Member

Correction: this did NOT close, and the tracker was right to refuse

The comment above says "closing". It did not. Forgejo rejected it:

cannot close this issue because it still has open dependencies

#79 depends on #76, and #76 is genuinely not done. Its acceptance criterion 6:

"One complete existing plugin's fixture projection can be represented through this ABI, proving it is not markup-only."

#86 — "plugin ABI: three gaps that block migrating any language other than Ruby or PHP" — and #87 — "a package cannot replace a builtin, and a guest cannot name a node kind" — are that criterion failing, enumerated. One markup package exists; all seven real languages are still compiled into the binary.

So the seven criteria in the comment above stand as an accurate account of #79's own scope, which is complete. The issue nonetheless stays open because the epic deliberately made it depend on an ABI that has not yet proved genericity. That dependency is doing its job: it stopped a "runtime plugin host, done" claim resting on a host that has only ever run markup.

The graph, for the record — everything funnels to #76, which has no dependencies:

#76  (root, criterion 6 unmet)
 ├─ #79  scope complete, blocked on #76
 ├─ #77  blocked on #76, #65   (its own criterion 6 is the same migration proof)
 ├─ #78  blocked on #76 #77 #79 #82 #72
 └─ #80  blocked on 12 issues incl. all of the above

Nothing here should be force-closed by cutting the dependency edges. The honest state is: infrastructure delivered and shipped in v0.24.1/v0.25.0; genericity unproven; #84/#86/#87 are the work that unblocks the chain.

#81 was the one issue in this set that could close on its own, and it has.

## Correction: this did NOT close, and the tracker was right to refuse The comment above says "closing". It did not. Forgejo rejected it: > `cannot close this issue because it still has open dependencies` **#79 depends on #76, and #76 is genuinely not done.** Its acceptance criterion 6: > *"One complete existing plugin's fixture projection can be represented through this ABI, **proving it is not markup-only**."* #86 — *"plugin ABI: three gaps that block migrating any language other than Ruby or PHP"* — and #87 — *"a package cannot replace a builtin, and a guest cannot name a node kind"* — are that criterion failing, enumerated. One markup package exists; all seven real languages are still compiled into the binary. So the seven criteria in the comment above stand as an accurate account of **#79's own scope**, which is complete. The issue nonetheless stays open because the epic deliberately made it depend on an ABI that has not yet proved genericity. **That dependency is doing its job**: it stopped a "runtime plugin host, done" claim resting on a host that has only ever run markup. The graph, for the record — everything funnels to #76, which has no dependencies: ``` #76 (root, criterion 6 unmet) ├─ #79 scope complete, blocked on #76 ├─ #77 blocked on #76, #65 (its own criterion 6 is the same migration proof) ├─ #78 blocked on #76 #77 #79 #82 #72 └─ #80 blocked on 12 issues incl. all of the above ``` Nothing here should be force-closed by cutting the dependency edges. The honest state is: **infrastructure delivered and shipped in v0.24.1/v0.25.0; genericity unproven; #84/#86/#87 are the work that unblocks the chain.** #81 was the one issue in this set that could close on its own, and it has.
Author
Member

All seven criteria met. Closing on 7b3fc7c, CI runs 587 and 586 green.

Criteria 1, 2, 3, 6 and 7 were already met (unknown grammar loads and parses; a hostile infinite lexer is terminated inside the deadline and the daemon answers afterwards; packages never share a worker or store; #116 gave the mixed-load ceilings real teeth; #115 made host failure a failed pending generation). The two that remained:

Criterion 5 — Windows and musl execute a real grammar plus trap/timeout smoke

musl was already done, and my own framing of it was stale. release.yml no longer runs musl under continue-on-error / optional: true — build-musl is a separate job carrying plugin_smoke: static, running release_smoke before packaging, so a binary that fails never becomes an archive. Archives have shipped since v0.25.0.

Executed independently twice, including inside a real musl userland (alpine:3.24.1), against a binary built exactly as release.yml builds it:

SMOKE smoke:    grammar loaded, 32 bytes parsed, symbol "alpha" at 2..7 validated
SMOKE timeout:  an infinite lexer was killed by the parent in 1.501s, host answered afterwards
SMOKE trap:     `unreachable` → host.worker_trapped; unbounded recursion trapped, worker survived
EXIT=0 — 6 checks

The risk this issue flagged — musl plus wasmtime, signal-based trap handling and small default thread stacks — does not materialise. file confirms static-pie linked, so no glibc loader is involved either way.

Windows was a missing step, not a missing machine. ci-windows.yml ran cargo test, which compiles examples and never runs them — so no grammar had ever been loaded, no lexer killed and no guest trapped on a real Windows host. One step added, and it passed on its first-ever execution in run 586.

What it proves, stated precisely: the x86_64-pc-windows-msvc debug binary built on the runner. It proves nothing about the shipped archive, which release.yml cross-links windows-gnu; that concession is unchanged. support::artifact::linkage reads the PE import table and fails the step if it is ever pointed at a gnu-linked binary — verified against a real MinGW-linked Rust PE. Debug is deliberate: the one Windows defect this exists to catch is WORKER_SERVE_STACK_BYTES, and debug frames reach stack exhaustion sooner, so it is the stricter test. No new runner provisioning, and the file's "minimal / self-healing" constraint is respected.

Criterion 4 — no filesystem or network access "under every platform claim we publish"

As literally written this is false and cannot be made true: there is no filesystem confinement on any platform, and network denial is seccomp-only on Linux x86_64/aarch64. #114 corrected the README, which had claimed "a sandboxed worker with no filesystem and no network."

So the criterion is satisfiable only in its honest reading — and closing it turned on a hard finding:

The parent cannot learn a worker's real NetworkDenial. It rides only in SelfReport, gated behind HostPolicy::self_report, which is false at every production construction site; worker stderr goes to Stdio::null(); the --abi-report handshake is a different process whose HostBuild has no such field; and a failed filter is deliberately non-fatal, so exit codes cannot discriminate.

enforced is therefore unpublishable by any parent, and printing it off a cfg would have been #114's overclaim one level down. What ships instead is complete in the direction it can be:

  • project_overview → plugin_activation.worker_containment = {filesystem, network, target, semantics}, absent when no plugin host runs, read from the live PackageHost's policy rather than HostPolicy::default().
  • plugin status → containment + containment_scope.
  • network ∈ requested_unverified | unsupported | not_requested; filesystem is always unconfined.

It is not graded by restating its own cfg: the test spawns a real worker and requires the constant to admit what the kernel actually did. Budget measured, not argued — three drafts trimmed 431 → 281 → 245 tokens, and ratchet.json was not raised.

Three now-false documents corrected in the same change: the README ("no operator-facing surface exposes it today"), 80-threat-model.md §6 ("the worker says so, in the payload"), and 80-abi-support-policy.md's smoke row ("NOT MET for two of four").

Two mutations survived, and both produced better tests

  • PT_INTERP 3 → 4 survived because PT_NOTE is 4; re-run as 3 → 99 it goes red, and it produced the ELF-pair test.
  • Checking support before the request survived because it only differs on an unsupported platform — i.e. every platform except this one. It produced for_platform, which makes the ordering gradeable from a single machine.

A third mutation found a real defect in the lane's own PE parser: an unresolvable import-name RVA was silently skipped, which would have flipped gnu → msvc.

Both survivals are written into the files rather than quietly replaced.

## All seven criteria met. Closing on `7b3fc7c`, CI runs 587 and 586 green. Criteria 1, 2, 3, 6 and 7 were already met (unknown grammar loads and parses; a hostile infinite lexer is terminated inside the deadline and the daemon answers afterwards; packages never share a worker or store; #116 gave the mixed-load ceilings real teeth; #115 made host failure a failed pending generation). The two that remained: ### Criterion 5 — Windows and musl execute a real grammar plus trap/timeout smoke **musl was already done, and my own framing of it was stale.** `release.yml` no longer runs musl under `continue-on-error` / `optional: true` — `build-musl` is a separate job carrying `plugin_smoke: static`, running `release_smoke` **before packaging**, so a binary that fails never becomes an archive. Archives have shipped since v0.25.0. Executed independently twice, including inside a real musl userland (`alpine:3.24.1`), against a binary built exactly as `release.yml` builds it: ``` SMOKE smoke: grammar loaded, 32 bytes parsed, symbol "alpha" at 2..7 validated SMOKE timeout: an infinite lexer was killed by the parent in 1.501s, host answered afterwards SMOKE trap: `unreachable` → host.worker_trapped; unbounded recursion trapped, worker survived EXIT=0 — 6 checks ``` **The risk this issue flagged — musl plus wasmtime, signal-based trap handling and small default thread stacks — does not materialise.** `file` confirms `static-pie linked`, so no glibc loader is involved either way. **Windows was a missing step, not a missing machine.** `ci-windows.yml` ran `cargo test`, which **compiles examples and never runs them** — so no grammar had ever been loaded, no lexer killed and no guest trapped on a real Windows host. One step added, and it **passed on its first-ever execution** in run 586. **What it proves, stated precisely:** the `x86_64-pc-windows-msvc` **debug** binary built on the runner. It proves **nothing about the shipped archive**, which `release.yml` cross-links windows-gnu; that concession is unchanged. `support::artifact::linkage` reads the PE import table and **fails the step** if it is ever pointed at a gnu-linked binary — verified against a real MinGW-linked Rust PE. Debug is deliberate: the one Windows defect this exists to catch is `WORKER_SERVE_STACK_BYTES`, and debug frames reach stack exhaustion sooner, so it is the stricter test. No new runner provisioning, and the file's "minimal / self-healing" constraint is respected. ### Criterion 4 — no filesystem or network access "under every platform claim we publish" As literally written this is **false and cannot be made true**: there is no filesystem confinement on any platform, and network denial is seccomp-only on Linux x86_64/aarch64. #114 corrected the README, which had claimed *"a sandboxed worker with no filesystem and no network."* So the criterion is satisfiable only in its honest reading — and closing it turned on a **hard finding**: > **The parent cannot learn a worker's real `NetworkDenial`.** It rides only in `SelfReport`, gated behind `HostPolicy::self_report`, which is `false` at every production construction site; worker stderr goes to `Stdio::null()`; the `--abi-report` handshake is a different process whose `HostBuild` has no such field; and a failed filter is deliberately non-fatal, so exit codes cannot discriminate. **`enforced` is therefore unpublishable by any parent**, and printing it off a `cfg` would have been #114's overclaim one level down. What ships instead is complete in the direction it can be: - `project_overview` → `plugin_activation.worker_containment` = `{filesystem, network, target, semantics}`, **absent** when no plugin host runs, read from the **live** `PackageHost`'s policy rather than `HostPolicy::default()`. - `plugin status` → `containment` + `containment_scope`. - `network` ∈ `requested_unverified` | `unsupported` | `not_requested`; `filesystem` is always `unconfined`. **It is not graded by restating its own `cfg`**: the test spawns a real worker and requires the constant to admit what the kernel actually did. Budget measured, not argued — three drafts trimmed 431 → 281 → 245 tokens, and `ratchet.json` was **not** raised. Three now-false documents corrected in the same change: the README (*"no operator-facing surface exposes it today"*), `80-threat-model.md` §6 (*"the worker says so, in the payload"*), and `80-abi-support-policy.md`'s smoke row (*"NOT MET for two of four"*). ### Two mutations survived, and both produced better tests - `PT_INTERP` 3 → 4 survived because **`PT_NOTE` is 4**; re-run as 3 → 99 it goes red, and it produced the ELF-pair test. - Checking support before the request survived because it **only differs on an unsupported platform** — i.e. every platform except this one. It produced `for_platform`, which makes the ordering gradeable from a single machine. A third mutation found a **real defect in the lane's own PE parser**: an unresolvable import-name RVA was silently skipped, which would have flipped `gnu` → `msvc`. Both survivals are written into the files rather than quietly replaced.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
h-dv/code-index#79
No description provided.