Skip to main content

Crate rpi_plugin_sdk

Crate rpi_plugin_sdk 

Source
Expand description

Stable #[repr(C)] ABI contract for rpi Rust-native (cdylib) plugins.

rpi loads extensions as compiled Rust cdylibs (.dll/.so/.dylib) via libloadingnot TS/jiti. Because we control both sides, the plugin is Rust, but the boundary is still a hand-defined C ABI: the two sides may be compiled with different Rust versions / crate versions, so no Rust type with a non-C repr or a Drop impl may cross. This crate defines exactly those crossing types and the registration contract.

§Soundness rules (load-bearing — verified by an adversarial review)

  1. Every crossing type is #[repr(C)]. Enums used in unions carry #[repr(u32)] so the discriminant width is pinned.
  2. No Drop type crosses. Vec/String/serde_json::Value/Option of those / Result never appear in the ABI. Owned data crosses as StbString (ptr+len) with an explicit free_string the producer exports. StbString is Copy (raw pointers are Copy); copying duplicates the pointer, not the allocation, so each allocation is freed exactly once by the side that received it (see StbString docs).
  3. Owned → JSON round-trip. Structured host data ([StableJsonValue], tool params, AgentToolResult, events) crosses as a JSON string in a StbString. serde_json with preserve_order + arbitrary_precision must be enabled consistently on host AND plugin or integers > u64/i64 lose precision and object keys may reorder — documented as a an ABI-wide limit. Tool args from the model rarely carry overflow ints, but it is never silent.
  4. Unions are all-Copy payloads. EventPayload / StepResultPayload variants are #[repr(C)] structs of primitives or StbString only, so the union is Copy-able and a wrong-variant read is unsafe (caller discriminates by tag).
  5. Unwinding never crosses the ABI. Every host→plugin and plugin→host call is extern "C"; both sides wrap dispatch in catch_unwind (abort-on-unwind / log-and-drop). A poisoned mutex or panicking emitter cannot unwind into the other side.

§Lifetime: the 4-function handle

A registered tool drives an execution through four plugin-exported functions (see ToolExecuteFn / ToolPollFn / ToolCancelFn / ToolDestroyFn) — executeStepHandle (plugin-allocates), poll (non-blocking, borrows the handle, returns StepResult), cancel (sets an internal AtomicBool flag; idempotent; does NOT free; thread-safe), destroy (frees; idempotent; called exactly once by the blocking driver). canceldestroy: conflating them is a UAF / double-free. The adapter’s blocking driver calls poll in a loop until Done/Err, forwards Pending partials, and calls destroy once on exit.

poll is non-blocking and MUST observe the cancel flag and return Done/Err within a bounded number of polls; otherwise a cancelled call leaks a spawn_blocking thread forever (those tasks run to completion regardless of outer-future drop).

The crate is std (not no_std): the ABI types are #[repr(C)] POD with no Drop — that is what makes the boundary sound — but plugins and the host are ordinary binaries with std, so the constructor/reader helpers and tests use String/Vec/serde_json directly. The json feature keeps serde_json optional for a plugin that wants to skip it.

Modules§

json
Helpers to cross structured data as JSON-in-StbString. See the module docs for the precision/order limit.

Macros§

export_plugin_v2
Export an ABI v2 plugin entrypoint under rpi_plugin_register_v2.

Structs§

