merge: plugin runtime implementation chain

This commit is contained in:
Keisuke Hirata 2026-06-25 18:18:14 +09:00
commit a525ba4d01
No known key found for this signature in database
36 changed files with 4793 additions and 1831 deletions

View File

@ -0,0 +1 @@
{"id":"orch-plan-20260624-201247-1","ticket_id":"00001KVXK0WD3","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WD3` は dependency chain の先頭で implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` と branch `work/00001KVXK0WD3-remove-legacy-wasm-runtime` で、Pod runtime 内の `LegacyToolAdapter` / raw-WASM active execution path を削除し、Component Model path を唯一の active execution path にする。Manifest/CLI diagnostics rejection は後続 `00001KVXK0WDH` の範囲に残す。","branch":"work/00001KVXK0WD3-remove-legacy-wasm-runtime","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: remove active legacy raw-WASM runtime path in dedicated child worktree. Reviewer: read-only review focusing on preserving component Tool execution, grants, discovery/enablement/ToolRegistry, and not implementing manifest/CLI rejection slice."},"author":"yoi-orchestrator","at":"2026-06-24T20:12:47Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'Remove legacy raw WASM Plugin runtime' title: 'Remove legacy raw WASM Plugin runtime'
state: 'queued' state: 'done'
created_at: '2026-06-24T19:51:56Z' created_at: '2026-06-24T19:51:56Z'
updated_at: '2026-06-24T20:11:56Z' updated_at: '2026-06-24T20:51:02Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-24T20:11:56Z' queued_at: '2026-06-24T20:11:56Z'

View File

