rpi-plugin-sdk 0.3.17

Stable #[repr(C)] ABI contract for rpi Rust-native (cdylib) plugins — no rpi-* deps
Documentation

rpi-plugin-sdk

crates.io docs.rs

The stable #[repr(C)] ABI contract for rpi plugins. Depend on this crate, and nothing else, to build a Rust cdylib that extends the rpi agent with tools, providers, slash commands, event handlers, renderers and discovered resources.

This crate has no dependency on the rpi runtime on purpose. A plugin must not link the host — the host links the plugin. Both sides share only the types defined here.

Install

[package]

name = "my-rpi-extension"



[lib]

crate-type = ["cdylib"]



[dependencies]

rpi-plugin-sdk = "0.3"

serde_json = "1"

Minimal plugin

use rpi_plugin_sdk::{export_plugin, StbStringRef};

export_plugin!(|api| {
    // Optional: declare a load priority and the platforms this plugin supports.
    if let Some(declare) = api.declare {
        declare(StbStringRef::from_str(r#"{"priority":60,"platforms":["linux"]}"#));
    }

    // ... register tools, providers, commands and handlers through `api` ...

    0 // status code: 0 = success
});

The macro emits the rpi_plugin_register symbol. The version lives inside the PluginApi struct (abi_version, struct_size) rather than in the symbol name, so the entrypoint signature does not change when the struct grows.

Positioned plugin panels

Hosts with declarative panel support accept a panel field on the existing RuntimeActionId::SetStatus (18). The ABI and legacy {"key":"my-plugin","value":"footer text"} behavior are unchanged. No host changes are needed for each new plugin, its text or its position.

{
  "key": "my-plugin.monitor",
  "panel": {
    "version": 1,
    "anchor": "top-right",
    "offsetX": -2,
    "offsetY": 1,
    "width": 44,
    "maxHeight": 9,
    "minScreenWidth": 50,
    "border": true,
    "title": "My monitor",
    "lines": ["1 round · 87 steps · 252 tok/s", "First token: 0.42s"]
  }
}

Pass this JSON using api.runtime_action and free its output with api.free_string, as for other runtime actions. Update the same key to replace its panel, or send {"key":"my-plugin.monitor","panel":null} to remove it. Clear panels during SessionShutdown as well. Footer statuses and panels have independent registries; clearing one does not clear the other. Headless runs store panels without rendering them. Older hosts do not render the new field.

Supported anchors: top-left, top-center, top-right, left-center, center, right-center, bottom-left, bottom-center, bottom-right. Signed offsets are terminal columns/rows relative to the anchor; negative offsets move left/up. Positions are relative to the visible terminal, so they stay fixed while chat scrolls. Panels are passive text overlays and do not change keyboard focus. They can cover chat content; use distinct anchors or offsets for multiple panels.

Set "layout":"sidebar" to reserve a column beside the transcript instead of covering chat. The default "layout":"overlay" preserves existing panels. Left anchors use the left sidebar; other anchors use the right sidebar. A single panel follows its vertical anchor and offsetY; multiple panels on one side stack in key order. offsetX applies only to overlays. A sidebar adds a two-column separator to the requested width. It hides when minScreenWidth is not met or less than 48 columns would remain for chat. The editor and footer stay full width. Removing sidebar panels restores the transcript width. Sidebar mode leaves keyboard focus and transcript scrolling with the original components.

version and lines are required. Defaults: anchor top-right, width 44, maxHeight 16, offsets 0, minScreenWidth 0, border true, title empty. Width is 4–240 columns; maxHeight is 1–80 rows including title and borders. Content is clipped to width and height, with Unicode display widths respected. Terminal control sequences in content are stripped. At most 32 panels, 64 lines per panel, 2048 UTF-8 bytes per line and 256 bytes per title are accepted. Invalid updates return an error and preserve the previous panel.

Why a hand-written C ABI

Both sides are Rust, but they are compiled separately and may use different compiler versions and different versions of this crate. That rules out passing any Rust type that is not #[repr(C)] or that has a Drop impl. The design rules the SDK enforces:

  1. Every crossing type is #[repr(C)], and enums in unions carry an explicit #[repr(u32)] so the discriminant width is pinned.
  2. No Drop type crosses. No Vec, String, serde_json::Value, Option of those, or Result. Owned strings cross as StbString (pointer + length) together with an explicit free_string function the producer exports, so each allocation is freed exactly once, by the side that received it.
  3. Structured data crosses as JSON in a StbString. serde_json must be configured consistently on host and plugin (preserve_order + arbitrary_precision), or integers beyond u64/i64 lose precision and object key order can change.
  4. Unions hold Copy payloads only, so a variant read is unsafe and the caller discriminates on the tag.
  5. Unwinding never crosses the boundary. Every call is extern "C", and both sides contain panics with catch_unwind; a panic is converted into a status code rather than aborting the process.

These are not stylistic preferences — violating any of them is undefined behaviour at the boundary.

Compatibility

The host loader only resolves rpi_plugin_register, exported with export_plugin!. The host passes a PluginApi containing registration slots, runtime_action and declare (priority and platforms).

The SDK checks abi_version against RPI_PLUGIN_ABI_VERSION_UNIFIED and requires struct_size to cover the PluginApi expected by the plugin before calling its registration body. A mismatch returns a nonzero status and the host reports a load failure. Extensions using retired entrypoints must be updated to export_plugin! and rebuilt with the current SDK.

Versioning

A plugin depends on rpi-plugin-sdk only, so its version is decoupled from the rest of the family. ABI compatibility is checked through abi_version and struct_size; each optional capability must also be checked before use. Rebuild extensions when the SDK contract they require changes.

Examples and docs

License

MIT. See LICENSE and NOTICE.