WASM plugins

August 24, 2026 · View on GitHub

CLAT can run WebAssembly components as in-process tools. A component implements the clat:plugin@0.1.0 WIT contract and is distributed as one .wasm file. End users do not need Node.js, Python, or a platform-specific plugin binary.

Choose WASM for portable local computation with a narrow capability set. Use MCP when the extension needs unrestricted networking, a long-lived service, an existing language runtime, or an out-of-process isolation boundary.

Runtime guarantees

Configured components run inside Wasmtime with no ambient authority:

  • no inherited environment variables;
  • closed stdin/stdout/stderr;
  • no usable sockets or network addresses;
  • only explicitly preopened filesystem directories;
  • 256 MiB memory limit;
  • a fresh fuel budget for every tool call;
  • epoch interruption connected to the run cancel token.

Fuel is consumed only while guest instructions execute. Waiting for a host model call or user question consumes neither fuel nor a wall-clock execution budget. Cancelling a run interrupts a spinning guest promptly and also aborts host waits.

Installation

Distributable components use clat-plugin.json and the transactional package store:

clat plugin inspect ./greeter-package
clat plugin install ./greeter-package --config-json '{"greeting":"Hola"}' \
  --accept-capabilities

Restart CLAT after activation. The package is content-addressed, and later updates can be rolled back with clat plugin rollback <id>.

Published components can also be discovered and installed through the signed market. Search and inspect are read-only; the CLI verifies the market, publisher, bundle and component package before one atomic activation:

clat plugin market search wasm
clat plugin market info dev.example.greeter
clat plugin market install dev.example.greeter --accept-capabilities

To publish a deterministic market artifact, first create the signed package directory, then run clat plugin pack <directory> --output <file.clatpkg>. See the plugin market and supply-chain model.

Legacy configuration

~/.clat/plugins.json remains a user-managed development/override path. Absence means no legacy components and no extra WASM startup cost.

{
  "digest": {
    "path": "~/.clat/plugins/digest.wasm"
  },
  "greeter": {
    "path": "./plugins/greeter.wasm",
    "config": { "greeting": "Hola", "upper": true }
  },
  "workspace-tools": {
    "path": "/opt/clat/workspace-tools.wasm",
    "dirs": ["/Volumes/Data"]
  }
}

It can also point directly at a manifest while developing:

{
  "dev.example.greeter": {
    "manifest": "~/.clat/plugins/greeter/clat-plugin.json",
    "config": { "greeting": "Hola" }
  }
}

The package's clat-plugin.json binds identity, version, component path, SHA-256, declared capabilities, config schema, and static system prompts:

