[PR #2694] fix(wasm): raise the WABT output budget to 128 MiB #5318

Open
opened 2026-10-31 01:05:42 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2694
Author: @eliot-h
Created: 10/13/2026
Status: 🔄 Open

Base: main ← Head: codex/wabt-output-budget


📝 Commits (10+)

  • 5bda610 fix(wasm): raise the WABT output budget to 128 MiB
  • 6c85c66 test(wasm): pin the default MCP response budget in the WABT lane
  • 6a25ae2 test(wasm): strip the MCP budget variable case-insensitively
  • bb479a4 fix(wasm): derive the advertised WABT limits from WABT_LIMITS
  • a6b0e33 Merge remote-tracking branch 'origin/main' into codex/wabt-output-budget
  • da7dc2d test(wasm): pin the WABT budget in unit tests and cover paged MCP reads
  • 68eafbb test(wasm): resolve retained partial Evidence in the WABT lane
  • 66c1181 chore: merge main into PR #2694
  • 156e5fd Merge branch 'main' into codex/wabt-output-budget
  • 8498414 Merge remote-tracking branch 'origin/main' into codex/wabt-output-budget

📊 Changes

7 files changed (+132 additions, -9 deletions)

View changed files

📝 docs/testing.md (+6 -1)
📝 docs/wasm-artifacts.md (+3 -1)
📝 scripts/verify-wasm-artifact.mjs (+110 -4)
📝 src/wasm/WabtArtifactProvider.test.ts (+7 -0)
📝 src/wasm/WabtArtifactProvider.ts (+1 -1)
📝 src/wasm/WabtRelease.ts (+4 -1)
📝 tests/fixtures/wasm/artifact.ts (+1 -1)

📄 Description

Summary

inspect_wasm_artifact accepts modules up to 8 MiB, but each WABT subprocess had a fixed 32 MiB output budget. Real public modules far below the input limit decode to more WAT than that and fail: main now reports resource_constraint (since #2117), and before #2117 it reported unreadable_output. This raises the per-subprocess budget to 128 MiB. No issue is linked; the reproduction below is self-contained.

Problem and expected behavior

Smallest trigger: npm pack @resvg/resvg-wasm@2.6.2, then inspect package/index_bg.wasm with official WABT 1.0.42 linux-x64 (wabt-1.0.42-linux-x64.tar.gz, SHA-256 84895407a6bbb80e918f33b16b2fb2206021c150b6bc9ff6f761263a745ab131, matching the release .sha256):

REA_WABT_BIN_DIRECTORY=/abs/wabt-1.0.42/bin rea inspect-wasm-artifact /abs/package/index_bg.wasm --format json

All three modules below pass wasm-validate --enable-all. Costs are in the resource report under Validation performed.

npm module .wasm bytes wasm2wat --enable-all bytes main (bee73fc) This branch
@resvg/resvg-wasm@2.6.2 index_bg.wasm 2,478,606 53,431,449 (51 MiB) resource_constraint: "WABT output exceeded the 33554432-byte output budget" complete
@dimforge/rapier3d@0.21.0 rapier_wasm3d_bg.wasm 3,082,103 77,060,835 (73 MiB) resource_constraint complete
canvaskit-wasm@0.42.0 bin/canvaskit.wasm 7,317,345 83,263,693 (79 MiB) resource_constraint complete

On this branch, the WAT SHA-256 in the Evidence equals the SHA-256 of a standalone wasm2wat --enable-all run for all three. Other packaged modules in the same 32–128 MiB range: wasm-vips@0.0.19 (vips.wasm, vips-heif.wasm, vips-jxl.wasm) and the @jsquash/avif@2.1.1 encoders.

Expected: a valid module within the input limit whose decoded WAT fits the budget returns complete Evidence.

Change and scope

Follow the contribution scope.

  • WABT_LIMITS.outputBytes goes from 32 MiB to 128 MiB, with a short comment naming the measured modules. The input and tool limits and the per-command deadline (command_timeout_ms, default 30,000 ms) are unchanged.
  • Output over the budget still fails closed. Output size depends on the module's shape, not only its size, so this does not promise that every module under 8 MiB decodes.
  • The Evidence limitation sentence now takes its MiB figure from WABT_LIMITS.outputBytes instead of a literal, as its deadline already comes from command_timeout_ms. Apart from 128 MiB, its text is unchanged.
  • MCP delivery is unchanged. A complete result this large exceeds the default response budget and takes the existing resource_constraint/transport path with a retained Evidence reference. detail: "summary" (which runs the inspection again) and the paged wat view of inspect_analysis_view (which reads the retained record) then return it in small replies.
  • The real-WABT lane's rejected-module check, added on main in #5798, now expects the 128 MiB budget in the retained Evidence.
  • Over-budget failures are classified and excerpted by #2117, now on main. This branch is merged with it, and the shared budget sentence in docs/wasm-artifacts.md now states both changes.

Contract and boundary impact

  • Semantic owner and earliest changed stage: src/wasm/WabtRelease.ts (WABT adapter limits).
  • Contribution scope fit and linked maintainer decision: not applicable; this tunes an existing adapter limit within the current WABT boundary.
  • CLI and MCP/tool-catalog contract: schemas are unchanged. Modules whose output is between 32 and 128 MiB now succeed through the CLI, and over MCP through the existing transport fallback, detail: "summary" and paged views.
  • Provider, bridge, target-format, or platform compatibility: WABT 1.0.42 only, unchanged.
  • Evidence, artifact, provenance, or reconstruction contract: tool_profile.output_budget_bytes reports 134217728 and the limitation text says 128 MiB, so successful Evidence for the same input gets a new evidence_id.
  • Target selection, authority, upstream behavior, and cleanup details (if applicable):
    • Process ownership, cancellation and cleanup are unchanged.
    • Captured output is buffered in proportion to what is produced, so the larger budget allocates nothing up front.
    • Over-budget failures keep #2117's bounded resource_constraint error, which is 4.75 KB on both builds. Before failing, the subprocess output is still captured up to the larger budget. A 28 KB nested-block module whose WAT exceeds 128 MiB therefore peaked at 1,133 MiB process-family memory, compared with 599 MiB on main (warm medians).
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact: none.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process. (The existing WABT subprocesses; unchanged.)
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated:
    • src/wasm/WabtArtifactProvider.test.ts: the success mode pins tool_profile.timeout_ms, tool_profile.output_budget_bytes and the rendered limitation text, so CI checks the budget.
    • scripts/verify-wasm-artifact.mjs (manual real-WABT lane) builds a deterministic code-heavy module with wat2wasm (3.87 MiB, 38.2 MiB of WAT):
      • The CLI decodes it completely, matching a standalone wasm2wat digest and the 128 MiB budget.
      • Over MCP the complete reply returns the transport constraint with a retained-evidence reference equal to the CLI Evidence ID.
      • detail: "summary" reports the same WAT digest, and a paged wat view returns its first code units.
      • The MCP child drops every spelling of REA_MCP_MAX_RESPONSE_BYTES, so this check does not depend on the caller's configuration.
      • A rejected module's retained Evidence reports the 128 MiB budget.
    • tests/fixtures/wasm/artifact.ts mirrors the new output_budget_bytes.
  • Base reproduction or other evidence: the three npm modules above fail on main and decode completely on this branch. Run against a main build, the updated lane fails only at the new >32 MiB case (unreadable_output at d2e16b2, resource_constraint since #2117). rive.wasm and sqlite3.wasm complete on both builds with byte-identical WAT (see the resource report).
  • User-visible CLI/MCP output: canvaskit reports tool_profile.output_budget_bytes: 134217728 and the limitation "Each WABT command has a 30000 ms deadline and a fixed 128 MiB combined output budget. The deadline is not a whole-inspection budget. Oversized output fails instead of producing truncated success."
  • Remaining proof gaps:
    • Linux x64 only. macOS and Windows were not run.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

At head 4d5d626, which merges main at a4c183b (including #5798's per-command deadline and #7822): Node 24.22.1 / npm 11.19.1 (official tarball, checked against SHASUMS256.txt), Ubuntu 24.04 x86_64, official WABT 1.0.42 linux-x64.

  • npx vitest run src/wasm src/application/wasm tests/boundary/mcp/wasmArtifactMcp.test.ts — 10 files, 167 tests passed.
  • npm run verify:wasm:artifact -- --require-tools — passed, 68 cases; again with REA_MCP_MAX_RESPONSE_BYTES=268435456 — passed, 68 cases.
  • npm run verify:wasm:package -- --require-tools — 1 of 3 runs passed with 68 cases. The other 2 failed in process cleanup with environment_errno_ESRCH, an ownership-token read on a process that had just exited; no assertion failed. Plain main at a4c183b fails the same way in 3 of 3 runs.
  • The compiled CLI on resvg, rapier3d and canvaskit — complete, and every WAT SHA-256 equals a standalone wasm2wat run.
  • npm run verify:test-discovery — passed (2,857 files).
  • npm run check:ci — every task passes except knip, which reports isArtifactFormatId (src/domain/artifactGraph.ts:103) and a duplicate export in src/domain/analysisResourceLimits.ts. Plain main at a4c183b reports the same two findings.

Earlier heads used Node 24.18.0 / npm 11.16.0 with the same platform and WABT.

At head 8498414, which merges main at bee73fc (including #2117, #2114 and #2464):

  • npm run check:ci — passed (0 lint errors). npm run verify:test-discovery — passed.
  • npx vitest run src/wasm src/application/wasm tests/boundary/mcp/wasmArtifactMcp.test.ts — 6 files, 62 tests passed.
  • npm run verify:wasm:artifact -- --require-tools — passed, 45 cases.
  • The compiled CLI on resvg, rapier3d, canvaskit, rive and sqlite — complete. Every WAT SHA-256 equals a standalone wasm2wat run, and the typed interface is complete. The over-budget module returns a 4,750 B resource_constraint.

At head 68eafbb, which includes main at d2e16b2:

  • npm run verify:wasm:artifact -- --require-tools — passed, 45 cases; again with REA_MCP_MAX_RESPONSE_BYTES=268435456 set — passed, 45 cases.
  • npm run verify:wasm:package — passed, 45 cases from the packed and installed package.
  • The same lane against a main build — fails only at the new >32 MiB case, with unreadable_output.

At head da7dc2d, which includes main at d2e16b2:

  • npx vitest run src/wasm src/application/wasm tests/boundary/mcp/wasmArtifactMcp.test.ts — 4 files, 43 tests passed.
  • npm run check:ci — every task passes except lint:built. Its only error is max-lines-per-function in tests/boundary/providers/ghidra/ghidraLauncher.test.ts:431, a file identical to main (#2470).
  • npm run verify:test-discovery — passed.
  • git diff --check — clean.

At head bb479a4, merged locally with main at 2f3dee3:

  • npm run check:ci, npm run verify:test-discovery, npm run test:python and npm run test:prepare — passed.
  • npm run test:run — 1044 files passed, 15 skipped (macOS/Hopper only); 10,760 tests passed.
  • The compiled CLI on the three npm modules — exit 0, and every WAT SHA-256 equals a standalone wasm2wat run. On the main build they exit 1 with unreadable_output.

Resource report

main = d2e16b2 (32 MiB budget), branch = 68eafbb (128 MiB), both built with npm run build. Linux x64, i5-13500 hybrid (6 P + 8 E cores), shared host (1-minute load 2.4–13), Node 24.18.0, WABT 1.0.42. These tables measure the PR's diff, which merges since then leave unchanged. The over-budget path changed on main with #2117, so it is re-measured at head 8498414 against main at bee73fc.

Each run is one systemd-run unit with -p CPUQuota=100% -p MemoryMax=4G (the #2016 caps); "uncapped" drops the CPU quota. Elapsed is the unit's Service runtime, CPU is cgroup cpu.stat usage_usec, and peak is cgroup v2 memory.peak read inside the unit (node, WABT and ps; stdout is consumed outside the unit). Builds alternate ABBA. Cold runs first drop the module, WABT, node, dist, scripts and the 3,065 node_modules files REA opens from the page cache with per-file posix_fadvise(DONTNEED): 0% stayed resident in 75 of 76 cold runs, and the 65.6–72.7 MiB read back from disk is charged to the unit, so cold peaks sit about 67 MiB above warm. Warm runs start with those files resident. Tokens are tiktoken 0.14 o200k_base counts of each whole output, not Claude's tokenizer. Cells are median (min–max).

Equivalent workflows (complete on both builds). The WAT is byte-identical and equals standalone wasm2wat --enable-all. The JSON differs only in evidence_id, the budget sentence (twice) and output_budget_bytes.

Module Run (n) Build Elapsed s CPU s Peak MiB
@rive-app/canvas@2.44.2 rive.wasm (29.4 MB WAT) cold (13) main 4.38 (3.33–5.53) 4.23 (3.20–5.38) 555 (554–672)
branch 3.81 (3.11–5.50) 3.68 (2.98–5.35) 560 (554–674)
warm (13) main 3.92 (2.88–5.12) 3.89 (2.87–5.11) 490 (486–606)
branch 4.00 (3.12–5.47) 4.03 (3.09–5.46) 490 (487–605)
@sqlite.org/sqlite-wasm@3.53.4-build2 sqlite3.wasm (15.4 MB WAT) cold (13) main 2.80 (2.48–3.74) 2.67 (2.33–3.54) 540 (538–547)
branch 2.88 (2.62–3.82) 2.73 (2.46–3.59) 539 (532–546)
warm (13) main 2.58 (2.23–3.78) 2.56 (2.21–3.77) 473 (465–478)
branch 2.54 (2.10–3.57) 2.52 (2.08–3.54) 473 (471–478)

Uncapped warm medians (n = 3), main → branch: rive 2.20 → 2.14 s elapsed, 3.35 → 3.44 s CPU, 606 → 604 MiB; sqlite 1.68 → 1.60 s, 2.34 → 2.34 s, 473 → 473 MiB. CLI stdout: rive 32,623,950 → 32,623,953 B and 7,228,545 → 7,228,539 tokens; sqlite 16,440,057 → 16,440,060 B and 2,970,947 → 2,970,949 tokens. rive's peak is bimodal on both builds (about 555 or 672 MiB cold, 488 or 605 MiB warm). Below 32 MiB the capture path is identical, so I attribute this to GC timing, but did not isolate it.

Newly completed modules. main at d2e16b2 fails with unreadable_output, returning 32 MiB of partial WAT in the error. Since #2117, main returns a 4.75 KB resource_constraint instead. The branch completes, and its WAT equals standalone wasm2wat. Cells are elapsed s / CPU s / peak MiB, median of 3.

Module (WAT) Build Cold Warm Uncapped warm stdout B / tokens
resvg 2.6.2 (53.4 MB) main 3.25 / 3.10 / 560 3.53 / 3.53 / 492 1.72 / 2.75 / 498 34,037,036 / 3,361,201
branch 6.42 / 6.25 / 717 3.88 / 3.85 / 648 2.67 / 4.30 / 649 56,826,304 / 8,596,415
rapier3d 0.21.0 (77.1 MB) main 3.50 / 3.35 / 605 3.39 / 3.34 / 532 2.00 / 3.10 / 531 34,454,018 / 5,909,443
branch 6.58 / 6.43 / 914 6.33 / 6.29 / 841 3.23 / 5.14 / 847 79,385,834 / 10,154,493
canvaskit 0.42.0 (83.3 MB) main 6.18 / 6.06 / 768 4.29 / 4.28 / 691 2.48 / 4.11 / 708 34,630,017 / 7,388,388
branch 8.62 / 8.48 / 887 6.49 / 6.44 / 821 4.31 / 7.03 / 770 95,930,158 / 25,909,801

Over-budget failure path (28 KB generated module, 147.3 MB WAT), at head 8498414 vs main at bee73fc (both include #2117). Both builds fail closed with resource_constraint. Warm runs, n = 3.

Build Elapsed s CPU s Peak MiB CLI error B
main 4.10 (2.91–4.41) 4.10 (2.91–4.40) 599 (552–600) 4,754
branch 5.15 (4.61–6.18) 5.15 (4.59–6.18) 1,133 (1,108–1,247) 4,750

MCP (default 10 MiB reply budget, warm, 2 sessions per cell). Each session connects, calls tools/list, then makes (a) a complete inspect_wasm_artifact call, (b) the same call with detail: "summary" and (c) inspect_analysis_view for wat 0..4096. Tokens count the text content. Successful replies repeat it byte for byte in structuredContent, so a client that forwards both pays about twice. Session CPU and peak cover the SDK client, the REA server and WABT in one 1-CPU unit, and elapsed was within 0.07 s of CPU.

Module Tokens (a)+(b)+(c), main → branch Session CPU s, main → branch Session peak MiB, main → branch
rive 345+1,668+2,950 = 4,963 → 333+1,653+2,925 = 4,911 12.16, 14.14 → 13.67, 14.11 1,194, 1,179 → 1,159, 1,204
sqlite 341+1,661+2,915 = 4,917 → 345+1,667+2,928 = 4,940 11.29, 9.95 → 11.44, 8.92 935, 908 → 943, 955
resvg 636 (error) → 335+1,652+2,935 = 4,922 14.65, 11.12 → 12.53, 13.60 1,570, 1,465 → 1,515, 1,631
rapier3d 632 (error) → 351+1,710+2,994 = 5,055 12.93, 12.47 → 15.26, 20.33 1,475, 1,481 → 1,967, 1,996
canvaskit 640 (error) → 333+1,653+2,935 = 4,921 13.58, 21.84 → 25.61, 18.05 1,542, 1,600 → 2,135, 1,878
over-budget (bee73fc → 8498414) 612 + 612 (error) → 610 + 610 (error) 12.23, 14.10 → 15.35, 10.42 1,063, 1,047 → 1,556, 1,717
  • Call (a) returns a 1,461 B transport constraint with a retained-evidence reference on success, or an 818 B error reference on failure. Call (b) runs the inspection again (1.6–10.8 s), and (c) reads the retained record (2–736 ms). When the module fails, (b) fails again and (c) returns unsupported_target.
  • tools/list (191 tools) is 2,636,597 B and 689,078 tokens, including 4,062 for inspect_wasm_artifact and 29,791 for inspect_analysis_view; the instructions add 159. Both are byte-identical on both builds, so the change is 0. An idle session (connect and tools/list) took 1.83–2.85 s CPU and 470–491 MiB on both builds.
  • A re-run with the same harness (36 CLI runs and 6 MCP sessions, 1-minute load 12.9–23.5) reproduced every CLI outcome, stdout size and SHA-256. Every peak was within 10%, apart from rive's two modes and one over-budget session. Elapsed and CPU ran 14–69% higher for the CLI and 42–79% higher for MCP, while branch vs main for rive and sqlite stayed between −8.4% and +1.4%. resvg warm CPU came out at 4.07 → 6.29 s, a 2.2 s increase rather than the 0.3 s above. Before #2117, one of 10 branch over-budget MCP calls returned cleanup_incomplete instead of unreadable_output. The retained error names a run-token mismatch for the wasm2wat PID, which had already exited 0 with no runtime root left. Linux returns an empty /proc/<pid>/environ when the owner exits between open and read. With traced runs and a forced race, the same false report reproduces on main (1 of 10 traced main sessions with a module that overshoots 32 MiB by the same 13 MB). It is a pre-existing ownership-check race whose odds depend on how far output overshoots the budget, not a change in this PR; 0 of 40 interleaved MCP calls hit it on either build.

Not measured:

  • Claude tokenizer counts; tokens for tool-call arguments, JSON-RPC framing and client wrappers; and whether a given client forwards text, structuredContent or both.
  • Cold MCP sessions and cold uncapped runs. Shared libraries, ps, dentry and inode caches and filesystem metadata were not evicted, and drop_caches was not used on this shared host.
  • Follow-up calls: get_evidence_bundle and export_evidence_bundle (the remediation the transport error names), WAT pages past offset 0, other facets and detail levels, and retries (none occurred).
  • Per-process memory split, V8 heap and GC time, and the cause of rive's bimodal peak. Also concurrent inspections, long sessions holding several large retained records, memory caps below 4 GiB, temporary-file I/O, and caller-side handling of 57–96 MB of CLI JSON.
  • macOS, Windows, ARM, other Node or WABT versions, non-hybrid CPUs, and deadline headroom on slower hosts (the slowest branch CLI run here took 11.7 s end to end).
  • Modules near the 128 MiB boundary, significance tests (n = 13, 3 or 2 on a loaded hybrid-core host), and the CI cost of the changed tests.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. Calls that used to fail can now succeed, and successful Evidence for the same input gets a new evidence_id.
  • Real Hopper/Ghidra, browser, or OS coverage: real WABT 1.0.42 on Linux x64 only, through the source checkout and the packed package.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review:
    • The same owned subprocesses, cleanup and fail-closed truncation apply.
    • Only the per-subprocess output ceiling grows. Under 1 CPU / 4 GiB caps, completing the three modules peaked at 648–841 MiB process-family memory (warm medians), compared with 492–691 MiB when main fails on them. Modules that already completed on main show no change. See the resource report.
    • Over-budget failures return #2117's bounded error, but capture up to 128 MiB first. The peak on that path rose from 599 to 1,133 MiB (see above).
    • Modules whose WAT exceeds 128 MiB, such as the tree-sitter PowerShell grammar reported in #2117, still fail.

Author readiness

Human owner: @eliot-h

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BycevXKBAk4aDiZYQ1qJLU


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/2694 **Author:** [@eliot-h](https://github.com/eliot-h) **Created:** 10/13/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `codex/wabt-output-budget` --- ### 📝 Commits (10+) - [`5bda610`](https://github.com/morluto/rea/commit/5bda6103e3dd643debb31e106e69c3e5e7b0cd83) fix(wasm): raise the WABT output budget to 128 MiB - [`6c85c66`](https://github.com/morluto/rea/commit/6c85c66ca177636b844d50575fcb70f6cb65cf0b) test(wasm): pin the default MCP response budget in the WABT lane - [`6a25ae2`](https://github.com/morluto/rea/commit/6a25ae2df058a102af6ba700b230f31593ea938e) test(wasm): strip the MCP budget variable case-insensitively - [`bb479a4`](https://github.com/morluto/rea/commit/bb479a4d50e640d37cf3638db23c011ec82a6ef1) fix(wasm): derive the advertised WABT limits from WABT_LIMITS - [`a6b0e33`](https://github.com/morluto/rea/commit/a6b0e33c3a8d4d1c3dab53dc6692874e80270176) Merge remote-tracking branch 'origin/main' into codex/wabt-output-budget - [`da7dc2d`](https://github.com/morluto/rea/commit/da7dc2d7caba9f42043688a837c007edcb629c4c) test(wasm): pin the WABT budget in unit tests and cover paged MCP reads - [`68eafbb`](https://github.com/morluto/rea/commit/68eafbb158ae760939076702310faa3503beed59) test(wasm): resolve retained partial Evidence in the WABT lane - [`66c1181`](https://github.com/morluto/rea/commit/66c1181671bafa1d8cc228da59b9abf5dcdd3b59) chore: merge main into PR #2694 - [`156e5fd`](https://github.com/morluto/rea/commit/156e5fd11ba0402af6904f4e75dda30c982f97ba) Merge branch 'main' into codex/wabt-output-budget - [`8498414`](https://github.com/morluto/rea/commit/8498414d6d679080a28b4953d16b7271cb05352f) Merge remote-tracking branch 'origin/main' into codex/wabt-output-budget ### 📊 Changes **7 files changed** (+132 additions, -9 deletions) <details> <summary>View changed files</summary> 📝 `docs/testing.md` (+6 -1) 📝 `docs/wasm-artifacts.md` (+3 -1) 📝 `scripts/verify-wasm-artifact.mjs` (+110 -4) 📝 `src/wasm/WabtArtifactProvider.test.ts` (+7 -0) 📝 `src/wasm/WabtArtifactProvider.ts` (+1 -1) 📝 `src/wasm/WabtRelease.ts` (+4 -1) 📝 `tests/fixtures/wasm/artifact.ts` (+1 -1) </details> ### 📄 Description ## Summary `inspect_wasm_artifact` accepts modules up to 8 MiB, but each WABT subprocess had a fixed 32 MiB output budget. Real public modules far below the input limit decode to more WAT than that and fail: `main` now reports `resource_constraint` (since #2117), and before #2117 it reported `unreadable_output`. This raises the per-subprocess budget to 128 MiB. No issue is linked; the reproduction below is self-contained. ## Problem and expected behavior Smallest trigger: `npm pack @resvg/resvg-wasm@2.6.2`, then inspect `package/index_bg.wasm` with official WABT 1.0.42 linux-x64 (`wabt-1.0.42-linux-x64.tar.gz`, SHA-256 `84895407a6bbb80e918f33b16b2fb2206021c150b6bc9ff6f761263a745ab131`, matching the release `.sha256`): ```bash REA_WABT_BIN_DIRECTORY=/abs/wabt-1.0.42/bin rea inspect-wasm-artifact /abs/package/index_bg.wasm --format json ``` All three modules below pass `wasm-validate --enable-all`. Costs are in the resource report under Validation performed. | npm module | `.wasm` bytes | `wasm2wat --enable-all` bytes | `main` (`bee73fc`) | This branch | |---|---:|---:|---|---| | `@resvg/resvg-wasm@2.6.2` `index_bg.wasm` | 2,478,606 | 53,431,449 (51 MiB) | `resource_constraint`: "WABT output exceeded the 33554432-byte output budget" | complete | | `@dimforge/rapier3d@0.21.0` `rapier_wasm3d_bg.wasm` | 3,082,103 | 77,060,835 (73 MiB) | `resource_constraint` | complete | | `canvaskit-wasm@0.42.0` `bin/canvaskit.wasm` | 7,317,345 | 83,263,693 (79 MiB) | `resource_constraint` | complete | On this branch, the WAT SHA-256 in the Evidence equals the SHA-256 of a standalone `wasm2wat --enable-all` run for all three. Other packaged modules in the same 32–128 MiB range: `wasm-vips@0.0.19` (`vips.wasm`, `vips-heif.wasm`, `vips-jxl.wasm`) and the `@jsquash/avif@2.1.1` encoders. Expected: a valid module within the input limit whose decoded WAT fits the budget returns complete Evidence. ## Change and scope Follow the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope). - `WABT_LIMITS.outputBytes` goes from 32 MiB to 128 MiB, with a short comment naming the measured modules. The input and tool limits and the per-command deadline (`command_timeout_ms`, default 30,000 ms) are unchanged. - Output over the budget still fails closed. Output size depends on the module's shape, not only its size, so this does not promise that every module under 8 MiB decodes. - The Evidence limitation sentence now takes its MiB figure from `WABT_LIMITS.outputBytes` instead of a literal, as its deadline already comes from `command_timeout_ms`. Apart from 128 MiB, its text is unchanged. - MCP delivery is unchanged. A complete result this large exceeds the default response budget and takes the existing `resource_constraint`/`transport` path with a retained Evidence reference. `detail: "summary"` (which runs the inspection again) and the paged `wat` view of `inspect_analysis_view` (which reads the retained record) then return it in small replies. - The real-WABT lane's rejected-module check, added on `main` in #5798, now expects the 128 MiB budget in the retained Evidence. - Over-budget failures are classified and excerpted by #2117, now on `main`. This branch is merged with it, and the shared budget sentence in `docs/wasm-artifacts.md` now states both changes. ## Contract and boundary impact - Semantic owner and earliest changed stage: `src/wasm/WabtRelease.ts` (WABT adapter limits). - Contribution scope fit and linked maintainer decision: not applicable; this tunes an existing adapter limit within the current WABT boundary. - CLI and MCP/tool-catalog contract: schemas are unchanged. Modules whose output is between 32 and 128 MiB now succeed through the CLI, and over MCP through the existing transport fallback, `detail: "summary"` and paged views. - Provider, bridge, target-format, or platform compatibility: WABT 1.0.42 only, unchanged. - Evidence, artifact, provenance, or reconstruction contract: `tool_profile.output_budget_bytes` reports 134217728 and the limitation text says 128 MiB, so successful Evidence for the same input gets a new `evidence_id`. - Target selection, authority, upstream behavior, and cleanup details (if applicable): - Process ownership, cancellation and cleanup are unchanged. - Captured output is buffered in proportion to what is produced, so the larger budget allocates nothing up front. - Over-budget failures keep #2117's bounded `resource_constraint` error, which is 4.75 KB on both builds. Before failing, the subprocess output is still captured up to the larger budget. A 28 KB nested-block module whose WAT exceeds 128 MiB therefore peaked at 1,133 MiB process-family memory, compared with 599 MiB on `main` (warm medians). - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact: none. Expected effects: - [x] Read existing files or captured data. - [x] Launch or attach to a process. (The existing WABT subprocesses; unchanged.) - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: - `src/wasm/WabtArtifactProvider.test.ts`: the success mode pins `tool_profile.timeout_ms`, `tool_profile.output_budget_bytes` and the rendered limitation text, so CI checks the budget. - `scripts/verify-wasm-artifact.mjs` (manual real-WABT lane) builds a deterministic code-heavy module with `wat2wasm` (3.87 MiB, 38.2 MiB of WAT): - The CLI decodes it completely, matching a standalone `wasm2wat` digest and the 128 MiB budget. - Over MCP the complete reply returns the transport constraint with a `retained-evidence` reference equal to the CLI Evidence ID. - `detail: "summary"` reports the same WAT digest, and a paged `wat` view returns its first code units. - The MCP child drops every spelling of `REA_MCP_MAX_RESPONSE_BYTES`, so this check does not depend on the caller's configuration. - A rejected module's retained Evidence reports the 128 MiB budget. - `tests/fixtures/wasm/artifact.ts` mirrors the new `output_budget_bytes`. - Base reproduction or other evidence: the three npm modules above fail on `main` and decode completely on this branch. Run against a `main` build, the updated lane fails only at the new >32 MiB case (`unreadable_output` at `d2e16b2`, `resource_constraint` since #2117). `rive.wasm` and `sqlite3.wasm` complete on both builds with byte-identical WAT (see the resource report). - User-visible CLI/MCP output: canvaskit reports `tool_profile.output_budget_bytes: 134217728` and the limitation "Each WABT command has a 30000 ms deadline and a fixed 128 MiB combined output budget. The deadline is not a whole-inspection budget. Oversized output fails instead of producing truncated success." - Remaining proof gaps: - Linux x64 only. macOS and Windows were not run. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed At head `4d5d626`, which merges `main` at `a4c183b` (including #5798's per-command deadline and #7822): Node 24.22.1 / npm 11.19.1 (official tarball, checked against `SHASUMS256.txt`), Ubuntu 24.04 x86_64, official WABT 1.0.42 linux-x64. - `npx vitest run src/wasm src/application/wasm tests/boundary/mcp/wasmArtifactMcp.test.ts` — 10 files, 167 tests passed. - `npm run verify:wasm:artifact -- --require-tools` — passed, 68 cases; again with `REA_MCP_MAX_RESPONSE_BYTES=268435456` — passed, 68 cases. - `npm run verify:wasm:package -- --require-tools` — 1 of 3 runs passed with 68 cases. The other 2 failed in process cleanup with `environment_errno_ESRCH`, an ownership-token read on a process that had just exited; no assertion failed. Plain `main` at `a4c183b` fails the same way in 3 of 3 runs. - The compiled CLI on resvg, rapier3d and canvaskit — complete, and every WAT SHA-256 equals a standalone `wasm2wat` run. - `npm run verify:test-discovery` — passed (2,857 files). - `npm run check:ci` — every task passes except `knip`, which reports `isArtifactFormatId` (`src/domain/artifactGraph.ts:103`) and a duplicate export in `src/domain/analysisResourceLimits.ts`. Plain `main` at `a4c183b` reports the same two findings. Earlier heads used Node 24.18.0 / npm 11.16.0 with the same platform and WABT. At head `8498414`, which merges `main` at `bee73fc` (including #2117, #2114 and #2464): - `npm run check:ci` — passed (0 lint errors). `npm run verify:test-discovery` — passed. - `npx vitest run src/wasm src/application/wasm tests/boundary/mcp/wasmArtifactMcp.test.ts` — 6 files, 62 tests passed. - `npm run verify:wasm:artifact -- --require-tools` — passed, 45 cases. - The compiled CLI on resvg, rapier3d, canvaskit, rive and sqlite — complete. Every WAT SHA-256 equals a standalone `wasm2wat` run, and the typed interface is `complete`. The over-budget module returns a 4,750 B `resource_constraint`. At head `68eafbb`, which includes `main` at `d2e16b2`: - `npm run verify:wasm:artifact -- --require-tools` — passed, 45 cases; again with `REA_MCP_MAX_RESPONSE_BYTES=268435456` set — passed, 45 cases. - `npm run verify:wasm:package` — passed, 45 cases from the packed and installed package. - The same lane against a `main` build — fails only at the new >32 MiB case, with `unreadable_output`. At head `da7dc2d`, which includes `main` at `d2e16b2`: - `npx vitest run src/wasm src/application/wasm tests/boundary/mcp/wasmArtifactMcp.test.ts` — 4 files, 43 tests passed. - `npm run check:ci` — every task passes except `lint:built`. Its only error is `max-lines-per-function` in `tests/boundary/providers/ghidra/ghidraLauncher.test.ts:431`, a file identical to `main` (#2470). - `npm run verify:test-discovery` — passed. - `git diff --check` — clean. At head `bb479a4`, merged locally with `main` at `2f3dee3`: - `npm run check:ci`, `npm run verify:test-discovery`, `npm run test:python` and `npm run test:prepare` — passed. - `npm run test:run` — 1044 files passed, 15 skipped (macOS/Hopper only); 10,760 tests passed. - The compiled CLI on the three npm modules — exit 0, and every WAT SHA-256 equals a standalone `wasm2wat` run. On the `main` build they exit 1 with `unreadable_output`. ### Resource report `main` = `d2e16b2` (32 MiB budget), branch = `68eafbb` (128 MiB), both built with `npm run build`. Linux x64, i5-13500 hybrid (6 P + 8 E cores), shared host (1-minute load 2.4–13), Node 24.18.0, WABT 1.0.42. These tables measure the PR's diff, which merges since then leave unchanged. The over-budget path changed on `main` with #2117, so it is re-measured at head `8498414` against `main` at `bee73fc`. Each run is one `systemd-run` unit with `-p CPUQuota=100% -p MemoryMax=4G` (the #2016 caps); "uncapped" drops the CPU quota. Elapsed is the unit's `Service runtime`, CPU is cgroup `cpu.stat usage_usec`, and peak is cgroup v2 `memory.peak` read inside the unit (node, WABT and `ps`; stdout is consumed outside the unit). Builds alternate ABBA. **Cold** runs first drop the module, WABT, node, `dist`, `scripts` and the 3,065 `node_modules` files REA opens from the page cache with per-file `posix_fadvise(DONTNEED)`: 0% stayed resident in 75 of 76 cold runs, and the 65.6–72.7 MiB read back from disk is charged to the unit, so cold peaks sit about 67 MiB above warm. **Warm** runs start with those files resident. Tokens are tiktoken 0.14 `o200k_base` counts of each whole output, not Claude's tokenizer. Cells are median (min–max). **Equivalent workflows (complete on both builds).** The WAT is byte-identical and equals standalone `wasm2wat --enable-all`. The JSON differs only in `evidence_id`, the budget sentence (twice) and `output_budget_bytes`. | Module | Run (n) | Build | Elapsed s | CPU s | Peak MiB | |---|---|---|---:|---:|---:| | `@rive-app/canvas@2.44.2` `rive.wasm` (29.4 MB WAT) | cold (13) | `main` | 4.38 (3.33–5.53) | 4.23 (3.20–5.38) | 555 (554–672) | | | | branch | 3.81 (3.11–5.50) | 3.68 (2.98–5.35) | 560 (554–674) | | | warm (13) | `main` | 3.92 (2.88–5.12) | 3.89 (2.87–5.11) | 490 (486–606) | | | | branch | 4.00 (3.12–5.47) | 4.03 (3.09–5.46) | 490 (487–605) | | `@sqlite.org/sqlite-wasm@3.53.4-build2` `sqlite3.wasm` (15.4 MB WAT) | cold (13) | `main` | 2.80 (2.48–3.74) | 2.67 (2.33–3.54) | 540 (538–547) | | | | branch | 2.88 (2.62–3.82) | 2.73 (2.46–3.59) | 539 (532–546) | | | warm (13) | `main` | 2.58 (2.23–3.78) | 2.56 (2.21–3.77) | 473 (465–478) | | | | branch | 2.54 (2.10–3.57) | 2.52 (2.08–3.54) | 473 (471–478) | Uncapped warm medians (n = 3), `main` → branch: rive 2.20 → 2.14 s elapsed, 3.35 → 3.44 s CPU, 606 → 604 MiB; sqlite 1.68 → 1.60 s, 2.34 → 2.34 s, 473 → 473 MiB. CLI stdout: rive 32,623,950 → 32,623,953 B and 7,228,545 → 7,228,539 tokens; sqlite 16,440,057 → 16,440,060 B and 2,970,947 → 2,970,949 tokens. rive's peak is bimodal on both builds (about 555 or 672 MiB cold, 488 or 605 MiB warm). Below 32 MiB the capture path is identical, so I attribute this to GC timing, but did not isolate it. **Newly completed modules.** `main` at `d2e16b2` fails with `unreadable_output`, returning 32 MiB of partial WAT in the error. Since #2117, `main` returns a 4.75 KB `resource_constraint` instead. The branch completes, and its WAT equals standalone `wasm2wat`. Cells are elapsed s / CPU s / peak MiB, median of 3. | Module (WAT) | Build | Cold | Warm | Uncapped warm | stdout B / tokens | |---|---|---|---|---|---| | resvg 2.6.2 (53.4 MB) | `main` | 3.25 / 3.10 / 560 | 3.53 / 3.53 / 492 | 1.72 / 2.75 / 498 | 34,037,036 / 3,361,201 | | | branch | 6.42 / 6.25 / 717 | 3.88 / 3.85 / 648 | 2.67 / 4.30 / 649 | 56,826,304 / 8,596,415 | | rapier3d 0.21.0 (77.1 MB) | `main` | 3.50 / 3.35 / 605 | 3.39 / 3.34 / 532 | 2.00 / 3.10 / 531 | 34,454,018 / 5,909,443 | | | branch | 6.58 / 6.43 / 914 | 6.33 / 6.29 / 841 | 3.23 / 5.14 / 847 | 79,385,834 / 10,154,493 | | canvaskit 0.42.0 (83.3 MB) | `main` | 6.18 / 6.06 / 768 | 4.29 / 4.28 / 691 | 2.48 / 4.11 / 708 | 34,630,017 / 7,388,388 | | | branch | 8.62 / 8.48 / 887 | 6.49 / 6.44 / 821 | 4.31 / 7.03 / 770 | 95,930,158 / 25,909,801 | **Over-budget failure path** (28 KB generated module, 147.3 MB WAT), at head `8498414` vs `main` at `bee73fc` (both include #2117). Both builds fail closed with `resource_constraint`. Warm runs, n = 3. | Build | Elapsed s | CPU s | Peak MiB | CLI error B | |---|---:|---:|---:|---:| | `main` | 4.10 (2.91–4.41) | 4.10 (2.91–4.40) | 599 (552–600) | 4,754 | | branch | 5.15 (4.61–6.18) | 5.15 (4.59–6.18) | 1,133 (1,108–1,247) | 4,750 | **MCP** (default 10 MiB reply budget, warm, 2 sessions per cell). Each session connects, calls `tools/list`, then makes (a) a complete `inspect_wasm_artifact` call, (b) the same call with `detail: "summary"` and (c) `inspect_analysis_view` for `wat` 0..4096. Tokens count the text content. Successful replies repeat it byte for byte in `structuredContent`, so a client that forwards both pays about twice. Session CPU and peak cover the SDK client, the REA server and WABT in one 1-CPU unit, and elapsed was within 0.07 s of CPU. | Module | Tokens (a)+(b)+(c), `main` → branch | Session CPU s, `main` → branch | Session peak MiB, `main` → branch | |---|---|---|---| | rive | 345+1,668+2,950 = 4,963 → 333+1,653+2,925 = 4,911 | 12.16, 14.14 → 13.67, 14.11 | 1,194, 1,179 → 1,159, 1,204 | | sqlite | 341+1,661+2,915 = 4,917 → 345+1,667+2,928 = 4,940 | 11.29, 9.95 → 11.44, 8.92 | 935, 908 → 943, 955 | | resvg | 636 (error) → 335+1,652+2,935 = 4,922 | 14.65, 11.12 → 12.53, 13.60 | 1,570, 1,465 → 1,515, 1,631 | | rapier3d | 632 (error) → 351+1,710+2,994 = 5,055 | 12.93, 12.47 → 15.26, 20.33 | 1,475, 1,481 → 1,967, 1,996 | | canvaskit | 640 (error) → 333+1,653+2,935 = 4,921 | 13.58, 21.84 → 25.61, 18.05 | 1,542, 1,600 → 2,135, 1,878 | | over-budget (`bee73fc` → `8498414`) | 612 + 612 (error) → 610 + 610 (error) | 12.23, 14.10 → 15.35, 10.42 | 1,063, 1,047 → 1,556, 1,717 | - Call (a) returns a 1,461 B transport constraint with a `retained-evidence` reference on success, or an 818 B error reference on failure. Call (b) runs the inspection again (1.6–10.8 s), and (c) reads the retained record (2–736 ms). When the module fails, (b) fails again and (c) returns `unsupported_target`. - `tools/list` (191 tools) is 2,636,597 B and 689,078 tokens, including 4,062 for `inspect_wasm_artifact` and 29,791 for `inspect_analysis_view`; the instructions add 159. Both are byte-identical on both builds, so the change is 0. An idle session (connect and `tools/list`) took 1.83–2.85 s CPU and 470–491 MiB on both builds. - A re-run with the same harness (36 CLI runs and 6 MCP sessions, 1-minute load 12.9–23.5) reproduced every CLI outcome, stdout size and SHA-256. Every peak was within 10%, apart from rive's two modes and one over-budget session. Elapsed and CPU ran 14–69% higher for the CLI and 42–79% higher for MCP, while branch vs `main` for rive and sqlite stayed between −8.4% and +1.4%. resvg warm CPU came out at 4.07 → 6.29 s, a 2.2 s increase rather than the 0.3 s above. Before #2117, one of 10 branch over-budget MCP calls returned `cleanup_incomplete` instead of `unreadable_output`. The retained error names a run-token mismatch for the `wasm2wat` PID, which had already exited 0 with no runtime root left. Linux returns an empty `/proc/<pid>/environ` when the owner exits between open and read. With traced runs and a forced race, the same false report reproduces on `main` (1 of 10 traced `main` sessions with a module that overshoots 32 MiB by the same 13 MB). It is a pre-existing ownership-check race whose odds depend on how far output overshoots the budget, not a change in this PR; 0 of 40 interleaved MCP calls hit it on either build. **Not measured:** - Claude tokenizer counts; tokens for tool-call arguments, JSON-RPC framing and client wrappers; and whether a given client forwards text, `structuredContent` or both. - Cold MCP sessions and cold uncapped runs. Shared libraries, `ps`, dentry and inode caches and filesystem metadata were not evicted, and `drop_caches` was not used on this shared host. - Follow-up calls: `get_evidence_bundle` and `export_evidence_bundle` (the remediation the transport error names), WAT pages past offset 0, other facets and `detail` levels, and retries (none occurred). - Per-process memory split, V8 heap and GC time, and the cause of rive's bimodal peak. Also concurrent inspections, long sessions holding several large retained records, memory caps below 4 GiB, temporary-file I/O, and caller-side handling of 57–96 MB of CLI JSON. - macOS, Windows, ARM, other Node or WABT versions, non-hybrid CPUs, and deadline headroom on slower hosts (the slowest branch CLI run here took 11.7 s end to end). - Modules near the 128 MiB boundary, significance tests (n = 13, 3 or 2 on a loaded hybrid-core host), and the CI cost of the changed tests. ## Compatibility, safety, and release - Breaking changes or migration steps: none. Calls that used to fail can now succeed, and successful Evidence for the same input gets a new `evidence_id`. - Real Hopper/Ghidra, browser, or OS coverage: real WABT 1.0.42 on Linux x64 only, through the source checkout and the packed package. - Package or release metadata impact: none. - Security, privacy, process, or containment review: - The same owned subprocesses, cleanup and fail-closed truncation apply. - Only the per-subprocess output ceiling grows. Under 1 CPU / 4 GiB caps, completing the three modules peaked at 648–841 MiB process-family memory (warm medians), compared with 492–691 MiB when `main` fails on them. Modules that already completed on `main` show no change. See the resource report. - Over-budget failures return #2117's bounded error, but capture up to 128 MiB first. The peak on that path rose from 599 to 1,133 MiB (see above). - Modules whose WAT exceeds 128 MiB, such as the tree-sitter PowerShell grammar reported in #2117, still fail. ## Author readiness <!-- The human owner completes these declarations before requesting review. The PR author is the owner by default. A different owner must explicitly accept ownership on this PR. AI agents must leave these declarations to a human to dictate to them or there could be catastrophic consequences for many users of REA. Draft PRs may leave them unchecked. These declarations record initial readiness when opening the PR for review; later pushes do not require a new human sign-off. Keep the description and validation evidence current as the work changes. See CONTRIBUTING.md#pull-request-ownership-and-readiness. --> Human owner: @eliot-h - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. - [x] User-visible CLI/MCP changes include representative output. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01BycevXKBAk4aDiZYQ1qJLU --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
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.

Dependencies

No dependencies set.

Reference
morluto/rea#5318
No description provided.