feat: update plugin service authoring templates
This commit is contained in:
@@ -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 })))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,55 @@ world instance {
|
||||
import yoi:host/websocket@1.0.0;
|
||||
import yoi:host/fs@1.0.0;
|
||||
|
||||
/// Start one host-managed Plugin instance. `config-json` is the opaque
|
||||
/// enablement config copied from the Profile/plugin grant record. The return
|
||||
/// string is PluginStatus JSON: `{ "state": "ready|running|stopped|...",
|
||||
/// "data": <json> }`.
|
||||
export start: func(config-json: string) -> string;
|
||||
|
||||
/// Execute a manifest-declared Tool on the shared instance. `input-json` is
|
||||
/// ordinary Tool input JSON and the return string is ToolOutput JSON.
|
||||
export handle-tool: func(name: string, input-json: string) -> string;
|
||||
|
||||
/// Handle one host-dispatched Service/Ingress event. `event-json` is an
|
||||
/// ingress event envelope with at least:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "kind": "websocket_text|websocket_close|websocket_error|...",
|
||||
/// "source": "websocket:wss://host/path|...",
|
||||
/// "ingress_name": "manifest_ingress_name",
|
||||
/// "payload": { "text": "..." },
|
||||
/// "created_at": "RFC3339 timestamp",
|
||||
/// "attempt": 1,
|
||||
/// "correlation_id": "host event id"
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The return string is ServiceOutput JSON. To request host-mediated side
|
||||
/// effects, return top-level `output_commands`, for example:
|
||||
///
|
||||
/// ```json
|
||||
/// {
|
||||
/// "accepted": true,
|
||||
/// "output_commands": [{
|
||||
/// "correlation_id": "command correlation id",
|
||||
/// "source_event_id": "matching ingress correlation_id",
|
||||
/// "command_id": "guest command id",
|
||||
/// "kind": "websocket_send",
|
||||
/// "payload": { "url": "wss://host/path", "text": "reply" },
|
||||
/// "requested_at": "RFC3339 timestamp"
|
||||
/// }]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Output commands are parsed, bounded, grant-checked, and executed by the
|
||||
/// host. They are not ordinary ToolOutput and do not inject hidden context.
|
||||
export handle-ingress: func(name: string, event-json: string) -> string;
|
||||
|
||||
/// Return PluginStatus JSON for the shared host-managed instance.
|
||||
export status: func() -> string;
|
||||
|
||||
/// Stop the shared instance and return final PluginStatus JSON.
|
||||
export stop: func() -> string;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ world tool {
|
||||
import yoi:host/websocket@1.0.0;
|
||||
import yoi:host/fs@1.0.0;
|
||||
|
||||
/// Execute a manifest-declared Tool. `input-json` is the normal Tool input
|
||||
/// JSON and the returned string is the same ToolOutput JSON accepted by the
|
||||
/// legacy raw-Wasm ABI.
|
||||
/// Execute a manifest-declared Tool. `input-json` is ordinary Tool input JSON
|
||||
/// and the returned string is ToolOutput JSON accepted by the current
|
||||
/// Component Model Plugin runtime.
|
||||
export call: func(tool-name: string, input-json: string) -> string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user