diff --git a/crates/plugin-pdk/src/lib.rs b/crates/plugin-pdk/src/lib.rs index 00008722..8e2fd4fc 100644 --- a/crates/plugin-pdk/src/lib.rs +++ b/crates/plugin-pdk/src/lib.rs @@ -484,6 +484,10 @@ mod tests { /// PluginInstanceRegistry. pub const PLUGIN_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0"; +/// Repository WIT for the current instance world. +pub const INSTANCE_WIT: &str = + include_str!("../../../resources/plugin/wit/yoi-plugin-instance-v1.wit"); + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct PluginIngressEvent { pub kind: String, @@ -531,117 +535,110 @@ pub trait Plugin: Sized + 'static { #[doc(hidden)] pub fn plugin_instance_error(message: impl Into) -> String { - serde_json::json!({ "error": message.into() }).to_string() + serde_json::json!({ "error": { "message": message.into() } }).to_string() } -/// Export the simple string-json instance ABI used by -/// `yoi:plugin/instance@1.0.0`. +#[doc(hidden)] +pub fn plugin_instance_status(status: &PluginStatus) -> String { + serde_json::to_string(status).unwrap_or_else(|error| plugin_instance_error(error.to_string())) +} + +/// Implement the generated Component Model `Guest` trait for an instance Plugin +/// and export it with the `wit-bindgen` generated `export!` macro. +/// +/// The caller must invoke `wit_bindgen::generate!` for the `instance` world +/// first, with `runtime_path: "yoi_plugin_pdk::wit_bindgen::rt"`. That defines +/// the `Guest` trait and `export!` macro in the current module. #[macro_export] macro_rules! export_plugin_instance { - ($plugin:ty) => { - mod __yoi_plugin_instance_export { - use super::*; - use std::cell::RefCell; + ($adapter:ident, $plugin:ty) => { + struct $adapter; - thread_local! { - static INSTANCE: RefCell> = const { RefCell::new(None) }; + thread_local! { + static YOI_PLUGIN_INSTANCE: ::std::cell::RefCell<::std::option::Option<$plugin>> = const { ::std::cell::RefCell::new(None) }; + } + + impl Guest for $adapter { + fn start(config_json: ::std::string::String) -> ::std::string::String { + let config = serde_json::from_str(&config_json).unwrap_or(serde_json::Value::Null); + match <$plugin as $crate::Plugin>::start(config) { + Ok(plugin) => { + YOI_PLUGIN_INSTANCE.with(|slot| *slot.borrow_mut() = Some(plugin)); + $crate::plugin_instance_status(&$crate::PluginStatus::ready(serde_json::Value::Null)) + } + Err(error) => $crate::plugin_instance_error(error.to_string()), + } } - #[unsafe(export_name = "start")] - pub extern "C" fn __yoi_start( - _config_json_ptr: *const u8, - _config_json_len: usize, - ) -> usize { - // This low-level symbol is a placeholder for non-component raw builds. - // Component builds should bind this macro through generated WIT glue. - 0 + fn handle_tool( + name: ::std::string::String, + input_json: ::std::string::String, + ) -> ::std::string::String { + let input = serde_json::from_str(&input_json).unwrap_or(serde_json::Value::Null); + YOI_PLUGIN_INSTANCE.with(|slot| { + let mut slot = slot.borrow_mut(); + let Some(plugin) = slot.as_mut() else { + return $crate::plugin_instance_error("plugin instance has not been started"); + }; + match plugin.handle_tool(&name, input) { + Ok(output) => output.to_json_string(), + Err(error) => error.into_tool_output().to_json_string(), + } + }) } - pub struct InstanceGuest; + fn handle_ingress( + name: ::std::string::String, + event_json: ::std::string::String, + ) -> ::std::string::String { + let event = match serde_json::from_str::<$crate::PluginIngressEvent>(&event_json) { + Ok(event) => event, + Err(error) => return $crate::plugin_instance_error(error.to_string()), + }; + YOI_PLUGIN_INSTANCE.with(|slot| { + let mut slot = slot.borrow_mut(); + let Some(plugin) = slot.as_mut() else { + return $crate::plugin_instance_error("plugin instance has not been started"); + }; + match plugin.handle_ingress(&name, event) { + Ok(output) => serde_json::to_string(&output) + .unwrap_or_else(|error| $crate::plugin_instance_error(error.to_string())), + Err(error) => $crate::plugin_instance_error(error.to_string()), + } + }) + } - impl InstanceGuest { - pub fn start(config_json: String) -> String { - let config = - serde_json::from_str(&config_json).unwrap_or(serde_json::Value::Null); - match <$plugin as $crate::Plugin>::start(config) { - Ok(plugin) => { - INSTANCE.with(|slot| *slot.borrow_mut() = Some(plugin)); - serde_json::to_string(&$crate::PluginStatus::ready( - serde_json::Value::Null, - )) - .unwrap() + fn status() -> ::std::string::String { + YOI_PLUGIN_INSTANCE.with(|slot| { + let slot = slot.borrow(); + let Some(plugin) = slot.as_ref() else { + return $crate::plugin_instance_error("plugin instance has not been started"); + }; + match plugin.status() { + Ok(status) => $crate::plugin_instance_status(&status), + Err(error) => $crate::plugin_instance_error(error.to_string()), + } + }) + } + + fn stop() -> ::std::string::String { + YOI_PLUGIN_INSTANCE.with(|slot| { + let mut slot = slot.borrow_mut(); + let Some(plugin) = slot.as_mut() else { + return $crate::plugin_instance_error("plugin instance has not been started"); + }; + match plugin.stop() { + Ok(status) => { + let output = $crate::plugin_instance_status(&status); + *slot = None; + output } Err(error) => $crate::plugin_instance_error(error.to_string()), } - } - - pub fn handle_tool(name: String, input_json: String) -> String { - let input = - serde_json::from_str(&input_json).unwrap_or(serde_json::Value::Null); - INSTANCE.with(|slot| { - let mut slot = slot.borrow_mut(); - let Some(plugin) = slot.as_mut() else { - return $crate::plugin_instance_error( - "plugin instance has not been started", - ); - }; - match plugin.handle_tool(&name, input) { - Ok(output) => serde_json::to_string(&output).unwrap(), - Err(error) => $crate::plugin_instance_error(error.to_string()), - } - }) - } - - pub fn handle_ingress(name: String, event_json: String) -> String { - let event = - match serde_json::from_str::<$crate::PluginIngressEvent>(&event_json) { - Ok(event) => event, - Err(error) => return $crate::plugin_instance_error(error.to_string()), - }; - INSTANCE.with(|slot| { - let mut slot = slot.borrow_mut(); - let Some(plugin) = slot.as_mut() else { - return $crate::plugin_instance_error( - "plugin instance has not been started", - ); - }; - match plugin.handle_ingress(&name, event) { - Ok(output) => serde_json::to_string(&output).unwrap(), - Err(error) => $crate::plugin_instance_error(error.to_string()), - } - }) - } - - pub fn status() -> String { - INSTANCE.with(|slot| { - let slot = slot.borrow(); - let Some(plugin) = slot.as_ref() else { - return $crate::plugin_instance_error( - "plugin instance has not been started", - ); - }; - match plugin.status() { - Ok(status) => serde_json::to_string(&status).unwrap(), - Err(error) => $crate::plugin_instance_error(error.to_string()), - } - }) - } - - pub fn stop() -> String { - INSTANCE.with(|slot| { - let mut slot = slot.borrow_mut(); - let Some(plugin) = slot.as_mut() else { - return $crate::plugin_instance_error( - "plugin instance has not been started", - ); - }; - match plugin.stop() { - Ok(status) => serde_json::to_string(&status).unwrap(), - Err(error) => $crate::plugin_instance_error(error.to_string()), - } - }) - } + }) } } + + export!($adapter); }; } diff --git a/crates/pod/src/feature/plugin.rs b/crates/pod/src/feature/plugin.rs index 8318cc91..bd3a3a9e 100644 --- a/crates/pod/src/feature/plugin.rs +++ b/crates/pod/src/feature/plugin.rs @@ -67,17 +67,42 @@ pub fn plugin_tool_features(config: &PluginConfig) -> Vec { .collect() } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct PluginToolFeature { record: ResolvedPluginRecord, feature_id: FeatureId, + registry: PluginInstanceRegistry, } impl PluginToolFeature { pub fn new(record: ResolvedPluginRecord) -> Self { let feature_id = FeatureId::new(format!("plugin:{}:tool", record.identity)) .expect("source-qualified plugin identity yields non-empty feature id"); - Self { record, feature_id } + Self { + record, + feature_id, + registry: PluginInstanceRegistry::default(), + } + } + + fn ensure_instance(&self) -> Result { + self.registry.register(self.record.clone()) + } + + pub fn instance_status(&self) -> Option { + self.registry.status(&self.record.identity.to_string()) + } + + pub fn dispatch_ingress( + &self, + ingress_name: &str, + event: PluginIngressEvent, + ) -> Result { + let handle = self + .registry + .handle(&self.record.identity.to_string()) + .ok_or_else(|| PluginWasmError::Module("plugin instance is not started".to_string()))?; + handle.deliver_ingress(ingress_name, event) } pub fn origin(&self) -> ToolOrigin { @@ -454,7 +479,6 @@ impl FeatureModule for PluginToolFeature { fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { validate_declared_tool_names(&self.record)?; - let registry = PluginInstanceRegistry::default(); let mut instance: Option = None; let mut registered = 0usize; let mut denied = Vec::new(); @@ -476,6 +500,9 @@ impl FeatureModule for PluginToolFeature { denied.push(message); continue; } + if instance.is_none() { + instance = Some(self.ensure_instance()?); + } context.services().provide(ServiceDeclaration::new( plugin_service_id(&self.record, &service.name), self.record.manifest.version.clone(), @@ -498,6 +525,8 @@ impl FeatureModule for PluginToolFeature { ); context.diagnostics().warning(message.clone()); denied.push(message); + } else if instance.is_none() { + instance = Some(self.ensure_instance()?); } } for tool in &self.record.manifest.tools { @@ -527,7 +556,7 @@ impl FeatureModule for PluginToolFeature { let tool_instance = match &instance { Some(instance) => instance.clone(), None => { - let created = registry.register(self.record.clone())?; + let created = self.ensure_instance()?; instance = Some(created.clone()); created } @@ -1939,10 +1968,11 @@ impl PluginInstanceDiagnostic { } } -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize)] pub struct PluginInstanceStatus { pub plugin_ref: String, pub lifecycle: PluginInstanceLifecycleState, + pub component_status: Option, pub diagnostics: Vec, } @@ -1998,6 +2028,14 @@ impl PluginInstanceRegistry { .map(PluginInstanceHandle::status) } + pub fn handle(&self, plugin_ref: &str) -> Option { + self.instances + .lock() + .expect("plugin instance registry poisoned") + .get(plugin_ref) + .cloned() + } + pub fn stop(&self, plugin_ref: &str) -> Result, PluginWasmError> { let handle = self .instances @@ -2019,6 +2057,7 @@ impl PluginInstanceHandle { record, runtime, lifecycle: PluginInstanceLifecycleState::Ready, + component_status: None, diagnostics: Vec::new(), }; instance.start()?; @@ -2044,13 +2083,14 @@ impl PluginInstanceHandle { } pub fn status(&self) -> PluginInstanceStatus { - self.0.lock().expect("plugin instance poisoned").status() + let mut instance = self.0.lock().expect("plugin instance poisoned"); + instance.status() } pub fn stop(&self) -> Result { let mut instance = self.0.lock().expect("plugin instance poisoned"); instance.stop()?; - Ok(instance.status()) + Ok(instance.snapshot_status()) } fn record_diagnostic(&self, diagnostic: PluginInstanceDiagnostic) { @@ -2065,6 +2105,7 @@ struct PluginInstance { record: ResolvedPluginRecord, runtime: PluginInstanceRuntime, lifecycle: PluginInstanceLifecycleState, + component_status: Option, diagnostics: Vec, } @@ -2083,7 +2124,8 @@ impl PluginInstance { self.lifecycle = PluginInstanceLifecycleState::Started; } PluginInstanceRuntime::ComponentInstance(runtime) => { - runtime.start(&self.record)?; + let status = runtime.start(&self.record)?; + self.component_status = Some(status); self.lifecycle = PluginInstanceLifecycleState::Started; } } @@ -2200,16 +2242,43 @@ impl PluginInstance { PluginInstanceRuntime::LegacyToolAdapter => {} #[cfg(test)] PluginInstanceRuntime::TestIngress { .. } => {} - PluginInstanceRuntime::ComponentInstance(runtime) => runtime.stop()?, + PluginInstanceRuntime::ComponentInstance(runtime) => { + self.component_status = Some(runtime.stop()?); + } } self.lifecycle = PluginInstanceLifecycleState::Stopped; Ok(()) } - fn status(&self) -> PluginInstanceStatus { + fn snapshot_status(&self) -> PluginInstanceStatus { PluginInstanceStatus { plugin_ref: self.record.identity.to_string(), lifecycle: self.lifecycle.clone(), + component_status: self.component_status.clone(), + diagnostics: self.diagnostics.clone(), + } + } + + fn status(&mut self) -> PluginInstanceStatus { + if let PluginInstanceRuntime::ComponentInstance(runtime) = &mut self.runtime { + match runtime.status() { + Ok(status) => self.component_status = Some(status), + Err(error) => { + self.lifecycle = PluginInstanceLifecycleState::Failed; + self.diagnostics.push(PluginInstanceDiagnostic::new( + PluginInstanceLifecycleState::Failed, + format!( + "plugin component status failed: {}", + error.bounded_message() + ), + )); + } + } + } + PluginInstanceStatus { + plugin_ref: self.record.identity.to_string(), + lifecycle: self.lifecycle.clone(), + component_status: self.component_status.clone(), diagnostics: self.diagnostics.clone(), } } @@ -2230,6 +2299,8 @@ impl PluginInstanceRuntime { return Ok(Self::LegacyToolAdapter); }; match runtime.kind.as_str() { + #[cfg(test)] + "test-ingress" => Ok(Self::TestIngress { calls: 0 }), PLUGIN_RUNTIME_WASM_KIND => Ok(Self::LegacyToolAdapter), PLUGIN_RUNTIME_COMPONENT_KIND if runtime.world.as_deref() == Some(PLUGIN_COMPONENT_INSTANCE_WORLD) => @@ -2299,7 +2370,7 @@ impl PluginComponentInstanceRuntime { .map_err(|error| PluginWasmError::Execution(error.to_string())) } - fn start(&mut self, record: &ResolvedPluginRecord) -> Result<(), PluginWasmError> { + fn start(&mut self, record: &ResolvedPluginRecord) -> Result { self.reset_fuel()?; let start = self .instance @@ -2311,10 +2382,10 @@ impl PluginComponentInstanceRuntime { )) })?; let config_json = plugin_config_json(record); - let (_status,) = start + let (status,) = start .call(&mut self.store, (&config_json,)) .map_err(|error| PluginWasmError::Execution(error.to_string()))?; - Ok(()) + decode_plugin_lifecycle_output("start", &status) } fn handle_tool( @@ -2372,7 +2443,7 @@ impl PluginComponentInstanceRuntime { }) } - fn stop(&mut self) -> Result<(), PluginWasmError> { + fn stop(&mut self) -> Result { self.reset_fuel()?; let stop = self .instance @@ -2383,11 +2454,55 @@ impl PluginComponentInstanceRuntime { PLUGIN_COMPONENT_INSTANCE_WORLD )) })?; - let (_status,) = stop + let (status,) = stop .call(&mut self.store, ()) .map_err(|error| PluginWasmError::Execution(error.to_string()))?; - Ok(()) + decode_plugin_lifecycle_output("stop", &status) } + + fn status(&mut self) -> Result { + self.reset_fuel()?; + let status = self + .instance + .get_typed_func::<(), (String,)>(&mut self.store, "status") + .map_err(|error| { + PluginWasmError::Module(format!( + "component does not export expected `{}` status function: {error}", + PLUGIN_COMPONENT_INSTANCE_WORLD + )) + })?; + let (status,) = status + .call(&mut self.store, ()) + .map_err(|error| PluginWasmError::Execution(error.to_string()))?; + decode_plugin_lifecycle_output("status", &status) + } +} + +fn decode_plugin_lifecycle_output(phase: &str, output: &str) -> Result { + if output.len() > PLUGIN_WASM_MAX_OUTPUT_BYTES { + return Err(PluginWasmError::Output(format!( + "plugin component {phase} output exceeds {} bytes", + PLUGIN_WASM_MAX_OUTPUT_BYTES + ))); + } + let value: Value = serde_json::from_str(output).map_err(|error| { + PluginWasmError::Output(format!( + "plugin component {phase} output is not JSON: {error}" + )) + })?; + if let Some(error) = value.get("error") { + return Err(PluginWasmError::Execution(format!( + "plugin component {phase} returned error: {}", + bounded_message(error.to_string()) + ))); + } + if value.get("state").and_then(Value::as_str) == Some("failed") { + return Err(PluginWasmError::Execution(format!( + "plugin component {phase} returned failed status: {}", + bounded_message(value.to_string()) + ))); + } + Ok(value) } fn plugin_config_json(record: &ResolvedPluginRecord) -> String { @@ -3608,6 +3723,188 @@ mod tests { permissions } + fn install_feature( + feature: PluginToolFeature, + ) -> ( + super::super::FeatureRegistryInstallReport, + Vec, + ) { + let mut pending = Vec::new(); + let mut hooks = crate::hook::HookRegistryBuilder::new(); + let report = super::super::FeatureRegistryBuilder::default() + .with_module(feature) + .install_into_pending(&mut pending, &mut hooks); + (report, pending) + } + + #[test] + fn component_lifecycle_rejects_start_error_status() { + let component = component_instance_with_outputs( + br#"{"error":{"message":"boom"}}"#, + br#"{"state":"ready"}"#, + br#"{"state":"stopped"}"#, + br#"{"summary":"tool"}"#, + br#"{"accepted":true}"#, + ); + let (_dir, mut record) = resolved_record_with_component(component); + record.manifest.runtime.as_mut().unwrap().world = + Some(PLUGIN_COMPONENT_INSTANCE_WORLD.into()); + let error = match PluginInstanceHandle::new(record) { + Ok(_) => panic!("component start error should fail instance creation"), + Err(error) => error, + }; + assert!(error.bounded_message().contains("start returned error")); + } + + #[test] + fn component_lifecycle_reports_status_and_stop_outputs() { + let component = component_instance_with_outputs( + br#"{"state":"ready","data":{"phase":"start"}}"#, + br#"{"state":"ready","data":{"phase":"status"}}"#, + br#"{"state":"stopped","data":{"phase":"stop"}}"#, + br#"{"summary":"tool"}"#, + br#"{"accepted":true}"#, + ); + let (_dir, mut record) = resolved_record_with_component(component); + record.manifest.runtime.as_mut().unwrap().world = + Some(PLUGIN_COMPONENT_INSTANCE_WORLD.into()); + let handle = PluginInstanceHandle::new(record).unwrap(); + let status = handle.status(); + assert_eq!(status.lifecycle, PluginInstanceLifecycleState::Started); + assert_eq!(status.component_status.unwrap()["data"]["phase"], "status"); + let stopped = handle.stop().unwrap(); + assert_eq!(stopped.lifecycle, PluginInstanceLifecycleState::Stopped); + assert_eq!(stopped.component_status.unwrap()["data"]["phase"], "stop"); + } + + fn add_service(record: &mut ResolvedPluginRecord, name: &str) { + record.manifest.surfaces.push(PluginSurface::Service); + record.enabled_surfaces.push(PluginSurface::Service); + record + .manifest + .services + .push(manifest::plugin::PluginServiceManifest { + name: name.into(), + description: "test service".into(), + lifecycle: "host-managed".into(), + status_schema: None, + side_effects: Vec::new(), + }); + record + .manifest + .permissions + .push(PluginPermission::surface(PluginSurface::Service)); + record + .manifest + .permissions + .push(PluginPermission::service(name)); + record + .grants + .permissions + .push(PluginPermission::surface(PluginSurface::Service)); + record + .grants + .permissions + .push(PluginPermission::service(name)); + } + + fn add_ingress(record: &mut ResolvedPluginRecord, name: &str) { + record.manifest.surfaces.push(PluginSurface::Ingress); + record.enabled_surfaces.push(PluginSurface::Ingress); + record + .manifest + .ingresses + .push(manifest::plugin::PluginIngressManifest { + name: name.into(), + description: "test ingress".into(), + event_kinds: vec!["test".into()], + input_schema: None, + sources: Vec::new(), + side_effects: Vec::new(), + }); + record + .manifest + .permissions + .push(PluginPermission::surface(PluginSurface::Ingress)); + record + .manifest + .permissions + .push(PluginPermission::ingress(name)); + record + .grants + .permissions + .push(PluginPermission::surface(PluginSurface::Ingress)); + record + .grants + .permissions + .push(PluginPermission::ingress(name)); + } + + #[test] + fn service_only_install_retains_host_managed_instance() { + let mut record = record(Vec::new()); + add_service(&mut record, "svc"); + record.manifest.runtime = Some(manifest::plugin::PluginRuntimeManifest { + kind: "test-ingress".into(), + entry: None, + abi: None, + component: None, + world: Some(PLUGIN_COMPONENT_INSTANCE_WORLD.into()), + }); + let feature = PluginToolFeature::new(record); + let (report, _pending) = install_feature(feature.clone()); + assert!( + report.reports.iter().all(|report| report.installed), + "{report:#?}" + ); + let status = feature.instance_status().expect("service instance started"); + assert_eq!(status.lifecycle, PluginInstanceLifecycleState::Started); + } + + #[test] + fn installed_ingress_dispatch_uses_retained_shared_instance() { + let mut record = record(vec![tool("shared_tool")]); + add_ingress(&mut record, "shared_ingress"); + record.manifest.runtime = Some(manifest::plugin::PluginRuntimeManifest { + kind: "test-ingress".into(), + entry: None, + abi: None, + component: None, + world: Some(PLUGIN_COMPONENT_INSTANCE_WORLD.into()), + }); + let feature = PluginToolFeature::new(record); + let (report, pending) = install_feature(feature.clone()); + assert!( + report.reports.iter().all(|report| report.installed), + "{report:#?}" + ); + let (_meta, tool) = pending + .into_iter() + .map(|definition| definition()) + .find(|(meta, _tool)| meta.name == "shared_tool") + .unwrap(); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); + let output = runtime + .block_on(tool.execute(r#"{"first":true}"#, ToolExecutionContext::default())) + .unwrap(); + assert!(output.summary.contains("shared_tool")); + let report = feature + .dispatch_ingress( + "shared_ingress", + PluginIngressEvent { + kind: "test".into(), + source: "unit".into(), + payload: serde_json::json!({ "hello": "world" }), + }, + ) + .unwrap(); + assert!(report.accepted); + assert_eq!(report.output["calls"], 1); + } + #[test] fn instance_ingress_dispatch_uses_shared_in_process_instance() { let mut record = record(vec![tool("shared_tool")]); @@ -3644,6 +3941,7 @@ mod tests { record, runtime: PluginInstanceRuntime::TestIngress { calls: 0 }, lifecycle: PluginInstanceLifecycleState::Started, + component_status: None, diagnostics: Vec::new(), }))); @@ -4929,6 +5227,81 @@ input_schema = {{ type = "object", additionalProperties = true }} (dir, record) } + fn component_instance_with_outputs( + start: &[u8], + status: &[u8], + stop: &[u8], + tool: &[u8], + ingress: &[u8], + ) -> Vec { + wat::parse_str(format!( + r#"(component + (core module $m + (memory (export "memory") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + (if (result i32) (i32.eqz (local.get 0)) + (then (i32.const 8192)) + (else (local.get 0)))) + (data (i32.const 1024) "{}") + (data (i32.const 2048) "{}") + (data (i32.const 3072) "{}") + (data (i32.const 4096) "{}") + (data (i32.const 5120) "{}") + (func $write (param i32 i32) + (i32.store (i32.const 6144) (local.get 0)) + (i32.store (i32.const 6148) (local.get 1))) + (func (export "start") (param i32 i32) (result i32) + (call $write (i32.const 1024) (i32.const {})) + (i32.const 6144)) + (func (export "status") (result i32) + (call $write (i32.const 2048) (i32.const {})) + (i32.const 6144)) + (func (export "stop") (result i32) + (call $write (i32.const 3072) (i32.const {})) + (i32.const 6144)) + (func (export "tool") (param i32 i32 i32 i32) (result i32) + (call $write (i32.const 4096) (i32.const {})) + (i32.const 6144)) + (func (export "ingress") (param i32 i32 i32 i32) (result i32) + (call $write (i32.const 5120) (i32.const {})) + (i32.const 6144)) + ) + (core instance $i (instantiate $m)) + (alias core export $i "memory" (core memory $mem)) + (alias core export $i "realloc" (core func $realloc)) + (alias core export $i "start" (core func $start_core)) + (alias core export $i "status" (core func $status_core)) + (alias core export $i "stop" (core func $stop_core)) + (alias core export $i "tool" (core func $tool_core)) + (alias core export $i "ingress" (core func $ingress_core)) + (type $start_ty (func (param "config-json" string) (result string))) + (type $noarg_ty (func (result string))) + (type $twoarg_ty (func (param "name" string) (param "json" string) (result string))) + (func $start (type $start_ty) (canon lift (core func $start_core) (memory $mem) (realloc $realloc) string-encoding=utf8)) + (func $status (type $noarg_ty) (canon lift (core func $status_core) (memory $mem) (realloc $realloc) string-encoding=utf8)) + (func $stop (type $noarg_ty) (canon lift (core func $stop_core) (memory $mem) (realloc $realloc) string-encoding=utf8)) + (func $tool (type $twoarg_ty) (canon lift (core func $tool_core) (memory $mem) (realloc $realloc) string-encoding=utf8)) + (func $ingress (type $twoarg_ty) (canon lift (core func $ingress_core) (memory $mem) (realloc $realloc) string-encoding=utf8)) + (export "start" (func $start)) + (export "status" (func $status)) + (export "stop" (func $stop)) + (export "handle-tool" (func $tool)) + (export "handle-ingress" (func $ingress)) + )"#, + wat_bytes(start), + wat_bytes(status), + wat_bytes(stop), + wat_bytes(tool), + wat_bytes(ingress), + start.len(), + status.len(), + stop.len(), + tool.len(), + ingress.len(), + )) + .unwrap() + } + fn component_tool_that_returns(output: &[u8]) -> Vec { component_tool_with_memory_pages(output, 1) } diff --git a/resources/plugin/templates/rust-component-instance/Cargo.toml b/resources/plugin/templates/rust-component-instance/Cargo.toml index afd6d830..06a0cf83 100644 --- a/resources/plugin/templates/rust-component-instance/Cargo.toml +++ b/resources/plugin/templates/rust-component-instance/Cargo.toml @@ -1,3 +1,5 @@ +[workspace] + [package] name = "example-yoi-instance-plugin" version = "0.1.0" diff --git a/resources/plugin/templates/rust-component-instance/src/lib.rs b/resources/plugin/templates/rust-component-instance/src/lib.rs index a8da3ba5..0cc5a31a 100644 --- a/resources/plugin/templates/rust-component-instance/src/lib.rs +++ b/resources/plugin/templates/rust-component-instance/src/lib.rs @@ -1,6 +1,14 @@ use serde_json::{json, Value}; +use yoi_plugin_pdk::wit_bindgen; use yoi_plugin_pdk::{export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ToolOutput}; +wit_bindgen::generate!({ + world: "instance", + path: "../../../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", +}); + struct ExamplePlugin { calls: u64, } @@ -12,14 +20,21 @@ impl Plugin for ExamplePlugin { fn handle_tool(&mut self, name: &str, input: Value) -> yoi_plugin_pdk::Result { self.calls += 1; - Ok(ToolOutput::text(json!({ - "tool": name, - "calls": self.calls, - "input": input - }).to_string())) + ToolOutput::json( + format!("{name} handled by shared instance"), + json!({ + "tool": name, + "calls": self.calls, + "input": input + }), + ) } - fn handle_ingress(&mut self, name: &str, event: PluginIngressEvent) -> yoi_plugin_pdk::Result { + fn handle_ingress( + &mut self, + name: &str, + event: PluginIngressEvent, + ) -> yoi_plugin_pdk::Result { Ok(json!({ "ingress": name, "kind": event.kind, @@ -34,4 +49,4 @@ impl Plugin for ExamplePlugin { } } -export_plugin_instance!(ExamplePlugin); +export_plugin_instance!(ExamplePluginComponent, ExamplePlugin);