@ -30,4 +30,244 @@ Marked ready by `yoi ticket state`.
Ticket を `workspace-panel` が queued にしました。 Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T20:13:18Z -->
## Decision
Routing decision: implementation_ready
Reason:
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
- Ticket body は legacy raw `wasm` runtime redesign のうち最初の concrete slice として、Pod runtime 内の `PluginInstanceRuntime::LegacyToolAdapter` 相当 active execution path を削除し、`wasm-component` / Component Model path を唯一の active runtime path にする範囲に限定している。
- Manifest / CLI diagnostics rejection、Service / Ingress event queue、WebSocket driver、WIT / PDK / templates は明示的に non-goal / 後続 Ticket に分割されている。
- `TicketRelationQuery` は 1 件で、この Ticket は後続 Ticket から参照される dependency chain の先頭であり、blocking outgoing dependency はない。
- `TicketOrchestrationPlanQuery` は routing 前 plan 0 件。accepted plan `orch-plan-20260624-201247-1` を記録済み。
- bounded context check で `crates/pod/src/feature/plugin.rs``LegacyToolAdapter` / raw `wasm` active path、`crates/manifest/src/plugin.rs` と `crates/yoi/src/plugin_cli.rs` の legacy manifest/diagnostic fixtures、docs の transitional runtime 記述を確認した。Ticket は manifest rejection を後続 Ticket に分けているため、残る不確実性は local implementation / test update に収まる。
Evidence checked:
- Ticket body / thread: `item.md`, `thread.md`。thread は create、planning->ready、ready->queued のみで未解決 blocker は記録されていない。
- Relations / orchestration plan: relation 1 件(後続 Ticket がこの Ticket に依存する view、routing 前 plan 0 件。
- Code/docs context: `crates/pod/src/feature/plugin.rs``LegacyToolAdapter` / raw WASM handling、`crates/manifest/src/plugin.rs` / `crates/yoi/src/plugin_cli.rs` の legacy runtime constants/tests、Plugin docs の current transition notes。
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
- Queue context: 他の queued Plugin follow-up Tickets は dependency chain 上でこの Ticket の完了後に進める。
IntentPacket:
Intent:
- Pod Plugin runtime の active execution path から legacy raw core-WASM Tool adapter を削除し、Plugin Tool execution を Component Model runtime path に一本化する。
Binding decisions / invariants:
- この Ticket では active runtime execution path を整理する。Manifest / CLI の外向き rejection UX は後続 `00001KVXK0WDH` の範囲として残す。
- Component Model Plugin Tool execution、Host API grant boundary、package discovery、enablement、digest pinning、ToolRegistry registration は維持する。
- raw core-WASM path を compatibility fallback として active execution に残さない。
- Service / Ingress event runtime、WebSocket driver、WIT/PDK/templates service event update は実装しない。
- broad Plugin redesign や public registry/install/update policy に範囲を広げない。
Requirements / acceptance criteria:
- `PluginInstanceRuntime::LegacyToolAdapter` または同等の legacy raw-WASM adapter が active runtime から削除される。
- raw `wasm` runtime を通じた Plugin Tool execution path が使われない。
- Component Model Plugin Tool execution tests が通る。
- Legacy runtime 削除に伴う dead code / dead tests / obsolete fixtures が整理される。
- Discovery / enablement / Tool registration / grant validation の既存挙動が壊れない。
Implementation latitude:
- `PluginInstanceRuntime` enum の shape を単一 component path に畳むか、名前を残して variant を整理するかは既存 code style に合わせてよい。
- Manifest layer の legacy constants/tests は、active runtime removal に必要な最小限だけ調整してよい。ただし user-facing rejection/diagnostic completion は後続 Ticket に残す。
- Docs の compatibility/transitional wording は、active runtime removal と矛盾する部分を最小限更新してよい。
Escalate if:
- Component Model path だけでは既存 Tool execution / grant validation / ToolRegistry registration を維持できない。
- Manifest/CLI outward rejection を同時実装しないと build/test が成立しない。
- Wasmtime/component runtime limits or sandbox boundaries を再設計する必要がある。
- Service/Ingress/WebSocket/WIT/PDK の設計変更が必要になる。
Validation:
- `cargo test -p pod`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
- 必要に応じて focused Plugin tests / `cargo test -p manifest` / `cargo test -p yoi plugin_cli`
Current code/docs map:
- Primary: `crates/pod/src/feature/plugin.rs`
- Secondary: `crates/manifest/src/plugin.rs`, `crates/yoi/src/plugin_cli.rs`, Plugin docs/templates only as needed for active runtime removal consistency。
- Avoid: full service runtime, WebSocket driver, WIT/PDK event model, remote plugin registry, root/original workspace operations。
Critical risks / reviewer focus:
- legacy raw-WASM execution path accidentally remains as fallback。
- Component Model Tool execution regression。
- host API grant / digest pinning / enablement / ToolRegistry registration regression。
- scope creep into manifest rejection or Service/Ingress runtime beyond this slice。
Next action:
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T20:14:08Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
## State changed
Orchestrator acceptance: queued -> inprogress
- 直前確認で `TicketShow` は state `queued`、blocking outgoing relation は 0 件、accepted plan `orch-plan-20260624-201247-1` を確認した。
- 後続 queued Tickets (`00001KVXK0WDH`, `00001KVXK0WDQ`, `00001KVXK0WDX`, `00001KVXK0WE4`, `00001KVXK0WEA`) は dependency chain 上この Ticket またはその後続 prerequisites に blocked として orchestration plan に記録済み。
- routing decision と IntentPacket は Ticket thread に記録済み。
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` を作成し、multi-agent-workflow に接続する。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T20:14:56Z -->
## Implementation report
Implementation orchestration update:
- queue review で dependency chain を確認し、`00001KVXK0WD3` のみを先頭 prerequisite として受理した。
- 後続 queued Tickets (`00001KVXK0WDH`, `00001KVXK0WDQ`, `00001KVXK0WDX`, `00001KVXK0WE4`, `00001KVXK0WEA`) は relation/orchestration plan 上の dependency blocker として待機理由を記録した。
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` と branch `work/00001KVXK0WD3-remove-legacy-wasm-runtime` を作成した。
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
- Coder Pod `yoi-coder-00001KVXK0WD3-legacy-wasm-removal` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
---
<!-- event: implementation_report author: yoi-coder-00001KVXK0WD3-legacy-wasm-removal at: 2026-06-24T20:43:50Z -->
## Implementation report
Implementation report:
実装概要:
- `PluginInstanceRuntime::LegacyToolAdapter` 相当の raw core-WASM fallback 実行経路を削除した。
- `wasm-component` の Tool world は `ComponentToolAdapter` として明示的に扱い、ToolRegistry 経由の実行は `run_plugin_component_tool` のみを使うよう変更した。
- raw `wasm` runtime metadata は static inspection / instance startup の両方で非対応として扱い、実行 fallback しないようにした。
- Component Model Plugin Tool / Component Instance 実行経路、host API grant boundary、ToolRegistry 登録経路は維持した。
- raw WASM 実行用の `wasmi` 依存・テスト・古い helper を削除した。
- `package.nix``cargoHash` を更新した。
- Component Model design doc の現状記述を active runtime に合わせて更新した。
変更ファイル:
- `crates/pod/src/feature/plugin.rs`
- `crates/pod/Cargo.toml`
- `Cargo.lock`
- `package.nix`
- `docs/design/plugin-component-model.md`
Validation:
- `cargo test -p pod feature::plugin --lib`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed in unrelated prompt snapshot tests:
- `workflow::tests::workflow_usage_guidance_contract_snapshot_matches_prompt_resource`
- `tests::companion_prompt_matches_resource_snapshot`
Commit:
- `741d71327aec213e564d47a4b4c37141c727803c refactor: remove legacy wasm plugin runtime`
Residual risks / notes:
- Full `cargo test -p pod` failure appears unrelated to this Plugin runtime change and was not fixed here.
- Follow-up Ticket `00001KVXK0WDH` remains responsible for manifest / CLI legacy runtime rejection UX. This implementation only removes active runtime fallback and minimal static inspection consistency.
---
<!-- event: review author: yoi-reviewer-00001KVXK0WD3-legacy-wasm-removal at: 2026-06-24T20:48:55Z status: approve -->
## Review: approve
Review result: approve
確認範囲:
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WD3-remove-legacy-wasm-runtime` のみで read-only inspection。
- Ticket / IntentPacket / `1b5a39db..HEAD` の diff を確認。
- reviewer では cargo/nix は再実行していない。`git diff --check 1b5a39db..HEAD` は問題なし。
承認根拠:
- Ticket / IntentPacket の核心である「Pod Plugin runtime の active execution path から legacy raw core-WASM Tool adapter を削除し、Component Model path に一本化」が満たされている。
- `crates/pod/src/feature/plugin.rs` から `LegacyToolAdapter` / `PluginWasmTool` / `run_plugin_wasm_tool` / `wasmi` host adapter 系が削除され、`crates/pod/Cargo.toml` からも `wasmi` dependency が削除されている。
- raw `wasm` runtime は static inspection で `eligible: false` かつ active execution path ではない diagnostic になり、`PluginInstanceRuntime::new` でも `PLUGIN_RUNTIME_WASM_KIND` が fail-closed するため、fallback 実行は残っていない。
- Component Model Tool path は `ComponentToolAdapter -> run_plugin_component_tool -> read_resolved_plugin_runtime_component -> wasmtime::component` に整理され、ToolRegistry 登録も既存の `ToolContribution` / `plugin_instance_tool_definition` 経由を維持している。
- Host API grant / digest binding / tool permission validation は `authorize_plugin_tool`、`authorize_plugin_host_api`、`validate_component_imports` 側で維持され、component import permission/grant check tests も残っている。
- Manifest / CLI rejection UX には踏み込んでおらず、raw `wasm` constants / manifest parsing が残っているのは後続 Ticket `00001KVXK0WDH` の範囲として許容できる。
- Service / Ingress / WebSocket / WIT / PDK runtime への不要な scope creep は見当たらない。
Non-blocking notes:
- 報告された `cargo test -p pod` 全体失敗は prompt snapshot 2 件で、今回 diff の変更対象外に見えるため blocker とは判断しない。ただし完了処理時には既知 unrelated failure として扱う根拠を残すか、別対応後に再実行するのが安全。
- `docs/development/plugin-development.md` には raw core-Wasm compatibility がまだ有効に読める文言が残っている。今回 Ticket の主目的は active runtime removal で、外向き rejection/docs 全面更新は後続範囲なので blocker ではないが、後続 Ticket で整理した方がよい。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T20:50:37Z -->
## Implementation report
Integration outcome:
- Coder `yoi-coder-00001KVXK0WD3-legacy-wasm-removal` が implementation branch `work/00001KVXK0WD3-remove-legacy-wasm-runtime` に実装を commit した。
- implementation commit: `741d7132 refactor: remove legacy wasm plugin runtime`
- Reviewer `yoi-reviewer-00001KVXK0WD3-legacy-wasm-removal` は read-only review で `approve`。legacy raw-WASM fallback execution path が削除され、Component Model Tool path、host API grant、digest binding、ToolRegistry registration が維持され、manifest/CLI rejection や Service/Ingress/WebSocket/WIT/PDK へ scope creep していないことを確認した。
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WD3-remove-legacy-wasm-runtime` を merge 済み。
- merge commit: `29953111 merge: 00001KVXK0WD3 legacy wasm removal`
Implemented behavior:
- `crates/pod/src/feature/plugin.rs` から `LegacyToolAdapter` / raw core-WASM Tool execution helper / `wasmi` host adapter 系を削除。
- raw `wasm` runtime は static inspection と instance startup で fail-closed / non-executable diagnostic になり、active fallback execution path は残していない。
- Component Model Tool runtime path は `ComponentToolAdapter` / `run_plugin_component_tool` / `wasmtime::component` に整理。
- `crates/pod/Cargo.toml` から `wasmi` dependency を削除し、`Cargo.lock` と `package.nix` cargoHash を更新。
- `docs/design/plugin-component-model.md` の transitional runtime wording を active runtime removal に合わせて最小更新。
Validation in Orchestrator worktree:
- `cargo test -p pod feature::plugin --lib`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed in two prompt guidance snapshot assertions that are outside this Plugin diff:
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
- Plugin focused tests within the same full run passed, including `core_wasm_is_not_silently_reinterpreted_as_component`, component execution, component grant/import, lifecycle, memory/table/output cap tests.
Notes:
- Full `cargo test -p pod` failure is recorded as an existing/unrelated prompt-resource assertion issue and not treated as this Ticket blocker because changed files do not touch prompt resources or those test modules, focused Plugin tests pass, reviewer approved, and `nix build .#yoi --no-link` succeeds.
- Follow-up `00001KVXK0WDH` remains responsible for external manifest/CLI rejection diagnostics and broader docs cleanup such as `docs/development/plugin-development.md` compatibility wording.
Next action:
- Mark Ticket done after this integration/validation evidence.
- Then stop related child Pods and remove only the child implementation worktree/branch.
- Dependency-unblocked queued Ticket `00001KVXK0WDH` can be re-routed next.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T20:50:45Z from: inprogress to: done reason: merged_validated_with_unrelated_full_suite_caveat field: state -->
## State changed
Implementation is merged into the orchestration branch and validated with one unrelated existing full-suite caveat.
Evidence:
- merge commit: `29953111 merge: 00001KVXK0WD3 legacy wasm removal`
- reviewer result: approve
- validation in `/home/hare/Projects/yoi/.worktree/orchestration`:
- `cargo test -p pod feature::plugin --lib`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed only in prompt guidance snapshot assertions outside this Plugin diff; focused Plugin tests in the same run passed.
Closure is not performed here; this state records implementation completion after merge/review/focused validation.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T20:51:02Z -->
## Implementation report
Correction:
- 直前の integration outcome / state-change body に記載した merge commit `29953111` は誤記。
- 正しい merge commit は `bedbb670 merge: 00001KVXK0WD3 legacy wasm removal`
- 実装 commit `741d7132`、review approve、validation results、Ticket done 判断には変更なし。
--- ---

View File

@ -0,0 +1,2 @@
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WDH","kind":"blocked_by","related_ticket":"00001KVXK0WD3","note":"Queue review: `00001KVXK0WDH` は manifest/CLI rejection slice だが、active legacy runtime path removal `00001KVXK0WD3` に depends_on している。`00001KVXK0WD3` を先に受理し、この Ticket は dependency completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
{"id":"orch-plan-20260624-205226-2","ticket_id":"00001KVXK0WDH","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WDH` は prerequisite `00001KVXK0WD3` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy` と branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy` で、legacy raw `wasm` / `yoi-plugin-wasm-1` manifest/CLI diagnostics を拒否・整理する。Service/Ingress/WebSocket/WIT/PDK runtime work は後続 Tickets に残す。","branch":"work/00001KVXK0WDH-plugin-manifest-reject-legacy","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: manifest/CLI/docs diagnostics implementation in dedicated child worktree. Reviewer: read-only review focusing on clear legacy rejection, component package non-regression, no runtime-scope creep."},"author":"yoi-orchestrator","at":"2026-06-24T20:52:26Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'Reject legacy Plugin runtime in manifest and CLI diagnostics' title: 'Reject legacy Plugin runtime in manifest and CLI diagnostics'
state: 'queued' state: 'done'
created_at: '2026-06-24T19:51:56Z' created_at: '2026-06-24T19:51:56Z'
updated_at: '2026-06-24T20:11:58Z' updated_at: '2026-06-24T21:20:45Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-24T20:11:58Z' queued_at: '2026-06-24T20:11:58Z'

View File

@ -30,4 +30,277 @@ Marked ready by `yoi ticket state`.
Ticket を `workspace-panel` が queued にしました。 Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T20:52:56Z -->
## Decision
Routing decision: implementation_ready
Reason:
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
- この Ticket は legacy Plugin runtime redesign chain の 2 番目の concrete slice で、manifest validation / CLI diagnostics / docs cleanup に範囲が限定されている。
- outgoing `depends_on``00001KVXK0WD3` だが、`00001KVXK0WD3` は done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
- incoming dependent `00001KVXK0WDQ` はこの Ticket 完了後に進めるべき後続であり、この Ticket の acceptance blocker ではない。
- `TicketOrchestrationPlanQuery` には以前の `blocked_by 00001KVXK0WD3` があるが、prerequisite 完了により解消済みとして扱い、accepted plan `orch-plan-20260624-205226-2` を記録した。
- bounded context check で current orchestration branch の `crates/manifest/src/plugin.rs`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`, `docs/design/plugin-component-model.md`, `docs/design/plugin-packages.md` 周辺に raw `wasm` / `yoi-plugin-wasm-1` / transitional wording が残っていることを確認した。Ticket の scope はこれらの outward schema/diagnostic/docs 整理として十分に具体的。
Evidence checked:
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WD3` は done。incoming dependent `00001KVXK0WDQ` は後続。
- Related Ticket: `00001KVXK0WD3` は done。active legacy runtime fallback removal completed with corrected merge commit `bedbb670`
- Code/docs context: `crates/manifest/src/plugin.rs`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`, `docs/design/plugin-component-model.md`, `docs/design/plugin-packages.md`
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
IntentPacket:
Intent:
- Legacy raw `wasm` / `yoi-plugin-wasm-1` Plugin runtime を external manifest/schema/CLI/docs surface でも rejected/retired として扱い、Component Model `wasm-component` を正の public/recommended runtime に一本化する。
Binding decisions / invariants:
- `00001KVXK0WD3` の active runtime removal を前提にする。raw-WASM execution fallback を戻さない。
- `runtime.kind = "wasm-component"` が positive accepted runtime kind。
- legacy raw `wasm` / `yoi-plugin-wasm-1` は new/current Plugin package として validation/inspection/check/list/show 上曖昧に active 表示しない。
- Component Model package の `check/list/show`、package discovery、digest, grants, Tool schema diagnostics は regress させない。
- Service / Ingress runtime、WebSocket driver、WIT/PDK/templates service event update は実装しない。
Requirements / acceptance criteria:
- Legacy raw `wasm` runtime manifest が validation/check で明確に rejected/unsupported になる。
- `yoi plugin check` は legacy package を invalid/unsupported として bounded diagnostic し、exit behavior が current check semantics と整合する。
- `yoi plugin list/show` は legacy package を有効/active Plugin として曖昧に表示しない。
- Component Model packages の check/list/show は通る。
- docs/design / docs/development / templates/examples から raw core-Wasm compatibility bridge 前提を削除または撤回済み方針に更新する。
- Manifest schema / static inspection / error message tests を更新する。
Implementation latitude:
- `PLUGIN_RUNTIME_WASM_KIND` / `PLUGIN_RUNTIME_WASM_ABI` constants を削除するか、legacy rejection diagnostic 用に internal-only に残すかは code style と tests に合わせてよい。
- Legacy fixture は削除または rejected fixture に変換してよい。
- CLI human/json diagnostic wording は bounded and clear であれば具体文言は coder が選んでよい。
Escalate if:
- Manifest parser cannot reject legacy runtime without breaking component packages。
- Plugin list/show architecture cannot represent rejected package diagnostics without broader API redesign。
- Implementing rejection requires Service/Ingress/WebSocket/WIT/PDK changes。
- Existing closed Ticket constraints force raw core-Wasm compatibility to remain externally supported。
Validation:
- `cargo test -p manifest`
- `cargo test -p yoi`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
- Add/run focused plugin CLI/check/list/show tests as appropriate。
Current code/docs map:
- Primary: `crates/manifest/src/plugin.rs`, `crates/yoi/src/plugin_cli.rs`
- Secondary: `docs/development/plugin-development.md`, `docs/design/plugin-component-model.md`, `docs/design/plugin-packages.md`, plugin templates/examples only as needed。
- Avoid: `crates/pod` active runtime implementation unless minimal diagnostic alignment is needed; Service/Ingress/WebSocket/WIT/PDK event model。
Critical risks / reviewer focus:
- legacy package still appears active/eligible in CLI inspection。
- component package check/list/show regression。
- raw-WASM execution fallback being reintroduced。
- diagnostic wording that implies compatibility support remains。
- scope creep into later service runtime Tickets。
Next action:
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T20:53:45Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
## State changed
Orchestrator acceptance: queued -> inprogress
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
- outgoing dependency `00001KVXK0WD3` は done / merged / reviewed / validated 済み。
- accepted plan `orch-plan-20260624-205226-2` を確認した。
- routing decision と IntentPacket は Ticket thread に記録済み。
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy` を作成し、multi-agent-workflow に接続する。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T20:54:43Z -->
## Implementation report
Implementation orchestration update:
- prerequisite `00001KVXK0WD3` が done になったため、`00001KVXK0WDH` を再 routing して受理した。
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy` と branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy` を作成した。
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
- Coder Pod `yoi-coder-00001KVXK0WDH-plugin-manifest-reject` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
---
<!-- event: implementation_report author: yoi-coder-00001KVXK0WDH-plugin-manifest-reject at: 2026-06-24T21:11:58Z -->
## Implementation report
Implementation report:
対象 Ticket:
- `00001KVXK0WDH` — Reject legacy Plugin runtime in manifest and CLI diagnostics
作業ブランチ / Worktree:
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy`
- Branch: `work/00001KVXK0WDH-plugin-manifest-reject-legacy`
変更内容:
- `crates/manifest/src/plugin.rs`
- legacy raw `wasm` / `yoi-plugin-wasm-1` runtime を manifest validation で reject。
- `wasm-component` を現行 public runtime としてドキュメントコメントを更新。
- raw-WASM runtime module 読み込み helper を削除。
- legacy manifest rejection のテストを追加。
- `crates/yoi/src/plugin_cli.rs`
- CLI テスト fixture を `wasm-component` package へ移行。
- `yoi plugin check` が legacy raw-WASM package を `rejected` として返すテストを追加。
- `list/show` inspection 上で legacy package が active/eligible 表示されないことをテスト。
- `crates/pod/src/feature/plugin.rs`
- public manifest constant 依存を外し、legacy runtime 名は内部診断用 const に限定。
- 既存の runtime fallback rejection 経路を維持。
- `docs/development/plugin-development.md`
- raw-WASM runtime を「互換/推奨」ではなく retired/rejected として記述更新。
- `wasm-component` を supported/public runtime として明示。
- `docs/design/plugin-component-model.md`
- Component Model runtime を現行 supported runtime として更新。
- raw-WASM は bounded diagnostic 用の rejected metadata として整理。
- `docs/design/plugin-packages.md`
- package runtime 記述を `wasm-component` 中心に更新。
- legacy raw-WASM package は active/eligible ではなく rejected として表示されることを明記。
Validation:
- `cargo fmt --check`: success
- `cargo test -p manifest`: success
- `cargo test -p yoi`: success
- `cargo check -p yoi`: success
- `cargo test -p pod legacy_raw_wasm_runtime_is_rejected_without_fallback_execution`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- dirty tree warning は未コミット検証時点の通常 warning。ビルド自体は成功。
Commit:
- `6086099fe4807b5b12c8d13333f5544a4174e3d9 feat: reject legacy plugin runtime manifests`
Residual risks / notes:
- 既知の残リスクなし。
- raw-WASM execution fallback は再導入していない。
- legacy runtime 名は manifest/pod 内部の rejected diagnostic fixture/判定用としてのみ残している。
- Service/Ingress runtime、WebSocket driver、WIT/PDK/templates の service event 更新、broader runtime redesign は実施していない。
---
<!-- event: review author: yoi-reviewer-00001KVXK0WDH-plugin-manifest-reject at: 2026-06-24T21:18:53Z status: approve -->
## Review: approve
Review result: approve
確認範囲:
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDH-plugin-manifest-reject-legacy`
- branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy`
- HEAD `6086099fe4807b5b12c8d13333f5544a4174e3d9`
- 実履歴上の親 `ef1d8d9a` → HEAD の差分をレビューした。指定 base `187e6426` はこの worktree では object missing だったが、HEAD の直前 commit は `ticket: accept plugin manifest rejection task` で、レビュー対象差分として妥当と判断した。
承認根拠:
- Ticket / IntentPacket の要件と照合済み。
- `crates/manifest/src/plugin.rs`:
- legacy `wasm` / `yoi-plugin-wasm-1` は public const ではなく internal rejection 用 const になっている。
- `validate_manifest``kind = "wasm"``legacy raw wasm ... retired; use wasm-component` として `Api/Manifest` diagnostic 付きで拒否している。
- `wasm-component` は正の runtime kind として `component` / `world` validation を維持している。
- legacy rejection test が追加され、discovery package には入らず diagnostic になることを確認している。
- `crates/yoi/src/plugin_cli.rs`:
- `plugin check` は manifest/discovery rejection を `status: "rejected"` として bounded diagnostic に載せ、既存 check semantics 通り rejected で Err を返す。
- list/show snapshot は discovery/resolution diagnostics から rejected item を構成し、legacy package を active/eligible として扱わない。
- component fixture は `wasm-component` / `plugin.component.wasm` に更新され、legacy fixture は rejected fixture に変換されている。
- `crates/pod/src/feature/plugin.rs`:
- manifest crate の legacy runtime public const 依存は削除済み。
- legacy name は internal diagnostic/rejection matching のみ。
- `PluginInstanceRuntime::new` は legacy `wasm` を即エラーにし、raw-WASM fallback 実行は戻っていない。
- docs:
- `docs/development/plugin-development.md`
- `docs/design/plugin-component-model.md`
- `docs/design/plugin-packages.md`
で raw core-Wasm compatibility 前提は撤回され、`wasm-component` が supported/public runtime と明記されている。
- `rg 'plugin\.wasm|kind = "wasm"|abi = "yoi-plugin-wasm-1"'` で残存箇所を確認。残りは rejection docs/tests/diagnostics と component build artifact 名の文脈のみで、active public compatibility 表現は見当たらない。
- 差分対象は manifest / CLI / pod diagnostic alignment / docs の 6 files に限定され、Service/Ingress/WebSocket/WIT/PDK runtime redesign への scope creep は確認していない。
Validation:
- reviewer は read-only 制約のため `cargo test` / `cargo check` / `nix build` は再実行していない。
- reviewer 側で `git diff --check HEAD^..HEAD`: success。
- Coder reported validation (`cargo fmt --check`, `cargo test -p manifest`, `cargo test -p yoi`, `cargo check -p yoi`, focused pod test, `nix build .#yoi --no-link`) は実装内容と整合しており、追加で疑う材料はない。
Non-blocking note:
- 親から指定された review base hash `187e6426` は誤記/存在しない hash だった。実レビューは child branch の実 parent `ef1d8d9a` を base として行われた。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T21:20:36Z -->
## Implementation report
Integration outcome:
- Coder `yoi-coder-00001KVXK0WDH-plugin-manifest-reject` が implementation branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy` に実装を commit した。
- implementation commit: `6086099f feat: reject legacy plugin runtime manifests`
- Reviewer `yoi-reviewer-00001KVXK0WDH-plugin-manifest-reject` は read-only review で `approve`。legacy `wasm` / `yoi-plugin-wasm-1` manifest rejection、CLI check/list/show diagnostic behavior、component package non-regression、pod diagnostic alignment、docs update、scope creep なしを確認した。
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WDH-plugin-manifest-reject-legacy` を merge 済み。
- merge commit: `449745ee merge: 00001KVXK0WDH plugin manifest rejection`
Implemented behavior:
- `crates/manifest/src/plugin.rs`:
- legacy raw `wasm` / `yoi-plugin-wasm-1` runtime を manifest validation で rejected diagnostic にする。
- `wasm-component` を supported public runtime として維持。
- `crates/yoi/src/plugin_cli.rs`:
- `yoi plugin check` は legacy package を `status: "rejected"` として bounded diagnostic に載せ、既存 check semantics 通り失敗扱いにする。
- list/show fixture は legacy package を active/eligible として表示しない。
- `crates/pod/src/feature/plugin.rs`:
- legacy runtime public constant dependency を外し、legacy runtime name は internal diagnostic/rejection matching のみに限定。
- raw-WASM fallback execution は再導入していない。
- docs:
- `docs/development/plugin-development.md`, `docs/design/plugin-component-model.md`, `docs/design/plugin-packages.md` で raw core-Wasm compatibility 前提を撤回し、`wasm-component` を supported/public runtime と明記。
Validation in Orchestrator worktree:
- `cargo fmt --check`: success
- `cargo test -p manifest`: success
- `cargo test -p yoi`: success
- `cargo check -p yoi`: success
- `cargo test -p pod legacy_raw_wasm_runtime_is_rejected_without_fallback_execution --lib`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Notes:
- Reviewer non-blocking note: Orchestrator から渡した review base `187e6426` は誤記で、child branch の実 parent `ef1d8d9a` を base として review された。review scope は適切だった。
- Follow-up `00001KVXK0WDQ` は this Ticket completion により dependency-unblocked and can be re-routed next.
Next action:
- Mark Ticket done after this integration/validation evidence.
- Then stop related child Pods and remove only the child implementation worktree/branch.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T21:20:45Z from: inprogress to: done reason: merged_validated field: state -->
## State changed
Implementation is merged into the orchestration branch and validated.
Evidence:
- merge commit: `449745ee merge: 00001KVXK0WDH plugin manifest rejection`
- reviewer result: approve
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
- `cargo fmt --check`
- `cargo test -p manifest`
- `cargo test -p yoi`
- `cargo check -p yoi`
- `cargo test -p pod legacy_raw_wasm_runtime_is_rejected_without_fallback_execution --lib`
- `git diff --check`
- `nix build .#yoi --no-link`
Closure is not performed here; this state records implementation completion after merge/validation.
--- ---

View File

@ -0,0 +1,2 @@
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WDQ","kind":"blocked_by","related_ticket":"00001KVXK0WDH","note":"Queue review: `00001KVXK0WDQ` は Service lifecycle / ingress queue runtime slice だが、Component Model-only runtime authority / manifest rejection slice `00001KVXK0WDH` に depends_on している。prerequisite completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
{"id":"orch-plan-20260624-212209-2","ticket_id":"00001KVXK0WDQ","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WDQ` は prerequisites `00001KVXK0WD3` と `00001KVXK0WDH` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle` と branch `work/00001KVXK0WDQ-plugin-service-lifecycle` で、Plugin Service lifecycle と ingress queue runtime を実装する。Output command model / WebSocket driver / WIT-PDK templates は後続 Tickets に残す。","branch":"work/00001KVXK0WDQ-plugin-service-lifecycle","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: implement service lifecycle and ingress queue runtime in dedicated child worktree. Reviewer: read-only review focusing on bounded in-process event queue, no WebSocket driver/output command scope creep, grant/capability boundaries, and component Tool regression."},"author":"yoi-orchestrator","at":"2026-06-24T21:22:09Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'Define Plugin Service lifecycle and ingress queue runtime' title: 'Define Plugin Service lifecycle and ingress queue runtime'
state: 'queued' state: 'done'
created_at: '2026-06-24T19:51:56Z' created_at: '2026-06-24T19:51:56Z'
updated_at: '2026-06-24T20:12:00Z' updated_at: '2026-06-24T21:51:13Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-24T20:12:00Z' queued_at: '2026-06-24T20:12:00Z'

View File

@ -30,4 +30,274 @@ Marked ready by `yoi ticket state`.
Ticket を `workspace-panel` が queued にしました。 Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-24T21:22:37Z -->
## Decision
Routing decision: implementation_ready
Reason:
- Dashboard Queue により人間が Orchestrator routing を許可した queued Ticket として確認した。
- `00001KVXK0WDQ` は Service lifecycle / ingress queue runtime の concrete slice で、WebSocket driver、output command model、WIT/PDK/templates update を non-goal として後続に分離している。
- outgoing `depends_on``00001KVXK0WDH` だが、`00001KVXK0WDH` は done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
- incoming dependents (`00001KVXK0WDX`, `00001KVXK0WE4`) はこの Ticket 完了後に進めるべき後続であり、この Ticket の acceptance blocker ではない。
- bounded context check で `crates/pod/src/feature/plugin.rs` の current `PluginInstanceRegistry`, `PluginInstanceHandle`, `PluginIngressEvent`, `PluginIngressDispatchReport`, `ComponentInstanceRuntime` lifecycle methods を確認した。Ticket は in-process service lifecycle / bounded ingress queue / serial dispatch / diagnostics に収まっており、残る不確実性は local implementation に閉じる。
Evidence checked:
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WDH` は done。routing 前 plan は historical blocked_by `00001KVXK0WDH` のみで、prerequisite 完了により解消済み。accepted plan `orch-plan-20260624-212209-2` を記録済み。
- Related Tickets: `00001KVXK0WD3` / `00001KVXK0WDH` は done。
- Code context: `crates/pod/src/feature/plugin.rs` の service/ingress registration, instance registry, start/status/stop/handle-ingress, component runtime tests。
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
IntentPacket:
Intent:
- Plugin Service を host-managed lifecycle と bounded ingress queue を持つ in-process runtime として扱い、`start()` を initialization-only にし、後続 ingress events を queue 経由で serial dispatch できるようにする。
Binding decisions / invariants:
- Existing Tool Plugin execution は request-response operation として維持し、service queue に巻き込まない。
- v0 dispatch は per-plugin serial dispatch。concurrent per-plugin event execution は non-goal。
- Queue は bounded。full / timeout / failed service / stop 中 event / invalid event は typed error / diagnostic として扱う。
- `start()` は long-running loop / polling loop / recv loop を担わない。
- Durable cross-process event queue は non-goal。まず host-managed in-process queue/lifecycle として実装する。
- WebSocket driver と output command model は後続 Tickets (`00001KVXK0WE4`, `00001KVXK0WDX`) に残す。
- Component Model-only runtime authority and manifest rejection from prerequisites must not regress.
Requirements / acceptance criteria:
- Service Plugin instance が ready/starting/running/stopping/stopped/failed 相当の lifecycle state を持つ。
- `start()` return 後も ingress event を queue 経由で配送できる。
- Ingress event has source / ingress name / payload / created_at / attempt / correlation id.
- Queue depth / lifecycle state / last error / dispatch counters are visible in status diagnostics.
- Unit tests cover lifecycle start/stop/failure, bounded queue full, serial dispatch, timeout/failure diagnostics, stop-time event rejection, Tool execution regression.
- `cargo test -p pod`, `cargo check -p yoi`, `git diff --check`, `nix build .#yoi --no-link` are validation targets.
Implementation latitude:
- Choose exact structs/enums/names and whether queue processing is sync-step driven or background worker, as long as start returns promptly and dispatch remains host-managed/serial.
- Existing `PluginInstanceRegistry` may be extended or refactored if authority boundaries remain clear.
- Tests may use existing `test-ingress` runtime or focused component fixtures.
Escalate if:
- Implementation requires durable cross-process queue or scheduler semantics.
- WebSocket driver or output command model must be implemented to make lifecycle tests pass.
- Component Tool execution has to be routed through service queue.
- Host API grant/runtime authority or Component Model manifest behavior needs redesign.
Validation:
- `cargo test -p pod`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
- Focused tests for service lifecycle/queue may be run during development.
Current code map:
- Primary: `crates/pod/src/feature/plugin.rs`
- Secondary only if needed: manifest service/ingress declarations and docs comments.
- Avoid: WebSocket driver, output command model, WIT/PDK/templates event update, remote runtime protocol, durable cross-process queue.
Critical risks / reviewer focus:
- accidentally making Service runtime a scheduler/durable queue.
- Tool execution regression or Tool path accidentally using Service queue.
- unbounded queue or hidden background loop.
- missing stop/failure/timeout diagnostics.
- scope creep into WebSocket/output commands/WIT-PDK.
Next action:
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T21:23:16Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
## State changed
Orchestrator acceptance: queued -> inprogress
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
- outgoing dependency `00001KVXK0WDH` は done / merged / reviewed / validated 済み。
- accepted plan `orch-plan-20260624-212209-2` を確認した。
- routing decision と IntentPacket は Ticket thread に記録済み。
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle` を作成し、multi-agent-workflow に接続する。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T21:24:10Z -->
## Implementation report
Implementation orchestration update:
- prerequisite `00001KVXK0WDH` が done になったため、`00001KVXK0WDQ` を再 routing して受理した。
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle` と branch `work/00001KVXK0WDQ-plugin-service-lifecycle` を作成した。
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
- Coder Pod `yoi-coder-00001KVXK0WDQ-service-lifecycle` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
---
<!-- event: implementation_report author: yoi-coder-00001KVXK0WDQ-service-lifecycle at: 2026-06-24T21:43:56Z -->
## Implementation report
Implementation report:
対象 Ticket:
- `00001KVXK0WDQ` — Define Plugin Service lifecycle and ingress queue runtime
変更ファイル:
- `crates/pod/src/feature/plugin.rs`
主な実装:
- Plugin Service の host-managed lifecycle を追加。
- `Ready / Starting / Running / Stopping / Stopped / Failed`
- in-process bounded ingress queue を追加。
- v0 は per-plugin serial dispatch。
- Tool Plugin 実行は従来通り request-response のまま維持。
- `PluginIngressEvent` に以下を追加。
- `source`
- `ingress_name`
- `payload`
- `created_at`
- `attempt`
- `correlation_id`
- typed ingress dispatch error / diagnostic を追加。
- invalid event
- queue full
- service unavailable / failed / stopped
- dispatch timeout
- dispatch failure
- status diagnostics に以下を追加。
- lifecycle state
- queue depth / capacity
- last error
- dispatch counters
- Unit tests を追加・更新。
- lifecycle start/stop/failure
- bounded queue full
- serial dispatch
- timeout diagnostic
- dispatch failure diagnostic
- stopped service rejection
- invalid event rejection
- Tool execution regression
Commit:
- `4e138b7e365d940bf31397aa64cf14a58b6bc51a feat: add plugin service ingress queue`
Validation:
- `cargo test -p pod feature::plugin::tests`: success (`73 passed`)
- `cargo check -p yoi`: success
- `git diff --check`: success
- `git diff --check HEAD~1 HEAD`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed in existing prompt guidance assertions outside this Ticket:
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
- failure: `assertion failed: rendered.contains("worktree status, diff, and test results")`
Residual risks / notes:
- Full `cargo test -p pod` remains blocked by unrelated prompt/resource snapshot assertions; Plugin-focused tests pass.
- v0 dispatch timeout is implemented as elapsed check after synchronous dispatch completion and typed diagnostic. Force-cancelling executing Wasm in a separate worker is broader runtime redesign and remains non-goal.
---
<!-- event: review author: yoi-reviewer-00001KVXK0WDQ-service-lifecycle at: 2026-06-24T21:49:35Z status: approve -->
## Review: approve
Review result: approve
確認範囲:
- child worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDQ-plugin-service-lifecycle`
- branch `work/00001KVXK0WDQ-plugin-service-lifecycle`
- HEAD `4e138b7e365d940bf31397aa64cf14a58b6bc51a`
- base `f26c7e0d`
- diff `f26c7e0d..HEAD`
- Ticket `00001KVXK0WDQ``item.md` / `thread.md` / orchestration plan
判断:
- Ticket intent / IntentPacket に対して実装は妥当。
- 変更は `crates/pod/src/feature/plugin.rs` のみで、WebSocket driver / output command model / WIT/PDK/templates / durable queue への scope creep は見当たらない。
- Component Model-only runtime と legacy raw wasm rejection は維持されている。
主な確認根拠:
- `PluginInstanceLifecycleState``Ready / Starting / Running / Stopping / Stopped / Failed` に拡張され、start/stop/status/dispatch failure で lifecycle と diagnostics が更新される。
- `PluginIngressEvent``source / ingress_name / payload / created_at / attempt / correlation_id` が揃っている。
- `VecDeque` + capacity による in-process bounded queue、`QueueFull` / `InvalidEvent` / `ServiceFailed` / `ServiceStopped` / `DispatchTimeout` / `DispatchFailed` の typed error/diagnostic が追加されている。
- dispatch は `Mutex` 下の per-plugin serial dispatch で、concurrent per-plugin execution にはしていない。
- Tool execution path は `PluginInstanceTool::execute -> handle_tool` の request-response のままで、Service queue に流していない。
- status に `queue_depth / queue_capacity / last_error / dispatch_counters / diagnostics` が出る。
- unit tests は lifecycle、queue full、serial dispatch、timeout/failure、stopped rejection、invalid event、Tool regression をカバーしている。
検証:
- reviewer: `git diff --check f26c7e0d..HEAD`: success
- cargo/nix は read-only 指示のため再実行せず、coder 報告を確認対象として扱った。
Non-blocking notes:
- queue processing は sync-step driven で、現 public delivery は enqueue 直後に同じ lock 内で dispatch するため、通常経路では queue depth はほぼ 0 になる。Ticket の implementation latitude 内だが、後続の WebSocket driver 側で backpressure/queue depth をどう見せるかは改めて注意が必要。
- `cargo test -p pod` は coder 報告では既存の prompt guidance assertion 由来の失敗あり。Plugin-focused tests / `cargo check -p yoi` / nix build は成功報告で、本 Ticket の blocker とは判断しない。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-24T21:51:01Z -->
## Implementation report
Integration outcome:
- Coder `yoi-coder-00001KVXK0WDQ-service-lifecycle` が implementation branch `work/00001KVXK0WDQ-plugin-service-lifecycle` に実装を commit した。
- implementation commit: `4e138b7e feat: add plugin service ingress queue`
- Reviewer `yoi-reviewer-00001KVXK0WDQ-service-lifecycle` は read-only review で `approve`。host-managed in-process lifecycle/queue、bounded queue、serial dispatch、typed diagnostics、Tool path non-regression、scope creep なしを確認した。
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WDQ-plugin-service-lifecycle` を merge 済み。
- merge commit: `000afbbe merge: 00001KVXK0WDQ plugin service lifecycle`
Implemented behavior:
- `crates/pod/src/feature/plugin.rs` に Plugin Service lifecycle state を追加。
- `Ready / Starting / Running / Stopping / Stopped / Failed`
- in-process bounded ingress queue と per-plugin serial dispatch を追加。
- `PluginIngressEvent``source`, `ingress_name`, `payload`, `created_at`, `attempt`, `correlation_id` を追加。
- `QueueFull`, `InvalidEvent`, `ServiceFailed`, `ServiceStopped`, `DispatchTimeout`, `DispatchFailed` など typed error/diagnostic を追加。
- status diagnostics に queue depth/capacity, last_error, dispatch_counters, lifecycle diagnostics を追加。
- Tool Plugin execution は従来の request-response path のまま維持。
Validation in Orchestrator worktree:
- `cargo test -p pod feature::plugin::tests`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed only in known prompt guidance snapshot assertions outside this Plugin diff:
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
- Plugin-focused tests in the same run passed, including service lifecycle/queue/timeout/Tool regression coverage.
Notes:
- Reviewer non-blocking note: current queue processing is sync-step driven, and public delivery enqueues then dispatches immediately under the lock, so normal queue depth is usually 0. This remains within Ticket latitude, but follow-up WebSocket driver/backpressure work should revisit how queue depth is exposed.
- Follow-up `00001KVXK0WDX` is dependency-unblocked and can be re-routed next.
Next action:
- Mark Ticket done after this integration/validation evidence.
- Then stop related child Pods and remove only the child implementation worktree/branch.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-24T21:51:13Z from: inprogress to: done reason: merged_validated_with_unrelated_full_suite_caveat field: state -->
## State changed
Implementation is merged into the orchestration branch and validated with one unrelated known full-suite caveat.
Evidence:
- merge commit: `000afbbe merge: 00001KVXK0WDQ plugin service lifecycle`
- reviewer result: approve
- validation in `/home/hare/Projects/yoi/.worktree/orchestration`:
- `cargo test -p pod feature::plugin::tests`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed only in prompt guidance snapshot assertions outside this Plugin diff; Plugin-focused tests passed.
Closure is not performed here; this state records implementation completion after merge/review/focused validation.
--- ---

View File

@ -0,0 +1,2 @@
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WDX","kind":"blocked_by","related_ticket":"00001KVXK0WDQ","note":"Queue review: `00001KVXK0WDX` は service output command model slice だが、output commands are returned by service ingress dispatch のため `00001KVXK0WDQ` に depends_on している。prerequisite completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
{"id":"orch-plan-20260625-054531-2","ticket_id":"00001KVXK0WDX","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WDX` は prerequisite `00001KVXK0WDQ` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands` と branch `work/00001KVXK0WDX-plugin-output-commands` で、Plugin Service ingress handler の output command envelope / validation / grant-check / diagnostics を追加する。WebSocket transport、HTTP request dispatch 完成、WIT/PDK/templates update は後続 Tickets に残す。","branch":"work/00001KVXK0WDX-plugin-output-commands","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: implement service output command envelope and grant-check diagnostics in dedicated child worktree. Reviewer: read-only review focusing on command/result separation from ToolOutput, fail-closed grants, no WebSocket/HTTP transport scope creep, and service lifecycle non-regression."},"author":"yoi-orchestrator","at":"2026-06-25T05:45:31Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'Add Plugin service output command model' title: 'Add Plugin service output command model'
state: 'queued' state: 'done'
created_at: '2026-06-24T19:51:56Z' created_at: '2026-06-24T19:51:56Z'
updated_at: '2026-06-24T20:12:02Z' updated_at: '2026-06-25T06:20:26Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-24T20:12:02Z' queued_at: '2026-06-24T20:12:02Z'

View File

@ -30,4 +30,270 @@ Marked ready by `yoi ticket state`.
Ticket を `workspace-panel` が queued にしました。 Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T05:46:02Z -->
## Decision
Routing decision: implementation_ready
Reason:
- ユーザーから「続けて」と明示 follow-up があり、queued dependency chain の次 Ticket として再確認した。
- `00001KVXK0WDX` は Service output command model の concrete slice で、WebSocket transport、HTTP request dispatch completion、Domain operation command completion、WIT/PDK/templates update は non-goal として後続に分離されている。
- outgoing `depends_on``00001KVXK0WDQ` だが、`00001KVXK0WDQ` は done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
- incoming dependent `00001KVXK0WE4` は WebSocket driver slice で、この Ticket 完了後に進めるべき後続であり、この Ticket の acceptance blocker ではない。
- bounded context check で current Plugin service lifecycle / ingress queue implementation が `crates/pod/src/feature/plugin.rs` に入り、event dispatch/status diagnostics の拡張点が存在することを確認した。Ticket の残る不確実性は command envelope / validation / grant-check / diagnostics の local implementation に閉じる。
Evidence checked:
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WDQ` は done。routing 前 plan は historical blocked_by `00001KVXK0WDQ` のみで、prerequisite 完了により解消済み。accepted plan `orch-plan-20260625-054531-2` を記録済み。
- Related Tickets: `00001KVXK0WD3`, `00001KVXK0WDH`, `00001KVXK0WDQ` は done。
- Code context: `crates/pod/src/feature/plugin.rs` の Plugin Service lifecycle / bounded ingress queue / `PluginIngressEvent` / status diagnostics / component runtime。
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
IntentPacket:
Intent:
- Service Plugin ingress handler の戻り値として output command envelope を表現し、Host が command ごとに manifest declaration / enablement grant / runtime policy を fail-closed に検査し、結果を service diagnostics/status から追えるようにする。
Binding decisions / invariants:
- Service output command は Tool Plugin の ordinary `ToolOutput` path と型・処理経路・docs/tests で区別する。
- v0 command kind は最小集合に留める: diagnostic/status update, host request dispatch placeholder, websocket send placeholder。
- WebSocket send の実 transport 実装、HTTP request dispatch completion、Domain operation command completion は non-goal。
- Unsupported / ungranted / malformed command は実行せず typed diagnostic にする。
- Unrestricted shell / filesystem command や hidden LLM context injection は絶対に導入しない。
- Existing Plugin Service lifecycle / bounded ingress queue and Component Model-only runtime from prerequisites must not regress.
Requirements / acceptance criteria:
- `handle-ingress` / service event handler result can carry output command list.
- Command has correlation id / source event id / command id / kind / payload / requested_at.
- Host parses, validates, and grant-checks each command.
- Ungranted command is not executed and appears as typed diagnostic.
- Safe diagnostic/status update command is executed or recorded.
- WebSocket send / request dispatch placeholders are grant-checkable and safely unsupported without transport.
- Command execution result is visible from service status / diagnostics or run overview-equivalent state.
- Tests distinguish Tool Plugin output from Service output commands.
Implementation latitude:
- Exact Rust names/enums and JSON/envelope shape may follow existing plugin code style.
- Manifest declaration/grant mapping can be minimal v0 as long as it is explicit and fail-closed.
- Existing tests/fixtures may be extended in `crates/pod/src/feature/plugin.rs`; docs/comments may be updated if needed.
Escalate if:
- Implementing output commands requires actual WebSocket/HTTP transport.
- Current manifest/grant model cannot represent placeholder command grants without broader schema redesign.
- Command results must be persisted in a durable cross-process run overview to satisfy tests.
- Tool Plugin output must be routed through Service command processing.
Validation:
- `cargo test -p pod`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
- Focused plugin service command tests during development are expected.
Current code map:
- Primary: `crates/pod/src/feature/plugin.rs`
- Secondary only if necessary: manifest grant declarations/tests/docs comments。
- Avoid: WebSocket transport driver, HTTP request dispatch completion, WIT/PDK/templates service event update, durable cross-process queue, remote runtime protocol。
Critical risks / reviewer focus:
- output commands becoming ambient authority.
- ToolOutput and Service output commands being conflated.
- ungranted/malformed commands partially executing.
- placeholders accidentally performing network I/O。
- scope creep into WebSocket driver or PDK/WIT updates.
Next action:
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T05:46:44Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
## State changed
Orchestrator acceptance: queued -> inprogress
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
- outgoing dependency `00001KVXK0WDQ` は done / merged / reviewed / validated 済み。
- accepted plan `orch-plan-20260625-054531-2` を確認した。
- routing decision と IntentPacket は Ticket thread に記録済み。
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands` を作成し、multi-agent-workflow に接続する。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T05:47:43Z -->
## Implementation report
Implementation orchestration update:
- prerequisite `00001KVXK0WDQ` が done になったため、`00001KVXK0WDX` を再 routing して受理した。
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WDX-plugin-output-commands` と branch `work/00001KVXK0WDX-plugin-output-commands` を作成した。
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
- Coder Pod `yoi-coder-00001KVXK0WDX-output-commands` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
---
<!-- event: implementation_report author: yoi-coder-00001KVXK0WDX-output-commands at: 2026-06-25T06:12:58Z -->
## Implementation report
Implementation report:
対象 Ticket:
- `00001KVXK0WDX` — Add Plugin service output command model
変更ファイル:
- `crates/pod/src/feature/plugin.rs`
主な実装:
- Service/Ingress handler の戻り値から `output_commands` を読む Service output command model を追加。
- command envelope に以下を追加。
- `correlation_id`
- `source_event_id`
- `command_id`
- `kind`
- `payload`
- `requested_at`
- v0 command kind を追加。
- `diagnostic_status_update`
- `host_request_dispatch`
- `websocket_send`
- Host 側で envelope parse / field validation / `requested_at` RFC3339 validation / `source_event_id` と ingress event correlation id の照合 / payload size bound / kind-specific payload validation / request・websocket grant check を行うようにした。
- malformed / ungranted / unsupported command は typed diagnostic として記録し、実行しない。
- `diagnostic_status_update` は安全に `component_status` / diagnostics に記録する。
- `host_request_dispatch` / `websocket_send` は grant-check 可能だが、v0 では transport 実行せず `Unsupported` として記録する。
- Service output command results を `PluginIngressDispatchReport``PluginInstanceStatus` から見えるように追加。
- ordinary Tool Plugin `ToolOutput` path と Service output command path が混ざらないよう、型・コメント・テストを分離。
- focused tests を追加。
- diagnostic/status command の記録
- ungranted request command の reject と status side effect 非実行
- request/websocket placeholder の grant-check + unsupported 記録
- malformed envelope の reject
Commit:
- `755d460f0d4e2dccb3f75d9ad980b1e701464a4e feat: add plugin service output commands`
Validation:
- `cargo fmt --check`: success
- `cargo test -p pod service_output_command -- --nocapture`: success (`4 passed`)
- `cargo test -p pod feature::plugin::tests`: success (`77 passed`)
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed in known prompt guidance snapshot assertions outside this Ticket (`prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`, `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`), with Plugin tests passing.
Residual risks / notes:
- WebSocket send transport と host request dispatch の実実行は Ticket non-goal に従い未実装で、grant-check 後に `Unsupported` diagnostic として記録する。
- Service output command schema は Rust 側 JSON envelope として追加した。WIT/PDK/templates 更新は non-goal として触っていない。
- Full `cargo test -p pod` の 2 failures は今回の Plugin diff 外の既知 prompt snapshot failure と一致している。
---
<!-- event: review author: yoi-reviewer-00001KVXK0WDX-output-commands at: 2026-06-25T06:18:37Z status: approve -->
## Review: approve
Review result: approve
確認結果:
- Ticket `00001KVXK0WDX` の IntentPacket / 受け入れ条件に対して、実装 commit `755d460f feat: add plugin service output commands` は妥当。
承認根拠:
- 差分は `crates/pod/src/feature/plugin.rs` のみで、scope creep は見当たらない。
- Service/Ingress handler output の `output_commands` を読む専用経路が追加され、ordinary Tool Plugin `ToolOutput` path とは型・処理・report/status field が分離されている。
- envelope は `correlation_id` / `source_event_id` / `command_id` / `kind` / `payload` / `requested_at` を持ち、unknown fields reject、id / RFC3339 / `source_event_id == ingress correlation_id` / payload size / kind-specific payload を検証している。
- `host_request_dispatch``websocket_send` は既存の manifest declaration + enabled grant / allowlist 検査に乗っており、実行段階では `Unsupported` result を返すだけで HTTP/WebSocket transport 実装に踏み込んでいない。
- malformed / ungranted command は `ServiceOutputCommandRejected` diagnostic になり、mixed batch でも安全側に倒して部分的な status update を実行しないテストがある。
- `diagnostic_status_update` は bounded payload/message 経由で `component_status` / diagnostics / `output_command_results` に記録され、外部 side effect や hidden LLM context injection はない。
- Service command results は `PluginIngressDispatchReport``PluginInstanceStatus.output_command_results` から追跡可能。
- 追加テストは recorded diagnostic、ungranted rejection、placeholder unsupported、malformed envelope を押さえている。
検証:
- Ticket `item.md` / `thread.md` / orchestration plan / relations を確認。
- child worktree で `git status` / `git log` / `git diff --stat` / diff 内容を read-only 確認。
- read-only 指示のため cargo/nix 等は再実行していない。
Non-blocking note:
- Coder 報告では `cargo test -p pod` が既知の prompt guidance snapshot assertion でのみ失敗、Plugin tests / `cargo check -p yoi` / `git diff --check` / `nix build .#yoi --no-link` は成功。最終統合時に既知 failure 扱いだけ確認すること。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T06:20:17Z -->
## Implementation report
Integration outcome:
- Coder `yoi-coder-00001KVXK0WDX-output-commands` が implementation branch `work/00001KVXK0WDX-plugin-output-commands` に実装を commit した。
- implementation commit: `755d460f feat: add plugin service output commands`
- Reviewer `yoi-reviewer-00001KVXK0WDX-output-commands` は read-only review で `approve`。Service output command path と ToolOutput path の分離、fail-closed validation/grant checks、placeholder command の non-transport behavior、diagnostic/status recording、scope creep なしを確認した。
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WDX-plugin-output-commands` を merge 済み。
- merge commit: `d2aa92a7 merge: 00001KVXK0WDX plugin output commands`
Implemented behavior:
- `crates/pod/src/feature/plugin.rs` に Service output command envelope handling を追加。
- `output_commands``correlation_id`, `source_event_id`, `command_id`, `kind`, `payload`, `requested_at` を持つ。
- v0 command kinds:
- `diagnostic_status_update`
- `host_request_dispatch`
- `websocket_send`
- Host-side validation:
- envelope parse / unknown field rejection
- id and RFC3339 validation
- `source_event_id` と ingress event correlation id の照合
- payload size bound
- kind-specific payload validation
- manifest declaration + enablement grant / allowlist checks
- malformed / ungranted / unsupported commands は typed diagnostic になり、実行しない。
- `diagnostic_status_update` は bounded payload/message で component status / diagnostics / output command results に記録。
- `host_request_dispatch` / `websocket_send` は grant-check 後に `Unsupported` として記録し、HTTP/WebSocket transport には踏み込まない。
- `PluginIngressDispatchReport``PluginInstanceStatus.output_command_results` から command result を追跡可能。
Validation in Orchestrator worktree:
- `cargo fmt --check`: success
- `cargo test -p pod service_output_command -- --nocapture`: success
- `cargo test -p pod feature::plugin::tests`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed only in known prompt guidance snapshot assertions outside this Plugin diff:
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
- Plugin-focused tests in the same run passed.
Notes:
- Full `cargo test -p pod` failure is recorded as the same known unrelated prompt-resource assertion issue seen in previous Plugin Tickets, not a blocker for this Ticket.
- Follow-up `00001KVXK0WE4` is dependency-unblocked and can be re-routed next.
Next action:
- Mark Ticket done after this integration/validation evidence.
- Then stop related child Pods and remove only the child implementation worktree/branch.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T06:20:26Z from: inprogress to: done reason: merged_validated_with_unrelated_full_suite_caveat field: state -->
## State changed
Implementation is merged into the orchestration branch and validated with one unrelated known full-suite caveat.
Evidence:
- merge commit: `d2aa92a7 merge: 00001KVXK0WDX plugin output commands`
- reviewer result: approve
- validation in `/home/hare/Projects/yoi/.worktree/orchestration`:
- `cargo fmt --check`: success
- `cargo test -p pod service_output_command -- --nocapture`: success
- `cargo test -p pod feature::plugin::tests`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed only in prompt guidance snapshot assertions outside this Plugin diff; Plugin-focused tests passed.
Closure is not performed here; this state records implementation completion after merge/review/focused validation.
--- ---

View File

@ -0,0 +1,3 @@
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WE4","kind":"blocked_by","related_ticket":"00001KVXK0WDQ","note":"Queue review: `00001KVXK0WE4` は WebSocket driver slice だが、service ingress queue `00001KVXK0WDQ` と output command model `00001KVXK0WDX` に depends_on している。両 prerequisite completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
{"id":"orch-plan-20260624-201341-2","ticket_id":"00001KVXK0WE4","kind":"blocked_by","related_ticket":"00001KVXK0WDX","note":"Queue review: `00001KVXK0WE4` は output command dispatch/grant model `00001KVXK0WDX` にも depends_on している。`00001KVXK0WDQ` と `00001KVXK0WDX` の両方が完了するまで acceptance しない。","author":"yoi-orchestrator","at":"2026-06-24T20:13:41Z"}
{"id":"orch-plan-20260625-062148-3","ticket_id":"00001KVXK0WE4","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WE4` は prerequisites `00001KVXK0WDQ` と `00001KVXK0WDX` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver` と branch `work/00001KVXK0WE4-plugin-websocket-driver` で、Plugin Service 用 host-owned WebSocket driver を追加し、incoming text frames を ingress queue に入れ、output command の websocket send を grant-check 後に送信する。Discord protocol/full reconnect/secret-store/browser WebSocket/WIT-PDK template update は non-goals。","branch":"work/00001KVXK0WE4-plugin-websocket-driver","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: implement host-owned WebSocket driver slice in dedicated child worktree. Reviewer: read-only review focusing on host-owned connection authority, ingress queue integration, output command send execution, grant checks, and no protocol-specific/reconnect/scope creep."},"author":"yoi-orchestrator","at":"2026-06-25T06:21:48Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'Add host-owned WebSocket driver for Plugin services' title: 'Add host-owned WebSocket driver for Plugin services'
state: 'queued' state: 'done'
created_at: '2026-06-24T19:51:56Z' created_at: '2026-06-24T19:51:56Z'
updated_at: '2026-06-24T20:12:03Z' updated_at: '2026-06-25T07:06:30Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-24T20:12:03Z' queued_at: '2026-06-24T20:12:03Z'

View File

@ -30,4 +30,259 @@ Marked ready by `yoi ticket state`.
Ticket を `workspace-panel` が queued にしました。 Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T06:22:18Z -->
## Decision
Routing decision: implementation_ready
Reason:
- ユーザーから「続けて」と明示 follow-up があり、queued dependency chain の次 Ticket として再確認した。
- `00001KVXK0WE4` は host-owned WebSocket driver の concrete slice で、Discord protocol、full reconnect/resume policy、secret store/auth injection、browser-facing WebSocket API、WIT/PDK templates update は non-goal として分離されている。
- outgoing `depends_on``00001KVXK0WDQ``00001KVXK0WDX` だが、両方とも done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
- incoming dependent `00001KVXK0WEA` は WIT/PDK/templates update で、この Ticket 完了後に進めるべき後続であり、この Ticket の acceptance blocker ではない。
- bounded context check で current `crates/pod/src/feature/plugin.rs` に existing pull `host_api.websocket` helpers、WebSocket grants/allowlist、Plugin Service lifecycle / ingress queue、Service output command model が存在することを確認した。Ticket の残る不確実性は host-owned connection driver / frame-to-ingress / output command send integration の local implementation に閉じる。
Evidence checked:
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WDQ``00001KVXK0WDX` は done。routing 前 plan は historical blocked_by records 2 件で、prerequisites 完了により解消済み。accepted plan `orch-plan-20260625-062148-3` を記録済み。
- Related Tickets: `00001KVXK0WDQ` and `00001KVXK0WDX` are done.
- Code context: `crates/pod/src/feature/plugin.rs` の WebSocket allowlist/grants, existing pull API helpers, Plugin Service ingress queue, output command model。
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
IntentPacket:
Intent:
- Service Plugin に紐づく host-owned WebSocket connection driver を追加し、incoming text frames を Plugin ingress queue に event として enqueue し、Service output command の `websocket_send` を grant-check 後に Host が送信できるようにする。
Binding decisions / invariants:
- Connection ownership is Host-side. Plugin code does not run a long-lived recv loop as recommended service path.
- Existing pull `host_api.websocket.recv(timeout)` API may remain for compatibility/internal bounded use, but docs/recommended service integration path must move away from it.
- WebSocket target authority is manifest declaration + enabled grant / allowlist. Unauthorized target/send fails closed.
- v0 handles text frames. Binary application payload is unsupported/diagnostic. ping/pong/close handling should be bounded/control/diagnostic, not app payload.
- Incoming frames are converted to service ingress events using existing bounded queue and lifecycle diagnostics.
- Outgoing sends use Service output command path from `00001KVXK0WDX`; do not bypass its validation/grant boundary.
- Full reconnect/resume policy, Discord/Slack protocol logic, secret injection design, browser-facing WebSocket API, WIT/PDK/templates update are non-goals.
Requirements / acceptance criteria:
- Host-owned WebSocket driver can manage a connection associated with a Service Plugin.
- Incoming text frames enqueue Plugin ingress events.
- Close/error/reconnect-needed states become ingress events or status diagnostics.
- Plugin output command can trigger WebSocket text send after grant checks.
- Ungranted send / unauthorized target is fail-closed diagnostic.
- Connection status, last frame time, last error, queue drops, send failures are visible in diagnostics/status.
- Tests cover driver lifecycle, incoming frame to ingress, send command execution/rejection, close/error diagnostics, and frame kind handling.
Implementation latitude:
- Exact struct names and whether tests use mock WebSocket client/connection or local bounded fake are coder choices.
- v0 may model reconnect-needed as diagnostic instead of automatic reconnect.
- Keep integration in `crates/pod/src/feature/plugin.rs` unless a small helper module split is clearly cleaner.
Escalate if:
- Real network tests or external services are required.
- Secret store/auth injection becomes necessary.
- Durable cross-process queue or scheduler semantics are required.
- Implementing this requires WIT/PDK/template changes rather than Rust-side host runtime only.
- Browser-facing WebSocket API or protocol-specific Discord/Slack behavior is needed.
Validation:
- `cargo test -p pod`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
- Focused WebSocket driver / service ingress tests during development.
Current code map:
- Primary: `crates/pod/src/feature/plugin.rs`
- Secondary only if necessary: docs/development recommended path wording。
- Avoid: WIT/PDK/templates update (reserved for `00001KVXK0WEA`), protocol-specific integrations, full reconnect policy, secret store, browser WebSocket API。
Critical risks / reviewer focus:
- hidden ambient network authority or bypassing manifest/grant allowlist。
- pull `recv(timeout)` remaining as recommended long-lived service path。
- output command send bypassing Service command validation。
- unbounded reader task/queue or dropped diagnostics。
- tests that require real external network。
- scope creep into PDK/templates or protocol-specific behavior。
Next action:
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T06:23:16Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
## State changed
Orchestrator acceptance: queued -> inprogress
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
- outgoing dependencies `00001KVXK0WDQ``00001KVXK0WDX` は done / merged / reviewed / validated 済み。
- accepted plan `orch-plan-20260625-062148-3` を確認した。
- routing decision と IntentPacket は Ticket thread に記録済み。
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver` を作成し、multi-agent-workflow に接続する。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T06:24:16Z -->
## Implementation report
Implementation orchestration update:
- prerequisite `00001KVXK0WDX` が done になったため、`00001KVXK0WE4` を再 routing して受理した。
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver` と branch `work/00001KVXK0WE4-plugin-websocket-driver` を作成した。
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
- Coder Pod `yoi-coder-00001KVXK0WE4-websocket-driver` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
---
<!-- event: implementation_report author: yoi-coder-00001KVXK0WE4-websocket-driver at: 2026-06-25T06:56:37Z -->
## Implementation report
Implementation report:
- Ticket: `00001KVXK0WE4`
- Branch/worktree: `work/00001KVXK0WE4-plugin-websocket-driver` / `/home/hare/Projects/yoi/.worktree/00001KVXK0WE4-plugin-websocket-driver`
- Commit: `f2c51ffe399fc4eaa0ab4e4052867d55e534b229`
- Commit message: `feat: add plugin websocket service driver`
変更ファイル:
- `crates/pod/src/feature/plugin.rs`
- Service Plugin 用の host-owned WebSocket driver を追加。
- `ingress.sources``websocket:wss://...` / `ws(s)://...` を Service WebSocket 接続として扱う実装を追加。
- incoming text frame を既存の bounded Service ingress queue へ `websocket_text` event として enqueue。
- close/error を `websocket_close` / `websocket_error` ingress event と status diagnostic に反映。
- `websocket_send` Service output command を host-owned connection 経由の text send に接続。
- manifest declaration + enabled grant / allowlist を通らない open/send は fail-closed。
- status に connection state、last frame time、last error、received/sent count、queue drops、send failures を追加。
- driver lifecycle / incoming / send success / send rejection / send failure / close-error diagnostics / binary-like frame error の focused tests を追加。
- `docs/development/plugin-development.md`
- Service Plugin の推奨 WebSocket integration を、guest long-lived recv loop ではなく host-owned Service WebSocket driver + `websocket_send` output command に更新。
検証結果:
- `cargo test -p pod --lib websocket_`: success (`14 passed`)
- `cargo test -p pod --lib service_output_command`: success (`4 passed`)
- `cargo check -p pod`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- Nix dirty warning は worktree の ignored `target/` によるもので、tracked changes はなし。
- `cargo test -p pod`: failed only in known prompt guidance snapshot tests outside this Ticket (`438 passed; 2 failed`)
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
Residual risks / notes:
- v0 は自動 reconnect/resume policy を実装せず、close/error は diagnostic/event 化に留めている。
- Service WebSocket source の manifest 表現は既存 `ingress.sources` の文字列規約として追加。WIT/PDK/templates 更新は Ticket `00001KVXK0WEA` 側に残している。
- 実ネットワーク test は non-goal のため、mock WebSocket client/connection による bounded focused tests で検証している。
---
<!-- event: review author: yoi-reviewer-00001KVXK0WE4-websocket-driver at: 2026-06-25T07:03:47Z status: approve -->
## Review: approve
Review result: approve
読み取り専用で Ticket / IntentPacket / `62e467c0..HEAD` diff を確認した。Ticket review 記録・merge・close・検証コマンド実行は reviewer では行っていない。
確認した証拠:
- 変更範囲は `crates/pod/src/feature/plugin.rs``docs/development/plugin-development.md` のみで、WIT / PDK / templates へのスコープ拡大はない。
- Host-owned Service WebSocket driver:
- Service/Ingress 有効時に `ingress.sources` から `websocket:` / `ws(s)://` source を subscription 化している。
- instance start 後に host 側で connection start し、reader thread を生成している。
- authority / fail-closed:
- open は `host_api.websocket` permission と manifest/grant allowlist を通した既存 validation を再利用している。
- `websocket_send` は Service output command envelope の validation/grant check 経由でのみ実行され、URL scheme/credentials/static target/manifest+grant allowlist を検査している。
- ingress queue / diagnostics:
- text frame は `websocket_text` event として既存 `deliver_ingress` path に渡され、失敗時は queue drop と diagnostic を記録する。
- close/error は `websocket_close` / `websocket_error` event と status diagnostic に反映される。
- status には connection state, last frame, last error, received/sent counts, queue drops, send failures が出る。
- output command send:
- `PluginServiceOutputCommandKind::WebSocketSend` が追加され、実行時は host-owned connection の `send_text` に接続され、send failure diagnostic も記録する。
- docs:
- `recv(timeout)` は bounded/internal/tool-style use として残しつつ、Service Plugin では host-owned Service WebSocket driver + `websocket_send` を推奨する文面に更新されている。
- tests:
- mock/fake client による incoming text/close、send success/failure、unauthorized target、receive error/binary相当 diagnostic のテストが追加されている。
検証状況:
- reviewer は cargo/nix 等を実行していないread-only / no writes 指示のため)。
- diff・対象コード・Ticket acceptance を静的確認し、coder 報告の `cargo check` / focused tests / nix build 結果を前提証跡として扱った。
Non-blocking notes:
- `docs/development/plugin-development.md` の “Current status” 付近にはまだ Service/Ingress/WebSocket を future work と読む余地が残っているが、本 Ticket の受け入れ条件である「pull recv を recommended service path にしない」は満たしている。後続の docs/PDK/templates Ticket で整理するとよい。
- close/error event の enqueue 失敗は text frame のような queue-drop counter には載せていないが、status diagnostic は記録されるため今回の acceptance 上は blocker ではない。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T07:06:19Z -->
## Implementation report
Integration outcome:
- Coder `yoi-coder-00001KVXK0WE4-websocket-driver` が implementation branch `work/00001KVXK0WE4-plugin-websocket-driver` に実装を commit した。
- implementation commit: `f2c51ffe feat: add plugin websocket service driver`
- Reviewer `yoi-reviewer-00001KVXK0WE4-websocket-driver` は read-only review で `approve`。host-owned connection authority、manifest/grant allowlist、ingress queue integration、`websocket_send` command validation、status diagnostics、scope creep なしを確認した。
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WE4-plugin-websocket-driver` を merge 済み。
- merge commit: `b9e786e1 merge: 00001KVXK0WE4 plugin websocket driver`
Implemented behavior:
- `crates/pod/src/feature/plugin.rs` に Service Plugin 用 host-owned WebSocket driver を追加。
- `ingress.sources``websocket:wss://...` / `ws(s)://...` source を Service WebSocket connection として扱う。
- incoming text frame を existing bounded Service ingress queue へ `websocket_text` event として enqueue。
- close/error は `websocket_close` / `websocket_error` event と status diagnostic に反映。
- `websocket_send` Service output command は host-owned connection 経由の text send に接続。
- manifest declaration + enabled grant / allowlist を通らない open/send は fail-closed。
- status に connection state, last frame time, last error, received/sent counts, queue drops, send failures を追加。
- `docs/development/plugin-development.md` の Service Plugin 推奨 WebSocket integration を host-owned driver + `websocket_send` output command に更新。
Validation in Orchestrator worktree:
- `cargo test -p pod --lib websocket_`: success
- `cargo test -p pod --lib service_output_command`: success
- `cargo check -p pod`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed only in known prompt guidance snapshot assertions outside this Plugin diff:
- `prompt::catalog::tests::pod_orchestration_guidance_section_renders_resource_body`
- `prompt::system::tests::pod_orchestration_guidance_is_included_for_pod_management_tools`
- Plugin/WebSocket focused tests in the same run passed.
Notes:
- Reviewer non-blocking note: `docs/development/plugin-development.md` の “Current status” 付近にはまだ Service/Ingress/WebSocket を future work と読む余地が残る。後続 `00001KVXK0WEA` の docs/PDK/templates update で整理するのがよい。
- Reviewer non-blocking note: close/error event enqueue failure は text frame と同じ queue-drop counter には載らないが、status diagnostic は記録されるため blocker ではない。
- Follow-up `00001KVXK0WEA` is dependency-unblocked and can be re-routed next.
Next action:
- Mark Ticket done after this integration/validation evidence.
- Then stop related child Pods and remove only the child implementation worktree/branch.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T07:06:30Z from: inprogress to: done reason: merged_validated_with_unrelated_full_suite_caveat field: state -->
## State changed
Implementation is merged into the orchestration branch and validated with one unrelated known full-suite caveat.
Evidence:
- merge commit: `b9e786e1 merge: 00001KVXK0WE4 plugin websocket driver`
- reviewer result: approve
- validation in `/home/hare/Projects/yoi/.worktree/orchestration`:
- `cargo test -p pod --lib websocket_`: success
- `cargo test -p pod --lib service_output_command`: success
- `cargo check -p pod`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- `cargo test -p pod`: failed only in prompt guidance snapshot assertions outside this Plugin diff; Plugin/WebSocket-focused tests passed.
Closure is not performed here; this state records implementation completion after merge/review/focused validation.
--- ---

