diff --git a/crates/plugin-pdk/tests/template.rs b/crates/plugin-pdk/tests/template.rs index 526abb34..82b8ff13 100644 --- a/crates/plugin-pdk/tests/template.rs +++ b/crates/plugin-pdk/tests/template.rs @@ -72,6 +72,14 @@ fn rust_component_service_template_has_event_output_pattern() { plugin["runtime"]["world"].as_str(), Some("yoi:plugin/instance@1.0.0") ); + assert!( + plugin["permissions"] + .as_array() + .expect("permissions array") + .iter() + .any(|permission| permission["kind"].as_str() == Some("host_api") + && permission["api"].as_str() == Some("websocket")) + ); assert_eq!( plugin["services"].as_array().expect("services array").len(), 1 @@ -89,10 +97,17 @@ fn rust_component_service_template_has_event_output_pattern() { .as_array() .expect("sources") .iter() - .any(|source| source - .as_str() - .unwrap_or_default() - .starts_with("websocket:wss://")) + .any(|source| source.as_str() == Some("websocket:wss://example.com/socket")) + ); + let websocket = &plugin["websocket"].as_array().expect("websocket targets")[0]; + assert_eq!(websocket["scheme"].as_str(), Some("wss")); + assert_eq!(websocket["host"].as_str(), Some("example.com")); + assert!( + websocket["path_prefixes"] + .as_array() + .expect("websocket path prefixes") + .iter() + .any(|prefix| prefix.as_str() == Some("/socket")) ); assert!(SERVICE_TEMPLATE_LIB.contains("world: \"instance\"")); diff --git a/crates/yoi/src/plugin_cli.rs b/crates/yoi/src/plugin_cli.rs index 69ffe40a..c54251b3 100644 --- a/crates/yoi/src/plugin_cli.rs +++ b/crates/yoi/src/plugin_cli.rs @@ -2188,6 +2188,10 @@ mod tests { assert!(manifest.contains("kind = \"wasm-component\"")); assert!(manifest.contains("[[services]]")); assert!(manifest.contains("[[ingresses]]")); + assert!(manifest.contains("{ kind = \"host_api\", api = \"websocket\" }")); + assert!(manifest.contains("[[websocket]]")); + assert!(manifest.contains("host = \"example.com\"")); + assert!(manifest.contains("path_prefixes = [\"/socket\"]")); let source = fs::read_to_string(service_destination.join("src/lib.rs")).unwrap(); assert!(source.contains("ServiceOutput::websocket_send")); assert!(!source.contains("recv(timeout")); diff --git a/docs/development/plugin-development.md b/docs/development/plugin-development.md index 68a31066..0e745dd7 100644 --- a/docs/development/plugin-development.md +++ b/docs/development/plugin-development.md @@ -352,6 +352,13 @@ A minimal manifest shape is: ```toml surfaces = ["tool", "service", "ingress"] +permissions = [ + { kind = "surface", surface = "service" }, + { kind = "service", name = "example_service" }, + { kind = "surface", surface = "ingress" }, + { kind = "ingress", name = "example_ws" }, + { kind = "host_api", api = "websocket" }, +] [runtime] kind = "wasm-component" @@ -369,8 +376,15 @@ 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" } + +[[websocket]] +scheme = "wss" +host = "gateway.example.com" +path_prefixes = ["/gateway"] ``` +The `host_api.websocket` permission and `[[websocket]]` target are required for `websocket_send` output commands. Runtime enablement grants must explicitly allow the same WebSocket target; the manifest declaration alone is not authority. + Generate a fuller example with `yoi plugin new rust-component-service ./my-service-plugin`. ## `websocket` host API diff --git a/resources/plugin/templates/rust-component-instance/README.md b/resources/plugin/templates/rust-component-instance/README.md index 6682aed4..8a9930bc 100644 --- a/resources/plugin/templates/rust-component-instance/README.md +++ b/resources/plugin/templates/rust-component-instance/README.md @@ -5,6 +5,6 @@ This template targets the Component Model-only runtime (`runtime.kind = "wasm-co 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. +- `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. The manifest declares `host_api.websocket` plus a matching `[[websocket]]` target for the example URL. Enablement grants must explicitly allow the same WebSocket target before the host will send output commands. 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. diff --git a/resources/plugin/templates/rust-component-instance/plugin.toml b/resources/plugin/templates/rust-component-instance/plugin.toml index a7bfd629..59495d37 100644 --- a/resources/plugin/templates/rust-component-instance/plugin.toml +++ b/resources/plugin/templates/rust-component-instance/plugin.toml @@ -11,6 +11,7 @@ permissions = [ { kind = "service", name = "example_service" }, { kind = "surface", surface = "ingress" }, { kind = "ingress", name = "example_ws" }, + { kind = "host_api", api = "websocket" }, ] [runtime] @@ -34,3 +35,8 @@ description = "Handles host-owned WebSocket text events and returns websocket_se event_kinds = ["websocket_text"] sources = ["websocket:wss://example.com/socket"] input_schema = { type = "object" } + +[[websocket]] +scheme = "wss" +host = "example.com" +path_prefixes = ["/socket"]