Skip to content

Creating a Plugin

Atsukoro1 edited this page Sep 14, 2026 · 2 revisions

Create a plugin

The supported Rust starter lives in examples/extensions. Copy the sdk directory and one plugin directory into your own repository. The SDK is intentionally small and exposes Serein ABI version 1.

1. Create the workspace

Start with this layout:

my-serein-plugin/
├── Cargo.toml
├── Cargo.lock
├── LICENSE
├── sdk/
└── hello-serein/
    ├── Cargo.toml
    ├── manifest.json
    └── src/lib.rs

Use the starter workspace configuration and keep the plugin crate as a WebAssembly library:

[package]
name = "hello-serein"
version = "1.0.0"
edition = "2024"
license = "MIT OR Apache-2.0"

[lib]
crate-type = ["cdylib"]

[dependencies]
serein-extension-sdk.workspace = true

2. Define the manifest

Every package uses api_version: 1, a stable lowercase ID, a version, license, public source URL, and kind: "plugin".

{
  "api_version": 1,
  "id": "hello-serein",
  "name": "Hello Serein",
  "version": "1.0.0",
  "author": "Your name",
  "license": "MIT",
  "source": "https://github.com/your-name/hello-serein",
  "kind": "plugin",
  "capabilities": [],
  "actions": [
    {
      "id": "open",
      "label": "Open hello panel",
      "surface": "panel"
    }
  ]
}

IDs may contain lowercase ASCII letters, digits, and hyphens, must start with a letter or digit, and may be at most 64 bytes. Action labels may be at most 128 UTF-8 bytes. A plugin may declare at most 16 actions and one activation action.

3. Implement the handler

The SDK turns a Rust function into the required serein_alloc and serein_invoke exports:

use serein_extension_sdk::{Element, Invocation, Output};

fn handle(input: Invocation) -> Output {
    let panel = if input.action == "open" {
        vec![
            Element::Heading { text: "Hello Serein".into() },
            Element::Text { text: "My first sandboxed plugin is running.".into() },
        ]
    } else {
        Vec::new()
    };
    Output { panel, ..Default::default() }
}

serein_extension_sdk::export!(handle);

Each invocation receives a fresh WebAssembly instance. Return native panel elements rather than drawing UI directly.

Supported panel elements are heading, text, separator, row, button, text_input, checkbox, select, and integer slider. Button IDs must point to actions whose surface is panel.

4. Choose surfaces and capabilities

Action surfaces

Surface Where the action appears Context it may receive
message Selected message context menu Selected message, with consent
composer Composer tools Current draft, with consent
panel Extensions page or Tools menu Native form values and granted storage
activation Enable and account load Granted storage; no conversation text

Capabilities

Capability Allows
selected_message The selected message text for a message action
composer The current composer draft and an explicit replacement proposal
storage One bounded local UTF-8 value for plugin settings
appearance App-wide colors and supported native control metrics
deleted_messages An activation action may request bounded, session-only retention of already loaded deleted messages

Request only what the plugin uses. Users review capabilities before enabling a package and again when an update changes them. Composer replacements always require the user to press Apply; plugins cannot send messages.

5. Build and package

From your workspace root:

rustup target add wasm32-unknown-unknown
cargo build --locked --release --target wasm32-unknown-unknown
python pack.py hello-serein/manifest.json target/wasm32-unknown-unknown/release/hello_serein.wasm packages/hello-serein.serein-extension

Copy pack.py from the starter. It combines the manifest and Wasm bytes without downloading or executing creator code, then prints the package size and SHA-256 digest.

Continue with Testing and Packaging.