use std::ffi::c_void;
use std::sync::{Arc, Mutex};
use rpi_plugin_sdk::{
BeforeAgentStartFn, EventHandlerFn, EventTag, FreeStringFn, OAuthRequestFn, ProviderRequestFn,
RenderFn, ResourcesDiscoverFn, RuntimeActionFn, StablePluginEvent, StableToolSchema, StbString,
StbStringRef, ToolCancelFn, ToolDestroyFn, ToolExecuteFn, ToolPollFn,
};
use thiserror::Error;
pub use actions::{
reload_callback_from_mailbox, trampoline_runtime_action, ActionBridge, ReloadMailbox,
RuntimeActionHost, UiDialogMailbox, UiDialogRequest,
};
pub use editor_text::{EditorTextEdit, EditorTextMailbox, EditorTextMode};
pub use event_log::{event_log_path, log_handler_invocation, EventLogger};
pub use loader::{
load_dir, load_one, load_session, load_session_mixed, merge_registries, ExtensionSession,
LoadedPlugin, PluginKeepalive, PluginLoadError,
};
pub use oauth::{
list as list_oauth_providers, request as request_oauth, OAuthActionResponse,
OAuthExtensionError, OAuthProviderInfo, OAuthResponseKind,
};
pub use prompt_transform::emit_before_agent_start;
pub use provider::PluggableProvider;
pub use provider_hooks::ExtensionProviderHooks;
pub use registry::{
assert_active, current_platform, platform_allows, BeforeAgentStartHandler, ExtensionRegistry,
ExtensionTool, RegisteredFlag, RegisteredHandler, RegisteredOAuthProvider, RegisteredProvider,
RegisteredRenderer, RegisteredRendererKind, RegisteredShortcut, RegistryEntry,
RegistrySnapshot, ResourcesDiscoverHandler, DEFAULT_PRIORITY,
};
pub use resources::{emit_resources_discover, DiscoveredResources};
pub use status::ExtensionStatusMailbox;
mod panel;
pub use panel::{ExtensionPanel, PanelAnchor, PanelLayout};
pub use tool::{PluginToolAdapter, PluginToolHandle, ToolCallContext};
pub use translate::{
dispatch_data_event, dispatch_data_event_claiming, dispatch_empty_event,
dispatch_lifecycle_event, dispatch_tool_call_event, ExtensionEmitter, TeeEmitter,
};
pub use stdio::StdioLifecycle;
mod actions;
mod editor_text;
mod event_log;
mod loader;
mod oauth;
mod prompt_transform;
mod provider;
mod provider_hooks;
mod registry;
mod resources;
mod status;
mod stdio;
mod tool;
mod translate;
pub trait PluginDiagnostics: Send + Sync {
fn warn(&self, message: &str);
fn unsupported(&self, message: &str);
}
#[derive(Default)]
pub struct NullDiagnostics;
impl PluginDiagnostics for NullDiagnostics {
fn warn(&self, _message: &str) {}
fn unsupported(&self, _message: &str) {}
}
#[derive(Debug, Error)]
pub enum PluginToolError {
#[error("plugin execute returned a null handle (allocation failure)")]
NullHandle,
#[error("plugin error: {0}")]
Plugin(String),
#[error("runtime unavailable: {0}")]
NoRuntime(String),
}
pub use rpi_plugin_sdk::callbacks::free_string as host_free_string;
pub struct HostApi {
registry: Mutex<Option<ExtensionRegistry>>,
name: String,
action_bridge: Option<Arc<ActionBridge>>,
}
impl HostApi {
pub fn new(
name: impl Into<String>,
registry: ExtensionRegistry,
_diagnostics: Arc<dyn PluginDiagnostics>,
) -> Arc<Self> {
Arc::new(Self {
registry: Mutex::new(Some(registry)),
name: name.into(),
action_bridge: None,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn with_action_bridge(
name: impl Into<String>,
registry: ExtensionRegistry,
_diagnostics: Arc<dyn PluginDiagnostics>,
action_bridge: Arc<ActionBridge>,
) -> Arc<Self> {
let name = name.into();
let action_bridge = action_bridge.for_plugin(&name);
Arc::new(Self {
registry: Mutex::new(Some(registry)),
name,
action_bridge: Some(action_bridge),
})
}
fn with_registry<R>(&self, f: impl FnOnce(&mut ExtensionRegistry) -> R) -> Option<R> {
let mut guard = self.registry.lock().expect("host api registry lock");
guard.as_mut().map(f)
}
pub fn take_registry(&self) -> Option<ExtensionRegistry> {
self.registry.lock().expect("host api registry lock").take()
}
pub fn build_vtable(self: &Arc<Self>) -> rpi_plugin_sdk::PluginApi {
let (runtime_action_fn, ud) = match &self.action_bridge {
Some(bridge) => (
trampoline_runtime_action as RuntimeActionFn,
Arc::as_ptr(bridge) as *mut c_void,
),
None => (
stub_runtime_action as RuntimeActionFn,
Arc::as_ptr(self) as *mut c_void,
),
};
rpi_plugin_sdk::PluginApi {
abi_version: rpi_plugin_sdk::RPI_PLUGIN_ABI_VERSION_UNIFIED,
struct_size: std::mem::size_of::<rpi_plugin_sdk::PluginApi>() as u32,
free_string: host_free_string,
register_tool: Some(trampoline_register_tool),
register_command: Some(trampoline_register_command),
register_shortcut: Some(trampoline_register_shortcut),
register_flag: Some(trampoline_register_flag),
register_provider: Some(trampoline_register_provider), register_message_renderer: Some(trampoline_register_message_renderer), register_markdown_transformer: Some(trampoline_register_markdown_transformer), register_entry_renderer: Some(trampoline_register_entry_renderer), register_event_handler: Some(trampoline_register_event_handler),
register_resources_discover: Some(trampoline_register_resources_discover),
register_before_agent_start: Some(trampoline_register_before_agent_start),
register_oauth_provider: Some(trampoline_register_oauth_provider),
runtime_action: runtime_action_fn,
dispatch_event: Some(trampoline_dispatch_event),
user_data: ud,
declare: Some(trampoline_declare),
}
}
}
thread_local! {
static CURRENT_HOST_API: std::cell::RefCell<Option<Arc<HostApi>>> = const { std::cell::RefCell::new(None) };
}
struct CurrentApiGuard {
previous: Option<Arc<HostApi>>,
_thread_bound: std::marker::PhantomData<std::rc::Rc<()>>,
}
impl Drop for CurrentApiGuard {
fn drop(&mut self) {
let displaced = CURRENT_HOST_API.with(|slot| slot.replace(self.previous.take()));
drop(displaced);
}
}
fn enter_current_api(api: &Arc<HostApi>) -> CurrentApiGuard {
let previous = CURRENT_HOST_API.with(|slot| slot.replace(Some(Arc::clone(api))));
CurrentApiGuard {
previous,
_thread_bound: std::marker::PhantomData,
}
}
fn with_current_api<R>(f: impl FnOnce(&HostApi) -> R) -> Option<R> {
let api = CURRENT_HOST_API.with(|slot| slot.borrow().clone())?;
Some(f(&api))
}
fn current_api_present() -> bool {
CURRENT_HOST_API.with(|slot| slot.borrow().is_some())
}
extern "C" fn trampoline_register_tool(
schema: *const StableToolSchema,
execute_fn: ToolExecuteFn,
poll_fn: ToolPollFn,
cancel_fn: ToolCancelFn,
destroy_fn: ToolDestroyFn,
plugin_free_string: FreeStringFn,
) -> i32 {
if !current_api_present() {
return -1;
}
if schema.is_null() {
return 1;
}
let (name, description, parameters_value) = unsafe {
let s = &*schema;
let name =
rpi_plugin_sdk::callbacks::OwnedString::from_raw(s.name, Some(plugin_free_string));
let description = rpi_plugin_sdk::callbacks::OwnedString::from_raw(
s.description,
Some(plugin_free_string),
);
let parameters = rpi_plugin_sdk::callbacks::OwnedString::from_raw(
s.parameters,
Some(plugin_free_string),
);
(
name.to_string_lossy(),
description.to_string_lossy(),
serde_json::from_str::<serde_json::Value>(¶meters.to_string_lossy()).ok(),
)
};
let Some(parameters_value) = parameters_value else {
return 2;
};
if name.trim().is_empty() || !parameters_value.is_object() {
return 2;
}
let tool = rpi_ai::types::Tool {
name,
description,
parameters: rpi_ai::types::Schema::new(parameters_value),
constrained_sampling: None,
};
let handle = PluginToolHandle {
execute_fn,
poll_fn,
cancel_fn,
destroy_fn,
plugin_free_string,
};
let ok =
with_current_api(
|api| match api.with_registry(|reg| reg.register_tool(tool, handle)) {
Some(_) => true,
None => false,
},
);
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_command(
name: StbStringRef,
description: StbStringRef,
handler: rpi_plugin_sdk::CommandHandlerFn,
) -> i32 {
if !current_api_present() {
return -1;
}
let (name, description) =
unsafe { (name.as_str().to_string(), description.as_str().to_string()) };
let ok = with_current_api(|api| {
match api.with_registry(|reg| {
reg.register_command(name, description, handler, std::ptr::null_mut())
}) {
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_shortcut(key: StbStringRef, description: StbStringRef) -> i32 {
if !current_api_present() {
return -1;
}
let (key, description) =
unsafe { (key.as_str().to_string(), description.as_str().to_string()) };
let ok = with_current_api(|api| {
match api
.with_registry(|reg| reg.register_shortcut(api.name().to_string(), key, description))
{
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_flag(name: StbStringRef, description: StbStringRef) -> i32 {
if !current_api_present() {
return -1;
}
let (name, description) =
unsafe { (name.as_str().to_string(), description.as_str().to_string()) };
if name.trim().is_empty() || name.starts_with('-') || name.contains('=') || name.contains(' ') {
return 2;
}
let ok = with_current_api(|api| {
match api.with_registry(|reg| reg.register_flag(name, description)) {
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_event_handler(
tag: EventTag,
handler: EventHandlerFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let ok = with_current_api(|api| {
match api.with_registry(|reg| {
reg.register_event_handler(api.name().to_string(), tag, handler, user_data)
}) {
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_declare(json: StbStringRef) -> i32 {
if !current_api_present() {
return -1;
}
let text = unsafe { json.as_str() };
match with_current_api(|api| {
api.with_registry(|reg| {
let Ok(value) = serde_json::from_str::<serde_json::Value>(text) else {
return false;
};
if value.get("ownsStdio").and_then(serde_json::Value::as_bool) == Some(true) {
if let Some(bridge) = &api.action_bridge {
if let Err(error) = bridge.claim_stdio() {
tracing::error!("{error}");
return false;
}
} else {
return false; }
}
reg.apply_declaration_json(text)
})
}) {
Some(Some(true)) => 0,
Some(Some(false)) => 1,
_ => -1,
}
}
extern "C" fn trampoline_register_provider(
provider_id: StbStringRef,
base_url: StbStringRef,
api_style: StbStringRef,
request_fn: ProviderRequestFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let record = crate::registry::RegisteredProvider {
provider_id: unsafe { provider_id.as_str().to_string() },
base_url: unsafe { base_url.as_str().to_string() },
api_style: unsafe { api_style.as_str().to_string() },
request_fn,
plugin_free_string,
user_data,
};
let ok = with_current_api(
|api| match api.with_registry(|reg| reg.register_provider(record)) {
Some(_) => true,
None => false,
},
);
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_oauth_provider(
manifest_json: StbStringRef,
request_fn: OAuthRequestFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let manifest = unsafe { manifest_json.as_str().to_string() };
let record = crate::registry::RegisteredOAuthProvider {
plugin: with_current_api(|api| api.name.clone()).unwrap_or_default(),
manifest_json: manifest,
request_fn,
plugin_free_string,
user_data,
};
match with_current_api(|api| api.with_registry(|reg| reg.register_oauth_provider(record))) {
Some(Some(true)) => 1,
Some(Some(false)) => 0,
_ => -1,
}
}
fn register_renderer_common(
name: StbStringRef,
kind: crate::registry::RegisteredRendererKind,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let record = crate::registry::RegisteredRenderer {
name: unsafe { name.as_str().to_string() },
kind,
render_fn,
plugin_free_string,
user_data,
};
let ok = with_current_api(
|api| match api.with_registry(|reg| reg.register_renderer(record)) {
Some(_) => true,
None => false,
},
);
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_message_renderer(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
register_renderer_common(
name,
crate::registry::RegisteredRendererKind::Message,
render_fn,
plugin_free_string,
user_data,
)
}
extern "C" fn trampoline_register_markdown_transformer(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
register_renderer_common(
name,
crate::registry::RegisteredRendererKind::Markdown,
render_fn,
plugin_free_string,
user_data,
)
}
extern "C" fn trampoline_register_entry_renderer(
name: StbStringRef,
render_fn: RenderFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
register_renderer_common(
name,
crate::registry::RegisteredRendererKind::Entry,
render_fn,
plugin_free_string,
user_data,
)
}
extern "C" fn trampoline_dispatch_event(_event: StablePluginEvent, _user_data: *mut c_void) -> i32 {
0
}
extern "C" fn trampoline_register_resources_discover(
handler: ResourcesDiscoverFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let ok = with_current_api(|api| {
match api.with_registry(|reg| {
reg.register_resources_discover(handler, plugin_free_string, user_data)
}) {
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn trampoline_register_before_agent_start(
handler: BeforeAgentStartFn,
plugin_free_string: FreeStringFn,
user_data: *mut c_void,
) -> i32 {
if !current_api_present() {
return -1;
}
let ok = with_current_api(|api| {
match api.with_registry(|reg| {
reg.register_before_agent_start(handler, plugin_free_string, user_data)
}) {
Some(_) => true,
None => false,
}
});
if ok == Some(true) {
0
} else {
-1
}
}
extern "C" fn stub_runtime_action(
_action_id: u32,
_args: StbStringRef,
_out: *mut StbString,
_user_data: *mut c_void,
) -> i32 {
-1
}
#[allow(dead_code)]
type _RuntimeActionFnDoc = RuntimeActionFn;
#[cfg(test)]
mod current_api_tests {
use super::*;
fn api(name: &str) -> Arc<HostApi> {
HostApi::new(name, ExtensionRegistry::new(), Arc::new(NullDiagnostics))
}
#[test]
fn registration_owns_its_api_and_releases_it_afterward() {
let owner = api("owned");
let weak = Arc::downgrade(&owner);
let registration = enter_current_api(&owner);
drop(owner);
assert!(weak.upgrade().is_some());
assert_eq!(
with_current_api(|api| api.name().to_string()).as_deref(),
Some("owned")
);
drop(registration);
assert!(!current_api_present());
assert!(weak.upgrade().is_none());
}
#[test]
fn nested_registration_can_reenter_and_restores_the_outer_api() {
let outer = api("outer");
let outer_registration = enter_current_api(&outer);
with_current_api(|current| {
assert_eq!(current.name(), "outer");
let inner = api("inner");
let _inner_registration = enter_current_api(&inner);
assert_eq!(
with_current_api(|api| api.name().to_string()).as_deref(),
Some("inner")
);
})
.unwrap();
assert_eq!(
with_current_api(|api| api.name().to_string()).as_deref(),
Some("outer")
);
drop(outer_registration);
assert!(!current_api_present());
}
#[test]
fn a_panicking_registration_restores_the_outer_api() {
let outer = api("outer");
let _outer_registration = enter_current_api(&outer);
let inner = api("inner");
let failed = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _inner_registration = enter_current_api(&inner);
assert_eq!(
with_current_api(|api| api.name().to_string()).as_deref(),
Some("inner")
);
panic!("registration failed");
}));
assert!(failed.is_err());
assert_eq!(
with_current_api(|api| api.name().to_string()).as_deref(),
Some("outer")
);
}
#[test]
fn registration_context_is_thread_local() {
let parent = api("parent");
let _registration = enter_current_api(&parent);
std::thread::spawn(|| {
assert!(!current_api_present());
let child = api("child");
let _registration = enter_current_api(&child);
assert_eq!(
with_current_api(|api| api.name().to_string()).as_deref(),
Some("child")
);
})
.join()
.unwrap();
assert_eq!(
with_current_api(|api| api.name().to_string()).as_deref(),
Some("parent")
);
}
}