EventData
A generic JSON-data payload for the long-tail events whose structured shape the host serializes wholesale (context, before_provider_request, model select, resources_discover response, etc.). The plugin reads the fields it needs.
EventEmpty
No-payload marker for events that carry none (e.g. session_shutdown). Carries a dummy byte so the empty-struct isn’t flagged FFI-unsafe by improper_ctypes (zero-sized C structs are rejected regardless of repr(C)).
EventError
An error/failure payload.
EventMessage
A serialized message payload (message_start/update/end, tool-result messages). message is a JSON AgentMessage.
EventToolCall
A tool-call payload (tool_call, tool_execution_start/update). tool_call_id + tool_name are raw strings; params is the JSON args.
EventToolResult
A tool-result payload (tool_result, tool_execution_end).
LegacyPluginApiV1
Frozen host vtable layout used by plugins exporting LEGACY_REGISTER_SYMBOL. Do not add, remove, reorder, or retype fields in this struct. New plugins use PluginApiVt and REGISTER_SYMBOL_V2.
PluginApiVt
The ABI v2 host-provided vtable, passed to rpi_plugin_register_v2 as a *const.
StablePluginEvent
One event dispatched to a plugin handler. The host translates its native AgentEvent / HarnessEvent into this and calls every registered handler for the tag (dispatch wrapped in catch_unwind). Ownership of the StbStrings passes to the handler; the handler frees them via the host’s free_string.
StableToolSchema
A tool’s provider-facing schema crossing the ABI. name / description are raw strings; parameters is a JSON Schema serialized to a JSON string (the host parses it into its native schemars::Schema).
StbDone
Terminal success payload. result is a JSON AgentToolResult.
StbErr
Terminal failure payload. message is a UTF-8 error string.
StbPending
A partial/progress result emitted during Pending. progress is a JSON AgentToolResult (the same shape on_update carries) — the host forwards it to the adapter’s on_update callback. May be empty.
StbString
An owned UTF-8 string crossing the ABI as a (ptr, len) pair.
StbStringRef
A borrowed, non-owning view of a string passed as an input to an FFI call. The callee MUST NOT free it and MUST NOT retain it past the call.
StepResult
Return value of ToolPollFn. The blocking driver matches on tag, reads the matching payload, and breaks the loop on Done/Err.
UnknownRuntimeActionId
Error returned when a plugin passes a numeric runtime-action id that this ABI does not define.

Enums§

EventTag
Discriminant for StablePluginEvent, one variant per pi on() category (33 total). #[repr(u32)] pins the discriminant width.
RuntimeActionId
Identifier for a host runtime action the plugin may invoke via PluginApiVt::runtime_action. One slot dispatches all actions; the numeric id crosses the FFI boundary as a u32 and the host validates it with TryFrom<u32> before constructing this enum. Args/results cross as JSON strings.
StepResultTag
Discriminant for StepResult. #[repr(u32)] pins the width so the union payload is sound across compilers.

Constants§

EVENT_TAG_COUNT
Number of on() event categories — 33. A test asserts EVENT_TAG_COUNT == 33 so a future edit that adds/removes a tag is caught.
LEGACY_PLUGIN_ABI_VERSION
ABI version passed to the legacy rpi_plugin_register entrypoint.
LEGACY_REGISTER_SYMBOL
The ABI v1 symbol used only when REGISTER_SYMBOL_V2 is absent.
REGISTER_SYMBOL
Alias for the current SDK’s register symbol.
REGISTER_SYMBOL_V2
The ABI v2 symbol the host looks up first in each cdylib. Must be an extern "C" fn(*const PluginApiVt, u32) -> i32.
RPI_PLUGIN_ABI_VERSION
The ABI version this SDK publishes. ABI v2 adds the GetCliFlag runtime action and makes RuntimeActionFn’s action parameter an explicitly validated u32.

Functions§

register_entrypoint
Convenience for host + plugin: declare the register entrypoint.

Type Aliases§

