feat(skills): run WASM skill runtime via the runtime WasmSandbox#5835
Merged
Conversation
The `SkillRuntime::Wasm` arm was a dead stub returning `RuntimeNotAvailable("not yet implemented")`, even though a hardened `WasmSandbox` already shipped in `librefang-runtime` — capability gating, fuel/memory/wall-clock metering, and the denial-of-wallet host-call reservations from #3532 / #3864 / #3866. Wire the two together so a `[runtime] type = "wasm"` skill actually executes.
Routing lives in `librefang-runtime` (`tool_runner/wasm_skill.rs`), not the skills loader: the sandbox and its `host_call` ABI need a `KernelHandle`, and `librefang-skills` must not depend on `librefang-runtime` (circular). The dispatcher branches on `runtime_type == Wasm` and calls `execute_wasm_skill`, which resolves the module path through the same `validate_script_path` containment guard the subprocess runtimes use (now `pub`), maps the skill's declared `[requirements] capabilities` to `Capability` grants (fail-closed: an unrecognised string is logged and dropped, never granted), applies `requirements.timeout_secs`, and feeds the guest the same `{"tool","input"[,"config"]}` envelope the Python/Node/Shell runtimes use.
Guest authoring tooling (an SDK helper for the `alloc`/`execute`/`host_call` ABI) is a separate concern and not part of this change.
Tests cover capability parsing (arg / no-arg / numeric variants and fail-closed garbage), an end-to-end echo module run through the sandbox, and path-traversal rejection.
Makes the WASM skill runtime actually usable: a guest-side SDK that hides the sandbox ABI (`alloc` / `execute` / `memory` exports, `librefang.host_call` / `host_log` imports) behind one `skill!(handler)` macro plus typed host-call wrappers. An author writes `fn(Request) -> Result<Value, String>` and builds a `cdylib` for `wasm32-unknown-unknown`.
- `sdk/rust/librefang-skill` is its own workspace root (mirrors the sidecar SDKs): a WASM guest must not transitively pull in any host-only crate, and an isolated `target/` keeps it off the kernel's shared one.
- `Request` carries the `{tool, input, config}` envelope — identical to what the Python / Node / Shell runtimes receive. A handler `Err` or a malformed envelope surfaces to the agent as `{"error": ...}`.
- `host::` exposes the built-in methods (`time_now`, `fs_*`, `env_read`, `kv_*`, `net_fetch`, `shell_exec`, `agent_send`, `agent_spawn`) with each method's capability and fuel cost documented; `host_call` / `log` are escape hatches.
- Pointer marshaling is gated to `wasm32`; the envelope decode, pack/unpack, and dispatch logic are target-agnostic and unit-tested on the host (7 tests).
- Ships `examples/echo.rs` and a README (crate-type, `panic = "abort"`, build command, matching `skill.toml`).
Verified end-to-end: a real `cargo build --target wasm32-unknown-unknown` of an SDK-based skill emits a module importing exactly `librefang.host_call` / `librefang.host_log` (no WASI) and exporting `memory` / `alloc` / `execute` — the exact surface `WasmSandbox` instantiates. Host clippy `-D warnings` clean; `cargo test` 7 passed.
added 2 commits
May 29, 2026 03:31
…nore - Add `resolve_capabilities_keeps_known_and_drops_unrecognized`: asserts the fail-closed contract at the integration point (manifest with mixed valid / garbage capability strings yields exactly the valid grants in order, garbage dropped) — previously only `parse_capability` was unit-tested in isolation. - `sdk/rust/librefang-skill/.gitignore`: also ignore `Cargo.lock`, matching the sidecar SDK convention (a library crate should not commit its lockfile).
…xample
The WASM runtime existed but the supporting pieces did not, so a skill author could not actually produce or test one. This fills those gaps.
- docs (`agent/skills` EN + zh): the WASM section described a `wasm32-wasi` + `_start` + stdin model that never matched the sandbox and would fail to instantiate (the sandbox provides only the `librefang` host imports, no WASI). Rewritten to the real `librefang-skill` SDK flow: `wasm32-unknown-unknown`, the `skill!` macro, the `{tool, input, config}` envelope, the host-call capability/fuel table, and accurate sandbox limits.
- `librefang skill create`: the scaffold hard-coded `entry = "src/main.py"` for every runtime and wrote a `// TODO` stub for anything non-Python. Now generates a correct per-runtime scaffold; for `wasm` a `cdylib` `Cargo.toml` (`librefang-skill` dep, `panic = "abort"`), a `src/lib.rs` with the `skill!` handler, the right `entry`, and the build/copy steps. Fixes the Node entry-path bug in passing.
- `librefang skill test`: WASM skills now run in the real sandbox with no kernel. `execute_wasm_skill` is now `pub` and re-exported from `tool_runner`; pure-compute tools run locally, capability-bearing host calls report an error instead of the previous "execution skipped".
- `publish.rs`: a `.wasm` entry is validated to start with the `\0asm` magic, catching an unbuilt placeholder or wrong path before publish. Convention is `entry = "skill.wasm"` at the skill root because the packager excludes `target/`.
- `examples/custom-skill-wasm/`: the WASM twin of `custom-skill-python`; builds to a valid wasm32-unknown-unknown module (verified to carry the `\0asm` magic).
Verification (dev container): clippy `--no-deps -D warnings` clean on cli/runtime/skills; `cargo test -p librefang-runtime --lib wasm_skill` 5 passed; `cargo test -p librefang-skills --lib` 201 passed; example compiles to wasm32 with correct magic bytes.
Deploying librefang-docs with
|
| Latest commit: |
cdab780
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://97955658.librefang-docs-web.pages.dev |
| Branch Preview URL: | https://feat-wasm-skill-runtime.librefang-docs-web.pages.dev |
`sdk/rust/librefang-skill` and `examples/custom-skill-wasm` are independent Cargo workspaces, so the kernel-workspace CI lanes never compile them — the SDK could rot or drift from the sandbox guest ABI unnoticed. Adds a `skill_sdk` path flag to the change router and a `WASM Skill SDK` job gated on it: fmt + clippy + test the SDK, and compile the example for `wasm32-unknown-unknown` (the target the sandbox loads). Gated to `sdk/rust/librefang-skill/` and `examples/custom-skill-wasm/` so unrelated PRs don't pay for it. Scoped deliberately to the crates this series introduces; the sidecar SDK crates remain uncovered by PR CI as before (separate concern).
added 5 commits
May 29, 2026 07:59
`WasmSandbox::execute` builds its own per-invocation engine (epoch isolation needs one engine per guest), so calling `WasmSandbox::new()` first only created and discarded a second engine on every WASM skill invocation. Construct the marker directly instead. A bad engine config still surfaces — `execute` reports it as `SandboxError::Compilation`. Surfaced by self-review of the WASM-skill series; no behavior change (the `echo_wasm_skill_runs_in_sandbox_and_returns_envelope` test still passes end to end).
`librefang skill create` for a `wasm` runtime wrote the raw skill name into the generated `Cargo.toml` `[package] name`. A skill name with characters a Cargo package name forbids (uppercase, spaces, a leading digit, punctuation) produced an invalid manifest, so the very first `cargo build` the scaffold tells the author to run would fail. Derive a legal package name (lowercased, non-`[a-z0-9_-]` mapped to `-`, leading-digit prefixed) for the `Cargo.toml`. The artifact name is already fixed to `skill` via `[lib] name`, so the package name only needs to be valid, not meaningful. The skill.toml `[skill] name` keeps the author's original name. Surfaced by self-review of the WASM-skill series.
Two correctness gaps found in self-review:
- is_error was hardcoded false, so a WASM skill returning the SDK error
envelope {"error": ...} surfaced to the agent as a success. Detect a
top-level error key and set is_error, matching the Python/Node runtimes
where a non-zero exit yields is_error = true.
- timeout_secs was passed through unclamped, letting a manifest request an
unbounded wall-clock budget. Clamp to MAX_SKILL_TIMEOUT_SECS (600s) like
the subprocess runtimes; treat 0 as unset (sandbox default).
Add wasm_skill_error_envelope_sets_is_error covering the error path.
# Conflicts: # .secrets.baseline
# Conflicts: # .secrets.baseline
houko
enabled auto-merge (squash)
May 28, 2026 23:44
# Conflicts: # .secrets.baseline
houko
added a commit
that referenced
this pull request
May 29, 2026
#5853) rustfmt flags several blocks merged via #5835, #5839, #5848, and #5849 (method chains and multi-line statements rustfmt collapses/reflows). main was therefore red on the Quality "Check formatting" step, which fails the Quality job on every open PR. Pure `cargo fmt` output — no logic change. Co-authored-by: Evan <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Makes WASM skills a first-class, usable runtime end to end — execution, authoring SDK, and the supporting pieces (docs / CLI / example / validation).
Part 1 — execute
SkillRuntime::Wasmin the existing sandboxSkillRuntime::Wasmwas a dead stub (RuntimeNotAvailable("WASM skill runtime not yet implemented")), even thoughlibrefang-runtimealready ships a hardenedWasmSandbox(capability gating, fuel/memory/wall-clock metering, denial-of-wallet host-call reservations from #3532 / #3864 / #3866).Routing lives in
librefang-runtime(tool_runner/wasm_skill.rs), not the skills loader: the sandbox and itshost_callABI need aKernelHandle, andlibrefang-skillsmust not depend onlibrefang-runtime(circular).execute_wasm_skillresolves the module path through the samevalidate_script_pathguard the subprocess runtimes use (nowpub), reads the.wasm/.wat, maps declared[requirements] capabilitiestoCapabilitygrants (fail-closed — an unrecognised string is dropped, never granted), appliesrequirements.timeout_secs, and feeds the guest the same{tool, input, config}envelope every runtime uses.Part 2 —
librefang-skill, a Rust SDK for authoring WASM skillsNew crate at
sdk/rust/librefang-skill(its own workspace root, like the sidecar SDKs). Oneskill!(handler)macro emits thealloc/executeexports; the cdylib exportsmemoryautomatically.host::wraps every built-in host method with its capability + fuel cost; pointer marshaling is gated towasm32, envelope/pack/dispatch logic is target-agnostic and host-unit-tested.Part 3 — authoring made real (docs / CLI / example / validation)
agent/skills, EN + zh): the WASM section described awasm32-wasi+_start+ stdin model that never matched the sandbox and would fail to instantiate (no WASI imports are provided). Rewritten to the real SDK flow.librefang skill create: scaffold hard-codedentry = "src/main.py"for every runtime and emitted// TODOfor non-Python. Now generates a correct per-runtime scaffold;wasmgets acdylibCargo.toml+skill!src/lib.rs+ build/copy steps. Node entry-path bug fixed in passing.librefang skill test: WASM skills now run in the real sandbox withkernel = None(pure-compute runs; host calls error gracefully) instead of "execution skipped".publish.rs: a.wasmentry is validated against the\0asmmagic. Convention isentry = "skill.wasm"at the skill root (the packager excludestarget/).examples/custom-skill-wasm/(WASM twin ofcustom-skill-python).Verification
Dev container (
Dockerfile.rust-dev):cargo clippy --no-deps -D warningsclean on cli / runtime / skills; SDK crate clippy clean.cargo test -p librefang-runtime --lib wasm_skill— 5 passed (parse known/garbage, resolve_capabilities fail-closed, end-to-end echo throughWasmSandbox, path traversal).cargo test -p librefang-skills --lib— 201 passed; SDKcargo test— 7 passed.cargo build --target wasm32-unknown-unknownof an SDK-based skill (and the example) imports exactlylibrefang.host_call/host_log(no WASI), exportsmemory/alloc/execute, and carries the\0asmmagic — the precise surfaceWasmSandboxinstantiates.Open questions for reviewers
timeout_secs; fuel (1M) and memory (16 MiB) use sandbox defaults. Add a[sandbox]block toskill.toml? (Left out as an expert-only knob.)sdk/rust/*: the sidecar SDK crates are not built/tested in PR CI either (onlyrelease.ymltouchessdk/rust). Wiring a CI lane for these independent workspaces is shared-infra scope not currently applied to the peer crates — happy to add if wanted.