{
  "manifestVersion": 1,
  "id": "dev.example.greeter",
  "name": "Greeter",
  "version": "1.0.0",
  "runtime": {
    "kind": "wasm-component",
    "entry": "greeter.wasm",
    "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  },
  "capabilities": {
    "tools": true,
    "prompts": true,
    "hostContext": true,
    "hostTools": ["read_file"]
  },
  "prompts": [{ "name": "greeting", "system": "Be concise and friendly." }],
  "configSchema": { "type": "object", "required": ["greeting"] }
}

For both installed and legacy manifest entries, the manifest path is authoritative: it cannot be combined with path, its entry must remain package-relative, its id must equal the plugins.json key, and its digest is always checked before compilation. The machine-readable schema is schemas/clat-plugin-manifest.schema.json.

For manifest-backed packages, capability declarations are an enforced ceiling: undeclared sampling, elicitation, host-context, and individual host-tool imports fail at the host boundary. Legacy path-only entries retain their existing behavior so previously built components continue to load.

path accepts:

  • ~/..., expanded against the user's home directory;
  • a relative path, resolved from ~/.clat;
  • an absolute path.

config is private to that plugin. The SDK's plugin_config::<T>() helper deserializes it; one plugin cannot see another plugin's object or host environment. A plugin that expects missing configuration receives an explicit error.

dirs lists extra host directories that may become read-write only under Full Access and only for a component declaring write-capable tools. It does not grant anything by itself.

Restart CLAT after changing the file. Components appear in /mcp with transport wasm; tools are named wasm_{plugin}_{tool}.

Filesystem capability model

Every component receives the project root as guest path project, initially read-only. Extra directories are preopened under a sanitized basename, falling back to dirN on an empty or colliding name.

The effective preopens are the intersection of the component's declared tool effects, current permission mode, configured directories, and stored write grant:

Mode/clientProject preopenExtra dirs
Read Onlyread-onlyabsent
Project Writeread-write only after grant for a write-capable componentabsent
Full Accessread-write only after grant for a write-capable componentread-write after the same exact-set grant
clat exec classic moderead-onlyabsent

Components declaring only read/pure tools never receive write authority, regardless of mode. A mode change rebuilds the instance before the next call so capabilities change immediately. In-memory guest state does not survive that rebuild; persist necessary state to a granted directory.

Filesystem write grants

Permission mode describes how much the user trusts the agent. It does not imply trust in globally installed third-party component code. Write preopens therefore require a separate approval bound to:

  • plugin name;
  • component SHA-256 digest;
  • exact host directory set that would become writable.

The first write-capable use asks with the digest and directories. Approval is recorded in ~/.clat/plugin-grants.json. Rebuilding the component, renaming it, or adding a directory changes the identity and asks again. A previously granted superset also covers a later subset.

A deny leaves that plugin's preopens read-only for the rest of the run. The next run asks again. Headless operation without a matching record cannot prompt and therefore remains read-only. Failure to persist an allow affects only reuse: the current run proceeds, and a later run asks again.

Delete a matching record—or the file—to revoke. A missing or malformed grants file is treated as no grants and does not prevent CLAT startup.

WIT contract

The world in wit/plugin.wit exposes one required export and four optional guest imports. Existing components that do not import newer host interfaces continue to instantiate.

Export: tools

list-tools() returns definitions containing:

  • name and description;
  • JSON Schema input;
  • one CLAT effect (pure, read, write, execute, network, external-read, destructive, or session-write).

call(name, arguments-json) returns a JSON string or an error string. The effect enters the same permission table as native and MCP tools; declaring it accurately is part of the component's security contract.

Import: sampling

sampling.create-message(request) borrows the active session model through the shared plugin-host bridge. It is permission-gated, usage-accounted, cancellable, and subject to the shared per-run sampling limits described in MCP sampling.

Import: elicitation

elicitation.elicit(form) asks primitive text, number, boolean, or single-choice fields through the frontend's user-question port. The result is typed, declined, or cancelled. Headless clients without a question frontend return an error.

Imports: config and host

config.get() returns only this plugin's configured JSON object.

host.context() returns a detached, bounded snapshot of the active project, run, session id, model surface, and available host tools. host.call-tool() can invoke only the audited native allowlist. It uses the same run-scoped permission policy, project path fence, cancellation token, and tool middleware pipeline as an agent-issued call; it is unavailable outside an active run.

This is the same semantic host contract that the DSH adapter reaches through MCP. Rust plugins do not depend on TypeScript or Cordis.

A component importing none of sampling, elicitation, or the general host bridge and declaring only pure/read tools is effectively a local bounded computation with read-only project access.

Authoring with the Rust SDK

The author SDK lives at sdk/clat-plugin. plugins/greeter is the smallest template.

wit_bindgen::generate!({ path: "../../wit", world: "plugin" });

#[derive(serde::Deserialize)]
struct GreetArgs {
    name: String,
}

fn greet_impl(args: GreetArgs) -> Result<String, String> {
    Ok(format!("Hello, {}", args.name))
}

clat_plugin::define_plugin! {
    tool "greet" desc("Greets one person.")
        effect(Pure) schema(GREET_SCHEMA) args(GreetArgs) call(greet_impl);
}

The macro generates the WIT Guest implementation, tool listing, JSON argument decoding, and result serialization. Handlers accept typed arguments and can return any serializable value.

Host imports are ordinary generated functions:

clat::plugin::sampling::create_message(request);
clat::plugin::elicitation::elicit(form);
clat::plugin::host::context();
clat::plugin::host::call_tool("read_file", r#"{"path":"README.md"}"#);

Plugins using define_plugin! also receive typed helpers host_context::<T>() and call_host_tool::<A, T>() alongside plugin_config::<T>().

The SDK pins versions validated with the host. Because proc-macro resolution still requires direct dependencies, plugin crates should copy the wit-bindgen, serde, and serde_json versions from plugins/greeter/Cargo.toml.

Implementing the generated Guest trait by hand remains supported; see plugins/probe and plugins/read for lower-level examples.

Build and install

Install the WASI Preview 2 target once:

rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2

Point the plugin's path at target/wasm32-wasip2/release/<name>.wasm, restart CLAT, and inspect /mcp.

For distribution, publish the component and clat-plugin.json. The manifest records:

  • tool names and effects;
  • configuration schema;
  • imported host services;
  • files/directories it expects;
  • whether writes require Project Write or Full Access.

Failure behavior

  • A missing, malformed, incompatible, or uninstantiable component fails only that plugin and appears in /mcp diagnostics.
  • A fuel, memory, or epoch trap becomes a tool error; it does not kill CLAT.
  • Invalid tool JSON or an unknown tool name becomes a bounded tool error.
  • A denied write grant gives the guest physical read-only preopens, so a buggy tool cannot bypass the policy by ignoring its own return path.
  • Mode changes and grant changes rebuild the guest instance, intentionally invalidating in-memory state.

These failure boundaries make WASM suitable for local extensions, but not a substitute for reviewing third-party code and declared effects.