feat(plugins): wire up Extism WASM execution bridge (Phase 2 D2 plumbing)#5913
Conversation
Completes the WASM plugin tool bridge that was previously a TODO placeholder. Plugins can now run inside ZeroClaw with sandboxed host-function access. - Add extism 1.21 + reqwest blocking dependencies to zeroclaw-plugins - New runtime.rs: Extism plugin creation + zc_http_request / zc_env_read host functions, each gated on a PluginPermission - WasmTool::execute now calls into the Extism runtime via spawn_blocking (Extism Plugin is !Send, so a fresh instance is created per call) - WasmTool::from_wasm reads tool metadata from the plugin's tool_metadata export, replacing the hardcoded placeholder schema - PluginHost::tool_plugin_details exposes resolved wasm_path + permissions so the runtime can construct WasmTools correctly - Runtime plugin loader (tools/mod.rs) switched to from_wasm - Hash derive added to PluginPermission for HashSet usage - ADR-003 records the Extism choice (retroactive, per Documentation Standards wiki) - docs/reference/api/plugin-protocol.md specifies the plugin JSON contract so authors can write plugins without a ZeroClaw-specific SDK (they depend on extism-pdk directly) - AGENTS.md added for the zeroclaw-plugins crate - wasm_channel.rs doc comment notes it is Phase 3 work (channel bridge not yet connected) Implements the Phase 2 "Complete WASM execution bridge with Extism" deliverable from the Intentional Architecture RFC. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
WareWolf-MoonWall
left a comment
There was a problem hiding this comment.
PR Review — #5913 feat(plugins): wire up Extism WASM execution bridge
Reviewing on current HEAD. No prior review threads; no active blocks. First review.
What this change does
Wires up a real Extism-based WASM execution bridge inside crates/zeroclaw-plugins,
replacing the TODO placeholder in WasmTool with a working implementation. The core
addition is runtime.rs — a 275-line execution layer that loads WASM plugins via
ExtismPlugin, enforces PluginPermission gates before each privileged host function
call, and drives execution through a spawn_blocking wrapper (correct for !Send
Extism handles in an async context). Host functions exposed to plugins are
zc_env_read (env var access) and zc_http_request (outbound HTTP). The bridge is
entirely behind the plugins-wasm feature flag; default builds are unaffected.
Supporting the bridge: host.rs gains a tool_plugin_details() method returning
(manifest, wasm_path) tuples; wasm_tool.rs gets a from_wasm constructor with
graceful degradation when the tool_metadata WASM export is absent; lib.rs exposes
the new runtime module and adds Hash to PluginPermission for HashSet use.
The one external touch is crates/zeroclaw-runtime/src/tools/mod.rs — a call-site
rename from WasmTool::new to WasmTool::from_wasm, no logic change.
Documentation lands in two new files: adr-003-wasm-extism-plugin-model.md (retroactive
ADR with an explicit threat model section) and plugin-protocol.md (plugin author
protocol reference for extism-pdk consumers). A per-crate crates/zeroclaw-plugins/AGENTS.md
is also added.
Blast radius: Zero on default builds. On plugins-wasm builds, the new host
functions (zc_env_read, zc_http_request) are the security surface. The zeroclaw-runtime
touch is cosmetic — one renamed constructor, no behavioral change. The Cargo.lock
expansion is significant (Extism pulls in Cranelift and wasmtime internals) but is
fully gated.
✅ What's solid
Permission check placement is right. Both handle_env_read and handle_http_request
gate on PluginPermission before touching any privileged resource — a plugin that lacks
the permission never reaches the syscall or the network stack. ✅
spawn_blocking is the correct answer for !Send Extism handles, and the comment
says why. The inline explanation of the !Send constraint is exactly what a future
reader needs before reaching for a "simplification." Worth keeping as-is. ✅
from_wasm degrades gracefully on a missing tool_metadata export. For a runtime
where third-party authors control the binary, tolerating missing optional exports is
the right default posture. ✅
PluginPermission gets Hash cleanly. Minimal, correct, nothing disturbed. ✅
plugins-wasm fully contains the Extism/Cranelift footprint. Default binary is
untouched; users opt into the weight consciously. ✅
ADR-003 names the gaps explicitly. SSRF, env exfiltration, CPU exhaustion — calling
them out in the threat model section creates an auditable record that the tradeoffs were
considered rather than missed. That's the right approach. ✅
plugin-protocol.md is complete. A plugin author working against extism-pdk has
everything they need without reading Rust internals. ✅
🟡 ADVISORY — SSRF exposure in handle_http_request
handle_http_request forwards plugin-supplied URLs to reqwest with no domain
allowlist, no IP range restriction, and no SSRF protection. A plugin with the
http_client permission can issue requests to:
- Internal RFC-1918 addresses (
10.x.x.x,172.16.x.x,192.168.x.x) - Link-local addresses (
169.254.169.254— AWS IMDS, GCP metadata server) localhostand loopback on any port- Any external host for data exfiltration
The PR description notes that "plugins can do less than a native tool, not more" and
cites native tools as also being unrestricted. That's accurate, but native tools are
authored in Rust and reviewed in this repo; WASM plugins are third-party binaries. The
permission model implies a trust boundary that the HTTP host function doesn't enforce.
This doesn't need to block the PR — the plugins-wasm flag gates the exposure and
http_client permission must be explicitly granted — but the gap should be tracked.
Worth opening a follow-up issue now — "SSRF protection for zc_http_request: domain
allowlist or IP range deny-list" — and referencing it in adr-003 under known gaps.
The fix (URL parsing + IP range rejection before the reqwest call) is self-contained
and can slot into a future sprint, but having a tracking issue means it doesn't quietly
ship in a release without a conscious decision to address it or accept it.
🟡 ADVISORY — zc_env_read exposes the full process environment
A plugin with the env_read permission can read any variable by name: OPENAI_API_KEY,
CARGO_REGISTRY_TOKEN, DATABASE_URL, AWS_SECRET_ACCESS_KEY — any secret present
in the process environment at runtime. The PR acknowledges "same mechanism native tools
already use," which is true, but again: native tools are in-repo Rust; WASM plugins are
third-party binaries whose source is not reviewed here.
The present design is reasonable as a v1 foundation, but an allowlist path is worth
establishing early while the ABI is still being cut.
Same ask: a follow-up issue — "zc_env_read allowlist: restrict to ZC_PLUGIN_*
prefix or a config-defined allowlist" — so the gap has a home before it ships in a
release. Doesn't need to hold this PR.
🟡 ADVISORY — 120-second reqwest timeout inside spawn_blocking
reqwest::blocking::Client is built with a 120-second timeout. The spawn_blocking
task holds a thread from Tokio's blocking pool for the entire duration of any hung
request. There is no outer timeout on the spawn_blocking call itself.
In an agent-loop context where tool calls are user-facing, a two-minute thread hold on a
stalled HTTP request is a long time — especially since the blocking pool is finite. A
misconfigured or adversarial plugin with http_client permission could stall the pool
by holding multiple threads simultaneously.
The 120-second value may be intentional (large file downloads, slow inference
endpoints), but it's currently undocumented.
A comment in runtime.rs explaining why 120s is the right ceiling here would be the
minimum — even just "large file downloads / slow inference endpoints" is enough context.
If the value is conservative rather than deliberate, 30s default with a per-plugin
config override is worth a follow-up. Not holding the PR for this.
🟡 ADVISORY — Labels need a fix; milestone worth a conversation
Labels: The PR carries dependencies, docs — the auto-labeler keyed on the
Cargo.lock wall and the two new doc files, but that's not what this is. It should be
at minimum enhancement, risk: medium, size: L. Easy fix.
Milestone: Not set, and worth a quick check-in. The v0.7.4 scope boundary is
"no runtime changes" — this PR touches crates/zeroclaw-runtime/src/tools/mod.rs
(a one-line constructor rename, no behavioral change, but it's still a touch on that
path). Technically it could slot into v0.7.4 on the strength of being purely additive
and feature-gated.
The bigger question is whether there's a user or contributor actively asking for WASM
plugin support right now. If this is ahead of demand — building infrastructure before
the first person is blocked on it — it might be cleaner to park it for a dedicated
plugin milestone (v0.8.0 or later) rather than merge it into a maintenance window. If
someone is waiting on this, that changes the calculus entirely. What's the driver?
🔵 SUGGESTION — Fallback schema is duplicated across two error arms in from_wasm
The fallback serde_json::Value that substitutes when tool_metadata is unavailable
is duplicated verbatim in (at least) two match arms: the call_tool_metadata error
path and the create_plugin error path. Both arms construct the same literal object.
Extract it:
fn default_tool_schema(name: &str, wasm_path: &Path) -> serde_json::Value {
serde_json::json!({
"name": name,
"description": format!("WASM plugin loaded from {}", wasm_path.display()),
// ... rest of the fallback fields
})
}
Then both arms call default_tool_schema(name, wasm_path). This is a small DRY fix —
the fallback schema is a promise to callers and should have exactly one definition site.
Not a merge blocker, but the duplication will bite the next person who needs to change
the fallback shape.
🟢 NIT — crates/zeroclaw-plugins/AGENTS.md references runtime.rs by line number
Per-crate AGENTS.md files are useful. If runtime.rs is referenced by line number
for any "key section" pointers, those will drift as the file grows. Section name or
function name references age better than line numbers in living documents.
Gates
CI is passing. The cargo fmt, cargo clippy, and cargo test results are consistent
with the stated validation evidence. The Cargo.lock expansion is large but structurally
expected — Extism's dependency tree (Cranelift, wasmtime internals, cap-* crates) is
well-known, and gating it behind plugins-wasm means it does not affect default build
times or binary size. For plugins-wasm builds, contributors should be aware the
footprint increase is substantial; this is worth one line in the feature flag's
documentation.
The zeroclaw-runtime touch (WasmTool::new → WasmTool::from_wasm) is a pure
call-site rename. No logic change, no risk surface change.
Verdict
Approved. The execution bridge is correct, permission gate placement is right,
spawn_blocking is the right call for !Send Extism handles, and the feature flag
fully contains the blast radius. ADR-003 and plugin-protocol.md are both genuinely
useful — not boilerplate.
The advisory items are tracking issues and a comment, not code changes. The SSRF and
env allowlist issues are the ones worth opening now so they have a home before anything
plugin-related ships in a release. Labels are a quick fix.
The milestone question is the only thing I'd want a read on before this merges — if
there's demand pulling this in, ship it. If it's ahead of the queue, parking it for a
plugin-focused milestone keeps v0.7.4 clean and gives the SSRF/env work a natural
slot. Either way, solid foundation — the threat model thinking in ADR-003 is exactly
the right approach for this kind of capability.
Apply non-blocking advisory items from the review while leaving the two security follow-ups (SSRF, env allowlist) tracked as separate issues. - runtime.rs: document the 120s HTTP timeout ceiling and its blocking- pool implication (reviewer asked for rationale; large file downloads and slow inference endpoints justify the ceiling) - wasm_tool.rs: extract `default_schema()` so the fallback JSON Schema has a single definition site instead of duplicated verbatim in two match arms - adr-003: add "Known gaps (tracked follow-ups)" section linking #5918 (SSRF protection) and #5919 (env_read allowlist), plus the out-of-scope CPU exhaustion concern. Fixes stale `plugins/image-gen-wasm/` path reference. No behavior change. 27/27 tests still pass. Relates to #5912. See #5918 and #5919 for tracked gaps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for the thoughtful review. Addressing each item: Applied in this branch (commit fdbf446)
Tracked follow-ups (filed before merge, per your ask)
Both linked from ADR-003 so anyone reading the architecture record hits the tracking issues immediately. Open question — milestone
Phase 2 D2 of RFC #5574 is the scheduled driver; tracking issue #5912. No one is blocked on it today. Two ways to slot this:
I lean toward (2) myself since the review correctly identified that the permission model overpromises against the current host-function implementations. Landing the SSRF/env-allowlist work before the first plugin-capable release ships feels safer than shipping plumbing now and scrambling to harden it before someone installs a community plugin. But happy to ship in v0.7.4 if demand pulls it in — #5912 has no subscribers arguing for speed. What's your call? |
…iles) - workspace 版本 0.6.9 → 0.7.3(全部 14 子 crate + zeroclaw-huanxing 跟随) - observability-prometheus feature 追加 zeroclaw-gateway forward - Cargo.lock 采上游版本,由 cargo check 自动重算 huanxing 路径依赖 上游亮点:zeroclaw-labs#5168 HMAC 工具执行凭证 / zeroclaw-labs#5913 WASM 插件桥接 P2 / zeroclaw-labs#5817 cron 防雪崩 / zeroclaw-labs#5712 IMAP 轮询 / onboard 向导重写 / providers 兼容层重写。 验证: - cargo check --bin zeroclaw ✅(无 cfg 泄漏) - cargo check --bin zeroclaw --features huanxing ✅ - cargo clippy --bin zeroclaw --features huanxing ✅ 无错误 (83 个 collapsible_if 警告为 Rust 1.92 新 lint 触发预存代码,非合并引入) 待做(另起 commit):冒烟测试 sidecar 启动 + 桌面端 HASN 链路。
…ing) (zeroclaw-labs#5913) - 2dabdf1 feat(plugins): wire up Extism WASM execution bridge - a747e71 style(plugins): apply cargo fmt - fdbf446 refactor(plugins): address PR zeroclaw-labs#5913 review feedback
…ing) (zeroclaw-labs#5913) - 2dabdf1 feat(plugins): wire up Extism WASM execution bridge - a747e71 style(plugins): apply cargo fmt - fdbf446 refactor(plugins): address PR zeroclaw-labs#5913 review feedback
Summary
masterWasmTool::executewas a TODO placeholder returning"WASM execution bridge not yet implemented". This PR wires Extism 1.21 into the plugin runtime so WASM plugins can actually execute inside ZeroClaw, implementing Phase 2 D2 of the Intentional Architecture RFC.zc_http_requestforHttpClient,zc_env_readforEnvRead) enforce thePluginPermissionmodel at the host boundary — this is the first deliverable that actually validates the permission enum.WasmTool::from_wasmloads tool metadata (name, description, JSON Schema) from the plugin'stool_metadataexport, replacing the hardcoded placeholder schema previously used inall_tools_with_runtime().Tooltrait bridges to Extism's sync calls viatokio::task::spawn_blocking— ExtismPluginis!Send, so a fresh instance is created per call (instantiation is fast; module caching handles the expensive compile step).docs/reference/api/plugin-protocol.mdspecifies the JSON contracts so plugin authors can build againstextism-pdkdirectly with no ZeroClaw-specific SDK;crates/zeroclaw-plugins/AGENTS.mdfollows the per-crate template.WasmChannel(channel plugin bridge is Phase 3 per the RFC), and does NOT introduce a plugin registry client. The reference plugin lands in a follow-up stacked PR (feat/wasm-image-gen-plugin).--features plugins-wasmchange. Default builds, CI withoutplugins-wasm, and users who haven't enabled plugins see zero behavior change. Theextismdependency adds ~20-30 MB to builds that opt in.Closes #5912,Related #5574,Related #5576,Related #5617Validation Evidence (required)
cargo fmt --all -- --check cargo clippy -p zeroclaw-plugins --all-targets -- -D warnings cargo clippy -p zeroclaw-runtime --features plugins-wasm --all-targets -- -D warnings cargo test -p zeroclaw-pluginsCommands run and tail output:
cargo fmt --all -- --check— clean (no output).cargo clippy -p zeroclaw-plugins --all-targets -- -D warnings:cargo clippy -p zeroclaw-runtime --features plugins-wasm --all-targets -- -D warnings:cargo test -p zeroclaw-plugins:Beyond CI — what did you manually verify?
cargo build --target wasm32-wasip1 --release→ 285 KB.wasm).tool_metadataexport is invoked, JSON Schema is parsed, andexecuteruns with permission enforcement. 4 integration tests load the built plugin and verify error paths (missing prompt, invalid size, model traversal protection, metadata roundtrip).cargo check -p zeroclaw-runtimeWITHOUTplugins-wasmstill compiles — the feature gate is clean.If any command was intentionally skipped, why: Full workspace
cargo testnot run; the only code path touched is underplugins-wasmfeature and is exercised by the crate-scoped run above.Security & Privacy Impact (required)
No— WASM plugins run in Extism's sandbox with no filesystem access by default. Futurefile_read/file_writehost functions are declared inPluginPermissionbut not implemented here.Yes—zc_http_requesthost function allows WASM plugins to make HTTP calls viareqwest::blocking, but only when the plugin's manifest declareshttp_clientpermission. Without that permission, the host function returns a permission-denied error without touching the network.Yes—zc_env_readhost function lets plugins read environment variables (e.g., API keys), gated on theenv_readpermission. This is the same mechanism native tools already use; WASM plugins are no more privileged than native tool code.No.Compatibility (required)
Yes— changes are gated behind the existingplugins-wasmfeature flag. Users withoutplugins-wasmsee no difference.No— existing[plugins]config schema, CLI subcommands, and gateway API are unchanged.docs/reference/api/plugin-protocol.md.Rollback (required for
risk: mediumandrisk: high)Low-risk.
git revert <sha>is the plan — reverting reinstates the TODO placeholder inWasmTool::execute; existing plugin scaffolding (discovery, signatures, CLI) continues to work unchanged.i18n Follow-Through (required only when docs or user-facing wording change)
README*,docs/README*, anddocs/SUMMARY.mdfor supported locales?N.A.N.A.N.A.adr-003-*.md,plugin-protocol.md,AGENTS.md) are architecture/contributor-facing English-only per RFC RFC: Documentation Standards and Knowledge Architecture #5576 Documentation Standards ("All repository documents: English only"). No user-facing README/CLI/config wording changed.