Plugin Development

This guide covers the plugin surface that Carapace currently ships and loads at runtime.

The workflow documented here is:

  1. choose a plugin shape and WIT world
  2. build a WASM component against wit/plugin.wit
  3. load it locally through plugins.load.paths
  4. restart Carapace
  5. verify activation with cara plugins status and cara logs
  6. use cara plugins install / cara plugins update only when you want the managed distribution path

This guide is intentionally written around the public surfaces Carapace exposes:

What you can build

This guide covers these public plugin targets:

Plugin shape WIT world Notes
Tool tool-plugin Agent-callable tools
Webhook webhook-plugin HTTP handlers under /plugins/<plugin-id>/...
Service service-plugin Background lifecycle services
Channel channel-plugin Channel metadata + adapter exports, plus hook exports for channel lifecycle integration

Not covered here:

Two plugin workflows

Carapace has two distinct plugin workflows:

If you are writing a new plugin, start with plugins.load.paths.

Plugin identity, names, and manifest metadata

There are two different names you will see in the plugin tooling:

For simplest operations, keep the managed plugin name and the runtime pluginId the same.

Reserved managed plugin names:

Core manifest fields:

Optional manifest fields:

Carapace can load a plugin even if you do not embed explicit manifest metadata. The loader derives metadata in this order:

  1. plugin-manifest custom section, if present
  2. component export inspection for the plugin kind
  3. file name / file metadata fallbacks

Inference details:

For reproducible managed distribution, prefer explicit manifest metadata rather than relying on inference.

Build target

Carapace plugins are WebAssembly Component Model components. Target the package namespace declared in wit/plugin.wit:

package carapace:plugin@1.0.0;

Practical Rust setup with cargo-component:

cargo install cargo-component
cargo component new --lib my-plugin

Point your component target at Carapace's WIT file and choose the world that matches your plugin shape:

[package.metadata.component]
target = { path = "/absolute/path/to/carapace/wit/plugin.wit", world = "tool-plugin" }

The absolute path above is just an example. A path relative to your component crate or a package/dependency reference is also fine as long as it resolves to the same wit/plugin.wit contract.

Build:

cargo component build --release

Use the generated .wasm artifact from your component build output directory. The exact target/.../release/ path can vary by toolchain version; the thing that matters is the built component file.

Any toolchain that produces a valid WASM component for the same WIT contract is fine. Rust plus cargo-component is just the most direct path.

Fastest path: first tool plugin

If you are building your first plugin, start with a tool plugin. It has the smallest surface and the fastest edit/build/restart loop.

Recommended sequence:

  1. create a new component crate:

    cargo install cargo-component
    cargo component new --lib my-tool
  2. point it at Carapace's WIT and select the tool-plugin world:

    [package.metadata.component]
    target = { path = "/absolute/path/to/carapace/wit/plugin.wit", world = "tool-plugin" }
  3. implement the required exports for that world:

    • manifest.get-manifest()
    • tool.get-definitions()
    • tool.invoke(...)
  4. build the component:

    cargo component build --release
  5. copy the generated .wasm into a directory listed in plugins.load.paths

  6. restart Carapace and verify:

    cara plugins status --port 18789 --name my-tool
    cara logs -n 200 --port 18789

If that path works, then move on to webhook, service, or channel plugins. The local development loop stays the same; only the WIT world and required exports change.

Shape-specific contracts

Tool plugins

A tool plugin built against tool-plugin exports:

Its manifest kind should be tool.

Tool definition name rules:

Config and credential lookups are exact:

Carapace does not translate api_key to apiKey for you.

Webhook plugins

A webhook plugin built against webhook-plugin exports:

Webhook-specific behavior:

Service plugins

A service plugin built against service-plugin exports:

Service-specific behavior:

Channel plugins

A channel plugin built against channel-plugin exports:

Channel-specific behavior:

Advanced manifest path

If your build pipeline can embed a plugin-manifest custom section, Carapace will use it directly instead of inferring metadata from the file and component exports.

The JSON structure below shows the core PluginManifest fields:

{
  "id": "my-tool",
  "name": "My Tool",
  "description": "Example plugin",
  "version": "1.0.0",
  "kind": "tool"
}

This is the most predictable path for managed distribution because it avoids:

Embedding that custom section is toolchain-specific, so this guide keeps the main workflow on the plain cargo-component path and treats the custom-section manifest as an advanced option.

Local development walkthrough

Use plugins.load.paths for day-to-day development. Do not use state_dir/plugins for your normal edit/build loop.

Recommended config shape:

{
  plugins: {
    enabled: true,
    load: {
      paths: [
        "/absolute/path/to/dev-plugins",
      ],
    },

    "my-tool": {
      apiKey: "dev-key-here",
    },
  },
}

Concrete inner loop:

mkdir -p /absolute/path/to/dev-plugins
cargo component build --release
cp /path/to/generated/my_plugin.wasm /absolute/path/to/dev-plugins/my-tool.wasm
cara start --port 18789
cara plugins status --port 18789 --name my-tool
cara logs -n 200 --port 18789

What success looks like in cara plugins status:

Useful status fields to watch:

On each edit cycle:

  1. rebuild the component
  2. copy the new .wasm into your dev plugin directory
  3. restart Carapace
  4. rerun:
    • cara plugins status --port 18789 --name my-tool
    • cara logs -n 200 --port 18789

Important behavior:

Managed plugins and distribution

Use managed plugins when you want the managed distribution path, not when you just want the fastest local development loop.

Managed plugin commands:

cara plugins install demo-plugin --file ./path/to/demo_plugin.wasm --port 18789
cara plugins update demo-plugin --file ./path/to/demo_plugin.wasm --port 18789
cara plugins bins --port 18789
cara plugins status --port 18789 --name demo-plugin

Important managed-plugin behavior:

plugins-manifest.json entries carry the managed artifact metadata Carapace uses at load time, including:

plugins.entries.<name> carries the managed install metadata that shows up in cara plugins status, including:

Do not put runtime configuration like apiKey, webhook settings, or service options under plugins.entries.<name>. Those still belong under plugins.<plugin-id>.*.

Optional publisher metadata:

Those values are recorded at install/update time and enforced later at plugin load time according to plugins.signature policy.

Relevant config keys:

Host capabilities and sandbox boundaries

Every plugin imports the host interface from wit/plugin.wit.

Common host functions:

Host function Purpose
log-debug/info/warn/error Structured plugin logs
config-get(key) Read plugins.<plugin-id>.* config
credential-get/set Plugin-scoped secret storage
http-fetch(request) HTTP client with SSRF protection
media-fetch(url, max-bytes, timeout-ms) Media fetch with SSRF protection

Runtime limits worth designing for:

Other behavioral rules worth knowing:

The WIT file is the authoritative ABI and capability reference.

Troubleshooting