View File

@ -0,0 +1,2 @@
{"id":"orch-plan-20260624-201335-1","ticket_id":"00001KVXK0WEA","kind":"blocked_by","related_ticket":"00001KVXK0WE4","note":"Queue review: `00001KVXK0WEA` は WIT/PDK/templates finishing slice で、implemented WebSocket event/command model `00001KVXK0WE4` に depends_on している。prerequisite completion 後に再 routing する。","author":"yoi-orchestrator","at":"2026-06-24T20:13:35Z"}
{"id":"orch-plan-20260625-070739-2","ticket_id":"00001KVXK0WEA","kind":"accepted_plan","accepted_plan":{"summary":"Ticket `00001KVXK0WEA` は prerequisite `00001KVXK0WE4` が done になったため implementation_ready。専用 worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events` と branch `work/00001KVXK0WEA-plugin-pdk-service-events` で、Plugin WIT / Rust PDK / embedded templates / docs を Component Model-only Service ingress event + output command + host-owned WebSocket model に合わせて更新する。Runtime実装の再設計や protocol-specific integration は non-goal。","branch":"work/00001KVXK0WEA-plugin-pdk-service-events","worktree":"/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events","role_plan":"Orchestrator: accept/routing, worktree creation, final integration/validation/cleanup. Coder: update WIT/PDK/templates/docs/authoring CLI fixtures in dedicated child worktree. Reviewer: read-only review focusing on PDK/template consistency with implemented runtime, no legacy raw-wasm compatibility, and no runtime reimplementation."},"author":"yoi-orchestrator","at":"2026-06-25T07:07:39Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'Update Plugin WIT PDK templates for service event runtime' title: 'Update Plugin WIT PDK templates for service event runtime'
state: 'queued' state: 'done'
created_at: '2026-06-24T19:51:56Z' created_at: '2026-06-24T19:51:56Z'
updated_at: '2026-06-24T20:12:05Z' updated_at: '2026-06-25T07:57:15Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-24T20:12:05Z' queued_at: '2026-06-24T20:12:05Z'

View File

@ -30,4 +30,334 @@ Marked ready by `yoi ticket state`.
Ticket を `workspace-panel` が queued にしました。 Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T07:08:09Z -->
## Decision
Routing decision: implementation_ready
Reason:
- ユーザーから「続けて」と明示 follow-up があり、queued dependency chain の最終 Ticket として再確認した。
- `00001KVXK0WEA` は WIT / PDK / templates / docs を、既に実装済みの Component Model-only runtime、Service lifecycle / ingress queue、Service output commands、host-owned WebSocket driver に合わせる finishing slice である。
- outgoing `depends_on``00001KVXK0WE4` だが、`00001KVXK0WE4` は done / merged / reviewed / validated 済み。`TicketShow` derived blockers は空で、implementation acceptance blocker は残っていない。
- bounded context check で `resources/plugin/wit/*`, `crates/plugin-pdk`, `resources/plugin/templates/rust-component-tool`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md` 周辺に authoring-facing surfaces があることを確認した。Ticket は runtime 実装ではなく authoring surface alignment に限定されており、残る不確実性は local implementation / fixture update に閉じる。
Evidence checked:
- Ticket body / thread: `item.md`, `thread.md`。未解決 planning question は記録されていない。
- Relations / orchestration plan: outgoing depends_on `00001KVXK0WE4` は done。routing 前 plan は historical blocked_by `00001KVXK0WE4` のみで、prerequisite 完了により解消済み。accepted plan `orch-plan-20260625-070739-2` を記録済み。
- Related Tickets: `00001KVXK0WD3`, `00001KVXK0WDH`, `00001KVXK0WDQ`, `00001KVXK0WDX`, `00001KVXK0WE4` are done.
- Code/docs context: `resources/plugin/wit/*.wit`, `crates/plugin-pdk`, `resources/plugin/templates/rust-component-tool`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`
- Workspace state: `/home/hare/Projects/yoi/.worktree/orchestration` は clean。inprogress Ticket は 0 件。
IntentPacket:
Intent:
- Plugin authoring surface (WIT, Rust PDK, embedded templates, docs, `yoi plugin new/check/pack` fixtures) を、Component Model-only runtime と Service ingress event / output command / host-owned WebSocket model に合わせて更新する。
Binding decisions / invariants:
- `plugin.toml` template は `wasm-component` runtime のみを生成する。
- Service/WebSocket authoring pattern は polling `recv(timeout)` loop ではなく、ingress event handler + output command (`websocket_send`) を正とする。
- Tool Plugin authoring support must continue to work.
- Runtime implementation from previous Tickets is not redesigned here; this Ticket updates WIT/PDK/templates/docs/tests to match it.
- No raw core-Wasm compatibility template or legacy runtime alias is introduced.
- No protocol-specific Discord/Slack integration, secret store/auth injection, or full reconnect policy is implemented.
Requirements / acceptance criteria:
- WIT expresses Service ingress event payloads and output command model enough for authoring/tests.
- `yoi-plugin-pdk` exposes ergonomic Tool and Service/Ingress helpers aligned with runtime JSON envelopes.
- Embedded templates include current Tool template and a service-oriented template or equivalent examples using ingress event / output command pattern.
- `yoi plugin new` / `check` / `pack` are consistent with new templates/schema.
- Docs no longer recommend long-running `recv(timeout)` loop for Service WebSocket integration.
- Tests cover PDK helpers, template generation/check/pack, and docs/manifest fixture consistency.
Implementation latitude:
- Exact WIT/interface names and Rust PDK helper APIs may follow existing PDK style, as long as runtime envelopes and docs are consistent.
- If a full new `plugin new` template name is too large, coder may add minimal service template/example plus CLI support needed to satisfy acceptance, but must keep scope bounded.
- Template wasm build/check may use existing test helpers and temporary target dirs.
Escalate if:
- WIT/PDK update requires changing runtime JSON envelope semantics from previous Tickets.
- `yoi plugin new/check/pack` requires broad CLI redesign.
- Real WebSocket network/protocol integration or secret handling is needed.
- Legacy raw-WASM compatibility has to be restored for templates/tests.
Validation:
- `cargo test -p yoi-plugin-pdk`
- `cargo test -p yoi plugin` or focused plugin CLI/template tests
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
- Template cargo-check if applicable, with cleanup of generated template artifacts.
Current code/docs map:
- Primary: `resources/plugin/wit/*.wit`, `crates/plugin-pdk`, `resources/plugin/templates/rust-component-tool`, possible new template under `resources/plugin/templates/`, `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`
- Secondary: manifest tests/fixtures only as needed.
- Avoid: Pod runtime reimplementation, WebSocket driver changes unless minor doc/test alignment, protocol-specific integrations, secret store/auth injection, raw-WASM compatibility。
Critical risks / reviewer focus:
- PDK/template API drift from runtime JSON envelopes。
- Tool template regression while adding Service support。
- reintroducing `recv(timeout)` as recommended Service pattern。
- template generation/check/pack writing outside destination or leaving build artifacts。
- accidental legacy raw-WASM runtime compatibility in examples。
Next action:
- `queued -> inprogress` を記録してから worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events` を作成し、multi-agent-workflow で Coder/Reviewer sibling loop に進める。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T07:08:46Z from: queued to: inprogress reason: orchestrator_acceptance_unblocked field: state -->
## State changed
Orchestrator acceptance: queued -> inprogress
- 直前確認で `TicketShow` は state `queued`、derived blockers は空。
- outgoing dependency `00001KVXK0WE4` は done / merged / reviewed / validated 済み。
- accepted plan `orch-plan-20260625-070739-2` を確認した。
- routing decision と IntentPacket は Ticket thread に記録済み。
- これ以降に worktree-workflow で `/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events` を作成し、multi-agent-workflow に接続する。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T07:09:39Z -->
## Implementation report
Implementation orchestration update:
- prerequisite `00001KVXK0WE4` が done になったため、`00001KVXK0WEA` を再 routing して受理した。
- worktree-workflow に従い、implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVXK0WEA-plugin-pdk-service-events` と branch `work/00001KVXK0WEA-plugin-pdk-service-events` を作成した。
- child worktree では tracked `.yoi` project records を見える状態にし、`.yoi/memory`、local/runtime/log/lock/secret-like `.yoi` paths を sparse-checkout で除外する方針にした。
- Coder Pod `yoi-coder-00001KVXK0WEA-pdk-service-events` を sibling として起動した。write scope は child worktree に限定し、runtime workspace root `/home/hare/Projects/yoi` は startup 要件を満たすため non-recursive read のみにした。
- Coder には root/original workspace と orchestration worktree で read/write/validation/git operation を行わないよう明示した。
---
<!-- event: implementation_report author: yoi-coder-00001KVXK0WEA-pdk-service-events at: 2026-06-25T07:38:44Z -->
## Implementation report
Implementation report:
対象 Ticket:
- `00001KVXK0WEA` — Update Plugin WIT PDK templates for service event runtime
実装概要:
- Plugin authoring surface を Component Model-only / Service ingress event / output command model に合わせて更新した。
変更ファイル:
- `resources/plugin/wit/yoi-plugin-instance-v1.wit`
- Service ingress event JSON と `output_commands` / `websocket_send` の authoring 向け説明を追加。
- `resources/plugin/wit/yoi-plugin-tool-v1.wit`
- current runtime 前提の説明に整理。
- `crates/plugin-pdk/src/lib.rs`
- `PluginIngressEvent``ingress_name`, `created_at`, `attempt`, `correlation_id` を追加。
- `ServiceOutput`, `ServiceOutputCommand`, `ServiceOutputCommandKind` を追加。
- `PluginIngressEvent::websocket_text()`, `websocket_send()`, `ServiceOutput::websocket_send()` を追加。
- `Plugin::handle_ingress` の戻り値を `ServiceOutput` に変更。
- `crates/plugin-pdk/tests/template.rs`
- PDK helper / WIT / template consistency tests を追加・更新。
- `resources/plugin/templates/rust-component-instance/*`
- Service-oriented template に更新。
- guest-side `recv(timeout)` loop ではなく、`handle_ingress` で ingress event を受け、`ServiceOutput::websocket_send` で output command を返す構成に変更。
- `plugin.toml``runtime.kind = "wasm-component"` のみ。
- Tool handler も残し、Tool Plugin authoring support を維持。
- `crates/yoi/src/plugin_cli.rs`, `crates/yoi/src/main.rs`
- `yoi plugin new rust-component-service <path>` を追加。
- service template の `new` / `check` / `pack` consistency を unit test で確認。
- CLI usage を更新。
- `docs/development/plugin-development.md`
- Service/Ingress は host-owned ingress event + output command model として記述。
- Service WebSocket integration で long-running guest `recv(timeout)` loop を推奨しない内容に更新。
Validation:
- `cargo test -p yoi-plugin-pdk`: success
- `cargo test -p yoi plugin`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success (`nix-exit:0`)
- Template cargo-check: success
- `cargo test -p yoi-plugin-pdk` 内で `rust-component-tool` / `rust-component-instance``cargo check --target wasm32-unknown-unknown --offline --target-dir <temp>` を実行し、temporary target dir を使用。
Commit:
- `7a4fd975268b655dca2f0861b8f2bd767c1e914f feat: update plugin service authoring templates`
Residual risks / notes:
- Runtime envelope の再設計は行っていない。
- protocol-specific integration / secret injection / reconnect policy / runtime reimplementation は範囲外として未実装。
---
<!-- event: review author: yoi-reviewer-00001KVXK0WEA-pdk-service-events at: 2026-06-25T07:44:48Z status: request_changes -->
## Review: request changes
Review result: request_changes
実装は概ね Ticket の方向に沿っているが、Service WebSocket template の manifest が runtime の authority boundary と不整合で、生成テンプレートの中心例が実行時に `websocket_send` できない状態に見える。
Blocker:
- Service template が `websocket_send` に必要な manifest 権限/target を宣言していない。
- `resources/plugin/templates/rust-component-instance/src/lib.rs``ServiceOutput::websocket_send(...)` を返す Service/WebSocket 例になっている。
- `resources/plugin/templates/rust-component-instance/plugin.toml``surface/tool/service/ingress``tool/service/ingress` permission はあるが、`{ kind = "host_api", api = "websocket" }` がない。
- 同 `plugin.toml` には matching `[[websocket]]` target declaration もない。
- runtime 側の `websocket_send` 処理は manifest permissions の `host_api.websocket`、grants permissions の `host_api.websocket`、manifest の `[[websocket]]` target declaration、enablement 側の usable websocket grant を要求する。
- そのため生成された service template は `yoi plugin check/pack` は通っても、実行時に `requested host_api.websocket permission is missing` / `manifest host_api.websocket target declaration is missing` 系で拒否されるはず。
Required fixes:
- `resources/plugin/templates/rust-component-instance/plugin.toml` に少なくとも以下を追加する。
- `permissions``{ kind = "host_api", api = "websocket" }`
- `[[websocket]]` target declaration例: `scheme = "wss"`, `host = "example.com"`, `path_prefixes = ["/socket"]`
- template/CLI tests に、service template が `host_api.websocket` permission と matching `[[websocket]]` declaration を含むことの assertion を追加する。
- 必要なら README/docs で、enablement grants も同じ WebSocket target を明示的に許可する必要があることを補足する。
Good points:
- WIT は `handle-ingress`、ingress event envelope、`output_commands` / `websocket_send` 例を記述している。
- PDK は `PluginIngressEvent`、`ServiceOutput`、`ServiceOutputCommand`、`websocket_text()` / `websocket_send()` helper を追加し、runtime envelope 形状に概ね合っている。
- Service template は `recv(timeout)` polling loop を推奨しておらず、`handle_ingress` + `ServiceOutput::websocket_send` の形になっている。
- template `runtime.kind``wasm-component` で、raw `wasm` template 復活は見当たらない。
- `yoi plugin new rust-component-service` は既存の path-safety 方針上に追加されており、広い CLI redesign や runtime reimplementation は見当たらない。
Validation:
- reviewer は read-only 指示のため cargo/nix/git diff validation は再実行していない。
- Ticket item/thread、IntentPacket、`cd200630..HEAD` の変更、WIT / PDK / template / CLI / docs / runtime envelope 関連箇所を静的確認した。
---
<!-- event: implementation_report author: yoi-coder-00001KVXK0WEA-pdk-service-events at: 2026-06-25T07:53:08Z -->
## Implementation report
Follow-up implementation report after review request_changes:
対応内容:
- Reviewer blocker に対応し、Service template の `websocket_send` output command に必要な runtime authority を追加した。
変更点:
- `resources/plugin/templates/rust-component-instance/plugin.toml`
- `permissions``{ kind = "host_api", api = "websocket" }` を追加。
- matching target として `[[websocket]]` を追加。
- `scheme = "wss"`
- `host = "example.com"`
- `path_prefixes = ["/socket"]`
- `crates/plugin-pdk/tests/template.rs`
- service template manifest に `host_api.websocket` permission が含まれることを検証。
- `[[websocket]]` target が `wss://example.com/socket` に対応していることを検証。
- `crates/yoi/src/plugin_cli.rs`
- `yoi plugin new rust-component-service ...` で生成される manifest に `host_api.websocket` permission / `[[websocket]]` declaration が含まれることを CLI test に追加。
- `resources/plugin/templates/rust-component-instance/README.md`
- manifest 側の `host_api.websocket` / `[[websocket]]` declaration と、enablement grants 側でも同じ WebSocket target を明示許可する必要があることを追記。
- `docs/development/plugin-development.md`
- Service ingress / output command の minimal manifest example に `host_api.websocket` permission と `[[websocket]]` target を追加。
- `websocket_send` output command には manifest declaration だけでなく runtime enablement grants も必要であることを明記。
Validation:
- `cargo test -p yoi-plugin-pdk`: success
- `cargo test -p yoi plugin`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success (`nix-exit:0`)
Commit:
- `6c8998878dc1d59ad70da3280efd503a9a060ebb fix: declare plugin service websocket authority`
Residual risks / notes:
- なし。runtime envelope の再設計、raw wasm compatibility、protocol-specific integration、secret/reconnect policy、runtime reimplementation は行っていない。
---
<!-- event: review author: yoi-reviewer-00001KVXK0WEA-pdk-service-events at: 2026-06-25T07:55:26Z status: approve -->
## Review: approve
Review result: approve
前回 blocker は解消されている。
確認した証拠:
- `resources/plugin/templates/rust-component-instance/plugin.toml`
- `permissions``{ kind = "host_api", api = "websocket" }` が追加済み。
- `[[websocket]]` target が追加済みで、`wss://example.com/socket` と一致する `scheme = "wss"`, `host = "example.com"`, `path_prefixes = ["/socket"]` を宣言している。
- `runtime.kind = "wasm-component"` のままで、raw `wasm` 互換は導入されていない。
- Service template README:
- `websocket_send` には manifest の `host_api.websocket` と matching `[[websocket]]` target が必要で、enablement grants も同じ target を許可する必要がある、と明記済み。
- Development docs:
- Service ingress/output command 節で `websocket_send` に必要な `host_api.websocket` permission / `[[websocket]]` target / enablement grant 境界を説明している。
- polling `recv(timeout)` loop ではなく ingress event / output command pattern を推奨する説明を維持している。
- Tests:
- PDK template test が service template manifest の websocket permission と matching target を assert するよう更新されている。
- CLI `plugin new rust-component-service` test も生成 manifest に websocket permission / `[[websocket]]` / host / path_prefix が含まれることを assert している。
リスク確認:
- 前回懸念した「template は `ServiceOutput::websocket_send` を返すのに manifest authority が足りない」不整合は修正済み。
- 今回差分は template/docs/tests の authority declaration 補正に閉じており、runtime redesign、protocol-specific integration、secret injection、raw-WASM compatibility の追加は見当たらない。
検証:
- reviewer は read-only 指定のため `cargo` / `nix` / `git diff --check` は再実行していない。
- `7a4fd975..HEAD` と fix commit `6c899887` の静的レビューのみ実施。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T07:57:03Z -->
## Implementation report
Integration outcome:
- Coder `yoi-coder-00001KVXK0WEA-pdk-service-events` が implementation branch `work/00001KVXK0WEA-plugin-pdk-service-events` に実装と review fix を commit した。
- implementation commit: `7a4fd975 feat: update plugin service authoring templates`
- fix commit: `6c899887 fix: declare plugin service websocket authority`
- Reviewer `yoi-reviewer-00001KVXK0WEA-pdk-service-events` は初回 review で request_changes。Service template が `ServiceOutput::websocket_send` を返す一方で `host_api.websocket` permission と matching `[[websocket]]` target declaration を持たない点を blocker とした。
- Coder は service template manifest / tests / docs を修正し、follow-up review は `approve`。template authority declaration、enablement grants guidance、runtime envelope alignment、raw-WASM compatibility 非復活を確認した。
- Orchestrator worktree `/home/hare/Projects/yoi/.worktree/orchestration` で branch `work/00001KVXK0WEA-plugin-pdk-service-events` を merge 済み。
- merge commit: `8d4fee23 merge: 00001KVXK0WEA plugin pdk service events`
Implemented behavior:
- WIT docs/descriptionsを Service ingress event JSON と `output_commands` / `websocket_send` authoring に合わせて更新。
- `crates/plugin-pdk``PluginIngressEvent` 拡張、`ServiceOutput` / `ServiceOutputCommand` / `ServiceOutputCommandKind`、`websocket_text` / `websocket_send` helpers を追加。
- `Plugin::handle_ingress` returns `ServiceOutput`
- `resources/plugin/templates/rust-component-instance` を service-oriented template に更新し、`handle_ingress` + `ServiceOutput::websocket_send` pattern を示す。
- service template `plugin.toml``runtime.kind = "wasm-component"`、`host_api.websocket` permission、matching `[[websocket]]` target を含む。
- `yoi plugin new rust-component-service <path>` を追加し、new/check/pack consistency tests を更新。
- docs は long-running guest `recv(timeout)` loop ではなく host-owned ingress event + output command model を推奨し、enablement grants でも matching websocket target が必要と明記。
Validation in Orchestrator worktree:
- `cargo test -p yoi-plugin-pdk`: success
- `cargo test -p yoi plugin`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
- Template cargo-check is covered by `cargo test -p yoi-plugin-pdk` using temporary target dirs.
Notes:
- This Ticket completes the queued Plugin runtime redesign chain slices that were split from the original broad redesign request.
- No runtime envelope redesign, protocol-specific integration, secret injection, reconnect policy, runtime reimplementation, or raw-WASM compatibility was introduced.
Next action:
- Mark Ticket done after this integration/validation evidence.
- Then stop related child Pods and remove only the child implementation worktree/branch.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T07:57:15Z from: inprogress to: done reason: merged_validated field: state -->
## State changed
Implementation is merged into the orchestration branch and validated.
Evidence:
- merge commit: `8d4fee23 merge: 00001KVXK0WEA plugin pdk service events`
- reviewer result: approve after requested changes were fixed
- validation in `/home/hare/Projects/yoi/.worktree/orchestration` succeeded:
- `cargo test -p yoi-plugin-pdk`
- `cargo test -p yoi plugin`
- `cargo check -p yoi`
- `git diff --check`
- `nix build .#yoi --no-link`
Closure is not performed here; this state records implementation completion after merge/validation.
--- ---

67
Cargo.lock generated
View File

@ -2913,7 +2913,6 @@ dependencies = [
"tracing", "tracing",
"tungstenite", "tungstenite",
"uuid", "uuid",
"wasmi",
"wasmtime", "wasmtime",
"wat", "wat",
"workflow", "workflow",
@ -4035,12 +4034,6 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@ -4053,16 +4046,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "string-interner"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0"
dependencies = [
"hashbrown 0.15.5",
"serde",
]
[[package]] [[package]]
name = "string_cache" name = "string_cache"
version = "0.8.9" version = "0.8.9"
@ -5096,56 +5079,6 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "wasmi"
version = "0.51.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb321403ce594274827657a908e13d1d9918aa02257b8bf8391949d9764023ff"
dependencies = [
"spin",
"wasmi_collections",
"wasmi_core",
"wasmi_ir",
"wasmparser 0.228.0",
]
[[package]]
name = "wasmi_collections"
version = "0.51.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9b8e98e45a2a534489f8225e765cbf1cb9a3078072605e58158910cf4749172"
dependencies = [
"string-interner",
]
[[package]]
name = "wasmi_core"
version = "0.51.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c25f375c0cdf14810eab07f532f61f14d4966f09c747a55067fdf3196e8512e6"
dependencies = [
"libm",
]
[[package]]
name = "wasmi_ir"
version = "0.51.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624e2a68a4293ecb8f564260b68394b29cf3b3edba6bce35532889a2cb33c3d9"
dependencies = [
"wasmi_core",
]
[[package]]
name = "wasmparser"
version = "0.228.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
dependencies = [
"bitflags 2.11.0",
"indexmap",
]
[[package]] [[package]]
name = "wasmparser" name = "wasmparser"
version = "0.244.0" version = "0.244.0"

View File

@ -560,13 +560,14 @@ impl PluginPackageManifest {
} }
} }
pub const PLUGIN_RUNTIME_WASM_KIND: &str = "wasm"; const LEGACY_PLUGIN_RUNTIME_WASM_KIND: &str = "wasm";
pub const PLUGIN_RUNTIME_WASM_ABI: &str = "yoi-plugin-wasm-1"; const LEGACY_PLUGIN_RUNTIME_WASM_ABI: &str = "yoi-plugin-wasm-1";
/// Manifest runtime kind for WebAssembly Component Model Tool packages. /// Manifest runtime kind for current WebAssembly Component Model packages.
/// ///
/// Component runtime manifests must set `component` to the packaged component /// Component runtime manifests must set `component` to the packaged component
/// artifact path and `world` to [`PLUGIN_COMPONENT_TOOL_WORLD`]. Raw core-Wasm /// artifact path and `world` to [`PLUGIN_COMPONENT_TOOL_WORLD`] or
/// packages remain explicit `kind = "wasm"` plus `abi = "yoi-plugin-wasm-1"`. /// [`PLUGIN_COMPONENT_INSTANCE_WORLD`]. Legacy raw core-Wasm manifests are
/// intentionally rejected by validation; `wasm-component` is the public runtime.
pub const PLUGIN_RUNTIME_COMPONENT_KIND: &str = "wasm-component"; pub const PLUGIN_RUNTIME_COMPONENT_KIND: &str = "wasm-component";
pub const PLUGIN_COMPONENT_TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0"; pub const PLUGIN_COMPONENT_TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0";
pub const PLUGIN_COMPONENT_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0"; pub const PLUGIN_COMPONENT_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0";
@ -1060,158 +1061,6 @@ pub fn resolve_plugin_config_for_startup(
snapshot snapshot
} }
/// Load the recorded WASM runtime module for a resolved plugin package.
///
/// Restore and execution paths use this helper instead of reading arbitrary
/// package paths directly so module selection remains tied to the resolved
/// package identity, runtime manifest entry, and deterministic package digest.
pub fn read_resolved_plugin_runtime_module(
record: &ResolvedPluginRecord,
limits: &PluginDiscoveryLimits,
) -> Result<Vec<u8>, PluginDiagnostic> {
let runtime = record.manifest.runtime.as_ref().ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"resolved plugin package does not declare a WASM runtime",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
if runtime.kind != PLUGIN_RUNTIME_WASM_KIND {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
"plugin runtime kind is unsupported",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest));
}
if runtime.abi.as_deref() != Some(PLUGIN_RUNTIME_WASM_ABI) {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Api,
PluginDiagnosticPhase::Manifest,
"plugin WASM ABI is unsupported",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest));
}
let entry = runtime.entry.as_deref().ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"plugin WASM runtime entry is required",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
let metadata = fs::metadata(&record.package_path).map_err(|error| {
PluginDiagnostic::new(
PluginDiagnosticKind::Io,
PluginDiagnosticPhase::Discovery,
format!(
"resolved plugin package metadata could not be read: {}",
safe_io_error(&error)
),
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
if !metadata.is_file() {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Malformed,
PluginDiagnosticPhase::Discovery,
"resolved plugin package is not a regular file",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest));
}
if metadata.len() > limits.max_package_size_bytes {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Bounds,
PluginDiagnosticPhase::Discovery,
"resolved plugin package exceeds the configured package size bound",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest));
}
let bytes = fs::read(&record.package_path).map_err(|error| {
PluginDiagnostic::new(
PluginDiagnosticKind::Io,
PluginDiagnosticPhase::Discovery,
format!(
"resolved plugin package content could not be read: {}",
safe_io_error(&error)
),
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
let archive = parse_stored_zip(&bytes, &record.package_label, record.source, limits)?;
let actual_digest = deterministic_digest(&archive.files);
if !digest_matches(&record.digest, &actual_digest) {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Digest,
PluginDiagnosticPhase::Resolution,
"resolved plugin package digest does not match current package content",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(actual_digest));
}
validate_manifest_path(
entry,
&archive,
&record.package_label,
record.source,
&record.manifest.id,
)?;
let normalized = normalize_archive_path(entry).ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Traversal,
PluginDiagnosticPhase::Manifest,
"plugin manifest references a path outside the package root",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})?;
archive.files.get(&normalized).cloned().ok_or_else(|| {
PluginDiagnostic::new(
PluginDiagnosticKind::Missing,
PluginDiagnosticPhase::Manifest,
"plugin runtime module entry is missing from the package",
)
.with_source(record.source)
.with_identity(&record.identity)
.with_package(&record.package_label)
.with_digest(&record.digest)
})
}
/// Reads the WebAssembly Component Model artifact selected by a resolved plugin /// Reads the WebAssembly Component Model artifact selected by a resolved plugin
/// package manifest while preserving package digest pinning. /// package manifest while preserving package digest pinning.
pub fn read_resolved_plugin_runtime_component( pub fn read_resolved_plugin_runtime_component(
@ -2046,38 +1895,21 @@ fn validate_manifest(
} }
if let Some(runtime) = &manifest.runtime { if let Some(runtime) = &manifest.runtime {
match runtime.kind.as_str() { match runtime.kind.as_str() {
PLUGIN_RUNTIME_WASM_KIND => { LEGACY_PLUGIN_RUNTIME_WASM_KIND => {
if runtime.abi.as_deref() != Some(PLUGIN_RUNTIME_WASM_ABI) { return Err(PluginDiagnostic::new(
return Err(PluginDiagnostic::new( PluginDiagnosticKind::Api,
PluginDiagnosticKind::Api, PluginDiagnosticPhase::Manifest,
PluginDiagnosticPhase::Manifest, format!(
"plugin WASM ABI is unsupported", "legacy raw wasm plugin runtime `{LEGACY_PLUGIN_RUNTIME_WASM_KIND}` / `{}` is retired; use `{PLUGIN_RUNTIME_COMPONENT_KIND}`",
) runtime
.with_source(source) .abi
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone())) .as_deref()
.with_package(label)); .unwrap_or(LEGACY_PLUGIN_RUNTIME_WASM_ABI)
} ),
let Some(entry) = runtime.entry.as_deref() else { )
return Err(PluginDiagnostic::new( .with_source(source)
PluginDiagnosticKind::Missing, .with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
PluginDiagnosticPhase::Manifest, .with_package(label));
"plugin WASM runtime entry is required",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
};
if runtime.component.is_some() || runtime.world.is_some() {
return Err(PluginDiagnostic::new(
PluginDiagnosticKind::Malformed,
PluginDiagnosticPhase::Manifest,
"plugin WASM runtime must not declare component metadata",
)
.with_source(source)
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
.with_package(label));
}
validate_manifest_path(entry, archive, label, source, &manifest.id)?;
} }
PLUGIN_RUNTIME_COMPONENT_KIND => { PLUGIN_RUNTIME_COMPONENT_KIND => {
if runtime.abi.is_some() || runtime.entry.is_some() { if runtime.abi.is_some() || runtime.entry.is_some() {
@ -2844,6 +2676,51 @@ description = "bad"
assert!(err.message.contains("service/ingress")); assert!(err.message.contains("service/ingress"));
} }
#[test]
fn legacy_raw_wasm_runtime_manifest_is_rejected() {
let temp = TempDir::new().unwrap();
let workspace = temp.path().join("workspace");
let plugins = workspace.join(".yoi/plugins");
fs::create_dir_all(&plugins).unwrap();
let manifest = r#"
schema_version = 1
id = "legacy"
name = "Legacy"
version = "0.1.0"
surfaces = ["tool"]
[runtime]
kind = "wasm"
entry = "plugin.wasm"
abi = "yoi-plugin-wasm-1"
[[tools]]
name = "Echo"
description = "legacy"
input_schema = { type = "object" }
"#;
write_stored_zip(
&plugins.join("legacy.yoi-plugin"),
&[
("plugin.toml", manifest.as_bytes().to_vec(), 0),
("plugin.wasm", b"not wasm".to_vec(), 0),
],
);
let report = discover_plugins(&PluginDiscoveryOptions::new(&workspace));
assert!(report.packages.is_empty());
let diagnostic = report
.diagnostics
.iter()
.find(|diag| diag.kind == PluginDiagnosticKind::Api)
.unwrap();
assert_eq!(diagnostic.phase, PluginDiagnosticPhase::Manifest);
assert_eq!(diagnostic.identity.as_deref(), Some("project:legacy"));
assert!(diagnostic.message.contains("legacy raw wasm"));
assert!(diagnostic.message.contains(PLUGIN_RUNTIME_COMPONENT_KIND));
}
#[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();
@ -3521,9 +3398,9 @@ version = "1.0.0"
surfaces = ["tool"] surfaces = ["tool"]
[runtime] [runtime]
kind = "wasm" kind = "wasm-component"
entry = "plugin.wasm" component = "plugin.component.wasm"
abi = "yoi-plugin-wasm-1" world = "yoi:plugin/tool@1.0.0"
[[permissions]] [[permissions]]
kind = "host_api" kind = "host_api"

View File

@ -468,15 +468,48 @@ mod tests {
assert!(error.message().len() <= MAX_ERROR_MESSAGE_BYTES + "".len()); assert!(error.message().len() <= MAX_ERROR_MESSAGE_BYTES + "".len());
} }
#[test]
fn service_output_helper_builds_runtime_command_envelope() {
let event = PluginIngressEvent {
kind: "websocket_text".to_string(),
source: "websocket:wss://example.test/socket".to_string(),
ingress_name: "example_ws".to_string(),
payload: json!({"text":"ping"}),
created_at: "2026-06-25T00:00:00Z".to_string(),
attempt: 1,
correlation_id: "event-1".to_string(),
};
assert_eq!(event.websocket_text(), Some("ping"));
let output =
ServiceOutput::websocket_send(&event, "reply-1", "wss://example.test/socket", "pong")
.unwrap();
let value = serde_json::to_value(output).unwrap();
assert_eq!(value["accepted"], true);
assert_eq!(value["output_commands"][0]["source_event_id"], "event-1");
assert_eq!(value["output_commands"][0]["command_id"], "reply-1");
assert_eq!(value["output_commands"][0]["kind"], "websocket_send");
assert_eq!(value["output_commands"][0]["payload"]["text"], "pong");
assert_eq!(
value["output_commands"][0]["requested_at"],
"2026-06-25T00:00:00Z"
);
}
#[test] #[test]
fn wit_constants_match_current_world() { fn wit_constants_match_current_world() {
assert!(TOOL_WIT.contains("package yoi:plugin@1.0.0")); assert!(TOOL_WIT.contains("package yoi:plugin@1.0.0"));
assert!(TOOL_WIT.contains("world tool")); assert!(TOOL_WIT.contains("world tool"));
assert!(TOOL_WIT.contains("export call")); assert!(TOOL_WIT.contains("export call"));
assert_eq!(TOOL_WORLD, "yoi:plugin/tool@1.0.0"); assert_eq!(TOOL_WORLD, "yoi:plugin/tool@1.0.0");
assert!(HOST_WIT.contains("interface https")); assert!(HOST_WIT.contains("interface request"));
assert!(HOST_WIT.contains("interface websocket"));
assert!(HOST_WIT.contains("interface fs")); assert!(HOST_WIT.contains("interface fs"));
assert!(HOST_WIT.contains("%list: func")); assert!(HOST_WIT.contains("%list: func"));
assert!(INSTANCE_WIT.contains("world instance"));
assert!(INSTANCE_WIT.contains("export handle-ingress"));
assert!(INSTANCE_WIT.contains("websocket_send"));
} }
} }
@ -488,12 +521,48 @@ pub const PLUGIN_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0";
pub const INSTANCE_WIT: &str = pub const INSTANCE_WIT: &str =
include_str!("../../../resources/plugin/wit/yoi-plugin-instance-v1.wit"); include_str!("../../../resources/plugin/wit/yoi-plugin-instance-v1.wit");
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PluginIngressEvent { pub struct PluginIngressEvent {
pub kind: String, pub kind: String,
pub source: String, pub source: String,
#[serde(default)] #[serde(default)]
pub ingress_name: String,
#[serde(default)]
pub payload: Value, pub payload: Value,
#[serde(default)]
pub created_at: String,
#[serde(default = "default_attempt")]
pub attempt: u32,
#[serde(default)]
pub correlation_id: String,
}
impl PluginIngressEvent {
/// Return the text payload carried by a host-owned WebSocket ingress event.
pub fn websocket_text(&self) -> Option<&str> {
self.payload.get("text").and_then(Value::as_str)
}
/// Build a `websocket_send` output command that replies through the
/// host-owned Service WebSocket driver. The host still validates the target
/// URL and matching grants before sending.
pub fn websocket_send(
&self,
command_id: impl Into<String>,
url: impl Into<String>,
text: impl Into<String>,
) -> Result<ServiceOutputCommand> {
ServiceOutputCommand::new(
self,
command_id,
ServiceOutputCommandKind::WebSocketSend,
serde_json::json!({ "url": url.into(), "text": text.into() }),
)
}
}
fn default_attempt() -> u32 {
1
} }
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
@ -519,12 +588,118 @@ impl PluginStatus {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ServiceOutputCommandKind {
DiagnosticStatusUpdate,
HostRequestDispatch,
#[serde(rename = "websocket_send")]
WebSocketSend,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ServiceOutputCommand {
pub correlation_id: String,
pub source_event_id: String,
pub command_id: String,
pub kind: ServiceOutputCommandKind,
pub payload: Value,
pub requested_at: String,
}
impl ServiceOutputCommand {
pub fn new(
event: &PluginIngressEvent,
command_id: impl Into<String>,
kind: ServiceOutputCommandKind,
payload: impl Serialize,
) -> Result<Self> {
let command_id = sanitize_command_id(command_id.into());
if command_id.is_empty() {
return Err(ToolError::invalid_output(
"service output command_id must not be empty",
));
}
let source_event_id = event.correlation_id.clone();
if source_event_id.is_empty() {
return Err(ToolError::invalid_output(
"service output command requires ingress event correlation_id",
));
}
let requested_at = if event.created_at.is_empty() {
"1970-01-01T00:00:00Z".to_string()
} else {
event.created_at.clone()
};
Ok(Self {
correlation_id: sanitize_command_id(format!("{source_event_id}:{command_id}")),
source_event_id,
command_id,
kind,
payload: serde_json::to_value(payload).map_err(ToolError::serialization)?,
requested_at,
})
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ServiceOutput {
#[serde(default)]
pub accepted: bool,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub data: Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub output_commands: Vec<ServiceOutputCommand>,
}
impl ServiceOutput {
pub fn accepted(data: impl Serialize) -> Result<Self> {
Ok(Self {
accepted: true,
data: serde_json::to_value(data).map_err(ToolError::serialization)?,
output_commands: Vec::new(),
})
}
pub fn empty() -> Self {
Self {
accepted: true,
data: Value::Null,
output_commands: Vec::new(),
}
}
pub fn with_command(mut self, command: ServiceOutputCommand) -> Self {
self.output_commands.push(command);
self
}
pub fn websocket_send(
event: &PluginIngressEvent,
command_id: impl Into<String>,
url: impl Into<String>,
text: impl Into<String>,
) -> Result<Self> {
Ok(Self::empty().with_command(event.websocket_send(command_id, url, text)?))
}
}
fn sanitize_command_id(value: String) -> String {
bounded_text(
value
.chars()
.map(|ch| if ch.is_control() { '-' } else { ch })
.collect(),
128,
)
}
/// Rust-facing instance Plugin contract. Hosts call `start` once, then route /// Rust-facing instance Plugin contract. Hosts call `start` once, then route
/// Tool/Ingress surfaces through the same mutable instance. /// Tool/Ingress surfaces through the same mutable instance.
pub trait Plugin: Sized + 'static { pub trait Plugin: Sized + 'static {
fn start(config: Value) -> Result<Self>; fn start(config: Value) -> Result<Self>;
fn handle_tool(&mut self, name: &str, input: Value) -> Result<ToolOutput>; fn handle_tool(&mut self, name: &str, input: Value) -> Result<ToolOutput>;
fn handle_ingress(&mut self, name: &str, event: PluginIngressEvent) -> Result<Value>; fn handle_ingress(&mut self, name: &str, event: PluginIngressEvent) -> Result<ServiceOutput>;
fn status(&self) -> Result<PluginStatus> { fn status(&self) -> Result<PluginStatus> {
Ok(PluginStatus::ready(Value::Null)) Ok(PluginStatus::ready(Value::Null))
} }

View File

@ -12,6 +12,14 @@ const TEMPLATE_PLUGIN: &str =
include_str!("../../../resources/plugin/templates/rust-component-tool/plugin.toml"); include_str!("../../../resources/plugin/templates/rust-component-tool/plugin.toml");
const TEMPLATE_README: &str = const TEMPLATE_README: &str =
include_str!("../../../resources/plugin/templates/rust-component-tool/README.md"); include_str!("../../../resources/plugin/templates/rust-component-tool/README.md");
const SERVICE_TEMPLATE_CARGO: &str =
include_str!("../../../resources/plugin/templates/rust-component-instance/Cargo.toml");
const SERVICE_TEMPLATE_LIB: &str =
include_str!("../../../resources/plugin/templates/rust-component-instance/src/lib.rs");
const SERVICE_TEMPLATE_PLUGIN: &str =
include_str!("../../../resources/plugin/templates/rust-component-instance/plugin.toml");
const SERVICE_TEMPLATE_README: &str =
include_str!("../../../resources/plugin/templates/rust-component-instance/README.md");
const SAMPLE_LIB: &str = include_str!("../../../docs/examples/plugin-component-tool/lib.rs"); const SAMPLE_LIB: &str = include_str!("../../../docs/examples/plugin-component-tool/lib.rs");
const PDK_CARGO: &str = include_str!("../Cargo.toml"); const PDK_CARGO: &str = include_str!("../Cargo.toml");
@ -25,7 +33,7 @@ fn rust_component_tool_template_has_expected_files() {
cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(), cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(),
Some("../../../../crates/plugin-pdk") Some("../../../../crates/plugin-pdk")
); );
assert!(TEMPLATE_CARGO.contains("rev = \"<pinned-yoi-revision>\"")); assert!(TEMPLATE_CARGO.contains("rev = \"<pinned-yoi-commit-sha>\""));
let plugin: Value = toml::from_str(TEMPLATE_PLUGIN).expect("template plugin.toml parses"); 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["schema_version"].as_integer(), Some(1));
@ -45,6 +53,71 @@ fn rust_component_tool_template_has_expected_files() {
assert!(TEMPLATE_README.contains("Component Model Tool Plugin")); assert!(TEMPLATE_README.contains("Component Model Tool Plugin"));
} }
#[test]
fn rust_component_service_template_has_event_output_pattern() {
let cargo: Value = toml::from_str(SERVICE_TEMPLATE_CARGO).expect("service Cargo.toml parses");
assert_eq!(cargo["package"]["edition"].as_str(), Some("2024"));
assert_eq!(cargo["lib"]["crate-type"][0].as_str(), Some("cdylib"));
assert_eq!(
cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(),
Some("../../../../crates/plugin-pdk")
);
assert!(SERVICE_TEMPLATE_CARGO.contains("rev = \"<pinned-yoi-commit-sha>\""));
let plugin: Value =
toml::from_str(SERVICE_TEMPLATE_PLUGIN).expect("service 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/instance@1.0.0")
);
assert!(
plugin["permissions"]
.as_array()
.expect("permissions array")
.iter()
.any(|permission| permission["kind"].as_str() == Some("host_api")
&& permission["api"].as_str() == Some("websocket"))
);
assert_eq!(
plugin["services"].as_array().expect("services array").len(),
1
);
let ingress = &plugin["ingresses"].as_array().expect("ingresses array")[0];
assert!(
ingress["event_kinds"]
.as_array()
.expect("event kinds")
.iter()
.any(|kind| kind.as_str() == Some("websocket_text"))
);
assert!(
ingress["sources"]
.as_array()
.expect("sources")
.iter()
.any(|source| source.as_str() == Some("websocket:wss://example.com/socket"))
);
let websocket = &plugin["websocket"].as_array().expect("websocket targets")[0];
assert_eq!(websocket["scheme"].as_str(), Some("wss"));
assert_eq!(websocket["host"].as_str(), Some("example.com"));
assert!(
websocket["path_prefixes"]
.as_array()
.expect("websocket path prefixes")
.iter()
.any(|prefix| prefix.as_str() == Some("/socket"))
);
assert!(SERVICE_TEMPLATE_LIB.contains("world: \"instance\""));
assert!(SERVICE_TEMPLATE_LIB.contains("PluginIngressEvent"));
assert!(SERVICE_TEMPLATE_LIB.contains("ServiceOutput::websocket_send"));
assert!(!SERVICE_TEMPLATE_LIB.contains("recv(timeout"));
assert!(SERVICE_TEMPLATE_README.contains("output command"));
assert!(!SERVICE_TEMPLATE_PLUGIN.contains("kind = \"wasm\""));
}
#[test] #[test]
fn documented_sample_uses_pdk_component_path() { fn documented_sample_uses_pdk_component_path() {
assert!(SAMPLE_LIB.contains("use yoi_plugin_pdk::wit_bindgen")); assert!(SAMPLE_LIB.contains("use yoi_plugin_pdk::wit_bindgen"));
@ -57,8 +130,17 @@ fn documented_sample_uses_pdk_component_path() {
#[test] #[test]
fn embedded_template_cargo_checks_for_wasm_target() { fn embedded_template_cargo_checks_for_wasm_target() {
cargo_check_template("rust-component-tool");
}
#[test]
fn embedded_service_template_cargo_checks_for_wasm_target() {
cargo_check_template("rust-component-instance");
}
fn cargo_check_template(template: &str) {
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")); let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let template_dir = crate_dir.join("../../resources/plugin/templates/rust-component-tool"); let template_dir = crate_dir.join(format!("../../resources/plugin/templates/{template}"));
let manifest_path = template_dir.join("Cargo.toml"); let manifest_path = template_dir.join("Cargo.toml");
let lock_path = template_dir.join("Cargo.lock"); let lock_path = template_dir.join("Cargo.lock");
let _ = fs::remove_file(&lock_path); let _ = fs::remove_file(&lock_path);

View File

@ -37,7 +37,6 @@ workflow-crate = { package = "workflow", path = "../workflow" }
uuid = { workspace = true, features = ["v7"] } uuid = { workspace = true, features = ["v7"] }
session-metrics = { workspace = true } session-metrics = { workspace = true }
arc-swap = "1.9.1" arc-swap = "1.9.1"
wasmi = { version = "0.51.1", default-features = false, features = ["std", "extra-checks"] }
wasmtime = { version = "45.0.2", default-features = false, features = ["std", "runtime", "cranelift", "component-model"] } wasmtime = { version = "45.0.2", default-features = false, features = ["std", "runtime", "cranelift", "component-model"] }
tungstenite = { version = "0.28.0", default-features = false, features = ["handshake", "native-tls", "url"] } tungstenite = { version = "0.28.0", default-features = false, features = ["handshake", "native-tls", "url"] }
tokio-tungstenite = { version = "0.28.0", default-features = false, features = ["native-tls", "connect"] } tokio-tungstenite = { version = "0.28.0", default-features = false, features = ["native-tls", "connect"] }

File diff suppressed because it is too large Load Diff

View File

@ -727,7 +727,7 @@ fn parse_plugin_pack_args(
} }
fn plugin_usage() -> &'static str { fn plugin_usage() -> &'static str {
"usage: yoi plugin new rust-component-tool <path-or-name> [--json]\n yoi plugin check <path-or-package> [--json]\n yoi plugin pack <path> [--output <file>] [--json]\n yoi plugin list [--workspace PATH] [--profile REF] [--json]\n yoi plugin show <ref> [--workspace PATH] [--profile REF] [--json]" "usage: yoi plugin new <rust-component-tool|rust-component-service> <path-or-name> [--json]\n yoi plugin check <path-or-package> [--json]\n yoi plugin pack <path> [--output <file>] [--json]\n yoi plugin list [--workspace PATH] [--profile REF] [--json]\n yoi plugin show <ref> [--workspace PATH] [--profile REF] [--json]"
} }
fn parse_mcp_args(args: &[String]) -> Result<mcp_cli::McpCliCommand, ParseError> { fn parse_mcp_args(args: &[String]) -> Result<mcp_cli::McpCliCommand, ParseError> {
@ -901,7 +901,7 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
fn print_help() { fn print_help() {
println!( println!(
"yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi pod delete <NAME> [--force] [--dry-run]\n yoi pod prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace serve [OPTIONS]\n yoi plugin new rust-component-tool <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, yoi resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n --workspace <PATH> Runtime workspace root for default Console/--pod (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n" "yoi\n\nUsage:\n yoi [OPTIONS]\n yoi resume [--workspace <PATH>] [--all]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi pod delete <NAME> [--force] [--dry-run]\n yoi pod prune --older-than <DURATION> [--force] [--dry-run]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi session prune --unreferenced [--older-than <DURATION>] [--force] [--dry-run]\n yoi ticket <COMMAND> [OPTIONS]\n yoi workspace serve [OPTIONS]\n yoi plugin new <rust-component-tool|rust-component-service> <PATH> [--json]\n yoi plugin check <PATH_OR_PACKAGE> [--json]\n yoi plugin pack <PATH> [--output <FILE>] [--json]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, yoi resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n --workspace <PATH> Runtime workspace root for default Console/--pod (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
); );
} }

View File

@ -9,10 +9,10 @@ use manifest::plugin::{
MaterializedPluginPackage, PluginConfig, PluginDiagnostic, PluginDiagnosticKind, MaterializedPluginPackage, PluginConfig, PluginDiagnostic, PluginDiagnosticKind,
PluginDiagnosticPhase, PluginDiscoveryLimits, PluginDiscoveryOptions, PluginDiscoveryReport, PluginDiagnosticPhase, PluginDiscoveryLimits, PluginDiscoveryOptions, PluginDiscoveryReport,
PluginExactVersion, PluginGrantConfig, PluginPackageManifest, PluginPermission, PluginExactVersion, PluginGrantConfig, PluginPackageManifest, PluginPermission,
PluginResolution, PluginSourceKind, PluginSurface, RUST_COMPONENT_TOOL_TEMPLATE, PluginResolution, PluginSourceKind, PluginSurface, PluginTemplateResource,
ResolvedPlugin, ResolvedPluginRecord, SourceQualifiedPluginId, discover_plugins, RUST_COMPONENT_INSTANCE_TEMPLATE, RUST_COMPONENT_TOOL_TEMPLATE, ResolvedPlugin,
read_plugin_directory, read_plugin_package_file, resolve_enabled_plugins, ResolvedPluginRecord, SourceQualifiedPluginId, discover_plugins, read_plugin_directory,
write_plugin_package_file, read_plugin_package_file, resolve_enabled_plugins, write_plugin_package_file,
}; };
use manifest::{ProfileResolveOptions, ProfileResolver, ProfileSelector, paths}; use manifest::{ProfileResolveOptions, ProfileResolver, ProfileSelector, paths};
use pod::feature::plugin::{PluginStaticInspection, inspect_resolved_plugin_static}; use pod::feature::plugin::{PluginStaticInspection, inspect_resolved_plugin_static};
@ -85,27 +85,29 @@ pub(crate) fn run(command: PluginCliCommand) -> Result<()> {
} }
fn render_new(template: &str, destination: &Path, args: &PluginCliArgs) -> Result<String> { fn render_new(template: &str, destination: &Path, args: &PluginCliArgs) -> Result<String> {
if template != "rust-component-tool" { let (template_name, resources) = embedded_template_resources(template)?;
return Err(format!( materialize_template(destination, resources)?;
"unsupported plugin template `{template}` (supported: rust-component-tool)" let mut next_steps = vec![
) "Review plugin.toml and generated Rust source.".to_string(),
.into()); "Replace the placeholder plugin.component.wasm with a real built component before enabling or execution.".to_string(),
"Run `yoi plugin check <path>` and then `yoi plugin pack <path>`.".to_string(),
];
if template == "rust-component-service" {
next_steps.insert(
1,
"Implement Service ingress logic in handle_ingress and return ServiceOutput output_commands for host-owned WebSocket sends.".to_string(),
);
} }
materialize_template(destination)?;
let report = NewReport { let report = NewReport {
command: "new", command: "new",
template: "rust-component-tool", template: template_name,
destination: destination.display().to_string(), destination: destination.display().to_string(),
files: RUST_COMPONENT_TOOL_TEMPLATE files: resources
.iter() .iter()
.map(|resource| resource.path.to_string()) .map(|resource| resource.path.to_string())
.collect(), .collect(),
safety: AuthoringSafetyReport::default(), safety: AuthoringSafetyReport::default(),
next_steps: vec![ next_steps,
"Review plugin.toml and generated Rust source.".to_string(),
"Replace the placeholder plugin.component.wasm with a real built component before enabling or execution.".to_string(),
"Run `yoi plugin check <path>` and then `yoi plugin pack <path>`.".to_string(),
],
}; };
if args.json { if args.json {
return Ok(format!("{}\n", serde_json::to_string_pretty(&report)?)); return Ok(format!("{}\n", serde_json::to_string_pretty(&report)?));
@ -113,7 +115,23 @@ fn render_new(template: &str, destination: &Path, args: &PluginCliArgs) -> Resul
render_new_human(&report) render_new_human(&report)
} }
fn materialize_template(destination: &Path) -> Result<()> { fn embedded_template_resources(
template: &str,
) -> Result<(&'static str, &'static [PluginTemplateResource])> {
match template {
"rust-component-tool" => Ok(("rust-component-tool", RUST_COMPONENT_TOOL_TEMPLATE)),
"rust-component-service" => Ok(("rust-component-service", RUST_COMPONENT_INSTANCE_TEMPLATE)),
_ => Err(format!(
"unsupported plugin template `{template}` (supported: rust-component-tool, rust-component-service)"
)
.into()),
}
}
fn materialize_template(
destination: &Path,
resources: &'static [PluginTemplateResource],
) -> Result<()> {
match fs::symlink_metadata(destination) { match fs::symlink_metadata(destination) {
Ok(metadata) => { Ok(metadata) => {
if metadata.file_type().is_symlink() { if metadata.file_type().is_symlink() {
@ -144,7 +162,7 @@ fn materialize_template(destination: &Path) -> Result<()> {
Err(error) => return Err(error.into()), Err(error) => return Err(error.into()),
} }
for resource in RUST_COMPONENT_TOOL_TEMPLATE { for resource in resources {
let relative = safe_template_relative_path(resource.path)?; let relative = safe_template_relative_path(resource.path)?;
let path = destination.join(relative); let path = destination.join(relative);
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
@ -1853,6 +1871,39 @@ mod tests {
); );
} }
#[test]
fn legacy_raw_wasm_package_is_rejected_not_active_or_eligible() {
let dir = tempdir().unwrap();
let workspace = dir.path();
fs::create_dir_all(workspace.join(".yoi/plugins")).unwrap();
write_stored_zip(
&workspace.join(".yoi/plugins/legacy.yoi-plugin"),
&[
("plugin.toml", plugin_legacy_manifest("legacy").as_bytes()),
("plugin.wasm", b"not wasm"),
],
);
let snapshot = inspect_snapshot(workspace, &PluginConfig::default());
let legacy = select_item(&snapshot, "project:legacy").unwrap();
assert_eq!(legacy.status, "rejected");
assert!(!legacy.discovered);
assert!(!legacy.configured);
assert!(legacy.enabled_surfaces.is_empty());
assert!(legacy.diagnostics.iter().any(|diagnostic| {
diagnostic.kind == "api"
&& diagnostic.message.contains("legacy raw wasm")
&& diagnostic.message.contains("wasm-component")
}));
let list_output = render_list_snapshot_human(&snapshot).unwrap();
assert!(list_output.contains("project:legacy [rejected]"));
assert!(!list_output.contains("project:legacy [active]"));
let show_output = render_item_human(legacy).unwrap();
assert!(show_output.contains("status: rejected"));
assert!(show_output.contains("legacy raw wasm"));
}
#[test] #[test]
fn configured_invalid_or_incompatible_package_is_rejected_not_missing() { fn configured_invalid_or_incompatible_package_is_rejected_not_missing() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
@ -1867,7 +1918,7 @@ mod tests {
&workspace.join(".yoi/plugins/incompat.yoi-plugin"), &workspace.join(".yoi/plugins/incompat.yoi-plugin"),
&[ &[
("plugin.toml", incompatible_manifest.as_bytes()), ("plugin.toml", incompatible_manifest.as_bytes()),
("plugin.wasm", b"not wasm"), ("plugin.component.wasm", b"not wasm"),
], ],
); );
let mut config = PluginConfig::default(); let mut config = PluginConfig::default();
@ -1934,7 +1985,7 @@ mod tests {
fs::create_dir_all(workspace.join(".yoi/plugins")).unwrap(); fs::create_dir_all(workspace.join(".yoi/plugins")).unwrap();
write_stored_zip( write_stored_zip(
&workspace.join(".yoi/plugins/no_manifest.yoi-plugin"), &workspace.join(".yoi/plugins/no_manifest.yoi-plugin"),
&[("plugin.wasm", b"not wasm")], &[("plugin.component.wasm", b"not wasm")],
); );
let missing_runtime_manifest = plugin_manifest_missing_runtime_entry("missing_runtime"); let missing_runtime_manifest = plugin_manifest_missing_runtime_entry("missing_runtime");
write_stored_zip( write_stored_zip(
@ -2103,6 +2154,65 @@ mod tests {
let human_check = render_check(&destination, &PluginCliArgs::default()).unwrap(); let human_check = render_check(&destination, &PluginCliArgs::default()).unwrap();
assert!(human_check.contains("[partial]")); assert!(human_check.contains("[partial]"));
assert!(human_check.contains("not ready to enable")); assert!(human_check.contains("not ready to enable"));
let service_destination = dir.path().join("my-service-plugin");
let service_json = render_new(
"rust-component-service",
&service_destination,
&PluginCliArgs {
json: true,
..PluginCliArgs::default()
},
)
.unwrap();
let service_value: serde_json::Value = serde_json::from_str(&service_json).unwrap();
assert_eq!(service_value["template"], "rust-component-service");
assert!(
service_value["next_steps"]
.as_array()
.unwrap()
.iter()
.any(|step| step
.as_str()
.unwrap_or_default()
.contains("Service ingress"))
);
for resource in RUST_COMPONENT_INSTANCE_TEMPLATE {
assert!(
service_destination.join(resource.path).is_file(),
"missing service {}",
resource.path
);
}
let manifest = fs::read_to_string(service_destination.join("plugin.toml")).unwrap();
assert!(manifest.contains("kind = \"wasm-component\""));
assert!(manifest.contains("[[services]]"));
assert!(manifest.contains("[[ingresses]]"));
assert!(manifest.contains("{ kind = \"host_api\", api = \"websocket\" }"));
assert!(manifest.contains("[[websocket]]"));
assert!(manifest.contains("host = \"example.com\""));
assert!(manifest.contains("path_prefixes = [\"/socket\"]"));
let source = fs::read_to_string(service_destination.join("src/lib.rs")).unwrap();
assert!(source.contains("ServiceOutput::websocket_send"));
assert!(!source.contains("recv(timeout"));
let service_check = render_check(&service_destination, &PluginCliArgs::default()).unwrap();
assert!(service_check.contains("plugin check:"));
assert!(service_check.contains("service"));
let service_package = dir.path().join("my-service-plugin.yoi-plugin");
let service_pack_json = render_pack(
&service_destination,
Some(&service_package),
&PluginCliArgs {
json: true,
..PluginCliArgs::default()
},
)
.unwrap();
let service_pack_value: serde_json::Value =
serde_json::from_str(&service_pack_json).unwrap();
assert_eq!(service_pack_value["status"], "packed");
assert!(service_package.is_file());
let error = render_new( let error = render_new(
"rust-component-tool", "rust-component-tool",
&destination, &destination,
@ -2140,7 +2250,7 @@ mod tests {
plugin_manifest("echo", "echo", "object", &["echo"]), plugin_manifest("echo", "echo", "object", &["echo"]),
) )
.unwrap(); .unwrap();
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap(); fs::write(plugin.join("plugin.component.wasm"), b"not wasm").unwrap();
let human = render_check(&plugin, &PluginCliArgs::default()).unwrap(); let human = render_check(&plugin, &PluginCliArgs::default()).unwrap();
assert!(human.contains("[active]")); assert!(human.contains("[active]"));
@ -2163,6 +2273,42 @@ mod tests {
assert_eq!(value["safety"]["no_plugin_execution"], true); assert_eq!(value["safety"]["no_plugin_execution"], true);
} }
#[test]
fn plugin_check_rejects_legacy_raw_wasm_package() {
let dir = tempdir().unwrap();
let plugin = dir.path().join("legacy");
fs::create_dir_all(&plugin).unwrap();
fs::write(plugin.join("plugin.toml"), plugin_legacy_manifest("legacy")).unwrap();
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
let report = build_check_report(&plugin);
assert_eq!(report.status, "rejected");
assert!(report.diagnostics.iter().any(|diagnostic| {
diagnostic.kind == "api"
&& diagnostic.message.contains("legacy raw wasm")
&& diagnostic.message.contains("wasm-component")
}));
let human = render_check_report(&report, &PluginCliArgs::default()).unwrap();
assert!(human.contains("[rejected]"));
assert!(human.contains("legacy raw wasm"));
let json = render_check_report(
&report,
&PluginCliArgs {
json: true,
..PluginCliArgs::default()
},
)
.unwrap();
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(value["status"], "rejected");
assert!(
value["diagnostics"][0]["message"]
.as_str()
.unwrap_or_default()
.contains("wasm-component")
);
}
#[test] #[test]
fn plugin_check_rejects_invalid_manifest_and_missing_runtime_artifact() { fn plugin_check_rejects_invalid_manifest_and_missing_runtime_artifact() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
@ -2228,7 +2374,7 @@ mod tests {
plugin_manifest("echo", "echo", "object", &["echo"]), plugin_manifest("echo", "echo", "object", &["echo"]),
) )
.unwrap(); .unwrap();
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap(); fs::write(plugin.join("plugin.component.wasm"), b"not wasm").unwrap();
let first = dir.path().join("first.yoi-plugin"); let first = dir.path().join("first.yoi-plugin");
let second = dir.path().join("second.yoi-plugin"); let second = dir.path().join("second.yoi-plugin");
@ -2466,9 +2612,9 @@ surfaces = ["tool"]
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}] permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}]
[runtime] [runtime]
kind = "wasm" kind = "wasm-component"
entry = "missing.wasm" component = "missing.component.wasm"
abi = "yoi-plugin-wasm-1" world = "yoi:plugin/tool@1.0.0"
[[tools]] [[tools]]
name = "Echo" name = "Echo"
@ -2500,9 +2646,9 @@ surfaces = ["tool"]
permissions = [{{ kind = "surface", surface = "tool" }}, {permissions}] permissions = [{{ kind = "surface", surface = "tool" }}, {permissions}]
[runtime] [runtime]
kind = "wasm" kind = "wasm-component"
entry = "plugin.wasm" component = "plugin.component.wasm"
abi = "yoi-plugin-wasm-1" world = "yoi:plugin/tool@1.0.0"
[[tools]] [[tools]]
name = "{tool_name}" name = "{tool_name}"
@ -2512,6 +2658,28 @@ input_schema = {{ type = "{schema_type}" }}
) )
} }
fn plugin_legacy_manifest(id: &str) -> String {
format!(
r#"
schema_version = 1
id = "{id}"
name = "{id}"
version = "0.1.0"
surfaces = ["tool"]
[runtime]
kind = "wasm"
entry = "plugin.wasm"
abi = "yoi-plugin-wasm-1"
[[tools]]
name = "Echo"
description = "Legacy raw wasm tool"
input_schema = {{ type = "object" }}
"#
)
}
fn write_plugin_package(workspace: &Path, id: &str) -> String { fn write_plugin_package(workspace: &Path, id: &str) -> String {
let manifest = format!( let manifest = format!(
r#" r#"
@ -2523,9 +2691,9 @@ surfaces = ["tool"]
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}] permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}]
[runtime] [runtime]
kind = "wasm" kind = "wasm-component"
entry = "plugin.wasm" component = "plugin.component.wasm"
abi = "yoi-plugin-wasm-1" world = "yoi:plugin/tool@1.0.0"
[[tools]] [[tools]]
name = "Echo" name = "Echo"
@ -2547,9 +2715,9 @@ surfaces = ["tool"]
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}, {{ kind = "tool", name = "Other" }}] permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}, {{ kind = "tool", name = "Other" }}]
[runtime] [runtime]
kind = "wasm" kind = "wasm-component"
entry = "plugin.wasm" component = "plugin.component.wasm"
abi = "yoi-plugin-wasm-1" world = "yoi:plugin/tool@1.0.0"
[[tools]] [[tools]]
name = "Echo" name = "Echo"
@ -2573,7 +2741,7 @@ input_schema = {{ type = "object" }}
&package, &package,
&[ &[
("plugin.toml", manifest.as_bytes()), ("plugin.toml", manifest.as_bytes()),
("plugin.wasm", b"not wasm"), ("plugin.component.wasm", b"not wasm"),
], ],
); );

View File

@ -1,8 +1,8 @@
# Plugin Component Model migration # Plugin Component Model migration
Yoi's current Plugin Tool runtime uses a narrow core-WebAssembly ABI. That was the right MVP shape because it made sandboxing, bounded input/output, and fail-closed host imports explicit. It should not become the long-term authoring interface. Yoi's original Plugin Tool runtime used a narrow core-WebAssembly ABI. That was the right MVP shape because it made sandboxing, bounded input/output, and fail-closed host imports explicit, but it is no longer the public authoring interface.
The preferred direction is to adopt the WebAssembly Component Model for Plugin Tool authoring and host APIs. Component Model adoption means Plugin interfaces are described as typed WIT worlds and lowered through the canonical ABI, instead of every Plugin author or SDK wrapper hand-writing pointer/length memory plumbing. The supported runtime kind is now `wasm-component`, using the WebAssembly Component Model for Plugin Tool authoring and host APIs. Component Model adoption means Plugin interfaces are described as typed WIT worlds and lowered through the canonical ABI, instead of every Plugin author or SDK wrapper hand-writing pointer/length memory plumbing.
## What Component Model changes ## What Component Model changes
@ -74,40 +74,21 @@ Adopting the Component Model must not change Yoi's authority model:
## Migration shape ## Migration shape
Yoi should support Component Model as an explicit runtime kind rather than silently changing existing raw-ABI packages. `runtime.kind = "wasm-component"` is the sole public Plugin runtime kind. Legacy raw core-Wasm declarations (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are rejected by manifest validation and are surfaced only as bounded diagnostics; they are not active/eligible Plugins and are not executed.
Possible manifest direction: The migration is now focused on the component surface:
```toml 1. Keep WIT packages/worlds for Tool Plugin and initial host APIs versioned under `resources/plugin/wit`.
[runtime] 2. Keep manifest/schema support centered on `runtime.kind = "wasm-component"`.
kind = "wasm-component" 3. Keep the component runtime backend and typed host import/export binding as the active execution path.
component = "plugin.component.wasm" 4. Port future host API designs to WIT-compatible interfaces.
world = "yoi:plugin/tool@1.0.0" 5. Keep the Rust PDK/template aligned with the component world.
```
The current raw core-Wasm runtime can remain explicit during migration:
```toml
[runtime]
kind = "wasm"
entry = "plugin.wasm"
abi = "yoi-plugin-wasm-1"
```
The migration should be phased:
1. Define WIT packages/worlds for Tool Plugin and initial host APIs.
2. Add manifest/schema support for `runtime.kind = "wasm-component"` without executing it during discovery.
3. Add a component runtime backend and typed host import/export binding.
4. Port `https` and `fs` host API designs to WIT-compatible interfaces.
5. Add a Rust PDK/template around the component world.
6. Decide whether the raw ABI remains supported, becomes legacy-only, or is deprecated after examples and tests move.
## Runtime/backend caution ## Runtime/backend caution
The current implementation uses `wasmi` for core Wasm. Component Model support will likely require a different backend or a significantly richer component adapter path, such as `wasmtime::component` plus generated bindings. That has consequences for binary size, Nix packaging, build time, runtime limits, and sandbox policy. The migration Ticket must measure and validate those effects explicitly. The legacy core-Wasm implementation used `wasmi` as a transitional backend. The active Plugin Tool runtime is now selected by package runtime metadata and executed through `wasmtime::component`; discovery and static inspection must continue to avoid executing package code.
If a component backend is added, keep it selected by package runtime metadata and Profile/feature policy. Do not make all Plugin packages depend on component execution during discovery or inspection. Keep the component backend selected by package runtime metadata and Profile/feature policy. Do not make all Plugin packages depend on component execution during discovery or inspection.
## Relationship to pending host APIs ## Relationship to pending host APIs
@ -133,21 +114,13 @@ component = "plugin.component.wasm"
world = "yoi:plugin/tool@1.0.0" world = "yoi:plugin/tool@1.0.0"
``` ```
The legacy core-Wasm ABI remains explicit and is not reinterpreted as a Legacy core-Wasm metadata is accepted only far enough to produce migration diagnostics: package checks and discovery reject `kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`, `list`/`show` report those packages as rejected rather than active/eligible, and the active runtime path does not execute them.
component:
```toml
[runtime]
kind = "wasm"
entry = "plugin.wasm"
abi = "yoi-plugin-wasm-1"
```
The component runtime uses `wasmtime::component` and expects the exported world The component runtime uses `wasmtime::component` and expects the exported world
`yoi:plugin/tool@1.0.0` with a `call(tool-name: string, input-json: string) -> `yoi:plugin/tool@1.0.0` with a `call(tool-name: string, input-json: string) ->
string` export. The returned string is the same ToolOutput JSON used by the raw string` export. The returned string is the normal ToolOutput JSON, so
runtime, so registration and execution still flow through the existing registration and execution still flow through the existing ToolRegistry and
ToolRegistry and Worker Tool-result history path. Worker Tool-result history path.
Host imports are stable names under `yoi:host/*@1.0.0`; the repository WIT files Host imports are stable names under `yoi:host/*@1.0.0`; the repository WIT files
live in `resources/plugin/wit/`. Importing `yoi:host/request@1.0.0` or live in `resources/plugin/wit/`. Importing `yoi:host/request@1.0.0` or

View File

@ -6,7 +6,7 @@ The initial goal is a durable `.yoi-plugin` package format that later Tickets ca
## Package shape ## Package shape
A `.yoi-plugin` file is a single-file archive. The initial archive format should be a constrained ZIP profile because it is easy to inspect without executing code and can carry text manifests, WASM modules, schemas, and license material. A `.yoi-plugin` file is a single-file archive. The archive format is a constrained ZIP profile because it is easy to inspect without executing code and can carry text manifests, WebAssembly Component Model modules, schemas, and license material.
The archive root must contain `plugin.toml` directly at the root. Packages should not require a wrapping directory whose name must match the plugin id. The archive root must contain `plugin.toml` directly at the root. Packages should not require a wrapping directory whose name must match the plugin id.
@ -14,7 +14,7 @@ Recommended root layout:
```text ```text
plugin.toml # required package manifest plugin.toml # required package manifest
module.wasm # optional; required when plugin.toml declares a WASM runtime plugin.component.wasm # required when plugin.toml declares the component runtime
hooks/*.toml # optional declarative hook definitions hooks/*.toml # optional declarative hook definitions
schemas/*.schema.json # optional JSON schemas for configuration or tool input/output schemas/*.schema.json # optional JSON schemas for configuration or tool input/output
README.md # recommended human description README.md # recommended human description
@ -43,16 +43,7 @@ id = "summary"
file = "hooks/summary.md" file = "hooks/summary.md"
``` ```
The package archive must contain both root `plugin.toml` and the referenced `hooks/summary.md` entry. Optional WASM metadata is accepted only for the declared future runtime boundary and is not executed: The package archive must contain both root `plugin.toml` and referenced runtime/content entries. Component runtime metadata is explicit and static inspection never executes the artifact:
```toml
[runtime]
kind = "wasm"
entry = "plugin.wasm"
abi = "yoi-plugin-wasm-1"
```
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]
@ -61,13 +52,15 @@ component = "plugin.component.wasm"
world = "yoi:plugin/tool@1.0.0" world = "yoi:plugin/tool@1.0.0"
``` ```
`wasm-component` is the public/recommended runtime kind, recorded in [Plugin Component Model migration](plugin-component-model.md). Legacy raw core-Wasm declarations (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are retired: manifest validation rejects them and CLI inspection reports the package as rejected rather than active/eligible.
First-pass fields accepted by the parser: First-pass fields accepted by the parser:
- `schema_version`: required integer; unsupported versions fail closed. - `schema_version`: required integer; unsupported versions fail closed.
- `id`: required unqualified local id. It is scoped by the source that discovered the package; it is not globally unique by itself. - `id`: required unqualified local id. It is scoped by the source that discovered the package; it is not globally unique by itself.
- `name`, `version`, `description`: human metadata used in listings and diagnostics. - `name`, `version`, `description`: human metadata used in listings and diagnostics.
- `surfaces`: optional declared contribution surface names. - `surfaces`: optional declared contribution surface names.
- `runtime`: optional WASM metadata only. Discovery records metadata and never executes it. - `runtime`: optional component runtime metadata. Discovery records metadata and never executes it; unsupported/retired runtime kinds fail closed.
- `hooks`: optional hook metadata. Discovery records metadata and does not register hooks. - `hooks`: optional hook metadata. Discovery records metadata and does not register hooks.
Future descriptor sections such as `[package]`, `[permissions]`, richer `contributions`, or `runtime.kind = "declarative"` are aspirational and are intentionally rejected by the current strict parser until implemented safely. Future descriptor sections such as `[package]`, `[permissions]`, richer `contributions`, or `runtime.kind = "declarative"` are aspirational and are intentionally rejected by the current strict parser until implemented safely.
@ -223,17 +216,4 @@ documents a future out-of-tree pinned git `rev` dependency pattern. Crates.io
publication, remote template fetching, and package authoring commands are not publication, remote template fetching, and package authoring commands are not
part of the current package/runtime contract. part of the current package/runtime contract.
This is separate from the legacy raw core-Wasm runtime: Legacy raw core-Wasm metadata remains documented only as a rejected migration diagnostic. Packages must not use `entry`/`abi`; discovery reports `kind = "wasm"` / `abi = "yoi-plugin-wasm-1"` packages as rejected without executing the artifact. Component execution still requires explicit package enablement, exact source/version/digest grants, and matching Tool/host API permissions.
```toml
[runtime]
kind = "wasm"
entry = "plugin.wasm"
abi = "yoi-plugin-wasm-1"
```
Component packages must not use `entry`/`abi`; raw packages must not use
`component`/`world`. Discovery reports the selected runtime kind/world without
executing the artifact. Component execution still requires explicit package
enablement, exact source/version/digest grants, and matching Tool/host API
permissions.

View File

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

View File

@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter; filter = sourceFilter;
}; };
cargoHash = "sha256-8mo2/IZMq3tfnv8fKRxJOdfb+T3NOheUmqT8TiR+Wag="; cargoHash = "sha256-kO1hvSYAVOVOIubP0Tm2Gpx2EWK7jwUKe30I8qTTU00=";
depsExtraArgs = { depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint, # Older fetchCargoVendor utilities used crates.io's API download endpoint,

View File

@ -1,14 +1,22 @@
[workspace]
[package] [package]
name = "example-yoi-instance-plugin" name = "yoi-rust-component-service-template"
version = "0.1.0" version = "0.1.0"
edition = "2024" 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] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" } yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" }
serde = { version = "1", features = ["derive"] }
serde_json = "1" # Out-of-tree Plugin packages should replace the local path with a pinned
# Yoi source revision. Use rev, not branch, for reproducible builds:
# yoi-plugin-pdk = { git = "https://gitea.hareworks.net/Hare/yoi.git", package = "yoi-plugin-pdk", rev = "<pinned-yoi-commit-sha>" }

View File

@ -1,9 +1,10 @@
# Yoi instance Plugin template # Rust Service Plugin Template
This template targets `yoi:plugin/instance@1.0.0`. The host creates one This template targets the Component Model-only runtime (`runtime.kind = "wasm-component"`) and exports the `yoi:plugin/instance@1.0.0` world.
`PluginInstance` for the package; Tool, Service, and Ingress surfaces share that
instance state while each surface keeps separate permissions/grants.
Tools still run only through ordinary model/user-initiated Tool calls. Ingress It demonstrates both authoring surfaces supported by a shared Plugin instance:
handlers receive bounded typed untrusted events and must return explicit JSON
for host-mediated visible/durable paths. - `example_echo` is an ordinary request/response Tool handler.
- `example_ws` is a Service ingress handler. The host owns WebSocket receive/reconnect work and dispatches bounded `websocket_text` events into `handle_ingress`. The guest replies by returning a `websocket_send` output command in `ServiceOutput`; do not run a guest-side `recv(timeout)` polling loop. The manifest declares `host_api.websocket` plus a matching `[[websocket]]` target for the example URL. Enablement grants must explicitly allow the same WebSocket target before the host will send output commands.
Build with `cargo component build --release` (or the project-specific build command used by your Plugin packaging flow), then run `yoi plugin check` / `yoi plugin pack` from the generated Plugin directory.

View File

@ -1,16 +1,17 @@
schema_version = 1 schema_version = 1
id = "example.rust_instance_plugin" id = "example.rust_service_plugin"
name = "Rust Instance Plugin Template" name = "Rust Service Plugin Template"
version = "0.1.0" version = "0.1.0"
description = "Example instance-oriented Yoi Plugin with shared Tool/Ingress state." description = "Example Component Model Plugin with Tool and Service ingress handlers."
surfaces = ["tool", "service", "ingress"] surfaces = ["tool", "service", "ingress"]
permissions = [ permissions = [
{ kind = "surface", surface = "tool" }, { kind = "surface", surface = "tool" },
{ kind = "tool", name = "example_instance_tool" }, { kind = "tool", name = "example_echo" },
{ kind = "surface", surface = "service" }, { kind = "surface", surface = "service" },
{ kind = "service", name = "example_instance_service" }, { kind = "service", name = "example_service" },
{ kind = "surface", surface = "ingress" }, { kind = "surface", surface = "ingress" },
{ kind = "ingress", name = "example_instance_ingress" }, { kind = "ingress", name = "example_ws" },
{ kind = "host_api", api = "websocket" },
] ]
[runtime] [runtime]
@ -19,17 +20,23 @@ world = "yoi:plugin/instance@1.0.0"
component = "plugin.component.wasm" component = "plugin.component.wasm"
[[tools]] [[tools]]
name = "example_instance_tool" name = "example_echo"
description = "Return the input and increment shared instance state." description = "Echo input text through the shared Plugin instance."
input_schema = { type = "object" } input_schema = { type = "object" }
[[services]] [[services]]
name = "example_instance_service" name = "example_service"
description = "Reports shared plugin instance lifecycle status." description = "Host-managed service instance for bounded ingress events."
lifecycle = "host-managed" lifecycle = "host-managed"
[[ingresses]] [[ingresses]]
name = "example_instance_ingress" name = "example_ws"
description = "Accepts bounded in-process ingress events." description = "Handles host-owned WebSocket text events and returns websocket_send output commands."
event_kinds = ["example"] event_kinds = ["websocket_text"]
sources = ["websocket:wss://example.com/socket"]
input_schema = { type = "object" } input_schema = { type = "object" }
[[websocket]]
scheme = "wss"
host = "example.com"
path_prefixes = ["/socket"]

View File

@ -1,6 +1,10 @@
use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
use yoi_plugin_pdk::wit_bindgen; use yoi_plugin_pdk::wit_bindgen;
use yoi_plugin_pdk::{export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ToolOutput}; use yoi_plugin_pdk::{
export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ServiceOutput, ToolError,
ToolOutput,
};
wit_bindgen::generate!({ wit_bindgen::generate!({
world: "instance", world: "instance",
@ -9,24 +13,42 @@ wit_bindgen::generate!({
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt", runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
}); });
#[derive(Default)]
struct ExamplePlugin { struct ExamplePlugin {
calls: u64, count: u64,
}
#[derive(Deserialize)]
struct EchoInput {
text: String,
}
#[derive(Serialize)]
struct EchoOutput {
text: String,
count: u64,
} }
impl Plugin for ExamplePlugin { impl Plugin for ExamplePlugin {
fn start(_config: Value) -> yoi_plugin_pdk::Result<Self> { fn start(config: Value) -> Result<Self, ToolError> {
Ok(Self { calls: 0 }) Ok(Self {
count: config.get("start_count").and_then(Value::as_u64).unwrap_or(0),
})
} }
fn handle_tool(&mut self, name: &str, input: Value) -> yoi_plugin_pdk::Result<ToolOutput> { fn handle_tool(&mut self, name: &str, input: Value) -> Result<ToolOutput, ToolError> {
self.calls += 1; 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( ToolOutput::json(
format!("{name} handled by shared instance"), format!("echoed {} bytes", input.text.len()),
json!({ EchoOutput {
"tool": name, text: input.text,
"calls": self.calls, count: self.count,
"input": input },
}),
) )
} }
@ -34,18 +56,29 @@ impl Plugin for ExamplePlugin {
&mut self, &mut self,
name: &str, name: &str,
event: PluginIngressEvent, event: PluginIngressEvent,
) -> yoi_plugin_pdk::Result<Value> { ) -> Result<ServiceOutput, ToolError> {
Ok(json!({ if name != "example_ws" {
"ingress": name, return Ok(ServiceOutput::accepted(json!({ "ignored": name }))?);
"kind": event.kind, }
"source": event.source,
"calls": self.calls, let Some(text) = event.websocket_text() else {
"accepted": true 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) -> yoi_plugin_pdk::Result<PluginStatus> { fn status(&self) -> Result<PluginStatus, ToolError> {
Ok(PluginStatus::ready(json!({ "calls": self.calls }))) Ok(PluginStatus::ready(json!({ "count": self.count })))
} }
} }

View File

@ -5,9 +5,55 @@ world instance {
import yoi:host/websocket@1.0.0; import yoi:host/websocket@1.0.0;
import yoi:host/fs@1.0.0; import yoi:host/fs@1.0.0;
/// Start one host-managed Plugin instance. `config-json` is the opaque
/// enablement config copied from the Profile/plugin grant record. The return
/// string is PluginStatus JSON: `{ "state": "ready|running|stopped|...",
/// "data": <json> }`.
export start: func(config-json: string) -> string; export start: func(config-json: string) -> string;
/// Execute a manifest-declared Tool on the shared instance. `input-json` is
/// ordinary Tool input JSON and the return string is ToolOutput JSON.
export handle-tool: func(name: string, input-json: string) -> string; export handle-tool: func(name: string, input-json: string) -> string;
/// Handle one host-dispatched Service/Ingress event. `event-json` is an
/// ingress event envelope with at least:
///
/// ```json
/// {
/// "kind": "websocket_text|websocket_close|websocket_error|...",
/// "source": "websocket:wss://host/path|...",
/// "ingress_name": "manifest_ingress_name",
/// "payload": { "text": "..." },
/// "created_at": "RFC3339 timestamp",
/// "attempt": 1,
/// "correlation_id": "host event id"
/// }
/// ```
///
/// The return string is ServiceOutput JSON. To request host-mediated side
/// effects, return top-level `output_commands`, for example:
///
/// ```json
/// {
/// "accepted": true,
/// "output_commands": [{
/// "correlation_id": "command correlation id",
/// "source_event_id": "matching ingress correlation_id",
/// "command_id": "guest command id",
/// "kind": "websocket_send",
/// "payload": { "url": "wss://host/path", "text": "reply" },
/// "requested_at": "RFC3339 timestamp"
/// }]
/// }
/// ```
///
/// Output commands are parsed, bounded, grant-checked, and executed by the
/// host. They are not ordinary ToolOutput and do not inject hidden context.
export handle-ingress: func(name: string, event-json: string) -> string; export handle-ingress: func(name: string, event-json: string) -> string;
/// Return PluginStatus JSON for the shared host-managed instance.
export status: func() -> string; export status: func() -> string;
/// Stop the shared instance and return final PluginStatus JSON.
export stop: func() -> string; export stop: func() -> string;
} }

View File

@ -5,8 +5,8 @@ world tool {
import yoi:host/websocket@1.0.0; import yoi:host/websocket@1.0.0;
import yoi:host/fs@1.0.0; import yoi:host/fs@1.0.0;
/// Execute a manifest-declared Tool. `input-json` is the normal Tool input /// Execute a manifest-declared Tool. `input-json` is ordinary Tool input JSON
/// JSON and the returned string is the same ToolOutput JSON accepted by the /// and the returned string is ToolOutput JSON accepted by the current
/// legacy raw-Wasm ABI. /// Component Model Plugin runtime.
export call: func(tool-name: string, input-json: string) -> string; export call: func(tool-name: string, input-json: string) -> string;
} }