CommandHandlerFn
A generic command-handler fn (for register_command). args_json is a borrowed {"args":"...","command":"/..."} envelope; out is owning JSON output reclaimed with the host free_string. The TUI understands {kind:"message",text}, {kind:"selector",items:[...]}, {kind:"editor",initialText}, and {kind:"input",title,placeholder} responses; selector/editor/input submissions call the same handler with an action field in args.
EventHandlerFn
Handler fn pointer registered via register_event_handler(tag, handler). user_data is the plugin’s opaque context. Return 0 on success; nonzero signals a handled error (the host logs it; dispatch continues to other handlers — one handler’s error does not abort the fan-out).
FreeStringFn
Function pointer a plugin exports to free a StbString it produced. Idempotent: freeing an already-freed or empty StbString is a no-op.
LegacyRpiPluginRegister
ABI v1 plugin entrypoint signature.
LegacyRuntimeActionFn
ABI v1 runtime-action signature. The original SDK exposed a #[repr(u32)] enum at this position; the legacy host view uses the ABI-equivalent raw integer so it can reject unknown values before constructing an enum.
ProviderRequestFn
A provider-injection factory fn (for register_provider). req_json is a borrowed request envelope; out is an owning response the plugin frees. The host wraps this into a Provider impl (B4/B5).
RenderFn
A render/transform fn (for the renderer registrars). input_json is borrowed; out is owning output the plugin frees via host free_string. Markdown handlers return {markdown:"..."}; message/entry handlers return {text:"...",markdown?:true} or {lines:["..."]} for terminal UI.
ResourcesDiscoverFn
resources_discover handler signature (B5b). Unlike EventHandlerFn (fire-and-forget, i32 only), this carries an owning out so the plugin can hand {skillPaths, promptPaths, themePaths} back to the host. cwd and reason are borrowed inputs (StbStringRef); out is plugin-produced and reclaimed via the plugin_free_string the host stored alongside the handler at registration. user_data is the plugin’s opaque context. Returns 0 on success (host reads out); nonzero on a handled error (host logs + skips this handler, fan-out continues — mirrors pi runner.ts:1179-1188).
RpiPluginRegister
Plugin entrypoint signature. The host loads the cdylib, looks up rpi_plugin_register_v2, and calls it with the host PluginApiVt and the host’s current RPI_PLUGIN_ABI_VERSION.
RuntimeActionFn
Runtime-action signature: runtime_action(action_id, args_json, out, user_data) -> i32. args_json is a borrowed input (StbStringRef); out is an owning output (StbString) the host produces and the plugin frees via the host’s free_string. action_id is deliberately a raw u32, not a Rust enum: an unknown value must be rejected as a normal protocol error rather than materializing an invalid enum discriminant. Returns 0 on success, nonzero on error.
StepHandle
Opaque, plugin-allocated handle for one tool execution drive. Produced by ToolExecuteFn, polled by ToolPollFn, cancelled by ToolCancelFn, freed by ToolDestroyFn (exactly once, idempotent).
ToolCancelFn
cancel(handle). Sets an internal AtomicBool (SeqCst) cancel flag. Idempotent, thread-safe, does NOT free. The poll loop observes it.
ToolDestroyFn
destroy(handle). Frees the handle. Idempotent; called exactly once by the blocking driver on exit (after the loop sees Done/Err, or after cancel propagated). Null handle is a no-op.
ToolExecuteFn
execute(tool_call_id, params) -> StepHandle. Plugin-allocates a drive handle and begins the work (non-blocking — the real progress comes via poll). tool_call_id is a borrowed StbStringRef (valid for the call); params is an owning JSON string of the tool-call arguments (the plugin frees it via the host’s free_string). Returns null on allocation failure.
ToolPartialCb
Partial-result callback the blocking driver passes to poll, wrapped in catch_unwind on the host side. The plugin invokes it synchronously inside poll() when it has a Pending partial — never retained, never invoked after Done/Err.
ToolPollFn
poll(handle, partial_cb, user_data) -> StepResult. Non-blocking. Must observe the cancel flag (set by ToolCancelFn) and return Done/Err within a bounded number of polls. Borrows handle (does not free it).

Unions§

EventPayload
Payload union for StablePluginEvent. All variants are #[repr(C)] structs of StbString / primitives (Copy), so the union is Copy. Discriminate by StablePluginEvent::tag before reading.
StepResultPayload
The poll() return value. Read the payload variant matching tag.