Skip to content

CAMEL-25531: camel-wasm - Run WASI Preview 1 command modules - #27678

Draft
allthingssecurity wants to merge 2 commits into
apache:mainfrom
allthingssecurity:camel-wasm-wasi-runner
Draft

allthingssecurity wants to merge 2 commits into
apache:mainfrom
allthingssecurity:camel-wasm-wasi-runner

Conversation

@allthingssecurity

@allthingssecurity allthingssecurity commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Description

CAMEL-25531

Part 1 of 4 of the camel-wasm redesign agreed with @davsclaus on Zulip: the wasm endpoint now runs WASI Preview 1 command modules instead of calling a function through the custom alloc/dealloc and JSON message ABI. A Rust program built for wasm32-wasip1 or a Go program built with GOOS=wasip1 GOARCH=wasm can be used as it is.

Author: Shashank Mohan Jain (@allthingssecurity), working with Claude Code (Claude Opus 5.5).

What changes

  • URI: the path is the module, wasm:classpath:upper.wasm (any Camel resource: classpath, file:, http:...). The function name and the module option are gone.
  • Messages: the body is standard input (no body is empty input; a body that cannot be converted to an InputStream fails the exchange with InvalidPayloadException); standard output becomes the body (byte[]); the exit code is in CamelWasmExitCode (Integer) and standard error in CamelWasmStderr (UTF-8). A non-zero exit fails the exchange with a new WasmExitCodeException (exit code + stderr, body unchanged) unless failOnNonZeroExit=false. Both headers are removed before every run, so a value sent with the message, or left by an earlier wasm step, never survives (on success, non-zero exit, trap, output limit or timeout). Other headers are kept and are not passed to the module.
  • Instances: the module is parsed once per producer (doInit, so a bad module or bad options fail the route start) and every exchange runs on a new Endive instance, dropped afterwards. Exchanges share no guest memory or globals, there is no lock, and a trap or a timeout leaves nothing behind in a shared instance.
  • What the module sees: args (fixed by the route, argument zero is the file name of the module, such as upper.wasm: the location itself can carry credentials or host paths and is not passed on), WASI random_get backed by a shared SecureRandom (Endive's default is ThreadLocalRandom), environment variables only from environment.NAME=value and the headers listed in environmentHeaders (an allow-list of names, looked up like any Camel header; a missing header is left unset; a value with a NUL fails the exchange). Never the JVM environment. No preopened directories, no sockets. Clock and random are the host's. The standard streams are reported as non-terminals (Endive reports terminals unless told otherwise), so a program that checks for a terminal does not add colours or other terminal output to the body.
  • Limits (all off by default):
    • maxMemoryPages: Endive MemoryLimits on the module's memory; growing past it fails inside the guest. Lower than the module's initial memory is rejected at start.
    • timeout: the module runs on a cached thread pool of the producer (created in doStart only when a timeout is set, shutdownNow in doStop) and the route thread waits on the future. On expiry the worker is interrupted; Endive checks the interrupt flag on calls and backward branches, so a spinning guest stops at its next iteration. The exchange fails with ExchangeTimedOutException. Interrupting a producer thread keeps the interrupt flag away from route threads. No thread hop without a timeout.
    • maxOutputSize: cap on the buffered standard output and, separately, standard error. Exceeding it stops the module (the buffer throws a RuntimeException, which Endive does not turn into an EIO for the guest) and fails the exchange with a message naming the stream and the option.
  • Trap (for example a Rust panic): CamelExchangeException caused by Endive's TrapException, with the start of stderr in the message (Rust prints the panic message there) and CamelWasmStderr set; a hint is added when the memory is at its maximum.

New public types: org.apache.camel.wasm.WasiCommand (runs a parsed command module once, given stdin, stdout, stderr and the environment: the place where PR 2 adds the compiled machine factory and PR 3 can reuse it for the language), org.apache.camel.component.wasm.WasmExitCodeException, the two header constants in Wasm.Headers, WasmEndpoint.getModule(). Removed: WasmComponent.get/setModule, WasmConfiguration.get/setModule, the endpoint's functionName, WasmSupport.deserialize (only the old producer used it; the language still uses serialize); WasmProducer now takes a WasmEndpoint. WasmConfiguration.copy() now copies the environment map, so endpoints never share it.

Not changed: the wasm language (it still uses WasmFunction and the custom ABI; PR 3 rebases or drops it). The 4.23 upgrade-guide note about a failed call discarding the instance now only mentions the wasm expression, since the endpoint no longer keeps an instance.

Security model

Modules stay trusted route code, as in the security model page; the docs say that the limits are robustness controls, not a sandbox. Bodies and headers stay untrusted data: the body only reaches the module as standard input, headers only through the explicit allow-list, arguments are fixed by the route. No option relaxes a default, so none is marked insecure:*.

Docs and generated files

  • wasm-component.adoc rewritten for the WASI model: writing a module (Rust and Go), how a run works, what the module can access, limits (with the security-model sentence), an error table, performance, Java/XML/YAML examples. Catalog copy and components/wasm.json regenerated with the prepare-catalog mojo (identical to the component files); endpoint DSL (WasmEndpointBuilderFactory, StaticEndpointBuilders, EndpointHeaderBuilders) and component DSL description regenerated with the generate-endpoint-dsl / generate-component-dsl mojos.
  • 4.23 upgrade guide: === camel-wasm - the endpoint runs WASI command modules (Breaking change), next to the existing camel-wasm entries: what changed and how to migrate (turn the function into a program reading stdin and writing stdout, build it for WASI Preview 1).

Dependencies

Adds run.endive:wasi:${endive-version} (1.1.0, Apache-2.0, same project as the runtime already used; brings run.endive:log). Endive alignment in the parent POM (importing the Endive BOM so camel-quickjs's wasi 1.0.0 via quickjs4j 0.1.0 and camel-opa's runtime/compiler 1.0.0 via opa-java-wasm 1.1.0 move to 1.1.0) is not in this PR: it changes the Endive versions camel-opa and camel-quickjs run with and the generated camel-dependencies, so it needs their test suites and belongs with PR 2, where the compiler dependency (the one camel-opa already brings at 1.0.0) arrives. This PR does not add a new split: camel-quickjs already runs quickjs4j's wasi 1.0.0 on runtime 1.1.0, and the current SBOM already lists Endive runtime and wasm at both 1.0.0 and 1.1.0. @davsclaus if you would rather have the alignment in this PR, I will add it here.

Tests

WasmComponentTest is rewritten: 26 test methods (37 runs with the parameterized ones), plus WasiCommandTest. The modules are written in WAT in the test and compiled when the test runs with run.endive:wabt (already a test dependency), so no binary is added; the existing functions.wasm is used to check that a module without _start is rejected (and keeps serving WasmLanguageTest).

  • body to stdin, stdout to body, headers set, other headers kept; 1 MiB body; empty body
  • exit 3: WasmExitCodeException with code and stderr, body unchanged, headers set; same with failOnNonZeroExit=false (body = stdout); exit 0 through proc_exit with stderr
  • result headers sent with the message are replaced; trap, without and with a timeout (so also when the exception comes back from the producer thread): exception with cause TrapException and the stderr text, CamelWasmStderr set, inbound CamelWasmExitCode removed
  • environment: exactly LEVEL=debug and TENANT=acme from environment.LEVEL + environmentHeaders=TENANT,REGION while the message also carries SECRET and CamelHttpUri (and no JVM variables); header with NUL fails; a name in both options fails the start
  • args with a quoted argument, argument zero is args.wasm; the program name for file, classpath, ref, http(s) with user info and query, base64 and gzip locations; unbalanced quote fails the start
  • a body without an InputStream converter fails the exchange with InvalidPayloadException
  • random_get returns 32 bytes that differ between runs; WasiCommandTest asserts that the WASI options carry a SecureRandom
  • the module sees standard input, output and error as non-terminals (WASI file type 0, not 2)
  • timeout on a spinning module, twice on the same endpoint (ExchangeTimedOutException, no result headers), then awaits (Awaitility, 10 s) until no producer thread runs guest code; timeout endpoint across a route stop/start
  • maxMemoryPages=4: the guest that grows its memory ends at 4 pages (17 without the cap); a cap below the initial memory fails the start
  • maxOutputSize on stdout (1000 bytes pass at 1000, fail at 999) and on stderr
  • fresh state: a module with a global counter returns 1 on every exchange
  • 8 threads x 50 exchanges with distinct payloads, all correct
  • module without _start, module importing outside wasi_snapshot_preview1: start fails

Sensitivity: with the header removal deleted, the interrupt replaced by cancel(false), the memory limit not applied and the output cap disabled, 6 tests fail (resultHeadersSentWithTheMessageAreReplaced, trapFailsTheExchangeWithStandardError, timeoutInterruptsASpinningModule (the spinning guest is still running after 10 s), maxMemoryPagesLimitsMemoryGrowth, maxOutputSizeLimitsStandardOutput, maxOutputSizeLimitsStandardError). With Endive's default terminal streams, standardStreamsAreNotTerminals fails (222 instead of 000).

camel-wasm with install (formatter, import sort, generation): 47 tests, 0 failures (WasmComponentTest 37 + WasmLanguageTest 6 + WasmFunctionTest 3 + WasiCommandTest 1).

Performance

Measured through a route (ProducerTemplate.requestBody -> direct: -> wasm:), interpreted, 1 KiB ASCII body, Apple M4 Pro, JDK 21, Endive 1.1.0; local probe, not committed:

Module Size Parse (once) Per exchange, median p10 / p90
standard Go 1.26 GOOS=wasip1 program (io.ReadAll + bytes.ToUpper) 2.3 MB 0.2-0.4 s 27-28 ms (n=50 after 5 warm-up, two rounds) 26.8-27.5 / 29.4-32.4 ms
tiny no_std Rust WASI command (ASCII upper-case) 3.9 KB < 10 ms 270-283 us (n=2000 after 300 warm-up) 247-256 / 327-351 us

A fresh instance per exchange is cheap for a small module; a standard Go program spends most of each run initializing the Go runtime again. Per-exchange cost of this design vs the old endpoint: one instantiation (data segments copied, Go's are large) plus a WASI context, no lock. The optional compiler (PR 2) cut the Go case to about 5 ms per exchange in the earlier prototype; TinyGo/Rust/C guests avoid most of it. This is why PR 4 will show both Rust and Go and state the numbers.

Plan

  1. This PR: WASI runner, endpoint and limits replacing the custom ABI, docs and upgrade note.
  2. Optional runtime compiler (compile=true, off by default because of native images), docs for the Endive build-time compiler plugin, Endive version alignment in the parent POM.
  3. The wasm language: rebase it on WasiCommand, or drop it.
  4. Rust and Go examples in camel-examples.

@davsclaus please review.

Target

  • I checked that the commit is targeting the correct branch (Camel 4 uses the main branch)

Tracking

  • If this is a large change, bug fix, or code improvement, I checked there is a JIRA issue filed for the change (usually before you start working on it).

Apache Camel coding standards and style

  • I checked that each commit in the pull request has a meaningful subject line and body.
  • I have run mvn clean install -DskipTests locally from root folder and I have committed all auto-generated changes.
    (I built and tested camel-wasm with install, including the formatter and import-sort plugins, and regenerated the catalog copy and the endpoint and component DSL with the camel-package-maven-plugin mojos. I did not run the full root build.)

AI-assisted contributions

  • If this PR includes AI-generated code, commits have proper co-authorship attribution (e.g., Co-authored-by trailers) and the PR description identifies the AI tool used.
    Prepared by Shashank Mohan Jain (@allthingssecurity) with Claude Code (Claude Opus 5.5). The commit carries a Co-Authored-By trailer.

Claude Code on behalf of allthingssecurity

🤖 Generated with Claude Code

The wasm endpoint now runs a WASI Preview 1 command module (a Rust
program built for wasm32-wasip1, a Go program built with GOOS=wasip1
GOARCH=wasm, ...) instead of calling a function through the custom
alloc/dealloc and JSON message ABI, which is removed from the endpoint.

- The URI path is the module: wasm:classpath:upper.wasm.
- The body is standard input, standard output becomes the body; the
  exit code and standard error are in CamelWasmExitCode and
  CamelWasmStderr. A non-zero exit fails the exchange with a
  WasmExitCodeException unless failOnNonZeroExit=false.
- The module is parsed once per producer and every exchange runs on a
  new instance, so exchanges share no guest state and need no lock.
- Environment only from environment.* and the environmentHeaders
  allow-list; fixed args; no preopened directories, no sockets.
- Limits: maxMemoryPages, timeout (the module runs on a producer thread
  that is interrupted) and maxOutputSize for the buffered output.
- WasiCommand runs a module once and is the place where the optional
  compiler and the language can plug in later.

The wasm language is unchanged. Docs, catalog, endpoint and component
DSL regenerated; 4.23 upgrade-guide entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🌟 Thank you for your contribution to the Apache Camel project! 🌟
🤖 CI automation will test this PR automatically.

🐫 Apache Camel Committers, please review the following items:

  • First-time contributors require MANUAL approval for the GitHub Actions to run
  • You can use the command /component-test (camel-)component-name1 (camel-)component-name2.. to request a test from the test bot although they are normally detected and executed by CI.
  • You can label PRs using skip-tests and test-dependents to fine-tune the checks executed by this PR.
  • Build and test logs are available in the summary page. Only Apache Camel committers have access to the summary.

⚠️ Be careful when sharing logs. Review their contents before sharing them publicly.

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this is a solid redesign: running WASI command modules with stdin/stdout is a much more natural contract than the custom alloc/JSON ABI, and the sandboxing defaults (no preopens, no inherited env, null streams) plus the 24 tests are very good to see.

One thing needs fixing before merge: the WASI random_get source (see inline). Two smaller points:

  • argv[0] is the raw module location (inline), which can carry credentials for an http: resource.
  • The PR body still contains the section "Notes (for the author, remove before opening)" referring to a local findings/wasm-evidence/... path; please remove it.

Questions (not blocking):

  • WasmProducer: message.getBody(InputStream.class) returns null for a body without an InputStream converter (e.g. a POJO), which silently becomes empty stdin. Would getMandatoryBody(InputStream.class) for a non-null body be better?
  • Timeout mode uses an unbounded cached pool; a module stuck in a host call that ignores interrupts keeps its thread. Acceptable, or worth a cap?

Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying. It is a static review against the project conventions and does not replace static analysis or specialized review tools.

Comment thread components/camel-wasm/src/main/java/org/apache/camel/wasm/WasiCommand.java Outdated
…, file name as argument zero, fail on a body that is not a stream

- WASI random_get is served by a shared SecureRandom instead of Endive's default ThreadLocalRandom
- argument zero is the file name of the module, not its location, which can carry credentials or host paths
- a non-null body that cannot be converted to an InputStream fails the exchange instead of becoming empty
  standard input

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

🧪 CI tested the following changed modules:

  • catalog/camel-catalog
  • components/camel-wasm
  • docs
  • dsl/camel-componentdsl
  • dsl/camel-endpointdsl

🔬 Scalpel shadow comparison — Scalpel: 9 of 704 tested, 3 compile-only — current: 9 all tested

Maveniverse Scalpel detected 9 affected modules (current approach: 9).

Skip-tests mode would test 9 modules (5 direct + 0 downstream), skip tests for 3 (generated code, meta-modules)

Modules Scalpel would test (9)
  • camel-jbang-mcp ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
  • camel-jbang-plugin-mcp ← depends on affected reactor module org.apache.camel:camel-jbang-core
  • camel-jbang-plugin-route-parser ← depends on affected reactor module org.apache.camel:camel-route-parser
  • camel-jbang-plugin-tui ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
  • camel-jbang-plugin-validate ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
  • camel-launcher-container ← depends on affected reactor module org.apache.camel:camel-launcher
  • camel-wasm ← components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmConfigurationConfigurer.java, components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointConfigurer.java, components/camel-wasm/src/generated/java/org/apache/camel/component/wasm/WasmEndpointUriFactory.java, components/camel-wasm/src/generated/resources/META-INF/org/apache/camel/component/wasm/wasm.json, components/camel-wasm/src/main/docs/wasm-component.adoc, components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmComponent.java, components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmConfiguration.java, components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmEndpoint.java, components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmExitCodeException.java, components/camel-wasm/src/main/java/org/apache/camel/component/wasm/WasmProducer.java, components/camel-wasm/src/main/java/org/apache/camel/wasm/WasiCommand.java, components/camel-wasm/src/main/java/org/apache/camel/wasm/Wasm.java, components/camel-wasm/src/main/java/org/apache/camel/wasm/WasmSupport.java, components/camel-wasm/src/test/java/org/apache/camel/component/wasm/WasmComponentTest.java, components/camel-wasm/src/test/java/org/apache/camel/wasm/WasiCommandTest.java, own pom components/camel-wasm/pom.xml changed
  • camel-yaml-dsl-validator ← depends on affected reactor module org.apache.camel:camel-catalog
  • camel-yaml-dsl-validator-maven-plugin ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
Modules with tests skipped (3)
  • camel-itest
  • camel-yaml-dsl
  • camel-yaml-dsl-deserializers

ℹ️ Shadow mode — Scalpel observes but does not affect test execution. Learn more

All tested modules (36 modules, 5m 58s total)

Total reactor time: 5m 58s

Module Duration Status
Camel :: Launcher 51.6s SUCCESS
Camel :: JBang :: MCP 44.2s SUCCESS
Camel :: JBang :: Plugin :: Kubernetes 28.9s SUCCESS
Camel :: Catalog :: Camel Catalog 27.1s SUCCESS
Camel :: YAML DSL :: Validator 24.2s SUCCESS
Camel :: Component DSL 22.9s SUCCESS
Camel :: YAML DSL 22.5s SUCCESS
Camel :: JBang :: Plugin :: Validate 17.8s SUCCESS
Camel :: Docs 17.0s SUCCESS
Camel :: Wasm 16.3s SUCCESS
Camel :: Kamelet Main 14.5s SUCCESS
Camel :: YAML DSL :: Deserializers 9.5s SUCCESS
Camel :: Catalog :: Camel Route Parser 9.3s SUCCESS
Camel :: JBang :: Plugin :: Testing 8.9s SUCCESS
Camel :: Catalog :: Camel Report Maven Plugin 7.7s SUCCESS
Camel :: All Components Sync point 6.0s SUCCESS
Camel :: YAML DSL :: Validator Maven Plugin 5.1s SUCCESS
Camel :: YAML DSL :: Maven Plugins 3.8s SUCCESS
Camel :: Catalog :: Maven 3.4s SUCCESS
Camel :: Catalog :: Suggest (deprecated) 3.3s SUCCESS
Camel :: Assembly 2.1s SUCCESS
Camel :: JBang :: Plugin :: MCP 1.9s SUCCESS
Camel :: Coverage 1.8s SUCCESS
Camel :: JBang :: Plugin :: Edit 1.4s SUCCESS
Camel :: JBang :: Integration tests 1.3s SUCCESS
Camel :: JBang :: Plugin :: Generate 1.1s SUCCESS
Camel :: Catalog :: Dummy Component 1.0s SUCCESS
Camel :: Catalog :: Console 0.9s SUCCESS
Camel :: JBang :: Main 0.8s SUCCESS
Camel :: Endpoint DSL :: Support 0.8s SUCCESS
Camel :: Launcher :: Container 0.7s SUCCESS
Camel :: JBang :: Plugin :: Route Parser 0.6s SUCCESS
Camel :: Endpoint DSL n/a
Camel :: Integration Tests n/a
Camel :: JBang :: Core n/a
Camel :: JBang :: Plugin :: TUI n/a

Top 20 slowest modules:

  • Camel :: Launcher (51.6s)
  • Camel :: JBang :: MCP (44.2s)
  • Camel :: JBang :: Plugin :: Kubernetes (28.9s)
  • Camel :: Catalog :: Camel Catalog (27.1s)
  • Camel :: YAML DSL :: Validator (24.2s)
  • Camel :: Component DSL (22.9s)
  • Camel :: YAML DSL (22.5s)
  • Camel :: JBang :: Plugin :: Validate (17.8s)
  • Camel :: Docs (17.0s)
  • Camel :: Wasm (16.3s)
  • Camel :: Kamelet Main (14.5s)
  • Camel :: YAML DSL :: Deserializers (9.5s)
  • Camel :: Catalog :: Camel Route Parser (9.3s)
  • Camel :: JBang :: Plugin :: Testing (8.9s)
  • Camel :: Catalog :: Camel Report Maven Plugin (7.7s)
  • Camel :: All Components Sync point (6.0s)
  • Camel :: YAML DSL :: Validator Maven Plugin (5.1s)
  • Camel :: YAML DSL :: Maven Plugins (3.8s)
  • Camel :: Catalog :: Maven (3.4s)
  • Camel :: Catalog :: Suggest (deprecated) (3.3s)

⚙️ View full build and test results

@allthingssecurity

Copy link
Copy Markdown
Contributor Author
  • random_get: fixed, see the inline reply (cd87fa0).
  • argv[0]: now the file name of the module, see the inline reply (cd87fa0).
  • PR body: the section "Notes (for the author, remove before opening)" is removed; the test list and counts are
    updated.
  • getBody(InputStream.class): confirmed, a body without an InputStream converter (a POJO) gave null and the
    module ran with empty standard input. In cd87fa0 a non-null body is read with
    getMandatoryBody(InputStream.class), so such an exchange fails with InvalidPayloadException; a null body is
    still empty input. Test: bodyThatIsNotAStreamFailsTheExchange.
  • camel-wasm suite after the change: 47 tests, 0 failures.

Claude Code on behalf of allthingssecurity

@davsclaus davsclaus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround. All review points are addressed: random_get is served by a shared SecureRandom (with tests on the WASI options and a module calling random_get), argument zero is now only the file name of the module (a stronger choice than sanitizing the URI), the notes are gone from the description, and a body that cannot be read as a stream now fails the exchange instead of becoming empty input. LGTM once CI is green.


Claude Code on behalf of davsclaus. This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying. It is a static review against the project conventions and does not replace static analysis or specialized review tools.

@davsclaus
davsclaus marked this pull request as draft October 11, 2026 06:57
@davsclaus

Copy link
Copy Markdown
Contributor

pending this for after the 4.23 release

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants