From 06287aca407ce433354371ff08d2ca2f9aaa5d33 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:15:20 +0900 Subject: [PATCH 1/9] plugin: add rust pdk template --- Cargo.lock | 13 + Cargo.toml | 3 + crates/manifest/src/plugin.rs | 70 +++ crates/plugin-pdk/Cargo.toml | 16 + crates/plugin-pdk/src/lib.rs | 468 ++++++++++++++++++ crates/plugin-pdk/tests/template.rs | 73 +++ crates/pod/Cargo.toml | 1 + crates/pod/src/feature/plugin.rs | 12 + docs/design/plugin-component-model.md | 14 +- docs/design/plugin-packages.md | 9 +- docs/development/plugin-development.md | 63 ++- docs/examples/plugin-component-tool/lib.rs | 42 +- package.nix | 2 +- .../templates/rust-component-tool/Cargo.toml | 17 + .../templates/rust-component-tool/README.md | 39 ++ .../templates/rust-component-tool/plugin.toml | 20 + .../templates/rust-component-tool/src/lib.rs | 34 ++ 17 files changed, 859 insertions(+), 37 deletions(-) create mode 100644 crates/plugin-pdk/Cargo.toml create mode 100644 crates/plugin-pdk/src/lib.rs create mode 100644 crates/plugin-pdk/tests/template.rs create mode 100644 resources/plugin/templates/rust-component-tool/Cargo.toml create mode 100644 resources/plugin/templates/rust-component-tool/README.md create mode 100644 resources/plugin/templates/rust-component-tool/plugin.toml create mode 100644 resources/plugin/templates/rust-component-tool/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1789ed70..a5c7fd56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2623,6 +2623,7 @@ dependencies = [ "wasmtime", "wat", "workflow", + "yoi-plugin-pdk", ] [[package]] @@ -5388,6 +5389,7 @@ version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ + "bitflags 2.11.0", "wit-bindgen-rust-macro", ] @@ -5553,6 +5555,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "yoi-plugin-pdk" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror 2.0.18", + "toml", + "wit-bindgen", +] + [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 424360ab..2dadc297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/secrets", "crates/manifest", "crates/pod", + "crates/plugin-pdk", "crates/yoi", "crates/pod-store", "crates/protocol", @@ -34,6 +35,7 @@ default-members = [ "crates/secrets", "crates/manifest", "crates/pod", + "crates/plugin-pdk", "crates/yoi", "crates/pod-store", "crates/protocol", @@ -65,6 +67,7 @@ memory = { path = "crates/memory" } ticket = { path = "crates/ticket" } project-record = { path = "crates/project-record" } pod = { path = "crates/pod" } +yoi-plugin-pdk = { path = "crates/plugin-pdk" } yoi = { path = "crates/yoi" } pod-registry = { path = "crates/pod-registry" } pod-store = { path = "crates/pod-store" } diff --git a/crates/manifest/src/plugin.rs b/crates/manifest/src/plugin.rs index 338620de..b005752b 100644 --- a/crates/manifest/src/plugin.rs +++ b/crates/manifest/src/plugin.rs @@ -15,6 +15,42 @@ const ZIP_COMPRESSION_STORED: u16 = 0; const ZIP_UNIX_SYMLINK_TYPE: u32 = 0o120000; const ZIP_UNIX_FILE_TYPE_MASK: u32 = 0o170000; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PluginTemplateResource { + pub path: &'static str, + pub contents: &'static str, +} + +/// Embedded starter template for Rust Component Model Tool Plugins. +/// +/// The template is data only: it performs no filesystem/network operations and +/// grants no authority. Future authoring CLI commands can materialize these +/// files into a chosen destination after applying their own overwrite policy. +pub const RUST_COMPONENT_TOOL_TEMPLATE: &[PluginTemplateResource] = &[ + PluginTemplateResource { + path: "Cargo.toml", + contents: include_str!( + "../../../resources/plugin/templates/rust-component-tool/Cargo.toml" + ), + }, + PluginTemplateResource { + path: "src/lib.rs", + contents: include_str!( + "../../../resources/plugin/templates/rust-component-tool/src/lib.rs" + ), + }, + PluginTemplateResource { + path: "plugin.toml", + contents: include_str!( + "../../../resources/plugin/templates/rust-component-tool/plugin.toml" + ), + }, + PluginTemplateResource { + path: "README.md", + contents: include_str!("../../../resources/plugin/templates/rust-component-tool/README.md"), + }, +]; + #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct PluginConfig { @@ -1957,6 +1993,40 @@ mod tests { use super::*; use tempfile::TempDir; + #[test] + fn embedded_rust_component_tool_template_is_valid_package_shape() { + let paths: BTreeSet<_> = RUST_COMPONENT_TOOL_TEMPLATE + .iter() + .map(|file| file.path) + .collect(); + assert_eq!( + paths, + BTreeSet::from(["Cargo.toml", "src/lib.rs", "plugin.toml", "README.md"]) + ); + assert!( + RUST_COMPONENT_TOOL_TEMPLATE + .iter() + .all(|file| !file.path.starts_with('/') && !file.path.contains("..")) + ); + + let manifest_text = RUST_COMPONENT_TOOL_TEMPLATE + .iter() + .find(|file| file.path == "plugin.toml") + .unwrap() + .contents; + let manifest: PluginPackageManifest = toml::from_str(manifest_text).unwrap(); + assert_eq!(manifest.schema_version, SUPPORTED_PLUGIN_API_VERSION); + assert_eq!( + manifest.runtime.as_ref().unwrap().kind, + PLUGIN_RUNTIME_COMPONENT_KIND + ); + assert_eq!( + manifest.runtime.as_ref().unwrap().world.as_deref(), + Some(PLUGIN_COMPONENT_TOOL_WORLD) + ); + assert_eq!(manifest.tools.len(), 1); + } + #[test] fn discovers_valid_user_and_workspace_packages() { let temp = TempDir::new().unwrap(); diff --git a/crates/plugin-pdk/Cargo.toml b/crates/plugin-pdk/Cargo.toml new file mode 100644 index 00000000..025aa81e --- /dev/null +++ b/crates/plugin-pdk/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "yoi-plugin-pdk" +version = "0.1.0" +edition.workspace = true +license.workspace = true +description = "Guest-side Rust PDK helpers for Yoi Component Model Tool plugins" +publish = false + +[dependencies] +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +thiserror.workspace = true +wit-bindgen = "0.51.0" + +[dev-dependencies] +toml.workspace = true diff --git a/crates/plugin-pdk/src/lib.rs b/crates/plugin-pdk/src/lib.rs new file mode 100644 index 00000000..898f953d --- /dev/null +++ b/crates/plugin-pdk/src/lib.rs @@ -0,0 +1,468 @@ +//! Guest-side helpers for Yoi Component Model Tool plugins. +//! +//! This crate is intentionally small and guest-only: it depends on JSON/WIT +//! binding support, but not on Yoi host/runtime crates. It grants no authority; +//! package manifests and host-side Plugin grants decide whether a Tool or host +//! API can run. +//! +//! Component authors still generate the WIT bindings in their guest crate, then +//! delegate the exported `call` function to [`run_json_tool`] or to the +//! [`export_component_tool!`] macro: +//! +//! ```ignore +//! yoi_plugin_pdk::wit_bindgen::generate!({ +//! world: "tool", +//! path: "../../../../resources/plugin/wit", +//! }); +//! +//! fn echo( +//! ctx: yoi_plugin_pdk::ToolContext, +//! input: EchoInput, +//! ) -> Result { +//! yoi_plugin_pdk::ToolOutput::json( +//! format!("{} ok", ctx.tool_name()), +//! EchoOutput { text: input.text }, +//! ) +//! } +//! +//! yoi_plugin_pdk::export_component_tool!(Plugin, echo); +//! ``` + +use serde::Serialize; +use serde::de::DeserializeOwned; +use serde_json::Value; + +pub use wit_bindgen; + +/// Current Yoi Component Model Tool world targeted by this PDK. +pub const TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0"; + +/// Repository WIT for the current Tool world, exposed for authoring tools and +/// tests. Runtime components should still generate bindings at compile time. +pub const TOOL_WIT: &str = include_str!("../../../resources/plugin/wit/yoi-plugin-tool-v1.wit"); + +/// Repository WIT for the grant-bound host APIs importable by Tool components. +pub const HOST_WIT: &str = include_str!("../../../resources/plugin/wit/yoi-host-v1.wit"); + +/// Maximum serialized ToolOutput JSON accepted by Yoi's current Plugin runtime. +pub const MAX_TOOL_OUTPUT_BYTES: usize = 64 * 1024; +/// Maximum summary bytes accepted by Yoi's current Plugin runtime. +pub const MAX_SUMMARY_BYTES: usize = 1024; +/// Conservative content cap that leaves room for JSON framing and escaping. +pub const MAX_CONTENT_BYTES: usize = MAX_TOOL_OUTPUT_BYTES - MAX_SUMMARY_BYTES - 4096; +/// Maximum structured error message bytes retained by the PDK. +pub const MAX_ERROR_MESSAGE_BYTES: usize = 4096; + +/// Per-call context passed to a typed JSON handler. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ToolContext { + tool_name: String, +} + +impl ToolContext { + /// Create context for the manifest-declared Tool name selected by the host. + pub fn new(tool_name: impl Into) -> Self { + Self { + tool_name: bounded_text(tool_name.into(), MAX_SUMMARY_BYTES), + } + } + + /// Manifest-declared Tool name supplied by the host runtime. + pub fn tool_name(&self) -> &str { + &self.tool_name + } +} + +/// ToolOutput JSON shape accepted by the current Yoi Plugin runtime. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct ToolOutput { + summary: String, + #[serde(skip_serializing_if = "Option::is_none")] + content: Option, +} + +impl ToolOutput { + /// Create an ordinary Tool output. + pub fn new(summary: impl Into, content: Option) -> Self { + let mut output = Self { + summary: normalize_summary(summary.into()), + content: content.map(|value| bounded_text(value, MAX_CONTENT_BYTES)), + }; + output.bound_serialized_json(); + output + } + + /// Create a Tool output whose content is typed JSON. + pub fn json(summary: impl Into, value: impl Serialize) -> Result { + let content = serde_json::to_string(&value).map_err(ToolError::serialization)?; + let output = Self { + summary: normalize_summary(summary.into()), + content: Some(content), + }; + let serialized = serde_json::to_string(&output).map_err(ToolError::serialization)?; + if serialized.len() > MAX_TOOL_OUTPUT_BYTES { + return Err(ToolError::invalid_output(format!( + "serialized ToolOutput JSON exceeds {MAX_TOOL_OUTPUT_BYTES} bytes" + ))); + } + Ok(output) + } + + /// Create a summary-only Tool output. + pub fn summary(summary: impl Into) -> Self { + Self::new(summary, None) + } + + /// Return the bounded summary. + pub fn summary_text(&self) -> &str { + &self.summary + } + + /// Return optional detailed content. + pub fn content(&self) -> Option<&str> { + self.content.as_deref() + } + + /// Serialize to the ToolOutput JSON string returned by the WIT `call` export. + pub fn to_json_string(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| { + r#"{"summary":"tool error: serialization","content":"{\"error\":{\"code\":\"serialization\",\"message\":\"failed to serialize ToolOutput\"}}"}"#.to_string() + }) + } + + fn bound_serialized_json(&mut self) { + loop { + let Ok(serialized) = serde_json::to_string(self) else { + return; + }; + if serialized.len() <= MAX_TOOL_OUTPUT_BYTES { + return; + } + let Some(content) = self.content.take() else { + return; + }; + if content.is_empty() { + return; + } + + let overflow = serialized.len() - MAX_TOOL_OUTPUT_BYTES; + let target = content.len().saturating_sub(overflow + "…".len()); + self.content = Some(bounded_text(content, target)); + } + } +} + +/// Stable, low-cardinality error codes for PDK-produced Tool errors. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ToolErrorCode { + InvalidInput, + InvalidOutput, + Serialization, + Denied, + UnsupportedTool, + Failed, +} + +impl ToolErrorCode { + pub fn as_str(self) -> &'static str { + match self { + Self::InvalidInput => "invalid_input", + Self::InvalidOutput => "invalid_output", + Self::Serialization => "serialization", + Self::Denied => "denied", + Self::UnsupportedTool => "unsupported_tool", + Self::Failed => "failed", + } + } +} + +/// Structured, bounded error that is rendered as ordinary ToolOutput JSON. +#[derive(Clone, Debug, PartialEq, Serialize, thiserror::Error)] +#[error("{code:?}: {message}")] +pub struct ToolError { + code: ToolErrorCode, + message: String, + #[serde(skip_serializing_if = "Option::is_none")] + details: Option, +} + +impl ToolError { + pub fn new(code: ToolErrorCode, message: impl Into) -> Self { + Self { + code, + message: bounded_text(message.into(), MAX_ERROR_MESSAGE_BYTES), + details: None, + } + } + + pub fn invalid_input(message: impl Into) -> Self { + Self::new(ToolErrorCode::InvalidInput, message) + } + + pub fn invalid_output(message: impl Into) -> Self { + Self::new(ToolErrorCode::InvalidOutput, message) + } + + pub fn serialization(error: impl std::fmt::Display) -> Self { + Self::new(ToolErrorCode::Serialization, error.to_string()) + } + + pub fn denied(message: impl Into) -> Self { + Self::new(ToolErrorCode::Denied, message) + } + + pub fn unsupported_tool(tool_name: impl Into) -> Self { + Self::new( + ToolErrorCode::UnsupportedTool, + format!("unsupported tool `{}`", tool_name.into()), + ) + } + + pub fn failed(message: impl Into) -> Self { + Self::new(ToolErrorCode::Failed, message) + } + + pub fn with_details(mut self, details: impl Serialize) -> Self { + self.details = serde_json::to_value(details).ok(); + self + } + + pub fn code(&self) -> ToolErrorCode { + self.code + } + + pub fn code_str(&self) -> &'static str { + self.code.as_str() + } + + pub fn message(&self) -> &str { + &self.message + } + + /// Render this error as ordinary ToolOutput JSON content. + pub fn into_tool_output(self) -> ToolOutput { + #[derive(Serialize)] + struct ErrorBody<'a> { + error: ErrorPayload<'a>, + } + + #[derive(Serialize)] + struct ErrorPayload<'a> { + code: &'static str, + message: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + details: Option<&'a Value>, + } + + let summary = format!("tool error: {}", self.code.as_str()); + let payload = ErrorBody { + error: ErrorPayload { + code: self.code.as_str(), + message: &self.message, + details: self.details.as_ref(), + }, + }; + let mut content = serde_json::to_string(&payload).unwrap_or_else(|_| { + format!( + r#"{{"error":{{"code":"{}","message":"failed to serialize error details"}}}}"#, + ToolErrorCode::Serialization.as_str() + ) + }); + if content.len() > MAX_CONTENT_BYTES { + let payload = ErrorBody { + error: ErrorPayload { + code: self.code.as_str(), + message: &self.message, + details: None, + }, + }; + content = serde_json::to_string(&payload).unwrap_or_else(|_| { + r#"{"error":{"code":"serialization","message":"failed to serialize error"}}"# + .to_string() + }); + } + ToolOutput::new(summary, Some(content)) + } +} + +/// Parse the WIT `input-json` string into a typed input value. +pub fn parse_json_input(input_json: &str) -> Result +where + T: DeserializeOwned, +{ + serde_json::from_str(input_json).map_err(|error| { + ToolError::invalid_input(format!( + "tool input is not valid JSON for this schema: {error}" + )) + }) +} + +/// Execute a typed JSON Tool handler and return ToolOutput JSON for the runtime. +/// +/// Handler errors and parse/serialization failures are not panics or host-side +/// authority decisions. They are rendered into ordinary ToolOutput JSON so the +/// runtime can route them through the normal Tool result path. +pub fn run_json_tool(tool_name: &str, input_json: &str, handler: F) -> String +where + I: DeserializeOwned, + F: FnOnce(ToolContext, I) -> Result, +{ + let result = parse_json_input::(input_json).and_then(|input| { + let context = ToolContext::new(tool_name); + handler(context, input) + }); + match result { + Ok(output) => output.to_json_string(), + Err(error) => error.into_tool_output().to_json_string(), + } +} + +/// Implement the generated Component Model `Guest` trait for a typed JSON +/// handler and export it with the `wit-bindgen` generated `export!` macro. +/// +/// The caller must invoke `yoi_plugin_pdk::wit_bindgen::generate!` for the +/// `tool` world first, which defines the `Guest` trait and `export!` macro in +/// the current module. The generated component still imports only WIT-declared +/// host APIs; this macro does not grant filesystem, network, or environment +/// authority. +#[macro_export] +macro_rules! export_component_tool { + ($adapter:ident, $handler:path) => { + struct $adapter; + + impl Guest for $adapter { + fn call( + tool_name: ::std::string::String, + input_json: ::std::string::String, + ) -> ::std::string::String { + $crate::run_json_tool(&tool_name, &input_json, $handler) + } + } + + export!($adapter); + }; +} + +fn normalize_summary(summary: String) -> String { + let summary = bounded_text(summary, MAX_SUMMARY_BYTES); + if summary.trim().is_empty() { + "tool completed".to_string() + } else { + summary + } +} + +fn bounded_text(mut value: String, max_bytes: usize) -> String { + if max_bytes == 0 { + return String::new(); + } + + value = value + .chars() + .map(|ch| { + if ch.is_control() && ch != '\n' && ch != '\t' { + ' ' + } else { + ch + } + }) + .collect(); + if value.len() <= max_bytes { + return value; + } + + let suffix = "…"; + let budget = max_bytes.saturating_sub(suffix.len()); + let mut cut = budget.min(value.len()); + while cut > 0 && !value.is_char_boundary(cut) { + cut -= 1; + } + value.truncate(cut); + value.push_str(suffix); + value +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + use serde_json::json; + + #[derive(Debug, Deserialize)] + struct EchoInput { + text: String, + } + + #[derive(Debug, Serialize)] + struct EchoOutput<'a> { + tool: &'a str, + text: String, + } + + #[test] + fn run_json_tool_parses_input_and_serializes_output() { + let output = run_json_tool( + "example_echo", + r#"{"text":"hello"}"#, + |ctx, input: EchoInput| { + ToolOutput::json( + format!("{} ok", ctx.tool_name()), + EchoOutput { + tool: ctx.tool_name(), + text: input.text, + }, + ) + }, + ); + + let value: Value = serde_json::from_str(&output).unwrap(); + assert_eq!(value["summary"], "example_echo ok"); + let content: Value = serde_json::from_str(value["content"].as_str().unwrap()).unwrap(); + assert_eq!(content, json!({"tool":"example_echo","text":"hello"})); + assert!(output.len() <= MAX_TOOL_OUTPUT_BYTES); + } + + #[test] + fn run_json_tool_renders_error_output() { + let output = + run_json_tool::("example_echo", r#"{"text":1}"#, |_ctx, _input| { + Ok(ToolOutput::summary("unreachable")) + }); + + let value: Value = serde_json::from_str(&output).unwrap(); + assert_eq!(value["summary"], "tool error: invalid_input"); + let content: Value = serde_json::from_str(value["content"].as_str().unwrap()).unwrap(); + assert_eq!(content["error"]["code"], "invalid_input"); + assert!(content["error"]["message"].as_str().unwrap().len() <= MAX_ERROR_MESSAGE_BYTES); + assert!(output.len() <= MAX_TOOL_OUTPUT_BYTES); + } + + #[test] + fn json_output_rejects_oversized_serialized_tool_output() { + let too_large = vec!["quoted \" text"; MAX_TOOL_OUTPUT_BYTES / 4]; + let error = ToolOutput::json("too large", too_large).unwrap_err(); + + assert_eq!(error.code(), ToolErrorCode::InvalidOutput); + } + + #[test] + fn tool_error_bounds_message_and_control_characters() { + let message = format!("bad\u{0007}{}", "x".repeat(MAX_ERROR_MESSAGE_BYTES * 2)); + let error = ToolError::failed(message); + + assert_eq!(error.code_str(), "failed"); + assert!(!error.message().contains('\u{0007}')); + assert!(error.message().len() <= MAX_ERROR_MESSAGE_BYTES + "…".len()); + } + + #[test] + fn wit_constants_match_current_world() { + assert!(TOOL_WIT.contains("package yoi:plugin@1.0.0")); + assert!(TOOL_WIT.contains("world tool")); + assert!(TOOL_WIT.contains("export call")); + assert_eq!(TOOL_WORLD, "yoi:plugin/tool@1.0.0"); + assert!(HOST_WIT.contains("interface https")); + assert!(HOST_WIT.contains("interface fs")); + } +} diff --git a/crates/plugin-pdk/tests/template.rs b/crates/plugin-pdk/tests/template.rs new file mode 100644 index 00000000..009cea58 --- /dev/null +++ b/crates/plugin-pdk/tests/template.rs @@ -0,0 +1,73 @@ +use toml::Value; + +const TEMPLATE_CARGO: &str = + include_str!("../../../resources/plugin/templates/rust-component-tool/Cargo.toml"); +const TEMPLATE_LIB: &str = + include_str!("../../../resources/plugin/templates/rust-component-tool/src/lib.rs"); +const TEMPLATE_PLUGIN: &str = + include_str!("../../../resources/plugin/templates/rust-component-tool/plugin.toml"); +const TEMPLATE_README: &str = + include_str!("../../../resources/plugin/templates/rust-component-tool/README.md"); +const SAMPLE_LIB: &str = include_str!("../../../docs/examples/plugin-component-tool/lib.rs"); +const PDK_CARGO: &str = include_str!("../Cargo.toml"); + +#[test] +fn rust_component_tool_template_has_expected_files() { + let cargo: Value = toml::from_str(TEMPLATE_CARGO).expect("template Cargo.toml parses"); + assert_eq!(cargo["package"]["edition"].as_str(), Some("2024")); + assert_eq!(cargo["lib"]["crate-type"][0].as_str(), Some("cdylib")); + assert_eq!( + cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(), + Some("../../../../crates/plugin-pdk") + ); + assert!(TEMPLATE_CARGO.contains("rev = \"\"")); + + let plugin: Value = toml::from_str(TEMPLATE_PLUGIN).expect("template plugin.toml parses"); + assert_eq!(plugin["schema_version"].as_integer(), Some(1)); + assert_eq!(plugin["runtime"]["kind"].as_str(), Some("wasm-component")); + assert_eq!( + plugin["runtime"]["world"].as_str(), + Some("yoi:plugin/tool@1.0.0") + ); + assert!(plugin["tools"].as_array().expect("tools array").len() == 1); + + assert!(TEMPLATE_LIB.contains("yoi_plugin_pdk::wit_bindgen::generate!")); + assert!(TEMPLATE_LIB.contains("yoi_plugin_pdk::export_component_tool!")); + assert!(TEMPLATE_LIB.contains("ToolOutput::json")); + assert!(TEMPLATE_README.contains("Component Model Tool Plugin")); +} + +#[test] +fn documented_sample_uses_pdk_component_path() { + assert!(SAMPLE_LIB.contains("yoi_plugin_pdk::wit_bindgen::generate!")); + assert!(SAMPLE_LIB.contains("yoi_plugin_pdk::export_component_tool!")); + assert!(!SAMPLE_LIB.contains("export_name")); +} + +#[test] +fn pdk_runtime_dependencies_are_guest_side_only() { + let cargo: Value = toml::from_str(PDK_CARGO).expect("PDK Cargo.toml parses"); + let dependencies = cargo["dependencies"] + .as_table() + .expect("dependencies table"); + let forbidden = [ + "pod", + "yoi-pod", + "llm-worker", + "tui", + "yoi-tui", + "client", + "yoi-client", + "manifest", + "yoi-manifest", + "ticket", + "yoi-ticket", + ]; + + for name in forbidden { + assert!( + !dependencies.contains_key(name), + "PDK must not depend on host/runtime crate `{name}`" + ); + } +} diff --git a/crates/pod/Cargo.toml b/crates/pod/Cargo.toml index 6bb0a8d6..42606ef0 100644 --- a/crates/pod/Cargo.toml +++ b/crates/pod/Cargo.toml @@ -44,6 +44,7 @@ dotenv = "0.15.0" futures = { workspace = true } tempfile = { workspace = true } wat = "1.241.2" +yoi-plugin-pdk = { workspace = true } [build-dependencies] toml = { workspace = true } diff --git a/crates/pod/src/feature/plugin.rs b/crates/pod/src/feature/plugin.rs index f03393f9..3ea94c52 100644 --- a/crates/pod/src/feature/plugin.rs +++ b/crates/pod/src/feature/plugin.rs @@ -3868,6 +3868,18 @@ mod tests { ); } + #[test] + fn pdk_tool_output_shape_is_accepted_by_wasm_decoder() { + let pdk_output = + yoi_plugin_pdk::ToolOutput::json("pdk ok", serde_json::json!({"answer": 42})) + .unwrap() + .to_json_string(); + + let output = decode_plugin_wasm_output(pdk_output.as_bytes()).unwrap(); + assert_eq!(output.summary, "pdk ok"); + assert_eq!(output.content.as_deref(), Some(r#"{"answer":42}"#)); + } + #[tokio::test] async fn malformed_input_json_fails_before_wasm_execution() { let (_dir, record) = resolved_record_with_wasm(input_reaches_guest_module()); diff --git a/docs/design/plugin-component-model.md b/docs/design/plugin-component-model.md index 1346e1b7..f3ab5ee5 100644 --- a/docs/design/plugin-component-model.md +++ b/docs/design/plugin-component-model.md @@ -100,7 +100,7 @@ The migration should be phased: 2. Add manifest/schema support for `runtime.kind = "wasm-component"` without executing it during discovery. 3. Add a component runtime backend and typed host import/export binding. 4. Port `https` and `fs` host API designs to WIT-compatible interfaces. -5. Add a Rust authoring SDK/template around the component world. +5. Add a Rust PDK/template around the component world. 6. Decide whether the raw ABI remains supported, becomes legacy-only, or is deprecated after examples and tests move. ## Runtime/backend caution @@ -161,10 +161,14 @@ Wrong `world`, missing artifact metadata, missing `call` export, unsupported imports, or core-Wasm bytes in a component package all fail closed with bounded Plugin diagnostics or ordinary Tool errors. -See `docs/examples/plugin-component-tool/lib.rs` for a minimal -`wit-bindgen`/SDK-style authoring sketch. Package authors should generate -bindings from `resources/plugin/wit`, build a component artifact, and set the -component runtime metadata above. +See `docs/examples/plugin-component-tool/lib.rs` and the embedded +`resources/plugin/templates/rust-component-tool/` starter for the preferred +Rust PDK authoring path. `yoi-plugin-pdk` is guest-side only: it re-exports +`wit-bindgen`, provides typed JSON input/output helpers, renders bounded +`ToolError` values as ordinary ToolOutput JSON, and does not depend on host +runtime crates or grant authority. Package authors should generate bindings from +`resources/plugin/wit`, build a component artifact, and set the component +runtime metadata above. ### v1 request/response shape diff --git a/docs/design/plugin-packages.md b/docs/design/plugin-packages.md index 3028a536..0a303361 100644 --- a/docs/design/plugin-packages.md +++ b/docs/design/plugin-packages.md @@ -52,7 +52,7 @@ entry = "plugin.wasm" abi = "yoi-plugin-wasm-1" ``` -The preferred future WASM authoring/runtime shape is the WebAssembly Component Model, recorded in [Plugin Component Model migration](plugin-component-model.md). Component packages should be explicit and source-compatible rather than silently changing the existing raw core-Wasm runtime: +The preferred WASM authoring/runtime shape is the WebAssembly Component Model, recorded in [Plugin Component Model migration](plugin-component-model.md). Component packages should be explicit and source-compatible rather than silently changing the existing raw core-Wasm runtime: ```toml [runtime] @@ -216,6 +216,13 @@ component = "plugin.component.wasm" world = "yoi:plugin/tool@1.0.0" ``` +For new Rust Tool packages, the preferred authoring path is the first-party +`yoi-plugin-pdk` plus the embedded `resources/plugin/templates/rust-component-tool/` +starter. The template uses a checkout-local path dependency for development and +documents a future out-of-tree pinned git `rev` dependency pattern. Crates.io +publication, remote template fetching, and package authoring commands are not +part of the current package/runtime contract. + This is separate from the legacy raw core-Wasm runtime: ```toml diff --git a/docs/development/plugin-development.md b/docs/development/plugin-development.md index 0c11bbd7..fc46081f 100644 --- a/docs/development/plugin-development.md +++ b/docs/development/plugin-development.md @@ -20,13 +20,15 @@ Implemented foundation: - Plugin permission grants; - raw core-Wasm Tool runtime; - Component Model Tool runtime; +- first-party Rust PDK helpers for Component Model Tool guests; +- embedded Rust Component Tool starter template; - `https` and `fs` host APIs for Tool runtime; - read-only `yoi plugin list/show` inspection. Still intentionally separate/future work: - `yoi plugin new/check/pack` authoring commands; -- polished multi-language SDK/PDK crates; +- multi-language SDK/PDK crates; - Service / Ingress surfaces; - WebSocket or inbound HTTP for bidirectional bridges; - public registry/install/update/signature tooling. @@ -90,9 +92,24 @@ abi = "yoi-plugin-wasm-1" Do not rely on package presence to activate anything. Discovery only records inventory. -## Component Model authoring sketch +## Component Model + Rust PDK authoring -Yoi's Component Model Tool world is stored in `resources/plugin/wit/`. A minimal Rust sketch is available at: +Component Model authoring with `yoi-plugin-pdk` is the preferred path for new Tool Plugins. The raw core-Wasm ABI remains available only as compatibility/transitional runtime support. + +Yoi's Component Model Tool world is stored in `resources/plugin/wit/`. The embedded Rust starter template is available as data in the Yoi source tree at: + +```text +resources/plugin/templates/rust-component-tool/ +``` + +It contains: + +- `Cargo.toml` with a checkout-local `yoi-plugin-pdk` path dependency; +- `src/lib.rs` with WIT binding generation and typed JSON Tool handling; +- `plugin.toml` targeting `kind = "wasm-component"` and `world = "yoi:plugin/tool@1.0.0"`; +- README next steps and the future out-of-tree pinned git `rev` dependency pattern. + +A minimal PDK-backed Rust sketch is also available at: ```text docs/examples/plugin-component-tool/lib.rs @@ -101,27 +118,43 @@ docs/examples/plugin-component-tool/lib.rs The important authoring shape is: ```rust -wit_bindgen::generate!({ +use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; + +yoi_plugin_pdk::wit_bindgen::generate!({ world: "tool", path: "../../../resources/plugin/wit", }); -struct Plugin; - -impl Guest for Plugin { - fn call(tool_name: String, input_json: String) -> String { - format!( - r#"{{"summary":"component tool {tool_name}","content":"input was {input_json}"}}"# - ) - } +#[derive(Deserialize)] +struct EchoInput { + text: String, } -export!(Plugin); +#[derive(Serialize)] +struct EchoOutput<'a> { + tool: &'a str, + text: String, +} + +fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result { + ToolOutput::json( + format!("{} ok", ctx.tool_name()), + EchoOutput { + tool: ctx.tool_name(), + text: input.text, + }, + ) +} + +yoi_plugin_pdk::export_component_tool!(Plugin, handle_echo); ``` -The returned string is ordinary `ToolOutput` JSON. It is routed through the normal Tool result path; the component cannot inject hidden context. +`run_json_tool` parses the WIT `input-json` string into a typed input, passes a `ToolContext` containing the selected Tool name, and serializes `ToolOutput` JSON accepted by the current component runtime. `ToolError` values are structured and bounded, then rendered through the ordinary Tool result path; the component cannot inject hidden context. -The exact build pipeline depends on the authoring toolchain (`wit-bindgen`, component adapter tooling, etc.). Until `yoi plugin new/check/pack` exists, Plugin authors should treat the example as the ABI contract sketch and use `yoi plugin list/show` plus focused runtime tests to verify packages. +The PDK is guest-side only. It does not depend on Yoi host/runtime crates and does not grant filesystem, network, or environment authority. Host-side Plugin manifests and explicit enablement grants remain the authority boundary for Tool execution and for WIT host APIs such as `yoi:host/https` and `yoi:host/fs`. + +The exact component build pipeline depends on the authoring toolchain (`wit-bindgen`, component adapter tooling, etc.). Crates.io publication, remote template fetching, and `yoi plugin new/check/pack` are intentionally deferred. Until they exist, Plugin authors should treat the template/example as the ABI contract sketch and use `yoi plugin list/show` plus focused runtime tests to verify packages. ## Enabling a Plugin in a workspace diff --git a/docs/examples/plugin-component-tool/lib.rs b/docs/examples/plugin-component-tool/lib.rs index 5b2724f7..94afd294 100644 --- a/docs/examples/plugin-component-tool/lib.rs +++ b/docs/examples/plugin-component-tool/lib.rs @@ -1,23 +1,35 @@ -//! Minimal Component Model Tool plugin authoring sketch. +//! Minimal Component Model Tool plugin authoring sketch using `yoi-plugin-pdk`. //! -//! Build this as a `wasm32-unknown-unknown` cdylib with `wit-bindgen`-generated -//! exports and package the adapted component as `plugin.component.wasm`. +//! Build this as a `wasm32-unknown-unknown` cdylib with Component Model tooling +//! and package the adapted component as `plugin.component.wasm`. -wit_bindgen::generate!({ +use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; + +yoi_plugin_pdk::wit_bindgen::generate!({ world: "tool", path: "../../../resources/plugin/wit", }); -struct Plugin; - -impl Guest for Plugin { - fn call(tool_name: String, input_json: String) -> String { - // Ordinary ToolOutput JSON. The runtime routes this through the normal - // Worker/Tool result path; no context is injected by the component. - format!( - r#"{{"summary":"component tool {tool_name}","content":"input was {input_json}"}}"# - ) - } +#[derive(Debug, Deserialize)] +struct EchoInput { + text: String, } -export!(Plugin); +#[derive(Debug, Serialize)] +struct EchoOutput<'a> { + tool: &'a str, + text: String, +} + +fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result { + ToolOutput::json( + format!("{} ok", ctx.tool_name()), + EchoOutput { + tool: ctx.tool_name(), + text: input.text, + }, + ) +} + +yoi_plugin_pdk::export_component_tool!(Plugin, handle_echo); diff --git a/package.nix b/package.nix index 17592a50..f1238c7a 100644 --- a/package.nix +++ b/package.nix @@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec { filter = sourceFilter; }; - cargoHash = "sha256-i4U7wXPoWIHA4EAJZva2HQXNN8P5+RhGVGNBAOZVGk0="; + cargoHash = "sha256-gMDU496wWn3LYhlXwxczHW/tT3IJxclwJtIY6ZjomtQ="; depsExtraArgs = { # Older fetchCargoVendor utilities used crates.io's API download endpoint, diff --git a/resources/plugin/templates/rust-component-tool/Cargo.toml b/resources/plugin/templates/rust-component-tool/Cargo.toml new file mode 100644 index 00000000..e852c364 --- /dev/null +++ b/resources/plugin/templates/rust-component-tool/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "yoi-rust-component-tool-template" +version = "0.1.0" +edition = "2024" +license = "MIT" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" } + +# Future out-of-tree Plugin packages should pin the Yoi revision instead of +# relying on crates.io publication or remote template fetching, for example: +# yoi-plugin-pdk = { git = "https://github.com/example/yoi.git", package = "yoi-plugin-pdk", rev = "" } diff --git a/resources/plugin/templates/rust-component-tool/README.md b/resources/plugin/templates/rust-component-tool/README.md new file mode 100644 index 00000000..aa07b2bf --- /dev/null +++ b/resources/plugin/templates/rust-component-tool/README.md @@ -0,0 +1,39 @@ +# Rust Component Tool Template + +This is the embedded starter template for a Yoi Component Model Tool Plugin written with the first-party Rust PDK. + +## What this template demonstrates + +- `wasm-component` runtime targeting `yoi:plugin/tool@1.0.0`. +- Guest-side WIT binding generation through the PDK's `wit_bindgen` re-export. +- Typed JSON input parsing through `run_json_tool` via `export_component_tool!`. +- Typed JSON output serialization with `ToolOutput::json`. +- Structured, bounded `ToolError` output for user-visible Tool failures. + +The PDK is guest-side only. It does not grant filesystem, network, or environment authority. Host-side Plugin manifests and grants remain the authority boundary for Tool execution and host APIs. + +## Checkout/development dependency + +Inside the Yoi checkout this template uses a local path dependency: + +```toml +yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" } +``` + +If this template is copied elsewhere before crates.io publication exists, pin a Yoi source revision instead of fetching an unpinned remote template: + +```toml +yoi-plugin-pdk = { git = "https://github.com/example/yoi.git", package = "yoi-plugin-pdk", rev = "" } +``` + +Crates.io publication, remote template fetching, and `yoi plugin new/check/pack` are intentionally deferred to later authoring-tooling work. + +## Next steps + +1. Replace package/plugin ids, names, descriptions, and Tool schema. +2. Replace `EchoInput` / `EchoOutput` and `handle_echo` with your Tool logic. +3. Build a component for `wasm32-unknown-unknown` with the Component Model tooling used by your environment. +4. Package `plugin.toml` and `plugin.component.wasm` into a `.yoi-plugin` archive. +5. Use `yoi plugin list` / `yoi plugin show` plus focused runtime tests to inspect and validate the package. + +The exact component build/pack command is not part of this template yet because deterministic `yoi plugin new/check/pack` authoring commands are a separate planned Ticket. diff --git a/resources/plugin/templates/rust-component-tool/plugin.toml b/resources/plugin/templates/rust-component-tool/plugin.toml new file mode 100644 index 00000000..9c5192d0 --- /dev/null +++ b/resources/plugin/templates/rust-component-tool/plugin.toml @@ -0,0 +1,20 @@ +schema_version = 1 +id = "example.rust_component_tool" +name = "Rust Component Tool Template" +version = "0.1.0" +surfaces = ["tool"] +permissions = [ + { kind = "surface", surface = "tool" }, + { kind = "tool", name = "example_echo" }, +] + +[runtime] +kind = "wasm-component" +component = "plugin.component.wasm" +world = "yoi:plugin/tool@1.0.0" + +[[tools]] +name = "example_echo" +description = "Echo input text using the Rust PDK." +input_schema = { type = "object", properties = { text = { type = "string" } }, required = ["text"], additionalProperties = false } +external_write = false diff --git a/resources/plugin/templates/rust-component-tool/src/lib.rs b/resources/plugin/templates/rust-component-tool/src/lib.rs new file mode 100644 index 00000000..ab8301c7 --- /dev/null +++ b/resources/plugin/templates/rust-component-tool/src/lib.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; + +yoi_plugin_pdk::wit_bindgen::generate!({ + world: "tool", + path: "../../../../resources/plugin/wit", +}); + +#[derive(Debug, Deserialize)] +struct EchoInput { + text: String, +} + +#[derive(Debug, Serialize)] +struct EchoOutput<'a> { + tool: &'a str, + text: String, +} + +fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result { + if input.text.trim().is_empty() { + return Err(ToolError::invalid_input("`text` must not be empty")); + } + + ToolOutput::json( + format!("{} echoed text", ctx.tool_name()), + EchoOutput { + tool: ctx.tool_name(), + text: input.text, + }, + ) +} + +yoi_plugin_pdk::export_component_tool!(Plugin, handle_echo); From 730bc73975d8d88abd9c54728de0cadcafb56c23 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:23:53 +0900 Subject: [PATCH 2/9] ticket: hold plugin authoring cli for pdk dependency --- .../artifacts/orchestration-plan.jsonl | 1 + .yoi/tickets/00001KVHKWNQS/item.md | 2 +- .yoi/tickets/00001KVHKWNQS/thread.md | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 .yoi/tickets/00001KVHKWNQS/artifacts/orchestration-plan.jsonl diff --git a/.yoi/tickets/00001KVHKWNQS/artifacts/orchestration-plan.jsonl b/.yoi/tickets/00001KVHKWNQS/artifacts/orchestration-plan.jsonl new file mode 100644 index 00000000..02343761 --- /dev/null +++ b/.yoi/tickets/00001KVHKWNQS/artifacts/orchestration-plan.jsonl @@ -0,0 +1 @@ +{"id":"orch-plan-20260620-052336-1","ticket_id":"00001KVHKWNQS","kind":"blocked_by","related_ticket":"00001KVHKWNQA","note":"Panel Queue was accepted for routing review, but implementation cannot start yet because `00001KVHKWNQS` depends on `00001KVHKWNQA` Rust PDK/templates, which is currently `inprogress` and in a reviewer-requested-changes loop. Leave `00001KVHKWNQS` queued until `00001KVHKWNQA` is closed, then reroute.","author":"yoi-orchestrator","at":"2026-06-20T05:23:36Z"} diff --git a/.yoi/tickets/00001KVHKWNQS/item.md b/.yoi/tickets/00001KVHKWNQS/item.md index f7f4d37d..45e3d111 100644 --- a/.yoi/tickets/00001KVHKWNQS/item.md +++ b/.yoi/tickets/00001KVHKWNQS/item.md @@ -2,7 +2,7 @@ title: 'Plugin: add authoring CLI new/check/pack' state: 'queued' created_at: '2026-06-20T04:16:14Z' -updated_at: '2026-06-20T05:23:14Z' +updated_at: '2026-06-20T05:23:44Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'cli', 'authoring', 'templates', 'package-validation', 'packaging', 'read-only-check'] diff --git a/.yoi/tickets/00001KVHKWNQS/thread.md b/.yoi/tickets/00001KVHKWNQS/thread.md index 2bd0edd3..f73d80b0 100644 --- a/.yoi/tickets/00001KVHKWNQS/thread.md +++ b/.yoi/tickets/00001KVHKWNQS/thread.md @@ -13,4 +13,24 @@ LocalTicketBackend によって作成されました。 Ticket を `workspace-panel` が queued にしました。 +--- + + + +## Decision + +Routing decision: blocked_pending_dependency + +Panel Queue により、この Ticket は Orchestrator routing 対象として明示許可されたが、実装開始はまだ行わない。 + +Reason: +- `00001KVHKWNQS` は `00001KVHKWNQA` に `depends_on` している。 +- `00001KVHKWNQA` は現在 `inprogress` で、Rust PDK / embedded template 実装が r1 review の `request_changes` 修正ループ中。 +- この Ticket の `new/check/pack` は PDK crate / embedded template を使うため、PDK/template が closed になるまで authoring CLI implementation の worktree/Pod side effects は開始しない。 +- Orchestrator workspace は clean、queued はこの Ticket のみ、inprogress は `00001KVHKWNQA` のみ、matching branch/worktree は存在しない。 + +Action: +- `00001KVHKWNQS` は `queued` のまま保持する。 +- `00001KVHKWNQA` が closed になった後、改めて body/thread/relations/workspace state を確認して routing する。 + --- From 0a9e585c1db3c324fd97f55c6c101b60a7370e55 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:39:10 +0900 Subject: [PATCH 3/9] plugin: fix rust pdk wit template probes --- Cargo.lock | 1 + crates/plugin-pdk/Cargo.toml | 1 + crates/plugin-pdk/src/lib.rs | 18 +++++-- crates/plugin-pdk/tests/template.rs | 48 ++++++++++++++++++- crates/plugin-pdk/tests/wit_bindgen_probe.rs | 25 ++++++++++ docs/development/plugin-development.md | 5 +- docs/examples/plugin-component-tool/lib.rs | 5 +- package.nix | 2 +- .../templates/rust-component-tool/Cargo.toml | 4 ++ .../templates/rust-component-tool/README.md | 2 +- .../templates/rust-component-tool/src/lib.rs | 5 +- .../wit/{ => deps/yoi-host}/yoi-host-v1.wit | 2 +- 12 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 crates/plugin-pdk/tests/wit_bindgen_probe.rs rename resources/plugin/wit/{ => deps/yoi-host}/yoi-host-v1.wit (90%) diff --git a/Cargo.lock b/Cargo.lock index a5c7fd56..ba35ed1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5561,6 +5561,7 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", + "tempfile", "thiserror 2.0.18", "toml", "wit-bindgen", diff --git a/crates/plugin-pdk/Cargo.toml b/crates/plugin-pdk/Cargo.toml index 025aa81e..99202a5b 100644 --- a/crates/plugin-pdk/Cargo.toml +++ b/crates/plugin-pdk/Cargo.toml @@ -13,4 +13,5 @@ thiserror.workspace = true wit-bindgen = "0.51.0" [dev-dependencies] +tempfile.workspace = true toml.workspace = true diff --git a/crates/plugin-pdk/src/lib.rs b/crates/plugin-pdk/src/lib.rs index 898f953d..69f9edd7 100644 --- a/crates/plugin-pdk/src/lib.rs +++ b/crates/plugin-pdk/src/lib.rs @@ -10,9 +10,13 @@ //! [`export_component_tool!`] macro: //! //! ```ignore -//! yoi_plugin_pdk::wit_bindgen::generate!({ +//! use yoi_plugin_pdk::wit_bindgen; +//! +//! wit_bindgen::generate!({ //! world: "tool", //! path: "../../../../resources/plugin/wit", +//! generate_all, +//! runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", //! }); //! //! fn echo( @@ -42,7 +46,8 @@ pub const TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0"; pub const TOOL_WIT: &str = include_str!("../../../resources/plugin/wit/yoi-plugin-tool-v1.wit"); /// Repository WIT for the grant-bound host APIs importable by Tool components. -pub const HOST_WIT: &str = include_str!("../../../resources/plugin/wit/yoi-host-v1.wit"); +pub const HOST_WIT: &str = + include_str!("../../../resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit"); /// Maximum serialized ToolOutput JSON accepted by Yoi's current Plugin runtime. pub const MAX_TOOL_OUTPUT_BYTES: usize = 64 * 1024; @@ -321,9 +326,11 @@ where /// Implement the generated Component Model `Guest` trait for a typed JSON /// handler and export it with the `wit-bindgen` generated `export!` macro. /// -/// The caller must invoke `yoi_plugin_pdk::wit_bindgen::generate!` for the -/// `tool` world first, which defines the `Guest` trait and `export!` macro in -/// the current module. The generated component still imports only WIT-declared +/// The caller must import the PDK's `wit_bindgen` re-export and invoke +/// `wit_bindgen::generate!` for the `tool` world first, with +/// `runtime_path: "yoi_plugin_pdk::wit_bindgen::rt"`. That defines the +/// `Guest` trait and `export!` macro in the current module. The generated +/// component still imports only WIT-declared /// host APIs; this macro does not grant filesystem, network, or environment /// authority. #[macro_export] @@ -464,5 +471,6 @@ mod tests { assert_eq!(TOOL_WORLD, "yoi:plugin/tool@1.0.0"); assert!(HOST_WIT.contains("interface https")); assert!(HOST_WIT.contains("interface fs")); + assert!(HOST_WIT.contains("%list: func")); } } diff --git a/crates/plugin-pdk/tests/template.rs b/crates/plugin-pdk/tests/template.rs index 009cea58..a5e2239d 100644 --- a/crates/plugin-pdk/tests/template.rs +++ b/crates/plugin-pdk/tests/template.rs @@ -1,3 +1,7 @@ +use std::fs; +use std::path::Path; +use std::process::Command; + use toml::Value; const TEMPLATE_CARGO: &str = @@ -15,6 +19,7 @@ const PDK_CARGO: &str = include_str!("../Cargo.toml"); fn rust_component_tool_template_has_expected_files() { let cargo: Value = toml::from_str(TEMPLATE_CARGO).expect("template Cargo.toml parses"); assert_eq!(cargo["package"]["edition"].as_str(), Some("2024")); + assert!(cargo["workspace"].is_table()); assert_eq!(cargo["lib"]["crate-type"][0].as_str(), Some("cdylib")); assert_eq!( cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(), @@ -31,7 +36,10 @@ fn rust_component_tool_template_has_expected_files() { ); assert!(plugin["tools"].as_array().expect("tools array").len() == 1); - assert!(TEMPLATE_LIB.contains("yoi_plugin_pdk::wit_bindgen::generate!")); + assert!(TEMPLATE_LIB.contains("use yoi_plugin_pdk::wit_bindgen")); + assert!(TEMPLATE_LIB.contains("wit_bindgen::generate!")); + assert!(TEMPLATE_LIB.contains("generate_all")); + assert!(TEMPLATE_LIB.contains("runtime_path: \"yoi_plugin_pdk::wit_bindgen::rt\"")); assert!(TEMPLATE_LIB.contains("yoi_plugin_pdk::export_component_tool!")); assert!(TEMPLATE_LIB.contains("ToolOutput::json")); assert!(TEMPLATE_README.contains("Component Model Tool Plugin")); @@ -39,11 +47,47 @@ fn rust_component_tool_template_has_expected_files() { #[test] fn documented_sample_uses_pdk_component_path() { - assert!(SAMPLE_LIB.contains("yoi_plugin_pdk::wit_bindgen::generate!")); + assert!(SAMPLE_LIB.contains("use yoi_plugin_pdk::wit_bindgen")); + assert!(SAMPLE_LIB.contains("wit_bindgen::generate!")); + assert!(SAMPLE_LIB.contains("generate_all")); + assert!(SAMPLE_LIB.contains("runtime_path: \"yoi_plugin_pdk::wit_bindgen::rt\"")); assert!(SAMPLE_LIB.contains("yoi_plugin_pdk::export_component_tool!")); assert!(!SAMPLE_LIB.contains("export_name")); } +#[test] +fn embedded_template_cargo_checks_for_wasm_target() { + let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let template_dir = crate_dir.join("../../resources/plugin/templates/rust-component-tool"); + let manifest_path = template_dir.join("Cargo.toml"); + let lock_path = template_dir.join("Cargo.lock"); + let _ = fs::remove_file(&lock_path); + + let target_dir = tempfile::tempdir().expect("temporary cargo target dir"); + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + let output = Command::new(cargo) + .arg("check") + .arg("--manifest-path") + .arg(&manifest_path) + .arg("--target") + .arg("wasm32-unknown-unknown") + .arg("--offline") + .arg("--target-dir") + .arg(target_dir.path()) + .env("CARGO_TERM_COLOR", "never") + .output() + .expect("spawn cargo check for embedded template"); + + let _ = fs::remove_file(&lock_path); + assert!( + output.status.success(), + "template cargo check failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + #[test] fn pdk_runtime_dependencies_are_guest_side_only() { let cargo: Value = toml::from_str(PDK_CARGO).expect("PDK Cargo.toml parses"); diff --git a/crates/plugin-pdk/tests/wit_bindgen_probe.rs b/crates/plugin-pdk/tests/wit_bindgen_probe.rs new file mode 100644 index 00000000..041e3a29 --- /dev/null +++ b/crates/plugin-pdk/tests/wit_bindgen_probe.rs @@ -0,0 +1,25 @@ +use yoi_plugin_pdk::wit_bindgen; + +wit_bindgen::generate!({ + world: "tool", + path: "../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", +}); + +struct Probe; + +impl Guest for Probe { + fn call(tool_name: String, input_json: String) -> String { + yoi_plugin_pdk::run_json_tool(&tool_name, &input_json, |_ctx, input: serde_json::Value| { + yoi_plugin_pdk::ToolOutput::json("probe ok", input) + }) + } +} + +#[test] +fn wit_bindgen_generates_current_tool_world() { + let output = ::call("probe".to_string(), r#"{"ok":true}"#.to_string()); + let value: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert_eq!(value["summary"], "probe ok"); +} diff --git a/docs/development/plugin-development.md b/docs/development/plugin-development.md index fc46081f..25edef62 100644 --- a/docs/development/plugin-development.md +++ b/docs/development/plugin-development.md @@ -119,11 +119,14 @@ The important authoring shape is: ```rust use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::wit_bindgen; use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; -yoi_plugin_pdk::wit_bindgen::generate!({ +wit_bindgen::generate!({ world: "tool", path: "../../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", }); #[derive(Deserialize)] diff --git a/docs/examples/plugin-component-tool/lib.rs b/docs/examples/plugin-component-tool/lib.rs index 94afd294..4311cc2f 100644 --- a/docs/examples/plugin-component-tool/lib.rs +++ b/docs/examples/plugin-component-tool/lib.rs @@ -4,11 +4,14 @@ //! and package the adapted component as `plugin.component.wasm`. use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::wit_bindgen; use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; -yoi_plugin_pdk::wit_bindgen::generate!({ +wit_bindgen::generate!({ world: "tool", path: "../../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", }); #[derive(Debug, Deserialize)] diff --git a/package.nix b/package.nix index f1238c7a..fdf427fe 100644 --- a/package.nix +++ b/package.nix @@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec { filter = sourceFilter; }; - cargoHash = "sha256-gMDU496wWn3LYhlXwxczHW/tT3IJxclwJtIY6ZjomtQ="; + cargoHash = "sha256-ci9h0U83YQQBeT3xlsGuKULnl1Aphgpg3pR4n0se16I="; depsExtraArgs = { # Older fetchCargoVendor utilities used crates.io's API download endpoint, diff --git a/resources/plugin/templates/rust-component-tool/Cargo.toml b/resources/plugin/templates/rust-component-tool/Cargo.toml index e852c364..a2013985 100644 --- a/resources/plugin/templates/rust-component-tool/Cargo.toml +++ b/resources/plugin/templates/rust-component-tool/Cargo.toml @@ -5,6 +5,10 @@ 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"] diff --git a/resources/plugin/templates/rust-component-tool/README.md b/resources/plugin/templates/rust-component-tool/README.md index aa07b2bf..9208c75f 100644 --- a/resources/plugin/templates/rust-component-tool/README.md +++ b/resources/plugin/templates/rust-component-tool/README.md @@ -14,7 +14,7 @@ The PDK is guest-side only. It does not grant filesystem, network, or environmen ## Checkout/development dependency -Inside the Yoi checkout this template uses a local path dependency: +Inside the Yoi checkout this template uses a local path dependency and declares an empty `[workspace]` so it can be checked in place without becoming a member of Yoi's root workspace: ```toml yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" } diff --git a/resources/plugin/templates/rust-component-tool/src/lib.rs b/resources/plugin/templates/rust-component-tool/src/lib.rs index ab8301c7..c954aedc 100644 --- a/resources/plugin/templates/rust-component-tool/src/lib.rs +++ b/resources/plugin/templates/rust-component-tool/src/lib.rs @@ -1,9 +1,12 @@ use serde::{Deserialize, Serialize}; +use yoi_plugin_pdk::wit_bindgen; use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput}; -yoi_plugin_pdk::wit_bindgen::generate!({ +wit_bindgen::generate!({ world: "tool", path: "../../../../resources/plugin/wit", + generate_all, + runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", }); #[derive(Debug, Deserialize)] diff --git a/resources/plugin/wit/yoi-host-v1.wit b/resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit similarity index 90% rename from resources/plugin/wit/yoi-host-v1.wit rename to resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit index ae66da92..e0544a0a 100644 --- a/resources/plugin/wit/yoi-host-v1.wit +++ b/resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit @@ -10,6 +10,6 @@ interface https { /// Grant-bound filesystem host API. No ambient WASI filesystem is exposed. interface fs { read: func(request-json: string) -> string; - list: func(request-json: string) -> string; + %list: func(request-json: string) -> string; write: func(request-json: string) -> string; } From 01f2e926b0d6945a96cf0ad0b5857fdba1ba891a Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:40:23 +0900 Subject: [PATCH 4/9] ticket: record plugin rust pdk template fix --- .yoi/tickets/00001KVHKWNQA/item.md | 2 +- .yoi/tickets/00001KVHKWNQA/thread.md | 55 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KVHKWNQA/item.md b/.yoi/tickets/00001KVHKWNQA/item.md index effe7b35..2208a31a 100644 --- a/.yoi/tickets/00001KVHKWNQA/item.md +++ b/.yoi/tickets/00001KVHKWNQA/item.md @@ -2,7 +2,7 @@ title: 'Plugin: add Rust PDK and embedded authoring templates for Component Model Tools' state: 'inprogress' created_at: '2026-06-20T04:16:14Z' -updated_at: '2026-06-20T05:21:50Z' +updated_at: '2026-06-20T05:40:15Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'pdk', 'component-model', 'authoring', 'templates', 'sdk', 'no-crates-io'] diff --git a/.yoi/tickets/00001KVHKWNQA/thread.md b/.yoi/tickets/00001KVHKWNQA/thread.md index d1ee097b..9f3e5c1b 100644 --- a/.yoi/tickets/00001KVHKWNQA/thread.md +++ b/.yoi/tickets/00001KVHKWNQA/thread.md @@ -223,3 +223,58 @@ Reviewer validation: Worktree status at review end: source tree clean; ignored artifacts under `target/` only。 --- + + + +## Implementation report + +Coder r1-fix report received from `yoi-coder-00001KVHKWNQA`. + +New fix commit: +- `0a9e585c plugin: fix rust pdk wit template probes` + +Branch commits now: +- `0a9e585c plugin: fix rust pdk wit template probes` +- `06287aca plugin: add rust pdk template` + +Fix summary: +- Moved host WIT to dependency package path `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit` so `resources/plugin/wit` is a valid WIT package directory with `yoi:host` resolved as dependency。 +- Escaped WIT keyword function as `%list` while preserving import name semantics。 +- Added empty `[workspace]` to embedded template `Cargo.toml` so it can be checked in-place without becoming an undeclared member of Yoi root workspace。 +- Updated template/example/docs/PDK snippets to the compile-tested binding pattern with PDK `wit_bindgen` re-export, `generate_all`, and `runtime_path: "yoi_plugin_pdk::wit_bindgen::rt"`。 +- Added compile-time probe `crates/plugin-pdk/tests/wit_bindgen_probe.rs` that runs `wit_bindgen::generate!` against `resources/plugin/wit`。 +- Added embedded template cargo-check probe for `wasm32-unknown-unknown` in `crates/plugin-pdk/tests/template.rs`。 +- Added `%list` WIT assertion and updated host WIT include path。 +- Updated `Cargo.lock` and `package.nix` for new dev dependency edge / cargo hash。 + +Coder validation reported as passing: +- `cargo test -p yoi-plugin-pdk` including PDK tests, WIT constants, actual `wit_bindgen::generate!` probe, and embedded template wasm32 check probe。 +- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape` +- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder` +- `cargo check` +- `cargo tree -p yoi-plugin-pdk --edges normal` +- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown` +- `cargo fmt --check` +- `git diff --check 5f7f81bd..HEAD` +- `git diff --check` +- `git diff --check --cached` +- `nix build .#yoi --no-link` + +Operational note: +- `nix build .#yoi --no-link` initially hit disk exhaustion after accepting the updated vendor hash; Coder removed local implementation worktree `target/` cache and reran successfully。 + +Remaining deferrals: +- No `yoi plugin new/check/pack`。 +- No crates.io publication or remote template fetching。 +- Full packaged component execution remains deferred; the current WIT parses with `wit-bindgen` and embedded Rust template compiles to the wasm target boundary。 + +Orchestrator evidence checked before r2 review: +- Implementation worktree is clean。 +- HEAD is `0a9e585c`。 +- Fix diff from `06287aca..HEAD` touches 12 files, about 105 insertions / 13 deletions。 +- Overall `git diff --check 5f7f81bd..HEAD` produced no diagnostics。 + +Next action: +- Route back to Reviewer for r2 verification of the WIT parse/template compile blockers and final acceptance criteria。 + +--- From 1f0766c1d79d66c84e25d37b898d152cce57a271 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:47:25 +0900 Subject: [PATCH 5/9] ticket: approve plugin rust pdk templates --- .yoi/tickets/00001KVHKWNQA/item.md | 2 +- .yoi/tickets/00001KVHKWNQA/thread.md | 49 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KVHKWNQA/item.md b/.yoi/tickets/00001KVHKWNQA/item.md index 2208a31a..3846e2d3 100644 --- a/.yoi/tickets/00001KVHKWNQA/item.md +++ b/.yoi/tickets/00001KVHKWNQA/item.md @@ -2,7 +2,7 @@ title: 'Plugin: add Rust PDK and embedded authoring templates for Component Model Tools' state: 'inprogress' created_at: '2026-06-20T04:16:14Z' -updated_at: '2026-06-20T05:40:15Z' +updated_at: '2026-06-20T05:47:13Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'pdk', 'component-model', 'authoring', 'templates', 'sdk', 'no-crates-io'] diff --git a/.yoi/tickets/00001KVHKWNQA/thread.md b/.yoi/tickets/00001KVHKWNQA/thread.md index 9f3e5c1b..ac715336 100644 --- a/.yoi/tickets/00001KVHKWNQA/thread.md +++ b/.yoi/tickets/00001KVHKWNQA/thread.md @@ -278,3 +278,52 @@ Next action: - Route back to Reviewer for r2 verification of the WIT parse/template compile blockers and final acceptance criteria。 --- + + + +## Review: approve + +Verdict: `approve` + +確認範囲: +- Ticket body/thread / Orchestrator IntentPacket +- r2 fix diff: `06287aca..0a9e585c` +- 全体実装 diff: `5f7f81bd..0a9e585c` +- 主な対象: `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit`, `resources/plugin/wit/yoi-plugin-tool-v1.wit`, embedded template, `crates/plugin-pdk/*`, `crates/manifest/src/plugin.rs`, `crates/pod/src/feature/plugin.rs`, docs, `package.nix`。 + +Previous blockers verification: +- Host WIT は `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit` に配置され、`resources/plugin/wit` は current package directory として `wit-bindgen` が parse できる形になった。 +- Keyword function は `%list` として escape され、import name semantics を保持している。 +- Embedded template の `Cargo.toml` には empty `[workspace]` があり、root workspace 配下の non-member package として拒否されない。 +- `crates/plugin-pdk/tests/wit_bindgen_probe.rs` は実際に `wit_bindgen::generate!` を `resources/plugin/wit` に対して実行している。 +- Embedded template は `wasm32-unknown-unknown` target で cargo-check できる。 + +Overall Ticket fit: +- PDK は guest-side に留まり、host/runtime Yoi crates への dependency は見当たらない。 +- PDK / template は authority を付与せず、host-side Plugin manifest grants が authority boundary であることを維持している。 +- Ambient fs/network/env authority は露出していない。 +- crates.io publication / remote template fetch / `yoi plugin new/check/pack` は実装していない。 +- Docs は full build/pack boundary と deferral を概ね正直に記述している。 + +Blocking issues: none。 + +Non-blocking concerns / follow-ups: +- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown` を直接実行すると template-local `Cargo.lock` と `target/` ができる。Reviewer は検証後に untracked artifacts を削除した。テスト自体は temporary target dir を使い template lockfile を消すため blocking ではないが、将来 docs/CI では explicit `--target-dir` を推奨するとよい。 +- Reviewer r2 の `nix build .#yoi --no-link` は final artifact copy 中に `No space left on device` で environment-fail。cargo hash/source-filter/code diagnostic ではない。Coder 側では同コマンド成功済み。Orchestrator final validation で再確認する。 + +Reviewer validation: +- `cargo test -p yoi-plugin-pdk`: passed +- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape`: passed +- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder`: passed +- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown`: passed +- `cargo tree -p yoi-plugin-pdk --edges normal`: passed; host/runtime Yoi crate dependencyなし +- `cargo fmt --check`: passed +- `git diff --check 5f7f81bd..HEAD`: passed +- `git diff --check`: passed +- `git diff --check --cached`: passed +- `cargo check`: passed +- `nix build .#yoi --no-link`: attempted, environment failed with `No space left on device` during final artifact copying + +Worktree status at review end: source tree clean; ignored artifacts under normal `target/` only。 + +--- From ab7ab69f2089e393fec1d0e4cecf6ef40822493d Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:51:47 +0900 Subject: [PATCH 6/9] ticket: complete plugin rust pdk templates --- .yoi/tickets/00001KVHKWNQA/item.md | 4 +-- .yoi/tickets/00001KVHKWNQA/thread.md | 51 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/.yoi/tickets/00001KVHKWNQA/item.md b/.yoi/tickets/00001KVHKWNQA/item.md index 3846e2d3..7b6c92cf 100644 --- a/.yoi/tickets/00001KVHKWNQA/item.md +++ b/.yoi/tickets/00001KVHKWNQA/item.md @@ -1,8 +1,8 @@ --- title: 'Plugin: add Rust PDK and embedded authoring templates for Component Model Tools' -state: 'inprogress' +state: 'done' created_at: '2026-06-20T04:16:14Z' -updated_at: '2026-06-20T05:47:13Z' +updated_at: '2026-06-20T05:51:42Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'pdk', 'component-model', 'authoring', 'templates', 'sdk', 'no-crates-io'] diff --git a/.yoi/tickets/00001KVHKWNQA/thread.md b/.yoi/tickets/00001KVHKWNQA/thread.md index ac715336..13bf2128 100644 --- a/.yoi/tickets/00001KVHKWNQA/thread.md +++ b/.yoi/tickets/00001KVHKWNQA/thread.md @@ -327,3 +327,54 @@ Reviewer validation: Worktree status at review end: source tree clean; ignored artifacts under normal `target/` only。 --- + + + +## Implementation report + +Merge and final validation report: + +Merged implementation branch into Orchestrator branch: +- Merge commit: `edc53a6b merge: plugin rust pdk templates` +- Implementation commits included: + - `06287aca plugin: add rust pdk template` + - `0a9e585c plugin: fix rust pdk wit template probes` + +Reviewer outcome: +- r1 requested changes for WIT parse failure (`list` keyword) and embedded template Cargo workspace check failure。 +- Coder fixed WIT package layout / `%list` escaping, added actual `wit_bindgen::generate!` probe, and made the embedded template standalone-checkable。 +- r2 approved with no blocking issues。 + +Orchestrator validation after merge passed: +- `cargo fmt --check` +- `git diff --check HEAD^1..HEAD` +- `cargo test -p yoi-plugin-pdk` +- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape` +- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder` +- `cargo check` +- `cargo tree -p yoi-plugin-pdk --edges normal` +- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown` +- `nix build .#yoi --no-link` + +Validation log: +- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-o9gvGb.log` + +Package impact: +- `nix path-info -S .#yoi`: `112156384` + +Cleanup note: +- Direct template cargo-check creates local `resources/plugin/templates/rust-component-tool/Cargo.lock` and `target/`; Orchestrator removed those untracked artifacts after validation. Worktree is clean。 + +Final state: +- Orchestrator worktree clean at `edc53a6b` after validation。 +- Implementation worktree remains available for cleanup after Ticket completion records are committed。 + +--- + + + +## State changed + +Implementation was merged into Orchestrator branch at `edc53a6b`, r2 review approved, and final Orchestrator validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, focused PDK/manifest/pod tests, `cargo check`, PDK dependency tree check, embedded template wasm32 check, and `nix build .#yoi --no-link`. + +--- From 902b383de7d3f75d0640aa643bf567fbcb0a89a4 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:53:26 +0900 Subject: [PATCH 7/9] ticket: close plugin rust pdk templates --- .yoi/tickets/00001KVHKWNQA/item.md | 4 +- .yoi/tickets/00001KVHKWNQA/resolution.md | 43 +++++++++++++++++ .yoi/tickets/00001KVHKWNQA/thread.md | 59 ++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 .yoi/tickets/00001KVHKWNQA/resolution.md diff --git a/.yoi/tickets/00001KVHKWNQA/item.md b/.yoi/tickets/00001KVHKWNQA/item.md index 7b6c92cf..c78125a4 100644 --- a/.yoi/tickets/00001KVHKWNQA/item.md +++ b/.yoi/tickets/00001KVHKWNQA/item.md @@ -1,8 +1,8 @@ --- title: 'Plugin: add Rust PDK and embedded authoring templates for Component Model Tools' -state: 'done' +state: 'closed' created_at: '2026-06-20T04:16:14Z' -updated_at: '2026-06-20T05:51:42Z' +updated_at: '2026-06-20T05:53:20Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'pdk', 'component-model', 'authoring', 'templates', 'sdk', 'no-crates-io'] diff --git a/.yoi/tickets/00001KVHKWNQA/resolution.md b/.yoi/tickets/00001KVHKWNQA/resolution.md new file mode 100644 index 00000000..b3479173 --- /dev/null +++ b/.yoi/tickets/00001KVHKWNQA/resolution.md @@ -0,0 +1,43 @@ +## Resolution + +`00001KVHKWNQA` を完了しました。 + +実装内容: +- Guest-side Rust PDK crate `yoi-plugin-pdk` を追加しました。 +- PDK は typed JSON input/output helper、bounded `ToolError`、`ToolContext`、`run_json_tool` 系 helper、`wit_bindgen` re-export、`export_component_tool!` macro を提供します。 +- PDK は host/runtime Yoi crates に依存せず、authority を付与しません。Host-side Plugin manifest grants が Tool execution / host API use の authority boundary のままです。 +- Embedded Rust Component Tool template を `resources/plugin/templates/rust-component-tool/` に追加しました。 +- Template は local checkout/dev path dependency を使い、future out-of-tree git `rev` pattern を docs に記録しています。 +- `resources/plugin/wit` を `wit-bindgen` が parse できる package layout に修正し、host WIT dependency を `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit` に移動しました。 +- WIT keyword `list` は `%list` escape にし、import name semantics を保持しました。 +- Embedded template は empty `[workspace]` により in-tree standalone package として check できます。 +- `wit_bindgen::generate!` を実際に `resources/plugin/wit` に対して実行する probe と、embedded template の `wasm32-unknown-unknown` cargo-check probe を追加しました。 +- Plugin development docs / design docs / package docs / example source を更新しました。 +- `yoi plugin new/check/pack`、remote template fetch、crates.io publication、full packaged component execution はこの Ticket の non-goals / follow-up として残しました。 + +主な commit: +- `06287aca plugin: add rust pdk template` +- `0a9e585c plugin: fix rust pdk wit template probes` +- `edc53a6b merge: plugin rust pdk templates` + +Review: +- r1 は WIT parse failure と embedded template Cargo workspace issue で `request_changes`。 +- Coder が WIT layout / `%list` / template `[workspace]` / actual probes を追加。 +- r2 は `approve`。 + +最終 validation: +- `cargo fmt --check` +- `git diff --check HEAD^1..HEAD` +- `cargo test -p yoi-plugin-pdk` +- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape` +- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder` +- `cargo check` +- `cargo tree -p yoi-plugin-pdk --edges normal` +- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown` +- `nix build .#yoi --no-link` + +Package impact: +- `nix path-info -S .#yoi`: `112156384` + +Validation log: +- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-o9gvGb.log` \ No newline at end of file diff --git a/.yoi/tickets/00001KVHKWNQA/thread.md b/.yoi/tickets/00001KVHKWNQA/thread.md index 13bf2128..0f39a1a5 100644 --- a/.yoi/tickets/00001KVHKWNQA/thread.md +++ b/.yoi/tickets/00001KVHKWNQA/thread.md @@ -378,3 +378,62 @@ Final state: Implementation was merged into Orchestrator branch at `edc53a6b`, r2 review approved, and final Orchestrator validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, focused PDK/manifest/pod tests, `cargo check`, PDK dependency tree check, embedded template wasm32 check, and `nix build .#yoi --no-link`. --- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +## Resolution + +`00001KVHKWNQA` を完了しました。 + +実装内容: +- Guest-side Rust PDK crate `yoi-plugin-pdk` を追加しました。 +- PDK は typed JSON input/output helper、bounded `ToolError`、`ToolContext`、`run_json_tool` 系 helper、`wit_bindgen` re-export、`export_component_tool!` macro を提供します。 +- PDK は host/runtime Yoi crates に依存せず、authority を付与しません。Host-side Plugin manifest grants が Tool execution / host API use の authority boundary のままです。 +- Embedded Rust Component Tool template を `resources/plugin/templates/rust-component-tool/` に追加しました。 +- Template は local checkout/dev path dependency を使い、future out-of-tree git `rev` pattern を docs に記録しています。 +- `resources/plugin/wit` を `wit-bindgen` が parse できる package layout に修正し、host WIT dependency を `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit` に移動しました。 +- WIT keyword `list` は `%list` escape にし、import name semantics を保持しました。 +- Embedded template は empty `[workspace]` により in-tree standalone package として check できます。 +- `wit_bindgen::generate!` を実際に `resources/plugin/wit` に対して実行する probe と、embedded template の `wasm32-unknown-unknown` cargo-check probe を追加しました。 +- Plugin development docs / design docs / package docs / example source を更新しました。 +- `yoi plugin new/check/pack`、remote template fetch、crates.io publication、full packaged component execution はこの Ticket の non-goals / follow-up として残しました。 + +主な commit: +- `06287aca plugin: add rust pdk template` +- `0a9e585c plugin: fix rust pdk wit template probes` +- `edc53a6b merge: plugin rust pdk templates` + +Review: +- r1 は WIT parse failure と embedded template Cargo workspace issue で `request_changes`。 +- Coder が WIT layout / `%list` / template `[workspace]` / actual probes を追加。 +- r2 は `approve`。 + +最終 validation: +- `cargo fmt --check` +- `git diff --check HEAD^1..HEAD` +- `cargo test -p yoi-plugin-pdk` +- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape` +- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder` +- `cargo check` +- `cargo tree -p yoi-plugin-pdk --edges normal` +- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown` +- `nix build .#yoi --no-link` + +Package impact: +- `nix path-info -S .#yoi`: `112156384` + +Validation log: +- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-o9gvGb.log` + +--- From d1095f854a736291049f99030ddd0c3b76bb40fc Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:54:50 +0900 Subject: [PATCH 8/9] ticket: accept plugin authoring cli --- .../artifacts/orchestration-plan.jsonl | 1 + .yoi/tickets/00001KVHKWNQS/item.md | 4 +- .yoi/tickets/00001KVHKWNQS/thread.md | 75 +++++++++++++++++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/.yoi/tickets/00001KVHKWNQS/artifacts/orchestration-plan.jsonl b/.yoi/tickets/00001KVHKWNQS/artifacts/orchestration-plan.jsonl index 02343761..d61d9134 100644 --- a/.yoi/tickets/00001KVHKWNQS/artifacts/orchestration-plan.jsonl +++ b/.yoi/tickets/00001KVHKWNQS/artifacts/orchestration-plan.jsonl @@ -1 +1,2 @@ {"id":"orch-plan-20260620-052336-1","ticket_id":"00001KVHKWNQS","kind":"blocked_by","related_ticket":"00001KVHKWNQA","note":"Panel Queue was accepted for routing review, but implementation cannot start yet because `00001KVHKWNQS` depends on `00001KVHKWNQA` Rust PDK/templates, which is currently `inprogress` and in a reviewer-requested-changes loop. Leave `00001KVHKWNQS` queued until `00001KVHKWNQA` is closed, then reroute.","author":"yoi-orchestrator","at":"2026-06-20T05:23:36Z"} +{"id":"orch-plan-20260620-055356-2","ticket_id":"00001KVHKWNQS","kind":"accepted_plan","accepted_plan":{"summary":"`yoi plugin new rust-component-tool`, `yoi plugin check`, `yoi plugin pack` を追加する。new は embedded template only、check/pack は Plugin code を実行せず、directory/package safety、manifest/runtime/schema/permission diagnostics、deterministic digest/package output、JSON reports、enablement guidance を提供する。","branch":"impl/00001KVHKWNQS-plugin-authoring-cli","worktree":"/home/hare/Projects/yoi/.worktree/00001KVHKWNQS-plugin-authoring-cli","role_plan":"Orchestrator は acceptance records を commit 後、専用 implementation worktree `.worktree/00001KVHKWNQS-plugin-authoring-cli` を作成し、Coder をその child worktree への narrow write scope で起動する。Coder 実装後、Reviewer が non-execution checks、archive safety、deterministic pack/digest、JSON report stability、template use、workspace/config non-mutation、Nix/resource packaging を確認する。"},"author":"yoi-orchestrator","at":"2026-06-20T05:53:56Z"} diff --git a/.yoi/tickets/00001KVHKWNQS/item.md b/.yoi/tickets/00001KVHKWNQS/item.md index 45e3d111..9f5565ec 100644 --- a/.yoi/tickets/00001KVHKWNQS/item.md +++ b/.yoi/tickets/00001KVHKWNQS/item.md @@ -1,8 +1,8 @@ --- title: 'Plugin: add authoring CLI new/check/pack' -state: 'queued' +state: 'inprogress' created_at: '2026-06-20T04:16:14Z' -updated_at: '2026-06-20T05:23:44Z' +updated_at: '2026-06-20T05:54:45Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'cli', 'authoring', 'templates', 'package-validation', 'packaging', 'read-only-check'] diff --git a/.yoi/tickets/00001KVHKWNQS/thread.md b/.yoi/tickets/00001KVHKWNQS/thread.md index f73d80b0..4981db0d 100644 --- a/.yoi/tickets/00001KVHKWNQS/thread.md +++ b/.yoi/tickets/00001KVHKWNQS/thread.md @@ -34,3 +34,78 @@ Action: - `00001KVHKWNQA` が closed になった後、改めて body/thread/relations/workspace state を確認して routing する。 --- + + + +## Decision + +Routing decision: implementation_ready + +Reason: +- Panel Queue により、この Ticket は Orchestrator routing 対象として明示許可された。 +- 前回は `00001KVHKWNQA` Rust PDK/templates が inprogress だったため blocked/queued hold としたが、現在 `00001KVHKWNQA` は closed。 +- Ticket body は `new` / `check` / `pack` の CLI surface、non-execution safety、archive safety、deterministic digest/package、JSON reports、diagnostics、tests、validation、non-goals を実装可能な粒度で定義している。 +- Related Plugin CLI inspection (`00001KVFD3YSV`) と Component runtime (`00001KVG0HR96`) は closed。 +- 現在 queued はこの Ticket のみ、inprogress は 0 件、child implementation Pods はなし、matching branch/worktree はなし、Orchestrator worktree は clean。 +- Risk domain は plugin / CLI / authoring / templates / package-validation / packaging / read-only-check だが、Ticket は check/pack が Plugin code を実行しない、new は embedded templates only、enablement config を mutate しない、safe overwrite refusal、archive traversal/root-escape rejection などの invariants を明示している。bounded context check 後も implementation 前に必要な追加 human decision は見つからなかった。 + +Evidence checked: +- Ticket `00001KVHKWNQS` body / thread / relations / artifacts。 +- `TicketRelationQuery(00001KVHKWNQS)`: outgoing `depends_on 00001KVHKWNQA` is now closed。Related records are closed context。 +- `TicketOrchestrationPlanQuery(00001KVHKWNQS)`: previous `blocked_by` plan is resolved by `00001KVHKWNQA` closure; accepted plan recorded now。 +- Workspace state: + - Orchestrator worktree clean at `902b383d`。 + - queued: this Ticket only。 + - inprogress: 0。 + - visible Pods: self + peers only; spawned children 0。 + - no matching implementation branch/worktree。 +- Code/resource context: + - Rust PDK/template resources are now merged from `00001KVHKWNQA`。 + - Component Model runtime and Plugin CLI inspection work are closed and available as implementation context。 + +IntentPacket: + +Intent: +- Add first-party local Plugin authoring CLI commands: `yoi plugin new rust-component-tool `, `yoi plugin check `, and `yoi plugin pack [--output ]`。 +- Make local authoring safe and deterministic without remote scripts, without executing Plugin code during validation, and without mutating workspace enablement config。 + +Binding decisions / invariants: +- `new` uses embedded templates only; no network, no remote template fetch, no `curl | sh` flow。 +- `new` writes only to the requested destination and refuses non-empty destinations unless a narrow explicit safe option is intentionally added。 +- Generated Rust Component Tool template should use the current PDK/template resources and current checkout/release dependency policy。 +- `check` and `pack` must not execute Plugin code or instantiate components。 +- `check` validates directory and `.yoi-plugin` package inputs with bounded diagnostics and stable JSON report shape for `--json`。 +- `pack` creates deterministic `.yoi-plugin` output and prints digest/path; `pack --json` returns stable typed output。 +- `check` validates manifest/runtime/schema/permission/host API declarations, referenced artifact presence, archive safety, and deterministic digest where applicable。 +- `pack` rejects unsafe paths/root escapes and unsupported package shapes; use currently supported archive format/constraints。 +- Commands do not mutate enablement/workspace config and do not generate/embed secrets。 +- Diagnostics/status language should align with existing `yoi plugin list/show` where possible。 +- Do not implement registry publish/install, enabling/disabling config, Plugin execution, Service/Ingress scaffolding, or extra language templates。 + +Requirements / acceptance criteria: +- `yoi plugin new rust-component-tool ./my-plugin` creates the expected file set from embedded template without network access。 +- `yoi plugin check ./my-plugin` validates the generated template and reports next steps/digest/enablement guidance。 +- `yoi plugin pack ./my-plugin` creates a `.yoi-plugin` package discoverable by existing Plugin discovery。 +- `check` validates existing `.yoi-plugin` archive inputs and rejects unsafe archives。 +- `check --json` and `pack --json` provide stable typed reports for tests/agents。 +- Tests cover generated file set, overwrite refusal, valid/invalid check paths, missing runtime artifact, unsafe package archive, deterministic pack digest, discoverability by existing Plugin discovery, and JSON report shape。 +- Validation includes focused CLI/plugin authoring tests, relevant `cargo check` / `cargo test`, `cargo fmt --check`, `git diff --check`, and `nix build .#yoi` because CLI/resources/package behavior changes。 + +Escalate if: +- Current package/archive format lacks enough existing helpers and needs a broader package-format decision。 +- `check` cannot validate Component world metadata without executing code and needs explicit scope narrowing。 +- Deterministic packaging conflicts with current discovery/package reader behavior。 +- Embedded template resource packaging/source filtering needs broader product decision。 + +Next action: +- Record `queued -> inprogress` and commit Ticket records before creating the implementation worktree and spawning Coder。 + +--- + + + +## State changed + +Ticket body/thread, relation metadata, resolved Rust PDK dependency, related closed Tickets, Orchestrator worktree state, visible Pods, existing branch/worktree, and bounded CLI/resource/package context were checked. Dependency `00001KVHKWNQA` is closed, no inprogress/capacity blocker exists, and no missing planning decision was found. Accepting this queued Ticket for implementation before worktree/Pod side effects. + +--- From c0239684ed47678183c3ac41eba4fb1d94d92175 Mon Sep 17 00:00:00 2001 From: Hare Date: Sat, 20 Jun 2026 14:55:36 +0900 Subject: [PATCH 9/9] ticket: start plugin authoring cli worktree --- .yoi/tickets/00001KVHKWNQS/item.md | 2 +- .yoi/tickets/00001KVHKWNQS/thread.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.yoi/tickets/00001KVHKWNQS/item.md b/.yoi/tickets/00001KVHKWNQS/item.md index 9f5565ec..1d3c4f6b 100644 --- a/.yoi/tickets/00001KVHKWNQS/item.md +++ b/.yoi/tickets/00001KVHKWNQS/item.md @@ -2,7 +2,7 @@ title: 'Plugin: add authoring CLI new/check/pack' state: 'inprogress' created_at: '2026-06-20T04:16:14Z' -updated_at: '2026-06-20T05:54:45Z' +updated_at: '2026-06-20T05:55:31Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'cli', 'authoring', 'templates', 'package-validation', 'packaging', 'read-only-check'] diff --git a/.yoi/tickets/00001KVHKWNQS/thread.md b/.yoi/tickets/00001KVHKWNQS/thread.md index 4981db0d..2b5a1650 100644 --- a/.yoi/tickets/00001KVHKWNQS/thread.md +++ b/.yoi/tickets/00001KVHKWNQS/thread.md @@ -109,3 +109,18 @@ Next action: Ticket body/thread, relation metadata, resolved Rust PDK dependency, related closed Tickets, Orchestrator worktree state, visible Pods, existing branch/worktree, and bounded CLI/resource/package context were checked. Dependency `00001KVHKWNQA` is closed, no inprogress/capacity blocker exists, and no missing planning decision was found. Accepting this queued Ticket for implementation before worktree/Pod side effects. --- + + + +## Implementation report + +Implementation orchestration start: +- Acceptance records were committed on Orchestrator branch: `d1095f85 ticket: accept plugin authoring cli`。 +- Created implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVHKWNQS-plugin-authoring-cli` on branch `impl/00001KVHKWNQS-plugin-authoring-cli` at `d1095f85`。 +- Spawned Coder Pod `yoi-coder-00001KVHKWNQS` with write scope limited to the implementation worktree and read-only root scope only for runtime workspace identity。 +- Coder task explicitly forbids root/original workspace implementation, merge, Ticket close, and cleanup。 + +Next action: +- Wait for Coder implementation report, then inspect branch diff/validation evidence and route to Reviewer。 + +---