Compare commits
63 Commits
c8877b49a4
...
5df7580a1e
| Author | SHA1 | Date | |
|---|---|---|---|
| 5df7580a1e | |||
| 96223148c0 | |||
| 68a8fc97d2 | |||
| ca28c927b2 | |||
| 9cf5344fc5 | |||
| e5510620cc | |||
| 8e6b440f9a | |||
| f396e1a253 | |||
| 39b55fb6e8 | |||
| c91ed5600f | |||
| 35e6533986 | |||
| a114fa9d0a | |||
| 017c4471ed | |||
| c0e760d73e | |||
| 8f5eef94e4 | |||
| c4a7eb7a2e | |||
| 9b7c4e279d | |||
| c6fa0b2d95 | |||
| a0cd3dd0e9 | |||
| e578d888e3 | |||
| e0680ccee0 | |||
| 6cc8551a6c | |||
| b0225e48b8 | |||
| a5df9e3728 | |||
| ead96654be | |||
| 0430ed982d | |||
| 52e40b2fdc | |||
| 93265ae65c | |||
| 699db538b6 | |||
| 9ac540f7cf | |||
| 8206b5912d | |||
| 59d0a58e3a | |||
| 945ecdf64d | |||
| e37a360d07 | |||
| 9881a061fe | |||
| 76729c3377 | |||
| f880007639 | |||
| eee2ce00e2 | |||
| a729282cc1 | |||
| 061322d425 | |||
| 191e7999c0 | |||
| c0239684ed | |||
| d1095f854a | |||
| 902b383de7 | |||
| ab7ab69f20 | |||
| edc53a6bc9 | |||
| 1f0766c1d7 | |||
| 01f2e926b0 | |||
| 0a9e585c1d | |||
| 75e8103cdd | |||
| 356d06ef58 | |||
| 730bc73975 | |||
| b4cb9fbc41 | |||
| 2c78428d9d | |||
| 88f4c7e104 | |||
| 3fb3368272 | |||
| af435fa9bc | |||
| 06287aca40 | |||
| 6c04cb9a0b | |||
| b5e623e5a1 | |||
| 5f7f81bdde | |||
| 9ca2f85b08 | |||
| 8b42e319e2 |
|
|
@ -1,24 +1,25 @@
|
|||
---
|
||||
title: 'MCP local stdio integration architecture'
|
||||
title: 'MCP local stdio integration roadmap'
|
||||
state: 'active'
|
||||
created_at: '2026-06-10T07:48:45Z'
|
||||
updated_at: '2026-06-13T15:30:22Z'
|
||||
updated_at: '2026-06-20T05:34:00Z'
|
||||
linked_tickets: ['00001KTR81P9X', '00001KV0SP0TY', '00001KVHR3WRF', '00001KVHR3WRY', '00001KVHR3WS6', '00001KVHR3WSD', '00001KVHR3WSN', '00001KVHR3WSW']
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
Add MCP local stdio integration to Yoi without weakening Worker history, prompt-context, scoped tool permission, or Plugin/Feature layering invariants.
|
||||
|
||||
MCP should be implemented as a protocol-backed integration layer on top of `pod::feature`. `pod::feature` supplies the contribution/lifecycle API substrate; MCP owns its own enablement, local server trust model, command/env/secret policy, and MCP-specific permissions. MCP is not the Plugin model, and Plugin permission policy is not implemented by feature-layer authority grants.
|
||||
MCP is a protocol-backed integration layer on top of `pod::feature`. `pod::feature` supplies contribution/lifecycle/runtime-discovered registration substrate; MCP owns its own enablement, local server trust model, command/env/secret policy, and MCP-specific permission decisions. MCP is not the Plugin model, and Plugin permission policy is not implemented by feature-layer authority grants.
|
||||
|
||||
## Strategic direction
|
||||
|
||||
- Baseline the implementation on MCP specification `2025-11-25`.
|
||||
- Baseline the initial implementation on MCP specification `2025-11-25`.
|
||||
- Start with local stdio MCP servers only.
|
||||
- Treat MCP server metadata, tools, resources, prompts, and results as untrusted content.
|
||||
- Do not allow MCP resources/prompts to be hidden context injection.
|
||||
- Do not allow MCP resources/prompts to become hidden context injection.
|
||||
- They must be explicit tool operations with history records.
|
||||
- Use the normal Yoi tool registry, PreToolCall permission, history, and bounded result paths.
|
||||
- Use the normal Yoi ToolRegistry, PreToolCall permission, history, and bounded result paths.
|
||||
- Do not add private MCP-only bypasses around Worker/tool invariants.
|
||||
- Keep sampling and elicitation fail-closed initially.
|
||||
- Keep Streamable HTTP, remote auth, OAuth, and MCP Registry/distribution out of the first slice.
|
||||
|
|
@ -28,37 +29,59 @@ MCP should be implemented as a protocol-backed integration layer on top of `pod:
|
|||
## Layering decisions
|
||||
|
||||
- `pod::feature` is an API/contribution substrate.
|
||||
- It owns contribution declarations, provider/service lifecycle hooks, diagnostics, dynamic registration plumbing, and integration with normal Worker/ToolRegistry paths.
|
||||
- It owns contribution declarations, provider/service lifecycle hooks, diagnostics, runtime-discovered registration plumbing, and integration with normal Worker/ToolRegistry paths.
|
||||
- It does not own Plugin permission policy or MCP server trust policy.
|
||||
- Plugin is a user-facing package/config/runtime layer over `pod::feature`.
|
||||
- Plugin permissions are Plugin-layer policy.
|
||||
- Plugin package discovery/enablement must not be conflated with MCP local server execution.
|
||||
- MCP is a separate feature-backed integration layer.
|
||||
- MCP enablement, command/env/secret handling, server trust, and MCP-specific permission decisions live in MCP config/implementation.
|
||||
- MCP dynamic tools/resources/prompts are exposed through the feature API and ordinary Yoi tool paths.
|
||||
- MCP provider-discovered tools/resources/prompts are exposed through the feature API and ordinary Yoi tool paths.
|
||||
|
||||
## Work breakdown
|
||||
## Concrete implementation tickets
|
||||
|
||||
1. `00001KTR81P9X` — Extend `pod::feature` API for protocol-backed external providers.
|
||||
- provider/service lifecycle
|
||||
- startup discovery and dynamic contribution registration
|
||||
- bounded refresh semantics
|
||||
- metadata/result normalization
|
||||
- no feature-layer authority model for MCP/Plugin permissions
|
||||
2. `00001KTR82RB7` — Implement MCP `2025-11-25` local stdio server bridge.
|
||||
- explicit MCP config and trust model
|
||||
- initialize/capability negotiation
|
||||
- tools/resources/prompts list/call/read/get
|
||||
- bounded result serialization
|
||||
- list-changed diagnostics/refresh behavior
|
||||
3. `00001KV0SP0TY` — Remove feature-layer HostAuthority model.
|
||||
- remove authority/grant terminology from `pod::feature`
|
||||
- keep real permission/trust policy in owning Plugin/MCP/manifest/tool layers
|
||||
4. Later follow-ups, if needed.
|
||||
- richer MCP tasks / task-support integration
|
||||
- remote/HTTP transports
|
||||
- OAuth / registry / package distribution
|
||||
- Plugin package/runtime alignment, if an explicit MCP/plugin bridge is later approved
|
||||
Completed prerequisites:
|
||||
|
||||
- `00001KTR81P9X` — Extend `pod::feature` API for external protocol-backed capability providers.
|
||||
- `00001KV0SP0TY` — Remove feature-layer HostAuthority model.
|
||||
|
||||
Concrete MCP implementation sequence:
|
||||
|
||||
1. `00001KVHR3WRF` — MCP local stdio server config and trust policy.
|
||||
- explicit config, command/env/secret redaction, local executable trust boundary, no auto-start.
|
||||
2. `00001KVHR3WRY` — MCP stdio JSON-RPC lifecycle client.
|
||||
- subprocess lifecycle, initialize/capability negotiation, diagnostics, shutdown.
|
||||
3. `00001KVHR3WS6` — MCP tools/list registration into ToolRegistry.
|
||||
- provider-discovered tools, stable namespacing, schema validation, untrusted metadata normalization, no tools/call yet.
|
||||
4. `00001KVHR3WSD` — MCP tools/call execution through ordinary Tool path.
|
||||
- PreToolCall gate before server call, bounded result serialization, history path.
|
||||
5. `00001KVHR3WSN` — MCP resources/prompts as explicit tool operations.
|
||||
- resources/list/read and prompts/list/get without hidden context injection.
|
||||
6. `00001KVHR3WSW` — MCP list_changed notification handling.
|
||||
- deterministic safe refresh/diagnostic behavior without breaking tool schema or prompt-cache invariants.
|
||||
|
||||
The old broad implementation Ticket `00001KTR82RB7` is superseded by this sequence and should not be used as an implementation work item.
|
||||
|
||||
## Terminology
|
||||
|
||||
Use `runtime-discovered` or `provider-discovered` for MCP tools/resources/prompts discovered from `tools/list`, `resources/list`, or `prompts/list`. Avoid `dynamic tools` / `dynamic registry` in new MCP design prose because those phrases imply that model-visible tool schemas may change during an active LLM run.
|
||||
|
||||
The intended invariant is:
|
||||
|
||||
```text
|
||||
provider-discovered at startup / provider initialization;
|
||||
registered into the ordinary ToolRegistry before model exposure;
|
||||
run-stable for the duration of a model request/run;
|
||||
refreshed only at a safe boundary or reported as a diagnostic.
|
||||
```
|
||||
|
||||
## Later follow-ups
|
||||
|
||||
- Richer MCP task/task-support integration if ordinary tool-call fallback is insufficient.
|
||||
- Streamable HTTP transport.
|
||||
- OAuth / remote auth.
|
||||
- Registry/package distribution.
|
||||
- Explicit MCP/Plugin bridge only if separately approved; do not conflate Plugin packages with MCP local server execution.
|
||||
|
||||
## Success criteria
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: 'Implement MCP 2025-11-25 local stdio server-feature bridge'
|
||||
state: 'planning'
|
||||
state: 'closed'
|
||||
created_at: '2026-06-10T07:48:49Z'
|
||||
updated_at: '2026-06-13T15:29:21Z'
|
||||
updated_at: '2026-06-20T05:33:15Z'
|
||||
assignee: null
|
||||
readiness: 'blocked'
|
||||
risk_flags: ['mcp', 'prompt-context', 'permission-scope', 'secrets', 'process-exec', 'feature-api', 'trust-boundary']
|
||||
|
|
|
|||
13
.yoi/tickets/00001KTR82RB7/resolution.md
Normal file
13
.yoi/tickets/00001KTR82RB7/resolution.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Closed as superseded by concrete MCP implementation Tickets.
|
||||
|
||||
This Ticket bundled config/trust policy, stdio lifecycle, tools/list registration, tools/call execution, resources/prompts operations, result serialization, and list_changed handling into one broad implementation item. That is too coarse for the current Ticket policy: Tickets should be concrete implementation tasks.
|
||||
|
||||
The MCP roadmap now lives in Objective `00001KTR80WMN` (`MCP local stdio integration roadmap`). Concrete follow-up Tickets are:
|
||||
- `00001KVHR3WRF` — local stdio server config and trust policy;
|
||||
- `00001KVHR3WRY` — stdio JSON-RPC lifecycle client;
|
||||
- `00001KVHR3WS6` — server tools registration into ToolRegistry;
|
||||
- `00001KVHR3WSD` — tools/call execution through ordinary Tool path;
|
||||
- `00001KVHR3WSN` — resources/prompts as explicit tool operations;
|
||||
- `00001KVHR3WSW` — list_changed notification handling.
|
||||
|
||||
Future MCP work should use those concrete Tickets or similarly scoped follow-ups, not this broad umbrella Ticket.
|
||||
|
|
@ -22,4 +22,34 @@ LocalTicketBackend によって作成されました。
|
|||
- `00001KSXRQ4G8` と `00001KT0Z4BK8` は Plugin permission を Plugin layer として扱い、MCP を初期 Plugin packaging/runtime から分離する。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-20T05:33:15Z from: planning to: closed reason: closed field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を closed にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-20T05:33:15Z status: closed -->
|
||||
|
||||
## 完了
|
||||
|
||||
Closed as superseded by concrete MCP implementation Tickets.
|
||||
|
||||
This Ticket bundled config/trust policy, stdio lifecycle, tools/list registration, tools/call execution, resources/prompts operations, result serialization, and list_changed handling into one broad implementation item. That is too coarse for the current Ticket policy: Tickets should be concrete implementation tasks.
|
||||
|
||||
The MCP roadmap now lives in Objective `00001KTR80WMN` (`MCP local stdio integration roadmap`). Concrete follow-up Tickets are:
|
||||
- `00001KVHR3WRF` — local stdio server config and trust policy;
|
||||
- `00001KVHR3WRY` — stdio JSON-RPC lifecycle client;
|
||||
- `00001KVHR3WS6` — server tools registration into ToolRegistry;
|
||||
- `00001KVHR3WSD` — tools/call execution through ordinary Tool path;
|
||||
- `00001KVHR3WSN` — resources/prompts as explicit tool operations;
|
||||
- `00001KVHR3WSW` — list_changed notification handling.
|
||||
|
||||
Future MCP work should use those concrete Tickets or similarly scoped follow-ups, not this broad umbrella Ticket.
|
||||
|
||||
|
||||
---
|
||||
|
|
|
|||
0
.yoi/tickets/00001KVHKWNQA/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHKWNQA/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260620-045332-1","ticket_id":"00001KVHKWNQA","kind":"accepted_plan","accepted_plan":{"summary":"Component Model Tool authoring用の first-party Rust PDK crate と embedded `rust-component-tool` template を追加する。PDK は guest-side only とし、raw pointer/length ABI を隠し、typed JSON helper / ToolError / ToolContext / WIT binding glue を提供する。crates.io 公開・remote template fetch・authoring CLI implementation は含めない。","branch":"impl/00001KVHKWNQA-plugin-rust-pdk-templates","worktree":"/home/hare/Projects/yoi/.worktree/00001KVHKWNQA-plugin-rust-pdk-templates","role_plan":"Orchestrator は queued acceptance を記録・commit 後、専用 implementation worktree を `.worktree/00001KVHKWNQA-plugin-rust-pdk-templates` に作成し、Coder をその child worktree への narrow write scope で起動する。Coder 実装後、Reviewer が PDK guest-only boundary、Component Model runtime compatibility、template/resource packaging、docs/tests/Nix impact を確認する。"},"author":"yoi-orchestrator","at":"2026-06-20T04:53:32Z"}
|
||||
37
.yoi/tickets/00001KVHKWNQA/artifacts/relations.json
Normal file
37
.yoi/tickets/00001KVHKWNQA/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHKWNQA",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVG0HR96",
|
||||
"note": "PDK targets the Component Model Tool runtime.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T04:17:24Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHKWNQA",
|
||||
"kind": "related",
|
||||
"target": "00001KVFD3YSV",
|
||||
"note": "PDK authoring flow should pair with read-only plugin inspection.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T04:17:24Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHKWNQA",
|
||||
"kind": "related",
|
||||
"target": "00001KVFDX9AF",
|
||||
"note": "PDK should later wrap https host API ergonomically.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T04:17:24Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHKWNQA",
|
||||
"kind": "related",
|
||||
"target": "00001KVFDX9AY",
|
||||
"note": "PDK should later wrap fs host API ergonomically.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T04:17:24Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
90
.yoi/tickets/00001KVHKWNQA/item.md
Normal file
90
.yoi/tickets/00001KVHKWNQA/item.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
title: 'Plugin: add Rust PDK and embedded authoring templates for Component Model Tools'
|
||||
state: 'closed'
|
||||
created_at: '2026-06-20T04:16:14Z'
|
||||
updated_at: '2026-06-20T05:53:20Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['plugin', 'pdk', 'component-model', 'authoring', 'templates', 'sdk', 'no-crates-io']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T04:52:58Z'
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Yoi can now discover Plugin packages, register Tool surfaces, execute sandboxed WASM/Component Model Tool Plugins, enforce Plugin grants, expose `https` / `fs` host APIs, and inspect Plugin state through CLI. The next gap is authoring: independent Plugin developers should not need to write raw WIT/ABI glue or copy ad-hoc examples by hand.
|
||||
|
||||
This Ticket adds a first-party Rust PDK for Component Model Tool Plugins and embeds a starter template into Yoi resources. It deliberately does not publish to crates.io yet and does not fetch remote templates. The PDK/API and WIT world are still young, so out-of-tree authors should initially use a git rev or generated local path dependency rather than depending on a public semver crate.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add a Rust PDK crate inside the Yoi workspace.
|
||||
- Suggested package name: `yoi-plugin-pdk`.
|
||||
- Suggested path: `crates/plugin-pdk`.
|
||||
- Target Component Model Tool Plugins, not raw core-Wasm as the primary authoring path.
|
||||
- PDK must be guest-side only.
|
||||
- Do not depend on host runtime crates such as `pod`, `llm-worker`, `tui`, or `client`.
|
||||
- Keep dependencies minimal, e.g. `serde`, `serde_json`, and WIT binding support.
|
||||
- Do not expose ambient fs/network/env authority.
|
||||
- Provide ergonomic Tool helpers.
|
||||
- Typed JSON input parsing.
|
||||
- Typed JSON output serialization.
|
||||
- Structured `ToolError` / error-code helpers.
|
||||
- `ToolContext` containing at least the tool name.
|
||||
- Helper equivalent to `run_json_tool` that returns the ToolOutput JSON expected by the Yoi runtime.
|
||||
- Provide Component Model binding glue.
|
||||
- Re-export or wrap generated WIT bindings enough that an author does not need to hand-write raw pointer/length ABI code.
|
||||
- Prefer a minimal `Guest` impl helper first; add a macro only if it is simpler and testable.
|
||||
- Include embedded authoring templates under runtime resources.
|
||||
- Suggested path: `resources/plugin/templates/rust-component-tool/`.
|
||||
- Template includes `Cargo.toml`, `src/lib.rs`, `plugin.toml`, and a short README.
|
||||
- Template should be usable by a future `yoi plugin new` command without network access.
|
||||
- Template dependency policy:
|
||||
- In checkout/dev mode, template may use a local path dependency to `crates/plugin-pdk`.
|
||||
- For out-of-tree authors, docs/template comments should show a git `rev` dependency pattern.
|
||||
- Do not publish or require crates.io for this Ticket.
|
||||
- Do not use `curl | sh` or remote template fetch.
|
||||
- Include at least one example or fixture Plugin using the PDK.
|
||||
- Echo-style Component Model Tool is enough.
|
||||
- It should compile to a component artifact through the documented toolchain or, if full component build automation is not yet available, be covered by a clear fixture/test boundary.
|
||||
- Update Plugin development docs.
|
||||
- Explain that Component Model + PDK is the preferred authoring path.
|
||||
- Explain that raw core-Wasm ABI is compatibility/transitional.
|
||||
- Explain why crates.io publication and remote templates are intentionally deferred.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Workspace contains a guest-side `yoi-plugin-pdk` crate.
|
||||
- A Rust Component Model Tool Plugin can use the PDK to implement a JSON Tool without raw pointer/length ABI code.
|
||||
- PDK-produced Tool success output is accepted by the existing Yoi Plugin Tool runtime.
|
||||
- PDK-produced Tool errors are bounded and represented as ordinary Tool result/error content.
|
||||
- PDK does not grant or imply authority; host-side Plugin grants still decide `https` / `fs` / Tool execution access.
|
||||
- Embedded `rust-component-tool` template exists in runtime resources and is suitable for future `yoi plugin new` expansion.
|
||||
- No crates.io publication is required or performed.
|
||||
- No remote template fetch is implemented.
|
||||
- Tests cover:
|
||||
- PDK JSON input/output happy path;
|
||||
- PDK error output path;
|
||||
- PDK sample/template compiles or fixture is validated;
|
||||
- sample Plugin can be executed by Yoi component runtime if feasible in current test infrastructure;
|
||||
- PDK has no host-runtime crate dependency.
|
||||
- Validation: focused PDK/plugin tests, `cargo fmt --check`, relevant `cargo check` / `cargo test`, `git diff --check`, and `nix build .#yoi` because workspace/package/resources may change.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Publishing `yoi-plugin-pdk` to crates.io.
|
||||
- Remote template fetch or `curl | sh` setup.
|
||||
- `yoi plugin new/check/pack` CLI implementation.
|
||||
- Multi-language PDKs.
|
||||
- Service / Ingress authoring.
|
||||
- WebSocket / inbound HTTP bridge support.
|
||||
- Replacing Plugin grants with PDK-side checks.
|
||||
|
||||
## Related work
|
||||
|
||||
- `00001KVG0HR9M` — Objective: Plugin platform roadmap.
|
||||
- `00001KVG0HR96` — Plugin Component Model runtime.
|
||||
- `00001KVFD3YSV` — Plugin read-only CLI inspection.
|
||||
- `00001KVFDX9AF` — Plugin https host API.
|
||||
- `00001KVFDX9AY` — Plugin fs host API.
|
||||
- `docs/development/plugin-development.md` — current Plugin development guide.
|
||||
43
.yoi/tickets/00001KVHKWNQA/resolution.md
Normal file
43
.yoi/tickets/00001KVHKWNQA/resolution.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
## Resolution
|
||||
|
||||
`00001KVHKWNQA` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- Guest-side Rust PDK crate `yoi-plugin-pdk` を追加しました。
|
||||
- PDK は typed JSON input/output helper、bounded `ToolError`、`ToolContext`、`run_json_tool` 系 helper、`wit_bindgen` re-export、`export_component_tool!` macro を提供します。
|
||||
- PDK は host/runtime Yoi crates に依存せず、authority を付与しません。Host-side Plugin manifest grants が Tool execution / host API use の authority boundary のままです。
|
||||
- Embedded Rust Component Tool template を `resources/plugin/templates/rust-component-tool/` に追加しました。
|
||||
- Template は local checkout/dev path dependency を使い、future out-of-tree git `rev` pattern を docs に記録しています。
|
||||
- `resources/plugin/wit` を `wit-bindgen` が parse できる package layout に修正し、host WIT dependency を `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit` に移動しました。
|
||||
- WIT keyword `list` は `%list` escape にし、import name semantics を保持しました。
|
||||
- Embedded template は empty `[workspace]` により in-tree standalone package として check できます。
|
||||
- `wit_bindgen::generate!` を実際に `resources/plugin/wit` に対して実行する probe と、embedded template の `wasm32-unknown-unknown` cargo-check probe を追加しました。
|
||||
- Plugin development docs / design docs / package docs / example source を更新しました。
|
||||
- `yoi plugin new/check/pack`、remote template fetch、crates.io publication、full packaged component execution はこの Ticket の non-goals / follow-up として残しました。
|
||||
|
||||
主な commit:
|
||||
- `06287aca plugin: add rust pdk template`
|
||||
- `0a9e585c plugin: fix rust pdk wit template probes`
|
||||
- `edc53a6b merge: plugin rust pdk templates`
|
||||
|
||||
Review:
|
||||
- r1 は WIT parse failure と embedded template Cargo workspace issue で `request_changes`。
|
||||
- Coder が WIT layout / `%list` / template `[workspace]` / actual probes を追加。
|
||||
- r2 は `approve`。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p yoi-plugin-pdk`
|
||||
- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape`
|
||||
- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder`
|
||||
- `cargo check`
|
||||
- `cargo tree -p yoi-plugin-pdk --edges normal`
|
||||
- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Package impact:
|
||||
- `nix path-info -S .#yoi`: `112156384`
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-o9gvGb.log`
|
||||
439
.yoi/tickets/00001KVHKWNQA/thread.md
Normal file
439
.yoi/tickets/00001KVHKWNQA/thread.md
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T04:16:14Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-20T04:52:58Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T04:53:59Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Panel Queue により、この Ticket は Orchestrator routing 対象として明示許可された。
|
||||
- Ticket body は Rust PDK crate、guest-side only boundary、ergonomic Tool helpers、Component Model binding glue、embedded `rust-component-tool` template、docs/tests/validation/non-goals を実装可能な粒度で定義している。
|
||||
- Blocking dependency `00001KVG0HR96` Component Model runtime は closed。Component runtime は explicit `wasm-component` metadata、ToolRegistry path、grant enforcement、no ambient WASI、resource limits、WIT files を含めて完了済み。
|
||||
- Incoming dependency from `00001KVHKWNQS` は、この Ticket が将来の authoring CLI Ticket を unblock する関係であり、本 Ticket の blocker ではない。
|
||||
- Related Plugin CLI / HTTPS / FS Tickets は closed または non-blocking context。
|
||||
- 現在 queued はこの Ticket のみ、inprogress は 0 件、child implementation Pods はなし、matching branch/worktree はなし、Orchestrator worktree は clean。
|
||||
- Risk domain は plugin / pdk / component-model / authoring / templates / sdk / no-crates-io だが、Ticket は no crates.io publication、no remote template fetch、guest-side only、no ambient authority、host-side grants remain authority などの invariants を明示している。bounded context check 後も implementation 前に必要な追加 human decision は見つからなかった。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket `00001KVHKWNQA` body / thread / relations / artifacts。
|
||||
- `TicketRelationQuery(00001KVHKWNQA)`: outgoing `depends_on` target `00001KVG0HR96` is closed。incoming `00001KVHKWNQS depends_on this` is not a blocker。
|
||||
- `TicketOrchestrationPlanQuery(00001KVHKWNQA)`: no previous plan records; accepted plan was recorded now。
|
||||
- Workspace state:
|
||||
- Orchestrator worktree clean at `9ca2f85b`。
|
||||
- queued: this Ticket only。
|
||||
- inprogress: 0。
|
||||
- visible Pods: self + peers only; spawned children 0。
|
||||
- no matching implementation branch/worktree。
|
||||
- Code/resource context:
|
||||
- `resources/plugin/wit/yoi-plugin-tool-v1.wit` and `resources/plugin/wit/yoi-host-v1.wit` exist from the Component runtime work。
|
||||
- `docs/development/plugin-development.md` is the current Plugin development guide target。
|
||||
- Cargo workspace has no existing `crates/plugin-pdk` crate。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Add a first-party Rust PDK for Component Model Tool Plugins and an embedded starter template so authors can implement Yoi Plugin Tools without raw pointer/length ABI plumbing or remote template fetches。
|
||||
- Position Component Model + PDK as preferred authoring path while keeping raw core-Wasm ABI as compatibility/transitional runtime support。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- PDK is guest-side only。It must not depend on host runtime crates such as `pod`, `llm-worker`, `tui`, or `client`。
|
||||
- PDK must not grant or imply authority。Host-side Plugin manifest grants remain the authority boundary for Tool execution / HTTPS / FS host APIs。
|
||||
- No ambient fs/network/env authority is introduced。
|
||||
- Do not publish to crates.io and do not implement remote template fetch。
|
||||
- Do not implement `yoi plugin new/check/pack` in this Ticket; embedded resources should be suitable for that future Ticket。
|
||||
- Template dependency policy must support checkout/dev local path dependency and document a future out-of-tree git `rev` pattern。
|
||||
- PDK should target the current Component Model Tool world (`yoi:plugin/tool@1.0.0`) and runtime ToolOutput JSON bridge。
|
||||
- Prefer minimal, testable helper APIs over broad macro magic。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Add workspace crate `yoi-plugin-pdk` under `crates/plugin-pdk` or a justified equivalent。
|
||||
- Provide typed JSON input parsing, output serialization, structured/bounded `ToolError`, `ToolContext` with at least tool name, and helper equivalent to `run_json_tool` producing ToolOutput JSON accepted by the current runtime。
|
||||
- Provide WIT binding glue/re-export/wrapper enough that authors do not hand-write raw pointer/length ABI code。
|
||||
- Add embedded template under `resources/plugin/templates/rust-component-tool/` with `Cargo.toml`, `src/lib.rs`, `plugin.toml`, and README/next steps。
|
||||
- Include example or fixture Plugin using the PDK。
|
||||
- Update Plugin development docs and explain publication/template-fetch deferrals。
|
||||
- Tests cover PDK happy/error paths, template/sample fixture validation, runtime acceptance if feasible, and no host-runtime crate dependency。
|
||||
- Validation includes focused PDK/plugin tests, `cargo fmt --check`, relevant `cargo check`/`cargo test`, `git diff --check`, and `nix build .#yoi` because workspace/package/resources may change。
|
||||
|
||||
Escalate if:
|
||||
- Current WIT/runtime shape cannot support a guest-side helper without changing the Component runtime contract。
|
||||
- A full sample component build requires external tooling that cannot be kept deterministic in this repo。
|
||||
- Keeping the PDK guest-only would require undesirable dependencies on host crates。
|
||||
- Template/resource packaging needs a broader CLI/resource architecture decision。
|
||||
|
||||
Next action:
|
||||
- Record `queued -> inprogress` and commit Ticket records before creating the implementation worktree and spawning Coder。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T04:54:04Z from: queued to: inprogress reason: orchestrator_acceptance_plugin_rust_pdk_templates field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket body/thread, relation metadata, Component Model runtime dependency state, related completed Tickets, Orchestrator worktree state, visible Pods, existing branch/worktree, and bounded resource/code context were checked. Dependency `00001KVG0HR96` is closed, no inprogress/capacity blocker exists, and no missing planning decision was found. Accepting this queued Ticket for implementation before worktree/Pod side effects.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T04:54:51Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration start:
|
||||
- Acceptance records were committed on Orchestrator branch: `5f7f81bd ticket: accept plugin rust pdk templates`.
|
||||
- Created implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVHKWNQA-plugin-rust-pdk-templates` on branch `impl/00001KVHKWNQA-plugin-rust-pdk-templates` at `5f7f81bd`.
|
||||
- Spawned Coder Pod `yoi-coder-00001KVHKWNQA` with write scope limited to the implementation worktree and read-only root scope only for runtime workspace identity.
|
||||
- Coder task explicitly forbids root/original workspace implementation, merge, Ticket close, and cleanup.
|
||||
|
||||
Next action:
|
||||
- Wait for Coder implementation report, then inspect branch diff/validation evidence and route to Reviewer.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T05:16:16Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder implementation report received from `yoi-coder-00001KVHKWNQA`.
|
||||
|
||||
Implementation commit:
|
||||
- `06287aca plugin: add rust pdk template`
|
||||
|
||||
Changed areas reported:
|
||||
- Added workspace crate `crates/plugin-pdk` / `yoi-plugin-pdk`:
|
||||
- guest-side PDK helpers;
|
||||
- typed JSON input parsing via `parse_json_input` / `run_json_tool`;
|
||||
- typed JSON output via `ToolOutput::json`;
|
||||
- bounded structured `ToolError` and error-code helpers;
|
||||
- `ToolContext` with selected tool name;
|
||||
- `wit_bindgen` re-export and `export_component_tool!` macro to avoid raw pointer/length ABI plumbing;
|
||||
- tests for happy path, error path, oversized output, template validation, and host-runtime dependency exclusion。
|
||||
- Added embedded starter template under `resources/plugin/templates/rust-component-tool/` with `Cargo.toml`, `src/lib.rs`, `plugin.toml`, and `README.md`。
|
||||
- Added embedded template constants in `crates/manifest/src/plugin.rs` for future authoring CLI use without remote fetching。
|
||||
- Updated Component Model example to use the PDK。
|
||||
- Added runtime decoder test confirming PDK-produced ToolOutput JSON shape is accepted。
|
||||
- Updated Plugin development/design/package docs。
|
||||
- Updated workspace/package metadata: root `Cargo.toml`, `Cargo.lock`, `package.nix` cargo hash。
|
||||
|
||||
Coder validation reported as passing:
|
||||
- `cargo test -p yoi-plugin-pdk`
|
||||
- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape`
|
||||
- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder`
|
||||
- `cargo check`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `git diff --check --cached`
|
||||
- `cargo tree -p yoi-plugin-pdk --edges normal`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Known deferrals reported:
|
||||
- No `yoi plugin new/check/pack`, remote template fetch, or crates.io publication。
|
||||
- Full deterministic sample component build/pack execution remains deferred to future authoring CLI/tooling; current sample/template are fixture-validated and use current WIT/PDK shape。
|
||||
- Raw core-Wasm ABI remains compatibility/transitional runtime support。
|
||||
|
||||
Orchestrator evidence checked before review dispatch:
|
||||
- Implementation worktree is clean on `impl/00001KVHKWNQA-plugin-rust-pdk-templates`。
|
||||
- HEAD is `06287aca`。
|
||||
- Diff from acceptance `5f7f81bd..HEAD` is one implementation commit touching 17 files, about 857 insertions / 35 deletions。
|
||||
- `git diff --check 5f7f81bd..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Dispatch Reviewer for r1 review against Ticket requirements, with focus on guest-only PDK boundary, no host-runtime deps, authority/grant wording, template/resource packaging, sample/test boundary honesty, docs, and Nix/workspace changes。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: yoi-orchestrator at: 2026-06-20T05:16:53Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Review dispatch:
|
||||
- Spawned Reviewer Pod `yoi-reviewer-00001KVHKWNQA-r1` against implementation branch `impl/00001KVHKWNQA-plugin-rust-pdk-templates`。
|
||||
- Review target commit: `06287aca plugin: add rust pdk template`。
|
||||
- Review baseline: `5f7f81bd`。
|
||||
- Reviewer task focuses on guest-only PDK boundary, absence of host-runtime deps, authority/grant wording, no ambient fs/network/env, template/resource packaging, current WIT/runtime compatibility, sample/test honesty, docs, and Nix/workspace changes。
|
||||
- Reviewer is instructed not to edit source, commit, merge, close the Ticket, or use TicketReview directly; it will report verdict/evidence back to Orchestrator。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVHKWNQA-r1 at: 2026-06-20T05:21:50Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Verdict: `request_changes`
|
||||
|
||||
確認範囲:
|
||||
- Ticket body/thread / Orchestrator IntentPacket
|
||||
- Diff: `5f7f81bd..06287aca`
|
||||
- 主な対象: `crates/plugin-pdk/*`, embedded template `resources/plugin/templates/rust-component-tool/*`, `resources/plugin/wit/*`, `crates/manifest/src/plugin.rs`, `crates/pod/src/feature/plugin.rs`, workspace/package docs/Nix/Cargo paths。
|
||||
|
||||
良い点:
|
||||
- PDK は guest-side only として提示され、Yoi host/runtime crates への normal dependency は見当たらない。
|
||||
- docs/templates は host-side Plugin grants が authority boundary であること、crates.io publication / remote template fetch を要求しないことを概ね維持している。
|
||||
- `ToolError` / `ToolOutput` bounds と runtime decoder test は現在の ToolOutput JSON bridge と整合している。
|
||||
|
||||
Blocking issues:
|
||||
1. PDK/template authoring path が現在の WIT で実際に compile/parse できない。
|
||||
- Template/example/docs は次の generated binding path を示している。
|
||||
- `resources/plugin/templates/rust-component-tool/src/lib.rs`
|
||||
- `docs/examples/plugin-component-tool/lib.rs`
|
||||
- `crates/plugin-pdk/src/lib.rs`
|
||||
- しかし `resources/plugin/wit/yoi-host-v1.wit` の interface `fs` が `list: func(...)` を使っており、`wit-bindgen` probe で `list` が keyword として扱われ parse error になった。
|
||||
- これは “author does not need to hand-write raw pointer/length ABI code” と “template/sample compile or honest validated fixture boundary” の Ticket contract に反する。
|
||||
|
||||
2. Embedded template を source tree 内で Cargo package として check できない。
|
||||
- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown` が root workspace 配下の non-member package として失敗した。
|
||||
- Template を workspace member にするか、workspace exclude / embedded template 側の empty `[workspace]` 等で standalone fixture として check 可能にする必要がある。
|
||||
- 現在の shape tests は `wit_bindgen::generate!` や WIT parse を実行していないため、この問題を捕捉できていない。
|
||||
|
||||
Required fix:
|
||||
- Current WIT を `wit-bindgen` が parse できるよう修正する。`list` は WIT keyword なので `%list` escape など、runtime import name semantics を壊さない形を優先すること。
|
||||
- Embedded template を in-place または controlled fixture copy で Cargo check できるようにする。
|
||||
- `wit_bindgen::generate!` against `resources/plugin/wit` を実際に通す test/probe を追加する。string-shape assertion だけでは不可。
|
||||
- Template/example/PDK docs が full build boundary と deferral を正直に表現していることを確認する。
|
||||
|
||||
Reviewer validation:
|
||||
- Passed:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check 5f7f81bd..HEAD`
|
||||
- `cargo test -p yoi-plugin-pdk`
|
||||
- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape`
|
||||
- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder`
|
||||
- `cargo check`
|
||||
- `cargo tree -p yoi-plugin-pdk --edges normal`
|
||||
- `nix build .#yoi --no-link`
|
||||
- Failed reviewer probes:
|
||||
- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown`: workspace membership/standalone failure。
|
||||
- copied template probe with adjusted relative paths and empty `[workspace]`: WIT parse failure at `resources/plugin/wit/yoi-host-v1.wit` because `list` is keyword。
|
||||
|
||||
Worktree status at review end: source tree clean; ignored artifacts under `target/` only。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T05:40:15Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder r1-fix report received from `yoi-coder-00001KVHKWNQA`.
|
||||
|
||||
New fix commit:
|
||||
- `0a9e585c plugin: fix rust pdk wit template probes`
|
||||
|
||||
Branch commits now:
|
||||
- `0a9e585c plugin: fix rust pdk wit template probes`
|
||||
- `06287aca plugin: add rust pdk template`
|
||||
|
||||
Fix summary:
|
||||
- Moved host WIT to dependency package path `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit` so `resources/plugin/wit` is a valid WIT package directory with `yoi:host` resolved as dependency。
|
||||
- Escaped WIT keyword function as `%list` while preserving import name semantics。
|
||||
- Added empty `[workspace]` to embedded template `Cargo.toml` so it can be checked in-place without becoming an undeclared member of Yoi root workspace。
|
||||
- Updated template/example/docs/PDK snippets to the compile-tested binding pattern with PDK `wit_bindgen` re-export, `generate_all`, and `runtime_path: "yoi_plugin_pdk::wit_bindgen::rt"`。
|
||||
- Added compile-time probe `crates/plugin-pdk/tests/wit_bindgen_probe.rs` that runs `wit_bindgen::generate!` against `resources/plugin/wit`。
|
||||
- Added embedded template cargo-check probe for `wasm32-unknown-unknown` in `crates/plugin-pdk/tests/template.rs`。
|
||||
- Added `%list` WIT assertion and updated host WIT include path。
|
||||
- Updated `Cargo.lock` and `package.nix` for new dev dependency edge / cargo hash。
|
||||
|
||||
Coder validation reported as passing:
|
||||
- `cargo test -p yoi-plugin-pdk` including PDK tests, WIT constants, actual `wit_bindgen::generate!` probe, and embedded template wasm32 check probe。
|
||||
- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape`
|
||||
- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder`
|
||||
- `cargo check`
|
||||
- `cargo tree -p yoi-plugin-pdk --edges normal`
|
||||
- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check 5f7f81bd..HEAD`
|
||||
- `git diff --check`
|
||||
- `git diff --check --cached`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Operational note:
|
||||
- `nix build .#yoi --no-link` initially hit disk exhaustion after accepting the updated vendor hash; Coder removed local implementation worktree `target/` cache and reran successfully。
|
||||
|
||||
Remaining deferrals:
|
||||
- No `yoi plugin new/check/pack`。
|
||||
- No crates.io publication or remote template fetching。
|
||||
- Full packaged component execution remains deferred; the current WIT parses with `wit-bindgen` and embedded Rust template compiles to the wasm target boundary。
|
||||
|
||||
Orchestrator evidence checked before r2 review:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `0a9e585c`。
|
||||
- Fix diff from `06287aca..HEAD` touches 12 files, about 105 insertions / 13 deletions。
|
||||
- Overall `git diff --check 5f7f81bd..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Route back to Reviewer for r2 verification of the WIT parse/template compile blockers and final acceptance criteria。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVHKWNQA-r1 at: 2026-06-20T05:47:13Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Verdict: `approve`
|
||||
|
||||
確認範囲:
|
||||
- Ticket body/thread / Orchestrator IntentPacket
|
||||
- r2 fix diff: `06287aca..0a9e585c`
|
||||
- 全体実装 diff: `5f7f81bd..0a9e585c`
|
||||
- 主な対象: `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit`, `resources/plugin/wit/yoi-plugin-tool-v1.wit`, embedded template, `crates/plugin-pdk/*`, `crates/manifest/src/plugin.rs`, `crates/pod/src/feature/plugin.rs`, docs, `package.nix`。
|
||||
|
||||
Previous blockers verification:
|
||||
- Host WIT は `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit` に配置され、`resources/plugin/wit` は current package directory として `wit-bindgen` が parse できる形になった。
|
||||
- Keyword function は `%list` として escape され、import name semantics を保持している。
|
||||
- Embedded template の `Cargo.toml` には empty `[workspace]` があり、root workspace 配下の non-member package として拒否されない。
|
||||
- `crates/plugin-pdk/tests/wit_bindgen_probe.rs` は実際に `wit_bindgen::generate!` を `resources/plugin/wit` に対して実行している。
|
||||
- Embedded template は `wasm32-unknown-unknown` target で cargo-check できる。
|
||||
|
||||
Overall Ticket fit:
|
||||
- PDK は guest-side に留まり、host/runtime Yoi crates への dependency は見当たらない。
|
||||
- PDK / template は authority を付与せず、host-side Plugin manifest grants が authority boundary であることを維持している。
|
||||
- Ambient fs/network/env authority は露出していない。
|
||||
- crates.io publication / remote template fetch / `yoi plugin new/check/pack` は実装していない。
|
||||
- Docs は full build/pack boundary と deferral を概ね正直に記述している。
|
||||
|
||||
Blocking issues: none。
|
||||
|
||||
Non-blocking concerns / follow-ups:
|
||||
- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown` を直接実行すると template-local `Cargo.lock` と `target/` ができる。Reviewer は検証後に untracked artifacts を削除した。テスト自体は temporary target dir を使い template lockfile を消すため blocking ではないが、将来 docs/CI では explicit `--target-dir` を推奨するとよい。
|
||||
- Reviewer r2 の `nix build .#yoi --no-link` は final artifact copy 中に `No space left on device` で environment-fail。cargo hash/source-filter/code diagnostic ではない。Coder 側では同コマンド成功済み。Orchestrator final validation で再確認する。
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo test -p yoi-plugin-pdk`: passed
|
||||
- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape`: passed
|
||||
- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder`: passed
|
||||
- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown`: passed
|
||||
- `cargo tree -p yoi-plugin-pdk --edges normal`: passed; host/runtime Yoi crate dependencyなし
|
||||
- `cargo fmt --check`: passed
|
||||
- `git diff --check 5f7f81bd..HEAD`: passed
|
||||
- `git diff --check`: passed
|
||||
- `git diff --check --cached`: passed
|
||||
- `cargo check`: passed
|
||||
- `nix build .#yoi --no-link`: attempted, environment failed with `No space left on device` during final artifact copying
|
||||
|
||||
Worktree status at review end: source tree clean; ignored artifacts under normal `target/` only。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T05:51:33Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Merge and final validation report:
|
||||
|
||||
Merged implementation branch into Orchestrator branch:
|
||||
- Merge commit: `edc53a6b merge: plugin rust pdk templates`
|
||||
- Implementation commits included:
|
||||
- `06287aca plugin: add rust pdk template`
|
||||
- `0a9e585c plugin: fix rust pdk wit template probes`
|
||||
|
||||
Reviewer outcome:
|
||||
- r1 requested changes for WIT parse failure (`list` keyword) and embedded template Cargo workspace check failure。
|
||||
- Coder fixed WIT package layout / `%list` escaping, added actual `wit_bindgen::generate!` probe, and made the embedded template standalone-checkable。
|
||||
- r2 approved with no blocking issues。
|
||||
|
||||
Orchestrator validation after merge passed:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p yoi-plugin-pdk`
|
||||
- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape`
|
||||
- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder`
|
||||
- `cargo check`
|
||||
- `cargo tree -p yoi-plugin-pdk --edges normal`
|
||||
- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-o9gvGb.log`
|
||||
|
||||
Package impact:
|
||||
- `nix path-info -S .#yoi`: `112156384`
|
||||
|
||||
Cleanup note:
|
||||
- Direct template cargo-check creates local `resources/plugin/templates/rust-component-tool/Cargo.lock` and `target/`; Orchestrator removed those untracked artifacts after validation. Worktree is clean。
|
||||
|
||||
Final state:
|
||||
- Orchestrator worktree clean at `edc53a6b` after validation。
|
||||
- Implementation worktree remains available for cleanup after Ticket completion records are committed。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T05:51:42Z from: inprogress to: done reason: merged_reviewed_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation was merged into Orchestrator branch at `edc53a6b`, r2 review approved, and final Orchestrator validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, focused PDK/manifest/pod tests, `cargo check`, PDK dependency tree check, embedded template wasm32 check, and `nix build .#yoi --no-link`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-20T05:53:20Z from: done to: closed reason: closed field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を closed にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-20T05:53:20Z status: closed -->
|
||||
|
||||
## 完了
|
||||
|
||||
## Resolution
|
||||
|
||||
`00001KVHKWNQA` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- Guest-side Rust PDK crate `yoi-plugin-pdk` を追加しました。
|
||||
- PDK は typed JSON input/output helper、bounded `ToolError`、`ToolContext`、`run_json_tool` 系 helper、`wit_bindgen` re-export、`export_component_tool!` macro を提供します。
|
||||
- PDK は host/runtime Yoi crates に依存せず、authority を付与しません。Host-side Plugin manifest grants が Tool execution / host API use の authority boundary のままです。
|
||||
- Embedded Rust Component Tool template を `resources/plugin/templates/rust-component-tool/` に追加しました。
|
||||
- Template は local checkout/dev path dependency を使い、future out-of-tree git `rev` pattern を docs に記録しています。
|
||||
- `resources/plugin/wit` を `wit-bindgen` が parse できる package layout に修正し、host WIT dependency を `resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit` に移動しました。
|
||||
- WIT keyword `list` は `%list` escape にし、import name semantics を保持しました。
|
||||
- Embedded template は empty `[workspace]` により in-tree standalone package として check できます。
|
||||
- `wit_bindgen::generate!` を実際に `resources/plugin/wit` に対して実行する probe と、embedded template の `wasm32-unknown-unknown` cargo-check probe を追加しました。
|
||||
- Plugin development docs / design docs / package docs / example source を更新しました。
|
||||
- `yoi plugin new/check/pack`、remote template fetch、crates.io publication、full packaged component execution はこの Ticket の non-goals / follow-up として残しました。
|
||||
|
||||
主な commit:
|
||||
- `06287aca plugin: add rust pdk template`
|
||||
- `0a9e585c plugin: fix rust pdk wit template probes`
|
||||
- `edc53a6b merge: plugin rust pdk templates`
|
||||
|
||||
Review:
|
||||
- r1 は WIT parse failure と embedded template Cargo workspace issue で `request_changes`。
|
||||
- Coder が WIT layout / `%list` / template `[workspace]` / actual probes を追加。
|
||||
- r2 は `approve`。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p yoi-plugin-pdk`
|
||||
- `cargo test -p manifest embedded_rust_component_tool_template_is_valid_package_shape`
|
||||
- `cargo test -p pod pdk_tool_output_shape_is_accepted_by_wasm_decoder`
|
||||
- `cargo check`
|
||||
- `cargo tree -p yoi-plugin-pdk --edges normal`
|
||||
- `cargo check --manifest-path resources/plugin/templates/rust-component-tool/Cargo.toml --target wasm32-unknown-unknown`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Package impact:
|
||||
- `nix path-info -S .#yoi`: `112156384`
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-o9gvGb.log`
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHKWNQS/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHKWNQS/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"id":"orch-plan-20260620-052336-1","ticket_id":"00001KVHKWNQS","kind":"blocked_by","related_ticket":"00001KVHKWNQA","note":"Panel Queue was accepted for routing review, but implementation cannot start yet because `00001KVHKWNQS` depends on `00001KVHKWNQA` Rust PDK/templates, which is currently `inprogress` and in a reviewer-requested-changes loop. Leave `00001KVHKWNQS` queued until `00001KVHKWNQA` is closed, then reroute.","author":"yoi-orchestrator","at":"2026-06-20T05:23:36Z"}
|
||||
{"id":"orch-plan-20260620-055356-2","ticket_id":"00001KVHKWNQS","kind":"accepted_plan","accepted_plan":{"summary":"`yoi plugin new rust-component-tool`, `yoi plugin check`, `yoi plugin pack` を追加する。new は embedded template only、check/pack は Plugin code を実行せず、directory/package safety、manifest/runtime/schema/permission diagnostics、deterministic digest/package output、JSON reports、enablement guidance を提供する。","branch":"impl/00001KVHKWNQS-plugin-authoring-cli","worktree":"/home/hare/Projects/yoi/.worktree/00001KVHKWNQS-plugin-authoring-cli","role_plan":"Orchestrator は acceptance records を commit 後、専用 implementation worktree `.worktree/00001KVHKWNQS-plugin-authoring-cli` を作成し、Coder をその child worktree への narrow write scope で起動する。Coder 実装後、Reviewer が non-execution checks、archive safety、deterministic pack/digest、JSON report stability、template use、workspace/config non-mutation、Nix/resource packaging を確認する。"},"author":"yoi-orchestrator","at":"2026-06-20T05:53:56Z"}
|
||||
29
.yoi/tickets/00001KVHKWNQS/artifacts/relations.json
Normal file
29
.yoi/tickets/00001KVHKWNQS/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHKWNQS",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVHKWNQA",
|
||||
"note": "Authoring new/check/pack uses the Rust PDK and embedded templates.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T04:17:24Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHKWNQS",
|
||||
"kind": "related",
|
||||
"target": "00001KVFD3YSV",
|
||||
"note": "Authoring check/pack diagnostics should align with plugin list/show inspection.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T04:17:24Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHKWNQS",
|
||||
"kind": "related",
|
||||
"target": "00001KVG0HR96",
|
||||
"note": "Authoring CLI validates Component Model Tool package metadata.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T04:17:24Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
93
.yoi/tickets/00001KVHKWNQS/item.md
Normal file
93
.yoi/tickets/00001KVHKWNQS/item.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
---
|
||||
title: 'Plugin: add authoring CLI new/check/pack'
|
||||
state: 'closed'
|
||||
created_at: '2026-06-20T04:16:14Z'
|
||||
updated_at: '2026-06-20T06:55:06Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['plugin', 'cli', 'authoring', 'templates', 'package-validation', 'packaging', 'read-only-check']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T05:23:14Z'
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
After the Rust PDK and embedded templates exist, independent Plugin developers need first-party CLI tooling to scaffold, validate, and package Plugins without relying on remote shell scripts or hand-written ZIP commands.
|
||||
|
||||
This Ticket adds authoring commands for local Plugin development. It complements read-only operational inspection (`yoi plugin list/show`) but serves a different audience: Plugin authors preparing a package before enabling it in Yoi.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add Plugin authoring subcommands to the product CLI.
|
||||
- `yoi plugin new rust-component-tool <path-or-name>`
|
||||
- `yoi plugin check <path-or-package>`
|
||||
- `yoi plugin pack <path> [--output <file>]`
|
||||
- `new` uses embedded templates only.
|
||||
- No remote template fetch.
|
||||
- No `curl | sh` flow.
|
||||
- Generated files should include `Cargo.toml`, `src/lib.rs`, `plugin.toml`, and README/next-steps.
|
||||
- Generated dependency should be appropriate for the current checkout/release mode: local path in checkout mode, or documented git rev/tag pattern if out-of-tree.
|
||||
- `check` validates a Plugin directory or `.yoi-plugin` package without executing Plugin code.
|
||||
- Parse `plugin.toml`.
|
||||
- Validate package id/version/source-compatible shape.
|
||||
- Validate runtime kind and referenced artifact presence.
|
||||
- Validate Component Model world metadata where possible.
|
||||
- Validate Tool schema shape.
|
||||
- Validate requested permissions / host API declarations.
|
||||
- Validate archive safety for packages: path traversal, root escape, unsupported compression, bounded file count/size.
|
||||
- Calculate and print deterministic digest.
|
||||
- Produce actionable diagnostics and a suggested enablement/grant snippet.
|
||||
- `pack` creates a deterministic `.yoi-plugin` package.
|
||||
- Include required manifest/runtime files.
|
||||
- Use currently supported archive format, including stored entries if compression is not supported.
|
||||
- Reject unsafe paths / root escapes.
|
||||
- Print output path and digest.
|
||||
- Do not modify workspace enablement config.
|
||||
- Provide JSON output for automation where useful.
|
||||
- At minimum `check --json` and `pack --json`.
|
||||
- Keep commands safe.
|
||||
- `check` and `pack` do not execute Plugin code.
|
||||
- `new` only writes into the requested destination and refuses to overwrite non-empty directories unless an explicit safe option is added.
|
||||
- No secrets are generated or embedded.
|
||||
- Integrate with existing inspection language.
|
||||
- Diagnostics/statuses should align with `yoi plugin list/show` where possible.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `yoi plugin new rust-component-tool ./my-plugin` creates a usable template without network access.
|
||||
- `yoi plugin check ./my-plugin` validates the generated template and reports next steps/digest/enablement guidance.
|
||||
- `yoi plugin pack ./my-plugin` creates a `.yoi-plugin` package that Yoi discovery can read.
|
||||
- `check` can validate an existing `.yoi-plugin` archive.
|
||||
- `check --json` returns a stable typed report suitable for tests/agents.
|
||||
- `pack --json` returns output path and digest.
|
||||
- Unsafe package paths / traversal / unsupported compression / missing manifest / missing runtime artifact are rejected with clear diagnostics.
|
||||
- Commands do not execute Plugin code or mutate enablement config.
|
||||
- Tests cover:
|
||||
- `new` generated file set;
|
||||
- refusal to overwrite non-empty destination;
|
||||
- `check` valid directory;
|
||||
- `check` invalid manifest;
|
||||
- `check` missing runtime artifact;
|
||||
- `check` unsafe package archive;
|
||||
- `pack` deterministic digest;
|
||||
- `pack` package is discoverable by existing Plugin discovery;
|
||||
- JSON report shape.
|
||||
- Validation: focused CLI/plugin authoring tests, relevant `cargo check` / `cargo test`, `cargo fmt --check`, `git diff --check`, and `nix build .#yoi` because product CLI/resources/packaging behavior changes.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Remote template fetching.
|
||||
- Publishing or installing from a registry.
|
||||
- Enabling/disabling Plugins in workspace config.
|
||||
- Executing Plugin code during `check`.
|
||||
- crates.io publication of PDK.
|
||||
- Service / Ingress scaffolding.
|
||||
- Multi-language templates beyond Rust Component Model Tool.
|
||||
|
||||
## Related work
|
||||
|
||||
- `00001KVG0HR9M` — Objective: Plugin platform roadmap.
|
||||
- `00001KVHKWNQA` — Rust PDK and embedded authoring templates.
|
||||
- `00001KVFD3YSV` — Plugin read-only CLI inspection list/show.
|
||||
- `00001KVG0HR96` — Plugin Component Model runtime.
|
||||
- `docs/development/plugin-development.md` — current Plugin development guide.
|
||||
41
.yoi/tickets/00001KVHKWNQS/resolution.md
Normal file
41
.yoi/tickets/00001KVHKWNQS/resolution.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
## Resolution
|
||||
|
||||
`00001KVHKWNQS` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- `yoi plugin new rust-component-tool <path-or-name>` を追加しました。
|
||||
- `yoi plugin check <path-or-package> [--json]` を追加しました。
|
||||
- `yoi plugin pack <path> [--output <file>] [--json]` を追加しました。
|
||||
- Safe directory/package reading、deterministic digesting、deterministic `.yoi-plugin` writing、symlink/root-escape rejection を含む materialized package helpers を追加しました。
|
||||
- `check` / `pack` は Plugin code を実行せず、既存 static Plugin inspection を再利用して manifest/runtime/schema/permission/host API declarations を検査します。
|
||||
- Embedded Rust Component Tool template を `new` で利用し、generated template を check/pack できるよう placeholder `plugin.component.wasm` を追加しました。
|
||||
- Placeholder artifact は `check` で検出され、generated template / packed archive は `partial` と bounded diagnostic を返します。placeholder が残る間は enablement-ready guidance を出しません。
|
||||
- `plugin new` は existing destination symlink を拒否し、write-through を防ぎます。
|
||||
- JSON report shape、human output、CLI help/docs を更新しました。
|
||||
- Focused tests と CLI smoke coverage を追加しました。
|
||||
|
||||
主な commit:
|
||||
- `945ecdf6 plugin: add authoring cli`
|
||||
- `699db538 plugin: harden authoring checks`
|
||||
- `87704ad4 merge: plugin authoring cli`
|
||||
|
||||
Review:
|
||||
- r1 は destination symlink write-through と placeholder artifact の enablement-ready 表示で `request_changes`。
|
||||
- Coder が symlink refusal、placeholder detection、`partial` status/diagnostics、tests/docs を追加。
|
||||
- r2 は `approve`。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo test -p yoi plugin_cli`
|
||||
- `cargo test -p yoi-plugin-pdk template`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
補足:
|
||||
- 初回 `nix build .#yoi --no-link` は `aws-lc-sys` build 中に `No space left on device` で environment failure。
|
||||
- Orchestrator worktree の Cargo build artifacts を `cargo clean` で削除してから再実行し、Nix build は成功しました。
|
||||
- `nix path-info -S .#yoi`: `112260512`
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-Q0KE3A.log`
|
||||
427
.yoi/tickets/00001KVHKWNQS/thread.md
Normal file
427
.yoi/tickets/00001KVHKWNQS/thread.md
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T04:16:14Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-20T05:23:14Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T05:23:44Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: blocked_pending_dependency
|
||||
|
||||
Panel Queue により、この Ticket は Orchestrator routing 対象として明示許可されたが、実装開始はまだ行わない。
|
||||
|
||||
Reason:
|
||||
- `00001KVHKWNQS` は `00001KVHKWNQA` に `depends_on` している。
|
||||
- `00001KVHKWNQA` は現在 `inprogress` で、Rust PDK / embedded template 実装が r1 review の `request_changes` 修正ループ中。
|
||||
- この Ticket の `new/check/pack` は PDK crate / embedded template を使うため、PDK/template が closed になるまで authoring CLI implementation の worktree/Pod side effects は開始しない。
|
||||
- Orchestrator workspace は clean、queued はこの Ticket のみ、inprogress は `00001KVHKWNQA` のみ、matching branch/worktree は存在しない。
|
||||
|
||||
Action:
|
||||
- `00001KVHKWNQS` は `queued` のまま保持する。
|
||||
- `00001KVHKWNQA` が closed になった後、改めて body/thread/relations/workspace state を確認して routing する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T05:54:38Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Panel Queue により、この Ticket は Orchestrator routing 対象として明示許可された。
|
||||
- 前回は `00001KVHKWNQA` Rust PDK/templates が inprogress だったため blocked/queued hold としたが、現在 `00001KVHKWNQA` は closed。
|
||||
- Ticket body は `new` / `check` / `pack` の CLI surface、non-execution safety、archive safety、deterministic digest/package、JSON reports、diagnostics、tests、validation、non-goals を実装可能な粒度で定義している。
|
||||
- Related Plugin CLI inspection (`00001KVFD3YSV`) と Component runtime (`00001KVG0HR96`) は closed。
|
||||
- 現在 queued はこの Ticket のみ、inprogress は 0 件、child implementation Pods はなし、matching branch/worktree はなし、Orchestrator worktree は clean。
|
||||
- Risk domain は plugin / CLI / authoring / templates / package-validation / packaging / read-only-check だが、Ticket は check/pack が Plugin code を実行しない、new は embedded templates only、enablement config を mutate しない、safe overwrite refusal、archive traversal/root-escape rejection などの invariants を明示している。bounded context check 後も implementation 前に必要な追加 human decision は見つからなかった。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket `00001KVHKWNQS` body / thread / relations / artifacts。
|
||||
- `TicketRelationQuery(00001KVHKWNQS)`: outgoing `depends_on 00001KVHKWNQA` is now closed。Related records are closed context。
|
||||
- `TicketOrchestrationPlanQuery(00001KVHKWNQS)`: previous `blocked_by` plan is resolved by `00001KVHKWNQA` closure; accepted plan recorded now。
|
||||
- Workspace state:
|
||||
- Orchestrator worktree clean at `902b383d`。
|
||||
- queued: this Ticket only。
|
||||
- inprogress: 0。
|
||||
- visible Pods: self + peers only; spawned children 0。
|
||||
- no matching implementation branch/worktree。
|
||||
- Code/resource context:
|
||||
- Rust PDK/template resources are now merged from `00001KVHKWNQA`。
|
||||
- Component Model runtime and Plugin CLI inspection work are closed and available as implementation context。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Add first-party local Plugin authoring CLI commands: `yoi plugin new rust-component-tool <path-or-name>`, `yoi plugin check <path-or-package>`, and `yoi plugin pack <path> [--output <file>]`。
|
||||
- Make local authoring safe and deterministic without remote scripts, without executing Plugin code during validation, and without mutating workspace enablement config。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- `new` uses embedded templates only; no network, no remote template fetch, no `curl | sh` flow。
|
||||
- `new` writes only to the requested destination and refuses non-empty destinations unless a narrow explicit safe option is intentionally added。
|
||||
- Generated Rust Component Tool template should use the current PDK/template resources and current checkout/release dependency policy。
|
||||
- `check` and `pack` must not execute Plugin code or instantiate components。
|
||||
- `check` validates directory and `.yoi-plugin` package inputs with bounded diagnostics and stable JSON report shape for `--json`。
|
||||
- `pack` creates deterministic `.yoi-plugin` output and prints digest/path; `pack --json` returns stable typed output。
|
||||
- `check` validates manifest/runtime/schema/permission/host API declarations, referenced artifact presence, archive safety, and deterministic digest where applicable。
|
||||
- `pack` rejects unsafe paths/root escapes and unsupported package shapes; use currently supported archive format/constraints。
|
||||
- Commands do not mutate enablement/workspace config and do not generate/embed secrets。
|
||||
- Diagnostics/status language should align with existing `yoi plugin list/show` where possible。
|
||||
- Do not implement registry publish/install, enabling/disabling config, Plugin execution, Service/Ingress scaffolding, or extra language templates。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- `yoi plugin new rust-component-tool ./my-plugin` creates the expected file set from embedded template without network access。
|
||||
- `yoi plugin check ./my-plugin` validates the generated template and reports next steps/digest/enablement guidance。
|
||||
- `yoi plugin pack ./my-plugin` creates a `.yoi-plugin` package discoverable by existing Plugin discovery。
|
||||
- `check` validates existing `.yoi-plugin` archive inputs and rejects unsafe archives。
|
||||
- `check --json` and `pack --json` provide stable typed reports for tests/agents。
|
||||
- Tests cover generated file set, overwrite refusal, valid/invalid check paths, missing runtime artifact, unsafe package archive, deterministic pack digest, discoverability by existing Plugin discovery, and JSON report shape。
|
||||
- Validation includes focused CLI/plugin authoring tests, relevant `cargo check` / `cargo test`, `cargo fmt --check`, `git diff --check`, and `nix build .#yoi` because CLI/resources/package behavior changes。
|
||||
|
||||
Escalate if:
|
||||
- Current package/archive format lacks enough existing helpers and needs a broader package-format decision。
|
||||
- `check` cannot validate Component world metadata without executing code and needs explicit scope narrowing。
|
||||
- Deterministic packaging conflicts with current discovery/package reader behavior。
|
||||
- Embedded template resource packaging/source filtering needs broader product decision。
|
||||
|
||||
Next action:
|
||||
- Record `queued -> inprogress` and commit Ticket records before creating the implementation worktree and spawning Coder。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T05:54:45Z from: queued to: inprogress reason: orchestrator_acceptance_plugin_authoring_cli field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket body/thread, relation metadata, resolved Rust PDK dependency, related closed Tickets, Orchestrator worktree state, visible Pods, existing branch/worktree, and bounded CLI/resource/package context were checked. Dependency `00001KVHKWNQA` is closed, no inprogress/capacity blocker exists, and no missing planning decision was found. Accepting this queued Ticket for implementation before worktree/Pod side effects.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T05:55:31Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration start:
|
||||
- Acceptance records were committed on Orchestrator branch: `d1095f85 ticket: accept plugin authoring cli`。
|
||||
- Created implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVHKWNQS-plugin-authoring-cli` on branch `impl/00001KVHKWNQS-plugin-authoring-cli` at `d1095f85`。
|
||||
- Spawned Coder Pod `yoi-coder-00001KVHKWNQS` with write scope limited to the implementation worktree and read-only root scope only for runtime workspace identity。
|
||||
- Coder task explicitly forbids root/original workspace implementation, merge, Ticket close, and cleanup。
|
||||
|
||||
Next action:
|
||||
- Wait for Coder implementation report, then inspect branch diff/validation evidence and route to Reviewer。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T06:25:39Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder implementation report received from `yoi-coder-00001KVHKWNQS`.
|
||||
|
||||
Implementation commit:
|
||||
- `945ecdf6 plugin: add authoring cli`
|
||||
|
||||
Changed areas reported:
|
||||
- Added `yoi plugin new rust-component-tool <path-or-name>`。
|
||||
- Added `yoi plugin check <path-or-package> [--json]`。
|
||||
- Added `yoi plugin pack <path> [--output <file>] [--json]`。
|
||||
- Added manifest-level materialized package helpers for safe directory/package reading, deterministic digesting, deterministic stored `.yoi-plugin` writing, and symlink/root-escape rejection。
|
||||
- Reused existing static Plugin inspection for Tool schema / permission / host API declaration checks without Plugin execution。
|
||||
- Added embedded `plugin.component.wasm` placeholder to Rust component Tool template so generated templates can be checked/packed immediately; docs instruct replacing it before enabling/execution。
|
||||
- Updated CLI parsing/help and Plugin development docs。
|
||||
- Added focused tests for generated file set, overwrite refusal, valid directory check, invalid manifest, missing runtime artifact, unsafe package archive, deterministic pack digest, package discoverability, and JSON report shape。
|
||||
|
||||
Coder validation reported:
|
||||
- `cargo fmt`
|
||||
- `cargo fmt --check`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo test -p yoi plugin_cli`
|
||||
- `cargo test -p yoi-plugin-pdk template`
|
||||
- `git diff --check`
|
||||
- CLI smoke:
|
||||
- `cargo run -q -p yoi -- plugin new rust-component-tool ... --json`
|
||||
- `cargo run -q -p yoi -- plugin check ... --json`
|
||||
- `cargo run -q -p yoi -- plugin pack ... --output ... --json`
|
||||
- invalid check reports `"status": "rejected"` and exits non-zero。
|
||||
- `nix build .#yoi --no-link` was attempted but failed due environment/storage: `No space left on device`。
|
||||
- No Cargo dependency or `Cargo.lock` changes were introduced。
|
||||
|
||||
Known risks / deferrals reported:
|
||||
- Component validation remains metadata/static only; no component instantiation/execution is performed by design。
|
||||
- Template includes a placeholder artifact solely for deterministic authoring check/pack; docs instruct replacing it before enabling/execution。
|
||||
|
||||
Orchestrator evidence checked before review dispatch:
|
||||
- Implementation worktree is clean on `impl/00001KVHKWNQS-plugin-authoring-cli`。
|
||||
- HEAD is `945ecdf6`。
|
||||
- Diff from acceptance `d1095f85..HEAD` is one implementation commit touching 6 files, about 1274 insertions / 24 deletions。
|
||||
- `git diff --check d1095f85..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Dispatch Reviewer for r1 review against Ticket requirements, with focus on non-execution safety, archive/path traversal safety, deterministic pack/digest behavior, placeholder artifact policy, JSON report stability, discoverability, workspace/config non-mutation, CLI UX, tests, and Nix/package validation feasibility。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: yoi-orchestrator at: 2026-06-20T06:26:24Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Review dispatch:
|
||||
- Spawned Reviewer Pod `yoi-reviewer-00001KVHKWNQS-r1` against implementation branch `impl/00001KVHKWNQS-plugin-authoring-cli`。
|
||||
- Review target commit: `945ecdf6 plugin: add authoring cli`。
|
||||
- Review baseline: `d1095f85`。
|
||||
- Reviewer task focuses on non-execution safety, archive/path traversal safety, deterministic pack/digest behavior, placeholder artifact policy, JSON report stability, discoverability, workspace/config non-mutation, CLI UX, tests, and Nix/package validation feasibility。
|
||||
- Reviewer is instructed not to edit source, commit, merge, close the Ticket, or use TicketReview directly; it will report verdict/evidence back to Orchestrator。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVHKWNQS-r1 at: 2026-06-20T06:36:52Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Verdict: `request_changes`
|
||||
|
||||
確認範囲:
|
||||
- Ticket body/thread / Orchestrator IntentPacket
|
||||
- Implementation diff: `d1095f85..945ecdf6`
|
||||
- 主な対象: `crates/yoi/src/plugin_cli.rs`, `crates/yoi/src/main.rs`, `crates/manifest/src/plugin.rs`, `resources/plugin/templates/rust-component-tool/*`, `docs/development/plugin-development.md`, `package.nix`。
|
||||
|
||||
Blocking issues:
|
||||
1. `yoi plugin new` が symlink destination を辿り、物理的な requested destination 外へ書き込める。
|
||||
- `materialize_template` は `fs::metadata(destination)` / `fs::read_dir(destination)` を使い、destination symlink を拒否せず `destination.join(relative)` に書く。
|
||||
- Reviewer smoke: `linkdest -> target` に対して `plugin new rust-component-tool "$tmp/linkdest"` を実行すると、`target/plugin.toml` が作られた。
|
||||
- Ticket invariant: `new` は requested destination にだけ書き、non-empty destination を拒否する。symlink destination はこの境界を破る。
|
||||
|
||||
2. Generated placeholder component が `check` で enablement-ready に見える。
|
||||
- Template は text placeholder `resources/plugin/templates/rust-component-tool/plugin.component.wasm` を含む。
|
||||
- `new` output は next steps で一度 warning するが、fresh generated template に対する `check --json` は `status=active diagnostics=0` になり、generic enablement guidance を出す。
|
||||
- Reviewer smoke: generated template check output に placeholder/non-executable warning がなく、`To enable after review...` guidance が出る。
|
||||
- Ticket/reviewer contract: check/pack may pass, but output must not imply executable component is ready for enablement if placeholder is not executable。
|
||||
|
||||
Required fixes:
|
||||
- `plugin new` destination symlink を `symlink_metadata` 等で拒否し、symlink write-through を防ぐ。関連する overwrite/destination tests を追加する。
|
||||
- Generated placeholder artifact を `check` が検出し、bounded diagnostic / next step を出す。Status は `partial` など enablement-ready でない表現にすることを優先する。少なくとも `active diagnostics=0` と enablement guidance の組み合わせは避ける。
|
||||
- `check --json` and human output tests should cover generated template placeholder warning/status。
|
||||
|
||||
Non-blocking concerns:
|
||||
- `nix build .#yoi --no-link` は environment storage (`No space left on device`) で失敗。source-filter/resource-packaging issue は見えていないが、最終 Orchestrator validation で再試行する。
|
||||
- `pack --json` digest is nested under `package.digest`, not top-level. Acceptable if documented/stable, but consider documenting shape。
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo fmt --check`: passed
|
||||
- `git diff --check d1095f85..HEAD`: passed
|
||||
- `cargo check -p yoi`: passed
|
||||
- `cargo test -p yoi plugin_cli`: passed (`15 tests`)
|
||||
- `cargo test -p yoi-plugin-pdk template`: passed
|
||||
- CLI smoke for `new`, `check --json`, `pack --json`, packed package check/list discovery, invalid check rejected: passed except blocker findings above。
|
||||
- `nix build .#yoi --no-link`: failed due `No space left on device`。
|
||||
|
||||
Worktree status at review end: clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T06:46:09Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder r1-fix report received from `yoi-coder-00001KVHKWNQS`.
|
||||
|
||||
New fix commit:
|
||||
- `699db538 plugin: harden authoring checks`
|
||||
|
||||
Branch commits now:
|
||||
- `699db538 plugin: harden authoring checks`
|
||||
- `945ecdf6 plugin: add authoring cli`
|
||||
|
||||
Fix summary:
|
||||
- `plugin new` rejects an existing destination symlink via `symlink_metadata` before writes。
|
||||
- Added regression test ensuring symlink destination is refused and target is not written。
|
||||
- `plugin check` detects embedded generated placeholder `plugin.component.wasm`。
|
||||
- Fresh generated templates report `status: "partial"` instead of `active`。
|
||||
- JSON/human diagnostics include bounded placeholder warning。
|
||||
- Enablement guidance no longer says a package is ready to enable while placeholder artifact remains。
|
||||
- `partial` checks exit successfully; `rejected` checks remain non-zero。
|
||||
- Docs updated for symlink refusal and placeholder/partial behavior。
|
||||
|
||||
Coder validation reported as passing:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check d1095f85..HEAD`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo test -p yoi plugin_cli`
|
||||
- `cargo test -p yoi-plugin-pdk template`
|
||||
- CLI smoke:
|
||||
- `plugin new ... --json`
|
||||
- `plugin check <generated> --json` => `partial` with placeholder diagnostic。
|
||||
- `plugin pack ... --json` => `packed`。
|
||||
- `plugin check <packed .yoi-plugin> --json` => `partial`。
|
||||
- copied packed archive into `.yoi/plugins` and confirmed `plugin list --workspace ... --json` discovers it。
|
||||
- invalid check after removing runtime artifact => `rejected` and non-zero。
|
||||
- Freed only child-worktree Cargo build artifacts with `cargo clean` and reran `nix build .#yoi --no-link`; it completed successfully。
|
||||
|
||||
Orchestrator evidence checked before r2 review:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `699db538`。
|
||||
- Fix diff from `945ecdf6..HEAD` touches `crates/yoi/src/plugin_cli.rs` and `docs/development/plugin-development.md`, about 124 insertions / 34 deletions。
|
||||
- Overall `git diff --check d1095f85..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Route back to Reviewer for r2 verification of symlink destination refusal, placeholder partial/check behavior, and final acceptance criteria。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVHKWNQS-r1 at: 2026-06-20T06:50:40Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Verdict: `approve`
|
||||
|
||||
確認範囲:
|
||||
- Ticket body/thread / Orchestrator IntentPacket
|
||||
- r2 fix diff: `945ecdf6..699db538`
|
||||
- 全体実装 diff: `d1095f85..699db538`
|
||||
- 主な対象: `crates/yoi/src/plugin_cli.rs`, `docs/development/plugin-development.md`, and prior `new` / `check` / `pack` implementation surface。
|
||||
|
||||
Previous blocker verification:
|
||||
1. `plugin new` symlink destination write-through
|
||||
- `materialize_template` now uses `fs::symlink_metadata` and rejects destination symlink before writing。
|
||||
- Regression test `plugin_new_refuses_symlink_destination_without_following_it` verifies refusal and no write-through。
|
||||
- CLI smoke confirmed symlink destination is rejected and target directory does not receive `plugin.toml`。
|
||||
|
||||
2. Placeholder component reported as enablement-ready
|
||||
- `check` now compares runtime component bytes against embedded template placeholder and adds bounded `placeholder` diagnostic。
|
||||
- Generated template now reports `status=partial` when static validation is otherwise clean but placeholder remains。
|
||||
- Enablement guidance is suppressed for `partial` and says the package is not ready to enable。
|
||||
- CLI smoke confirmed generated directory and packed `.yoi-plugin` both report `partial` with placeholder diagnostic; `partial` exits successfully, while invalid/missing paths remain non-zero rejected。
|
||||
|
||||
Blocking issues: none。
|
||||
|
||||
Non-blocking concerns / follow-ups:
|
||||
- None required for this Ticket. The hardening keeps status vocabulary aligned with existing Plugin inspection (`active` / `partial` / `rejected`)。
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo fmt --check`: passed
|
||||
- `git diff --check d1095f85..HEAD`: passed
|
||||
- `cargo check -p yoi`: passed
|
||||
- `cargo test -p yoi plugin_cli`: passed (`16 tests`)
|
||||
- `cargo test -p yoi-plugin-pdk template`: passed (`2 template tests`)
|
||||
- CLI smoke after `cargo build -p yoi`: passed for `new`, generated `check --json partial`, human `[partial]`, `pack --json`, packed package `check --json partial`, invalid check rejected/non-zero, symlink destination refusal, and `plugin list --workspace ... --json` discoverability as `disabled`。
|
||||
- `nix build .#yoi --no-link`: passed
|
||||
|
||||
Worktree status at review end: source tree clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T06:54:35Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Merge and final validation report:
|
||||
|
||||
Merged implementation branch into Orchestrator branch:
|
||||
- Merge commit: `87704ad4 merge: plugin authoring cli`
|
||||
- Implementation commits included:
|
||||
- `945ecdf6 plugin: add authoring cli`
|
||||
- `699db538 plugin: harden authoring checks`
|
||||
|
||||
Reviewer outcome:
|
||||
- r1 requested changes for symlink destination write-through and placeholder component being reported as enablement-ready。
|
||||
- Coder added symlink destination refusal, placeholder detection, `partial` status / diagnostics, and tests。
|
||||
- r2 approved with no blocking issues。
|
||||
|
||||
Orchestrator validation after merge:
|
||||
- Passed before Nix:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo test -p yoi plugin_cli`
|
||||
- `cargo test -p yoi-plugin-pdk template`
|
||||
- Initial `nix build .#yoi --no-link` failed with environment storage exhaustion while building `aws-lc-sys` (`No space left on device`), not a source/package diagnostic。
|
||||
- Orchestrator freed only Orchestrator-worktree Cargo build artifacts with `cargo clean` (`43.3GiB`) and reran:
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
- `nix path-info -S .#yoi`: `112260512`。
|
||||
|
||||
Validation log for first grouped run:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-Q0KE3A.log`
|
||||
|
||||
Final state:
|
||||
- Orchestrator worktree clean at `87704ad4` after successful Nix validation。
|
||||
- Implementation worktree remains available for cleanup after Ticket completion records are committed。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T06:54:45Z from: inprogress to: done reason: merged_reviewed_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation was merged into Orchestrator branch at `87704ad4`, r2 review approved, and final Orchestrator validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo check -p yoi`, focused `yoi plugin_cli` / `yoi-plugin-pdk template` tests, and `nix build .#yoi --no-link` after freeing Orchestrator worktree build artifacts.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-20T06:55:06Z from: done to: closed reason: closed field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を closed にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-20T06:55:06Z status: closed -->
|
||||
|
||||
## 完了
|
||||
|
||||
## Resolution
|
||||
|
||||
`00001KVHKWNQS` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- `yoi plugin new rust-component-tool <path-or-name>` を追加しました。
|
||||
- `yoi plugin check <path-or-package> [--json]` を追加しました。
|
||||
- `yoi plugin pack <path> [--output <file>] [--json]` を追加しました。
|
||||
- Safe directory/package reading、deterministic digesting、deterministic `.yoi-plugin` writing、symlink/root-escape rejection を含む materialized package helpers を追加しました。
|
||||
- `check` / `pack` は Plugin code を実行せず、既存 static Plugin inspection を再利用して manifest/runtime/schema/permission/host API declarations を検査します。
|
||||
- Embedded Rust Component Tool template を `new` で利用し、generated template を check/pack できるよう placeholder `plugin.component.wasm` を追加しました。
|
||||
- Placeholder artifact は `check` で検出され、generated template / packed archive は `partial` と bounded diagnostic を返します。placeholder が残る間は enablement-ready guidance を出しません。
|
||||
- `plugin new` は existing destination symlink を拒否し、write-through を防ぎます。
|
||||
- JSON report shape、human output、CLI help/docs を更新しました。
|
||||
- Focused tests と CLI smoke coverage を追加しました。
|
||||
|
||||
主な commit:
|
||||
- `945ecdf6 plugin: add authoring cli`
|
||||
- `699db538 plugin: harden authoring checks`
|
||||
- `87704ad4 merge: plugin authoring cli`
|
||||
|
||||
Review:
|
||||
- r1 は destination symlink write-through と placeholder artifact の enablement-ready 表示で `request_changes`。
|
||||
- Coder が symlink refusal、placeholder detection、`partial` status/diagnostics、tests/docs を追加。
|
||||
- r2 は `approve`。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo test -p yoi plugin_cli`
|
||||
- `cargo test -p yoi-plugin-pdk template`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
補足:
|
||||
- 初回 `nix build .#yoi --no-link` は `aws-lc-sys` build 中に `No space left on device` で environment failure。
|
||||
- Orchestrator worktree の Cargo build artifacts を `cargo clean` で削除してから再実行し、Nix build は成功しました。
|
||||
- `nix path-info -S .#yoi`: `112260512`
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-Q0KE3A.log`
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHQDS6B/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHQDS6B/artifacts/.gitkeep
Normal file
21
.yoi/tickets/00001KVHQDS6B/artifacts/relations.json
Normal file
21
.yoi/tickets/00001KVHQDS6B/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHQDS6B",
|
||||
"kind": "related",
|
||||
"target": "00001KVHKWNQA",
|
||||
"note": "Observed prerequisite Ticket already queued/in progress should allow dependent queueing.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:19:32Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHQDS6B",
|
||||
"kind": "related",
|
||||
"target": "00001KVHKWNQS",
|
||||
"note": "Dependent Ticket hit backend conflict despite Panel queue UI allowing it.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:19:32Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
55
.yoi/tickets/00001KVHQDS6B/item.md
Normal file
55
.yoi/tickets/00001KVHQDS6B/item.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
title: 'Panel Queue action should allow ready Tickets whose blockers are already queued or in progress'
|
||||
state: 'done'
|
||||
created_at: '2026-06-20T05:18:00Z'
|
||||
updated_at: '2026-06-20T05:19:32Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['ticket', 'panel', 'queue', 'dependency', 'blocker', 'orchestrator']
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Panel ViewModel already treats a ready Ticket as queueable when all blocking relations point at Tickets that are already `queued` or `inprogress`. That matches the intended workflow: once prerequisite work is queued/accepted by Orchestrator, dependent ready Tickets should also be queueable so the human does not need to return after every prerequisite completes.
|
||||
|
||||
However, pressing Enter in Panel still calls the backend `queue_ready` gate, and that backend rejected any unresolved relation blocker regardless of blocker state. This caused a `ticket conflict` even though the UI correctly showed Queue as available.
|
||||
|
||||
Concrete example:
|
||||
|
||||
- `00001KVHKWNQS` depends on `00001KVHKWNQA`.
|
||||
- `00001KVHKWNQA` is already `inprogress`.
|
||||
- Panel shows `00001KVHKWNQS` as queueable.
|
||||
- Enter/Queue fails with backend ticket conflict.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Backend `queue_ready` must match the Panel gating rule.
|
||||
- A ready Ticket may transition `ready -> queued` when every relation blocker is already `queued` or `inprogress`.
|
||||
- A ready Ticket must still be blocked when any relation blocker is `planning` or `ready`.
|
||||
- This change applies to queueing only.
|
||||
- `queued -> inprogress` acceptance remains blocked while unresolved dependency/blocker relations exist; Orchestrator must preserve execution order after queueing.
|
||||
- Existing Panel display behavior should remain aligned with backend behavior.
|
||||
|
||||
## Implementation summary
|
||||
|
||||
- Added backend helper `relation_blocker_allows_queue`.
|
||||
- Updated `LocalTicketBackend::queue_ready` to filter relation blockers by that helper before rejecting.
|
||||
- Kept `queued -> inprogress` relation blocker validation unchanged.
|
||||
- Added backend test coverage for ready Tickets with queued/inprogress blockers.
|
||||
- Re-ran existing Panel ViewModel test that already asserted ready+queued dependency appears queueable.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `ready` Ticket with `depends_on` target in `queued` state can be queued.
|
||||
- `ready` Ticket with incoming `blocks` blocker in `inprogress` state can be queued.
|
||||
- `ready` Ticket with dependency/blocker still in `planning` remains rejected.
|
||||
- Panel and backend queue behavior are consistent.
|
||||
- Orchestrator acceptance ordering remains guarded by existing `queued -> inprogress` relation check.
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p ticket queue_gate --lib`
|
||||
- `cargo test -p tui workspace_panel_allows_ready_ticket_when_relation_prerequisite_is_queued --lib`
|
||||
- `cargo check -p ticket -p tui`
|
||||
- `cargo fmt`
|
||||
- `git diff --check`
|
||||
7
.yoi/tickets/00001KVHQDS6B/thread.md
Normal file
7
.yoi/tickets/00001KVHQDS6B/thread.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T05:18:00Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHR3WRF/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHR3WRF/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"id":"orch-plan-20260620-060022-1","ticket_id":"00001KVHR3WRF","kind":"waiting_capacity_note","note":"Panel Queue was accepted for routing review, but implementation is held because `00001KVHKWNQS` is currently inprogress with active Coder work. Leave this MCP foundation Ticket queued; reroute when current implementation capacity is free.","author":"yoi-orchestrator","at":"2026-06-20T06:00:22Z"}
|
||||
{"id":"orch-plan-20260620-065554-2","ticket_id":"00001KVHR3WRF","kind":"accepted_plan","accepted_plan":{"summary":"Named local stdio MCP server configuration and trust policy metadataを追加する。This Ticket only parses/validates config and diagnostics; it must not spawn subprocesses or implement JSON-RPC lifecycle. Command/env/secret fields must fail closed, redact sensitive values, and document that local MCP executables run with user OS permissions outside Yoi feature sandbox authority.","branch":"impl/00001KVHR3WRF-mcp-stdio-config-trust","worktree":"/home/hare/Projects/yoi/.worktree/00001KVHR3WRF-mcp-stdio-config-trust","role_plan":"Orchestrator は acceptance records を commit 後、専用 implementation worktree `.worktree/00001KVHR3WRF-mcp-stdio-config-trust` を作成し、Coder をその child worktree への narrow write scope で起動する。Coder 実装後、Reviewer が config schema、trust boundary docs、secret redaction、fail-closed validation、no auto-start/no process execution、Profile/config layering separation を確認する。"},"author":"yoi-orchestrator","at":"2026-06-20T06:55:54Z"}
|
||||
13
.yoi/tickets/00001KVHR3WRF/artifacts/relations.json
Normal file
13
.yoi/tickets/00001KVHR3WRF/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHR3WRF",
|
||||
"kind": "related",
|
||||
"target": "00001KTR81P9X",
|
||||
"note": "MCP implementation builds on the protocol-backed provider feature substrate.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:33:03Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
49
.yoi/tickets/00001KVHR3WRF/item.md
Normal file
49
.yoi/tickets/00001KVHR3WRF/item.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: 'MCP: add local stdio server config and trust policy'
|
||||
state: 'closed'
|
||||
created_at: '2026-06-20T05:30:04Z'
|
||||
updated_at: '2026-06-20T07:28:55Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['mcp', 'config', 'trust-boundary', 'secrets', 'process-exec']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T05:58:46Z'
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
MCP integration starts with explicit local stdio server configuration and trust policy. Yoi must not auto-start MCP servers from workspace presence, package discovery, or Plugin packages. A configured MCP local stdio server is a local executable running with the user's OS permissions; Yoi feature authority does not sandbox its OS-level side effects.
|
||||
|
||||
This Ticket only defines/parses/validates config and diagnostics. It does not spawn MCP processes or implement JSON-RPC lifecycle.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Add typed Profile/config support for named local stdio MCP servers.
|
||||
- Config fields must cover command, args, cwd policy, env policy, and explicit secret/env references as needed.
|
||||
- No package/workspace presence auto-start.
|
||||
- Validate command/env/secret config fail-closed.
|
||||
- Define diagnostic surfaces for config parse/validation errors.
|
||||
- Redact command/env/secret values where needed; do not write plaintext secrets to logs/model context.
|
||||
- Document local executable trust boundary.
|
||||
- Keep MCP config/trust separate from Plugin permissions and `pod::feature` authority.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- A Profile/config can declare a named local stdio MCP server.
|
||||
- Invalid command/env/secret config is rejected with bounded diagnostic.
|
||||
- Secrets are not emitted in plaintext diagnostics/log/model context.
|
||||
- Config alone does not spawn a process.
|
||||
- Docs explain that configured local MCP servers are not OS-sandboxed by Yoi feature authority.
|
||||
- Tests cover valid config, invalid config, secret redaction, and no auto-start.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Spawning stdio subprocesses.
|
||||
- MCP initialize/capability negotiation.
|
||||
- Tool/resource/prompt registration.
|
||||
- Streamable HTTP/OAuth/remote MCP.
|
||||
|
||||
## Related work
|
||||
|
||||
- Objective: `00001KTR80WMN`.
|
||||
- Supersedes part of broad MCP Ticket `00001KTR82RB7`.
|
||||
37
.yoi/tickets/00001KVHR3WRF/resolution.md
Normal file
37
.yoi/tickets/00001KVHR3WRF/resolution.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
## Resolution
|
||||
|
||||
`00001KVHR3WRF` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- Typed MCP config schema を `crates/manifest` に追加しました。
|
||||
- Profile/config で named local stdio MCP server を宣言できるようにしました。
|
||||
- Config fields は `name`, `command`, `args`, `cwd`, `env.inherit`, `env.set` を含みます。
|
||||
- Env value は `literal`, `secret_ref`, `env_ref` をサポートします。
|
||||
- Validation は duplicate names、empty/NUL command/args、cwd policy/path、env var name、secret ref、NUL literal env values などを fail-closed で検査します。
|
||||
- Diagnostics / `Debug` は secret/env/literal values を plaintext で出さないよう redaction します。
|
||||
- Profile resolution / child manifest inheritance に MCP config を通しましたが、subprocess spawning / initialize / JSON-RPC lifecycle / tool/resource/prompt registration は実装していません。
|
||||
- Docs に local stdio MCP server の trust boundary を記録しました。Configured stdio server は user OS permissions で動く local executable であり、Yoi feature authority / Plugin permissions / MCP config validation は OS sandbox ではありません。
|
||||
|
||||
主な commit:
|
||||
- `e0680cce mcp: add stdio server config`
|
||||
- `9b7c4e27 merge: mcp stdio config trust`
|
||||
|
||||
Review:
|
||||
- r1 は `approve`。
|
||||
- Reviewer は config-only boundary、no process spawning/no auto-start、secret redaction、Profile/config integration、docs trust boundary を確認しました。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --all --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p manifest mcp --lib`
|
||||
- `cargo check`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Package impact:
|
||||
- `nix path-info -S .#yoi`: `112615056`
|
||||
|
||||
Known unrelated note:
|
||||
- Full `cargo test -p manifest --lib` は、branch 外の既存 Plugin template-shape mismatch で失敗するため最終 gate にしませんでした。Reviewer はこの failure が `b0225e48..HEAD` の diff に起因しないことを確認済みです。
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-uxMpR3.log`
|
||||
307
.yoi/tickets/00001KVHR3WRF/thread.md
Normal file
307
.yoi/tickets/00001KVHR3WRF/thread.md
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T05:30:04Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-20T05:58:46Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T06:00:44Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: waiting_capacity
|
||||
|
||||
Panel Queue により routing 対象として確認したが、現在 `00001KVHKWNQS` が `inprogress` で Coder 作業中のため、この MCP foundation Ticket は実装開始せず queued のまま保持する。
|
||||
|
||||
Evidence:
|
||||
- Ticket body / relations を確認。
|
||||
- Orchestrator worktree は clean。
|
||||
- queued は MCP chain とこの Ticket 群、inprogress は `00001KVHKWNQS` のみ。
|
||||
- matching branch/worktree は存在しない。
|
||||
|
||||
Next:
|
||||
- `00001KVHKWNQS` が完了して実装 capacity が空いた後、改めて reroute する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T06:56:16Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Panel Queue により、この Ticket は Orchestrator routing 対象として明示許可された。
|
||||
- Ticket body は local stdio MCP server configuration / trust policy の範囲を、spawning/lifecycle を含まない config parse/validation/diagnostics Ticket として明確に定義している。
|
||||
- 前回の waiting_capacity note は `00001KVHKWNQS` が inprogress だったためだが、現在 `00001KVHKWNQS` は closed で capacity blocker は解消済み。
|
||||
- `00001KVHR3WRF` 自身には未解決 blocking relation はない。Incoming `00001KVHR3WRY depends_on this` は後続 Ticket であり blocker ではない。
|
||||
- 現在 inprogress は 0 件、child implementation Pods はなし、matching branch/worktree はなし、Orchestrator worktree は clean。
|
||||
- Risk domain は mcp / config / trust-boundary / secrets / process-exec だが、Ticket は no process spawning、no auto-start、secret redaction、local executable trust boundary、Plugin permissions / `pod::feature` authority separation などの invariants を明示している。bounded context check 後も implementation 前に必要な追加 human decision は見つからなかった。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket `00001KVHR3WRF` body / thread / relations / artifacts。
|
||||
- `TicketRelationQuery(00001KVHR3WRF)`: no outgoing blocking dependency; incoming lifecycle Ticket depends on this。
|
||||
- `TicketOrchestrationPlanQuery(00001KVHR3WRF)`: previous waiting capacity note resolved by `00001KVHKWNQS` closure; accepted plan recorded now。
|
||||
- Workspace state:
|
||||
- Orchestrator worktree clean at `a5df9e37`。
|
||||
- queued: MCP chain remains queued。
|
||||
- inprogress: 0。
|
||||
- visible Pods: self + peers only; spawned children 0。
|
||||
- no matching MCP implementation branch/worktree。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Add typed Profile/config support for named local stdio MCP servers and the trust-policy diagnostics around that config。
|
||||
- This Ticket is intentionally config-only: parse, validate, redact, and document; do not spawn processes or implement JSON-RPC lifecycle。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- No package/workspace presence auto-start。Config alone must not spawn an MCP process。
|
||||
- Local stdio MCP servers are local executables running with user OS permissions; Yoi feature authority does not sandbox OS-level side effects。
|
||||
- Keep MCP config/trust policy separate from Plugin permissions and `pod::feature` authority。
|
||||
- Config must be explicit and named; no implicit discovery or auto-enablement。
|
||||
- Command/args/cwd/env/secret-related configuration must validate fail-closed with bounded diagnostics。
|
||||
- Secret/env values must be redacted in diagnostics/log/model-facing surfaces; do not persist plaintext secrets through errors/debug output。
|
||||
- This Ticket must not implement initialize/capability negotiation, tool/resource/prompt registration, streamable HTTP/OAuth/remote MCP, or subprocess spawning。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- A Profile/config can declare a named local stdio MCP server。
|
||||
- Invalid command/env/secret config is rejected with bounded diagnostic。
|
||||
- Secrets are not emitted in plaintext diagnostics/log/model context。
|
||||
- Config alone does not spawn a process。
|
||||
- Docs explain that configured local MCP servers are not OS-sandboxed by Yoi feature authority。
|
||||
- Tests cover valid config, invalid config, secret redaction, and no auto-start。
|
||||
|
||||
Escalate if:
|
||||
- Existing Profile/config model cannot represent MCP server declarations without a broader config/profile schema decision。
|
||||
- Secret reference representation would require changing the local secret-store policy rather than referencing existing secret/env abstractions。
|
||||
- Tests require actual process spawning; that belongs to a later Ticket, not this one。
|
||||
|
||||
Next action:
|
||||
- Record `queued -> inprogress` and commit Ticket records before creating the implementation worktree and spawning Coder。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T06:56:23Z from: queued to: inprogress reason: orchestrator_acceptance_mcp_stdio_config_trust field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket body/thread, relation metadata, resolved capacity blocker, Orchestrator worktree state, visible Pods, existing branch/worktree, and bounded MCP config/trust context were checked. There is no unresolved blocking dependency, no inprogress/capacity blocker, and no missing planning decision. Accepting this queued Ticket for implementation before worktree/Pod side effects.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T06:57:08Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration start:
|
||||
- Acceptance records were committed on Orchestrator branch: `b0225e48 ticket: accept mcp stdio config trust`。
|
||||
- Created implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVHR3WRF-mcp-stdio-config-trust` on branch `impl/00001KVHR3WRF-mcp-stdio-config-trust` at `b0225e48`。
|
||||
- Spawned Coder Pod `yoi-coder-00001KVHR3WRF` with write scope limited to the implementation worktree and read-only root scope only for runtime workspace identity。
|
||||
- Coder task explicitly forbids root/original workspace implementation, process spawning for this Ticket, merge, Ticket close, and cleanup。
|
||||
|
||||
Next action:
|
||||
- Wait for Coder implementation report, then inspect branch diff/validation evidence and route to Reviewer。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T07:18:35Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder implementation report received from `yoi-coder-00001KVHR3WRF`.
|
||||
|
||||
Implementation commit:
|
||||
- `e0680cce mcp: add stdio server config`
|
||||
|
||||
Changed areas reported:
|
||||
- Added typed MCP config schema in `crates/manifest`:
|
||||
- `mcp.stdio_server` list with explicit `name`, `command`, `args`, `cwd`, and `env`。
|
||||
- explicit env policy with `inherit` and `set`。
|
||||
- env values support `literal`, `secret_ref`, and `env_ref`。
|
||||
- literal env values are redacted in `Debug`。
|
||||
- Added validation for duplicate names, empty/NUL command and args, cwd policy/path, env variable names, secret refs, and bounded diagnostics without secret/env value leakage。
|
||||
- Integrated MCP config through Profile resolution and child manifest inheritance without adding process spawning。
|
||||
- Added focused tests for valid config, invalid command/duplicate names, invalid secret refs/redaction, nonexistent command accepted as no auto-start proof, and Lua Profile MCP config resolution。
|
||||
- Updated docs in `docs/design/profiles-manifests-prompts.md` with local stdio MCP config and trust boundary。
|
||||
- Updated Nix cargo hash due manifest dependency graph change。
|
||||
|
||||
Coder validation reported:
|
||||
- `cargo test -p manifest mcp --lib`: passed。
|
||||
- `cargo check`: passed。
|
||||
- `cargo fmt --all --check`: passed。
|
||||
- `git diff --check`: passed。
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
- `cargo test -p manifest --lib`: failed on pre-existing/unrelated `plugin::tests::template_includes_current_pdk_version`, reportedly due `resources/plugin-templates/rust/Cargo.toml` PDK version `0.2.0` vs `CARGO_PKG_VERSION` `0.1.0`。
|
||||
|
||||
Known deferrals:
|
||||
- No MCP subprocess spawning, initialize/capability negotiation, tool/resource/prompt registration, HTTP/OAuth, or JSON-RPC lifecycle added。
|
||||
- Command executability intentionally not checked at config-parse time; only structural validation is performed。
|
||||
|
||||
Orchestrator evidence checked before review dispatch:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `e0680cce`。
|
||||
- Diff from acceptance `b0225e48..HEAD` is one implementation commit touching 8 files, about 515 insertions / 10 deletions。
|
||||
- `git diff --check b0225e48..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Dispatch Reviewer for r1 review against Ticket requirements, with focus on config schema fit, validation/redaction boundaries, no process spawning/no auto-start, Profile/config layering, docs trust boundary, and whether the reported `manifest --lib` failure is unrelated or must be fixed。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: yoi-orchestrator at: 2026-06-20T07:19:20Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Review dispatch:
|
||||
- Spawned Reviewer Pod `yoi-reviewer-00001KVHR3WRF-r1` against implementation branch `impl/00001KVHR3WRF-mcp-stdio-config-trust`。
|
||||
- Review target commit: `e0680cce mcp: add stdio server config`。
|
||||
- Review baseline: `b0225e48`。
|
||||
- Reviewer task focuses on config-only boundary, no process spawning/no auto-start, secret/env redaction, fail-closed validation, Profile/config layering, trust-boundary docs, MCP vs Plugin/feature authority separation, and the reported `cargo test -p manifest --lib` failure。
|
||||
- Reviewer is instructed not to edit source, commit, merge, close the Ticket, or use TicketReview directly; it will report verdict/evidence back to Orchestrator。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVHR3WRF-r1 at: 2026-06-20T07:27:01Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Verdict: `approve`
|
||||
|
||||
確認範囲:
|
||||
- Ticket contract / Orchestrator IntentPacket。
|
||||
- Diff: `b0225e48..e0680cce`。
|
||||
- 主な対象: `crates/manifest/src/lib.rs`, `crates/manifest/src/config.rs`, `crates/manifest/src/profile.rs`, `crates/manifest/Cargo.toml`, `crates/pod/src/spawn/tool.rs`, `docs/design/profiles-manifests-prompts.md`, `Cargo.lock`, `package.nix`。
|
||||
- Diff 内で `Command::new`, `tokio::process`, `std::process`, spawn, initialize/capability negotiation, JSON-RPC/tool registration などの process/lifecycle implementation 追加を確認。
|
||||
|
||||
Blocking issues: none。
|
||||
|
||||
Critical risk checks:
|
||||
- Config-only boundary は維持されている。MCP subprocess spawning、MCP initialize/capability negotiation、JSON-RPC lifecycle、tool/resource/prompt registration は追加されていない。
|
||||
- `crates/manifest/src/lib.rs` は typed `McpConfig`、named `stdio_server` entries、`command` / `args` / `cwd` / explicit `env.inherit` / `env.set`、`literal` / `secret_ref` / `env_ref` を追加している。
|
||||
- `McpEnvValue::Debug` は literal env values を redact する。validation errors は literal/env/secret values を echo しない。
|
||||
- `crates/manifest/src/config.rs` は duplicate server names、empty/NUL names/commands/args、cwd absolute-after-resolution policy、env var names、secret ref IDs、NUL literal env values を bounded diagnostics で検証している。
|
||||
- Profile integration は通常の Profile resolution / path-resolution flow 経由。Reusable Profiles は absolute MCP cwd paths を拒否する。
|
||||
- `crates/pod/src/spawn/tool.rs` は `manifest.mcp.clone()` を inherited child manifest config に載せるだけで、child/grandchild MCP execution は追加しない。
|
||||
- Docs は stdio config 宣言だけでは subprocess / tool/resource/prompt registration が起きないこと、local stdio MCP server は user OS permissions の通常 executable として動くこと、Yoi feature flags / Plugin permissions / MCP config validation は OS sandbox ではないことを明記している。
|
||||
|
||||
Non-blocking concerns / follow-ups:
|
||||
- `cargo test -p manifest --lib` は `plugin::tests::embedded_rust_component_tool_template_is_valid_package_shape` で失敗するが、この branch の diff は `crates/manifest/src/plugin.rs` や template files を変更していない。Reviewer は `b0225e48` 時点で既に同じ template resource/test mismatch があるため unrelated/pre-existing と判断した。
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo fmt --all --check`: passed。
|
||||
- `git diff --check b0225e48..HEAD`: passed。
|
||||
- `cargo test -p manifest mcp --lib`: 5 MCP-focused tests passed。
|
||||
- `cargo check`: passed。
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
- `cargo test -p manifest --lib`: unrelated pre-existing plugin template-shape failure only。
|
||||
|
||||
Worktree status at review end: clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T07:28:20Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Merge and final validation report:
|
||||
|
||||
Merged implementation branch into Orchestrator branch:
|
||||
- Merge commit: `9b7c4e27 merge: mcp stdio config trust`
|
||||
- Implementation commit included:
|
||||
- `e0680cce mcp: add stdio server config`
|
||||
|
||||
Reviewer outcome:
|
||||
- r1 approved with no blocking issues。
|
||||
- Reviewer confirmed config-only boundary, no subprocess/MCP lifecycle implementation, redaction behavior, Profile/config integration, docs trust boundary, and that `cargo test -p manifest --lib` failure is unrelated/pre-existing plugin template test mismatch。
|
||||
|
||||
Orchestrator validation after merge passed:
|
||||
- `cargo fmt --all --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p manifest mcp --lib`
|
||||
- `cargo check`
|
||||
- `nix build .#yoi --no-link`
|
||||
- `nix path-info -S .#yoi`: `112615056`
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-uxMpR3.log`
|
||||
|
||||
Known unrelated validation note:
|
||||
- Full `cargo test -p manifest --lib` was not used as a final gate because Reviewer confirmed an unrelated/pre-existing plugin template-shape failure outside this branch's diff。
|
||||
|
||||
Final state:
|
||||
- Orchestrator worktree clean at `9b7c4e27` after validation。
|
||||
- Implementation worktree remains available for cleanup after Ticket completion records are committed。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T07:28:26Z from: inprogress to: done reason: merged_reviewed_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation was merged into Orchestrator branch at `9b7c4e27`, review approved, and final Orchestrator validation passed: `cargo fmt --all --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p manifest mcp --lib`, `cargo check`, and `nix build .#yoi --no-link`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-20T07:28:55Z from: done to: closed reason: closed field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を closed にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-20T07:28:55Z status: closed -->
|
||||
|
||||
## 完了
|
||||
|
||||
## Resolution
|
||||
|
||||
`00001KVHR3WRF` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- Typed MCP config schema を `crates/manifest` に追加しました。
|
||||
- Profile/config で named local stdio MCP server を宣言できるようにしました。
|
||||
- Config fields は `name`, `command`, `args`, `cwd`, `env.inherit`, `env.set` を含みます。
|
||||
- Env value は `literal`, `secret_ref`, `env_ref` をサポートします。
|
||||
- Validation は duplicate names、empty/NUL command/args、cwd policy/path、env var name、secret ref、NUL literal env values などを fail-closed で検査します。
|
||||
- Diagnostics / `Debug` は secret/env/literal values を plaintext で出さないよう redaction します。
|
||||
- Profile resolution / child manifest inheritance に MCP config を通しましたが、subprocess spawning / initialize / JSON-RPC lifecycle / tool/resource/prompt registration は実装していません。
|
||||
- Docs に local stdio MCP server の trust boundary を記録しました。Configured stdio server は user OS permissions で動く local executable であり、Yoi feature authority / Plugin permissions / MCP config validation は OS sandbox ではありません。
|
||||
|
||||
主な commit:
|
||||
- `e0680cce mcp: add stdio server config`
|
||||
- `9b7c4e27 merge: mcp stdio config trust`
|
||||
|
||||
Review:
|
||||
- r1 は `approve`。
|
||||
- Reviewer は config-only boundary、no process spawning/no auto-start、secret redaction、Profile/config integration、docs trust boundary を確認しました。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --all --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p manifest mcp --lib`
|
||||
- `cargo check`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Package impact:
|
||||
- `nix path-info -S .#yoi`: `112615056`
|
||||
|
||||
Known unrelated note:
|
||||
- Full `cargo test -p manifest --lib` は、branch 外の既存 Plugin template-shape mismatch で失敗するため最終 gate にしませんでした。Reviewer はこの failure が `b0225e48..HEAD` の diff に起因しないことを確認済みです。
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-uxMpR3.log`
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHR3WRY/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHR3WRY/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"id":"orch-plan-20260620-060022-1","ticket_id":"00001KVHR3WRY","kind":"blocked_by","related_ticket":"00001KVHR3WRF","note":"Lifecycle client requires explicit local stdio MCP config/trust policy. `00001KVHR3WRF` is queued and not yet implemented; leave this Ticket queued until that dependency is closed.","author":"yoi-orchestrator","at":"2026-06-20T06:00:22Z"}
|
||||
{"id":"orch-plan-20260620-072936-2","ticket_id":"00001KVHR3WRY","kind":"accepted_plan","accepted_plan":{"summary":"Configured local stdio MCP serverを明示 config から起動し、newline-delimited JSON-RPC over stdioで initialize/capability negotiation/initialized notification/shutdownを行う lifecycle client foundation を実装する。Tools/resources/prompts registration/executionは後続 Ticket のため含めない。","branch":"impl/00001KVHR3WRY-mcp-stdio-lifecycle-client","worktree":"/home/hare/Projects/yoi/.worktree/00001KVHR3WRY-mcp-stdio-lifecycle-client","role_plan":"Orchestrator は acceptance records を commit 後、専用 implementation worktree `.worktree/00001KVHR3WRY-mcp-stdio-lifecycle-client` を作成し、Coder をその child worktree への narrow write scope で起動する。Coder 実装後、Reviewer が process lifecycle safety、JSON-RPC framing、initialize/capability negotiation、stderr bounds/redaction、shutdown/kill fallback、no ToolRegistry/resources/prompts exposure を確認する。"},"author":"yoi-orchestrator","at":"2026-06-20T07:29:36Z"}
|
||||
21
.yoi/tickets/00001KVHR3WRY/artifacts/relations.json
Normal file
21
.yoi/tickets/00001KVHR3WRY/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHR3WRY",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVHR3WRF",
|
||||
"note": "Lifecycle client requires explicit local stdio MCP config/trust policy.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:33:03Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHR3WRY",
|
||||
"kind": "related",
|
||||
"target": "00001KTR81P9X",
|
||||
"note": "MCP lifecycle uses the protocol-backed provider feature substrate.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:33:03Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
.yoi/tickets/00001KVHR3WRY/item.md
Normal file
51
.yoi/tickets/00001KVHR3WRY/item.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
title: 'MCP: implement stdio JSON-RPC lifecycle client'
|
||||
state: 'closed'
|
||||
created_at: '2026-06-20T05:30:04Z'
|
||||
updated_at: '2026-06-20T07:59:30Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['mcp', 'stdio', 'json-rpc', 'process-lifecycle', 'diagnostics']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T05:58:54Z'
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
After MCP local stdio server config exists, Yoi needs a lifecycle client that can start a configured server, speak newline-delimited JSON-RPC over stdio, perform MCP initialize/capability negotiation, and shut down safely.
|
||||
|
||||
This Ticket creates the protocol/lifecycle foundation only. It does not expose MCP tools/resources/prompts to the model-visible ToolRegistry.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Spawn configured local stdio MCP servers from explicit config only.
|
||||
- Use stdin/stdout newline-delimited JSON-RPC.
|
||||
- Treat stdout as protocol messages.
|
||||
- Treat stderr as bounded diagnostics/logging, not automatic protocol failure.
|
||||
- Implement initialize, capability negotiation, and notifications/initialized.
|
||||
- Track server name and startup phase in diagnostics.
|
||||
- Implement graceful shutdown, terminate, and kill fallback.
|
||||
- Handle process exit/disconnect/startup failure with bounded diagnostics.
|
||||
- Do not declare sampling or elicitation client capabilities initially.
|
||||
- Add local mock MCP server tests.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Mock local stdio MCP server initializes successfully.
|
||||
- Initialize failure reports server name and phase.
|
||||
- stderr is bounded and redacted where needed.
|
||||
- Shutdown is safe and deterministic.
|
||||
- Sampling/elicitation are not advertised and fail closed if requested.
|
||||
- No tools/resources/prompts are registered by this Ticket.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- tools/list ToolRegistry registration.
|
||||
- tools/call execution.
|
||||
- resources/prompts operations.
|
||||
- Streamable HTTP/OAuth/remote MCP.
|
||||
|
||||
## Related work
|
||||
|
||||
- Depends on `00001KVHR3WRF`.
|
||||
- Objective: `00001KTR80WMN`.
|
||||
37
.yoi/tickets/00001KVHR3WRY/resolution.md
Normal file
37
.yoi/tickets/00001KVHR3WRY/resolution.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
## Resolution
|
||||
|
||||
`00001KVHR3WRY` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- New internal crate `mcp` を追加しました。
|
||||
- Explicit MCP stdio server config から resolved stdio server spec を作成する bridge を追加しました。
|
||||
- Tokio child process による local stdio MCP server lifecycle foundation を実装しました。
|
||||
- stdin/stdout/stderr handling、newline-delimited JSON-RPC request/response handling、initialize/capability negotiation、`notifications/initialized` を実装しました。
|
||||
- stdout/stderr/protocol payloads は bounded に扱います。
|
||||
- stderr は bounded diagnostics/logging として扱い、protocol failure とは別扱いです。
|
||||
- server name / phase-aware errors を追加しました。
|
||||
- shutdown は stdin close / wait / terminate / kill fallback で deterministic に行います。
|
||||
- Server-to-client requests は fail-closed し、sampling/elicitation は advertise せず、unknown request は JSON-RPC error で返します。
|
||||
- `McpStdioServerSpec` の `Debug` は custom redacted 実装にし、resolved env/secret-derived values を出さない regression test を追加しました。
|
||||
- ToolRegistry / tools/resources/prompts registration、remote MCP / Streamable HTTP / OAuth は実装していません。
|
||||
|
||||
主な commit:
|
||||
- `a114fa9d mcp: implement stdio lifecycle client`
|
||||
- `f396e1a2 mcp: redact stdio server spec debug`
|
||||
- `9cf5344f merge: mcp stdio lifecycle client`
|
||||
|
||||
Review:
|
||||
- r1 は resolved spec `Debug` による env/secret leak で `request_changes`。
|
||||
- Coder が custom redacted `Debug` と regression test を追加。
|
||||
- r2 は `approve`。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p mcp`
|
||||
- `cargo check`
|
||||
- `cargo tree -p mcp --depth 1`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Package impact:
|
||||
- `nix path-info -S .#yoi`: `112615056`
|
||||
400
.yoi/tickets/00001KVHR3WRY/thread.md
Normal file
400
.yoi/tickets/00001KVHR3WRY/thread.md
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T05:30:04Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-20T05:58:54Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T06:00:44Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: blocked_pending_dependency
|
||||
|
||||
Panel Queue により routing 対象として確認したが、`00001KVHR3WRY` は `00001KVHR3WRF` に `depends_on` している。`00001KVHR3WRF` はまだ queued で未実装のため、この Ticket は実装開始せず queued のまま保持する。
|
||||
|
||||
Next:
|
||||
- `00001KVHR3WRF` が closed になった後、改めて reroute する。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T07:29:56Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready
|
||||
|
||||
Reason:
|
||||
- Panel Queue により、この Ticket は Orchestrator routing 対象として明示許可された。
|
||||
- 前回は `00001KVHR3WRF` local stdio config/trust policy が未完了だったため blocked/queued hold としたが、現在 `00001KVHR3WRF` は closed。
|
||||
- Ticket body は stdio JSON-RPC lifecycle client の scope を initialize/capability negotiation/shutdown/process diagnostics foundation として明確に定義し、ToolRegistry/tools/resources/prompts exposure は non-goal として分離している。
|
||||
- 現在 inprogress は 0 件、child implementation Pods はなし、matching branch/worktree はなし、Orchestrator worktree は clean。
|
||||
- Risk domain は mcp / stdio / json-rpc / process-lifecycle / diagnostics だが、Ticket は explicit config only、bounded stderr diagnostics、safe shutdown/kill fallback、sampling/elicitation not advertised、no tools/resources/prompts registration などの invariants を明示している。bounded context check 後も implementation 前に必要な追加 human decision は見つからなかった。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket `00001KVHR3WRY` body / thread / relations / artifacts。
|
||||
- `TicketRelationQuery(00001KVHR3WRY)`: outgoing `depends_on 00001KVHR3WRF` is now closed。Incoming `00001KVHR3WS6` / `00001KVHR3WSN` are downstream and not blockers。
|
||||
- `TicketOrchestrationPlanQuery(00001KVHR3WRY)`: previous `blocked_by 00001KVHR3WRF` is resolved; accepted plan recorded now。
|
||||
- Workspace state:
|
||||
- Orchestrator worktree clean at `8f5eef94`。
|
||||
- queued: remaining MCP chain Tickets。
|
||||
- inprogress: 0。
|
||||
- visible Pods: self + peers only; spawned children 0。
|
||||
- no matching MCP lifecycle branch/worktree。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Implement a local stdio MCP lifecycle client foundation that can spawn an explicitly configured local server, exchange newline-delimited JSON-RPC on stdin/stdout, run initialize/capability negotiation, send `notifications/initialized`, and shut down safely。
|
||||
- This Ticket must not expose MCP tools/resources/prompts to the model-visible ToolRegistry; it only provides the lifecycle/protocol substrate for later Tickets。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- Only explicit named config from `00001KVHR3WRF` can be used; no implicit discovery or auto-start from workspace/package presence。
|
||||
- Treat stdout as protocol messages and stderr as bounded diagnostics/logging, not automatic protocol failure。
|
||||
- Bound stdout/stderr lines, diagnostics, protocol payloads, startup timeout, request timeout, shutdown timeout, and kill fallback。
|
||||
- Include server name and startup phase in errors/diagnostics。
|
||||
- Do not advertise sampling or elicitation client capabilities; if server requests them, fail closed / return an error as appropriate。
|
||||
- Do not register tools/resources/prompts or expose any ToolRegistry surface in this Ticket。
|
||||
- No remote MCP / Streamable HTTP / OAuth。
|
||||
- Preserve trust boundary: local stdio server process runs as user OS executable; config validation is not sandbox authority。
|
||||
- Secret/env values from config must remain redacted in diagnostics/logs/errors。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- Mock local stdio MCP server initializes successfully。
|
||||
- Initialize failure reports server name and phase。
|
||||
- Stderr is bounded and redacted where needed。
|
||||
- Shutdown is safe and deterministic, with terminate/kill fallback。
|
||||
- Sampling/elicitation are not advertised and fail closed if requested。
|
||||
- No tools/resources/prompts are registered by this Ticket。
|
||||
- Tests use local mock MCP server processes and cover success/failure/shutdown/diagnostic paths。
|
||||
|
||||
Escalate if:
|
||||
- Existing runtime boundaries make it unclear where lifecycle client should live without dragging ToolRegistry or Plugin feature authority into the config layer。
|
||||
- Protocol message typing requires a broad MCP provider substrate redesign。
|
||||
- Deterministic process tests are infeasible without an explicit test harness decision。
|
||||
|
||||
Next action:
|
||||
- Record `queued -> inprogress` and commit Ticket records before creating the implementation worktree and spawning Coder。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T07:30:03Z from: queued to: inprogress reason: orchestrator_acceptance_mcp_stdio_lifecycle_client field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket body/thread, relation metadata, closed MCP config dependency, Orchestrator worktree state, visible Pods, existing branch/worktree, and bounded MCP lifecycle context were checked. Dependency `00001KVHR3WRF` is closed, no inprogress/capacity blocker exists, and no missing planning decision was found. Accepting this queued Ticket for implementation before worktree/Pod side effects.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T07:30:59Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration start:
|
||||
- Acceptance records were committed on Orchestrator branch: `c0e760d7 ticket: accept mcp stdio lifecycle client`。
|
||||
- Created implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVHR3WRY-mcp-stdio-lifecycle-client` on branch `impl/00001KVHR3WRY-mcp-stdio-lifecycle-client` at `c0e760d7`。
|
||||
- Spawned Coder Pod `yoi-coder-00001KVHR3WRY` with write scope limited to the implementation worktree and read-only root scope only for runtime workspace identity。
|
||||
- Coder task explicitly forbids root/original workspace implementation, ToolRegistry/tools/resources/prompts exposure for this Ticket, merge, Ticket close, and cleanup。
|
||||
|
||||
Next action:
|
||||
- Wait for Coder implementation report, then inspect branch diff/validation evidence and route to Reviewer。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T07:45:58Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder implementation report received from `yoi-coder-00001KVHR3WRY`.
|
||||
|
||||
Implementation commit:
|
||||
- `a114fa9d mcp: implement stdio lifecycle client`
|
||||
|
||||
Changed areas reported:
|
||||
- Added new internal reusable crate `crates/mcp`。
|
||||
- Implemented stdio MCP lifecycle foundation:
|
||||
- explicit resolved stdio server spec/config bridge from typed manifest MCP config。
|
||||
- Tokio child process spawn with `stdin` / `stdout` / `stderr`。
|
||||
- newline-delimited JSON-RPC request/response handling。
|
||||
- `initialize` capability negotiation。
|
||||
- `notifications/initialized`。
|
||||
- bounded stdout/stderr/protocol payload handling。
|
||||
- bounded/redacted stderr diagnostics。
|
||||
- server-name + phase-aware errors。
|
||||
- safe shutdown via stdin close, wait, terminate, and kill fallback。
|
||||
- fail-closed server-to-client request handling including sampling/elicitation。
|
||||
- Added mock local stdio MCP server test binary scoped to `crates/mcp` tests。
|
||||
- Updated workspace membership and `package.nix` cargo hash。
|
||||
|
||||
Coder validation reported:
|
||||
- `cargo test -p mcp`: passed。
|
||||
- `cargo check`: passed。
|
||||
- `cargo fmt --check`: passed。
|
||||
- `git diff --check`: passed。
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
|
||||
Acceptance notes reported:
|
||||
- Mock stdio MCP server initializes successfully。
|
||||
- Initialize failure includes server name and `initialize` phase。
|
||||
- stderr diagnostics are bounded and secret/env values are redacted。
|
||||
- Shutdown is deterministic with terminate/kill fallback for uncooperative server。
|
||||
- Client capabilities do not advertise sampling or elicitation。
|
||||
- Server sampling requests receive JSON-RPC `-32601` fail-closed response。
|
||||
- No ToolRegistry/tool/resource/prompt registration was added。
|
||||
|
||||
Known deferrals:
|
||||
- No MCP tools/resources/prompts exposure。
|
||||
- No remote MCP / Streamable HTTP / OAuth。
|
||||
- Downstream list/call/resource/prompt support remains for later Tickets。
|
||||
|
||||
Orchestrator evidence checked before review dispatch:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `a114fa9d`。
|
||||
- Diff from acceptance `c0e760d7..HEAD` is one implementation commit touching 8 files, about 1369 insertions / 1 deletion。
|
||||
- `git diff --check c0e760d7..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Dispatch Reviewer for r1 review against Ticket requirements, with focus on process lifecycle cleanup, JSON-RPC framing/bounds, stdout/stderr handling, secret redaction, sampling/elicitation fail-closed, no ToolRegistry/resources/prompts exposure, test determinism, and package/Nix impact。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: yoi-orchestrator at: 2026-06-20T07:46:38Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Review dispatch:
|
||||
- Spawned Reviewer Pod `yoi-reviewer-00001KVHR3WRY-r1` against implementation branch `impl/00001KVHR3WRY-mcp-stdio-lifecycle-client`。
|
||||
- Review target commit: `a114fa9d mcp: implement stdio lifecycle client`。
|
||||
- Review baseline: `c0e760d7`。
|
||||
- Reviewer task focuses on process lifecycle cleanup, JSON-RPC framing/bounds, initialize negotiation, stderr diagnostics/redaction, sampling/elicitation fail-closed behavior, no ToolRegistry/resources/prompts exposure, crate layering, deterministic tests, and Nix/package impact。
|
||||
- Reviewer is instructed not to edit source, commit, merge, close the Ticket, or use TicketReview directly; it will report verdict/evidence back to Orchestrator。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVHR3WRY-r1 at: 2026-06-20T07:51:30Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Verdict: `request_changes`
|
||||
|
||||
確認範囲:
|
||||
- Ticket contract。
|
||||
- Diff: `c0e760d7..a114fa9d`。
|
||||
- 主な対象: `crates/mcp/src/stdio.rs`, `crates/mcp/src/lib.rs`, `crates/mcp/tests/stdio_lifecycle.rs`, `crates/mcp/tests/fixtures/mock_server.rs`, `crates/mcp/Cargo.toml`, root `Cargo.toml`, `Cargo.lock`, `package.nix`。
|
||||
- ToolRegistry/resource/prompt registration leakage と remote MCP/OAuth/HTTP scope creep を確認。
|
||||
|
||||
良い点:
|
||||
- stdio child spawn、newline JSON-RPC framing、initialize + initialized notification、bounded stdout/stderr handling、stderr-as-diagnostics、fail-closed server-to-client requests、shutdown/terminate/kill fallback、no model-visible registration surface は Ticket intent に概ね沿っている。
|
||||
|
||||
Blocking issue:
|
||||
1. Resolved stdio server spec の derived `Debug` が secret/env values を leak できる。
|
||||
- `crates/mcp/src/stdio.rs`
|
||||
- `McpStdioServerSpec` derives `Debug`。
|
||||
- `env: BTreeMap<String, String>` に inherited/env/literal/secret-derived values が入る。
|
||||
- `Redactions` は stderr/protocol diagnostics には使われているが、resolved spec 自体を `{:?}` で format/log すると env values が plaintext で出る。
|
||||
- これは Ticket の secret/env redaction 要件に反する。
|
||||
|
||||
Required fix:
|
||||
- `McpStdioServerSpec` から `Debug` を外す、または env values を出さない custom redacted `Debug` を実装する。
|
||||
- custom `Debug` を残す場合は regression test を追加する。
|
||||
|
||||
Non-blocking concerns:
|
||||
- Initialize response の `protocolVersion` compatibility enforcement は今後 tools/resources/prompts registration 前に判断が必要。Lifecycle slice としては follow-up で可。
|
||||
- `Drop` cleanup は best-effort `start_kill` / `kill_on_drop`。Downstream code should prefer explicit `shutdown()`。
|
||||
|
||||
Reviewer validation:
|
||||
- `git diff --check c0e760d7..HEAD`: passed。
|
||||
- `cargo fmt --check`: passed。
|
||||
- `cargo test -p mcp`: passed。
|
||||
- `cargo check`: passed。
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
- `cargo tree -p mcp --depth 1`: inspected; no product façade dependency found。
|
||||
|
||||
Worktree status at review end: clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T07:55:55Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder r1-fix report received from `yoi-coder-00001KVHR3WRY`.
|
||||
|
||||
New fix commit:
|
||||
- `f396e1a2 mcp: redact stdio server spec debug`
|
||||
|
||||
Branch commits now:
|
||||
- `f396e1a2 mcp: redact stdio server spec debug`
|
||||
- `a114fa9d mcp: implement stdio lifecycle client`
|
||||
|
||||
Fix summary:
|
||||
- Removed derived `Debug` from `McpStdioServerSpec`。
|
||||
- Added custom redacted `Debug` implementation。
|
||||
- Debug output preserves useful non-sensitive fields: `name`, `command`, `args`, `cwd`, env keys, and redaction count。
|
||||
- Debug output never prints resolved env values or redaction values。
|
||||
- Added regression test verifying `format!("{:?}", spec)` does not contain literal/inherited/env-ref/secret-ref plaintext values while still showing non-sensitive metadata。
|
||||
|
||||
Coder validation reported as passing:
|
||||
- `cargo fmt --check`
|
||||
- `cargo test -p mcp`
|
||||
- `cargo check`
|
||||
- `git diff --check c0e760d7..HEAD`
|
||||
- `cargo tree -p mcp --depth 1`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Remaining deferrals:
|
||||
- Protocol-version compatibility enforcement before downstream tools/resources/prompts registration。
|
||||
- Downstream callers should prefer explicit `shutdown()` over best-effort `Drop` cleanup。
|
||||
|
||||
Orchestrator evidence checked before r2 review:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `f396e1a2`。
|
||||
- Fix diff from `a114fa9d..HEAD` touches `crates/mcp/src/stdio.rs` and `crates/mcp/tests/stdio_lifecycle.rs`, about 47 insertions / 1 deletion。
|
||||
- Overall `git diff --check c0e760d7..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Route back to Reviewer for r2 verification of the redacted `Debug` blocker and final acceptance criteria。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVHR3WRY-r1 at: 2026-06-20T07:58:02Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Verdict: `approve`
|
||||
|
||||
確認範囲:
|
||||
- Ticket authority / Orchestrator IntentPacket。
|
||||
- r2 fix diff: `a114fa9d..f396e1a2`。
|
||||
- 全体実装 diff: `c0e760d7..f396e1a2`。
|
||||
- 主な対象: `crates/mcp/src/stdio.rs`, `crates/mcp/tests/stdio_lifecycle.rs`, new crate/workspace/package changes, ToolRegistry/resource/prompt registration leakage の有無。
|
||||
|
||||
Previous blocker verification:
|
||||
- `McpStdioServerSpec` no longer derives `Debug`; it is `Clone` only。
|
||||
- Custom `fmt::Debug` now redacts env values and prints only non-sensitive metadata / env keys / `redaction_count`。
|
||||
- `redactions` contents are not printed。
|
||||
- Regression test confirms `format!("{spec:?}")` includes useful non-sensitive metadata and excludes plaintext env-like values。
|
||||
|
||||
Blocking issues: none。
|
||||
|
||||
Non-blocking concerns / follow-ups:
|
||||
- None blocking for this Ticket。Implementation remains scoped to stdio lifecycle/protocol foundations and does not add ToolRegistry/resource/prompt registration or remote/OAuth surfaces。
|
||||
|
||||
Reviewer validation:
|
||||
- `git diff --check c0e760d7..HEAD`: passed。
|
||||
- `cargo fmt --check`: passed。
|
||||
- `cargo test -p mcp`: passed; 5 tests passed including redacted Debug regression。
|
||||
- `cargo check`: passed。
|
||||
- `cargo tree -p mcp --depth 1`: inspected; dependencies limited to `libc`, `manifest`, `secrets`, `serde`, `serde_json`, `thiserror`, `tokio`; no product façade dependency。
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
- Additional grep found no added ToolRegistry/resource/prompt registration or remote/OAuth/HTTP scope creep。
|
||||
|
||||
Worktree status at review end: clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T07:59:04Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Merge and final validation report:
|
||||
|
||||
Merged implementation branch into Orchestrator branch:
|
||||
- Merge commit: `9cf5344f merge: mcp stdio lifecycle client`
|
||||
- Implementation commits included:
|
||||
- `a114fa9d mcp: implement stdio lifecycle client`
|
||||
- `f396e1a2 mcp: redact stdio server spec debug`
|
||||
|
||||
Reviewer outcome:
|
||||
- r1 requested changes for `McpStdioServerSpec` derived `Debug` leaking resolved env/secret-derived values。
|
||||
- Coder added custom redacted `Debug` and regression test。
|
||||
- r2 approved with no blocking issues。
|
||||
|
||||
Orchestrator validation after merge passed:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p mcp`
|
||||
- `cargo check`
|
||||
- `cargo tree -p mcp --depth 1`
|
||||
- `nix build .#yoi --no-link`
|
||||
- `nix path-info -S .#yoi`: `112615056`
|
||||
|
||||
Validation log:
|
||||
- inline Bash output for final validation command。
|
||||
|
||||
Final state:
|
||||
- Orchestrator worktree clean at `9cf5344f` after validation。
|
||||
- Implementation worktree remains available for cleanup after Ticket completion records are committed。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T07:59:10Z from: inprogress to: done reason: merged_reviewed_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation was merged into Orchestrator branch at `9cf5344f`, r2 review approved, and final Orchestrator validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p mcp`, `cargo check`, `cargo tree -p mcp --depth 1`, and `nix build .#yoi --no-link`.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-20T07:59:30Z from: done to: closed reason: closed field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を closed にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-20T07:59:30Z status: closed -->
|
||||
|
||||
## 完了
|
||||
|
||||
## Resolution
|
||||
|
||||
`00001KVHR3WRY` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- New internal crate `mcp` を追加しました。
|
||||
- Explicit MCP stdio server config から resolved stdio server spec を作成する bridge を追加しました。
|
||||
- Tokio child process による local stdio MCP server lifecycle foundation を実装しました。
|
||||
- stdin/stdout/stderr handling、newline-delimited JSON-RPC request/response handling、initialize/capability negotiation、`notifications/initialized` を実装しました。
|
||||
- stdout/stderr/protocol payloads は bounded に扱います。
|
||||
- stderr は bounded diagnostics/logging として扱い、protocol failure とは別扱いです。
|
||||
- server name / phase-aware errors を追加しました。
|
||||
- shutdown は stdin close / wait / terminate / kill fallback で deterministic に行います。
|
||||
- Server-to-client requests は fail-closed し、sampling/elicitation は advertise せず、unknown request は JSON-RPC error で返します。
|
||||
- `McpStdioServerSpec` の `Debug` は custom redacted 実装にし、resolved env/secret-derived values を出さない regression test を追加しました。
|
||||
- ToolRegistry / tools/resources/prompts registration、remote MCP / Streamable HTTP / OAuth は実装していません。
|
||||
|
||||
主な commit:
|
||||
- `a114fa9d mcp: implement stdio lifecycle client`
|
||||
- `f396e1a2 mcp: redact stdio server spec debug`
|
||||
- `9cf5344f merge: mcp stdio lifecycle client`
|
||||
|
||||
Review:
|
||||
- r1 は resolved spec `Debug` による env/secret leak で `request_changes`。
|
||||
- Coder が custom redacted `Debug` と regression test を追加。
|
||||
- r2 は `approve`。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p mcp`
|
||||
- `cargo check`
|
||||
- `cargo tree -p mcp --depth 1`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Package impact:
|
||||
- `nix path-info -S .#yoi`: `112615056`
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHR3WS6/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHR3WS6/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260620-060022-1","ticket_id":"00001KVHR3WS6","kind":"blocked_by","related_ticket":"00001KVHR3WRY","note":"Tool registration requires initialized MCP stdio lifecycle. `00001KVHR3WRY` is queued and depends on `00001KVHR3WRF`; leave this Ticket queued until lifecycle is closed.","author":"yoi-orchestrator","at":"2026-06-20T06:00:22Z"}
|
||||
21
.yoi/tickets/00001KVHR3WS6/artifacts/relations.json
Normal file
21
.yoi/tickets/00001KVHR3WS6/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHR3WS6",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVHR3WRY",
|
||||
"note": "Tool registration requires initialized MCP stdio lifecycle.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:33:03Z"
|
||||
},
|
||||
{
|
||||
"ticket_id": "00001KVHR3WS6",
|
||||
"kind": "related",
|
||||
"target": "00001KTR81P9X",
|
||||
"note": "MCP tool registration uses feature runtime-discovered contribution plumbing.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:33:03Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
49
.yoi/tickets/00001KVHR3WS6/item.md
Normal file
49
.yoi/tickets/00001KVHR3WS6/item.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
title: 'MCP: register server tools into ToolRegistry'
|
||||
state: 'queued'
|
||||
created_at: '2026-06-20T05:30:04Z'
|
||||
updated_at: '2026-06-20T06:00:44Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['mcp', 'tools-list', 'tool-registry', 'schema', 'untrusted-metadata']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T05:58:58Z'
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
Once a configured MCP stdio server can initialize, Yoi should expose provider-discovered MCP tools as ordinary model-visible Yoi tools through the existing ToolRegistry path. Server-provided tool metadata and schemas are untrusted data.
|
||||
|
||||
This Ticket only registers tools discovered through `tools/list` at provider initialization / safe refresh boundaries. It does not implement `tools/call` execution and does not allow model-visible tool schema mutation during an active run.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Call MCP `tools/list` after initialize where supported.
|
||||
- Handle pagination / bounded listing.
|
||||
- Normalize MCP tool names into stable namespaced Yoi tool names that include server namespace.
|
||||
- Validate/normalize tool descriptions and input schemas as untrusted metadata.
|
||||
- Reject invalid schemas, duplicate names, and collisions fail-closed with diagnostics.
|
||||
- Register provider-discovered tool contributions through `pod::feature` / normal ToolRegistry path; no private MCP bypass.
|
||||
- Keep model-visible tool schema run-stable; `list_changed` handling is a later safe-boundary refresh/diagnostic problem, not mid-run mutation.
|
||||
- Do not register resources/prompts in this Ticket.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Provider-discovered MCP mock server tool appears as model-visible Yoi tool with stable namespaced name.
|
||||
- Invalid schema is rejected with bounded diagnostic.
|
||||
- Duplicate/colliding names are rejected fail-closed.
|
||||
- Server metadata cannot weaken Yoi instructions/scope/permissions.
|
||||
- No `tools/call` request is sent during registration.
|
||||
- Active-run model-visible schema is not mutated by this registration path.
|
||||
- Tests cover valid registration, pagination/bounds, invalid schema, duplicate/collision, untrusted metadata normalization, and run-stable schema behavior.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- MCP tool execution.
|
||||
- Resources/prompts operations.
|
||||
- list_changed notifications.
|
||||
|
||||
## Related work
|
||||
|
||||
- Depends on `00001KVHR3WRY`.
|
||||
- Objective: `00001KTR80WMN`.
|
||||
29
.yoi/tickets/00001KVHR3WS6/thread.md
Normal file
29
.yoi/tickets/00001KVHR3WS6/thread.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T05:30:04Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-20T05:58:58Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T06:00:44Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: blocked_pending_dependency
|
||||
|
||||
Panel Queue により routing 対象として確認したが、`00001KVHR3WS6` は `00001KVHR3WRY` に `depends_on` している。MCP tools/list registration は initialized stdio lifecycle を前提にするため、`00001KVHR3WRY` が closed になるまで実装開始せず queued のまま保持する。
|
||||
|
||||
Next:
|
||||
- `00001KVHR3WRY` が closed になった後、改めて reroute する。
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHR3WSD/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHR3WSD/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260620-060022-1","ticket_id":"00001KVHR3WSD","kind":"blocked_by","related_ticket":"00001KVHR3WS6","note":"tools/call execution requires registered MCP tools. `00001KVHR3WS6` is queued and depends on lifecycle; leave this Ticket queued until tool registration is closed.","author":"yoi-orchestrator","at":"2026-06-20T06:00:22Z"}
|
||||
13
.yoi/tickets/00001KVHR3WSD/artifacts/relations.json
Normal file
13
.yoi/tickets/00001KVHR3WSD/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHR3WSD",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVHR3WS6",
|
||||
"note": "tools/call execution requires registered MCP tools.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:33:03Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
45
.yoi/tickets/00001KVHR3WSD/item.md
Normal file
45
.yoi/tickets/00001KVHR3WSD/item.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: 'MCP: execute tools/call through ordinary Tool path'
|
||||
state: 'queued'
|
||||
created_at: '2026-06-20T05:30:04Z'
|
||||
updated_at: '2026-06-20T06:00:44Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['mcp', 'tools-call', 'permission', 'history', 'bounded-output']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T05:59:04Z'
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
After MCP tools are registered through ToolRegistry, invoking a Yoi MCP-backed tool should call the server's `tools/call` and return a bounded ordinary Tool result. Permission denial must happen before sending a request to the MCP server.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Route registered MCP tool invocation to MCP `tools/call`.
|
||||
- Apply existing PreToolCall / Tool permission path before MCP server call.
|
||||
- If permission is denied, do not send `tools/call` to the server.
|
||||
- Distinguish normal result, MCP `isError: true`, and JSON-RPC protocol error.
|
||||
- Serialize MCP result forms boundedly: `content[]`, `structuredContent`, `isError`, `_meta`, and supported rich content summaries.
|
||||
- Store result through ordinary Tool result/history path.
|
||||
- Treat all content as untrusted.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- MCP mock tool returns normal result through ordinary Yoi Tool result.
|
||||
- MCP `isError: true` is represented distinctly from JSON-RPC protocol failure.
|
||||
- Permission denied call is not sent to the MCP server.
|
||||
- Oversize/rich results are bounded/truncated or rejected according to explicit policy.
|
||||
- Tool history shows ordinary tool call/result, not hidden context injection.
|
||||
- Tests cover normal result, `isError`, protocol error, permission denial, and output bounds.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- resources/read or prompts/get.
|
||||
- list_changed notifications.
|
||||
- Sampling/elicitation.
|
||||
|
||||
## Related work
|
||||
|
||||
- Depends on `00001KVHR3WS6`.
|
||||
- Objective: `00001KTR80WMN`.
|
||||
29
.yoi/tickets/00001KVHR3WSD/thread.md
Normal file
29
.yoi/tickets/00001KVHR3WSD/thread.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T05:30:04Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-20T05:59:04Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T06:00:44Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: blocked_pending_dependency
|
||||
|
||||
Panel Queue により routing 対象として確認したが、`00001KVHR3WSD` は `00001KVHR3WS6` に `depends_on` している。MCP `tools/call` execution は registered MCP tools を前提にするため、`00001KVHR3WS6` が closed になるまで実装開始せず queued のまま保持する。
|
||||
|
||||
Next:
|
||||
- `00001KVHR3WS6` が closed になった後、改めて reroute する。
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHR3WSN/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHR3WSN/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260620-060022-1","ticket_id":"00001KVHR3WSN","kind":"blocked_by","related_ticket":"00001KVHR3WRY","note":"Resources/prompts operations require initialized MCP stdio lifecycle. `00001KVHR3WRY` is queued and depends on `00001KVHR3WRF`; leave this Ticket queued until lifecycle is closed.","author":"yoi-orchestrator","at":"2026-06-20T06:00:22Z"}
|
||||
13
.yoi/tickets/00001KVHR3WSN/artifacts/relations.json
Normal file
13
.yoi/tickets/00001KVHR3WSN/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHR3WSN",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVHR3WRY",
|
||||
"note": "resources/prompts operations require initialized MCP stdio lifecycle.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:33:03Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
45
.yoi/tickets/00001KVHR3WSN/item.md
Normal file
45
.yoi/tickets/00001KVHR3WSN/item.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: 'MCP: expose resources and prompts as explicit tool operations'
|
||||
state: 'queued'
|
||||
created_at: '2026-06-20T05:30:04Z'
|
||||
updated_at: '2026-06-20T06:00:44Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['mcp', 'resources', 'prompts', 'prompt-context', 'history', 'untrusted-content']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T05:58:57Z'
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
MCP resources and prompts must not become hidden context injection. They should be exposed as explicit Yoi tool operations whose results are recorded through ordinary Tool result/history paths.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Expose MCP resources/prompts as explicit namespaced Yoi tool operations: `resources/list`, `resources/read`, `prompts/list`, and `prompts/get`.
|
||||
- Treat returned content/templates as untrusted tool result data.
|
||||
- Do not inject resource/prompt content directly into context outside history/tool result.
|
||||
- Bound result sizes and rich/embedded content serialization.
|
||||
- Handle pagination/list bounds where applicable.
|
||||
- Diagnostics identify server/resource/prompt operation without leaking secrets.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `resources/list` and `resources/read` can be invoked as explicit tools.
|
||||
- `prompts/list` and `prompts/get` can be invoked as explicit tools.
|
||||
- Results are ordinary Tool results and history records.
|
||||
- No hidden context injection path is introduced.
|
||||
- Oversize/rich content is bounded.
|
||||
- Tests cover list/read/get happy paths, untrusted content, bounds, and no hidden injection.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- MCP tool execution itself.
|
||||
- list_changed notification refresh.
|
||||
- Sampling/elicitation.
|
||||
|
||||
## Related work
|
||||
|
||||
- Depends on `00001KVHR3WRY`.
|
||||
- Related to `00001KVHR3WSD` for result serialization policy.
|
||||
- Objective: `00001KTR80WMN`.
|
||||
29
.yoi/tickets/00001KVHR3WSN/thread.md
Normal file
29
.yoi/tickets/00001KVHR3WSN/thread.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T05:30:04Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-20T05:58:57Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T06:00:44Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: blocked_pending_dependency
|
||||
|
||||
Panel Queue により routing 対象として確認したが、`00001KVHR3WSN` は `00001KVHR3WRY` に `depends_on` している。MCP resources/prompts operations は initialized stdio lifecycle を前提にするため、`00001KVHR3WRY` が closed になるまで実装開始せず queued のまま保持する。
|
||||
|
||||
Next:
|
||||
- `00001KVHR3WRY` が closed になった後、改めて reroute する。
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHR3WSW/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHR3WSW/artifacts/.gitkeep
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260620-060022-1","ticket_id":"00001KVHR3WSW","kind":"blocked_by","related_ticket":"00001KVHR3WS6","note":"list_changed handling requires initial tools/list registration. `00001KVHR3WS6` is queued and depends on lifecycle; leave this Ticket queued until tool registration is closed.","author":"yoi-orchestrator","at":"2026-06-20T06:00:22Z"}
|
||||
13
.yoi/tickets/00001KVHR3WSW/artifacts/relations.json
Normal file
13
.yoi/tickets/00001KVHR3WSW/artifacts/relations.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": 1,
|
||||
"relations": [
|
||||
{
|
||||
"ticket_id": "00001KVHR3WSW",
|
||||
"kind": "depends_on",
|
||||
"target": "00001KVHR3WS6",
|
||||
"note": "tools/list_changed handling requires initial tools/list registration.",
|
||||
"author": "yoi ticket",
|
||||
"at": "2026-06-20T05:33:03Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
.yoi/tickets/00001KVHR3WSW/item.md
Normal file
43
.yoi/tickets/00001KVHR3WSW/item.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
title: 'MCP: handle list_changed notifications safely'
|
||||
state: 'queued'
|
||||
created_at: '2026-06-20T05:30:04Z'
|
||||
updated_at: '2026-06-20T06:00:44Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['mcp', 'notifications', 'tool-schema', 'prompt-cache', 'refresh']
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T05:59:05Z'
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
MCP servers can notify that tools/resources/prompts lists changed. Yoi must not silently go stale, but it also must not mutate the active run's model-visible tool schema or context in a way that violates history/prompt-cache invariants. This Ticket defines provider tool-list refresh behavior at safe boundaries.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Handle MCP list-changed notifications: `notifications/tools/list_changed`, `notifications/resources/list_changed`, and `notifications/prompts/list_changed`.
|
||||
- Choose and implement a safe refresh policy: next-turn refresh, restart/reinitialize-required diagnostic, or bounded live refresh only if it preserves schema/history invariants.
|
||||
- Do not mutate current LLM context with hidden resource/prompt content.
|
||||
- Emit bounded diagnostics when refresh cannot be applied safely.
|
||||
- Tests with mock server notifications.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- list_changed does not silently stale forever.
|
||||
- Current run tool schema consistency is not broken.
|
||||
- Refresh/diagnostic behavior is deterministic and documented.
|
||||
- Prompt-context/history invariants are preserved.
|
||||
- Tests cover tools/resources/prompts list_changed and unsafe refresh fallback.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Initial tools/list registration.
|
||||
- Initial resources/prompts operations.
|
||||
- Remote MCP transports.
|
||||
|
||||
## Related work
|
||||
|
||||
- Depends on `00001KVHR3WS6` for tool list registration.
|
||||
- Related to `00001KVHR3WSN` for resources/prompts lists.
|
||||
- Objective: `00001KTR80WMN`.
|
||||
29
.yoi/tickets/00001KVHR3WSW/thread.md
Normal file
29
.yoi/tickets/00001KVHR3WSW/thread.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<!-- event: create author: "yoi ticket" at: 2026-06-20T05:30:04Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: workspace-panel at: 2026-06-20T05:59:05Z from: ready to: queued reason: queued field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T06:00:44Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: blocked_pending_dependency
|
||||
|
||||
Panel Queue により routing 対象として確認したが、`00001KVHR3WSW` は `00001KVHR3WS6` に `depends_on` している。list_changed handling は initial tools/list registration を前提にするため、`00001KVHR3WS6` が closed になるまで実装開始せず queued のまま保持する。
|
||||
|
||||
Next:
|
||||
- `00001KVHR3WS6` が closed になった後、改めて reroute する。
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KVHX0WBE/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVHX0WBE/artifacts/.gitkeep
Normal file
161
.yoi/tickets/00001KVHX0WBE/item.md
Normal file
161
.yoi/tickets/00001KVHX0WBE/item.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
---
|
||||
title: 'Dashboard / Console 呼称導入と TUI モジュール境界整理'
|
||||
state: 'ready'
|
||||
created_at: '2026-06-20T06:55:49Z'
|
||||
updated_at: '2026-06-20T06:55:57Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['ux-naming', 'module-boundary', 'public-cli', 'test-coverage']
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
TUI まわりの user-facing surface 名と code/module 境界を明確にする。
|
||||
|
||||
- **Dashboard**: `yoi panel`。workspace-level の Ticket / Pod / Companion / Orchestrator / Intake 状態と action を扱う surface。
|
||||
- **Console**: 通常の単一 Pod に接続して会話する chat/client surface。
|
||||
- **TUI**: Dashboard / Console / picker / setup UI などを実装する terminal UI crate/layer の総称。
|
||||
|
||||
事前調査では `crates/tui/src/multi_pod.rs` は約 9452 lines で、実態は multi-Pod list というより workspace Dashboard 本体だった。主な責務分布は次の通り。
|
||||
|
||||
```text
|
||||
1- 307 entry/runtime loop
|
||||
308- 451 background reload/notice handles
|
||||
452- 792 intake launch/handoff helpers
|
||||
793- 1160 diagnostics/e2e dashboard structs
|
||||
1161- 2422 MultiPodApp state + key/mouse/composer methods
|
||||
2423- 3377 snapshot/lifecycle/load
|
||||
3378- 4986 companion/orchestrator/ticket actions/queue handoff
|
||||
4987- 6258 row classification/layout/render
|
||||
6259- 9452 tests
|
||||
```
|
||||
|
||||
関連ファイル規模の目安:
|
||||
|
||||
```text
|
||||
9452 crates/tui/src/multi_pod.rs
|
||||
2812 crates/tui/src/workspace_panel.rs
|
||||
2346 crates/tui/src/single_pod.rs
|
||||
3689 crates/tui/src/app.rs
|
||||
1851 crates/tui/src/ui.rs
|
||||
1209 crates/tui/src/pod_list.rs
|
||||
556 crates/tui/src/role_session_registry.rs
|
||||
```
|
||||
|
||||
`multi_pod.rs` は unit test / async test も多く、挙動維持の根拠としてテストを活かしながらまとめて整理する。
|
||||
|
||||
## Requirements
|
||||
|
||||
- `Dashboard` / `Console` / `TUI` の呼称を導入し、help / docs / comments / test names / internal naming を可能な範囲で揃える。
|
||||
- `Dashboard` は `yoi panel` の user-facing surface 名とする。
|
||||
- `Console` は単一 Pod 接続チャットクライアントの user-facing surface 名とする。
|
||||
- `TUI` は terminal UI implementation layer の総称として扱い、Dashboard / Console の代替 mode 名として乱用しない。
|
||||
- `yoi panel` command 名は維持する。
|
||||
- `yoi dashboard` などの不要な互換 alias は追加しない。
|
||||
- Dashboard の起動 entrypoint を Console / `single_pod` 側から分離する。
|
||||
- Dashboard から Pod を開いて Console に入る bridge は残すが、narrow API として責務を明確にする。
|
||||
- `crates/tui/src/multi_pod.rs` を Dashboard module 境界へ寄せる。
|
||||
- `multi_pod.rs` の巨大化を解消するため、少なくとも次の責務を分離・整理する。
|
||||
- Dashboard entry/runtime loop
|
||||
- Dashboard app state / action dispatch
|
||||
- Dashboard snapshot/lifecycle loading
|
||||
- Companion / Orchestrator / Intake / Ticket action glue
|
||||
- Dashboard render/layout/row classification
|
||||
- Dashboard e2e diagnostics structs
|
||||
- Dashboard tests
|
||||
- `workspace_panel.rs` は Dashboard view model builder としての責務を確認し、必要なら名前・配置・参照を Dashboard 語彙に寄せる。
|
||||
- `single_pod.rs` は Console 側の起動・接続・spawn/resume・chat loop を主責務にする。
|
||||
- 挙動変更は目的にしない。主眼は naming / module boundary / maintainability refactor。
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `yoi panel` が Dashboard として説明されている。
|
||||
- 単一 Pod UI が Console として説明されている。
|
||||
- `TUI` が Dashboard / Console の総称 implementation layer として整理されている。
|
||||
- `LaunchMode::Panel` が `single_pod::run_panel` のような Console module entrypoint に直接流れない。
|
||||
- Dashboard entrypoint が Dashboard module 側にある。
|
||||
- Console module は単一 Pod chat/connect/spawn/resume の責務を主に持つ。
|
||||
- Dashboard から Console を開く bridge の境界が読み取れる。
|
||||
- `multi_pod.rs` 相当の責務が Dashboard module 配下へ移され、巨大単一ファイル状態が改善されている。
|
||||
- render/list/layout、action/lifecycle、diagnostics/tests などの境界が reviewer に分かる形になっている。
|
||||
- 既存の workspace panel / action model の設計と矛盾しない。
|
||||
- `yoi panel` の Ticket-centric workspace cockpit という意味を保つ。
|
||||
- `yoi panel` が scheduler/backend であるかのような実装・説明になっていない。
|
||||
- 既存テストを維持・更新し、refactor による挙動退行が検出できる状態にする。
|
||||
- reviewer が diff から、単なる rename ではなく Dashboard / Console の責務境界が改善されたことを確認できる。
|
||||
|
||||
## Binding decisions / invariants
|
||||
|
||||
- **Dashboard = `yoi panel`**。
|
||||
- **Console = single-Pod chat/client surface**。
|
||||
- **TUI = terminal UI implementation layer / crate-level umbrella**。
|
||||
- `panel` / Dashboard は単一 Pod Console の名前には使わない。
|
||||
- Console は Dashboard の subordinate mode ではなく、別の user-facing surface として扱う。
|
||||
- Dashboard は Console の一部ではない。
|
||||
- Dashboard は workspace-level cockpit/action surface であり、scheduler/backend ではない。
|
||||
- `yoi panel` command 名は維持する。
|
||||
- 不要な compatibility alias は追加しない。
|
||||
- テストが維持できる範囲では、過度に分割を避ける必要はない。大きな file move / module split を許容する。
|
||||
|
||||
## Implementation latitude
|
||||
|
||||
- 最終的な module 構成は実装者判断でよいが、次の方向を推奨する。
|
||||
|
||||
```text
|
||||
crates/tui/src/
|
||||
console/
|
||||
mod.rs # user-facing Pod Console launch API
|
||||
app_loop.rs # current single_pod run loop 相当
|
||||
nested.rs # Dashboard から Pod Console を開く bridge
|
||||
terminal.rs # Console fullscreen/mouse helpers
|
||||
dashboard/
|
||||
mod.rs # user-facing Dashboard launch API
|
||||
app.rs # Dashboard app state / selection / key handling
|
||||
runtime.rs # current multi_pod::run loop 相当
|
||||
snapshot.rs # Dashboard snapshot loading
|
||||
lifecycle.rs # companion/orchestrator ensure/observe
|
||||
actions.rs # Ticket/Intake/Companion/Orchestrator actions
|
||||
render.rs # layout/draw/list rendering
|
||||
e2e.rs # dashboard-specific e2e structs/events
|
||||
tests.rs or tests/
|
||||
```
|
||||
|
||||
- 互換性や reviewability のため、type 名の rename は段階的でもよい。ただし module / docs / entrypoint は Dashboard / Console 語彙へ寄せる。
|
||||
- `MultiPodApp` などの既存名は必要なら一時的に残せるが、最終的な方向性が Dashboard であることをコード上から読めるようにする。
|
||||
- tests は同一 commit 内で移動・更新してよい。test coverage 維持を優先する。
|
||||
- exact string test がある場合は、新しい Dashboard / Console 呼称に合わせて期待値を更新する。
|
||||
|
||||
## Readiness
|
||||
|
||||
- readiness: implementation_ready
|
||||
- risk_flags: [ux-naming, module-boundary, public-cli, test-coverage]
|
||||
|
||||
Risk flags は stop gate ではなく reviewer focus として扱う。主な確認点は public CLI/help の不要な互換増加、Dashboard / Console 境界、既存 panel authority/action model の保持、テスト維持。
|
||||
|
||||
## Escalation conditions
|
||||
|
||||
- `Dashboard` / `Console` の二分では説明しきれない第三の user-facing surface が見つかった場合は確認する。
|
||||
- `yoi panel` command 名を変える必要が出た場合は確認する。
|
||||
- `yoi dashboard` などの alias 追加が必要だと判断した場合は確認する。
|
||||
- module split の過程で Ticket state transition authority、Pod lifecycle、Orchestrator handoff の挙動変更が必要になった場合は確認する。
|
||||
- テスト維持では覆えない user-visible behavior change が必要になった場合は確認する。
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -p tui`
|
||||
- `cargo test -p yoi`
|
||||
- `cargo check --workspace --all-targets`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- 必要に応じて CLI help / docs / resources grep で `Dashboard` / `Console` / `TUI` / `panel` / `multi_pod` の残存表現を確認する。
|
||||
|
||||
## Related work
|
||||
|
||||
- `00001KTCSRS61` Workspace orchestration panel design
|
||||
- `00001KTCSRS62` Workspace panel action model
|
||||
- `00001KSKBPYER` Multi-Pod view UI
|
||||
- `crates/tui/src/multi_pod.rs`
|
||||
- `crates/tui/src/single_pod.rs`
|
||||
- `crates/tui/src/workspace_panel.rs`
|
||||
- `crates/tui/src/lib.rs`
|
||||
- `yoi panel`
|
||||
23
.yoi/tickets/00001KVHX0WBE/thread.md
Normal file
23
.yoi/tickets/00001KVHX0WBE/thread.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-20T06:55:49Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: ticket-intake at: 2026-06-20T06:55:57Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
ユーザー合意に基づき、Dashboard / Console 呼称導入、`yoi panel` Dashboard 境界、単一 Pod Console 境界、`multi_pod.rs` の Dashboard module への移設・分割、docs/help/tests 更新を 1 つの concrete implementation Ticket として整理した。readiness は `implementation_ready`。risk flags は stop gate ではなく reviewer focus として扱う。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: ticket-intake at: 2026-06-20T06:55:57Z from: planning to: ready reason: planning_ready field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
要件・受け入れ条件・binding decisions・validation が揃っており、Orchestrator が実装 routing 可能。
|
||||
|
||||
---
|
||||
28
Cargo.lock
generated
28
Cargo.lock
generated
|
|
@ -2032,6 +2032,7 @@ dependencies = [
|
|||
"llm-worker",
|
||||
"mlua",
|
||||
"protocol",
|
||||
"secrets",
|
||||
"serde",
|
||||
"serde_ignored",
|
||||
"serde_json",
|
||||
|
|
@ -2077,6 +2078,19 @@ dependencies = [
|
|||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"manifest",
|
||||
"secrets",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
|
|
@ -2623,6 +2637,7 @@ dependencies = [
|
|||
"wasmtime",
|
||||
"wat",
|
||||
"workflow",
|
||||
"yoi-plugin-pdk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -5388,6 +5403,7 @@ version = "0.51.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
|
|
@ -5553,6 +5569,18 @@ dependencies = [
|
|||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoi-plugin-pdk"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"toml",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ members = [
|
|||
"crates/session-store",
|
||||
"crates/secrets",
|
||||
"crates/manifest",
|
||||
"crates/mcp",
|
||||
"crates/pod",
|
||||
"crates/plugin-pdk",
|
||||
"crates/yoi",
|
||||
"crates/pod-store",
|
||||
"crates/protocol",
|
||||
|
|
@ -33,7 +35,9 @@ default-members = [
|
|||
"crates/session-store",
|
||||
"crates/secrets",
|
||||
"crates/manifest",
|
||||
"crates/mcp",
|
||||
"crates/pod",
|
||||
"crates/plugin-pdk",
|
||||
"crates/yoi",
|
||||
"crates/pod-store",
|
||||
"crates/protocol",
|
||||
|
|
@ -60,11 +64,13 @@ client = { path = "crates/client" }
|
|||
llm-worker = { path = "crates/llm-worker", version = "0.2" }
|
||||
llm-worker-macros = { path = "crates/llm-worker-macros", version = "0.2" }
|
||||
manifest = { path = "crates/manifest" }
|
||||
mcp = { path = "crates/mcp" }
|
||||
lint-common = { path = "crates/lint-common" }
|
||||
memory = { path = "crates/memory" }
|
||||
ticket = { path = "crates/ticket" }
|
||||
project-record = { path = "crates/project-record" }
|
||||
pod = { path = "crates/pod" }
|
||||
yoi-plugin-pdk = { path = "crates/plugin-pdk" }
|
||||
yoi = { path = "crates/yoi" }
|
||||
pod-registry = { path = "crates/pod-registry" }
|
||||
pod-store = { path = "crates/pod-store" }
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ serde = { workspace = true, features = ["derive"] }
|
|||
serde_json = { workspace = true }
|
||||
serde_ignored = "0.1.14"
|
||||
sha2 = "0.10"
|
||||
secrets = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//! via [`PodManifestConfig::merge`] and the final config is converted to
|
||||
//! a validated [`PodManifest`] via `TryFrom`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
|
@ -17,10 +17,10 @@ use crate::defaults;
|
|||
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
||||
use crate::plugin::PluginConfig;
|
||||
use crate::{
|
||||
CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, MemoryConfig,
|
||||
PodManifest, PodMeta, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureAccessConfig,
|
||||
TicketFeatureConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule, WebConfig,
|
||||
WorkerManifest,
|
||||
CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, McpConfig, McpEnvValue,
|
||||
McpStdioCwdPolicy, MemoryConfig, PodManifest, PodMeta, ScopeConfig, SessionConfig,
|
||||
SkillsConfig, TicketFeatureAccessConfig, TicketFeatureConfig, ToolOutputLimits,
|
||||
ToolPermissionConfig, ToolPermissionRule, WebConfig, WorkerManifest,
|
||||
};
|
||||
|
||||
/// Partial-form Pod manifest. Every field is optional; one or more
|
||||
|
|
@ -57,6 +57,10 @@ pub struct PodManifestConfig {
|
|||
/// separate step and does not run during config merge.
|
||||
#[serde(default)]
|
||||
pub plugins: PluginConfig,
|
||||
/// Explicit Model Context Protocol provider declarations. Config parsing
|
||||
/// never starts a local MCP subprocess.
|
||||
#[serde(default)]
|
||||
pub mcp: McpConfig,
|
||||
#[serde(default)]
|
||||
pub compaction: Option<CompactionConfigPartial>,
|
||||
/// First-class web tool opt-in. See [`WebConfig`].
|
||||
|
|
@ -322,6 +326,11 @@ pub enum ResolveError {
|
|||
MissingField(&'static str),
|
||||
#[error("path must be absolute ({field}): {}", .path.display())]
|
||||
RelativePath { field: &'static str, path: PathBuf },
|
||||
#[error("invalid MCP config ({field}): {message}")]
|
||||
InvalidMcpConfig {
|
||||
field: &'static str,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Reject manifest fields that were intentionally removed and must not be
|
||||
|
|
@ -436,6 +445,11 @@ impl PodManifestConfig {
|
|||
*dir = join_if_relative(base, dir);
|
||||
}
|
||||
}
|
||||
for server in &mut self.mcp.stdio_servers {
|
||||
if let Some(McpStdioCwdPolicy::Path { path }) = &mut server.cwd {
|
||||
*path = join_if_relative(base, path);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -458,6 +472,7 @@ impl PodManifestConfig {
|
|||
),
|
||||
feature: self.feature.merge(upper.feature),
|
||||
plugins: merge_plugin_config(self.plugins, upper.plugins),
|
||||
mcp: merge_mcp_config(self.mcp, upper.mcp),
|
||||
compaction: merge_option(
|
||||
self.compaction,
|
||||
upper.compaction,
|
||||
|
|
@ -487,6 +502,11 @@ fn merge_plugin_config(mut base: PluginConfig, upper: PluginConfig) -> PluginCon
|
|||
base
|
||||
}
|
||||
|
||||
fn merge_mcp_config(mut base: McpConfig, upper: McpConfig) -> McpConfig {
|
||||
base.stdio_servers.extend(upper.stdio_servers);
|
||||
base
|
||||
}
|
||||
|
||||
impl WebConfig {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
|
|
@ -708,6 +728,149 @@ fn validate_model_paths(model: &ModelManifest, field: &'static str) -> Result<()
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_mcp_config(mcp: &McpConfig) -> Result<(), ResolveError> {
|
||||
let mut names = BTreeSet::new();
|
||||
for server in &mcp.stdio_servers {
|
||||
if server.name.trim().is_empty() {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.name",
|
||||
"server name must not be empty",
|
||||
));
|
||||
}
|
||||
if contains_nul(&server.name) {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.name",
|
||||
"server name must not contain NUL",
|
||||
));
|
||||
}
|
||||
if !names.insert(server.name.as_str()) {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.name",
|
||||
format!(
|
||||
"duplicate stdio server name `{}`",
|
||||
bounded_label(&server.name)
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if server.command.trim().is_empty() {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.command",
|
||||
"command must not be empty",
|
||||
));
|
||||
}
|
||||
if contains_nul(&server.command) {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.command",
|
||||
"command must not contain NUL",
|
||||
));
|
||||
}
|
||||
for arg in &server.args {
|
||||
if contains_nul(arg) {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.args",
|
||||
"argument must not contain NUL",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(McpStdioCwdPolicy::Path { path }) = &server.cwd {
|
||||
if path.as_os_str().is_empty() {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.cwd.path",
|
||||
"cwd path must not be empty",
|
||||
));
|
||||
}
|
||||
if !path.is_absolute() {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.cwd.path",
|
||||
"cwd path must be absolute after profile/manifest path resolution",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
for name in &server.env.inherit {
|
||||
validate_env_name("mcp.stdio_server.env.inherit", name)?;
|
||||
}
|
||||
for (name, value) in &server.env.set {
|
||||
validate_env_name("mcp.stdio_server.env.set", name)?;
|
||||
match value {
|
||||
McpEnvValue::Literal { value } => {
|
||||
if contains_nul(value) {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.env.set",
|
||||
"literal env value must not contain NUL",
|
||||
));
|
||||
}
|
||||
}
|
||||
McpEnvValue::SecretRef { ref_ } => {
|
||||
if secrets::validate_id(ref_).is_err() {
|
||||
return Err(invalid_mcp(
|
||||
"mcp.stdio_server.env.set.secret_ref",
|
||||
"secret_ref must be a valid local secret id",
|
||||
));
|
||||
}
|
||||
}
|
||||
McpEnvValue::EnvRef { name } => {
|
||||
validate_env_name("mcp.stdio_server.env.set.env_ref", name)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_env_name(field: &'static str, name: &str) -> Result<(), ResolveError> {
|
||||
let mut chars = name.chars();
|
||||
let Some(first) = chars.next() else {
|
||||
return Err(invalid_mcp(
|
||||
field,
|
||||
"environment variable name must not be empty",
|
||||
));
|
||||
};
|
||||
if !(first == '_' || first.is_ascii_alphabetic()) {
|
||||
return Err(invalid_mcp(
|
||||
field,
|
||||
"environment variable name must start with ASCII letter or underscore",
|
||||
));
|
||||
}
|
||||
if !chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric()) {
|
||||
return Err(invalid_mcp(
|
||||
field,
|
||||
"environment variable name must contain only ASCII letters, digits, and underscore",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn invalid_mcp(field: &'static str, message: impl Into<String>) -> ResolveError {
|
||||
ResolveError::InvalidMcpConfig {
|
||||
field,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_nul(value: &str) -> bool {
|
||||
value.as_bytes().contains(&0)
|
||||
}
|
||||
|
||||
fn bounded_label(value: &str) -> String {
|
||||
const MAX: usize = 80;
|
||||
let mut out = String::new();
|
||||
for (idx, ch) in value.chars().enumerate() {
|
||||
if idx >= MAX {
|
||||
out.push('…');
|
||||
break;
|
||||
}
|
||||
if ch.is_control() {
|
||||
out.push('?');
|
||||
} else {
|
||||
out.push(ch);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
type Error = ResolveError;
|
||||
|
||||
|
|
@ -842,6 +1005,8 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
|||
}
|
||||
}
|
||||
|
||||
validate_mcp_config(&cfg.mcp)?;
|
||||
|
||||
Ok(PodManifest {
|
||||
pod: PodMeta { name, prompt_pack },
|
||||
model: cfg.model,
|
||||
|
|
@ -852,6 +1017,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
|||
permissions,
|
||||
feature: FeatureConfig::from(cfg.feature),
|
||||
plugins: cfg.plugins,
|
||||
mcp: cfg.mcp,
|
||||
compaction,
|
||||
web: cfg.web,
|
||||
memory: cfg.memory,
|
||||
|
|
@ -899,6 +1065,7 @@ mod tests {
|
|||
permissions: None,
|
||||
feature: FeatureConfigPartial::default(),
|
||||
plugins: PluginConfig::default(),
|
||||
mcp: McpConfig::default(),
|
||||
session: None,
|
||||
compaction: None,
|
||||
web: None,
|
||||
|
|
@ -915,6 +1082,139 @@ mod tests {
|
|||
assert!(manifest.permissions.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_mcp_stdio_config_preserves_explicit_policy() {
|
||||
let mut cfg = minimal_valid();
|
||||
cfg.mcp.stdio_servers.push(crate::McpStdioServerConfig {
|
||||
name: "filesystem".into(),
|
||||
command: "node".into(),
|
||||
args: vec!["server.js".into(), "--root".into()],
|
||||
cwd: Some(McpStdioCwdPolicy::Path { path: abs("/mcp") }),
|
||||
env: crate::McpEnvConfig {
|
||||
inherit: vec!["PATH".into()],
|
||||
set: std::collections::BTreeMap::from([
|
||||
(
|
||||
"SAFE_MODE".into(),
|
||||
McpEnvValue::Literal { value: "1".into() },
|
||||
),
|
||||
(
|
||||
"TOKEN".into(),
|
||||
McpEnvValue::SecretRef {
|
||||
ref_: "providers/mcp-token".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"UPSTREAM".into(),
|
||||
McpEnvValue::EnvRef {
|
||||
name: "MCP_UPSTREAM_TOKEN".into(),
|
||||
},
|
||||
),
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
let manifest: PodManifest = cfg.try_into().unwrap();
|
||||
|
||||
assert_eq!(manifest.mcp.stdio_servers.len(), 1);
|
||||
let server = &manifest.mcp.stdio_servers[0];
|
||||
assert_eq!(server.name, "filesystem");
|
||||
assert_eq!(server.command, "node");
|
||||
assert_eq!(server.env.inherit, ["PATH"]);
|
||||
assert!(matches!(
|
||||
server.env.set["TOKEN"],
|
||||
McpEnvValue::SecretRef { .. }
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_mcp_rejects_empty_command_and_duplicates() {
|
||||
let mut cfg = minimal_valid();
|
||||
cfg.mcp.stdio_servers.push(crate::McpStdioServerConfig {
|
||||
name: "dup".into(),
|
||||
command: "".into(),
|
||||
args: Vec::new(),
|
||||
cwd: None,
|
||||
env: crate::McpEnvConfig::default(),
|
||||
});
|
||||
|
||||
let err = PodManifest::try_from(cfg).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ResolveError::InvalidMcpConfig {
|
||||
field: "mcp.stdio_server.command",
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
let mut cfg = minimal_valid();
|
||||
for command in ["one", "two"] {
|
||||
cfg.mcp.stdio_servers.push(crate::McpStdioServerConfig {
|
||||
name: "dup".into(),
|
||||
command: command.into(),
|
||||
args: Vec::new(),
|
||||
cwd: None,
|
||||
env: crate::McpEnvConfig::default(),
|
||||
});
|
||||
}
|
||||
|
||||
let err = PodManifest::try_from(cfg).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
ResolveError::InvalidMcpConfig {
|
||||
field: "mcp.stdio_server.name",
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_mcp_rejects_invalid_env_and_secret_ref_without_leaking_values() {
|
||||
let mut cfg = minimal_valid();
|
||||
cfg.mcp.stdio_servers.push(crate::McpStdioServerConfig {
|
||||
name: "secret".into(),
|
||||
command: "no-such-command-is-not-started".into(),
|
||||
args: Vec::new(),
|
||||
cwd: None,
|
||||
env: crate::McpEnvConfig {
|
||||
inherit: Vec::new(),
|
||||
set: std::collections::BTreeMap::from([(
|
||||
"TOKEN".into(),
|
||||
McpEnvValue::SecretRef {
|
||||
ref_: "bad secret id with spaces".into(),
|
||||
},
|
||||
)]),
|
||||
},
|
||||
});
|
||||
|
||||
let err = PodManifest::try_from(cfg).unwrap_err();
|
||||
let rendered = err.to_string();
|
||||
assert!(rendered.contains("secret_ref"));
|
||||
assert!(!rendered.contains("bad secret id with spaces"));
|
||||
|
||||
let value = McpEnvValue::Literal {
|
||||
value: "plaintext-secret-value".into(),
|
||||
};
|
||||
assert!(!format!("{value:?}").contains("plaintext-secret-value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_mcp_accepts_nonexistent_command_without_autostart() {
|
||||
let mut cfg = minimal_valid();
|
||||
cfg.mcp.stdio_servers.push(crate::McpStdioServerConfig {
|
||||
name: "later".into(),
|
||||
command: "definitely-not-a-command-yoi-must-spawn".into(),
|
||||
args: Vec::new(),
|
||||
cwd: None,
|
||||
env: crate::McpEnvConfig::default(),
|
||||
});
|
||||
|
||||
let manifest: PodManifest = cfg.try_into().unwrap();
|
||||
assert_eq!(
|
||||
manifest.mcp.stdio_servers[0].command,
|
||||
"definitely-not-a-command-yoi-must-spawn"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_session_record_event_trace() {
|
||||
let mut cfg = minimal_valid();
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ pub use profile::{
|
|||
pub use protocol::{Permission, ScopeRule};
|
||||
pub use scope::{DelegationScope, Scope, ScopeError, SharedScope};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fmt;
|
||||
use std::num::NonZeroU32;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::de::Error as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Declarative configuration for a Pod.
|
||||
|
|
@ -62,6 +64,12 @@ pub struct PodManifest {
|
|||
/// source-qualified entries listed here may resolve to active plugin metadata.
|
||||
#[serde(default)]
|
||||
pub plugins: plugin::PluginConfig,
|
||||
/// Explicit external Model Context Protocol provider configuration. This
|
||||
/// is config data only: declaring a server never starts a subprocess or
|
||||
/// grants OS sandboxing. Runtime MCP lifecycle/registration is a separate
|
||||
/// consumer boundary.
|
||||
#[serde(default)]
|
||||
pub mcp: McpConfig,
|
||||
#[serde(default)]
|
||||
pub compaction: Option<CompactionConfig>,
|
||||
/// Memory subsystem configuration. Presence of `[memory]` configures memory
|
||||
|
|
@ -194,6 +202,92 @@ pub struct SkillsConfig {
|
|||
pub directories: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Explicit Model Context Protocol configuration.
|
||||
///
|
||||
/// The manifest layer records local stdio MCP server declarations but never
|
||||
/// starts them. Future lifecycle code must opt in to spawning and must keep MCP
|
||||
/// process authority separate from Plugin permissions and `pod::feature` flags.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct McpConfig {
|
||||
/// Named local stdio servers. The list form keeps declarations explicit and
|
||||
/// lets validation reject duplicate names after profile/override merging.
|
||||
#[serde(default, rename = "stdio_server")]
|
||||
pub stdio_servers: Vec<McpStdioServerConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct McpStdioServerConfig {
|
||||
/// Stable profile-local name used by later lifecycle/tool-surface code.
|
||||
pub name: String,
|
||||
/// Executable path/name passed directly to process-spawn code in a later
|
||||
/// ticket. This is not a shell string and is not executed by config parsing.
|
||||
pub command: String,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
/// Optional working-directory policy for the future subprocess. Omitted
|
||||
/// means no config-level cwd override. Relative `path` values are resolved
|
||||
/// against the manifest/profile layer before final validation.
|
||||
#[serde(default)]
|
||||
pub cwd: Option<McpStdioCwdPolicy>,
|
||||
/// Explicit environment policy. There is no implicit environment discovery;
|
||||
/// future spawn code should inherit only names listed here and set only
|
||||
/// entries declared here.
|
||||
#[serde(default)]
|
||||
pub env: McpEnvConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum McpStdioCwdPolicy {
|
||||
/// Leave cwd selection to the lifecycle caller.
|
||||
Inherit,
|
||||
/// Use this absolute (after path resolution) working directory.
|
||||
Path { path: PathBuf },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct McpEnvConfig {
|
||||
/// Host environment variable names to copy explicitly at spawn time.
|
||||
#[serde(default)]
|
||||
pub inherit: Vec<String>,
|
||||
/// Environment variables to set explicitly.
|
||||
#[serde(default)]
|
||||
pub set: BTreeMap<String, McpEnvValue>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum McpEnvValue {
|
||||
/// Literal value. Use only for non-secret values; Debug/diagnostics redact
|
||||
/// it defensively because env values often become credentials over time.
|
||||
Literal { value: String },
|
||||
/// Local secret-store id. The plaintext is resolved only by a future runtime
|
||||
/// consumer and is never loaded during manifest/profile parsing.
|
||||
#[serde(rename = "secret_ref")]
|
||||
SecretRef {
|
||||
#[serde(rename = "ref")]
|
||||
ref_: String,
|
||||
},
|
||||
/// Name of a host environment variable to read explicitly at spawn time.
|
||||
EnvRef { name: String },
|
||||
}
|
||||
|
||||
impl fmt::Debug for McpEnvValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Literal { .. } => f
|
||||
.debug_struct("Literal")
|
||||
.field("value", &"[redacted]")
|
||||
.finish(),
|
||||
Self::SecretRef { ref_ } => f.debug_struct("SecretRef").field("ref_", ref_).finish(),
|
||||
Self::EnvRef { name } => f.debug_struct("EnvRef").field("name", name).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for WebSearch and WebFetch built-in tools.
|
||||
///
|
||||
/// Network tools are fail-closed: absent config or `enabled = false` disables
|
||||
|
|
@ -712,7 +806,10 @@ impl PodManifest {
|
|||
/// Parse a manifest from a TOML string.
|
||||
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
|
||||
config::reject_removed_manifest_fields(s)?;
|
||||
toml::from_str(s)
|
||||
let manifest: Self = toml::from_str(s)?;
|
||||
config::validate_mcp_config(&manifest.mcp)
|
||||
.map_err(|error| toml::de::Error::custom(error.to_string()))?;
|
||||
Ok(manifest)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,48 @@ const ZIP_COMPRESSION_STORED: u16 = 0;
|
|||
const ZIP_UNIX_SYMLINK_TYPE: u32 = 0o120000;
|
||||
const ZIP_UNIX_FILE_TYPE_MASK: u32 = 0o170000;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PluginTemplateResource {
|
||||
pub path: &'static str,
|
||||
pub contents: &'static str,
|
||||
}
|
||||
|
||||
/// Embedded starter template for Rust Component Model Tool Plugins.
|
||||
///
|
||||
/// The template is data only: it performs no filesystem/network operations and
|
||||
/// grants no authority. Future authoring CLI commands can materialize these
|
||||
/// files into a chosen destination after applying their own overwrite policy.
|
||||
pub const RUST_COMPONENT_TOOL_TEMPLATE: &[PluginTemplateResource] = &[
|
||||
PluginTemplateResource {
|
||||
path: "Cargo.toml",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-tool/Cargo.toml"
|
||||
),
|
||||
},
|
||||
PluginTemplateResource {
|
||||
path: "src/lib.rs",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-tool/src/lib.rs"
|
||||
),
|
||||
},
|
||||
PluginTemplateResource {
|
||||
path: "plugin.toml",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-tool/plugin.toml"
|
||||
),
|
||||
},
|
||||
PluginTemplateResource {
|
||||
path: "plugin.component.wasm",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-tool/plugin.component.wasm"
|
||||
),
|
||||
},
|
||||
PluginTemplateResource {
|
||||
path: "README.md",
|
||||
contents: include_str!("../../../resources/plugin/templates/rust-component-tool/README.md"),
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct PluginConfig {
|
||||
|
|
@ -482,6 +524,23 @@ pub struct DiscoveredPluginPackage {
|
|||
pub entries: BTreeSet<String>,
|
||||
}
|
||||
|
||||
/// Fully materialized package content used by local authoring checks and pack.
|
||||
///
|
||||
/// This is data-only metadata and bytes. Constructing it parses manifests and
|
||||
/// validates package/archive shape, but it does not load, instantiate, or
|
||||
/// execute Plugin code.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MaterializedPluginPackage {
|
||||
pub package: DiscoveredPluginPackage,
|
||||
pub files: BTreeMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PackedPluginPackage {
|
||||
pub output_path: PathBuf,
|
||||
pub package: DiscoveredPluginPackage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct PluginDiscoveryReport {
|
||||
pub packages: Vec<DiscoveredPluginPackage>,
|
||||
|
|
@ -1326,7 +1385,146 @@ fn read_package(
|
|||
.with_source(source)
|
||||
.with_package(label)
|
||||
})?;
|
||||
let archive = parse_stored_zip(&bytes, label, source, limits)?;
|
||||
materialize_archive(path, label, source, &bytes, limits)
|
||||
.map(|materialized| materialized.package)
|
||||
}
|
||||
|
||||
pub fn read_plugin_package_file(
|
||||
path: &Path,
|
||||
source: PluginSourceKind,
|
||||
limits: &PluginDiscoveryLimits,
|
||||
) -> Result<MaterializedPluginPackage, PluginDiagnostic> {
|
||||
let label = package_label(path);
|
||||
let metadata = fs::metadata(path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin package metadata could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.clone())
|
||||
})?;
|
||||
if !metadata.is_file() {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Malformed,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin package candidate is not a regular file",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label));
|
||||
}
|
||||
if metadata.len() > limits.max_package_size_bytes {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin package exceeds the configured package size bound",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label));
|
||||
}
|
||||
let bytes = fs::read(path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin package content could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.clone())
|
||||
})?;
|
||||
materialize_archive(path, &label, source, &bytes, limits)
|
||||
}
|
||||
|
||||
pub fn read_plugin_directory(
|
||||
path: &Path,
|
||||
source: PluginSourceKind,
|
||||
limits: &PluginDiscoveryLimits,
|
||||
) -> Result<MaterializedPluginPackage, PluginDiagnostic> {
|
||||
let label = package_label(path);
|
||||
let root = fs::canonicalize(path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin directory could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.clone())
|
||||
})?;
|
||||
let metadata = fs::metadata(&root).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin directory metadata could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.clone())
|
||||
})?;
|
||||
if !metadata.is_dir() {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Malformed,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin directory input is not a directory",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label));
|
||||
}
|
||||
|
||||
let mut files = BTreeMap::new();
|
||||
collect_directory_files(&root, &root, &label, source, limits, &mut files)?;
|
||||
materialize_files(path, label, source, files, limits)
|
||||
}
|
||||
|
||||
pub fn write_plugin_package_file(
|
||||
materialized: &MaterializedPluginPackage,
|
||||
output_path: &Path,
|
||||
limits: &PluginDiscoveryLimits,
|
||||
) -> Result<PackedPluginPackage, PluginDiagnostic> {
|
||||
write_stored_zip_file(output_path, &materialized.files, limits)?;
|
||||
let package = read_plugin_package_file(output_path, materialized.package.source(), limits)?;
|
||||
Ok(PackedPluginPackage {
|
||||
output_path: output_path.to_path_buf(),
|
||||
package: package.package,
|
||||
})
|
||||
}
|
||||
|
||||
impl DiscoveredPluginPackage {
|
||||
pub fn source(&self) -> PluginSourceKind {
|
||||
self.identity.source
|
||||
}
|
||||
}
|
||||
|
||||
fn materialize_archive(
|
||||
path: &Path,
|
||||
label: &str,
|
||||
source: PluginSourceKind,
|
||||
bytes: &[u8],
|
||||
limits: &PluginDiscoveryLimits,
|
||||
) -> Result<MaterializedPluginPackage, PluginDiagnostic> {
|
||||
let archive = parse_stored_zip(bytes, label, source, limits)?;
|
||||
materialize_files(path, label.to_string(), source, archive.files, limits)
|
||||
}
|
||||
|
||||
fn materialize_files(
|
||||
path: &Path,
|
||||
label: String,
|
||||
source: PluginSourceKind,
|
||||
files: BTreeMap<String, Vec<u8>>,
|
||||
limits: &PluginDiscoveryLimits,
|
||||
) -> Result<MaterializedPluginPackage, PluginDiagnostic> {
|
||||
let archive = StoredArchive {
|
||||
files: files.clone(),
|
||||
};
|
||||
let manifest_bytes = archive.files.get("plugin.toml").ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
|
|
@ -1334,7 +1532,7 @@ fn read_package(
|
|||
"plugin package is missing root plugin.toml",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label)
|
||||
.with_package(label.clone())
|
||||
})?;
|
||||
if manifest_bytes.len() > limits.max_manifest_size_bytes {
|
||||
return Err(PluginDiagnostic::new(
|
||||
|
|
@ -1352,7 +1550,7 @@ fn read_package(
|
|||
"plugin.toml is not valid UTF-8",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label)
|
||||
.with_package(label.clone())
|
||||
})?;
|
||||
let manifest: PluginPackageManifest = toml::from_str(manifest_text).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
|
|
@ -1361,20 +1559,282 @@ fn read_package(
|
|||
safe_toml_parse_message(&error),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label)
|
||||
.with_package(label.clone())
|
||||
})?;
|
||||
validate_manifest(&manifest, &archive, label, source)?;
|
||||
validate_manifest(&manifest, &archive, &label, source)?;
|
||||
let digest = deterministic_digest(&archive.files);
|
||||
let identity = SourceQualifiedPluginId::new(source, manifest.id.clone());
|
||||
|
||||
Ok(DiscoveredPluginPackage {
|
||||
let package = DiscoveredPluginPackage {
|
||||
identity,
|
||||
package_path: path.to_path_buf(),
|
||||
package_label: label.to_string(),
|
||||
package_label: label,
|
||||
digest,
|
||||
manifest,
|
||||
entries: archive.files.keys().cloned().collect(),
|
||||
})
|
||||
};
|
||||
Ok(MaterializedPluginPackage { package, files })
|
||||
}
|
||||
|
||||
fn collect_directory_files(
|
||||
root: &Path,
|
||||
dir: &Path,
|
||||
label: &str,
|
||||
source: PluginSourceKind,
|
||||
limits: &PluginDiscoveryLimits,
|
||||
files: &mut BTreeMap<String, Vec<u8>>,
|
||||
) -> Result<(), PluginDiagnostic> {
|
||||
let mut entries = fs::read_dir(dir)
|
||||
.map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin directory could not be listed: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string())
|
||||
})?
|
||||
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
|
||||
.collect::<Vec<_>>();
|
||||
entries.sort();
|
||||
|
||||
for path in entries {
|
||||
let metadata = fs::symlink_metadata(&path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin directory entry metadata could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string())
|
||||
})?;
|
||||
if metadata.file_type().is_symlink() {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Traversal,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin directory contains a symlink entry",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string()));
|
||||
}
|
||||
if metadata.is_dir() {
|
||||
collect_directory_files(root, &path, label, source, limits, files)?;
|
||||
continue;
|
||||
}
|
||||
if !metadata.is_file() {
|
||||
continue;
|
||||
}
|
||||
if metadata.len() > limits.max_file_size_bytes {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin directory file exceeds the configured per-file bound",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string()));
|
||||
}
|
||||
let canonical = fs::canonicalize(&path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin directory file could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string())
|
||||
})?;
|
||||
if !canonical.starts_with(root) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Traversal,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin directory file escapes the package root",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string()));
|
||||
}
|
||||
let relative = canonical.strip_prefix(root).map_err(|_| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Traversal,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin directory file escapes the package root",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string())
|
||||
})?;
|
||||
let normalized = relative
|
||||
.components()
|
||||
.map(|component| component.as_os_str().to_str())
|
||||
.collect::<Option<Vec<_>>>()
|
||||
.and_then(|parts| normalize_archive_path(&parts.join("/")))
|
||||
.ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Traversal,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin directory contains an unsafe relative path",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string())
|
||||
})?;
|
||||
let content = fs::read(&path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin directory file could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string())
|
||||
})?;
|
||||
files.insert(normalized, content);
|
||||
if files.len() > limits.max_entries_per_package {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin directory contains more files than the configured bound",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string()));
|
||||
}
|
||||
let expanded_size = files
|
||||
.values()
|
||||
.map(|content| content.len() as u64)
|
||||
.sum::<u64>();
|
||||
if expanded_size > limits.max_expanded_size_bytes {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin directory expanded size exceeds the configured bound",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_package(label.to_string()));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_stored_zip_file(
|
||||
output_path: &Path,
|
||||
files: &BTreeMap<String, Vec<u8>>,
|
||||
limits: &PluginDiscoveryLimits,
|
||||
) -> Result<(), PluginDiagnostic> {
|
||||
if files.len() > limits.max_entries_per_package {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin package contains more entries than the configured bound",
|
||||
));
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
let mut central = Vec::new();
|
||||
for (name, content) in files {
|
||||
let name = normalize_archive_path(name).ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Traversal,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin package entry path escapes the archive root",
|
||||
)
|
||||
})?;
|
||||
if content.len() as u64 > limits.max_file_size_bytes {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin package entry exceeds the configured per-file bound",
|
||||
));
|
||||
}
|
||||
let local_offset = bytes.len() as u32;
|
||||
write_u32_vec(&mut bytes, ZIP_LOCAL_FILE);
|
||||
write_u16_vec(&mut bytes, 20);
|
||||
write_u16_vec(&mut bytes, 0x0800);
|
||||
write_u16_vec(&mut bytes, ZIP_COMPRESSION_STORED);
|
||||
write_u16_vec(&mut bytes, 0);
|
||||
write_u16_vec(&mut bytes, 0);
|
||||
write_u32_vec(&mut bytes, 0);
|
||||
write_u32_vec(&mut bytes, content.len() as u32);
|
||||
write_u32_vec(&mut bytes, content.len() as u32);
|
||||
write_u16_vec(&mut bytes, name.len() as u16);
|
||||
write_u16_vec(&mut bytes, 0);
|
||||
bytes.extend_from_slice(name.as_bytes());
|
||||
bytes.extend_from_slice(content);
|
||||
|
||||
write_u32_vec(&mut central, ZIP_CENTRAL_DIRECTORY);
|
||||
write_u16_vec(&mut central, 20);
|
||||
write_u16_vec(&mut central, 20);
|
||||
write_u16_vec(&mut central, 0x0800);
|
||||
write_u16_vec(&mut central, ZIP_COMPRESSION_STORED);
|
||||
write_u16_vec(&mut central, 0);
|
||||
write_u16_vec(&mut central, 0);
|
||||
write_u32_vec(&mut central, 0);
|
||||
write_u32_vec(&mut central, content.len() as u32);
|
||||
write_u32_vec(&mut central, content.len() as u32);
|
||||
write_u16_vec(&mut central, name.len() as u16);
|
||||
write_u16_vec(&mut central, 0);
|
||||
write_u16_vec(&mut central, 0);
|
||||
write_u16_vec(&mut central, 0);
|
||||
write_u16_vec(&mut central, 0);
|
||||
write_u32_vec(&mut central, 0);
|
||||
write_u32_vec(&mut central, local_offset);
|
||||
central.extend_from_slice(name.as_bytes());
|
||||
}
|
||||
let central_offset = bytes.len() as u32;
|
||||
bytes.extend_from_slice(¢ral);
|
||||
write_u32_vec(&mut bytes, ZIP_EOCD);
|
||||
write_u16_vec(&mut bytes, 0);
|
||||
write_u16_vec(&mut bytes, 0);
|
||||
write_u16_vec(&mut bytes, files.len() as u16);
|
||||
write_u16_vec(&mut bytes, files.len() as u16);
|
||||
write_u32_vec(&mut bytes, central.len() as u32);
|
||||
write_u32_vec(&mut bytes, central_offset);
|
||||
write_u16_vec(&mut bytes, 0);
|
||||
if bytes.len() as u64 > limits.max_package_size_bytes {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"plugin package exceeds the configured package size bound",
|
||||
));
|
||||
}
|
||||
if let Some(parent) = output_path
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
{
|
||||
fs::create_dir_all(parent).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin package output directory could not be created: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
fs::write(output_path, bytes).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"plugin package output could not be written: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_u16_vec(out: &mut Vec<u8>, value: u16) {
|
||||
out.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
fn write_u32_vec(out: &mut Vec<u8>, value: u32) {
|
||||
out.extend_from_slice(&value.to_le_bytes());
|
||||
}
|
||||
|
||||
fn validate_manifest(
|
||||
|
|
@ -1957,6 +2417,40 @@ mod tests {
|
|||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn embedded_rust_component_tool_template_is_valid_package_shape() {
|
||||
let paths: BTreeSet<_> = RUST_COMPONENT_TOOL_TEMPLATE
|
||||
.iter()
|
||||
.map(|file| file.path)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
paths,
|
||||
BTreeSet::from(["Cargo.toml", "src/lib.rs", "plugin.toml", "README.md"])
|
||||
);
|
||||
assert!(
|
||||
RUST_COMPONENT_TOOL_TEMPLATE
|
||||
.iter()
|
||||
.all(|file| !file.path.starts_with('/') && !file.path.contains(".."))
|
||||
);
|
||||
|
||||
let manifest_text = RUST_COMPONENT_TOOL_TEMPLATE
|
||||
.iter()
|
||||
.find(|file| file.path == "plugin.toml")
|
||||
.unwrap()
|
||||
.contents;
|
||||
let manifest: PluginPackageManifest = toml::from_str(manifest_text).unwrap();
|
||||
assert_eq!(manifest.schema_version, SUPPORTED_PLUGIN_API_VERSION);
|
||||
assert_eq!(
|
||||
manifest.runtime.as_ref().unwrap().kind,
|
||||
PLUGIN_RUNTIME_COMPONENT_KIND
|
||||
);
|
||||
assert_eq!(
|
||||
manifest.runtime.as_ref().unwrap().world.as_deref(),
|
||||
Some(PLUGIN_COMPONENT_TOOL_WORLD)
|
||||
);
|
||||
assert_eq!(manifest.tools.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_valid_user_and_workspace_packages() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ use crate::config::{
|
|||
use crate::model::{AuthRef, ModelManifest};
|
||||
use crate::plugin::PluginConfig;
|
||||
use crate::{
|
||||
MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError,
|
||||
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifestConfig, paths,
|
||||
McpConfig, McpStdioCwdPolicy, MemoryConfig, Permission, PodManifest, PodManifestConfig,
|
||||
PodMetaConfig, ResolveError, ScopeConfig, ScopeRule, SkillsConfig, WebConfig,
|
||||
WorkerManifestConfig, paths,
|
||||
};
|
||||
|
||||
const PROFILE_FORMAT_V1: &str = "yoi.lua-profile.v1";
|
||||
|
|
@ -628,6 +629,7 @@ fn resolve_lua_profile_value(
|
|||
permissions: profile.permissions,
|
||||
feature: profile.feature,
|
||||
plugins: profile.plugins,
|
||||
mcp: profile.mcp,
|
||||
compaction,
|
||||
web: profile.web,
|
||||
memory: profile.memory,
|
||||
|
|
@ -691,6 +693,8 @@ struct ProfileConfig {
|
|||
#[serde(default)]
|
||||
plugins: PluginConfig,
|
||||
#[serde(default)]
|
||||
mcp: McpConfig,
|
||||
#[serde(default)]
|
||||
compaction: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
web: Option<WebConfig>,
|
||||
|
|
@ -1247,6 +1251,16 @@ fn validate_profile_paths(profile: &ProfileConfig) -> Result<(), ProfileError> {
|
|||
}
|
||||
}
|
||||
}
|
||||
for server in &profile.mcp.stdio_servers {
|
||||
if let Some(McpStdioCwdPolicy::Path { path }) = &server.cwd
|
||||
&& path.is_absolute()
|
||||
{
|
||||
return Err(ProfileError::InvalidProfile(
|
||||
"field `mcp.stdio_server.cwd.path` must be profile-relative in reusable Profiles"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn reject_absolute_auth_file(
|
||||
|
|
@ -1693,6 +1707,66 @@ return profile {
|
|||
Some("coder")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lua_profile_resolves_named_mcp_stdio_config_without_starting_command() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let profile = write_profile(
|
||||
tmp.path(),
|
||||
"mcp.lua",
|
||||
r#"
|
||||
local profile = require("yoi.profile")
|
||||
return profile {
|
||||
slug = "mcp",
|
||||
model = { scheme = "anthropic", model_id = "claude-sonnet-4-20250514" },
|
||||
mcp = {
|
||||
stdio_server = {
|
||||
{
|
||||
name = "filesystem",
|
||||
command = "definitely-not-spawned-during-profile-resolution",
|
||||
args = { "--root", "." },
|
||||
cwd = { kind = "path", path = "servers" },
|
||||
env = {
|
||||
inherit = { "PATH" },
|
||||
set = {
|
||||
SAFE_MODE = { kind = "literal", value = "1" },
|
||||
API_TOKEN = { kind = "secret_ref", ref = "providers/mcp-token" },
|
||||
FROM_ENV = { kind = "env_ref", name = "MCP_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
"#,
|
||||
);
|
||||
std::fs::create_dir(tmp.path().join("servers")).unwrap();
|
||||
let workspace = tmp.path().join("workspace");
|
||||
std::fs::create_dir(&workspace).unwrap();
|
||||
|
||||
let resolved = ProfileResolver::new()
|
||||
.with_workspace_base(&workspace)
|
||||
.resolve(
|
||||
&ProfileSelector::path(&profile),
|
||||
ProfileResolveOptions::with_pod_name("runtime-pod"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let server = &resolved.manifest.mcp.stdio_servers[0];
|
||||
assert_eq!(server.name, "filesystem");
|
||||
assert_eq!(
|
||||
server.command,
|
||||
"definitely-not-spawned-during-profile-resolution"
|
||||
);
|
||||
assert!(matches!(
|
||||
server.cwd,
|
||||
Some(McpStdioCwdPolicy::Path { ref path }) if path == &tmp.path().join("servers")
|
||||
));
|
||||
assert!(matches!(
|
||||
server.env.set["API_TOKEN"],
|
||||
crate::McpEnvValue::SecretRef { .. }
|
||||
));
|
||||
}
|
||||
#[test]
|
||||
fn resolves_lua_profile_feature_flags_without_runtime_state() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
|
|
|||
23
crates/mcp/Cargo.toml
Normal file
23
crates/mcp/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "mcp"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
manifest = { workspace = true }
|
||||
secrets = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["io-util", "process", "sync", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["io-util", "macros", "process", "rt-multi-thread", "sync", "time"] }
|
||||
|
||||
[[bin]]
|
||||
name = "mcp-stdio-mock-server"
|
||||
path = "tests/fixtures/mock_server.rs"
|
||||
test = false
|
||||
bench = false
|
||||
doc = false
|
||||
7
crates/mcp/src/lib.rs
Normal file
7
crates/mcp/src/lib.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
//! Model Context Protocol client foundations.
|
||||
//!
|
||||
//! This crate intentionally only owns protocol/lifecycle plumbing. It does not
|
||||
//! register MCP tools, resources, or prompts into Yoi's model-visible tool
|
||||
//! surface.
|
||||
|
||||
pub mod stdio;
|
||||
1130
crates/mcp/src/stdio.rs
Normal file
1130
crates/mcp/src/stdio.rs
Normal file
File diff suppressed because it is too large
Load Diff
116
crates/mcp/tests/fixtures/mock_server.rs
vendored
Normal file
116
crates/mcp/tests/fixtures/mock_server.rs
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
use std::env;
|
||||
use std::io::{self, BufRead, Write};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn main() {
|
||||
let mode = env::var("YOI_MCP_MOCK_MODE").unwrap_or_else(|_| "success".to_string());
|
||||
match mode.as_str() {
|
||||
"success" => success(),
|
||||
"fail-init" => fail_init(),
|
||||
"sampling" => sampling_request(),
|
||||
"shutdown-hang" => shutdown_hang(),
|
||||
other => panic!("unknown mock mode: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn success() {
|
||||
let init = read_json();
|
||||
assert_eq!(init["method"], "initialize");
|
||||
assert!(init["params"]["capabilities"].get("sampling").is_none());
|
||||
assert!(init["params"]["capabilities"].get("elicitation").is_none());
|
||||
write_json(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": init["id"],
|
||||
"result": initialize_result(),
|
||||
}));
|
||||
let initialized = read_json();
|
||||
assert_eq!(initialized["method"], "notifications/initialized");
|
||||
drain_stdin();
|
||||
}
|
||||
|
||||
fn fail_init() {
|
||||
let secret = env::var("MCP_TEST_SECRET").unwrap_or_default();
|
||||
for idx in 0..5 {
|
||||
eprintln!("diagnostic {idx}: secret={secret}");
|
||||
}
|
||||
let init = read_json();
|
||||
write_json(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": init["id"],
|
||||
"error": {
|
||||
"code": -32000,
|
||||
"message": format!("init rejected with {secret}"),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
fn sampling_request() {
|
||||
let init = read_json();
|
||||
write_json(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": init["id"],
|
||||
"result": initialize_result(),
|
||||
}));
|
||||
let initialized = read_json();
|
||||
assert_eq!(initialized["method"], "notifications/initialized");
|
||||
write_json(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 99,
|
||||
"method": "sampling/createMessage",
|
||||
"params": {},
|
||||
}));
|
||||
let response = read_json();
|
||||
assert_eq!(response["id"], 99);
|
||||
assert_eq!(response["error"]["code"], -32601);
|
||||
}
|
||||
|
||||
fn shutdown_hang() {
|
||||
let init = read_json();
|
||||
write_json(json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": init["id"],
|
||||
"result": initialize_result(),
|
||||
}));
|
||||
let initialized = read_json();
|
||||
assert_eq!(initialized["method"], "notifications/initialized");
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(60));
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_result() -> Value {
|
||||
json!({
|
||||
"protocolVersion": "2025-11-25",
|
||||
"capabilities": {
|
||||
"tools": { "listChanged": true }
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "mock-mcp",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn read_json() -> Value {
|
||||
let mut line = String::new();
|
||||
let read = io::stdin().lock().read_line(&mut line).expect("read stdin");
|
||||
assert_ne!(read, 0, "stdin closed before JSON-RPC message");
|
||||
serde_json::from_str(&line).expect("valid JSON-RPC line")
|
||||
}
|
||||
|
||||
fn write_json(value: Value) {
|
||||
let mut stdout = io::stdout().lock();
|
||||
serde_json::to_writer(&mut stdout, &value).expect("write JSON");
|
||||
stdout.write_all(b"\n").expect("write newline");
|
||||
stdout.flush().expect("flush stdout");
|
||||
}
|
||||
|
||||
fn drain_stdin() {
|
||||
let mut line = String::new();
|
||||
while io::stdin().lock().read_line(&mut line).unwrap_or(0) != 0 {
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
122
crates/mcp/tests/stdio_lifecycle.rs
Normal file
122
crates/mcp/tests/stdio_lifecycle.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use mcp::stdio::{McpErrorKind, McpPhase, McpStdioClient, McpStdioLimits, McpStdioServerSpec};
|
||||
|
||||
fn mock_server(mode: &str) -> McpStdioServerSpec {
|
||||
McpStdioServerSpec::new("mock", env!("CARGO_BIN_EXE_mcp-stdio-mock-server"))
|
||||
.env("YOI_MCP_MOCK_MODE", mode)
|
||||
}
|
||||
|
||||
fn tight_limits() -> McpStdioLimits {
|
||||
McpStdioLimits {
|
||||
startup_timeout: Duration::from_secs(2),
|
||||
request_timeout: Duration::from_secs(2),
|
||||
shutdown_timeout: Duration::from_millis(100),
|
||||
kill_timeout: Duration::from_millis(100),
|
||||
max_diagnostic_lines: 2,
|
||||
max_stderr_line_bytes: 256,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdio_server_spec_debug_redacts_resolved_env_values() {
|
||||
let spec = McpStdioServerSpec::new("debug-mock", "/bin/mock-mcp")
|
||||
.arg("--stdio")
|
||||
.cwd("/tmp/mock-mcp")
|
||||
.env("LITERAL_VALUE", "literal-plaintext")
|
||||
.env("INHERITED_VALUE", "inherited-plaintext")
|
||||
.env("ENV_REF_VALUE", "env-ref-plaintext")
|
||||
.env("SECRET_REF_VALUE", "secret-ref-plaintext");
|
||||
|
||||
let debug = format!("{spec:?}");
|
||||
|
||||
assert!(debug.contains("debug-mock"));
|
||||
assert!(debug.contains("/bin/mock-mcp"));
|
||||
assert!(debug.contains("--stdio"));
|
||||
assert!(debug.contains("/tmp/mock-mcp"));
|
||||
assert!(debug.contains("LITERAL_VALUE"));
|
||||
assert!(debug.contains("INHERITED_VALUE"));
|
||||
assert!(debug.contains("ENV_REF_VALUE"));
|
||||
assert!(debug.contains("SECRET_REF_VALUE"));
|
||||
assert!(debug.contains("[redacted]"));
|
||||
|
||||
assert!(!debug.contains("literal-plaintext"));
|
||||
assert!(!debug.contains("inherited-plaintext"));
|
||||
assert!(!debug.contains("env-ref-plaintext"));
|
||||
assert!(!debug.contains("secret-ref-plaintext"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initializes_mock_stdio_server() {
|
||||
let mut client = McpStdioClient::connect(mock_server("success"), tight_limits())
|
||||
.await
|
||||
.expect("initialize succeeds");
|
||||
let result = client.initialize_result().expect("initialize result");
|
||||
assert_eq!(result.protocol_version, "2025-11-25");
|
||||
assert_eq!(result.server_info.name, "mock-mcp");
|
||||
let shutdown = client.shutdown().await.expect("shutdown succeeds");
|
||||
assert!(!shutdown.terminated);
|
||||
assert!(!shutdown.killed);
|
||||
assert!(shutdown.exit_status.is_some_and(|status| status.success()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn initialize_failure_reports_server_phase_and_redacted_bounded_stderr() {
|
||||
let spec = mock_server("fail-init").env("MCP_TEST_SECRET", "super-secret-token");
|
||||
let err = match McpStdioClient::connect(spec, tight_limits()).await {
|
||||
Ok(mut client) => {
|
||||
let _ = client.shutdown().await;
|
||||
panic!("initialize unexpectedly succeeded");
|
||||
}
|
||||
Err(err) => err,
|
||||
};
|
||||
assert_eq!(err.server_name, "mock");
|
||||
assert_eq!(err.phase, McpPhase::Initialize);
|
||||
match &err.kind {
|
||||
McpErrorKind::JsonRpcError { code, message } => {
|
||||
assert_eq!(*code, -32000);
|
||||
assert!(!message.contains("super-secret-token"));
|
||||
assert!(message.contains("[redacted]"));
|
||||
}
|
||||
other => panic!("unexpected error kind: {other:?}"),
|
||||
}
|
||||
let rendered = err.to_string();
|
||||
assert!(rendered.contains("mock"));
|
||||
assert!(rendered.contains("initialize"));
|
||||
let diagnostics = err.diagnostics().expect("diagnostics");
|
||||
assert_eq!(diagnostics.server_name, "mock");
|
||||
assert_eq!(diagnostics.stderr.len(), 2);
|
||||
assert!(diagnostics.dropped_stderr_lines >= 3);
|
||||
assert!(
|
||||
diagnostics
|
||||
.stderr
|
||||
.iter()
|
||||
.all(|line| !line.contains("super-secret-token"))
|
||||
);
|
||||
assert!(
|
||||
diagnostics
|
||||
.stderr
|
||||
.iter()
|
||||
.any(|line| line.contains("[redacted]"))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shutdown_terminates_or_kills_uncooperative_server() {
|
||||
let mut client = McpStdioClient::connect(mock_server("shutdown-hang"), tight_limits())
|
||||
.await
|
||||
.expect("initialize succeeds");
|
||||
let shutdown = client.shutdown().await.expect("shutdown succeeds");
|
||||
assert!(shutdown.terminated || shutdown.killed);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sampling_requests_fail_closed_and_are_not_advertised() {
|
||||
let mut client = McpStdioClient::connect(mock_server("sampling"), tight_limits())
|
||||
.await
|
||||
.expect("initialize succeeds");
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
let shutdown = client.shutdown().await.expect("shutdown succeeds");
|
||||
assert!(shutdown.exit_status.is_some_and(|status| status.success()));
|
||||
}
|
||||
17
crates/plugin-pdk/Cargo.toml
Normal file
17
crates/plugin-pdk/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "yoi-plugin-pdk"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Guest-side Rust PDK helpers for Yoi Component Model Tool plugins"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
wit-bindgen = "0.51.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
toml.workspace = true
|
||||
476
crates/plugin-pdk/src/lib.rs
Normal file
476
crates/plugin-pdk/src/lib.rs
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
//! Guest-side helpers for Yoi Component Model Tool plugins.
|
||||
//!
|
||||
//! This crate is intentionally small and guest-only: it depends on JSON/WIT
|
||||
//! binding support, but not on Yoi host/runtime crates. It grants no authority;
|
||||
//! package manifests and host-side Plugin grants decide whether a Tool or host
|
||||
//! API can run.
|
||||
//!
|
||||
//! Component authors still generate the WIT bindings in their guest crate, then
|
||||
//! delegate the exported `call` function to [`run_json_tool`] or to the
|
||||
//! [`export_component_tool!`] macro:
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use yoi_plugin_pdk::wit_bindgen;
|
||||
//!
|
||||
//! wit_bindgen::generate!({
|
||||
//! world: "tool",
|
||||
//! path: "../../../../resources/plugin/wit",
|
||||
//! generate_all,
|
||||
//! runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
//! });
|
||||
//!
|
||||
//! fn echo(
|
||||
//! ctx: yoi_plugin_pdk::ToolContext,
|
||||
//! input: EchoInput,
|
||||
//! ) -> Result<yoi_plugin_pdk::ToolOutput, yoi_plugin_pdk::ToolError> {
|
||||
//! yoi_plugin_pdk::ToolOutput::json(
|
||||
//! format!("{} ok", ctx.tool_name()),
|
||||
//! EchoOutput { text: input.text },
|
||||
//! )
|
||||
//! }
|
||||
//!
|
||||
//! yoi_plugin_pdk::export_component_tool!(Plugin, echo);
|
||||
//! ```
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::Value;
|
||||
|
||||
pub use wit_bindgen;
|
||||
|
||||
/// Current Yoi Component Model Tool world targeted by this PDK.
|
||||
pub const TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0";
|
||||
|
||||
/// Repository WIT for the current Tool world, exposed for authoring tools and
|
||||
/// tests. Runtime components should still generate bindings at compile time.
|
||||
pub const TOOL_WIT: &str = include_str!("../../../resources/plugin/wit/yoi-plugin-tool-v1.wit");
|
||||
|
||||
/// Repository WIT for the grant-bound host APIs importable by Tool components.
|
||||
pub const HOST_WIT: &str =
|
||||
include_str!("../../../resources/plugin/wit/deps/yoi-host/yoi-host-v1.wit");
|
||||
|
||||
/// Maximum serialized ToolOutput JSON accepted by Yoi's current Plugin runtime.
|
||||
pub const MAX_TOOL_OUTPUT_BYTES: usize = 64 * 1024;
|
||||
/// Maximum summary bytes accepted by Yoi's current Plugin runtime.
|
||||
pub const MAX_SUMMARY_BYTES: usize = 1024;
|
||||
/// Conservative content cap that leaves room for JSON framing and escaping.
|
||||
pub const MAX_CONTENT_BYTES: usize = MAX_TOOL_OUTPUT_BYTES - MAX_SUMMARY_BYTES - 4096;
|
||||
/// Maximum structured error message bytes retained by the PDK.
|
||||
pub const MAX_ERROR_MESSAGE_BYTES: usize = 4096;
|
||||
|
||||
/// Per-call context passed to a typed JSON handler.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ToolContext {
|
||||
tool_name: String,
|
||||
}
|
||||
|
||||
impl ToolContext {
|
||||
/// Create context for the manifest-declared Tool name selected by the host.
|
||||
pub fn new(tool_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
tool_name: bounded_text(tool_name.into(), MAX_SUMMARY_BYTES),
|
||||
}
|
||||
}
|
||||
|
||||
/// Manifest-declared Tool name supplied by the host runtime.
|
||||
pub fn tool_name(&self) -> &str {
|
||||
&self.tool_name
|
||||
}
|
||||
}
|
||||
|
||||
/// ToolOutput JSON shape accepted by the current Yoi Plugin runtime.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct ToolOutput {
|
||||
summary: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
impl ToolOutput {
|
||||
/// Create an ordinary Tool output.
|
||||
pub fn new(summary: impl Into<String>, content: Option<String>) -> Self {
|
||||
let mut output = Self {
|
||||
summary: normalize_summary(summary.into()),
|
||||
content: content.map(|value| bounded_text(value, MAX_CONTENT_BYTES)),
|
||||
};
|
||||
output.bound_serialized_json();
|
||||
output
|
||||
}
|
||||
|
||||
/// Create a Tool output whose content is typed JSON.
|
||||
pub fn json(summary: impl Into<String>, value: impl Serialize) -> Result<Self, ToolError> {
|
||||
let content = serde_json::to_string(&value).map_err(ToolError::serialization)?;
|
||||
let output = Self {
|
||||
summary: normalize_summary(summary.into()),
|
||||
content: Some(content),
|
||||
};
|
||||
let serialized = serde_json::to_string(&output).map_err(ToolError::serialization)?;
|
||||
if serialized.len() > MAX_TOOL_OUTPUT_BYTES {
|
||||
return Err(ToolError::invalid_output(format!(
|
||||
"serialized ToolOutput JSON exceeds {MAX_TOOL_OUTPUT_BYTES} bytes"
|
||||
)));
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Create a summary-only Tool output.
|
||||
pub fn summary(summary: impl Into<String>) -> Self {
|
||||
Self::new(summary, None)
|
||||
}
|
||||
|
||||
/// Return the bounded summary.
|
||||
pub fn summary_text(&self) -> &str {
|
||||
&self.summary
|
||||
}
|
||||
|
||||
/// Return optional detailed content.
|
||||
pub fn content(&self) -> Option<&str> {
|
||||
self.content.as_deref()
|
||||
}
|
||||
|
||||
/// Serialize to the ToolOutput JSON string returned by the WIT `call` export.
|
||||
pub fn to_json_string(&self) -> String {
|
||||
serde_json::to_string(self).unwrap_or_else(|_| {
|
||||
r#"{"summary":"tool error: serialization","content":"{\"error\":{\"code\":\"serialization\",\"message\":\"failed to serialize ToolOutput\"}}"}"#.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn bound_serialized_json(&mut self) {
|
||||
loop {
|
||||
let Ok(serialized) = serde_json::to_string(self) else {
|
||||
return;
|
||||
};
|
||||
if serialized.len() <= MAX_TOOL_OUTPUT_BYTES {
|
||||
return;
|
||||
}
|
||||
let Some(content) = self.content.take() else {
|
||||
return;
|
||||
};
|
||||
if content.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let overflow = serialized.len() - MAX_TOOL_OUTPUT_BYTES;
|
||||
let target = content.len().saturating_sub(overflow + "…".len());
|
||||
self.content = Some(bounded_text(content, target));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable, low-cardinality error codes for PDK-produced Tool errors.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ToolErrorCode {
|
||||
InvalidInput,
|
||||
InvalidOutput,
|
||||
Serialization,
|
||||
Denied,
|
||||
UnsupportedTool,
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl ToolErrorCode {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::InvalidInput => "invalid_input",
|
||||
Self::InvalidOutput => "invalid_output",
|
||||
Self::Serialization => "serialization",
|
||||
Self::Denied => "denied",
|
||||
Self::UnsupportedTool => "unsupported_tool",
|
||||
Self::Failed => "failed",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Structured, bounded error that is rendered as ordinary ToolOutput JSON.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, thiserror::Error)]
|
||||
#[error("{code:?}: {message}")]
|
||||
pub struct ToolError {
|
||||
code: ToolErrorCode,
|
||||
message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
details: Option<Value>,
|
||||
}
|
||||
|
||||
impl ToolError {
|
||||
pub fn new(code: ToolErrorCode, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: bounded_text(message.into(), MAX_ERROR_MESSAGE_BYTES),
|
||||
details: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalid_input(message: impl Into<String>) -> Self {
|
||||
Self::new(ToolErrorCode::InvalidInput, message)
|
||||
}
|
||||
|
||||
pub fn invalid_output(message: impl Into<String>) -> Self {
|
||||
Self::new(ToolErrorCode::InvalidOutput, message)
|
||||
}
|
||||
|
||||
pub fn serialization(error: impl std::fmt::Display) -> Self {
|
||||
Self::new(ToolErrorCode::Serialization, error.to_string())
|
||||
}
|
||||
|
||||
pub fn denied(message: impl Into<String>) -> Self {
|
||||
Self::new(ToolErrorCode::Denied, message)
|
||||
}
|
||||
|
||||
pub fn unsupported_tool(tool_name: impl Into<String>) -> Self {
|
||||
Self::new(
|
||||
ToolErrorCode::UnsupportedTool,
|
||||
format!("unsupported tool `{}`", tool_name.into()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn failed(message: impl Into<String>) -> Self {
|
||||
Self::new(ToolErrorCode::Failed, message)
|
||||
}
|
||||
|
||||
pub fn with_details(mut self, details: impl Serialize) -> Self {
|
||||
self.details = serde_json::to_value(details).ok();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn code(&self) -> ToolErrorCode {
|
||||
self.code
|
||||
}
|
||||
|
||||
pub fn code_str(&self) -> &'static str {
|
||||
self.code.as_str()
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
|
||||
/// Render this error as ordinary ToolOutput JSON content.
|
||||
pub fn into_tool_output(self) -> ToolOutput {
|
||||
#[derive(Serialize)]
|
||||
struct ErrorBody<'a> {
|
||||
error: ErrorPayload<'a>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ErrorPayload<'a> {
|
||||
code: &'static str,
|
||||
message: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
details: Option<&'a Value>,
|
||||
}
|
||||
|
||||
let summary = format!("tool error: {}", self.code.as_str());
|
||||
let payload = ErrorBody {
|
||||
error: ErrorPayload {
|
||||
code: self.code.as_str(),
|
||||
message: &self.message,
|
||||
details: self.details.as_ref(),
|
||||
},
|
||||
};
|
||||
let mut content = serde_json::to_string(&payload).unwrap_or_else(|_| {
|
||||
format!(
|
||||
r#"{{"error":{{"code":"{}","message":"failed to serialize error details"}}}}"#,
|
||||
ToolErrorCode::Serialization.as_str()
|
||||
)
|
||||
});
|
||||
if content.len() > MAX_CONTENT_BYTES {
|
||||
let payload = ErrorBody {
|
||||
error: ErrorPayload {
|
||||
code: self.code.as_str(),
|
||||
message: &self.message,
|
||||
details: None,
|
||||
},
|
||||
};
|
||||
content = serde_json::to_string(&payload).unwrap_or_else(|_| {
|
||||
r#"{"error":{"code":"serialization","message":"failed to serialize error"}}"#
|
||||
.to_string()
|
||||
});
|
||||
}
|
||||
ToolOutput::new(summary, Some(content))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the WIT `input-json` string into a typed input value.
|
||||
pub fn parse_json_input<T>(input_json: &str) -> Result<T, ToolError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
serde_json::from_str(input_json).map_err(|error| {
|
||||
ToolError::invalid_input(format!(
|
||||
"tool input is not valid JSON for this schema: {error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a typed JSON Tool handler and return ToolOutput JSON for the runtime.
|
||||
///
|
||||
/// Handler errors and parse/serialization failures are not panics or host-side
|
||||
/// authority decisions. They are rendered into ordinary ToolOutput JSON so the
|
||||
/// runtime can route them through the normal Tool result path.
|
||||
pub fn run_json_tool<I, F>(tool_name: &str, input_json: &str, handler: F) -> String
|
||||
where
|
||||
I: DeserializeOwned,
|
||||
F: FnOnce(ToolContext, I) -> Result<ToolOutput, ToolError>,
|
||||
{
|
||||
let result = parse_json_input::<I>(input_json).and_then(|input| {
|
||||
let context = ToolContext::new(tool_name);
|
||||
handler(context, input)
|
||||
});
|
||||
match result {
|
||||
Ok(output) => output.to_json_string(),
|
||||
Err(error) => error.into_tool_output().to_json_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the generated Component Model `Guest` trait for a typed JSON
|
||||
/// handler and export it with the `wit-bindgen` generated `export!` macro.
|
||||
///
|
||||
/// The caller must import the PDK's `wit_bindgen` re-export and invoke
|
||||
/// `wit_bindgen::generate!` for the `tool` world first, with
|
||||
/// `runtime_path: "yoi_plugin_pdk::wit_bindgen::rt"`. That defines the
|
||||
/// `Guest` trait and `export!` macro in the current module. The generated
|
||||
/// component still imports only WIT-declared
|
||||
/// host APIs; this macro does not grant filesystem, network, or environment
|
||||
/// authority.
|
||||
#[macro_export]
|
||||
macro_rules! export_component_tool {
|
||||
($adapter:ident, $handler:path) => {
|
||||
struct $adapter;
|
||||
|
||||
impl Guest for $adapter {
|
||||
fn call(
|
||||
tool_name: ::std::string::String,
|
||||
input_json: ::std::string::String,
|
||||
) -> ::std::string::String {
|
||||
$crate::run_json_tool(&tool_name, &input_json, $handler)
|
||||
}
|
||||
}
|
||||
|
||||
export!($adapter);
|
||||
};
|
||||
}
|
||||
|
||||
fn normalize_summary(summary: String) -> String {
|
||||
let summary = bounded_text(summary, MAX_SUMMARY_BYTES);
|
||||
if summary.trim().is_empty() {
|
||||
"tool completed".to_string()
|
||||
} else {
|
||||
summary
|
||||
}
|
||||
}
|
||||
|
||||
fn bounded_text(mut value: String, max_bytes: usize) -> String {
|
||||
if max_bytes == 0 {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
value = value
|
||||
.chars()
|
||||
.map(|ch| {
|
||||
if ch.is_control() && ch != '\n' && ch != '\t' {
|
||||
' '
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if value.len() <= max_bytes {
|
||||
return value;
|
||||
}
|
||||
|
||||
let suffix = "…";
|
||||
let budget = max_bytes.saturating_sub(suffix.len());
|
||||
let mut cut = budget.min(value.len());
|
||||
while cut > 0 && !value.is_char_boundary(cut) {
|
||||
cut -= 1;
|
||||
}
|
||||
value.truncate(cut);
|
||||
value.push_str(suffix);
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct EchoInput {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct EchoOutput<'a> {
|
||||
tool: &'a str,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_json_tool_parses_input_and_serializes_output() {
|
||||
let output = run_json_tool(
|
||||
"example_echo",
|
||||
r#"{"text":"hello"}"#,
|
||||
|ctx, input: EchoInput| {
|
||||
ToolOutput::json(
|
||||
format!("{} ok", ctx.tool_name()),
|
||||
EchoOutput {
|
||||
tool: ctx.tool_name(),
|
||||
text: input.text,
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let value: Value = serde_json::from_str(&output).unwrap();
|
||||
assert_eq!(value["summary"], "example_echo ok");
|
||||
let content: Value = serde_json::from_str(value["content"].as_str().unwrap()).unwrap();
|
||||
assert_eq!(content, json!({"tool":"example_echo","text":"hello"}));
|
||||
assert!(output.len() <= MAX_TOOL_OUTPUT_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_json_tool_renders_error_output() {
|
||||
let output =
|
||||
run_json_tool::<EchoInput, _>("example_echo", r#"{"text":1}"#, |_ctx, _input| {
|
||||
Ok(ToolOutput::summary("unreachable"))
|
||||
});
|
||||
|
||||
let value: Value = serde_json::from_str(&output).unwrap();
|
||||
assert_eq!(value["summary"], "tool error: invalid_input");
|
||||
let content: Value = serde_json::from_str(value["content"].as_str().unwrap()).unwrap();
|
||||
assert_eq!(content["error"]["code"], "invalid_input");
|
||||
assert!(content["error"]["message"].as_str().unwrap().len() <= MAX_ERROR_MESSAGE_BYTES);
|
||||
assert!(output.len() <= MAX_TOOL_OUTPUT_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_output_rejects_oversized_serialized_tool_output() {
|
||||
let too_large = vec!["quoted \" text"; MAX_TOOL_OUTPUT_BYTES / 4];
|
||||
let error = ToolOutput::json("too large", too_large).unwrap_err();
|
||||
|
||||
assert_eq!(error.code(), ToolErrorCode::InvalidOutput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_error_bounds_message_and_control_characters() {
|
||||
let message = format!("bad\u{0007}{}", "x".repeat(MAX_ERROR_MESSAGE_BYTES * 2));
|
||||
let error = ToolError::failed(message);
|
||||
|
||||
assert_eq!(error.code_str(), "failed");
|
||||
assert!(!error.message().contains('\u{0007}'));
|
||||
assert!(error.message().len() <= MAX_ERROR_MESSAGE_BYTES + "…".len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wit_constants_match_current_world() {
|
||||
assert!(TOOL_WIT.contains("package yoi:plugin@1.0.0"));
|
||||
assert!(TOOL_WIT.contains("world tool"));
|
||||
assert!(TOOL_WIT.contains("export call"));
|
||||
assert_eq!(TOOL_WORLD, "yoi:plugin/tool@1.0.0");
|
||||
assert!(HOST_WIT.contains("interface https"));
|
||||
assert!(HOST_WIT.contains("interface fs"));
|
||||
assert!(HOST_WIT.contains("%list: func"));
|
||||
}
|
||||
}
|
||||
117
crates/plugin-pdk/tests/template.rs
Normal file
117
crates/plugin-pdk/tests/template.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
use toml::Value;
|
||||
|
||||
const TEMPLATE_CARGO: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-tool/Cargo.toml");
|
||||
const TEMPLATE_LIB: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-tool/src/lib.rs");
|
||||
const TEMPLATE_PLUGIN: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-tool/plugin.toml");
|
||||
const TEMPLATE_README: &str =
|
||||
include_str!("../../../resources/plugin/templates/rust-component-tool/README.md");
|
||||
const SAMPLE_LIB: &str = include_str!("../../../docs/examples/plugin-component-tool/lib.rs");
|
||||
const PDK_CARGO: &str = include_str!("../Cargo.toml");
|
||||
|
||||
#[test]
|
||||
fn rust_component_tool_template_has_expected_files() {
|
||||
let cargo: Value = toml::from_str(TEMPLATE_CARGO).expect("template Cargo.toml parses");
|
||||
assert_eq!(cargo["package"]["edition"].as_str(), Some("2024"));
|
||||
assert!(cargo["workspace"].is_table());
|
||||
assert_eq!(cargo["lib"]["crate-type"][0].as_str(), Some("cdylib"));
|
||||
assert_eq!(
|
||||
cargo["dependencies"]["yoi-plugin-pdk"]["path"].as_str(),
|
||||
Some("../../../../crates/plugin-pdk")
|
||||
);
|
||||
assert!(TEMPLATE_CARGO.contains("rev = \"<pinned-yoi-revision>\""));
|
||||
|
||||
let plugin: Value = toml::from_str(TEMPLATE_PLUGIN).expect("template plugin.toml parses");
|
||||
assert_eq!(plugin["schema_version"].as_integer(), Some(1));
|
||||
assert_eq!(plugin["runtime"]["kind"].as_str(), Some("wasm-component"));
|
||||
assert_eq!(
|
||||
plugin["runtime"]["world"].as_str(),
|
||||
Some("yoi:plugin/tool@1.0.0")
|
||||
);
|
||||
assert!(plugin["tools"].as_array().expect("tools array").len() == 1);
|
||||
|
||||
assert!(TEMPLATE_LIB.contains("use yoi_plugin_pdk::wit_bindgen"));
|
||||
assert!(TEMPLATE_LIB.contains("wit_bindgen::generate!"));
|
||||
assert!(TEMPLATE_LIB.contains("generate_all"));
|
||||
assert!(TEMPLATE_LIB.contains("runtime_path: \"yoi_plugin_pdk::wit_bindgen::rt\""));
|
||||
assert!(TEMPLATE_LIB.contains("yoi_plugin_pdk::export_component_tool!"));
|
||||
assert!(TEMPLATE_LIB.contains("ToolOutput::json"));
|
||||
assert!(TEMPLATE_README.contains("Component Model Tool Plugin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documented_sample_uses_pdk_component_path() {
|
||||
assert!(SAMPLE_LIB.contains("use yoi_plugin_pdk::wit_bindgen"));
|
||||
assert!(SAMPLE_LIB.contains("wit_bindgen::generate!"));
|
||||
assert!(SAMPLE_LIB.contains("generate_all"));
|
||||
assert!(SAMPLE_LIB.contains("runtime_path: \"yoi_plugin_pdk::wit_bindgen::rt\""));
|
||||
assert!(SAMPLE_LIB.contains("yoi_plugin_pdk::export_component_tool!"));
|
||||
assert!(!SAMPLE_LIB.contains("export_name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_template_cargo_checks_for_wasm_target() {
|
||||
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let template_dir = crate_dir.join("../../resources/plugin/templates/rust-component-tool");
|
||||
let manifest_path = template_dir.join("Cargo.toml");
|
||||
let lock_path = template_dir.join("Cargo.lock");
|
||||
let _ = fs::remove_file(&lock_path);
|
||||
|
||||
let target_dir = tempfile::tempdir().expect("temporary cargo target dir");
|
||||
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
|
||||
let output = Command::new(cargo)
|
||||
.arg("check")
|
||||
.arg("--manifest-path")
|
||||
.arg(&manifest_path)
|
||||
.arg("--target")
|
||||
.arg("wasm32-unknown-unknown")
|
||||
.arg("--offline")
|
||||
.arg("--target-dir")
|
||||
.arg(target_dir.path())
|
||||
.env("CARGO_TERM_COLOR", "never")
|
||||
.output()
|
||||
.expect("spawn cargo check for embedded template");
|
||||
|
||||
let _ = fs::remove_file(&lock_path);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"template cargo check failed\nstatus: {}\nstdout:\n{}\nstderr:\n{}",
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdk_runtime_dependencies_are_guest_side_only() {
|
||||
let cargo: Value = toml::from_str(PDK_CARGO).expect("PDK Cargo.toml parses");
|
||||
let dependencies = cargo["dependencies"]
|
||||
.as_table()
|
||||
.expect("dependencies table");
|
||||
let forbidden = [
|
||||
"pod",
|
||||
"yoi-pod",
|
||||
"llm-worker",
|
||||
"tui",
|
||||
"yoi-tui",
|
||||
"client",
|
||||
"yoi-client",
|
||||
"manifest",
|
||||
"yoi-manifest",
|
||||
"ticket",
|
||||
"yoi-ticket",
|
||||
];
|
||||
|
||||
for name in forbidden {
|
||||
assert!(
|
||||
!dependencies.contains_key(name),
|
||||
"PDK must not depend on host/runtime crate `{name}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
25
crates/plugin-pdk/tests/wit_bindgen_probe.rs
Normal file
25
crates/plugin-pdk/tests/wit_bindgen_probe.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
use yoi_plugin_pdk::wit_bindgen;
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "tool",
|
||||
path: "../../resources/plugin/wit",
|
||||
generate_all,
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
struct Probe;
|
||||
|
||||
impl Guest for Probe {
|
||||
fn call(tool_name: String, input_json: String) -> String {
|
||||
yoi_plugin_pdk::run_json_tool(&tool_name, &input_json, |_ctx, input: serde_json::Value| {
|
||||
yoi_plugin_pdk::ToolOutput::json("probe ok", input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wit_bindgen_generates_current_tool_world() {
|
||||
let output = <Probe as Guest>::call("probe".to_string(), r#"{"ok":true}"#.to_string());
|
||||
let value: serde_json::Value = serde_json::from_str(&output).unwrap();
|
||||
assert_eq!(value["summary"], "probe ok");
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ dotenv = "0.15.0"
|
|||
futures = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
wat = "1.241.2"
|
||||
yoi-plugin-pdk = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
toml = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -3868,6 +3868,18 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdk_tool_output_shape_is_accepted_by_wasm_decoder() {
|
||||
let pdk_output =
|
||||
yoi_plugin_pdk::ToolOutput::json("pdk ok", serde_json::json!({"answer": 42}))
|
||||
.unwrap()
|
||||
.to_json_string();
|
||||
|
||||
let output = decode_plugin_wasm_output(pdk_output.as_bytes()).unwrap();
|
||||
assert_eq!(output.summary, "pdk ok");
|
||||
assert_eq!(output.content.as_deref(), Some(r#"{"answer":42}"#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_input_json_fails_before_wasm_execution() {
|
||||
let (_dir, record) = resolved_record_with_wasm(input_reaches_guest_module());
|
||||
|
|
|
|||
|
|
@ -776,6 +776,7 @@ fn manifest_to_reusable_config(manifest: &PodManifest) -> PodManifestConfig {
|
|||
}),
|
||||
feature: manifest.feature.clone().into(),
|
||||
plugins: manifest.plugins.clone(),
|
||||
mcp: manifest.mcp.clone(),
|
||||
compaction: manifest
|
||||
.compaction
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -1650,11 +1650,15 @@ impl TicketBackend for LocalTicketBackend {
|
|||
let item = dir.join("item.md");
|
||||
let meta = ticket_meta_for_dir(&dir, read_item_file(&item)?.frontmatter)?;
|
||||
let blockers = self.relation_blockers_for_meta(&meta)?;
|
||||
if !blockers.is_empty() {
|
||||
let active_blockers = blockers
|
||||
.into_iter()
|
||||
.filter(|blocker| !relation_blocker_allows_queue(blocker))
|
||||
.collect::<Vec<_>>();
|
||||
if !active_blockers.is_empty() {
|
||||
return Err(TicketError::Conflict(format!(
|
||||
"ticket {} has unresolved blocking relation(s): {}",
|
||||
meta.id,
|
||||
format_relation_blockers(&blockers)
|
||||
format_relation_blockers(&active_blockers)
|
||||
)));
|
||||
}
|
||||
let at = now_utc();
|
||||
|
|
@ -2550,6 +2554,13 @@ fn relation_view_from_records(
|
|||
view
|
||||
}
|
||||
|
||||
fn relation_blocker_allows_queue(blocker: &TicketRelationBlocker) -> bool {
|
||||
matches!(
|
||||
blocker.blocking_state,
|
||||
TicketWorkflowState::Queued | TicketWorkflowState::InProgress
|
||||
)
|
||||
}
|
||||
|
||||
fn format_relation_blockers(blockers: &[TicketRelationBlocker]) -> String {
|
||||
blockers
|
||||
.iter()
|
||||
|
|
@ -4408,6 +4419,65 @@ state: planning
|
|||
assert_eq!(backend.doctor().unwrap().error_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_gate_allows_ready_ticket_when_blocking_relation_is_already_queued_or_inprogress() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let backend = backend(&tmp);
|
||||
let mut waiter_input = NewTicket::new("Ready After Queued Dependency");
|
||||
waiter_input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
let waiter = backend.create(waiter_input).unwrap();
|
||||
let mut dependency_input = NewTicket::new("Queued Dependency");
|
||||
dependency_input.workflow_state = Some(TicketWorkflowState::Queued);
|
||||
let dependency = backend.create(dependency_input).unwrap();
|
||||
backend
|
||||
.add_ticket_relation(
|
||||
TicketIdOrSlug::Id(waiter.id.clone()),
|
||||
NewTicketRelation {
|
||||
kind: TicketRelationKind::DependsOn,
|
||||
target: dependency.id.clone(),
|
||||
note: None,
|
||||
author: Some("test".to_string()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
backend
|
||||
.queue_ready(TicketIdOrSlug::Id(waiter.id.clone()), "test")
|
||||
.unwrap();
|
||||
let queued = backend.show(TicketIdOrSlug::Id(waiter.id.clone())).unwrap();
|
||||
assert_eq!(queued.meta.workflow_state, TicketWorkflowState::Queued);
|
||||
assert_eq!(queued.meta.queued_by.as_deref(), Some("test"));
|
||||
|
||||
let mut incoming_input = NewTicket::new("Ready After Inprogress Blocker");
|
||||
incoming_input.workflow_state = Some(TicketWorkflowState::Ready);
|
||||
let incoming = backend.create(incoming_input).unwrap();
|
||||
let mut blocker_input = NewTicket::new("Inprogress Blocker");
|
||||
blocker_input.workflow_state = Some(TicketWorkflowState::InProgress);
|
||||
let blocker = backend.create(blocker_input).unwrap();
|
||||
backend
|
||||
.add_ticket_relation(
|
||||
TicketIdOrSlug::Id(blocker.id.clone()),
|
||||
NewTicketRelation {
|
||||
kind: TicketRelationKind::Blocks,
|
||||
target: incoming.id.clone(),
|
||||
note: None,
|
||||
author: Some("test".to_string()),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
backend
|
||||
.queue_ready(TicketIdOrSlug::Id(incoming.id.clone()), "test")
|
||||
.unwrap();
|
||||
let queued_incoming = backend
|
||||
.show(TicketIdOrSlug::Id(incoming.id.clone()))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
queued_incoming.meta.workflow_state,
|
||||
TicketWorkflowState::Queued
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_gate_rejects_unresolved_dependency_and_incoming_blocker() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -429,10 +429,58 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
|||
fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, ParseError> {
|
||||
let Some((subcommand, rest)) = args.split_first() else {
|
||||
return Err(ParseError(
|
||||
"yoi plugin requires `list` or `show <ref>`".to_string(),
|
||||
"yoi plugin requires `new`, `check`, `pack`, `list`, or `show <ref>`".to_string(),
|
||||
));
|
||||
};
|
||||
match subcommand.as_str() {
|
||||
"new" => {
|
||||
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
|
||||
match positional.as_slice() {
|
||||
[template, destination] => Ok(plugin_cli::PluginCliCommand::New {
|
||||
template: template.clone(),
|
||||
destination: PathBuf::from(destination),
|
||||
args: plugin_args,
|
||||
}),
|
||||
[] | [_] => Err(ParseError(
|
||||
"yoi plugin new requires a template and destination".to_string(),
|
||||
)),
|
||||
_ => Err(ParseError(
|
||||
"yoi plugin new accepts exactly a template and destination".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"check" => {
|
||||
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
|
||||
match positional.as_slice() {
|
||||
[input] => Ok(plugin_cli::PluginCliCommand::Check {
|
||||
input: PathBuf::from(input),
|
||||
args: plugin_args,
|
||||
}),
|
||||
[] => Err(ParseError(
|
||||
"yoi plugin check requires a plugin directory or .yoi-plugin path".to_string(),
|
||||
)),
|
||||
_ => Err(ParseError(
|
||||
"yoi plugin check accepts exactly one plugin directory or .yoi-plugin path"
|
||||
.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"pack" => {
|
||||
let (plugin_args, positional, output) = parse_plugin_pack_args(rest)?;
|
||||
match positional.as_slice() {
|
||||
[input] => Ok(plugin_cli::PluginCliCommand::Pack {
|
||||
input: PathBuf::from(input),
|
||||
output,
|
||||
args: plugin_args,
|
||||
}),
|
||||
[] => Err(ParseError(
|
||||
"yoi plugin pack requires a plugin directory".to_string(),
|
||||
)),
|
||||
_ => Err(ParseError(
|
||||
"yoi plugin pack accepts exactly one plugin directory".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"list" => {
|
||||
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
|
||||
if !positional.is_empty() {
|
||||
|
|
@ -513,8 +561,36 @@ fn parse_plugin_common_args(
|
|||
Ok((parsed, positional))
|
||||
}
|
||||
|
||||
fn parse_plugin_pack_args(
|
||||
args: &[String],
|
||||
) -> Result<(plugin_cli::PluginCliArgs, Vec<String>, Option<PathBuf>), ParseError> {
|
||||
let mut normalized = Vec::new();
|
||||
let mut output = None;
|
||||
let mut index = 0;
|
||||
while index < args.len() {
|
||||
let arg = &args[index];
|
||||
if arg == "--output" {
|
||||
index += 1;
|
||||
let Some(value) = args.get(index) else {
|
||||
return Err(ParseError("--output requires a value".to_string()));
|
||||
};
|
||||
output = Some(PathBuf::from(value));
|
||||
} else if let Some(value) = arg.strip_prefix("--output=") {
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--output requires a value".to_string()));
|
||||
}
|
||||
output = Some(PathBuf::from(value));
|
||||
} else {
|
||||
normalized.push(arg.clone());
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
let (plugin_args, positional) = parse_plugin_common_args(&normalized)?;
|
||||
Ok((plugin_args, positional, output))
|
||||
}
|
||||
|
||||
fn plugin_usage() -> &'static str {
|
||||
"usage: 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 <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_panel_workspace(args: &[String]) -> Result<PathBuf, ParseError> {
|
||||
|
|
@ -547,7 +623,7 @@ fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
|
|||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"yoi\n\nUsage:\n yoi [OPTIONS] [POD_NAME]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi ticket <COMMAND> [OPTIONS]\n yoi plugin list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi plugin show <REF> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nOptions:\n -r, --resume Open the Pod picker and resume/attach a Pod\n --workspace <PATH> Runtime workspace root (defaults to cwd)\n --pod <NAME> Attach/restore/create a Pod by name\n --socket <PATH> Attach to a specific Pod socket with --pod\n --session <UUID> Resume a specific session segment\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
|
||||
"yoi\n\nUsage:\n yoi [OPTIONS] [POD_NAME]\n yoi panel [--workspace <PATH>]\n yoi keys\n yoi setup-model\n yoi pod [POD_OPTIONS]\n yoi objective <COMMAND> [OPTIONS]\n yoi session analyze <SESSION_JSONL_PATH> --json\n yoi ticket <COMMAND> [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 memory lint [OPTIONS]\n\nOptions:\n -r, --resume Open the Pod picker and resume/attach a Pod\n --workspace <PATH> Runtime workspace root (defaults to cwd)\n --pod <NAME> Attach/restore/create a Pod by name\n --socket <PATH> Attach to a specific Pod socket with --pod\n --session <UUID> Resume a specific session segment\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,17 @@ use std::collections::BTreeMap;
|
|||
use std::error::Error;
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use manifest::plugin::{
|
||||
PluginConfig, PluginDiagnostic, PluginDiagnosticKind, PluginDiscoveryLimits,
|
||||
PluginDiscoveryOptions, PluginDiscoveryReport, PluginPackageManifest, PluginPermission,
|
||||
PluginResolution, PluginSourceKind, PluginSurface, ResolvedPlugin, ResolvedPluginRecord,
|
||||
SourceQualifiedPluginId, discover_plugins, resolve_enabled_plugins,
|
||||
MaterializedPluginPackage, PluginConfig, PluginDiagnostic, PluginDiagnosticKind,
|
||||
PluginDiagnosticPhase, PluginDiscoveryLimits, PluginDiscoveryOptions, PluginDiscoveryReport,
|
||||
PluginExactVersion, PluginGrantConfig, PluginPackageManifest, PluginPermission,
|
||||
PluginResolution, PluginSourceKind, PluginSurface, RUST_COMPONENT_TOOL_TEMPLATE,
|
||||
ResolvedPlugin, ResolvedPluginRecord, SourceQualifiedPluginId, discover_plugins,
|
||||
read_plugin_directory, read_plugin_package_file, resolve_enabled_plugins,
|
||||
write_plugin_package_file,
|
||||
};
|
||||
use manifest::{ProfileResolveOptions, ProfileResolver, ProfileSelector, paths};
|
||||
use pod::feature::plugin::{PluginStaticInspection, inspect_resolved_plugin_static};
|
||||
|
|
@ -35,17 +39,607 @@ pub(crate) enum PluginCliCommand {
|
|||
reference: String,
|
||||
args: PluginCliArgs,
|
||||
},
|
||||
New {
|
||||
template: String,
|
||||
destination: PathBuf,
|
||||
args: PluginCliArgs,
|
||||
},
|
||||
Check {
|
||||
input: PathBuf,
|
||||
args: PluginCliArgs,
|
||||
},
|
||||
Pack {
|
||||
input: PathBuf,
|
||||
output: Option<PathBuf>,
|
||||
args: PluginCliArgs,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) fn run(command: PluginCliCommand) -> Result<()> {
|
||||
if let PluginCliCommand::Check { input, args } = command {
|
||||
let report = build_check_report(&input);
|
||||
let rendered = render_check_report(&report, &args)?;
|
||||
print!("{rendered}");
|
||||
if report.status == "rejected" {
|
||||
return Err("plugin check failed; see diagnostics above".into());
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let rendered = match command {
|
||||
PluginCliCommand::List(args) => render_list(&args)?,
|
||||
PluginCliCommand::Show { reference, args } => render_show(&reference, &args)?,
|
||||
PluginCliCommand::New {
|
||||
template,
|
||||
destination,
|
||||
args,
|
||||
} => render_new(&template, &destination, &args)?,
|
||||
PluginCliCommand::Check { .. } => unreachable!("handled above"),
|
||||
PluginCliCommand::Pack {
|
||||
input,
|
||||
output,
|
||||
args,
|
||||
} => render_pack(&input, output.as_deref(), &args)?,
|
||||
};
|
||||
print!("{rendered}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_new(template: &str, destination: &Path, args: &PluginCliArgs) -> Result<String> {
|
||||
if template != "rust-component-tool" {
|
||||
return Err(format!(
|
||||
"unsupported plugin template `{template}` (supported: rust-component-tool)"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
materialize_template(destination)?;
|
||||
let report = NewReport {
|
||||
command: "new",
|
||||
template: "rust-component-tool",
|
||||
destination: destination.display().to_string(),
|
||||
files: RUST_COMPONENT_TOOL_TEMPLATE
|
||||
.iter()
|
||||
.map(|resource| resource.path.to_string())
|
||||
.collect(),
|
||||
safety: AuthoringSafetyReport::default(),
|
||||
next_steps: vec![
|
||||
"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 {
|
||||
return Ok(format!("{}\n", serde_json::to_string_pretty(&report)?));
|
||||
}
|
||||
render_new_human(&report)
|
||||
}
|
||||
|
||||
fn materialize_template(destination: &Path) -> Result<()> {
|
||||
match fs::symlink_metadata(destination) {
|
||||
Ok(metadata) => {
|
||||
if metadata.file_type().is_symlink() {
|
||||
return Err(format!(
|
||||
"plugin template destination `{}` is a symlink; refusing to follow it",
|
||||
destination.display()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if !metadata.is_dir() {
|
||||
return Err(format!(
|
||||
"plugin template destination `{}` already exists and is not a directory",
|
||||
destination.display()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
if fs::read_dir(destination)?.next().is_some() {
|
||||
return Err(format!(
|
||||
"plugin template destination `{}` is not empty",
|
||||
destination.display()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {
|
||||
fs::create_dir_all(destination)?;
|
||||
}
|
||||
Err(error) => return Err(error.into()),
|
||||
}
|
||||
|
||||
for resource in RUST_COMPONENT_TOOL_TEMPLATE {
|
||||
let relative = safe_template_relative_path(resource.path)?;
|
||||
let path = destination.join(relative);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::write(path, resource.contents)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn safe_template_relative_path(path: &str) -> Result<&Path> {
|
||||
let relative = Path::new(path);
|
||||
if relative.is_absolute()
|
||||
|| relative
|
||||
.components()
|
||||
.any(|component| !matches!(component, std::path::Component::Normal(_)))
|
||||
{
|
||||
return Err(format!("embedded plugin template path `{path}` is unsafe").into());
|
||||
}
|
||||
Ok(relative)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn render_check(input: &Path, args: &PluginCliArgs) -> Result<String> {
|
||||
let report = build_check_report(input);
|
||||
render_check_report(&report, args)
|
||||
}
|
||||
|
||||
fn render_check_report(report: &CheckReport, args: &PluginCliArgs) -> Result<String> {
|
||||
if args.json {
|
||||
return Ok(format!("{}\n", serde_json::to_string_pretty(report)?));
|
||||
}
|
||||
render_check_human(report)
|
||||
}
|
||||
|
||||
fn render_pack(input: &Path, output: Option<&Path>, args: &PluginCliArgs) -> Result<String> {
|
||||
let limits = PluginDiscoveryLimits::default();
|
||||
let materialized = read_plugin_directory(input, PluginSourceKind::Project, &limits)
|
||||
.map_err(|diagnostic| plugin_diagnostic_error("plugin pack", diagnostic))?;
|
||||
let output_path = output
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or_else(|| default_package_output_path(input));
|
||||
let packed = write_plugin_package_file(&materialized, &output_path, &limits)
|
||||
.map_err(|diagnostic| plugin_diagnostic_error("plugin pack", diagnostic))?;
|
||||
let report = PackReport {
|
||||
command: "pack",
|
||||
status: "packed",
|
||||
input_path: input.display().to_string(),
|
||||
output_path: packed.output_path.display().to_string(),
|
||||
package: PackageReport::from_materialized(&MaterializedPluginPackage {
|
||||
package: packed.package,
|
||||
files: materialized.files,
|
||||
}),
|
||||
safety: AuthoringSafetyReport::default(),
|
||||
};
|
||||
if args.json {
|
||||
return Ok(format!("{}\n", serde_json::to_string_pretty(&report)?));
|
||||
}
|
||||
render_pack_human(&report)
|
||||
}
|
||||
|
||||
fn default_package_output_path(input: &Path) -> PathBuf {
|
||||
let name = input
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or("plugin");
|
||||
input.with_file_name(format!("{name}.yoi-plugin"))
|
||||
}
|
||||
|
||||
fn build_check_report(input: &Path) -> CheckReport {
|
||||
let limits = PluginDiscoveryLimits::default();
|
||||
let input_kind = if input.is_dir() {
|
||||
"directory"
|
||||
} else {
|
||||
"package"
|
||||
};
|
||||
let result = if input.is_dir() {
|
||||
read_plugin_directory(input, PluginSourceKind::Project, &limits)
|
||||
} else {
|
||||
read_plugin_package_file(input, PluginSourceKind::Project, &limits)
|
||||
};
|
||||
match result {
|
||||
Ok(materialized) => {
|
||||
let static_inspection = inspect_materialized_package(&materialized);
|
||||
let static_diagnostics = static_inspection_diagnostics(&static_inspection);
|
||||
let placeholder_diagnostic = placeholder_component_diagnostic(&materialized);
|
||||
let status = if !static_diagnostics.is_empty() {
|
||||
"rejected"
|
||||
} else if placeholder_diagnostic.is_some() {
|
||||
"partial"
|
||||
} else {
|
||||
"active"
|
||||
};
|
||||
let mut diagnostics = static_diagnostics;
|
||||
diagnostics.extend(placeholder_diagnostic);
|
||||
let reference = package_reference(&materialized.package.identity);
|
||||
CheckReport {
|
||||
command: "check",
|
||||
status,
|
||||
input_path: input.display().to_string(),
|
||||
input_kind,
|
||||
package: Some(PackageReport::from_materialized(&materialized)),
|
||||
diagnostics,
|
||||
static_inspection: Some(StaticInspectionReport::from_inspection(
|
||||
&static_inspection,
|
||||
)),
|
||||
safety: AuthoringSafetyReport::default(),
|
||||
next_steps: check_next_steps(status, &reference),
|
||||
}
|
||||
}
|
||||
Err(diagnostic) => CheckReport {
|
||||
command: "check",
|
||||
status: "rejected",
|
||||
input_path: input.display().to_string(),
|
||||
input_kind,
|
||||
package: None,
|
||||
diagnostics: vec![PluginDiagnosticReport::from_diagnostic(&diagnostic)],
|
||||
static_inspection: None,
|
||||
safety: AuthoringSafetyReport::default(),
|
||||
next_steps: vec![
|
||||
"Fix the reported package diagnostic and run `yoi plugin check` again.".to_string(),
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_materialized_package(
|
||||
materialized: &MaterializedPluginPackage,
|
||||
) -> PluginStaticInspection {
|
||||
let requested_permissions = materialized.package.manifest.permissions.clone();
|
||||
let record = ResolvedPluginRecord {
|
||||
identity: materialized.package.identity.clone(),
|
||||
source: materialized.package.identity.source,
|
||||
package_path: materialized.package.package_path.clone(),
|
||||
package_label: materialized.package.package_label.clone(),
|
||||
digest: materialized.package.digest.clone(),
|
||||
version: materialized.package.manifest.version.clone(),
|
||||
manifest: materialized.package.manifest.clone(),
|
||||
enabled_surfaces: materialized.package.manifest.surfaces.clone(),
|
||||
grants: PluginGrantConfig {
|
||||
id: Some(materialized.package.identity.to_string()),
|
||||
version: Some(PluginExactVersion(
|
||||
materialized.package.manifest.version.clone(),
|
||||
)),
|
||||
digest: Some(materialized.package.digest.clone()),
|
||||
permissions: requested_permissions,
|
||||
https: Vec::new(),
|
||||
fs: Vec::new(),
|
||||
},
|
||||
config: None,
|
||||
};
|
||||
inspect_resolved_plugin_static(&record)
|
||||
}
|
||||
|
||||
fn static_inspection_diagnostics(
|
||||
inspection: &PluginStaticInspection,
|
||||
) -> Vec<PluginDiagnosticReport> {
|
||||
let mut diagnostics = Vec::new();
|
||||
if let Some(message) = &inspection.runtime.diagnostic {
|
||||
diagnostics.push(PluginDiagnosticReport {
|
||||
kind: "malformed".to_string(),
|
||||
phase: "resolution".to_string(),
|
||||
message: bound_text(message.clone()),
|
||||
});
|
||||
}
|
||||
for api in &inspection.host_apis {
|
||||
if let Some(message) = &api.diagnostic {
|
||||
diagnostics.push(PluginDiagnosticReport {
|
||||
kind: "grant".to_string(),
|
||||
phase: "resolution".to_string(),
|
||||
message: bound_text(message.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
for tool in &inspection.tools {
|
||||
if let Some(message) = &tool.diagnostic {
|
||||
diagnostics.push(PluginDiagnosticReport {
|
||||
kind: "malformed".to_string(),
|
||||
phase: "resolution".to_string(),
|
||||
message: bound_text(message.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
diagnostics
|
||||
}
|
||||
|
||||
fn placeholder_component_diagnostic(
|
||||
materialized: &MaterializedPluginPackage,
|
||||
) -> Option<PluginDiagnosticReport> {
|
||||
let runtime = materialized.package.manifest.runtime.as_ref()?;
|
||||
let component = runtime.component.as_deref()?;
|
||||
let component_bytes = materialized.files.get(component)?;
|
||||
let placeholder_bytes = RUST_COMPONENT_TOOL_TEMPLATE
|
||||
.iter()
|
||||
.find(|resource| resource.path == "plugin.component.wasm")?
|
||||
.contents
|
||||
.as_bytes();
|
||||
if component_bytes != placeholder_bytes {
|
||||
return None;
|
||||
}
|
||||
Some(PluginDiagnosticReport {
|
||||
kind: "placeholder".to_string(),
|
||||
phase: "runtime".to_string(),
|
||||
message: format!(
|
||||
"plugin component runtime artifact `{component}` is the generated placeholder; replace it with a real built component before enabling or execution"
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn check_next_steps(status: &str, reference: &str) -> Vec<String> {
|
||||
match status {
|
||||
"active" => vec![
|
||||
"Package metadata is valid without executing Plugin code.".to_string(),
|
||||
format!(
|
||||
"To enable after review, add an explicit plugin enablement entry for `{reference}` with matching digest and grants."
|
||||
),
|
||||
"Run `yoi plugin pack <path>` to create a deterministic .yoi-plugin archive."
|
||||
.to_string(),
|
||||
],
|
||||
"partial" => vec![
|
||||
"Replace the generated placeholder component artifact with a real built component."
|
||||
.to_string(),
|
||||
"Run `yoi plugin check <path>` again before enabling or execution.".to_string(),
|
||||
"Do not enable this Plugin while check status is partial.".to_string(),
|
||||
],
|
||||
_ => {
|
||||
vec!["Fix the reported diagnostics before enabling or packing this Plugin.".to_string()]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_diagnostic_error(context: &str, diagnostic: PluginDiagnostic) -> String {
|
||||
format!("{context} failed: {}", diagnostic.message)
|
||||
}
|
||||
|
||||
fn render_new_human(report: &NewReport) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
writeln!(
|
||||
out,
|
||||
"created plugin template `{}` at {}",
|
||||
report.template, report.destination
|
||||
)?;
|
||||
writeln!(out, "files:")?;
|
||||
for file in &report.files {
|
||||
writeln!(out, " - {file}")?;
|
||||
}
|
||||
writeln!(
|
||||
out,
|
||||
"safety: no network; embedded template only; no secrets generated"
|
||||
)?;
|
||||
writeln!(out, "next steps:")?;
|
||||
for step in &report.next_steps {
|
||||
writeln!(out, " - {step}")?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn render_check_human(report: &CheckReport) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
writeln!(
|
||||
out,
|
||||
"plugin check: {} [{}] input_kind={}",
|
||||
report.input_path, report.status, report.input_kind
|
||||
)?;
|
||||
if let Some(package) = &report.package {
|
||||
writeln!(
|
||||
out,
|
||||
"package: {} version={} digest={} entries={} source={} surfaces={} tools={}",
|
||||
package.reference,
|
||||
package.version,
|
||||
package.digest,
|
||||
package.entries.len(),
|
||||
package.source,
|
||||
join_or_none(&package.surfaces),
|
||||
package.tools.len()
|
||||
)?;
|
||||
match report.status {
|
||||
"active" => writeln!(
|
||||
out,
|
||||
"enablement guidance: pin reference `{}` and digest `{}` explicitly; this command does not mutate config",
|
||||
package.reference, package.digest
|
||||
)?,
|
||||
"partial" => writeln!(
|
||||
out,
|
||||
"enablement guidance: not ready to enable; replace the generated placeholder component and rerun check; this command does not mutate config"
|
||||
)?,
|
||||
_ => writeln!(
|
||||
out,
|
||||
"enablement guidance: not ready to enable; fix diagnostics first; this command does not mutate config"
|
||||
)?,
|
||||
}
|
||||
}
|
||||
if report.diagnostics.is_empty() {
|
||||
writeln!(out, "diagnostics: none")?;
|
||||
} else {
|
||||
writeln!(out, "diagnostics:")?;
|
||||
for diagnostic in &report.diagnostics {
|
||||
writeln!(
|
||||
out,
|
||||
" - kind={} phase={} message={}",
|
||||
diagnostic.kind, diagnostic.phase, diagnostic.message
|
||||
)?;
|
||||
}
|
||||
}
|
||||
writeln!(
|
||||
out,
|
||||
"safety: no Plugin execution; no enablement config mutation; no secrets generated"
|
||||
)?;
|
||||
writeln!(out, "next steps:")?;
|
||||
for step in &report.next_steps {
|
||||
writeln!(out, " - {step}")?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn render_pack_human(report: &PackReport) -> Result<String> {
|
||||
let mut out = String::new();
|
||||
writeln!(
|
||||
out,
|
||||
"plugin pack: {} [{}]",
|
||||
report.output_path, report.status
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
"package: {} version={} digest={} entries={}",
|
||||
report.package.reference,
|
||||
report.package.version,
|
||||
report.package.digest,
|
||||
report.package.entries.len()
|
||||
)?;
|
||||
writeln!(
|
||||
out,
|
||||
"safety: deterministic stored .yoi-plugin archive; no Plugin execution; no config mutation"
|
||||
)?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AuthoringSafetyReport {
|
||||
no_network: bool,
|
||||
no_plugin_execution: bool,
|
||||
no_enablement_config_mutation: bool,
|
||||
no_secrets_generated: bool,
|
||||
}
|
||||
|
||||
impl Default for AuthoringSafetyReport {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
no_network: true,
|
||||
no_plugin_execution: true,
|
||||
no_enablement_config_mutation: true,
|
||||
no_secrets_generated: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NewReport {
|
||||
command: &'static str,
|
||||
template: &'static str,
|
||||
destination: String,
|
||||
files: Vec<String>,
|
||||
safety: AuthoringSafetyReport,
|
||||
next_steps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CheckReport {
|
||||
command: &'static str,
|
||||
status: &'static str,
|
||||
input_path: String,
|
||||
input_kind: &'static str,
|
||||
package: Option<PackageReport>,
|
||||
diagnostics: Vec<PluginDiagnosticReport>,
|
||||
static_inspection: Option<StaticInspectionReport>,
|
||||
safety: AuthoringSafetyReport,
|
||||
next_steps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PackReport {
|
||||
command: &'static str,
|
||||
status: &'static str,
|
||||
input_path: String,
|
||||
output_path: String,
|
||||
package: PackageReport,
|
||||
safety: AuthoringSafetyReport,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PackageReport {
|
||||
reference: String,
|
||||
package: String,
|
||||
source: String,
|
||||
version: String,
|
||||
schema_version: u32,
|
||||
digest: String,
|
||||
package_path: String,
|
||||
entries: Vec<String>,
|
||||
surfaces: Vec<String>,
|
||||
tools: Vec<String>,
|
||||
permissions: Vec<String>,
|
||||
}
|
||||
|
||||
impl PackageReport {
|
||||
fn from_materialized(materialized: &MaterializedPluginPackage) -> Self {
|
||||
Self {
|
||||
reference: package_reference(&materialized.package.identity),
|
||||
package: materialized.package.manifest.id.clone(),
|
||||
source: materialized.package.identity.source.to_string(),
|
||||
version: materialized.package.manifest.version.clone(),
|
||||
schema_version: materialized.package.manifest.schema_version,
|
||||
digest: materialized.package.digest.clone(),
|
||||
package_path: materialized.package.package_path.display().to_string(),
|
||||
entries: materialized.package.entries.iter().cloned().collect(),
|
||||
surfaces: materialized
|
||||
.package
|
||||
.manifest
|
||||
.surfaces
|
||||
.iter()
|
||||
.map(ToString::to_string)
|
||||
.collect(),
|
||||
tools: materialized
|
||||
.package
|
||||
.manifest
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect(),
|
||||
permissions: materialized
|
||||
.package
|
||||
.manifest
|
||||
.permissions
|
||||
.iter()
|
||||
.map(|permission| permission_name(permission.clone()).to_string())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PluginDiagnosticReport {
|
||||
kind: String,
|
||||
phase: String,
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl PluginDiagnosticReport {
|
||||
fn from_diagnostic(diagnostic: &PluginDiagnostic) -> Self {
|
||||
Self {
|
||||
kind: diagnostic_kind(&diagnostic.kind).to_string(),
|
||||
phase: diagnostic_phase(&diagnostic.phase).to_string(),
|
||||
message: bound_text(diagnostic.message.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn diagnostic_phase(phase: &PluginDiagnosticPhase) -> &'static str {
|
||||
match phase {
|
||||
PluginDiagnosticPhase::Discovery => "discovery",
|
||||
PluginDiagnosticPhase::Manifest => "manifest",
|
||||
PluginDiagnosticPhase::Resolution => "resolution",
|
||||
}
|
||||
}
|
||||
|
||||
fn package_reference(identity: &SourceQualifiedPluginId) -> String {
|
||||
identity.to_string()
|
||||
}
|
||||
|
||||
fn permission_name(permission: PluginPermission) -> String {
|
||||
permission.label()
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct StaticInspectionReport {
|
||||
status: String,
|
||||
diagnostics: usize,
|
||||
}
|
||||
|
||||
impl StaticInspectionReport {
|
||||
fn from_inspection(inspection: &PluginStaticInspection) -> Self {
|
||||
let diagnostics = static_inspection_diagnostics(inspection).len();
|
||||
let status = if diagnostics == 0 {
|
||||
"active"
|
||||
} else {
|
||||
"rejected"
|
||||
};
|
||||
Self {
|
||||
status: status.to_string(),
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_list(args: &PluginCliArgs) -> Result<String> {
|
||||
let snapshot = build_snapshot(args)?;
|
||||
if args.json {
|
||||
|
|
@ -1254,6 +1848,222 @@ mod tests {
|
|||
assert!(show_output.contains("eligible=false"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_new_creates_template_files_and_refuses_non_empty_destination() {
|
||||
let dir = tempdir().unwrap();
|
||||
let destination = dir.path().join("my-plugin");
|
||||
|
||||
let output = render_new(
|
||||
"rust-component-tool",
|
||||
&destination,
|
||||
&PluginCliArgs::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(output.contains("created plugin template"));
|
||||
for resource in RUST_COMPONENT_TOOL_TEMPLATE {
|
||||
assert!(
|
||||
destination.join(resource.path).is_file(),
|
||||
"missing {}",
|
||||
resource.path
|
||||
);
|
||||
}
|
||||
let check_json = render_check(
|
||||
&destination,
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let check_value: serde_json::Value = serde_json::from_str(&check_json).unwrap();
|
||||
assert_eq!(check_value["status"], "partial");
|
||||
assert_eq!(check_value["diagnostics"][0]["kind"], "placeholder");
|
||||
assert!(
|
||||
check_value["diagnostics"][0]["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("generated placeholder")
|
||||
);
|
||||
assert!(
|
||||
check_value["next_steps"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|step| step.as_str().unwrap_or_default().contains("Do not enable"))
|
||||
);
|
||||
let human_check = render_check(&destination, &PluginCliArgs::default()).unwrap();
|
||||
assert!(human_check.contains("[partial]"));
|
||||
assert!(human_check.contains("not ready to enable"));
|
||||
let error = render_new(
|
||||
"rust-component-tool",
|
||||
&destination,
|
||||
&PluginCliArgs::default(),
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains("not empty"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn plugin_new_refuses_symlink_destination_without_following_it() {
|
||||
let dir = tempdir().unwrap();
|
||||
let target = dir.path().join("target");
|
||||
fs::create_dir_all(&target).unwrap();
|
||||
let link = dir.path().join("linkdest");
|
||||
std::os::unix::fs::symlink(&target, &link).unwrap();
|
||||
|
||||
let error = render_new("rust-component-tool", &link, &PluginCliArgs::default())
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(error.contains("symlink"));
|
||||
assert!(!target.join("plugin.toml").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_check_accepts_valid_directory_and_reports_json_shape() {
|
||||
let dir = tempdir().unwrap();
|
||||
let plugin = dir.path().join("plugin");
|
||||
fs::create_dir_all(&plugin).unwrap();
|
||||
fs::write(
|
||||
plugin.join("plugin.toml"),
|
||||
plugin_manifest("echo", "echo", "object", &["echo"]),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
|
||||
|
||||
let human = render_check(&plugin, &PluginCliArgs::default()).unwrap();
|
||||
assert!(human.contains("[active]"));
|
||||
assert!(human.contains("digest="));
|
||||
assert!(human.contains("does not mutate config"));
|
||||
|
||||
let json = render_check(
|
||||
&plugin,
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(value["command"], "check");
|
||||
assert_eq!(value["status"], "active");
|
||||
assert_eq!(value["input_kind"], "directory");
|
||||
assert_eq!(value["package"]["reference"], "project:echo");
|
||||
assert_eq!(value["safety"]["no_plugin_execution"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_check_rejects_invalid_manifest_and_missing_runtime_artifact() {
|
||||
let dir = tempdir().unwrap();
|
||||
let invalid = dir.path().join("invalid");
|
||||
fs::create_dir_all(&invalid).unwrap();
|
||||
fs::write(
|
||||
invalid.join("plugin.toml"),
|
||||
"schema_version = 1\nid = [\"bad\"]\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let invalid_json = render_check(
|
||||
&invalid,
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let invalid_value: serde_json::Value = serde_json::from_str(&invalid_json).unwrap();
|
||||
assert_eq!(invalid_value["status"], "rejected");
|
||||
assert_eq!(invalid_value["diagnostics"][0]["phase"], "manifest");
|
||||
|
||||
let missing = dir.path().join("missing-runtime");
|
||||
fs::create_dir_all(&missing).unwrap();
|
||||
fs::write(
|
||||
missing.join("plugin.toml"),
|
||||
plugin_manifest_missing_runtime_entry("missing_runtime"),
|
||||
)
|
||||
.unwrap();
|
||||
let missing_output = render_check(&missing, &PluginCliArgs::default()).unwrap();
|
||||
assert!(missing_output.contains("rejected"));
|
||||
assert!(missing_output.contains("path not present"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_check_rejects_unsafe_package_archive() {
|
||||
let dir = tempdir().unwrap();
|
||||
let package = dir.path().join("unsafe.yoi-plugin");
|
||||
write_stored_zip(
|
||||
&package,
|
||||
&[
|
||||
(
|
||||
"plugin.toml",
|
||||
plugin_manifest("unsafe", "Echo", "object", &["Echo"]).as_bytes(),
|
||||
),
|
||||
("../escape.wasm", b"not wasm"),
|
||||
],
|
||||
);
|
||||
|
||||
let output = render_check(&package, &PluginCliArgs::default()).unwrap();
|
||||
assert!(output.contains("rejected"));
|
||||
assert!(output.contains("escapes"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_pack_is_deterministic_and_discoverable() {
|
||||
let dir = tempdir().unwrap();
|
||||
let plugin = dir.path().join("plugin");
|
||||
fs::create_dir_all(&plugin).unwrap();
|
||||
fs::write(
|
||||
plugin.join("plugin.toml"),
|
||||
plugin_manifest("echo", "echo", "object", &["echo"]),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
|
||||
let first = dir.path().join("first.yoi-plugin");
|
||||
let second = dir.path().join("second.yoi-plugin");
|
||||
|
||||
let first_json = render_pack(
|
||||
&plugin,
|
||||
Some(&first),
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let second_json = render_pack(
|
||||
&plugin,
|
||||
Some(&second),
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(fs::read(&first).unwrap(), fs::read(&second).unwrap());
|
||||
let first_value: serde_json::Value = serde_json::from_str(&first_json).unwrap();
|
||||
let second_value: serde_json::Value = serde_json::from_str(&second_json).unwrap();
|
||||
assert_eq!(first_value["command"], "pack");
|
||||
assert_eq!(first_value["status"], "packed");
|
||||
assert_eq!(
|
||||
first_value["package"]["digest"],
|
||||
second_value["package"]["digest"]
|
||||
);
|
||||
|
||||
let workspace = dir.path().join("workspace");
|
||||
fs::create_dir_all(workspace.join(".yoi/plugins")).unwrap();
|
||||
fs::copy(&first, workspace.join(".yoi/plugins/echo.yoi-plugin")).unwrap();
|
||||
let discovery = discover_plugins(&PluginDiscoveryOptions {
|
||||
workspace_root: workspace,
|
||||
user_data_home: None,
|
||||
limits: PluginDiscoveryLimits::default(),
|
||||
});
|
||||
assert_eq!(discovery.packages.len(), 1);
|
||||
assert_eq!(discovery.packages[0].identity.to_string(), "project:echo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambiguous_ref_is_bounded_error() {
|
||||
let snapshot = PluginInspectionSnapshot {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ It is not a dumping ground for external research, old plans, API inventories, or
|
|||
4. [`design/profiles-manifests-prompts.md`](design/profiles-manifests-prompts.md) — reusable Profiles, resolved Manifests, and prompt resources.
|
||||
5. [`design/tool-permissions-scope.md`](design/tool-permissions-scope.md) — tool policy and filesystem scope.
|
||||
6. [`design/plugin-packages.md`](design/plugin-packages.md) — plugin package distribution, discovery, and enablement boundaries.
|
||||
7. [`design/memory-knowledge.md`](design/memory-knowledge.md) — generated memory, Knowledge, and audit records.
|
||||
8. [`development/work-items.md`](development/work-items.md) — how project work is recorded and reviewed.
|
||||
9. [`development/validation.md`](development/validation.md) — how to check changes.
|
||||
7. [`development/plugin-development.md`](development/plugin-development.md) — how to build, package, enable, and inspect Yoi Plugins.
|
||||
8. [`design/memory-knowledge.md`](design/memory-knowledge.md) — generated memory, Knowledge, and audit records.
|
||||
9. [`development/work-items.md`](development/work-items.md) — how project work is recorded and reviewed.
|
||||
10. [`development/validation.md`](development/validation.md) — how to check changes.
|
||||
|
||||
## What belongs here
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ The migration should be phased:
|
|||
2. Add manifest/schema support for `runtime.kind = "wasm-component"` without executing it during discovery.
|
||||
3. Add a component runtime backend and typed host import/export binding.
|
||||
4. Port `https` and `fs` host API designs to WIT-compatible interfaces.
|
||||
5. Add a Rust authoring SDK/template around the component world.
|
||||
5. Add a Rust PDK/template around the component world.
|
||||
6. Decide whether the raw ABI remains supported, becomes legacy-only, or is deprecated after examples and tests move.
|
||||
|
||||
## Runtime/backend caution
|
||||
|
|
@ -161,10 +161,14 @@ Wrong `world`, missing artifact metadata, missing `call` export, unsupported
|
|||
imports, or core-Wasm bytes in a component package all fail closed with bounded
|
||||
Plugin diagnostics or ordinary Tool errors.
|
||||
|
||||
See `docs/examples/plugin-component-tool/lib.rs` for a minimal
|
||||
`wit-bindgen`/SDK-style authoring sketch. Package authors should generate
|
||||
bindings from `resources/plugin/wit`, build a component artifact, and set the
|
||||
component runtime metadata above.
|
||||
See `docs/examples/plugin-component-tool/lib.rs` and the embedded
|
||||
`resources/plugin/templates/rust-component-tool/` starter for the preferred
|
||||
Rust PDK authoring path. `yoi-plugin-pdk` is guest-side only: it re-exports
|
||||
`wit-bindgen`, provides typed JSON input/output helpers, renders bounded
|
||||
`ToolError` values as ordinary ToolOutput JSON, and does not depend on host
|
||||
runtime crates or grant authority. Package authors should generate bindings from
|
||||
`resources/plugin/wit`, build a component artifact, and set the component
|
||||
runtime metadata above.
|
||||
|
||||
### v1 request/response shape
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ entry = "plugin.wasm"
|
|||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
|
||||
The preferred future WASM authoring/runtime shape is the WebAssembly Component Model, recorded in [Plugin Component Model migration](plugin-component-model.md). Component packages should be explicit and source-compatible rather than silently changing the existing raw core-Wasm runtime:
|
||||
The preferred WASM authoring/runtime shape is the WebAssembly Component Model, recorded in [Plugin Component Model migration](plugin-component-model.md). Component packages should be explicit and source-compatible rather than silently changing the existing raw core-Wasm runtime:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
|
|
@ -216,6 +216,13 @@ component = "plugin.component.wasm"
|
|||
world = "yoi:plugin/tool@1.0.0"
|
||||
```
|
||||
|
||||
For new Rust Tool packages, the preferred authoring path is the first-party
|
||||
`yoi-plugin-pdk` plus the embedded `resources/plugin/templates/rust-component-tool/`
|
||||
starter. The template uses a checkout-local path dependency for development and
|
||||
documents a future out-of-tree pinned git `rev` dependency pattern. Crates.io
|
||||
publication, remote template fetching, and package authoring commands are not
|
||||
part of the current package/runtime contract.
|
||||
|
||||
This is separate from the legacy raw core-Wasm runtime:
|
||||
|
||||
```toml
|
||||
|
|
|
|||
|
|
@ -29,6 +29,37 @@ Source/partial layers may omit fields. Resolved manifests should be explicit eno
|
|||
|
||||
For normal Profile/default startup, a workspace may add `.yoi/override.local.toml` as a final local manifest layer. Yoi discovers the nearest ancestor `.yoi/override.local.toml` from the workspace base used for profile resolution, resolves relative paths in that file against its containing `.yoi` directory, and applies it after the selected Profile and builtin defaults. This file is intended for machine-local choices such as provider/model, worker language, prompt pack, and permission policy tweaks; it is ignored by git via the repository `*.local.*` rule. It is not applied in explicit `--manifest <path>` mode, and it cannot set `pod.name` because Pod identity remains a runtime input.
|
||||
|
||||
## Local stdio MCP server declarations
|
||||
|
||||
Profiles and manifest layers may declare named local stdio MCP servers under `mcp.stdio_server`. This is a typed configuration surface only. Declaring a server does not start a subprocess, discover packages, negotiate MCP capabilities, or register tools/resources/prompts.
|
||||
|
||||
Example Lua Profile fragment:
|
||||
|
||||
```lua
|
||||
mcp = {
|
||||
stdio_server = {
|
||||
{
|
||||
name = "filesystem",
|
||||
command = "node",
|
||||
args = { "server.js", "--root", "." },
|
||||
cwd = { kind = "path", path = "./mcp" },
|
||||
env = {
|
||||
inherit = { "PATH" },
|
||||
set = {
|
||||
SAFE_MODE = { kind = "literal", value = "1" },
|
||||
API_TOKEN = { kind = "secret_ref", ref = "providers/mcp-token" },
|
||||
UPSTREAM_TOKEN = { kind = "env_ref", name = "MCP_UPSTREAM_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`command` is a direct executable name/path, not a shell string. `args` are passed as argv entries by future lifecycle code. `cwd.kind = "path"` is resolved relative to the Profile or manifest layer; omit `cwd` or use `{ kind = "inherit" }` when the lifecycle caller should choose. Environment handling is explicit: future spawn code should inherit only names listed in `env.inherit` and set only variables in `env.set`. `literal` values are for non-secret data; credentials should use `secret_ref` or explicit `env_ref`. Diagnostics and Debug output must redact env literal values and must not print secret plaintext.
|
||||
|
||||
Local stdio MCP servers are ordinary local executables running with the user's OS permissions. Yoi's feature flags, Plugin permissions, and MCP config validation are not an operating-system sandbox and cannot prevent filesystem/network/process side effects once a later lifecycle implementation chooses to spawn a configured server.
|
||||
|
||||
## Spawned Pods
|
||||
|
||||
`SpawnPod.profile` is optional and resolves through defaults when omitted. The only concrete capability delegation in the tool call is `SpawnPod.scope`, and it must be a subset of the parent's effective scope.
|
||||
|
|
|
|||
341
docs/development/plugin-development.md
Normal file
341
docs/development/plugin-development.md
Normal file
|
|
@ -0,0 +1,341 @@
|
|||
# Plugin development
|
||||
|
||||
This guide is for building a Yoi Plugin outside the Yoi runtime codebase. It describes the current Plugin package shape, how to author a Tool Plugin, how to enable it in a workspace, and how to inspect/debug it.
|
||||
|
||||
Yoi Plugins are intentionally explicit:
|
||||
|
||||
- putting a package in `.yoi/plugins` only makes it discoverable;
|
||||
- a Profile/config entry must explicitly enable it;
|
||||
- Plugin grants must allow its surfaces and host APIs;
|
||||
- Plugin code runs only through the configured sandbox runtime;
|
||||
- Tool calls and Tool results use the ordinary Yoi Tool/Worker history path.
|
||||
|
||||
## Current status
|
||||
|
||||
Implemented foundation:
|
||||
|
||||
- package discovery from project/user Plugin stores;
|
||||
- explicit enablement resolution;
|
||||
- Tool surface registration;
|
||||
- Plugin permission grants;
|
||||
- raw core-Wasm Tool runtime;
|
||||
- Component Model Tool runtime;
|
||||
- first-party Rust PDK helpers for Component Model Tool guests;
|
||||
- embedded Rust Component Tool starter template;
|
||||
- `https` and `fs` host APIs for Tool runtime;
|
||||
- read-only `yoi plugin list/show` inspection;
|
||||
- local first-party authoring commands: `yoi plugin new`, `yoi plugin check`, and `yoi plugin pack`.
|
||||
|
||||
Still intentionally separate/future work:
|
||||
|
||||
- multi-language SDK/PDK crates;
|
||||
- Service / Ingress surfaces;
|
||||
- WebSocket or inbound HTTP for bidirectional bridges;
|
||||
- public registry/install/update/signature tooling.
|
||||
|
||||
## Package locations
|
||||
|
||||
Yoi discovers `.yoi-plugin` packages from:
|
||||
|
||||
```text
|
||||
<workspace>/.yoi/plugins/*.yoi-plugin
|
||||
${XDG_DATA_HOME:-~/.local/share}/yoi/plugins/*.yoi-plugin
|
||||
```
|
||||
|
||||
Use project packages for workspace-specific Plugins and user packages for personal reusable Plugins. Project packages should normally be committed only when the package content is safe and intended to be part of the project.
|
||||
|
||||
## Package archive format
|
||||
|
||||
A `.yoi-plugin` package is currently a bounded ZIP archive. For now, create it with stored entries, not compressed entries:
|
||||
|
||||
```bash
|
||||
(cd my-plugin && zip -0 -r ../example.echo.yoi-plugin plugin.toml plugin.component.wasm)
|
||||
```
|
||||
|
||||
The archive root must contain `plugin.toml`. Runtime files referenced by the manifest must also be inside the archive. Yoi rejects path traversal, root escapes, malformed manifests, unsupported API/runtime versions, and other unsafe archive shapes.
|
||||
|
||||
## Authoring CLI
|
||||
|
||||
Use the local authoring commands for first-party deterministic authoring. These commands never fetch remote templates, never run Plugin code, never mutate enablement configuration, and never generate or embed secrets.
|
||||
|
||||
Create a Rust Component Tool starter from embedded resources:
|
||||
|
||||
```bash
|
||||
yoi plugin new rust-component-tool ./my-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.
|
||||
|
||||
Validate a source directory or an existing `.yoi-plugin` archive:
|
||||
|
||||
```bash
|
||||
yoi plugin check ./my-plugin
|
||||
yoi plugin check ./my-plugin --json
|
||||
yoi plugin check ./my-plugin.yoi-plugin --json
|
||||
```
|
||||
|
||||
`check` performs bounded static validation of the directory/archive shape, manifest, runtime declaration, referenced artifact presence, Tool schemas, permission declarations, host API declarations, archive safety, and deterministic digest when a package can be materialized. Component-world validation is metadata-only: it verifies the declared world string and runtime manifest shape, but it does not instantiate or execute the component. A generated placeholder component produces `status = "partial"` plus a diagnostic and is not enablement-ready until replaced. Invalid checks print the same structured report and exit non-zero.
|
||||
|
||||
Pack a source directory into a deterministic stored `.yoi-plugin` archive:
|
||||
|
||||
```bash
|
||||
yoi plugin pack ./my-plugin
|
||||
yoi plugin pack ./my-plugin --output ./my-plugin.yoi-plugin --json
|
||||
```
|
||||
|
||||
`pack` rejects malformed manifests, missing runtime artifacts, symlinks/root escapes, and unsupported package shapes. The JSON output contains the stable package reference, output path, digest, entries, and safety flags. After review, copy the package to `.yoi/plugins/` (or the user Plugin store) and add explicit Profile/config enablement with pinned digest and grants; packing and checking do not do this for you.
|
||||
|
||||
## Manifest: `plugin.toml`
|
||||
|
||||
A minimal Component Model Tool Plugin manifest looks like this:
|
||||
|
||||
```toml
|
||||
schema_version = 1
|
||||
id = "example.echo"
|
||||
name = "Example Echo"
|
||||
version = "0.1.0"
|
||||
surfaces = ["tool"]
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "tool" },
|
||||
{ kind = "tool", name = "example_echo" },
|
||||
]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "example_echo"
|
||||
description = "Echo input text."
|
||||
input_schema = { type = "object", properties = { text = { type = "string" } }, required = ["text"], additionalProperties = false }
|
||||
external_write = false
|
||||
```
|
||||
|
||||
The preferred new runtime is `wasm-component`. The older raw core-Wasm runtime remains explicit for compatibility:
|
||||
|
||||
```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.
|
||||
|
||||
## Component Model + Rust PDK authoring
|
||||
|
||||
Component Model authoring with `yoi-plugin-pdk` is the preferred path for new Tool Plugins. The raw core-Wasm ABI remains available only as compatibility/transitional runtime support.
|
||||
|
||||
Yoi's Component Model Tool world is stored in `resources/plugin/wit/`. The embedded Rust starter template is available as data in the Yoi source tree at:
|
||||
|
||||
```text
|
||||
resources/plugin/templates/rust-component-tool/
|
||||
```
|
||||
|
||||
It contains:
|
||||
|
||||
- `Cargo.toml` with a checkout-local `yoi-plugin-pdk` path dependency;
|
||||
- `src/lib.rs` with WIT binding generation and typed JSON Tool handling;
|
||||
- `plugin.toml` targeting `kind = "wasm-component"` and `world = "yoi:plugin/tool@1.0.0"`;
|
||||
- README next steps and the future out-of-tree pinned git `rev` dependency pattern.
|
||||
|
||||
A minimal PDK-backed Rust sketch is also available at:
|
||||
|
||||
```text
|
||||
docs/examples/plugin-component-tool/lib.rs
|
||||
```
|
||||
|
||||
The important authoring shape is:
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yoi_plugin_pdk::wit_bindgen;
|
||||
use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "tool",
|
||||
path: "../../../resources/plugin/wit",
|
||||
generate_all,
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EchoInput {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct EchoOutput<'a> {
|
||||
tool: &'a str,
|
||||
text: String,
|
||||
}
|
||||
|
||||
fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result<ToolOutput, ToolError> {
|
||||
ToolOutput::json(
|
||||
format!("{} ok", ctx.tool_name()),
|
||||
EchoOutput {
|
||||
tool: ctx.tool_name(),
|
||||
text: input.text,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
yoi_plugin_pdk::export_component_tool!(Plugin, handle_echo);
|
||||
```
|
||||
|
||||
`run_json_tool` parses the WIT `input-json` string into a typed input, passes a `ToolContext` containing the selected Tool name, and serializes `ToolOutput` JSON accepted by the current component runtime. `ToolError` values are structured and bounded, then rendered through the ordinary Tool result path; the component cannot inject hidden context.
|
||||
|
||||
The PDK is guest-side only. It does not depend on Yoi host/runtime crates and does not grant filesystem, network, or environment authority. Host-side Plugin manifests and explicit enablement grants remain the authority boundary for Tool execution and for WIT host APIs such as `yoi:host/https` and `yoi:host/fs`.
|
||||
|
||||
The exact component build pipeline depends on the authoring toolchain (`wit-bindgen`, component adapter tooling, etc.). Crates.io publication, remote template fetching, and `yoi plugin new/check/pack` are intentionally deferred. Until they exist, Plugin authors should treat the template/example as the ABI contract sketch and use `yoi plugin list/show` plus focused runtime tests to verify packages.
|
||||
|
||||
## Enabling a Plugin in a workspace
|
||||
|
||||
Enablement belongs in the resolved Profile/config path for the workspace. For local dogfooding or private experiments, use the ignored local overlay rather than committing secrets or local paths:
|
||||
|
||||
```toml
|
||||
# .yoi/override.local.toml
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
|
||||
[[plugins.enabled]]
|
||||
id = "project:example.echo"
|
||||
version = "0.1.0"
|
||||
digest = "sha256:<digest from yoi plugin show/list>"
|
||||
surfaces = ["tool"]
|
||||
|
||||
[plugins.enabled.grants]
|
||||
id = "project:example.echo"
|
||||
version = "0.1.0"
|
||||
digest = "sha256:<same digest>"
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "tool" },
|
||||
{ kind = "tool", name = "example_echo" },
|
||||
]
|
||||
```
|
||||
|
||||
A source-qualified id is preferred:
|
||||
|
||||
```text
|
||||
project:example.echo
|
||||
user:example.echo
|
||||
builtin:example.echo
|
||||
```
|
||||
|
||||
Unqualified ids can be ambiguous and should fail closed when more than one source matches.
|
||||
|
||||
## Inspecting Plugins
|
||||
|
||||
Use the read-only CLI inspection commands first:
|
||||
|
||||
```bash
|
||||
yoi plugin list
|
||||
yoi plugin list --json
|
||||
yoi plugin show project:example.echo
|
||||
yoi plugin show project:example.echo --json
|
||||
```
|
||||
|
||||
`list/show` must not execute Plugin code. They are intended to explain static state:
|
||||
|
||||
- discovered packages;
|
||||
- enabled vs disabled packages;
|
||||
- missing packages referenced by enablement;
|
||||
- invalid manifests;
|
||||
- digest/version/source mismatches;
|
||||
- granted/denied permissions;
|
||||
- Tool registration eligibility;
|
||||
- runtime metadata.
|
||||
|
||||
Typical statuses:
|
||||
|
||||
```text
|
||||
active enabled and statically valid for at least one surface/tool
|
||||
disabled discovered but not explicitly enabled
|
||||
missing enablement references a package that is not discovered
|
||||
rejected invalid manifest, incompatible API, digest mismatch, grant denial, etc.
|
||||
partial usable package with some rejected surfaces/tools
|
||||
```
|
||||
|
||||
## `https` host API
|
||||
|
||||
The `https` host API is outbound-only and grant-gated. It is meant for Tool calls such as webhook posting or REST requests. It is not a WebSocket/Gateway or inbound HTTP bridge.
|
||||
|
||||
Manifest permissions should request `host_api.https` in addition to the Tool permissions. Enablement grants must then allow the API and constrain hosts/methods.
|
||||
|
||||
Example grant shape:
|
||||
|
||||
```toml
|
||||
[plugins.enabled.grants]
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "tool" },
|
||||
{ kind = "tool", name = "discord_post" },
|
||||
{ kind = "host_api", api = "https" },
|
||||
]
|
||||
|
||||
[[plugins.enabled.grants.https]]
|
||||
host = "discord.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/api/webhooks/"]
|
||||
```
|
||||
|
||||
Yoi rejects `http://`, localhost/private/link-local targets, disallowed hosts/methods, oversize requests/responses, and missing grants. Credentials must come from explicit config/secret references, not ambient environment variables.
|
||||
|
||||
## `fs` host API
|
||||
|
||||
The `fs` host API is Plugin-scoped and grant-gated. Plugins do not inherit the Pod/workspace filesystem authority automatically.
|
||||
|
||||
Example grant shape:
|
||||
|
||||
```toml
|
||||
[plugins.enabled.grants]
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "tool" },
|
||||
{ kind = "tool", name = "read_notes" },
|
||||
{ kind = "host_api", api = "fs" },
|
||||
]
|
||||
|
||||
[[plugins.enabled.grants.fs]]
|
||||
root = "/absolute/path/to/plugin-data"
|
||||
operations = ["read", "list"]
|
||||
```
|
||||
|
||||
Yoi normalizes paths, rejects `..` traversal, rejects symlink/root escapes, and applies read/write/list bounds. Diagnostics must not include file contents.
|
||||
|
||||
## Outbound vs bridge integrations
|
||||
|
||||
After `https`, an outbound Discord webhook Tool is feasible:
|
||||
|
||||
```text
|
||||
Yoi Tool call -> Plugin Tool -> yoi:host/https -> Discord REST/webhook
|
||||
```
|
||||
|
||||
A bidirectional Discord bridge is different. It needs a Service surface plus Ingress and either WebSocket/Gateway support or inbound HTTP interactions:
|
||||
|
||||
```text
|
||||
Discord Gateway/Webhook -> Plugin Service/Ingress -> host routing policy -> notify/run/drop/diagnostic
|
||||
```
|
||||
|
||||
Do not model bidirectional bridge work as an `https` Tool alone.
|
||||
|
||||
## Development checklist
|
||||
|
||||
1. Create a package directory with `plugin.toml` and the runtime artifact.
|
||||
2. Build the Wasm/component artifact.
|
||||
3. Package with stored ZIP entries as `.yoi-plugin`.
|
||||
4. Put it under `.yoi/plugins/` or the user Plugin store.
|
||||
5. Run `yoi plugin list` and `yoi plugin show <ref>`.
|
||||
6. Add explicit enablement and grants.
|
||||
7. Re-run `yoi plugin show <ref>` until status/diagnostics are correct.
|
||||
8. Start Yoi with `features.plugins = true` in the resolved config/Profile.
|
||||
9. Call the Tool and verify ordinary Tool result/history behavior.
|
||||
|
||||
## Safety rules for Plugin authors
|
||||
|
||||
- Do not assume ambient filesystem, network, or environment access.
|
||||
- Do not put secrets in `plugin.toml` or package files.
|
||||
- Request only the minimal host APIs and grants needed.
|
||||
- Keep Tool output bounded and structured.
|
||||
- Prefer Component Model authoring for new Plugins.
|
||||
- Treat raw core-Wasm ABI support as transitional compatibility.
|
||||
|
|
@ -1,23 +1,38 @@
|
|||
//! Minimal Component Model Tool plugin authoring sketch.
|
||||
//! Minimal Component Model Tool plugin authoring sketch using `yoi-plugin-pdk`.
|
||||
//!
|
||||
//! Build this as a `wasm32-unknown-unknown` cdylib with `wit-bindgen`-generated
|
||||
//! exports and package the adapted component as `plugin.component.wasm`.
|
||||
//! Build this as a `wasm32-unknown-unknown` cdylib with Component Model tooling
|
||||
//! and package the adapted component as `plugin.component.wasm`.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use yoi_plugin_pdk::wit_bindgen;
|
||||
use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "tool",
|
||||
path: "../../../resources/plugin/wit",
|
||||
generate_all,
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
struct Plugin;
|
||||
|
||||
impl Guest for Plugin {
|
||||
fn call(tool_name: String, input_json: String) -> String {
|
||||
// Ordinary ToolOutput JSON. The runtime routes this through the normal
|
||||
// Worker/Tool result path; no context is injected by the component.
|
||||
format!(
|
||||
r#"{{"summary":"component tool {tool_name}","content":"input was {input_json}"}}"#
|
||||
)
|
||||
}
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct EchoInput {
|
||||
text: String,
|
||||
}
|
||||
|
||||
export!(Plugin);
|
||||
#[derive(Debug, Serialize)]
|
||||
struct EchoOutput<'a> {
|
||||
tool: &'a str,
|
||||
text: String,
|
||||
}
|
||||
|
||||
fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result<ToolOutput, ToolError> {
|
||||
ToolOutput::json(
|
||||
format!("{} ok", ctx.tool_name()),
|
||||
EchoOutput {
|
||||
tool: ctx.tool_name(),
|
||||
text: input.text,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
yoi_plugin_pdk::export_component_tool!(Plugin, handle_echo);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
|
|||
filter = sourceFilter;
|
||||
};
|
||||
|
||||
cargoHash = "sha256-i4U7wXPoWIHA4EAJZva2HQXNN8P5+RhGVGNBAOZVGk0=";
|
||||
cargoHash = "sha256-EH4zdakrFxqVrgaNBx3dICN6KoLqskTEGYnU73XMVsU=";
|
||||
|
||||
depsExtraArgs = {
|
||||
# Older fetchCargoVendor utilities used crates.io's API download endpoint,
|
||||
|
|
|
|||
21
resources/plugin/templates/rust-component-tool/Cargo.toml
Normal file
21
resources/plugin/templates/rust-component-tool/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "yoi-rust-component-tool-template"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
# Keep the embedded template checkable in-place without making it a member of
|
||||
# Yoi's root workspace. A copied starter remains a normal standalone package.
|
||||
[workspace]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" }
|
||||
|
||||
# Future out-of-tree Plugin packages should pin the Yoi revision instead of
|
||||
# relying on crates.io publication or remote template fetching, for example:
|
||||
# yoi-plugin-pdk = { git = "https://github.com/example/yoi.git", package = "yoi-plugin-pdk", rev = "<pinned-yoi-revision>" }
|
||||
37
resources/plugin/templates/rust-component-tool/README.md
Normal file
37
resources/plugin/templates/rust-component-tool/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Rust Component Tool Template
|
||||
|
||||
This is the embedded starter template for a Yoi Component Model Tool Plugin written with the first-party Rust PDK.
|
||||
|
||||
## What this template demonstrates
|
||||
|
||||
- `wasm-component` runtime targeting `yoi:plugin/tool@1.0.0`.
|
||||
- Guest-side WIT binding generation through the PDK's `wit_bindgen` re-export.
|
||||
- Typed JSON input parsing through `run_json_tool` via `export_component_tool!`.
|
||||
- Typed JSON output serialization with `ToolOutput::json`.
|
||||
- Structured, bounded `ToolError` output for user-visible Tool failures.
|
||||
|
||||
The PDK is guest-side only. It does not grant filesystem, network, or environment authority. Host-side Plugin manifests and grants remain the authority boundary for Tool execution and host APIs.
|
||||
|
||||
## Checkout/development dependency
|
||||
|
||||
Inside the Yoi checkout this template uses a local path dependency and declares an empty `[workspace]` so it can be checked in place without becoming a member of Yoi's root workspace:
|
||||
|
||||
```toml
|
||||
yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" }
|
||||
```
|
||||
|
||||
If this template is copied elsewhere before crates.io publication exists, pin a Yoi source revision instead of fetching an unpinned remote template:
|
||||
|
||||
```toml
|
||||
yoi-plugin-pdk = { git = "https://github.com/example/yoi.git", package = "yoi-plugin-pdk", rev = "<pinned-yoi-revision>" }
|
||||
```
|
||||
|
||||
`plugin.component.wasm` in the template is a text placeholder so `yoi plugin check` and `yoi plugin pack` can exercise deterministic local package validation immediately. Replace it with a real built component before enabling or executing the Plugin.
|
||||
|
||||
## Next steps
|
||||
|
||||
1. Replace package/plugin ids, names, descriptions, and Tool schema.
|
||||
2. Replace `EchoInput` / `EchoOutput` and `handle_echo` with your Tool logic.
|
||||
3. Build a component for `wasm32-unknown-unknown` with the Component Model tooling used by your environment, replacing the placeholder `plugin.component.wasm`.
|
||||
4. Run `yoi plugin check .` and `yoi plugin pack . --output ./my-plugin.yoi-plugin`.
|
||||
5. Copy the package to a Plugin store and add explicit enablement with pinned digest/grants after review.
|
||||
|
|
@ -0,0 +1 @@
|
|||
placeholder component artifact for authoring-template checks; replace with a built wasm component before enabling.
|
||||
20
resources/plugin/templates/rust-component-tool/plugin.toml
Normal file
20
resources/plugin/templates/rust-component-tool/plugin.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
schema_version = 1
|
||||
id = "example.rust_component_tool"
|
||||
name = "Rust Component Tool Template"
|
||||
version = "0.1.0"
|
||||
surfaces = ["tool"]
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "tool" },
|
||||
{ kind = "tool", name = "example_echo" },
|
||||
]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "example_echo"
|
||||
description = "Echo input text using the Rust PDK."
|
||||
input_schema = { type = "object", properties = { text = { type = "string" } }, required = ["text"], additionalProperties = false }
|
||||
external_write = false
|
||||
37
resources/plugin/templates/rust-component-tool/src/lib.rs
Normal file
37
resources/plugin/templates/rust-component-tool/src/lib.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use yoi_plugin_pdk::wit_bindgen;
|
||||
use yoi_plugin_pdk::{ToolContext, ToolError, ToolOutput};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "tool",
|
||||
path: "../../../../resources/plugin/wit",
|
||||
generate_all,
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct EchoInput {
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct EchoOutput<'a> {
|
||||
tool: &'a str,
|
||||
text: String,
|
||||
}
|
||||
|
||||
fn handle_echo(ctx: ToolContext, input: EchoInput) -> Result<ToolOutput, ToolError> {
|
||||
if input.text.trim().is_empty() {
|
||||
return Err(ToolError::invalid_input("`text` must not be empty"));
|
||||
}
|
||||
|
||||
ToolOutput::json(
|
||||
format!("{} echoed text", ctx.tool_name()),
|
||||
EchoOutput {
|
||||
tool: ctx.tool_name(),
|
||||
text: input.text,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
yoi_plugin_pdk::export_component_tool!(Plugin, handle_echo);
|
||||
|
|
@ -10,6 +10,6 @@ interface https {
|
|||
/// Grant-bound filesystem host API. No ambient WASI filesystem is exposed.
|
||||
interface fs {
|
||||
read: func(request-json: string) -> string;
|
||||
list: func(request-json: string) -> string;
|
||||
%list: func(request-json: string) -> string;
|
||||
write: func(request-json: string) -> string;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user