feat: update plugin service authoring templates

This commit is contained in:
2026-06-25 16:37:37 +09:00
parent cd2006305e
commit 7a4fd97526
11 changed files with 530 additions and 80 deletions
@@ -1,14 +1,22 @@
[workspace]
[package]
name = "example-yoi-instance-plugin"
name = "yoi-rust-component-service-template"
version = "0.1.0"
edition = "2024"
license = "MIT"
publish = false
# Keep the embedded template checkable in-place without making it a member of
# Yoi's root workspace. A copied starter remains a normal standalone package.
[workspace]
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Out-of-tree Plugin packages should replace the local path with a pinned
# Yoi source revision. Use rev, not branch, for reproducible builds:
# yoi-plugin-pdk = { git = "https://gitea.hareworks.net/Hare/yoi.git", package = "yoi-plugin-pdk", rev = "<pinned-yoi-commit-sha>" }
@@ -1,9 +1,10 @@
# Yoi instance Plugin template
# Rust Service Plugin Template
This template targets `yoi:plugin/instance@1.0.0`. The host creates one
`PluginInstance` for the package; Tool, Service, and Ingress surfaces share that
instance state while each surface keeps separate permissions/grants.
This template targets the Component Model-only runtime (`runtime.kind = "wasm-component"`) and exports the `yoi:plugin/instance@1.0.0` world.
Tools still run only through ordinary model/user-initiated Tool calls. Ingress
handlers receive bounded typed untrusted events and must return explicit JSON
for host-mediated visible/durable paths.
It demonstrates both authoring surfaces supported by a shared Plugin instance:
- `example_echo` is an ordinary request/response Tool handler.
- `example_ws` is a Service ingress handler. The host owns WebSocket receive/reconnect work and dispatches bounded `websocket_text` events into `handle_ingress`. The guest replies by returning a `websocket_send` output command in `ServiceOutput`; do not run a guest-side `recv(timeout)` polling loop.
Build with `cargo component build --release` (or the project-specific build command used by your Plugin packaging flow), then run `yoi plugin check` / `yoi plugin pack` from the generated Plugin directory.
@@ -1,16 +1,16 @@
schema_version = 1
id = "example.rust_instance_plugin"
name = "Rust Instance Plugin Template"
id = "example.rust_service_plugin"
name = "Rust Service Plugin Template"
version = "0.1.0"
description = "Example instance-oriented Yoi Plugin with shared Tool/Ingress state."
description = "Example Component Model Plugin with Tool and Service ingress handlers."
surfaces = ["tool", "service", "ingress"]
permissions = [
{ kind = "surface", surface = "tool" },
{ kind = "tool", name = "example_instance_tool" },
{ kind = "tool", name = "example_echo" },
{ kind = "surface", surface = "service" },
{ kind = "service", name = "example_instance_service" },
{ kind = "service", name = "example_service" },
{ kind = "surface", surface = "ingress" },
{ kind = "ingress", name = "example_instance_ingress" },
{ kind = "ingress", name = "example_ws" },
]
[runtime]
@@ -19,17 +19,18 @@ world = "yoi:plugin/instance@1.0.0"
component = "plugin.component.wasm"
[[tools]]
name = "example_instance_tool"
description = "Return the input and increment shared instance state."
name = "example_echo"
description = "Echo input text through the shared Plugin instance."
input_schema = { type = "object" }
[[services]]
name = "example_instance_service"
description = "Reports shared plugin instance lifecycle status."
name = "example_service"
description = "Host-managed service instance for bounded ingress events."
lifecycle = "host-managed"
[[ingresses]]
name = "example_instance_ingress"
description = "Accepts bounded in-process ingress events."
event_kinds = ["example"]
name = "example_ws"
description = "Handles host-owned WebSocket text events and returns websocket_send output commands."
event_kinds = ["websocket_text"]
sources = ["websocket:wss://example.com/socket"]
input_schema = { type = "object" }
@@ -1,6 +1,10 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use yoi_plugin_pdk::wit_bindgen;
use yoi_plugin_pdk::{export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ToolOutput};
use yoi_plugin_pdk::{
export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ServiceOutput, ToolError,
ToolOutput,
};
wit_bindgen::generate!({
world: "instance",
@@ -9,24 +13,42 @@ wit_bindgen::generate!({
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
});
#[derive(Default)]
struct ExamplePlugin {
calls: u64,
count: u64,
}
#[derive(Deserialize)]
struct EchoInput {
text: String,
}
#[derive(Serialize)]
struct EchoOutput {
text: String,
count: u64,
}
impl Plugin for ExamplePlugin {
fn start(_config: Value) -> yoi_plugin_pdk::Result<Self> {
Ok(Self { calls: 0 })
fn start(config: Value) -> Result<Self, ToolError> {
Ok(Self {
count: config.get("start_count").and_then(Value::as_u64).unwrap_or(0),
})
}
fn handle_tool(&mut self, name: &str, input: Value) -> yoi_plugin_pdk::Result<ToolOutput> {
self.calls += 1;
fn handle_tool(&mut self, name: &str, input: Value) -> Result<ToolOutput, ToolError> {
if name != "example_echo" {
return Err(ToolError::invalid_input(format!("unknown tool: {name}")));
}
let input: EchoInput =
serde_json::from_value(input).map_err(|err| ToolError::invalid_input(err.to_string()))?;
self.count += 1;
ToolOutput::json(
format!("{name} handled by shared instance"),
json!({
"tool": name,
"calls": self.calls,
"input": input
}),
format!("echoed {} bytes", input.text.len()),
EchoOutput {
text: input.text,
count: self.count,
},
)
}
@@ -34,18 +56,29 @@ impl Plugin for ExamplePlugin {
&mut self,
name: &str,
event: PluginIngressEvent,
) -> yoi_plugin_pdk::Result<Value> {
Ok(json!({
"ingress": name,
"kind": event.kind,
"source": event.source,
"calls": self.calls,
"accepted": true
}))
) -> Result<ServiceOutput, ToolError> {
if name != "example_ws" {
return Ok(ServiceOutput::accepted(json!({ "ignored": name }))?);
}
let Some(text) = event.websocket_text() else {
return Ok(ServiceOutput::accepted(json!({
"accepted": true,
"kind": event.kind,
}))?);
};
self.count += 1;
ServiceOutput::websocket_send(
&event,
format!("example-reply-{}", self.count),
event.source.strip_prefix("websocket:").unwrap_or(&event.source),
format!("echo({}): {text}", self.count),
)
}
fn status(&self) -> yoi_plugin_pdk::Result<PluginStatus> {
Ok(PluginStatus::ready(json!({ "calls": self.calls })))
fn status(&self) -> Result<PluginStatus, ToolError> {
Ok(PluginStatus::ready(json!({ "count": self.count })))
}
}