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 v1 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.

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).
PluginApiVt
The host-provided vtable, passed to [rpi_plugin_register] 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.

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 — forward- compatible (new actions add ids, not vtable slots). 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.
REGISTER_SYMBOL
The symbol the host looks up in each cdylib via libloading::Library::get. Must be an extern "C" fn(*const PluginApiVt, u32) -> i32.
RPI_PLUGIN_ABI_VERSION
The ABI version this SDK publishes. The host refuses to load a plugin whose declared RPI_PLUGIN_ABI_VERSION differs from its own (skip + diagnostic, never load — no half-compatible call surface). Bump only on a breaking ABI change (reorder/retype a vtable slot, change a crossing struct layout); adding a nullable vtable slot or widening an existing nullable slot’s parameter list within a version is not a bump — the plugin and host are both recompiled from this same SDK, and a nullable slot a plugin never calls is unaffected by a wider callee signature. (B5c widens the four renderer/provider registrar slots within ABI v1 on this basis.)

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 borrowed input; out is owning output the plugin frees via host free_string.
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.
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.
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, 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. 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.