Skip to content

feat(skills): run WASM skill runtime via the runtime WasmSandbox#5835

Merged
houko merged 13 commits into
mainfrom
feat/wasm-skill-runtime
May 28, 2026
Merged

feat(skills): run WASM skill runtime via the runtime WasmSandbox#5835
houko merged 13 commits into
mainfrom
feat/wasm-skill-runtime

Conversation

@houko

@houko houko commented May 28, 2026

Copy link
Copy Markdown
Contributor

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::Wasm in the existing sandbox

SkillRuntime::Wasm was a dead stub (RuntimeNotAvailable("WASM skill runtime not yet implemented")), even though librefang-runtime already ships a hardened WasmSandbox (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 its host_call ABI need a KernelHandle, and librefang-skills must not depend on librefang-runtime (circular). execute_wasm_skill resolves the module path through the same validate_script_path guard the subprocess runtimes use (now pub), reads the .wasm/.wat, maps declared [requirements] capabilities to Capability grants (fail-closed — an unrecognised string is dropped, never granted), applies requirements.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 skills

New crate at sdk/rust/librefang-skill (its own workspace root, like the sidecar SDKs). One skill!(handler) macro emits the alloc/execute exports; the cdylib exports memory automatically. host:: wraps every built-in host method with its capability + fuel cost; pointer marshaling is gated to wasm32, envelope/pack/dispatch logic is target-agnostic and host-unit-tested.

Part 3 — authoring made real (docs / CLI / example / validation)

  • 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 (no WASI imports are provided). Rewritten to the real SDK flow.
  • librefang skill create: scaffold hard-coded entry = "src/main.py" for every runtime and emitted // TODO for non-Python. Now generates a correct per-runtime scaffold; wasm gets a cdylib Cargo.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 with kernel = None (pure-compute runs; host calls error gracefully) instead of "execution skipped".
  • publish.rs: a .wasm entry is validated against the \0asm magic. Convention is entry = "skill.wasm" at the skill root (the packager excludes target/).
  • example: examples/custom-skill-wasm/ (WASM twin of custom-skill-python).

Verification

Dev container (Dockerfile.rust-dev):

  • cargo clippy --no-deps -D warnings clean 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 through WasmSandbox, path traversal).
  • cargo test -p librefang-skills --lib — 201 passed; SDK cargo test — 7 passed.
  • End-to-end ABI: a real cargo build --target wasm32-unknown-unknown of an SDK-based skill (and the example) imports exactly librefang.host_call/host_log (no WASI), exports memory/alloc/execute, and carries the \0asm magic — the precise surface WasmSandbox instantiates.

Open questions for reviewers

  • Author-tunable fuel/memory: the manifest exposes timeout_secs; fuel (1M) and memory (16 MiB) use sandbox defaults. Add a [sandbox] block to skill.toml? (Left out as an expert-only knob.)
  • CI for sdk/rust/*: the sidecar SDK crates are not built/tested in PR CI either (only release.yml touches sdk/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.

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.
@github-actions github-actions Bot added size/L 250-999 lines changed area/docs Documentation and guides area/skills Skill system and FangHub marketplace area/runtime Agent loop, LLM drivers, WASM sandbox labels May 28, 2026
github-actions Bot and others added 2 commits May 28, 2026 18:12
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.
@github-actions github-actions Bot added the area/sdk JavaScript and Python SDKs label May 28, 2026
Evan 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.
@github-actions github-actions Bot added the type/skill New or updated skill (Python/JS/Prompt) label May 28, 2026
@cloudflare-workers-and-pages

Copy link
Copy Markdown

@github-actions github-actions Bot added size/XL 1000+ lines changed ready-for-review PR is ready for maintainer review and removed size/L 250-999 lines changed labels May 28, 2026
houko and others added 2 commits May 29, 2026 05:01
`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).
@github-actions github-actions Bot added area/ci CI/CD and build tooling has-conflicts PR has merge conflicts that need resolution and removed ready-for-review PR is ready for maintainer review labels May 28, 2026
Evan 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.
@houko
houko enabled auto-merge (squash) May 28, 2026 23:44
@houko
houko merged commit 17f2332 into main May 28, 2026
19 checks passed
@houko
houko deleted the feat/wasm-skill-runtime branch May 28, 2026 23:46
@github-actions github-actions Bot removed the has-conflicts PR has merge conflicts that need resolution label May 28, 2026
@github-actions github-actions Bot added the ready-for-review PR is ready for maintainer review label May 28, 2026
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci CI/CD and build tooling area/docs Documentation and guides area/runtime Agent loop, LLM drivers, WASM sandbox area/sdk JavaScript and Python SDKs area/skills Skill system and FangHub marketplace ready-for-review PR is ready for maintainer review size/XL 1000+ lines changed type/skill New or updated skill (Python/JS/Prompt)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant