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
+50 -4
View File
@@ -23,7 +23,7 @@ Yoi's Plugin platform is meant to make extension behavior reviewable before it b
Keep these layers separate when designing a Plugin. Do not make package discovery imply enablement. Do not make SDK/PDK convenience imply authority. Do not treat Rust helper APIs or host API wrappers as permission grants. The host always re-checks authority at registration/execution/API-call boundaries.
Yoi's preferred Plugin shape is **Tool first**. A good Tool Plugin has a narrow schema, deterministic input/output behavior, explicit side-effect metadata, and a minimal grant set. Long-running services, inbound events, and autonomous routing are future Service/Ingress work; they should not be hidden inside a Tool package.
Yoi's preferred Plugin shapes are **Tool first** for request/response capabilities and **Service/Ingress** for host-dispatched inbound events. A good Tool Plugin has a narrow schema, deterministic input/output behavior, explicit side-effect metadata, and a minimal grant set. A Service Plugin should keep long-lived transport ownership in the host and react to bounded ingress events by returning output commands.
Component Model authoring is the supported path for Plugins. Legacy raw core-Wasm manifests (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are retired and rejected by `yoi plugin check`, discovery, `list`, and `show`; use the Rust PDK/template and `kind = "wasm-component"` instead.
@@ -42,10 +42,10 @@ Implemented foundation:
- read-only `yoi plugin list/show` inspection;
- local first-party authoring commands: `yoi plugin new`, `yoi plugin check`, and `yoi plugin pack`.
Still intentionally separate/future work:
Still intentionally limited or separate from this guide:
- multi-language SDK/PDK crates;
- Service / Ingress surfaces;
- Service / Ingress surfaces, where the host owns transport lifecycle, dispatches bounded ingress events, and consumes output commands such as `websocket_send`;
- WebSocket or inbound HTTP for bidirectional external event integrations;
- public registry/install/update/signature tooling.
@@ -78,6 +78,8 @@ Create a Rust Component Tool starter from embedded resources:
```bash
yoi plugin new rust-component-tool ./my-plugin
# or, for a host-dispatched Service/Ingress example:
yoi plugin new rust-component-service ./my-service-plugin
```
`new` writes only inside the requested destination and refuses an existing non-empty destination or destination symlink. The generated template includes `plugin.toml`, Rust source, Cargo metadata, README next steps, and a placeholder `plugin.component.wasm` artifact so local `check`/`pack` validation can run immediately. Replace the placeholder with a real built component before enabling or executing the Plugin.
@@ -115,7 +117,7 @@ For Tool Plugins:
- return bounded summaries and content that are useful as Tool results;
- avoid hiding long workflows, background daemons, or inbound event handling inside a Tool call.
A Tool should be a capability the model may choose to call, not a second agent runtime. If the desired behavior needs a long-lived connection, incoming events, or autonomous routing, treat that as future Service/Ingress design rather than stretching the Tool surface.
A Tool should be a capability the model may choose to call, not a second agent runtime. If the desired behavior needs a long-lived connection, incoming events, or autonomous routing, put the transport lifecycle behind a Service/Ingress surface and let the host dispatch bounded events; do not stretch the Tool surface into a hidden polling loop.
Design package permissions as a review surface. A reviewer should be able to read `plugin.toml` plus the enablement grants and understand:
@@ -163,6 +165,8 @@ Create a starter with:
```bash
yoi plugin new rust-component-tool ./my-plugin
# or, for a host-dispatched Service/Ingress example:
yoi plugin new rust-component-service ./my-service-plugin
```
The generated package contains:
@@ -327,6 +331,48 @@ path_prefixes = ["/v1/"]
Yoi checks method, scheme, host, optional port, and path prefix against both the manifest declaration and enablement grant before any network I/O. `http://localhost`, loopback, private, and other local targets are never ambient; they require an explicit manifest request target and an explicit matching grant. The explicit request target is the declared URL authority; a granted DNS hostname may resolve to a loopback/private address without requiring a separate literal-IP grant, so reviewers should grant hostnames only when that resolution behavior is intended. Broad targets such as `host = "*"` are supported only as visibly broad request permissions in inspection/diagnostics. Embedded credentials, credential-like headers, oversize requests/responses, WebSocket URLs/upgrades, and SSE/event-stream requests are rejected.
## Service ingress and output commands
Service Plugins export the `yoi:plugin/instance@1.0.0` world. The host starts one Plugin instance, owns external ingress transports, and calls `handle_ingress(name, event_json)` with bounded event envelopes. A WebSocket ingress event contains fields such as `kind`, `source`, `ingress_name`, `payload`, `created_at`, `attempt`, and `correlation_id`; the Rust PDK maps this to `PluginIngressEvent`.
Service handlers return `ServiceOutput`, not ordinary ToolOutput. Side effects are requested through top-level `output_commands`. For a WebSocket reply, use the PDK helper:
```rust
ServiceOutput::websocket_send(
&event,
"reply-1",
event.source.strip_prefix("websocket:").unwrap_or(&event.source),
"pong",
)
```
This serializes a `websocket_send` command with `source_event_id`, `command_id`, `payload.url`, `payload.text`, and a request timestamp. The host parses, bounds, grant-checks, and dispatches the command through the host-owned WebSocket driver. Do not create a long-running guest receive loop for Service integrations; incoming messages should arrive as ingress events.
A minimal manifest shape is:
```toml
surfaces = ["tool", "service", "ingress"]
[runtime]
kind = "wasm-component"
world = "yoi:plugin/instance@1.0.0"
component = "plugin.component.wasm"
[[services]]
name = "example_service"
description = "Host-managed service instance."
lifecycle = "host-managed"
[[ingresses]]
name = "example_ws"
description = "Handles host-owned WebSocket text events."
event_kinds = ["websocket_text", "websocket_close", "websocket_error"]
sources = ["websocket:wss://gateway.example.com/gateway"]
input_schema = { type = "object" }
```
Generate a fuller example with `yoi plugin new rust-component-service ./my-service-plugin`.
## `websocket` host API
The `websocket` host API is a separate grant-gated capability named `host_api.websocket`, not an extension of `host_api.request`. It opens host-owned WebSocket connections only when both the package manifest and enablement config declare matching targets. Tool-style/internal bounded use can still drive the lifecycle explicitly through `open`, `send-text`, `recv`, and `close`; incoming messages are returned only from bounded `recv` calls and are not injected into model context, history, Dashboard state, or Ticket state. Service Plugins should prefer the host-owned Service WebSocket driver instead of running a long-lived guest recv loop: declare a Service ingress source as `websocket:wss://host/path`, include the `websocket_text`/`websocket_close`/`websocket_error` event kinds you want delivered, and emit the Service output command `websocket_send` to send text back through the same grant-checked host connection.