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 libloading — not 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) — execute→StepHandle (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). cancel ≠ destroy: 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
Export a unified ABI plugin entrypoint under rpi_plugin_register.

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 lifecycle payload (message_start/message_update/ message_end). message is JSON. For message_update, it follows native Pi’s envelope and carries assistantMessageEvent; streaming consumers must read its real text_delta.delta instead of diffing a cumulative snapshot.
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).
PluginApi
The host-provided API struct. The first two fields (abi_version and struct_size) are contract-identity markers that survive future signature changes: a plugin reads them from the pointer before anything else, so even if the entrypoint arity changes, the version check remains robust.
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) plus one rpi-specific category (34 total). #[repr(u32)] pins the discriminant width.
RuntimeActionId
Identifier for a host runtime action the plugin may invoke via PluginApi::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_HANDLER_ABORT
Veto: stop the current lifecycle phase. Honored only by the host’s lifecycle dispatch; the observe fan-out logs + continues (see the module docs on EVENT_HANDLER_CONTINUE).
EVENT_HANDLER_CLAIMED
Claim: the handler consumed this event and the host should not fall back to its own default handling. Honored only by dispatch paths that ask extensions to arbitrate (currently EventTag::Input key routing, via EVENT_HANDLER_CLAIMED); the ordinary observe fan-out treats it like any other nonzero (log + continue).
EVENT_HANDLER_CONTINUE
Return codes an EventHandlerFn may return.
EVENT_HANDLER_ERROR
The handler handled an error but the fan-out should continue (default nonzero semantics — equivalent to any other nonzero code).
EVENT_TAG_COUNT
Number of event categories — 37 (33 Pi on() categories + 1 rpi-specific EventTag::BeforeTuiStart + 2 UI prompt events + 1 rpi-specific EventTag::EditorChange). A test asserts EVENT_TAG_COUNT == 37 so a future edit that adds/removes a tag is caught.
HOST_CONTEXT_KEY
Reserved key the host adds to a plugin tool call’s arguments.
HOST_NAME
The application name of the host that loads plugins: "rpi".
HOST_VERSION
Version of the HOST_NAME host that this SDK ships with.
REGISTER_PANIC_STATUS
Status code a plugin’s register entrypoint returns when its body panicked.
REGISTER_SYMBOL_UNIFIED
The unversioned symbol for the unified ABI.
RPI_PLUGIN_ABI_VERSION_UNIFIED
The unified ABI version.
SESSION_ID_ENV
Environment variable carrying the id of the session this process serves.
SUBAGENT_PARENT_DEPTH_ENV
See SUBAGENT_PARENT_TRACE_ID_ENV. Nesting depth, so a grandchild knows how far down it is.
SUBAGENT_PARENT_ENV
Every SUBAGENT_PARENT_TRACE_ID_ENV sibling, as one slice.
SUBAGENT_PARENT_SESSION_ID_ENV
See SUBAGENT_PARENT_TRACE_ID_ENV. The parent’s session id.
SUBAGENT_PARENT_SPAN_ID_ENV
See SUBAGENT_PARENT_TRACE_ID_ENV. The parent turn’s root observation id.
SUBAGENT_PARENT_TRACE_ID_ENV
Environment variable a tracing extension publishes to name the trace a nested rpi should attach to, and reads back to decide it is a subagent.

Functions§

guard
Run body, converting a panic into Err instead of letting it unwind.
guard_or
Like guard, but returns fallback on panic. Handy for entrypoints that must return a value rather than a Result.
register_entrypoint⚠
Unified ABI entrypoint helper: reads abi_version and struct_size from the struct pointer before dereferencing anything else. This survives future signature changes because the pointer is always the first argument.

Type Aliases§

BeforeAgentStartFn
before_agent_start handler signature. Unlike EventHandlerFn (fire-and-forget, i32 only), this carries an owning out so the plugin can hand back a system prompt to install for the run — pi’s BeforeAgentStartEventResult (extensions/types.ts:1394), whose systemPrompt becomes forceSystemPrompt (runner.ts:1347).
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). The one exception: when the host dispatches a lifecycle event through its veto-aware path, returning EVENT_HANDLER_ABORT stops the fan-out and aborts that phase (see the constants above).
FreeStringFn
Function pointer a plugin exports to free a StbString it produced. Idempotent: freeing an already-freed or empty StbString is a no-op.
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).
RpiPluginRegisterUnified
Unified ABI plugin entrypoint signature: (api). The version is read from api->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.