merge: sync orchestration before queue 00001KVHR3WRF
This commit is contained in:
commit
191e7999c0
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Plugin: add Rust PDK and embedded authoring templates for Component Model Tools'
|
title: 'Plugin: add Rust PDK and embedded authoring templates for Component Model Tools'
|
||||||
state: 'inprogress'
|
state: 'closed'
|
||||||
created_at: '2026-06-20T04:16:14Z'
|
created_at: '2026-06-20T04:16:14Z'
|
||||||
updated_at: '2026-06-20T05:21:50Z'
|
updated_at: '2026-06-20T05:53:20Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
readiness: 'implementation_ready'
|
readiness: 'implementation_ready'
|
||||||
risk_flags: ['plugin', 'pdk', 'component-model', 'authoring', 'templates', 'sdk', 'no-crates-io']
|
risk_flags: ['plugin', 'pdk', 'component-model', 'authoring', 'templates', 'sdk', 'no-crates-io']
|
||||||
|
|
|
||||||
43
.yoi/tickets/00001KVHKWNQA/resolution.md
Normal file
43
.yoi/tickets/00001KVHKWNQA/resolution.md
Normal file
|
|
@ -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`
|
||||||
|
|
@ -223,3 +223,217 @@ Reviewer validation:
|
||||||
Worktree status at review end: source tree clean; ignored artifacts under `target/` only。
|
Worktree status at review end: source tree clean; ignored artifacts under `target/` only。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T05:40:15Z -->
|
||||||
|
|
||||||
|
## 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。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: review author: yoi-reviewer-00001KVHKWNQA-r1 at: 2026-06-20T05:47:13Z status: approve -->
|
||||||
|
|
||||||
|
## 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。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T05:51:33Z -->
|
||||||
|
|
||||||
|
## 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。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T05:51:42Z from: inprogress to: done reason: merged_reviewed_validated field: state -->
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: hare at: 2026-06-20T05:53:20Z from: done to: closed reason: closed field: state -->
|
||||||
|
|
||||||
|
## State changed
|
||||||
|
|
||||||
|
Ticket を closed にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: close author: hare at: 2026-06-20T05:53:20Z status: 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`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +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"}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
---
|
---
|
||||||
title: 'Plugin: add authoring CLI new/check/pack'
|
title: 'Plugin: add authoring CLI new/check/pack'
|
||||||
state: 'queued'
|
state: 'inprogress'
|
||||||
created_at: '2026-06-20T04:16:14Z'
|
created_at: '2026-06-20T04:16:14Z'
|
||||||
updated_at: '2026-06-20T05:23:14Z'
|
updated_at: '2026-06-20T05:55:31Z'
|
||||||
assignee: null
|
assignee: null
|
||||||
readiness: 'implementation_ready'
|
readiness: 'implementation_ready'
|
||||||
risk_flags: ['plugin', 'cli', 'authoring', 'templates', 'package-validation', 'packaging', 'read-only-check']
|
risk_flags: ['plugin', 'cli', 'authoring', 'templates', 'package-validation', 'packaging', 'read-only-check']
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,114 @@ LocalTicketBackend によって作成されました。
|
||||||
Ticket を `workspace-panel` が queued にしました。
|
Ticket を `workspace-panel` が queued にしました。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T05:23:44Z -->
|
||||||
|
|
||||||
|
## 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 する。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T05:54:38Z -->
|
||||||
|
|
||||||
|
## 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 <path-or-name>`, `yoi plugin check <path-or-package>`, and `yoi plugin pack <path> [--output <file>]`。
|
||||||
|
- 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。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T05:54:45Z from: queued to: inprogress reason: orchestrator_acceptance_plugin_authoring_cli field: state -->
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T05:55:31Z -->
|
||||||
|
|
||||||
|
## 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。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
14
Cargo.lock
generated
14
Cargo.lock
generated
|
|
@ -2623,6 +2623,7 @@ dependencies = [
|
||||||
"wasmtime",
|
"wasmtime",
|
||||||
"wat",
|
"wat",
|
||||||
"workflow",
|
"workflow",
|
||||||
|
"yoi-plugin-pdk",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5388,6 +5389,7 @@ version = "0.51.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
"wit-bindgen-rust-macro",
|
"wit-bindgen-rust-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -5553,6 +5555,18 @@ dependencies = [
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoi-plugin-pdk"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"toml",
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ members = [
|
||||||
"crates/secrets",
|
"crates/secrets",
|
||||||
"crates/manifest",
|
"crates/manifest",
|
||||||
"crates/pod",
|
"crates/pod",
|
||||||
|
"crates/plugin-pdk",
|
||||||
"crates/yoi",
|
"crates/yoi",
|
||||||
"crates/pod-store",
|
"crates/pod-store",
|
||||||
"crates/protocol",
|
"crates/protocol",
|
||||||
|
|
@ -34,6 +35,7 @@ default-members = [
|
||||||
"crates/secrets",
|
"crates/secrets",
|
||||||
"crates/manifest",
|
"crates/manifest",
|
||||||
"crates/pod",
|
"crates/pod",
|
||||||
|
"crates/plugin-pdk",
|
||||||
"crates/yoi",
|
"crates/yoi",
|
||||||
"crates/pod-store",
|
"crates/pod-store",
|
||||||
"crates/protocol",
|
"crates/protocol",
|
||||||
|
|
@ -65,6 +67,7 @@ memory = { path = "crates/memory" }
|
||||||
ticket = { path = "crates/ticket" }
|
ticket = { path = "crates/ticket" }
|
||||||
project-record = { path = "crates/project-record" }
|
project-record = { path = "crates/project-record" }
|
||||||
pod = { path = "crates/pod" }
|
pod = { path = "crates/pod" }
|
||||||
|
yoi-plugin-pdk = { path = "crates/plugin-pdk" }
|
||||||
yoi = { path = "crates/yoi" }
|
yoi = { path = "crates/yoi" }
|
||||||
pod-registry = { path = "crates/pod-registry" }
|
pod-registry = { path = "crates/pod-registry" }
|
||||||
pod-store = { path = "crates/pod-store" }
|
pod-store = { path = "crates/pod-store" }
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,42 @@ const ZIP_COMPRESSION_STORED: u16 = 0;
|
||||||
const ZIP_UNIX_SYMLINK_TYPE: u32 = 0o120000;
|
const ZIP_UNIX_SYMLINK_TYPE: u32 = 0o120000;
|
||||||
const ZIP_UNIX_FILE_TYPE_MASK: u32 = 0o170000;
|
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)]
|
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct PluginConfig {
|
pub struct PluginConfig {
|
||||||
|
|
@ -1957,6 +1993,40 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use tempfile::TempDir;
|
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]
|
#[test]
|
||||||
fn discovers_valid_user_and_workspace_packages() {
|
fn discovers_valid_user_and_workspace_packages() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
|
|
|
||||||
17
crates/plugin-pdk/Cargo.toml
Normal file
17
crates/plugin-pdk/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[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]
|
||||||
|
tempfile.workspace = true
|
||||||
|
toml.workspace = true
|
||||||
476
crates/plugin-pdk/src/lib.rs
Normal file
476
crates/plugin-pdk/src/lib.rs
Normal file
|
|
@ -0,0 +1,476 @@
|
||||||
|
//! 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
|
||||||
|
//! 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(
|
||||||
|
//! ctx: yoi_plugin_pdk::ToolContext,
|
||||||
|
//! input: EchoInput,
|
||||||
|
//! ) -> Result<yoi_plugin_pdk::ToolOutput, yoi_plugin_pdk::ToolError> {
|
||||||
|
//! 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/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;
|
||||||
|
/// 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<String>) -> 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolOutput {
|
||||||
|
/// Create an ordinary Tool output.
|
||||||
|
pub fn new(summary: impl Into<String>, content: Option<String>) -> 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<String>, value: impl Serialize) -> Result<Self, ToolError> {
|
||||||
|
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<String>) -> 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<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolError {
|
||||||
|
pub fn new(code: ToolErrorCode, message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
code,
|
||||||
|
message: bounded_text(message.into(), MAX_ERROR_MESSAGE_BYTES),
|
||||||
|
details: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalid_input(message: impl Into<String>) -> Self {
|
||||||
|
Self::new(ToolErrorCode::InvalidInput, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn invalid_output(message: impl Into<String>) -> 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<String>) -> Self {
|
||||||
|
Self::new(ToolErrorCode::Denied, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unsupported_tool(tool_name: impl Into<String>) -> Self {
|
||||||
|
Self::new(
|
||||||
|
ToolErrorCode::UnsupportedTool,
|
||||||
|
format!("unsupported tool `{}`", tool_name.into()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn failed(message: impl Into<String>) -> 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<T>(input_json: &str) -> Result<T, ToolError>
|
||||||
|
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<I, F>(tool_name: &str, input_json: &str, handler: F) -> String
|
||||||
|
where
|
||||||
|
I: DeserializeOwned,
|
||||||
|
F: FnOnce(ToolContext, I) -> Result<ToolOutput, ToolError>,
|
||||||
|
{
|
||||||
|
let result = parse_json_input::<I>(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 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]
|
||||||
|
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::<EchoInput, _>("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"));
|
||||||
|
assert!(HOST_WIT.contains("%list: func"));
|
||||||
|
}
|
||||||
|
}
|
||||||
117
crates/plugin-pdk/tests/template.rs
Normal file
117
crates/plugin-pdk/tests/template.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
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!(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(),
|
||||||
|
Some("../../../../crates/plugin-pdk")
|
||||||
|
);
|
||||||
|
assert!(TEMPLATE_CARGO.contains("rev = \"<pinned-yoi-revision>\""));
|
||||||
|
|
||||||
|
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("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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn documented_sample_uses_pdk_component_path() {
|
||||||
|
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");
|
||||||
|
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}`"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
crates/plugin-pdk/tests/wit_bindgen_probe.rs
Normal file
25
crates/plugin-pdk/tests/wit_bindgen_probe.rs
Normal file
|
|
@ -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 = <Probe as Guest>::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");
|
||||||
|
}
|
||||||
|
|
@ -44,6 +44,7 @@ dotenv = "0.15.0"
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
wat = "1.241.2"
|
wat = "1.241.2"
|
||||||
|
yoi-plugin-pdk = { workspace = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -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]
|
#[tokio::test]
|
||||||
async fn malformed_input_json_fails_before_wasm_execution() {
|
async fn malformed_input_json_fails_before_wasm_execution() {
|
||||||
let (_dir, record) = resolved_record_with_wasm(input_reaches_guest_module());
|
let (_dir, record) = resolved_record_with_wasm(input_reaches_guest_module());
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ The migration should be phased:
|
||||||
2. Add manifest/schema support for `runtime.kind = "wasm-component"` without executing it during discovery.
|
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.
|
3. Add a component runtime backend and typed host import/export binding.
|
||||||
4. Port `https` and `fs` host API designs to WIT-compatible interfaces.
|
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.
|
6. Decide whether the raw ABI remains supported, becomes legacy-only, or is deprecated after examples and tests move.
|
||||||
|
|
||||||
## Runtime/backend caution
|
## 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
|
imports, or core-Wasm bytes in a component package all fail closed with bounded
|
||||||
Plugin diagnostics or ordinary Tool errors.
|
Plugin diagnostics or ordinary Tool errors.
|
||||||
|
|
||||||
See `docs/examples/plugin-component-tool/lib.rs` for a minimal
|
See `docs/examples/plugin-component-tool/lib.rs` and the embedded
|
||||||
`wit-bindgen`/SDK-style authoring sketch. Package authors should generate
|
`resources/plugin/templates/rust-component-tool/` starter for the preferred
|
||||||
bindings from `resources/plugin/wit`, build a component artifact, and set the
|
Rust PDK authoring path. `yoi-plugin-pdk` is guest-side only: it re-exports
|
||||||
component runtime metadata above.
|
`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
|
### v1 request/response shape
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ entry = "plugin.wasm"
|
||||||
abi = "yoi-plugin-wasm-1"
|
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
|
```toml
|
||||||
[runtime]
|
[runtime]
|
||||||
|
|
@ -216,6 +216,13 @@ component = "plugin.component.wasm"
|
||||||
world = "yoi:plugin/tool@1.0.0"
|
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:
|
This is separate from the legacy raw core-Wasm runtime:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,15 @@ Implemented foundation:
|
||||||
- Plugin permission grants;
|
- Plugin permission grants;
|
||||||
- raw core-Wasm Tool runtime;
|
- raw core-Wasm Tool runtime;
|
||||||
- Component Model 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;
|
- `https` and `fs` host APIs for Tool runtime;
|
||||||
- read-only `yoi plugin list/show` inspection.
|
- read-only `yoi plugin list/show` inspection.
|
||||||
|
|
||||||
Still intentionally separate/future work:
|
Still intentionally separate/future work:
|
||||||
|
|
||||||
- `yoi plugin new/check/pack` authoring commands;
|
- `yoi plugin new/check/pack` authoring commands;
|
||||||
- polished multi-language SDK/PDK crates;
|
- multi-language SDK/PDK crates;
|
||||||
- Service / Ingress surfaces;
|
- Service / Ingress surfaces;
|
||||||
- WebSocket or inbound HTTP for bidirectional bridges;
|
- WebSocket or inbound HTTP for bidirectional bridges;
|
||||||
- public registry/install/update/signature tooling.
|
- 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.
|
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
|
```text
|
||||||
docs/examples/plugin-component-tool/lib.rs
|
docs/examples/plugin-component-tool/lib.rs
|
||||||
|
|
@ -101,27 +118,46 @@ docs/examples/plugin-component-tool/lib.rs
|
||||||
The important authoring shape is:
|
The important authoring shape is:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use yoi_plugin_pdk::wit_bindgen;
|
||||||
|
use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput};
|
||||||
|
|
||||||
wit_bindgen::generate!({
|
wit_bindgen::generate!({
|
||||||
world: "tool",
|
world: "tool",
|
||||||
path: "../../../resources/plugin/wit",
|
path: "../../../resources/plugin/wit",
|
||||||
|
generate_all,
|
||||||
|
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||||
});
|
});
|
||||||
|
|
||||||
struct Plugin;
|
#[derive(Deserialize)]
|
||||||
|
struct EchoInput {
|
||||||
impl Guest for Plugin {
|
text: String,
|
||||||
fn call(tool_name: String, input_json: String) -> String {
|
|
||||||
format!(
|
|
||||||
r#"{{"summary":"component tool {tool_name}","content":"input was {input_json}"}}"#
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export!(Plugin);
|
#[derive(Serialize)]
|
||||||
|
struct EchoOutput<'a> {
|
||||||
|
tool: &'a str,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result<ToolOutput, ToolError> {
|
||||||
|
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
|
## Enabling a Plugin in a workspace
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,38 @@
|
||||||
//! 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
|
//! Build this as a `wasm32-unknown-unknown` cdylib with Component Model tooling
|
||||||
//! exports and package the adapted component as `plugin.component.wasm`.
|
//! 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};
|
||||||
|
|
||||||
wit_bindgen::generate!({
|
wit_bindgen::generate!({
|
||||||
world: "tool",
|
world: "tool",
|
||||||
path: "../../../resources/plugin/wit",
|
path: "../../../resources/plugin/wit",
|
||||||
|
generate_all,
|
||||||
|
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||||
});
|
});
|
||||||
|
|
||||||
struct Plugin;
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct EchoInput {
|
||||||
impl Guest for Plugin {
|
text: String,
|
||||||
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}"}}"#
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export!(Plugin);
|
#[derive(Debug, Serialize)]
|
||||||
|
struct EchoOutput<'a> {
|
||||||
|
tool: &'a str,
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result<ToolOutput, ToolError> {
|
||||||
|
ToolOutput::json(
|
||||||
|
format!("{} ok", ctx.tool_name()),
|
||||||
|
EchoOutput {
|
||||||
|
tool: ctx.tool_name(),
|
||||||
|
text: input.text,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
yoi_plugin_pdk::export_component_tool!(Plugin, handle_echo);
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
|
||||||
filter = sourceFilter;
|
filter = sourceFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
cargoHash = "sha256-i4U7wXPoWIHA4EAJZva2HQXNN8P5+RhGVGNBAOZVGk0=";
|
cargoHash = "sha256-ci9h0U83YQQBeT3xlsGuKULnl1Aphgpg3pR4n0se16I=";
|
||||||
|
|
||||||
depsExtraArgs = {
|
depsExtraArgs = {
|
||||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||||
|
|
|
||||||
21
resources/plugin/templates/rust-component-tool/Cargo.toml
Normal file
21
resources/plugin/templates/rust-component-tool/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "yoi-rust-component-tool-template"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
license = "MIT"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# Keep the embedded template checkable in-place without making it a member of
|
||||||
|
# Yoi's root workspace. A copied starter remains a normal standalone package.
|
||||||
|
[workspace]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
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 = "<pinned-yoi-revision>" }
|
||||||
39
resources/plugin/templates/rust-component-tool/README.md
Normal file
39
resources/plugin/templates/rust-component-tool/README.md
Normal file
|
|
@ -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 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" }
|
||||||
|
```
|
||||||
|
|
||||||
|
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 = "<pinned-yoi-revision>" }
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
20
resources/plugin/templates/rust-component-tool/plugin.toml
Normal file
20
resources/plugin/templates/rust-component-tool/plugin.toml
Normal file
|
|
@ -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
|
||||||
37
resources/plugin/templates/rust-component-tool/src/lib.rs
Normal file
37
resources/plugin/templates/rust-component-tool/src/lib.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use yoi_plugin_pdk::wit_bindgen;
|
||||||
|
use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput};
|
||||||
|
|
||||||
|
wit_bindgen::generate!({
|
||||||
|
world: "tool",
|
||||||
|
path: "../../../../resources/plugin/wit",
|
||||||
|
generate_all,
|
||||||
|
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||||
|
});
|
||||||
|
|
||||||
|
#[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<ToolOutput, ToolError> {
|
||||||
|
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);
|
||||||
|
|
@ -10,6 +10,6 @@ interface https {
|
||||||
/// Grant-bound filesystem host API. No ambient WASI filesystem is exposed.
|
/// Grant-bound filesystem host API. No ambient WASI filesystem is exposed.
|
||||||
interface fs {
|
interface fs {
|
||||||
read: func(request-json: string) -> string;
|
read: func(request-json: string) -> string;
|
||||||
list: func(request-json: string) -> string;
|
%list: func(request-json: string) -> string;
|
||||||
write: func(request-json: string) -> string;
|
write: func(request-json: string) -> string;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user