feat: provide v2 Plugin Component contracts and offline validation
Validate exact Component interfaces and static Tool/DCDL schemas without guest execution. Share canonical archive packing and independent package/artifact verification; build real pure and HTTPS PDK fixtures and retire v1/Service authoring stubs.
This commit is contained in:
+1
-2
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "yoi-rust-component-service-template"
|
||||
name = "yoi-rust-component-https-template"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
@@ -14,7 +14,6 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" }
|
||||
|
||||
# Out-of-tree Plugin packages should replace the local path with a pinned
|
||||
@@ -0,0 +1,5 @@
|
||||
# Connection-bound HTTPS Tool雛形
|
||||
|
||||
接続先とsecretをguestが選べないよう、宣言slotからcall-local resourceを取得する。副作用の観測を失敗時にも保持し、自動再送やrollback成功を装わない。HTTP error statusはresponseとして扱う。
|
||||
|
||||
このソースをComponentへbuildできることは、HTTPS Hostの実装・認可・receipt保存を提供した証拠にはならない。packageの要求grantはWorkspace bindingや実行権限を自己発行しない。
|
||||
@@ -0,0 +1,28 @@
|
||||
schema_version = 2
|
||||
id = "example.rust_component_https"
|
||||
name = "Rust HTTPS Component Tool Template"
|
||||
version = "0.1.0"
|
||||
surfaces = ["tool"]
|
||||
config_schema = "{}"
|
||||
requested_grants = [{ kind = "https", slot = "api" }]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/https-tool-plugin@2.0.0"
|
||||
required_imports = ["yoi:host/errors@2.0.0", "yoi:host/connections@2.0.0", "yoi:host/http@2.0.0"]
|
||||
|
||||
[[tools]]
|
||||
name = "example_status"
|
||||
description = "Observe the status response through an explicitly bound HTTPS slot."
|
||||
input_schema = { type = "object", properties = {}, additionalProperties = false }
|
||||
output_schema = { type = "object", properties = { status = { type = "integer" }, body_bytes = { type = "integer" } }, required = ["status", "body_bytes"], additionalProperties = false }
|
||||
# Even GET may have side effects. Host receipt/admission policy still applies.
|
||||
error_codes = ["invalid_input", "invalid_output", "serialization", "unsupported_tool", "failed"]
|
||||
external_write = true
|
||||
|
||||
[[connections]]
|
||||
name = "api"
|
||||
# Package requests HTTPS bounds; Workspace binding supplies origin and secrets.
|
||||
methods = ["GET"]
|
||||
path_prefixes = ["/status"]
|
||||
@@ -0,0 +1,61 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yoi_plugin_pdk::wit_bindgen;
|
||||
use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "https-tool-plugin",
|
||||
path: "wit",
|
||||
generate_all,
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Input {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Output {
|
||||
status: u16,
|
||||
body_bytes: usize,
|
||||
}
|
||||
|
||||
fn handle_status(ctx: ToolContext, _input: Input) -> Result<ToolOutput, ToolError> {
|
||||
use yoi::host::{connections, http};
|
||||
if ctx.tool_name() != "example_status" {
|
||||
return Err(ToolError::unsupported_tool(ctx.tool_name()));
|
||||
}
|
||||
// This resource is call-local; the slot has no URL or credential value.
|
||||
let target = connections::open("api").map_err(host_failure)?;
|
||||
let response = http::send(
|
||||
&target,
|
||||
&http::Request {
|
||||
method: http::Method::Get,
|
||||
path: "/status".into(),
|
||||
query: Vec::new(),
|
||||
headers: Vec::new(),
|
||||
body: Vec::new(),
|
||||
timeout_ms: ctx.remaining_ms().min(15_000),
|
||||
// Stable within this logical invocation. Never automatically retry a failure.
|
||||
effect_key: "status-v1".into(),
|
||||
},
|
||||
)
|
||||
.map_err(host_failure)?;
|
||||
// HTTP 4xx/5xx remain responses, not transport failures. Do not log raw bodies.
|
||||
ToolOutput::json(
|
||||
"HTTP response observed",
|
||||
Output {
|
||||
status: response.status,
|
||||
body_bytes: response.body.len(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn host_failure(error: yoi::host::errors::HostError) -> ToolError {
|
||||
// Preserve the effect observation for the caller without claiming rollback or retry safety.
|
||||
ToolError::failed(format!(
|
||||
"Host {:?}; effect {:?}; diagnostic {}",
|
||||
error.code, error.effect, error.diagnostic_id
|
||||
))
|
||||
}
|
||||
|
||||
yoi_plugin_pdk::export_component_tool!(Plugin, handle_status);
|
||||
@@ -0,0 +1,61 @@
|
||||
package yoi:host@2.0.0;
|
||||
|
||||
interface errors {
|
||||
enum code {
|
||||
invalid-input, denied, not-found, conflict, unsupported,
|
||||
quota-exceeded, deadline-exceeded, cancelled, unavailable,
|
||||
indeterminate, receipt-expired,
|
||||
}
|
||||
enum effect-observation { not-dispatched, response-observed, indeterminate }
|
||||
record host-error {
|
||||
code: code,
|
||||
message: string,
|
||||
diagnostic-id: string,
|
||||
effect: effect-observation,
|
||||
}
|
||||
}
|
||||
|
||||
interface connections {
|
||||
use errors.{host-error};
|
||||
/// Call-local resource. No serialization, persistence or cross-Worker transfer.
|
||||
resource connection;
|
||||
open: func(slot: string) -> result<connection, host-error>;
|
||||
}
|
||||
|
||||
interface http {
|
||||
use connections.{connection};
|
||||
use errors.{host-error};
|
||||
enum method { get, head, post, put, patch, delete, options }
|
||||
record pair { name: string, value: string }
|
||||
record request {
|
||||
method: method,
|
||||
path: string,
|
||||
query: list<pair>,
|
||||
headers: list<pair>,
|
||||
body: list<u8>,
|
||||
timeout-ms: u32,
|
||||
effect-key: string,
|
||||
}
|
||||
record response {
|
||||
status: u16,
|
||||
headers: list<pair>,
|
||||
body: list<u8>,
|
||||
}
|
||||
/// Host authorizes each operation and does not automatically retry or follow redirects.
|
||||
send: func(target: borrow<connection>, input: request) -> result<response, host-error>;
|
||||
}
|
||||
|
||||
interface logging {
|
||||
use errors.{host-error};
|
||||
enum level { trace, debug, info, warn, error }
|
||||
variant scalar { text(string), boolean(bool), signed(s64), unsigned(u64), number(f64) }
|
||||
record field { name: string, value: scalar }
|
||||
record entry {
|
||||
level: level,
|
||||
event-code: string,
|
||||
message: string,
|
||||
fields: list<field>,
|
||||
}
|
||||
/// Bounded diagnostic data, not model context. Never include secrets or raw payloads.
|
||||
log: func(input: entry) -> result<_, host-error>;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package yoi:plugin@2.0.0;
|
||||
|
||||
/// Only plugin-specific payloads use schema-validated JSON. Context is not authority.
|
||||
interface tool {
|
||||
record invocation {
|
||||
invocation-id: string,
|
||||
tool-name: string,
|
||||
input-json: string,
|
||||
config-json: string,
|
||||
remaining-ms: u32,
|
||||
}
|
||||
record output {
|
||||
summary: string,
|
||||
content: option<string>,
|
||||
structured-json: option<string>,
|
||||
}
|
||||
record tool-error {
|
||||
code: string,
|
||||
message: string,
|
||||
}
|
||||
invoke: func(input: invocation) -> result<output, tool-error>;
|
||||
}
|
||||
|
||||
/// Pure computation: no ambient WASI or unused Host capabilities.
|
||||
world tool-plugin {
|
||||
export tool;
|
||||
}
|
||||
|
||||
world https-tool-plugin {
|
||||
import yoi:host/errors@2.0.0;
|
||||
import yoi:host/connections@2.0.0;
|
||||
import yoi:host/http@2.0.0;
|
||||
export tool;
|
||||
}
|
||||
|
||||
/// Logging is an explicit capability, never an ambient console.
|
||||
world logged-tool-plugin {
|
||||
import yoi:host/errors@2.0.0;
|
||||
import yoi:host/logging@2.0.0;
|
||||
export tool;
|
||||
}
|
||||
|
||||
world https-logged-tool-plugin {
|
||||
import yoi:host/errors@2.0.0;
|
||||
import yoi:host/connections@2.0.0;
|
||||
import yoi:host/http@2.0.0;
|
||||
import yoi:host/logging@2.0.0;
|
||||
export tool;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Rust Component Instance Plugin雛形
|
||||
|
||||
提案段階の`yoi:plugin/instance@1.0.0`を対象にしたオフラインpackageの雛形である。YoiはPluginのインストールやWorker内での実行を提供していない。
|
||||
|
||||
Tool・Serviceの要求と応答は将来の実行境界を検討するための例である。host APIやgrantのmetadataは現在の製品では権限を付与せず、オフライン検査の成功も実行の許可を意味しない。
|
||||
@@ -1,3 +0,0 @@
|
||||
# Build with:
|
||||
# cargo component build --release
|
||||
# cp target/wasm32-wasip1/release/example_yoi_instance_plugin.wasm plugin.component.wasm
|
||||
@@ -1,42 +0,0 @@
|
||||
schema_version = 1
|
||||
id = "example.rust_service_plugin"
|
||||
name = "Rust Service Plugin Template"
|
||||
version = "0.1.0"
|
||||
description = "Example Component Model Plugin with Tool and Service ingress handlers."
|
||||
surfaces = ["tool", "service", "ingress"]
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "tool" },
|
||||
{ kind = "tool", name = "example_echo" },
|
||||
{ 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"
|
||||
world = "yoi:plugin/instance@1.0.0"
|
||||
component = "plugin.component.wasm"
|
||||
|
||||
[[tools]]
|
||||
name = "example_echo"
|
||||
description = "Echo input text through the shared Plugin instance."
|
||||
input_schema = { type = "object" }
|
||||
|
||||
[[services]]
|
||||
name = "example_service"
|
||||
description = "Host-managed service instance for bounded ingress events."
|
||||
lifecycle = "host-managed"
|
||||
|
||||
[[ingresses]]
|
||||
name = "example_ws"
|
||||
description = "Handles host-owned WebSocket text events and returns websocket_send output commands."
|
||||
event_kinds = ["websocket_text"]
|
||||
sources = ["websocket:wss://example.com/socket"]
|
||||
input_schema = { type = "object" }
|
||||
|
||||
[[websocket]]
|
||||
scheme = "wss"
|
||||
host = "example.com"
|
||||
path_prefixes = ["/socket"]
|
||||
@@ -1,85 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use yoi_plugin_pdk::wit_bindgen;
|
||||
use yoi_plugin_pdk::{
|
||||
export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ServiceOutput, ToolError,
|
||||
ToolOutput,
|
||||
};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "instance",
|
||||
path: "../../../../resources/plugin/wit",
|
||||
generate_all,
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
#[derive(Default)]
|
||||
struct ExamplePlugin {
|
||||
count: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EchoInput {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EchoOutput {
|
||||
text: String,
|
||||
count: u64,
|
||||
}
|
||||
|
||||
impl Plugin for ExamplePlugin {
|
||||
fn start(config: Value) -> Result<Self, ToolError> {
|
||||
Ok(Self {
|
||||
count: config.get("start_count").and_then(Value::as_u64).unwrap_or(0),
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_tool(&mut self, name: &str, input: Value) -> Result<ToolOutput, ToolError> {
|
||||
if name != "example_echo" {
|
||||
return Err(ToolError::invalid_input(format!("unknown tool: {name}")));
|
||||
}
|
||||
let input: EchoInput =
|
||||
serde_json::from_value(input).map_err(|err| ToolError::invalid_input(err.to_string()))?;
|
||||
self.count += 1;
|
||||
ToolOutput::json(
|
||||
format!("echoed {} bytes", input.text.len()),
|
||||
EchoOutput {
|
||||
text: input.text,
|
||||
count: self.count,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_ingress(
|
||||
&mut self,
|
||||
name: &str,
|
||||
event: PluginIngressEvent,
|
||||
) -> Result<ServiceOutput, ToolError> {
|
||||
if name != "example_ws" {
|
||||
return Ok(ServiceOutput::accepted(json!({ "ignored": name }))?);
|
||||
}
|
||||
|
||||
let Some(text) = event.websocket_text() else {
|
||||
return Ok(ServiceOutput::accepted(json!({
|
||||
"accepted": true,
|
||||
"kind": event.kind,
|
||||
}))?);
|
||||
};
|
||||
|
||||
self.count += 1;
|
||||
ServiceOutput::websocket_send(
|
||||
&event,
|
||||
format!("example-reply-{}", self.count),
|
||||
event.source.strip_prefix("websocket:").unwrap_or(&event.source),
|
||||
format!("echo({}): {text}", self.count),
|
||||
)
|
||||
}
|
||||
|
||||
fn status(&self) -> Result<PluginStatus, ToolError> {
|
||||
Ok(PluginStatus::ready(json!({ "count": self.count })))
|
||||
}
|
||||
}
|
||||
|
||||
export_plugin_instance!(ExamplePluginComponent, ExamplePlugin);
|
||||
@@ -1,5 +1,5 @@
|
||||
# Rust Component Plugin雛形
|
||||
# Rust Tool Component雛形
|
||||
|
||||
提案段階の`yoi:plugin/tool@1.0.0`を対象にしたオフラインpackageの雛形である。YoiはPluginのインストールやWorker内での実行を提供していない。
|
||||
guest境界は生成されたWIT bindingsに任せ、手書きpointer ABIとWASI依存を持たせない。呼出し間の永続状態やServiceを成功stubとして公開しない。
|
||||
|
||||
guest側の型付き入出力とエラー表現はRust PDKが担当し、ファイル・ネットワーク・環境への権限は付与しない。`plugin.component.wasm`はpackage検査用のテキストplaceholderであり、実行可能なcomponentの証拠にはならない。
|
||||
`plugin.component.wasm`はソースからbuildして配置する成果物であり、placeholderをpackageへ混入させない。PDKからのbuild成功と、Hostの認可・実行経路の完成は別の判断である。
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
placeholder component artifact for authoring-template checks; replace with a built wasm component before enabling.
|
||||
@@ -1,20 +1,21 @@
|
||||
schema_version = 1
|
||||
schema_version = 2
|
||||
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" },
|
||||
]
|
||||
config_schema = "{}"
|
||||
requested_grants = []
|
||||
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
world = "yoi:plugin/tool-plugin@2.0.0"
|
||||
required_imports = []
|
||||
|
||||
[[tools]]
|
||||
name = "example_echo"
|
||||
description = "Echo input text using the Rust PDK."
|
||||
input_schema = { type = "object", properties = { text = { type = "string" } }, required = ["text"], additionalProperties = false }
|
||||
output_schema = { type = "object", properties = { tool = { type = "string" }, text = { type = "string" } }, required = ["tool", "text"], additionalProperties = false }
|
||||
error_codes = ["invalid_input", "invalid_output", "serialization", "unsupported_tool", "failed"]
|
||||
external_write = false
|
||||
|
||||
@@ -3,13 +3,14 @@ use yoi_plugin_pdk::wit_bindgen;
|
||||
use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "tool",
|
||||
path: "../../../../resources/plugin/wit",
|
||||
world: "tool-plugin",
|
||||
path: "wit",
|
||||
generate_all,
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct EchoInput {
|
||||
text: String,
|
||||
}
|
||||
@@ -21,6 +22,9 @@ struct EchoOutput<'a> {
|
||||
}
|
||||
|
||||
fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result<ToolOutput, ToolError> {
|
||||
if ctx.tool_name() != "example_echo" {
|
||||
return Err(ToolError::unsupported_tool(ctx.tool_name()));
|
||||
}
|
||||
if input.text.trim().is_empty() {
|
||||
return Err(ToolError::invalid_input("`text` must not be empty"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package yoi:host@2.0.0;
|
||||
|
||||
interface errors {
|
||||
enum code {
|
||||
invalid-input, denied, not-found, conflict, unsupported,
|
||||
quota-exceeded, deadline-exceeded, cancelled, unavailable,
|
||||
indeterminate, receipt-expired,
|
||||
}
|
||||
enum effect-observation { not-dispatched, response-observed, indeterminate }
|
||||
record host-error {
|
||||
code: code,
|
||||
message: string,
|
||||
diagnostic-id: string,
|
||||
effect: effect-observation,
|
||||
}
|
||||
}
|
||||
|
||||
interface connections {
|
||||
use errors.{host-error};
|
||||
/// Call-local resource. No serialization, persistence or cross-Worker transfer.
|
||||
resource connection;
|
||||
open: func(slot: string) -> result<connection, host-error>;
|
||||
}
|
||||
|
||||
interface http {
|
||||
use connections.{connection};
|
||||
use errors.{host-error};
|
||||
enum method { get, head, post, put, patch, delete, options }
|
||||
record pair { name: string, value: string }
|
||||
record request {
|
||||
method: method,
|
||||
path: string,
|
||||
query: list<pair>,
|
||||
headers: list<pair>,
|
||||
body: list<u8>,
|
||||
timeout-ms: u32,
|
||||
effect-key: string,
|
||||
}
|
||||
record response {
|
||||
status: u16,
|
||||
headers: list<pair>,
|
||||
body: list<u8>,
|
||||
}
|
||||
/// Host authorizes each operation and does not automatically retry or follow redirects.
|
||||
send: func(target: borrow<connection>, input: request) -> result<response, host-error>;
|
||||
}
|
||||
|
||||
interface logging {
|
||||
use errors.{host-error};
|
||||
enum level { trace, debug, info, warn, error }
|
||||
variant scalar { text(string), boolean(bool), signed(s64), unsigned(u64), number(f64) }
|
||||
record field { name: string, value: scalar }
|
||||
record entry {
|
||||
level: level,
|
||||
event-code: string,
|
||||
message: string,
|
||||
fields: list<field>,
|
||||
}
|
||||
/// Bounded diagnostic data, not model context. Never include secrets or raw payloads.
|
||||
log: func(input: entry) -> result<_, host-error>;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package yoi:plugin@2.0.0;
|
||||
|
||||
/// Only plugin-specific payloads use schema-validated JSON. Context is not authority.
|
||||
interface tool {
|
||||
record invocation {
|
||||
invocation-id: string,
|
||||
tool-name: string,
|
||||
input-json: string,
|
||||
config-json: string,
|
||||
remaining-ms: u32,
|
||||
}
|
||||
record output {
|
||||
summary: string,
|
||||
content: option<string>,
|
||||
structured-json: option<string>,
|
||||
}
|
||||
record tool-error {
|
||||
code: string,
|
||||
message: string,
|
||||
}
|
||||
invoke: func(input: invocation) -> result<output, tool-error>;
|
||||
}
|
||||
|
||||
/// Pure computation: no ambient WASI or unused Host capabilities.
|
||||
world tool-plugin {
|
||||
export tool;
|
||||
}
|
||||
|
||||
world https-tool-plugin {
|
||||
import yoi:host/errors@2.0.0;
|
||||
import yoi:host/connections@2.0.0;
|
||||
import yoi:host/http@2.0.0;
|
||||
export tool;
|
||||
}
|
||||
|
||||
/// Logging is an explicit capability, never an ambient console.
|
||||
world logged-tool-plugin {
|
||||
import yoi:host/errors@2.0.0;
|
||||
import yoi:host/logging@2.0.0;
|
||||
export tool;
|
||||
}
|
||||
|
||||
world https-logged-tool-plugin {
|
||||
import yoi:host/errors@2.0.0;
|
||||
import yoi:host/connections@2.0.0;
|
||||
import yoi:host/http@2.0.0;
|
||||
import yoi:host/logging@2.0.0;
|
||||
export tool;
|
||||
}
|
||||
Reference in New Issue
Block a user