Skip to content

WASM plugins

The feature-gated WASM plugin system: manifest, capabilities, permissions, and signature verification.

Revka can load WebAssembly plugins to extend the runtime with custom tools, channels, memory backends, or observer hooks. Each plugin is a directory containing a manifest.toml and a .wasm module. The host discovers plugins on the local disk, optionally verifies an Ed25519 signature on the manifest, and exposes the loaded set over the CLI and the gateway API.

The whole subsystem is compile-time gated behind the plugins-wasm Cargo feature. A standard Revka build does not include it: the revka plugin CLI subcommand and the GET /api/plugins endpoint only exist in a binary built with that feature. Reach for this page when you are building a custom WASM extension, packaging one for distribution, or hardening which plugins your operator will load.

The plugin system has two gates, and both must be satisfied:

  1. Build gate — Revka must be compiled with the feature:

    Terminal window
    cargo build --release --features plugins-wasm

    plugins-wasm is part of the full feature set, so a --features full build also includes it.

  2. Config gate — turn the system on in config.toml:

    [plugins]
    enabled = true

If the feature is compiled out, the CLI subcommand and API route are absent entirely. If the feature is present but [plugins] enabled = false, the API responds with plugins_enabled: false and an empty plugin list, and no plugin tools are registered.

See Cargo feature flags & ADRs for the full feature matrix and Configuration overview for how config.toml is loaded.

All plugin settings live under [plugins], with signature policy nested in [plugins.security].

[plugins]
enabled = true # master switch (default: false)
plugins_dir = "~/.revka/plugins" # where plugins live (default shown)
auto_discover = false # auto-discover on startup (default: false)
max_plugins = 50 # max number of plugins (default: 50)
[plugins.security]
signature_mode = "disabled" # "disabled" | "permissive" | "strict" (default: "disabled")
trusted_publisher_keys = [] # hex-encoded Ed25519 publisher public keys
KeyTypeDefaultMeaning
plugins.enabledboolfalseMaster switch for the plugin system.
plugins.plugins_dirstring~/.revka/pluginsDirectory that holds plugin sub-directories. A leading ~/ is expanded to your home directory.
plugins.auto_discoverboolfalseAuto-discover and load plugins on startup.
plugins.max_pluginsusize50Maximum number of plugins that may be loaded.
plugins.security.signature_modestring"disabled"How unsigned/unverified plugins are handled. See Signature modes.
plugins.security.trusted_publisher_keysstring[][]Hex-encoded Ed25519 public keys of publishers you trust.

Each plugin is its own directory under plugins_dir. The host scans that directory and treats any sub-directory containing a manifest.toml as a plugin candidate:

~/.revka/plugins/
└── my-plugin/
├── manifest.toml # required
└── plugin.wasm # the compiled module (path from manifest.wasm_path)

A plugin’s directory name on disk does not have to match its manifest name, but when you install via the CLI the host copies the plugin into a directory named after manifest.name.

The manifest is TOML that deserializes into the host’s PluginManifest. name, version, wasm_path, and capabilities are required; the rest are optional.

name = "my-plugin"
version = "0.1.0"
description = "What this plugin does"
author = "Acme Corp"
wasm_path = "plugin.wasm" # path to the .wasm, relative to this manifest
capabilities = ["tool"] # one or more of: tool, channel, memory, observer
permissions = ["http_client", "file_read"]
# Optional — only present on signed plugins:
signature = "<base64url Ed25519 signature>"
publisher_key = "<hex Ed25519 public key>"
FieldTypeRequiredMeaning
namestringyesUnique plugin identifier.
versionstringyesPlugin version string.
descriptionstringnoHuman-readable description.
authorstringnoAuthor name or organization.
wasm_pathstringyesPath to the .wasm file, relative to the manifest.
capabilitiesstring[]yesWhat the plugin provides (see below).
permissionsstring[]noHost capabilities the plugin requests (see below). Defaults to empty.
signaturestringnoBase64url-encoded Ed25519 signature over the canonical manifest.
publisher_keystringnoHex-encoded Ed25519 public key of the signer.

capabilities declares what kind of extension a plugin is. The values are serialized as snake_case:

CapabilityMeaning
toolProvides one or more callable tools.
channelProvides a messaging channel implementation.
memoryProvides a memory backend.
observerProvides an observer / metrics backend.

A plugin may declare more than one capability. Today the host only wires tool-capable plugins into the agent’s tool registry — see Plugin tools.

permissions is the set of host capabilities a plugin requests. They are advisory metadata in the manifest (the host records and reports them); the values are serialized as snake_case:

PermissionGrants
http_clientMake HTTP requests.
file_readRead from the filesystem (within the sandbox).
file_writeWrite to the filesystem (within the sandbox).
env_readRead environment variables.
memory_readRead agent memory.
memory_writeWrite agent memory.

When [plugins] enabled = true, the runtime constructs a plugin host over plugins_dir and registers every tool-capable plugin as a tool in the agent’s registry. The tool name is the plugin’s manifest.name, and the description comes from the manifest description.

Each registered plugin tool currently advertises a single fixed parameter schema:

{
"type": "object",
"properties": {
"input": { "type": "string", "description": "Input for the plugin" }
},
"required": ["input"]
}

Plugin manifests can be signed with Ed25519. The signature covers the canonical manifest — the TOML content with the signature and publisher_key lines stripped — so a plugin can ship its own signature inside the same file it signs. Editing any other field invalidates the signature.

signature_mode controls how the host treats unsigned or unverifiable plugins:

ModeBehavior
disabledDefault. No signature checking. Every discovered plugin loads.
permissiveVerify when possible. Unsigned, untrusted, or invalid plugins log a warning but still load.
strictReject unsigned plugins, plugins signed by a key not in trusted_publisher_keys, and plugins whose signature fails to verify.

Verification has two parts. First the manifest’s publisher_key must appear in trusted_publisher_keys (case-insensitive hex match) — otherwise the plugin is untrusted. Then the Ed25519 signature is checked against the canonical manifest bytes. In strict mode an untrusted publisher, an invalid signature, or a missing signature all cause the plugin to be skipped during discovery.

To run a locked-down host, sign your plugins and pin their keys:

[plugins.security]
signature_mode = "strict"
trusted_publisher_keys = [
"a1b2c3d4e5f6...your-publisher-public-key-hex...",
]

For where this fits in Revka’s broader trust posture, see the Security model.

With a plugins-wasm build, the revka plugin subcommand manages the plugin directory:

Terminal window
revka plugin list

Prints each installed plugin as name vX.Y.Z — description, or No plugins installed.

See the CLI overview for global flags and environment setup.

The gateway exposes the loaded plugin set so the dashboard can show plugin status. The route only exists in a plugins-wasm build.

GET /api/plugins — Bearer token required.

GET /api/plugins
Authorization: Bearer rk_<token>

Response:

{
"plugins_enabled": true,
"plugins_dir": "~/.revka/plugins",
"plugins": [
{
"name": "my-plugin",
"version": "0.1.0",
"description": "What this plugin does",
"capabilities": ["tool"],
"loaded": true
}
]
}
FieldMeaning
plugins_enabledMirrors [plugins] enabled. When false, plugins is always empty.
plugins_dirThe configured plugin directory (unexpanded, e.g. ~/.revka/plugins).
plugins[].name / version / descriptionFrom the manifest.
plugins[].capabilitiesThe manifest capability list.
plugins[].loadedtrue when the resolved .wasm file exists on disk.

If the feature is enabled but the plugin directory does not exist, plugins is an empty array. The endpoint requires a paired bearer token — see Pairing & authentication to obtain one.