merge: integrate orchestration branch
This commit is contained in:
commit
839783b2e6
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: 'Plugin Service/Ingress component lifecycle surface'
|
||||
state: 'inprogress'
|
||||
state: 'closed'
|
||||
created_at: '2026-06-20T13:01:37Z'
|
||||
updated_at: '2026-06-20T13:30:38Z'
|
||||
updated_at: '2026-06-20T15:23:35Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T13:28:19Z'
|
||||
|
|
|
|||
21
.yoi/tickets/00001KVJHYP4Q/resolution.md
Normal file
21
.yoi/tickets/00001KVJHYP4Q/resolution.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
Plugin Service/Ingress component lifecycle surface を実装し、Orchestrator worktree の `orchestration` branch に統合した。
|
||||
|
||||
主な成果:
|
||||
- Pod plugin feature に host-managed `PluginInstanceRegistry` / instance handle 境界を追加し、Tool dispatch を instance 経由に変更。
|
||||
- New instance-capable component world `yoi:plugin/instance@1.0.0` と WIT resource を追加。
|
||||
- `yoi-plugin-pdk` と Rust component template に instance-oriented authoring support を追加。
|
||||
- Existing component Tool world / raw wasm Tool runtime を instance registry compatibility path に維持。
|
||||
- Manifest/static validation に Service / Ingress declarations と per-surface grant validation を追加。
|
||||
- Service lifecycle/status/diagnostics と bounded in-process ingress dispatch path を実装。
|
||||
- Tool / Service / Ingress enabled-surface filtering を runtime install, dispatch guard, and resolved static inspection / `yoi plugin list/show` に適用。
|
||||
- `plugin check` は package declaration inspection、resolved `plugin list/show` は selected/enabled surfaces に基づく reporting に分離。
|
||||
- Focused tests added for manifest validation, legacy Tool compatibility, instance state persistence, ingress dispatch, Service/Ingress grant denial, failure diagnostics, and partial enabled-surface static reporting。
|
||||
|
||||
統合・検証:
|
||||
- Merge commit: `43c9216e merge: plugin instance lifecycle surface`
|
||||
- Implementation commits: `147a6005`, `870bcc76`, `79ca0f7f`, `627c8f36`
|
||||
- Reviewer final verdict: approve
|
||||
- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p manifest plugin -- --nocapture`, `cargo test -p pod plugin -- --nocapture`, `cargo test -p yoi plugin -- --nocapture`, `cargo check -p yoi`, `cargo check -p yoi-plugin-pdk`, template cargo-check, `yoi ticket doctor`, and `nix build .#yoi --no-link`。
|
||||
|
||||
範囲外:
|
||||
- Discord Bridge 本体、public registry/install/update/signature tooling、arbitrary Plugin UI channel、hidden context injection、Service/Ingress による model-visible Tool schema mutation は実装していない。
|
||||
|
|
@ -122,3 +122,584 @@ 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-20T14:16:45Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder implementation report received from `yoi-coder-00001KVJHYP4Q`.
|
||||
|
||||
Implementation commit:
|
||||
- `147a6005 plugin: add instance lifecycle surface`
|
||||
|
||||
Changed areas reported:
|
||||
- Manifest support for Service/Ingress declarations and permissions。
|
||||
- New `yoi:plugin/instance@1.0.0` WIT/resource surface。
|
||||
- Host-managed `PluginInstanceRegistry` / `PluginInstanceHandle` in Pod plugin feature layer。
|
||||
- Plugin Tool registration/dispatch now routes through instance handles。
|
||||
- Legacy raw-wasm and `yoi:plugin/tool@1.0.0` component Tool runtimes preserved through registry adapter path。
|
||||
- Instance lifecycle/status/stop/diagnostics surfaces and bounded Ingress dispatch API added。
|
||||
- Independent Tool/Service/Ingress grant validation added。
|
||||
- Static inspection / Plugin CLI diagnostics extended for Service/Ingress eligibility。
|
||||
- Rust PDK instance-oriented types/trait/macro support added。
|
||||
- Rust component instance template resources added。
|
||||
- Plugin component model docs updated。
|
||||
- Tests added/updated for manifest validation, CLI reporting/templates, legacy compatibility, grant checks, and in-process shared Tool/Ingress dispatch。
|
||||
|
||||
Coder validation reported as passing:
|
||||
- `cargo test -p manifest plugin -- --nocapture`
|
||||
- `cargo test -p pod plugin -- --nocapture`
|
||||
- `cargo test -p yoi plugin -- --nocapture`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo check -p yoi-plugin-pdk`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check`
|
||||
- `yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Dependency/package impact:
|
||||
- No new Rust dependencies。
|
||||
- Added package/resource files under `resources/plugin/templates/rust-component-instance/` and `resources/plugin/wit/yoi-plugin-instance-v1.wit`。
|
||||
- No `Cargo.lock` or `package.nix` changes required; Nix build succeeded。
|
||||
|
||||
Known risks / deferrals:
|
||||
- Instance Component ABI is intentionally minimal string-JSON host dispatch (`start`, `handle-tool`, `handle-ingress`, `status`, `stop`) and should be reviewed before treating it as stable public low-level ABI。
|
||||
- Service lifecycle is surfaced as host-managed registration/status/stop diagnostics; no unsafe hidden model/tool/context action path was added。
|
||||
- Rust PDK instance macro provides author-facing instance helpers, but full cargo-component generated glue ergonomics may need follow-up polish if public PDK shape becomes stricter。
|
||||
|
||||
Orchestrator evidence checked before review dispatch:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `147a6005`。
|
||||
- Diff from acceptance `5ec8bae9..HEAD` is one implementation commit touching 12 files, about 1399 insertions / 34 deletions。
|
||||
- `git diff --check 5ec8bae9..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Dispatch Reviewer for r1 review against Ticket requirements, with focus on instance registry boundary, legacy compatibility, ToolRegistry run-stability, no hidden context/action paths, per-surface grants, Service/Ingress lifecycle semantics, static inspection/CLI reporting, WIT/PDK/template correctness, bounds/resource limits, and validation evidence。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: yoi-orchestrator at: 2026-06-20T14:17:31Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Review dispatch:
|
||||
- Spawned Reviewer Pod `yoi-reviewer-00001KVJHYP4Q-r1` against implementation branch `impl/00001KVJHYP4Q-plugin-instance-lifecycle`。
|
||||
- Review target commit: `147a6005 plugin: add instance lifecycle surface`。
|
||||
- Review baseline: `5ec8bae9`。
|
||||
- Reviewer task focuses on instance registry boundary, legacy Tool compatibility, ordinary Tool result/history path, Service/Ingress no hidden action/context paths, ToolRegistry run-stability, per-surface and host API grant independence, no ambient WASI/network authority, bounded ingress/events/diagnostics, static inspection no-exec behavior, WIT/PDK/template coherence, tests, and Nix/package evidence。
|
||||
- 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-00001KVJHYP4Q-r1 at: 2026-06-20T14:24:40Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Verdict: `request_changes`
|
||||
|
||||
確認範囲:
|
||||
- Ticket contract / Orchestrator IntentPacket。
|
||||
- Implementation diff: `5ec8bae9..147a6005`。
|
||||
- 主な対象:
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
- `crates/manifest/src/plugin.rs`
|
||||
- `crates/yoi/src/plugin_cli.rs`
|
||||
- `crates/plugin-pdk/src/lib.rs`
|
||||
- `resources/plugin/wit/yoi-plugin-instance-v1.wit`
|
||||
- `resources/plugin/templates/rust-component-instance/*`
|
||||
- Plugin component docs/templates and focused tests。
|
||||
- Hidden history/context injection、ToolRegistry bypass、Ingress/Service production call path、ambient WASI/network authority の有無を確認。
|
||||
|
||||
Blocking issues:
|
||||
|
||||
1. Rust instance PDK/template が buildable ではなく、WIT component instance ABI を実際に export していない。
|
||||
- `resources/plugin/templates/rust-component-instance/Cargo.toml` は repository 配下 package だが `[workspace]` がなく、direct authoring check が workspace membership error で失敗する。
|
||||
- `resources/plugin/templates/rust-component-instance/src/lib.rs` は `ToolOutput::text(...)` を呼ぶが、`crates/plugin-pdk/src/lib.rs` には `ToolOutput::new`, `ToolOutput::json`, `ToolOutput::summary` しかない。
|
||||
- `export_plugin_instance!` は generated WIT bindings / generated `export!` macro for `world instance` を実装していない。raw placeholder `#[unsafe(export_name = "start")]` と private Rust methods を定義するだけで、host が期待する component-model exports (`start`, `handle-tool`, `handle-ingress`, `status`, `stop`) を生成しない。
|
||||
- Ticket が要求する WIT/PDK/template coherence と instance-oriented authoring surface を満たしていない。
|
||||
|
||||
2. Component instance lifecycle が status/error outputs を parse せず、component `status` export が実質 unused。
|
||||
- `PluginInstance::status` は host-side lifecycle/diagnostics のみを返し、component runtime の `status` export を呼ばない。
|
||||
- `PluginComponentInstanceRuntime::start` は component `start` export の returned string を捨てている。
|
||||
- `PluginComponentInstanceRuntime::stop` も returned string を捨てている。
|
||||
- WIT は `status` を export しているが、host-side runtime method がない。
|
||||
- Component が `{"error": ...}` を `start` から返しても host が started と扱い得るため、lifecycle/status/diagnostics acceptance criteria を満たしていない。
|
||||
|
||||
3. Service/Ingress surfaces が Tool 登録なしでは host-managed Plugin instance を作成・保持しない。
|
||||
- `PluginToolFeature::install` の実 install path では Service declarations は metadata only、Ingress declarations は authorization-check only。
|
||||
- Instance は Tool registration loop 内でのみ作られる。
|
||||
- Service-only / Ingress-only instance-capable plugin は host-managed instance、lifecycle start、status、diagnostics、retained ingress dispatch handle を得られない。
|
||||
- `deliver_ingress` は production caller に接続されておらず、focused ingress test は synthetic `TestIngress` instance を使っている。
|
||||
- Ticket intent の “Tool / Service / Ingress as surfaces of the same Plugin instance” を満たしていない。
|
||||
|
||||
Non-blocking concerns / follow-ups:
|
||||
- Human `yoi plugin show` output でも Service/Ingress eligibility を明示すると JSON なしでも rejected surfaces が分かりやすい。
|
||||
- Real instance-component tests が弱い。Rust/PDK generated `yoi:plugin/instance@1.0.0` component の build/execution through start/tool/ingress/status/stop coverage が欲しい。
|
||||
|
||||
Reviewer validation:
|
||||
- `git diff --check 5ec8bae9..HEAD`: passed。
|
||||
- `cargo fmt --check`: passed。
|
||||
- `cargo test -p manifest plugin -- --nocapture`: passed。
|
||||
- `cargo test -p pod plugin -- --nocapture`: passed。
|
||||
- `cargo test -p yoi plugin -- --nocapture`: passed。
|
||||
- `cargo check -p yoi`: passed。
|
||||
- `cargo check -p yoi-plugin-pdk`: passed。
|
||||
- `yoi ticket doctor`: passed。
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
- Additional focused template validation failed: `CARGO_TARGET_DIR=target/review-template cargo check --manifest-path resources/plugin/templates/rust-component-instance/Cargo.toml` with workspace membership error。
|
||||
|
||||
Worktree status at review end: clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T14:43:43Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder r1-fix report received from `yoi-coder-00001KVJHYP4Q`.
|
||||
|
||||
New fix commit:
|
||||
- `870bcc76 plugin: fix instance lifecycle blockers`
|
||||
|
||||
Branch commits now:
|
||||
- `870bcc76 plugin: fix instance lifecycle blockers`
|
||||
- `147a6005 plugin: add instance lifecycle surface`
|
||||
|
||||
Fix summary:
|
||||
- `crates/plugin-pdk/src/lib.rs`:
|
||||
- Reworked instance PDK export macro to use generated `wit-bindgen` `Guest` / `export!` shape instead of raw placeholder exports。
|
||||
- Added coherent instance-world helpers/status/error JSON handling。
|
||||
- `resources/plugin/templates/rust-component-instance/`:
|
||||
- Added empty `[workspace]` so template checks as an in-tree standalone authoring package。
|
||||
- Fixed template API mismatch by using existing `ToolOutput::json`。
|
||||
- Added `wit_bindgen::generate!` for `instance` world and updated export macro usage。
|
||||
- `crates/pod/src/feature/plugin.rs`:
|
||||
- Component instance lifecycle now decodes `start` / `stop` returned JSON and fails closed on returned errors / failed status。
|
||||
- Host now calls component `status` export and includes component status in `PluginInstanceStatus`。
|
||||
- Service/Ingress-capable installs create and retain host-managed instances even without Tool registration。
|
||||
- Added retained registry handle and bounded installed-path Ingress dispatch API。
|
||||
- Added tests for component start error handling, component status/stop reporting, service-only instance retention, and installed-path Tool/Ingress shared-instance dispatch。
|
||||
- No `Cargo.lock`, dependency, or `package.nix` changes。
|
||||
|
||||
Coder validation reported as passing:
|
||||
- `cargo test -p manifest plugin -- --nocapture`
|
||||
- `cargo test -p pod plugin -- --nocapture`
|
||||
- `cargo test -p yoi plugin -- --nocapture`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo check -p yoi-plugin-pdk`
|
||||
- `CARGO_TARGET_DIR=target/review-template cargo check --manifest-path resources/plugin/templates/rust-component-instance/Cargo.toml`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check 5ec8bae9..HEAD`
|
||||
- `yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Remaining deferral / risk:
|
||||
- Instance PDK now uses `wit-bindgen` export glue and template checks, but `yoi:plugin/instance@1.0.0` remains first public shape and should be reviewed before broad external stability promises。
|
||||
|
||||
Orchestrator evidence checked before r2 review:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `870bcc76`。
|
||||
- Fix diff from `147a6005..HEAD` touches 4 files, about 506 insertions / 119 deletions。
|
||||
- Overall `git diff --check 5ec8bae9..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Route back to Reviewer for r2 verification of the PDK/template export glue, lifecycle status/error parsing, Service/Ingress instance retention, installed-path ingress dispatch, and final acceptance criteria。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVJHYP4Q-r1 at: 2026-06-20T14:50:21Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Verdict: `request_changes`
|
||||
|
||||
確認範囲:
|
||||
- Ticket authority / Orchestrator IntentPacket。
|
||||
- Fix diff: `147a6005..870bcc76`。
|
||||
- Overall implementation shape: `5ec8bae9..870bcc76`。
|
||||
- 主な対象:
|
||||
- `crates/plugin-pdk/src/lib.rs`
|
||||
- `resources/plugin/templates/rust-component-instance/Cargo.toml`
|
||||
- `resources/plugin/templates/rust-component-instance/src/lib.rs`
|
||||
- `resources/plugin/wit/yoi-plugin-instance-v1.wit`
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
- `crates/manifest/src/plugin.rs`
|
||||
- `crates/yoi/src/plugin_cli.rs`
|
||||
|
||||
Previous blocker verification:
|
||||
- Rust instance PDK/template buildability / WIT export shape: substantially addressed。
|
||||
- `export_plugin_instance!` now targets generated `wit-bindgen` `Guest` / `export!` shape。
|
||||
- Instance template has empty `[workspace]`, uses `ToolOutput::json`, and includes `wit_bindgen::generate!`。
|
||||
- Focused template validation passes。
|
||||
- Component lifecycle start/status/stop handling: substantially addressed。
|
||||
- Host parses lifecycle JSON and fails closed on error/failure status。
|
||||
- Component `status` export is called and reflected in instance status。
|
||||
- Stop output is decoded/reported。
|
||||
- Service/Ingress instance retention and installed-path dispatch: substantially addressed。
|
||||
- `PluginToolFeature` retains a `PluginInstanceRegistry`。
|
||||
- Service/Ingress-capable installs can create/retain instance without Tool registration。
|
||||
- Installed-path `dispatch_ingress` exists and tests cover shared Tool/Ingress dispatch。
|
||||
|
||||
Blocking issue:
|
||||
|
||||
1. Enabled surface selection is not enforced per surface during descriptor/install, so mixed-surface plugins can expose or block the wrong surfaces。
|
||||
- `PluginToolFeature` is gated at coarse whole-record level by whether any of Tool/Service/Ingress appears in `record.enabled_surfaces`。
|
||||
- Once feature exists, descriptor and install paths iterate over all declared manifest services/tools/ingresses, not only selected/enabled surfaces。
|
||||
- Concrete risk:
|
||||
- Plugin declaring both Tool and Service with only Service selected can still attempt to authorize/register Tool。
|
||||
- Missing Tool grant can fail selected Service install。
|
||||
- If Tool grant exists, unselected Tool can become model-visible。
|
||||
- Unselected Service/Ingress can be provided/started if their grants exist。
|
||||
- This violates the Ticket requirement that Tool / Service / Ingress grants and exposure remain independent and explicitly gated。
|
||||
|
||||
Required fix:
|
||||
- Filter descriptor/install loops by `record.enabled_surfaces`。
|
||||
- Ensure registered/denial behavior accounts for selected surface set, not all manifest declarations。
|
||||
- Add focused tests for mixed-surface packages with partial enabled surfaces:
|
||||
- Service selected, Tool declared but not selected: Service installs and Tool does not register。
|
||||
- Tool selected, Service/Ingress declared but not selected: no Service/Ingress instance/dispatch exposure。
|
||||
- Missing grants for unselected surfaces do not fail selected-surface install。
|
||||
- Grants for unselected surfaces do not cause exposure。
|
||||
|
||||
Non-blocking concern:
|
||||
- Existing disabled-surface test only clears all enabled surfaces and does not cover mixed-surface partial enablement。
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo fmt --check`: passed。
|
||||
- `git diff --check 5ec8bae9..HEAD`: passed。
|
||||
- `CARGO_TARGET_DIR=target/review-template cargo check --manifest-path resources/plugin/templates/rust-component-instance/Cargo.toml`: passed。
|
||||
- `cargo test -p manifest plugin -- --nocapture`: passed。
|
||||
- `cargo test -p pod plugin -- --nocapture`: passed。
|
||||
- `cargo test -p yoi plugin -- --nocapture`: passed。
|
||||
- `cargo check -p yoi`: passed。
|
||||
- `cargo check -p yoi-plugin-pdk`: passed。
|
||||
- `yoi ticket doctor`: passed。
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
|
||||
Worktree status at end: clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T14:59:25Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder r2-fix report received from `yoi-coder-00001KVJHYP4Q`.
|
||||
|
||||
New fix commit:
|
||||
- `79ca0f7f plugin: enforce enabled lifecycle surfaces`
|
||||
|
||||
Branch commits now:
|
||||
- `79ca0f7f plugin: enforce enabled lifecycle surfaces`
|
||||
- `870bcc76 plugin: fix instance lifecycle blockers`
|
||||
- `147a6005 plugin: add instance lifecycle surface`
|
||||
|
||||
Fix summary:
|
||||
- `crates/pod/src/feature/plugin.rs`:
|
||||
- Added per-surface `record.enabled_surfaces` gating for Plugin descriptor construction。
|
||||
- Filtered install loops so Tool / Service / Ingress setup only processes selected surfaces。
|
||||
- Changed install denial/exposure accounting to consider selected surfaces only。
|
||||
- Added runtime dispatch guards so unselected Tool/Ingress surfaces cannot be used even if grants exist。
|
||||
- Added focused tests for mixed-surface partial enablement:
|
||||
- Service selected, Tool declared but not selected: Service installs, Tool does not register, missing Tool grants do not fail install。
|
||||
- Tool selected, Service/Ingress declared but not selected: no Service descriptor/install exposure, Ingress dispatch denied even with grants。
|
||||
- Kept previous lifecycle/status/ingress retention tests passing。
|
||||
|
||||
Coder validation reported as passing:
|
||||
- `cargo test -p manifest plugin -- --nocapture`
|
||||
- `cargo test -p pod plugin -- --nocapture`
|
||||
- `cargo test -p yoi plugin -- --nocapture`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo check -p yoi-plugin-pdk`
|
||||
- `CARGO_TARGET_DIR=target/review-template cargo check --manifest-path resources/plugin/templates/rust-component-instance/Cargo.toml`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check 5ec8bae9..HEAD`
|
||||
- `yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Orchestrator evidence checked before r3 review:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `79ca0f7f`。
|
||||
- Fix diff from `870bcc76..HEAD` touches only `crates/pod/src/feature/plugin.rs`, about 190 insertions / 97 deletions。
|
||||
- Overall `git diff --check 5ec8bae9..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Route back to Reviewer for r3 verification of per-surface enabled selection enforcement and final acceptance criteria。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVJHYP4Q-r1 at: 2026-06-20T15:04:08Z status: request_changes -->
|
||||
|
||||
## Review: request changes
|
||||
|
||||
Verdict: `request_changes`
|
||||
|
||||
確認範囲:
|
||||
- Ticket authority / Orchestrator IntentPacket。
|
||||
- r3 fix diff: `870bcc76..79ca0f7f`。
|
||||
- Relevant overall implementation shape: `5ec8bae9..79ca0f7f`。
|
||||
- 主な対象:
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
- `crates/yoi/src/plugin_cli.rs`
|
||||
- `crates/manifest/src/plugin.rs`
|
||||
- `crates/plugin-pdk/src/lib.rs`
|
||||
- `resources/plugin/templates/rust-component-instance/*`
|
||||
- `resources/plugin/wit/yoi-plugin-instance-v1.wit`
|
||||
|
||||
Previous r2 blocker verification:
|
||||
- Runtime/registration side is mostly fixed。
|
||||
- Descriptor construction is gated by `record.enabled_surfaces`。
|
||||
- Install loops now gate Service / Ingress / Tool setup independently。
|
||||
- Install denial accounting counts selected exposed surfaces rather than only Tools。
|
||||
- Runtime dispatch guards exist for unselected Tool/Ingress use。
|
||||
- Tests cover requested selected/unselected mixed-surface cases。
|
||||
|
||||
Blocking issue:
|
||||
|
||||
1. Static inspection / `yoi plugin list/show` still evaluates unselected surfaces and can mark valid partial-surface enablement as rejected。
|
||||
- Runtime install now filters enabled surfaces, but static inspection still treats all declared manifest Tool / Service / Ingress surfaces as active eligibility requirements。
|
||||
- `PluginStaticInspection::statically_eligible()` requires all inspected tools, services, and ingresses to be eligible。
|
||||
- `inspect_resolved_plugin_static()` still iterates over all declared manifest tools/services/ingresses rather than selected/enabled surfaces。
|
||||
- `yoi plugin list/show` uses this unfiltered static inspection for resolved items。
|
||||
- Status calculation can classify the resolved item as `rejected` when no usable Tool exists and `static_eligible` is false。
|
||||
|
||||
Concrete failure mode:
|
||||
- Mixed package declares Tool + Service。
|
||||
- Enablement selects only Service。
|
||||
- Service grants are present; Tool grants are absent because Tool is not selected。
|
||||
- Runtime install correctly ignores unselected Tool。
|
||||
- Static inspection still checks unselected Tool and records missing Tool grant diagnostics, so `static_eligible = false`。
|
||||
- For service-only selection with no usable Tool, `yoi plugin list/show` can report the resolved plugin as `rejected` even though selected Service surface is valid/installable。
|
||||
|
||||
Why this blocks approval:
|
||||
- Ticket requires Tool / Service / Ingress grants to be independent。
|
||||
- Unselected surfaces must not block selected surfaces。
|
||||
- Ticket also requires `yoi plugin check/list/show` to report legacy vs instance-capable/rejected surfaces accurately。
|
||||
- Runtime path is fixed, but inspection/status can still be blocked by unselected surfaces, creating authority/reporting mismatch。
|
||||
|
||||
Required fix:
|
||||
- Make resolved static inspection eligibility account for `record.enabled_surfaces`。
|
||||
- Alternatively, separate declared-surface inspection from enabled-surface inspection and compute resolved `list/show` status from enabled surfaces only。
|
||||
- Keep `plugin check` free to inspect full package declaration if appropriate, but `plugin list/show` for a resolved enablement must not mark selected Service/Ingress invalid because unrelated unselected Tool grants are absent。
|
||||
- Add focused `yoi plugin` tests for mixed-surface partial enablement status/reporting。
|
||||
|
||||
Non-blocking concern:
|
||||
- Runtime install tests now cover partial enablement, but focused `yoi plugin list/show` tests are still missing and should be added with the fix。
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo fmt --check`: passed。
|
||||
- `git diff --check 5ec8bae9..HEAD`: passed。
|
||||
- Template cargo-check: passed。
|
||||
- `cargo test -p manifest plugin -- --nocapture`: passed。
|
||||
- `cargo test -p pod plugin -- --nocapture`: passed。
|
||||
- `cargo test -p yoi plugin -- --nocapture`: passed。
|
||||
- `cargo check -p yoi`: passed。
|
||||
- `cargo check -p yoi-plugin-pdk`: passed。
|
||||
- `yoi ticket doctor`: passed。
|
||||
- `nix build .#yoi --no-link`: passed。
|
||||
|
||||
Worktree status at end: clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T15:13:56Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder r3-fix report received from `yoi-coder-00001KVJHYP4Q`.
|
||||
|
||||
New fix commit:
|
||||
- `627c8f36 plugin: filter static enabled surfaces`
|
||||
|
||||
Branch commits now:
|
||||
- `627c8f36 plugin: filter static enabled surfaces`
|
||||
- `79ca0f7f plugin: enforce enabled lifecycle surfaces`
|
||||
- `870bcc76 plugin: fix instance lifecycle blockers`
|
||||
- `147a6005 plugin: add instance lifecycle surface`
|
||||
|
||||
Fix summary:
|
||||
- `crates/pod/src/feature/plugin.rs`:
|
||||
- `inspect_resolved_plugin_static()` now filters Tool / Service / Ingress inspection by `record.enabled_surfaces`。
|
||||
- `PluginStaticInspection::statically_eligible()` therefore reflects enabled/selected surfaces only for resolved Plugin records。
|
||||
- `plugin check` remains full-declaration oriented because check-time inspection uses all declared manifest surfaces as enabled。
|
||||
- `crates/yoi/src/plugin_cli.rs`:
|
||||
- Resolved `plugin list/show` diagnostics now use filtered static inspection。
|
||||
- Added focused CLI inspection test for mixed Tool+Service package where only Service is enabled and Tool grants are absent:
|
||||
- status remains `active`。
|
||||
- static eligibility remains true。
|
||||
- unselected Tool is not listed/reported。
|
||||
- unselected Tool missing grants do not produce diagnostics/rejection。
|
||||
|
||||
Coder validation reported as passing:
|
||||
- `cargo test -p manifest plugin -- --nocapture`
|
||||
- `cargo test -p pod plugin -- --nocapture`
|
||||
- `cargo test -p yoi plugin -- --nocapture`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo check -p yoi-plugin-pdk`
|
||||
- `CARGO_TARGET_DIR=target/review-template cargo check --manifest-path resources/plugin/templates/rust-component-instance/Cargo.toml`
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check 5ec8bae9..HEAD`
|
||||
- `yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Orchestrator evidence checked before r4 review:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `627c8f36`。
|
||||
- Fix diff from `79ca0f7f..HEAD` touches `crates/pod/src/feature/plugin.rs` and `crates/yoi/src/plugin_cli.rs`, about 209 insertions / 76 deletions。
|
||||
- Overall `git diff --check 5ec8bae9..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Route back to Reviewer for r4 verification of static inspection / `plugin list/show` partial-surface reporting and final acceptance criteria。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: review author: yoi-reviewer-00001KVJHYP4Q-r1 at: 2026-06-20T15:18:34Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Verdict: `approve`
|
||||
|
||||
確認範囲:
|
||||
- Ticket contract:
|
||||
- `.yoi/tickets/00001KVJHYP4Q/item.md`
|
||||
- `.yoi/tickets/00001KVJHYP4Q/thread.md`
|
||||
- r4 fix diff: `79ca0f7f..627c8f36`
|
||||
- overall implementation shape: `5ec8bae9..627c8f36`
|
||||
- Focused files:
|
||||
- `crates/pod/src/feature/plugin.rs`
|
||||
- `crates/yoi/src/plugin_cli.rs`
|
||||
- `crates/manifest/src/plugin.rs`
|
||||
- `crates/plugin-pdk/src/lib.rs`
|
||||
- `resources/plugin/templates/rust-component-instance/*`
|
||||
- `resources/plugin/wit/yoi-plugin-instance-v1.wit`
|
||||
|
||||
r3 blocker verification:
|
||||
- Previous static-inspection/list-show blocker is resolved。
|
||||
- `inspect_resolved_plugin_static()` now filters Tool / Service / Ingress eligibility by `record.enabled_surfaces`。
|
||||
- `PluginStaticInspection::statically_eligible()` now operates over filtered/selected surface vectors for resolved records。
|
||||
- `plugin check` remains full-declaration oriented because check-time inspection constructs the record with all declared manifest surfaces enabled。
|
||||
- Resolved `plugin list/show` diagnostics now use filtered static inspection and include selected Service/Ingress diagnostics。
|
||||
- Focused CLI test `service_only_enablement_ignores_unselected_tool_static_grants` confirms a mixed Tool+Service package with only Service enabled remains `active`, is statically eligible, does not report the unselected Tool, and does not emit missing Tool grant diagnostics。
|
||||
|
||||
Earlier blocker re-check:
|
||||
- Instance PDK/template shape, lifecycle returned JSON/status handling, Service/Ingress instance retention, installed-path ingress dispatch, and per-surface runtime guards remain present at a high level。
|
||||
|
||||
Blocking issues:
|
||||
- None found。
|
||||
|
||||
Non-blocking follow-ups:
|
||||
- None requiring this Ticket to remain blocked。
|
||||
|
||||
Reviewer validation passed:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check 5ec8bae9..HEAD`
|
||||
- `CARGO_TARGET_DIR=target/review-template cargo check --manifest-path resources/plugin/templates/rust-component-instance/Cargo.toml`
|
||||
- `cargo test -p manifest plugin -- --nocapture`
|
||||
- `cargo test -p pod plugin -- --nocapture`
|
||||
- `cargo test -p yoi plugin -- --nocapture`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo check -p yoi-plugin-pdk`
|
||||
- `/home/hare/Projects/yoi/target/debug/yoi ticket doctor`
|
||||
- `nix build .#yoi --no-link`
|
||||
|
||||
Template check generated `resources/plugin/templates/rust-component-instance/Cargo.lock`; reviewer removed it after validation。
|
||||
|
||||
Worktree status:
|
||||
- Clean at commit `627c8f36` on `impl/00001KVJHYP4Q-plugin-instance-lifecycle`。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T15:22:49Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Reviewer r4 approvalを受けて、実装ブランチを Orchestrator worktree の `orchestration` branch に統合した。
|
||||
|
||||
統合:
|
||||
- Merge commit: `43c9216e merge: plugin instance lifecycle surface`
|
||||
- Merged branch: `impl/00001KVJHYP4Q-plugin-instance-lifecycle`
|
||||
- Implementation commits:
|
||||
- `147a6005 plugin: add instance lifecycle surface`
|
||||
- `870bcc76 plugin: fix instance lifecycle blockers`
|
||||
- `79ca0f7f plugin: enforce enabled lifecycle surfaces`
|
||||
- `627c8f36 plugin: filter static enabled surfaces`
|
||||
|
||||
Orchestrator worktree validation:
|
||||
- `cargo fmt --check`: passed
|
||||
- `git diff --check HEAD^1..HEAD`: passed
|
||||
- `cargo test -p manifest plugin -- --nocapture`: passed
|
||||
- `cargo test -p pod plugin -- --nocapture`: passed
|
||||
- `cargo test -p yoi plugin -- --nocapture`: passed
|
||||
- `cargo check -p yoi`: passed
|
||||
- `cargo check -p yoi-plugin-pdk`: passed
|
||||
- `CARGO_TARGET_DIR=target/review-template cargo check --manifest-path resources/plugin/templates/rust-component-instance/Cargo.toml`: passed
|
||||
- `/home/hare/Projects/yoi/target/debug/yoi ticket doctor`: passed
|
||||
- `nix build .#yoi --no-link`: passed
|
||||
|
||||
Template validation generated `resources/plugin/templates/rust-component-instance/Cargo.lock`; it was removed after validation. Final Orchestrator worktree status is clean。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T15:22:53Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Reviewer approval、Orchestrator worktree への統合、必要な検証が完了したため `done` に遷移する。実装は Plugin instance lifecycle surface、Service/Ingress declarations and grants、PDK/template、static/runtime inspection、legacy compatibility、および focused tests を含む。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-20T15:23:11Z from: done to: closed reason: closed field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を closed にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-20T15:23:11Z status: closed -->
|
||||
|
||||
## 完了
|
||||
|
||||
Plugin Service/Ingress component lifecycle surface を実装し、Orchestrator worktree の `orchestration` branch に統合した。
|
||||
|
||||
主な成果:
|
||||
- Pod plugin feature に host-managed `PluginInstanceRegistry` / instance handle 境界を追加し、Tool dispatch を instance 経由に変更。
|
||||
- New instance-capable component world `yoi:plugin/instance@1.0.0` と WIT resource を追加。
|
||||
- `yoi-plugin-pdk` と Rust component template に instance-oriented authoring support を追加。
|
||||
- Existing component Tool world / raw wasm Tool runtime を instance registry compatibility path に維持。
|
||||
- Manifest/static validation に Service / Ingress declarations と per-surface grant validation を追加。
|
||||
- Service lifecycle/status/diagnostics と bounded in-process ingress dispatch path を実装。
|
||||
- Tool / Service / Ingress enabled-surface filtering を runtime install, dispatch guard, and resolved static inspection / `yoi plugin list/show` に適用。
|
||||
- `plugin check` は package declaration inspection、resolved `plugin list/show` は selected/enabled surfaces に基づく reporting に分離。
|
||||
- Focused tests added for manifest validation, legacy Tool compatibility, instance state persistence, ingress dispatch, Service/Ingress grant denial, failure diagnostics, and partial enabled-surface static reporting。
|
||||
|
||||
統合・検証:
|
||||
- Merge commit: `43c9216e merge: plugin instance lifecycle surface`
|
||||
- Implementation commits: `147a6005`, `870bcc76`, `79ca0f7f`, `627c8f36`
|
||||
- Reviewer final verdict: approve
|
||||
- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p manifest plugin -- --nocapture`, `cargo test -p pod plugin -- --nocapture`, `cargo test -p yoi plugin -- --nocapture`, `cargo check -p yoi`, `cargo check -p yoi-plugin-pdk`, template cargo-check, `yoi ticket doctor`, and `nix build .#yoi --no-link`。
|
||||
|
||||
範囲外:
|
||||
- Discord Bridge 本体、public registry/install/update/signature tooling、arbitrary Plugin UI channel、hidden context injection、Service/Ingress による model-visible Tool schema mutation は実装していない。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T15:23:35Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Post-close cleanup completed。
|
||||
|
||||
- Stopped child Pods and reclaimed scope:
|
||||
- `yoi-coder-00001KVJHYP4Q`
|
||||
- `yoi-reviewer-00001KVJHYP4Q-r1`
|
||||
- Removed implementation worktree:
|
||||
- `/home/hare/Projects/yoi/.worktree/00001KVJHYP4Q-plugin-instance-lifecycle`
|
||||
- Deleted implementation branch:
|
||||
- `impl/00001KVJHYP4Q-plugin-instance-lifecycle`
|
||||
- Orchestrator worktree remains clean on `orchestration` at `bc484338`。
|
||||
|
||||
Root/original workspace was not used for merge/validation/cleanup operations beyond observing the worktree list output from the Orchestrator worktree command。
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
{"id":"orch-plan-20260620-133128-1","ticket_id":"00001KVJKHAFE","kind":"accepted_plan","accepted_plan":{"summary":"`yoi mcp` CLI namespaceを追加し、resolved MCP server config、trust policy、static diagnostics、provider-discovered tools/resources/prompts eligibility、live/unavailable stateを bounded human/JSON outputで inspectionできるようにする。CLIはMCP serverを起動せず、tools/call/resources/read/prompts/getを直接実行しない。","branch":"impl/00001KVJKHAFE-mcp-cli-inspection","worktree":"/home/hare/Projects/yoi/.worktree/00001KVJKHAFE-mcp-cli-inspection","role_plan":"Orchestrator は Plugin instance lifecycle work と並行して専用 implementation worktree `.worktree/00001KVJKHAFE-mcp-cli-inspection` を作成し、Coder をその child worktree への narrow write scope で起動する。Coder 実装後、Reviewer が read-only inspection境界、no server spawn/no tool execution/no content fetch、secret/content redaction、static-vs-live status表現、JSON shape、help/test/Nixを確認する。"},"author":"yoi-orchestrator","at":"2026-06-20T13:31:28Z"}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
title: 'MCP: add yoi CLI inspection commands'
|
||||
state: 'queued'
|
||||
state: 'closed'
|
||||
created_at: '2026-06-20T13:29:16Z'
|
||||
updated_at: '2026-06-20T13:31:00Z'
|
||||
updated_at: '2026-06-20T13:56:39Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-06-20T13:31:00Z'
|
||||
|
|
|
|||
44
.yoi/tickets/00001KVJKHAFE/resolution.md
Normal file
44
.yoi/tickets/00001KVJKHAFE/resolution.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
## Resolution
|
||||
|
||||
`00001KVJKHAFE` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- `yoi mcp` CLI namespace を追加しました。
|
||||
- Read-only inspection commands を追加しました。
|
||||
- `yoi mcp list`
|
||||
- `yoi mcp show <server>`
|
||||
- `yoi mcp tools [<server>]`
|
||||
- `yoi mcp resources [<server>]`
|
||||
- `yoi mcp prompts [<server>]`
|
||||
- Human-readable output と `--json` output を追加しました。
|
||||
- Inspection は static/resolved config のみを扱い、MCP server process を起動しません。
|
||||
- `tools/call`, `resources/read`, `prompts/get` は実行しません。
|
||||
- Live/provider-discovered state は `not_live` / `unavailable` と明示します。
|
||||
- Env values, secret refs, env refs, args, resource content, prompt content は redacted/omitted します。
|
||||
- Resource/prompt operation eligibility は content fetch なしで報告します。
|
||||
- MCP namespace は Plugin CLI namespace と分離したままです。
|
||||
|
||||
主な commit:
|
||||
- `c91f5fc9 mcp: add cli inspection`
|
||||
- `5e0b023a merge: mcp cli inspection`
|
||||
|
||||
Review:
|
||||
- r1 は `approve`。
|
||||
- Reviewer は read-only boundary、no process start、no tools/resource/prompt content fetch、static-vs-live unavailable state、secret/content redaction、MCP namespace separation、help/tests を確認しました。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p yoi mcp`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo run -q -p yoi -- --help` + MCP command grep
|
||||
- `TicketDoctor`: 0 errors
|
||||
|
||||
Known unrelated note:
|
||||
- `TicketDoctor` は既存 Ticket の warning 4 件を返しましたが、この Ticket の変更とは無関係です。
|
||||
|
||||
Nix validation:
|
||||
- Not run because no dependency/package/source-filter files changed。
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-xrqves.log`
|
||||
|
|
@ -30,4 +30,302 @@ Marked ready by `yoi ticket state`.
|
|||
Ticket を `workspace-panel` が queued にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: decision author: yoi-orchestrator at: 2026-06-20T13:31:52Z -->
|
||||
|
||||
## Decision
|
||||
|
||||
Routing decision: implementation_ready_parallel
|
||||
|
||||
Reason:
|
||||
- Workspace Dashboard Queue により、この Ticket は Orchestrator routing 対象として明示許可された。
|
||||
- User standing directive: blocker が無いものは並列実行する。現在 `00001KVJHYP4Q` Plugin instance lifecycle Coder が inprogress だが、この Ticket は MCP CLI inspection namespaceであり直接 dependency/conflict はない。Potential shared CLI parser edits are manageable in separate worktree and will be resolved at merge/review boundary。
|
||||
- Ticket body は `yoi mcp` namespace、list/show/tools/resources/prompts commands、human/JSON output、workspace/profile resolution、read-only inspection boundary、static-vs-live availability、secret/content redaction、acceptance testsを実装可能な粒度で定義している。
|
||||
- 未解決 blocker relation はない。Relations are `related` context to completed MCP foundation Tickets。
|
||||
- Orchestrator worktree は clean、matching branch/worktree はなし。
|
||||
- Risk domain は CLI / MCP / inspection / secrets / read-only boundary だが、Ticket は no server process start、no tool execution、no resources/prompts content fetch、secrets/content non-printing、bounded outputを明示している。bounded context check 後も implementation 前に必要な追加 human decision は見つからなかった。
|
||||
|
||||
Evidence checked:
|
||||
- Ticket `00001KVJKHAFE` body / thread / relations / artifacts。
|
||||
- `TicketRelationQuery(00001KVJKHAFE)`: only non-blocking `related` records to completed MCP config/lifecycle/tools/resources/list_changed Tickets。
|
||||
- `TicketOrchestrationPlanQuery(00001KVJKHAFE)`: no previous plan records; accepted plan recorded now。
|
||||
- Workspace state:
|
||||
- Orchestrator worktree clean at `142fdffb`。
|
||||
- queued: this Ticket only。
|
||||
- inprogress: `00001KVJHYP4Q` Plugin instance lifecycle。
|
||||
- visible spawned child: `yoi-coder-00001KVJHYP4Q` running。
|
||||
- no matching MCP CLI branch/worktree。
|
||||
|
||||
IntentPacket:
|
||||
|
||||
Intent:
|
||||
- Add read-only `yoi mcp` CLI inspection namespace for configured/resolved MCP servers and provider-discovered tools/resources/prompts eligibility。
|
||||
- Provide bounded human-readable and JSON reports without bypassing runtime Tool/resource/prompt paths。
|
||||
|
||||
Binding decisions / invariants:
|
||||
- CLI inspection must not start MCP server processes。
|
||||
- CLI inspection must not call MCP tools or fetch resource/prompt content。
|
||||
- Static config/resolution and live Pod state must be distinguished explicitly。
|
||||
- If live state is unavailable/unimplemented, output must say `not live` / `unavailable`, not silently stale。
|
||||
- Secrets, resolved secret/env values, resource contents, and prompt full text must not be printed in human or JSON output。
|
||||
- External server descriptions/schemas/annotations are untrusted and bounded/truncated。
|
||||
- Keep MCP separate from Plugin CLI namespace。
|
||||
- No Streamable HTTP/OAuth/sampling/elicitation/install/update/distribution implementation。
|
||||
|
||||
Requirements / acceptance criteria:
|
||||
- `yoi --help` shows MCP CLI namespace。
|
||||
- `yoi mcp list --json` returns resolved MCP server bounded structured report。
|
||||
- `yoi mcp show <server> --json` returns server identity, transport kind, trust policy summary, capabilities summary, diagnostics。
|
||||
- `yoi mcp tools [<server>] --json` returns Yoi stable tool name, MCP server/tool identity, schema availability, registration status/diagnostics。
|
||||
- `yoi mcp resources [<server>] --json` and `yoi mcp prompts [<server>] --json` return summaries/eligibility without content fetch。
|
||||
- Human-readable output distinguishes empty/missing/invalid/unavailable。
|
||||
- Focused CLI tests cover list/show/tools/resources/prompts, missing server, invalid config, JSON output, and secret/content non-leakage。
|
||||
- Validation includes fmt, focused tests, check, diff, TicketDoctor, Nix build。
|
||||
|
||||
Next action:
|
||||
- Record `queued -> inprogress` and commit Ticket records before creating implementation worktree and spawning Coder。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T13:32:15Z from: queued to: inprogress reason: orchestrator_acceptance_parallel_mcp_cli_inspection field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket body/thread, relation metadata, Orchestrator worktree state, visible Pods, existing branch/worktree, and bounded MCP CLI inspection context were checked. There is no unresolved blocking dependency, no direct conflict with current Plugin instance lifecycle work, no matching worktree/branch, and no missing planning decision. Accepting this queued Ticket for parallel implementation before worktree/Pod side effects.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T13:33:06Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Implementation orchestration start:
|
||||
- Acceptance records were committed on Orchestrator branch: `12d7e69f ticket: accept mcp cli inspection`。
|
||||
- Created implementation worktree `/home/hare/Projects/yoi/.worktree/00001KVJKHAFE-mcp-cli-inspection` on branch `impl/00001KVJKHAFE-mcp-cli-inspection` at `12d7e69f`。
|
||||
- Spawned Coder Pod `yoi-coder-00001KVJKHAFE` 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, MCP server process startup, tools/call/resource/prompt content fetch, Plugin CLI namespace mixing, 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-20T13:48:38Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Coder implementation report received from `yoi-coder-00001KVJKHAFE`.
|
||||
|
||||
Implementation commit:
|
||||
- `c91f5fc9 mcp: add cli inspection`
|
||||
|
||||
Changed areas reported:
|
||||
- Added `crates/yoi/src/mcp_cli.rs`:
|
||||
- Read-only `yoi mcp` inspection reports。
|
||||
- Commands: `list`, `show`, `tools`, `resources`, `prompts`。
|
||||
- Human and `--json` output。
|
||||
- Static resolved config inspection only; no MCP server processes are started。
|
||||
- Live/provider-discovered state is explicitly reported as `not_live` / `unavailable`。
|
||||
- Env values, secret refs, env refs, args, resource content, and prompt content are redacted/omitted。
|
||||
- Resource/prompt operation eligibility is reported without content fetch。
|
||||
- Updated `crates/yoi/src/main.rs`:
|
||||
- Added `mcp` namespace to parser, dispatcher, help output, and parser tests。
|
||||
|
||||
Coder validation reported:
|
||||
- `cargo test -p yoi mcp`: passed。
|
||||
- `cargo check -p yoi`: passed。
|
||||
- `cargo fmt --check`: passed。
|
||||
- `git diff --check`: passed。
|
||||
- `cargo run -p yoi -- --help | grep -E 'yoi mcp (list|show|tools)'`: passed; help shows MCP namespace。
|
||||
- `yoi ticket doctor`: passed。
|
||||
|
||||
Package impact:
|
||||
- No dependency, lockfile, source-filter, or `package.nix` changes。
|
||||
- Nix not run because packaging inputs were not changed。
|
||||
|
||||
Known risks / deferrals:
|
||||
- Live Pod MCP state is not integrated; provider-discovered tools/resources/prompts are therefore reported as `not_live` / `unavailable` rather than stale or fabricated。
|
||||
- `tools` cannot list Yoi stable provider tool names without live discovery; JSON schema includes future item shape, but item list is empty in static mode。
|
||||
- Touched `crates/yoi/src/main.rs` CLI parser/help, so there is possible narrow conflict risk with parallel Plugin Service/Ingress work. No Plugin Service/Ingress files were touched。
|
||||
|
||||
Orchestrator evidence checked before review dispatch:
|
||||
- Implementation worktree is clean。
|
||||
- HEAD is `c91f5fc9`。
|
||||
- Diff from acceptance `12d7e69f..HEAD` is one implementation commit touching 2 files, about 1438 insertions / 1 deletion。
|
||||
- `git diff --check 12d7e69f..HEAD` produced no diagnostics。
|
||||
|
||||
Next action:
|
||||
- Dispatch Reviewer for r1 review against Ticket requirements, with focus on read-only inspection boundary, no process start/no tools-call/no resource-prompt content fetch, static vs live unavailable honesty, redaction, JSON/human shape, help/tests, and conflict risk。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: plan author: yoi-orchestrator at: 2026-06-20T13:49:28Z -->
|
||||
|
||||
## Plan
|
||||
|
||||
Review dispatch:
|
||||
- Spawned Reviewer Pod `yoi-reviewer-00001KVJKHAFE-r1` against implementation branch `impl/00001KVJKHAFE-mcp-cli-inspection`。
|
||||
- Review target commit: `c91f5fc9 mcp: add cli inspection`。
|
||||
- Review baseline: `12d7e69f`。
|
||||
- Reviewer task focuses on read-only inspection boundary, no process start/no tools-call/no resource/prompt content fetch, static vs live unavailable honesty, secret/content redaction, JSON/human report shape, namespace separation from Plugin CLI, help/tests, scope creep absence, and parallel CLI merge conflict risk。
|
||||
- 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-00001KVJKHAFE-r1 at: 2026-06-20T13:54:05Z status: approve -->
|
||||
|
||||
## Review: approve
|
||||
|
||||
Verdict: `approve`
|
||||
|
||||
確認範囲:
|
||||
- Ticket contract / Orchestrator IntentPacket。
|
||||
- Implementation diff: `12d7e69f..c91f5fc9`。
|
||||
- 主な対象:
|
||||
- `crates/yoi/src/mcp_cli.rs`
|
||||
- `crates/yoi/src/main.rs`
|
||||
- MCP CLI path の process spawn / MCP client / tool-call / resource-read / prompt-get behavior を検索。
|
||||
- `crates/yoi/src/main.rs` の merge conflict risk も確認。
|
||||
|
||||
Blocking issues: none。
|
||||
|
||||
Approval evidence:
|
||||
- MCP CLI は static/read-only に留まっている。
|
||||
- `mcp_cli.rs` は manifest/profile config types を使い、MCP runtime/client path を import していない。
|
||||
- `inspect_static_config` は Profile/config を resolve して static-only diagnostics を report し、stdio lifecycle/client API を呼ばない。
|
||||
- CLI implementation から `std::process::Command`, `Command::new`, `spawn`, MCP lifecycle/client calls, ToolRegistry calls, `tools/call` path は見つからない。
|
||||
- Resource/prompt content fetch はない。
|
||||
- `resources/read` / `prompts/get` は operation label / stable name としてだけ出ており、`registration_status: "not_live"`, `content_state: "not_fetched"` を返す。
|
||||
- Static vs live state is explicit。
|
||||
- JSON includes `inspection_mode` booleans: `starts_servers: false`, `calls_tools: false`, `fetches_resource_or_prompt_content: false`。
|
||||
- Reports include `live_state.status = "not_live"`; human output says `live: not live / unavailable`。
|
||||
- Secrets/args are redacted。
|
||||
- stdio args are summarized as `arg_count` plus `args_redacted: true`。
|
||||
- env config is summarized by counts/kinds with `values_redacted: true`; env literal values, secret refs, and env refs are not emitted。
|
||||
- Tests cover non-leakage for arg secret, literal env value, secret ref, and env ref。
|
||||
- MCP namespace remains separate。
|
||||
- `main.rs` adds distinct top-level `mcp` parser/dispatcher path; Plugin CLI is not mixed beyond adjacency in top-level help/parsing。
|
||||
- Top-level `yoi --help` shows MCP namespace and commands。
|
||||
|
||||
Non-blocking concerns / follow-ups:
|
||||
- Implementation is intentionally static-only. It does not surface real live provider-discovered tools/resources/prompts or live `notifications/*/list_changed` restart/reinitialize diagnostics. It clearly reports unavailable/not live, which matches this implementation boundary; future live-state inspection can extend it。
|
||||
- `yoi mcp --help` / subcommand help may flow through `ParseError` and error exit rather than normal help exit. Top-level `yoi --help` satisfies the stated acceptance criterion, so this is not blocking。
|
||||
|
||||
Reviewer validation:
|
||||
- `cargo fmt --check`: passed。
|
||||
- `git diff --check 12d7e69f..HEAD`: passed。
|
||||
- `cargo test -p yoi mcp`: passed, 8 tests。
|
||||
- `cargo check -p yoi`: passed。
|
||||
- `cargo run -p yoi -- --help`: passed; output includes `yoi mcp list`, `show`, and `tools|resources|prompts`。
|
||||
- `cargo run -p yoi -- ticket doctor`: passed, `doctor: ok`。
|
||||
- Conflict-risk check: `main.rs` merge risk low; trivial merge check produced no conflict markers。
|
||||
- `nix build .#yoi --no-link` not run because no package/source-filter concern was found。
|
||||
|
||||
Worktree status at review end: source tree clean; ignored `target/` only。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-20T13:56:02Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Merge and final validation report:
|
||||
|
||||
Merged implementation branch into Orchestrator branch:
|
||||
- Merge commit: `5e0b023a merge: mcp cli inspection`
|
||||
- Implementation commit included:
|
||||
- `c91f5fc9 mcp: add cli inspection`
|
||||
|
||||
Reviewer outcome:
|
||||
- r1 approved with no blocking issues。
|
||||
- Reviewer confirmed CLI inspection is static/read-only, does not start MCP server processes, does not call tools/resources/prompts, distinguishes static vs live unavailable state, redacts/omits secrets/env/args/content, keeps MCP namespace separate from Plugin CLI, and has focused tests。
|
||||
|
||||
Orchestrator validation after merge passed:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p yoi mcp`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo run -q -p yoi -- --help` with targeted grep for MCP commands。
|
||||
- `TicketDoctor`: 0 errors, 4 existing warnings unrelated to this Ticket。
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-xrqves.log`
|
||||
|
||||
Nix validation:
|
||||
- Not run because this Ticket changed only Rust source under `crates/yoi` and no dependency/package/source-filter files changed。
|
||||
|
||||
Final state:
|
||||
- Orchestrator worktree clean at `5e0b023a` after validation。
|
||||
- Implementation worktree remains available for cleanup after Ticket completion records are committed。
|
||||
- Plugin instance lifecycle Coder continues in parallel。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-20T13:56:11Z from: inprogress to: done reason: merged_reviewed_validated field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Implementation was merged into Orchestrator branch at `5e0b023a`, review approved, and final Orchestrator validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p yoi mcp`, `cargo check -p yoi`, top-level help grep for MCP commands, and `TicketDoctor` with 0 errors.
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: hare at: 2026-06-20T13:56:39Z from: done to: closed reason: closed field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
Ticket を closed にしました。
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: close author: hare at: 2026-06-20T13:56:39Z status: closed -->
|
||||
|
||||
## 完了
|
||||
|
||||
## Resolution
|
||||
|
||||
`00001KVJKHAFE` を完了しました。
|
||||
|
||||
実装内容:
|
||||
- `yoi mcp` CLI namespace を追加しました。
|
||||
- Read-only inspection commands を追加しました。
|
||||
- `yoi mcp list`
|
||||
- `yoi mcp show <server>`
|
||||
- `yoi mcp tools [<server>]`
|
||||
- `yoi mcp resources [<server>]`
|
||||
- `yoi mcp prompts [<server>]`
|
||||
- Human-readable output と `--json` output を追加しました。
|
||||
- Inspection は static/resolved config のみを扱い、MCP server process を起動しません。
|
||||
- `tools/call`, `resources/read`, `prompts/get` は実行しません。
|
||||
- Live/provider-discovered state は `not_live` / `unavailable` と明示します。
|
||||
- Env values, secret refs, env refs, args, resource content, prompt content は redacted/omitted します。
|
||||
- Resource/prompt operation eligibility は content fetch なしで報告します。
|
||||
- MCP namespace は Plugin CLI namespace と分離したままです。
|
||||
|
||||
主な commit:
|
||||
- `c91f5fc9 mcp: add cli inspection`
|
||||
- `5e0b023a merge: mcp cli inspection`
|
||||
|
||||
Review:
|
||||
- r1 は `approve`。
|
||||
- Reviewer は read-only boundary、no process start、no tools/resource/prompt content fetch、static-vs-live unavailable state、secret/content redaction、MCP namespace separation、help/tests を確認しました。
|
||||
|
||||
最終 validation:
|
||||
- `cargo fmt --check`
|
||||
- `git diff --check HEAD^1..HEAD`
|
||||
- `cargo test -p yoi mcp`
|
||||
- `cargo check -p yoi`
|
||||
- `cargo run -q -p yoi -- --help` + MCP command grep
|
||||
- `TicketDoctor`: 0 errors
|
||||
|
||||
Known unrelated note:
|
||||
- `TicketDoctor` は既存 Ticket の warning 4 件を返しましたが、この Ticket の変更とは無関係です。
|
||||
|
||||
Nix validation:
|
||||
- Not run because no dependency/package/source-filter files changed。
|
||||
|
||||
Validation log:
|
||||
- `/run/user/1000/yoi/yoi-orchestrator/bash-output/bash-xrqves.log`
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -57,6 +57,40 @@ pub const RUST_COMPONENT_TOOL_TEMPLATE: &[PluginTemplateResource] = &[
|
|||
},
|
||||
];
|
||||
|
||||
/// Embedded starter template for Rust Component Model instance Plugins.
|
||||
pub const RUST_COMPONENT_INSTANCE_TEMPLATE: &[PluginTemplateResource] = &[
|
||||
PluginTemplateResource {
|
||||
path: "Cargo.toml",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-instance/Cargo.toml"
|
||||
),
|
||||
},
|
||||
PluginTemplateResource {
|
||||
path: "src/lib.rs",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-instance/src/lib.rs"
|
||||
),
|
||||
},
|
||||
PluginTemplateResource {
|
||||
path: "plugin.toml",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-instance/plugin.toml"
|
||||
),
|
||||
},
|
||||
PluginTemplateResource {
|
||||
path: "plugin.component.wasm",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-instance/plugin.component.wasm"
|
||||
),
|
||||
},
|
||||
PluginTemplateResource {
|
||||
path: "README.md",
|
||||
contents: include_str!(
|
||||
"../../../resources/plugin/templates/rust-component-instance/README.md"
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct PluginConfig {
|
||||
|
|
@ -170,6 +204,8 @@ pub enum PluginPermission {
|
|||
Surface { surface: PluginSurface },
|
||||
Tool { name: String },
|
||||
ToolNamespace { namespace: String },
|
||||
Service { name: String },
|
||||
Ingress { name: String },
|
||||
ExternalWrite,
|
||||
HostApi { api: PluginHostApi },
|
||||
}
|
||||
|
|
@ -249,6 +285,8 @@ impl PluginPermission {
|
|||
Self::Surface { surface } => format!("surfaces.{surface}"),
|
||||
Self::Tool { name } => format!("tool.{name}"),
|
||||
Self::ToolNamespace { namespace } => format!("tool_namespace.{namespace}"),
|
||||
Self::Service { name } => format!("service.{name}"),
|
||||
Self::Ingress { name } => format!("ingress.{name}"),
|
||||
Self::ExternalWrite => "external_write".to_string(),
|
||||
Self::HostApi { api } => format!("host_api.{api}"),
|
||||
}
|
||||
|
|
@ -268,6 +306,14 @@ impl PluginPermission {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn service(name: impl Into<String>) -> Self {
|
||||
Self::Service { name: name.into() }
|
||||
}
|
||||
|
||||
pub fn ingress(name: impl Into<String>) -> Self {
|
||||
Self::Ingress { name: name.into() }
|
||||
}
|
||||
|
||||
pub fn host_api(api: PluginHostApi) -> Self {
|
||||
Self::HostApi { api }
|
||||
}
|
||||
|
|
@ -382,7 +428,7 @@ pub enum PluginIdParseError {
|
|||
InvalidLocalId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginPackageManifest {
|
||||
pub schema_version: u32,
|
||||
|
|
@ -398,6 +444,10 @@ pub struct PluginPackageManifest {
|
|||
pub hooks: Vec<PluginHookManifest>,
|
||||
#[serde(default)]
|
||||
pub tools: Vec<PluginToolManifest>,
|
||||
#[serde(default)]
|
||||
pub services: Vec<PluginServiceManifest>,
|
||||
#[serde(default)]
|
||||
pub ingresses: Vec<PluginIngressManifest>,
|
||||
/// Permission requests declared by the package. These are requests only;
|
||||
/// enablement grants must match them before runtime surfaces are exposed.
|
||||
#[serde(default)]
|
||||
|
|
@ -413,6 +463,12 @@ impl PluginPackageManifest {
|
|||
if !self.tools.is_empty() {
|
||||
surfaces.insert(PluginSurface::Tool);
|
||||
}
|
||||
if !self.services.is_empty() {
|
||||
surfaces.insert(PluginSurface::Service);
|
||||
}
|
||||
if !self.ingresses.is_empty() {
|
||||
surfaces.insert(PluginSurface::Ingress);
|
||||
}
|
||||
if self.runtime.is_some() {
|
||||
surfaces.insert(PluginSurface::Wasm);
|
||||
}
|
||||
|
|
@ -429,6 +485,7 @@ pub const PLUGIN_RUNTIME_WASM_ABI: &str = "yoi-plugin-wasm-1";
|
|||
/// packages remain explicit `kind = "wasm"` plus `abi = "yoi-plugin-wasm-1"`.
|
||||
pub const PLUGIN_RUNTIME_COMPONENT_KIND: &str = "wasm-component";
|
||||
pub const PLUGIN_COMPONENT_TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0";
|
||||
pub const PLUGIN_COMPONENT_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
|
|
@ -464,6 +521,34 @@ pub struct PluginToolManifest {
|
|||
pub external_write: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginServiceManifest {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub lifecycle: String,
|
||||
#[serde(default)]
|
||||
pub status_schema: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub side_effects: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct PluginIngressManifest {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub event_kinds: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub input_schema: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub sources: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub side_effects: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PluginDiscoveryLimits {
|
||||
pub max_packages_per_store: usize,
|
||||
|
|
@ -514,7 +599,7 @@ impl PluginDiscoveryOptions {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DiscoveredPluginPackage {
|
||||
pub identity: SourceQualifiedPluginId,
|
||||
pub package_path: PathBuf,
|
||||
|
|
@ -529,19 +614,19 @@ pub struct DiscoveredPluginPackage {
|
|||
/// 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)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MaterializedPluginPackage {
|
||||
pub package: DiscoveredPluginPackage,
|
||||
pub files: BTreeMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PackedPluginPackage {
|
||||
pub output_path: PathBuf,
|
||||
pub package: DiscoveredPluginPackage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct PluginDiscoveryReport {
|
||||
pub packages: Vec<DiscoveredPluginPackage>,
|
||||
pub diagnostics: Vec<PluginDiagnostic>,
|
||||
|
|
@ -1072,7 +1157,10 @@ pub fn read_resolved_plugin_runtime_component(
|
|||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
if runtime.world.as_deref() != Some(PLUGIN_COMPONENT_TOOL_WORLD) {
|
||||
if !matches!(
|
||||
runtime.world.as_deref(),
|
||||
Some(PLUGIN_COMPONENT_TOOL_WORLD) | Some(PLUGIN_COMPONENT_INSTANCE_WORLD)
|
||||
) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
|
|
@ -1918,7 +2006,10 @@ fn validate_manifest(
|
|||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
if runtime.world.as_deref() != Some(PLUGIN_COMPONENT_TOOL_WORLD) {
|
||||
if !matches!(
|
||||
runtime.world.as_deref(),
|
||||
Some(PLUGIN_COMPONENT_TOOL_WORLD) | Some(PLUGIN_COMPONENT_INSTANCE_WORLD)
|
||||
) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
|
|
@ -1952,6 +2043,44 @@ fn validate_manifest(
|
|||
}
|
||||
}
|
||||
}
|
||||
let instance_capable = manifest.runtime.as_ref().is_some_and(|runtime| {
|
||||
runtime.kind == PLUGIN_RUNTIME_COMPONENT_KIND
|
||||
&& runtime.world.as_deref() == Some(PLUGIN_COMPONENT_INSTANCE_WORLD)
|
||||
});
|
||||
if (!manifest.services.is_empty() || !manifest.ingresses.is_empty()) && !instance_capable {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Surface,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin service/ingress declarations require the yoi:plugin/instance@1.0.0 component world",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
for service in &manifest.services {
|
||||
if !is_safe_id(&service.name) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Malformed,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin service name is not safe",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
}
|
||||
for ingress in &manifest.ingresses {
|
||||
if !is_safe_id(&ingress.name) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Malformed,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin ingress name is not safe",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
}
|
||||
for hook in &manifest.hooks {
|
||||
if !is_safe_id(&hook.id) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
|
|
@ -2425,7 +2554,13 @@ mod tests {
|
|||
.collect();
|
||||
assert_eq!(
|
||||
paths,
|
||||
BTreeSet::from(["Cargo.toml", "src/lib.rs", "plugin.toml", "README.md"])
|
||||
BTreeSet::from([
|
||||
"Cargo.toml",
|
||||
"src/lib.rs",
|
||||
"plugin.toml",
|
||||
"plugin.component.wasm",
|
||||
"README.md",
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
RUST_COMPONENT_TOOL_TEMPLATE
|
||||
|
|
@ -2451,6 +2586,86 @@ mod tests {
|
|||
assert_eq!(manifest.tools.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_rust_component_instance_template_is_valid_package_shape() {
|
||||
let paths: BTreeSet<_> = RUST_COMPONENT_INSTANCE_TEMPLATE
|
||||
.iter()
|
||||
.map(|file| file.path)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
paths,
|
||||
BTreeSet::from([
|
||||
"Cargo.toml",
|
||||
"src/lib.rs",
|
||||
"plugin.toml",
|
||||
"plugin.component.wasm",
|
||||
"README.md"
|
||||
])
|
||||
);
|
||||
assert!(
|
||||
RUST_COMPONENT_INSTANCE_TEMPLATE
|
||||
.iter()
|
||||
.all(|file| !file.path.starts_with('/') && !file.path.contains(".."))
|
||||
);
|
||||
let manifest_text = RUST_COMPONENT_INSTANCE_TEMPLATE
|
||||
.iter()
|
||||
.find(|file| file.path == "plugin.toml")
|
||||
.unwrap()
|
||||
.contents;
|
||||
let manifest: PluginPackageManifest = toml::from_str(manifest_text).unwrap();
|
||||
assert_eq!(
|
||||
manifest.runtime.as_ref().unwrap().world.as_deref(),
|
||||
Some(PLUGIN_COMPONENT_INSTANCE_WORLD)
|
||||
);
|
||||
assert_eq!(manifest.services.len(), 1);
|
||||
assert_eq!(manifest.ingresses.len(), 1);
|
||||
assert!(
|
||||
manifest
|
||||
.declared_surfaces()
|
||||
.contains(&PluginSurface::Service)
|
||||
);
|
||||
assert!(
|
||||
manifest
|
||||
.declared_surfaces()
|
||||
.contains(&PluginSurface::Ingress)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_ingress_require_instance_component_world() {
|
||||
let manifest: PluginPackageManifest = toml::from_str(
|
||||
r#"
|
||||
schema_version = 1
|
||||
id = "bad.service"
|
||||
name = "Bad Service"
|
||||
version = "0.1.0"
|
||||
surfaces = ["service"]
|
||||
permissions = [{ kind = "surface", surface = "service" }, { kind = "service", name = "svc" }]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
component = "plugin.component.wasm"
|
||||
|
||||
[[services]]
|
||||
name = "svc"
|
||||
description = "bad"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let archive = StoredArchive {
|
||||
files: BTreeMap::from([("plugin.component.wasm".to_string(), b"placeholder".to_vec())]),
|
||||
};
|
||||
let err = validate_manifest(
|
||||
&manifest,
|
||||
&archive,
|
||||
"bad.service",
|
||||
PluginSourceKind::Project,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.message.contains("service/ingress"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_valid_user_and_workspace_packages() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ use serde_json::Value;
|
|||
|
||||
pub use wit_bindgen;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, ToolError>;
|
||||
|
||||
/// Current Yoi Component Model Tool world targeted by this PDK.
|
||||
pub const TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0";
|
||||
|
||||
|
|
@ -98,7 +100,10 @@ impl ToolOutput {
|
|||
}
|
||||
|
||||
/// Create a Tool output whose content is typed JSON.
|
||||
pub fn json(summary: impl Into<String>, value: impl Serialize) -> Result<Self, ToolError> {
|
||||
pub fn json(
|
||||
summary: impl Into<String>,
|
||||
value: impl Serialize,
|
||||
) -> std::result::Result<Self, ToolError> {
|
||||
let content = serde_json::to_string(&value).map_err(ToolError::serialization)?;
|
||||
let output = Self {
|
||||
summary: normalize_summary(summary.into()),
|
||||
|
|
@ -292,7 +297,7 @@ impl ToolError {
|
|||
}
|
||||
|
||||
/// Parse the WIT `input-json` string into a typed input value.
|
||||
pub fn parse_json_input<T>(input_json: &str) -> Result<T, ToolError>
|
||||
pub fn parse_json_input<T>(input_json: &str) -> std::result::Result<T, ToolError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
|
|
@ -311,7 +316,7 @@ where
|
|||
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>,
|
||||
F: FnOnce(ToolContext, I) -> std::result::Result<ToolOutput, ToolError>,
|
||||
{
|
||||
let result = parse_json_input::<I>(input_json).and_then(|input| {
|
||||
let context = ToolContext::new(tool_name);
|
||||
|
|
@ -474,3 +479,166 @@ mod tests {
|
|||
assert!(HOST_WIT.contains("%list: func"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Versioned Component Model instance world handled by the host-managed
|
||||
/// PluginInstanceRegistry.
|
||||
pub const PLUGIN_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0";
|
||||
|
||||
/// Repository WIT for the current instance world.
|
||||
pub const INSTANCE_WIT: &str =
|
||||
include_str!("../../../resources/plugin/wit/yoi-plugin-instance-v1.wit");
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PluginIngressEvent {
|
||||
pub kind: String,
|
||||
pub source: String,
|
||||
#[serde(default)]
|
||||
pub payload: Value,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PluginStatus {
|
||||
pub state: String,
|
||||
#[serde(default)]
|
||||
pub data: Value,
|
||||
}
|
||||
|
||||
impl PluginStatus {
|
||||
pub fn ready(data: Value) -> Self {
|
||||
Self {
|
||||
state: "ready".to_string(),
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stopped() -> Self {
|
||||
Self {
|
||||
state: "stopped".to_string(),
|
||||
data: Value::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rust-facing instance Plugin contract. Hosts call `start` once, then route
|
||||
/// Tool/Ingress surfaces through the same mutable instance.
|
||||
pub trait Plugin: Sized + 'static {
|
||||
fn start(config: Value) -> Result<Self>;
|
||||
fn handle_tool(&mut self, name: &str, input: Value) -> Result<ToolOutput>;
|
||||
fn handle_ingress(&mut self, name: &str, event: PluginIngressEvent) -> Result<Value>;
|
||||
fn status(&self) -> Result<PluginStatus> {
|
||||
Ok(PluginStatus::ready(Value::Null))
|
||||
}
|
||||
fn stop(&mut self) -> Result<PluginStatus> {
|
||||
Ok(PluginStatus::stopped())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn plugin_instance_error(message: impl Into<String>) -> String {
|
||||
serde_json::json!({ "error": { "message": message.into() } }).to_string()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn plugin_instance_status(status: &PluginStatus) -> String {
|
||||
serde_json::to_string(status).unwrap_or_else(|error| plugin_instance_error(error.to_string()))
|
||||
}
|
||||
|
||||
/// Implement the generated Component Model `Guest` trait for an instance Plugin
|
||||
/// and export it with the `wit-bindgen` generated `export!` macro.
|
||||
///
|
||||
/// The caller must invoke `wit_bindgen::generate!` for the `instance` world
|
||||
/// first, with `runtime_path: "yoi_plugin_pdk::wit_bindgen::rt"`. That defines
|
||||
/// the `Guest` trait and `export!` macro in the current module.
|
||||
#[macro_export]
|
||||
macro_rules! export_plugin_instance {
|
||||
($adapter:ident, $plugin:ty) => {
|
||||
struct $adapter;
|
||||
|
||||
thread_local! {
|
||||
static YOI_PLUGIN_INSTANCE: ::std::cell::RefCell<::std::option::Option<$plugin>> = const { ::std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
impl Guest for $adapter {
|
||||
fn start(config_json: ::std::string::String) -> ::std::string::String {
|
||||
let config = serde_json::from_str(&config_json).unwrap_or(serde_json::Value::Null);
|
||||
match <$plugin as $crate::Plugin>::start(config) {
|
||||
Ok(plugin) => {
|
||||
YOI_PLUGIN_INSTANCE.with(|slot| *slot.borrow_mut() = Some(plugin));
|
||||
$crate::plugin_instance_status(&$crate::PluginStatus::ready(serde_json::Value::Null))
|
||||
}
|
||||
Err(error) => $crate::plugin_instance_error(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tool(
|
||||
name: ::std::string::String,
|
||||
input_json: ::std::string::String,
|
||||
) -> ::std::string::String {
|
||||
let input = serde_json::from_str(&input_json).unwrap_or(serde_json::Value::Null);
|
||||
YOI_PLUGIN_INSTANCE.with(|slot| {
|
||||
let mut slot = slot.borrow_mut();
|
||||
let Some(plugin) = slot.as_mut() else {
|
||||
return $crate::plugin_instance_error("plugin instance has not been started");
|
||||
};
|
||||
match plugin.handle_tool(&name, input) {
|
||||
Ok(output) => output.to_json_string(),
|
||||
Err(error) => error.into_tool_output().to_json_string(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_ingress(
|
||||
name: ::std::string::String,
|
||||
event_json: ::std::string::String,
|
||||
) -> ::std::string::String {
|
||||
let event = match serde_json::from_str::<$crate::PluginIngressEvent>(&event_json) {
|
||||
Ok(event) => event,
|
||||
Err(error) => return $crate::plugin_instance_error(error.to_string()),
|
||||
};
|
||||
YOI_PLUGIN_INSTANCE.with(|slot| {
|
||||
let mut slot = slot.borrow_mut();
|
||||
let Some(plugin) = slot.as_mut() else {
|
||||
return $crate::plugin_instance_error("plugin instance has not been started");
|
||||
};
|
||||
match plugin.handle_ingress(&name, event) {
|
||||
Ok(output) => serde_json::to_string(&output)
|
||||
.unwrap_or_else(|error| $crate::plugin_instance_error(error.to_string())),
|
||||
Err(error) => $crate::plugin_instance_error(error.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn status() -> ::std::string::String {
|
||||
YOI_PLUGIN_INSTANCE.with(|slot| {
|
||||
let slot = slot.borrow();
|
||||
let Some(plugin) = slot.as_ref() else {
|
||||
return $crate::plugin_instance_error("plugin instance has not been started");
|
||||
};
|
||||
match plugin.status() {
|
||||
Ok(status) => $crate::plugin_instance_status(&status),
|
||||
Err(error) => $crate::plugin_instance_error(error.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stop() -> ::std::string::String {
|
||||
YOI_PLUGIN_INSTANCE.with(|slot| {
|
||||
let mut slot = slot.borrow_mut();
|
||||
let Some(plugin) = slot.as_mut() else {
|
||||
return $crate::plugin_instance_error("plugin instance has not been started");
|
||||
};
|
||||
match plugin.stop() {
|
||||
Ok(status) => {
|
||||
let output = $crate::plugin_instance_status(&status);
|
||||
*slot = None;
|
||||
output
|
||||
}
|
||||
Err(error) => $crate::plugin_instance_error(error.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export!($adapter);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5375,6 +5375,8 @@ permission = "read"
|
|||
runtime: None,
|
||||
hooks: vec![],
|
||||
tools: vec![],
|
||||
services: vec![],
|
||||
ingresses: vec![],
|
||||
permissions: vec![],
|
||||
},
|
||||
enabled_surfaces: vec![manifest::plugin::PluginSurface::Hook],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod mcp_cli;
|
||||
mod memory_lint;
|
||||
mod objective_cli;
|
||||
mod plugin_cli;
|
||||
|
|
@ -18,6 +19,7 @@ enum Mode {
|
|||
Help,
|
||||
MemoryLintHelp,
|
||||
MemoryLint(LintCliOptions),
|
||||
Mcp(mcp_cli::McpCliCommand),
|
||||
Plugin(plugin_cli::PluginCliCommand),
|
||||
Objective(objective_cli::ObjectiveCli),
|
||||
Session(session_cli::SessionCli),
|
||||
|
|
@ -70,6 +72,13 @@ async fn main() -> ExitCode {
|
|||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
Mode::Mcp(command) => match mcp_cli::run(command) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("yoi mcp: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
},
|
||||
Mode::Plugin(command) => match plugin_cli::run(command) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
|
|
@ -186,6 +195,10 @@ fn parse_args_slice(args: &[String]) -> Result<Mode, ParseError> {
|
|||
let plugin_cli = parse_plugin_args(&args[1..])?;
|
||||
return Ok(Mode::Plugin(plugin_cli));
|
||||
}
|
||||
"mcp" => {
|
||||
let mcp_cli = parse_mcp_args(&args[1..])?;
|
||||
return Ok(Mode::Mcp(mcp_cli));
|
||||
}
|
||||
"panel" => {
|
||||
return Ok(Mode::Tui {
|
||||
mode: LaunchMode::Panel,
|
||||
|
|
@ -593,6 +606,147 @@ fn plugin_usage() -> &'static str {
|
|||
"usage: yoi plugin new rust-component-tool <path-or-name> [--json]\n yoi plugin check <path-or-package> [--json]\n yoi plugin pack <path> [--output <file>] [--json]\n yoi plugin list [--workspace PATH] [--profile REF] [--json]\n yoi plugin show <ref> [--workspace PATH] [--profile REF] [--json]"
|
||||
}
|
||||
|
||||
fn parse_mcp_args(args: &[String]) -> Result<mcp_cli::McpCliCommand, ParseError> {
|
||||
let Some((subcommand, rest)) = args.split_first() else {
|
||||
return Err(ParseError(
|
||||
"yoi mcp requires `list`, `show <server>`, `tools [server]`, `resources [server]`, or `prompts [server]`".to_string(),
|
||||
));
|
||||
};
|
||||
match subcommand.as_str() {
|
||||
"list" => {
|
||||
let (mcp_args, positional) = parse_mcp_common_args(rest)?;
|
||||
if !positional.is_empty() {
|
||||
return Err(ParseError(
|
||||
"yoi mcp list does not accept positional arguments".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(mcp_cli::McpCliCommand::List(mcp_args))
|
||||
}
|
||||
"show" => {
|
||||
let (mcp_args, positional) = parse_mcp_common_args(rest)?;
|
||||
match positional.as_slice() {
|
||||
[server] => Ok(mcp_cli::McpCliCommand::Show {
|
||||
server: server.clone(),
|
||||
args: mcp_args,
|
||||
}),
|
||||
[] => Err(ParseError(
|
||||
"yoi mcp show requires a server name".to_string(),
|
||||
)),
|
||||
_ => Err(ParseError(
|
||||
"yoi mcp show accepts exactly one server name".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"tools" => {
|
||||
let (mcp_args, positional) = parse_mcp_common_args(rest)?;
|
||||
match positional.as_slice() {
|
||||
[] => Ok(mcp_cli::McpCliCommand::Tools {
|
||||
server: None,
|
||||
args: mcp_args,
|
||||
}),
|
||||
[server] => Ok(mcp_cli::McpCliCommand::Tools {
|
||||
server: Some(server.clone()),
|
||||
args: mcp_args,
|
||||
}),
|
||||
_ => Err(ParseError(
|
||||
"yoi mcp tools accepts at most one server name".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"resources" => {
|
||||
let (mcp_args, positional) = parse_mcp_common_args(rest)?;
|
||||
match positional.as_slice() {
|
||||
[] => Ok(mcp_cli::McpCliCommand::Resources {
|
||||
server: None,
|
||||
args: mcp_args,
|
||||
}),
|
||||
[server] => Ok(mcp_cli::McpCliCommand::Resources {
|
||||
server: Some(server.clone()),
|
||||
args: mcp_args,
|
||||
}),
|
||||
_ => Err(ParseError(
|
||||
"yoi mcp resources accepts at most one server name".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"prompts" => {
|
||||
let (mcp_args, positional) = parse_mcp_common_args(rest)?;
|
||||
match positional.as_slice() {
|
||||
[] => Ok(mcp_cli::McpCliCommand::Prompts {
|
||||
server: None,
|
||||
args: mcp_args,
|
||||
}),
|
||||
[server] => Ok(mcp_cli::McpCliCommand::Prompts {
|
||||
server: Some(server.clone()),
|
||||
args: mcp_args,
|
||||
}),
|
||||
_ => Err(ParseError(
|
||||
"yoi mcp prompts accepts at most one server name".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"--help" | "-h" => Err(ParseError(mcp_usage().to_string())),
|
||||
other => Err(ParseError(format!("unknown yoi mcp command: {other}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mcp_common_args(
|
||||
args: &[String],
|
||||
) -> Result<(mcp_cli::McpCliArgs, Vec<String>), ParseError> {
|
||||
let mut mcp_args = mcp_cli::McpCliArgs::default();
|
||||
let mut positional = Vec::new();
|
||||
let mut index = 0;
|
||||
while index < args.len() {
|
||||
let arg = &args[index];
|
||||
if arg == "--json" {
|
||||
mcp_args.json = true;
|
||||
index += 1;
|
||||
} else if arg == "--workspace" {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| ParseError("--workspace requires a value".to_string()))?;
|
||||
if value.starts_with('-') {
|
||||
return Err(ParseError("--workspace requires a value".to_string()));
|
||||
}
|
||||
mcp_args.workspace = Some(PathBuf::from(value));
|
||||
index += 2;
|
||||
} else if let Some(value) = arg.strip_prefix("--workspace=") {
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--workspace requires a value".to_string()));
|
||||
}
|
||||
mcp_args.workspace = Some(PathBuf::from(value));
|
||||
index += 1;
|
||||
} else if arg == "--profile" {
|
||||
let value = args
|
||||
.get(index + 1)
|
||||
.ok_or_else(|| ParseError("--profile requires a value".to_string()))?;
|
||||
if value.starts_with('-') {
|
||||
return Err(ParseError("--profile requires a value".to_string()));
|
||||
}
|
||||
mcp_args.profile = Some(value.clone());
|
||||
index += 2;
|
||||
} else if let Some(value) = arg.strip_prefix("--profile=") {
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--profile requires a value".to_string()));
|
||||
}
|
||||
mcp_args.profile = Some(value.to_string());
|
||||
index += 1;
|
||||
} else if arg == "--help" || arg == "-h" {
|
||||
return Err(ParseError(mcp_usage().to_string()));
|
||||
} else if arg.starts_with('-') {
|
||||
return Err(ParseError(format!("unknown yoi mcp argument: {arg}")));
|
||||
} else {
|
||||
positional.push(arg.clone());
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
Ok((mcp_args, positional))
|
||||
}
|
||||
|
||||
fn mcp_usage() -> &'static str {
|
||||
"usage: yoi mcp list [--workspace PATH] [--profile REF] [--json]\n yoi mcp show <server> [--workspace PATH] [--profile REF] [--json]\n yoi mcp tools [server] [--workspace PATH] [--profile REF] [--json]\n yoi mcp resources [server] [--workspace PATH] [--profile REF] [--json]\n yoi mcp prompts [server] [--workspace PATH] [--profile REF] [--json]"
|
||||
}
|
||||
|
||||
fn parse_panel_workspace(args: &[String]) -> Result<PathBuf, ParseError> {
|
||||
match args {
|
||||
[] => std::env::current_dir()
|
||||
|
|
@ -623,7 +777,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 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\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, --resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n -r, --resume Open the Pod Console picker and resume/attach a Pod\n --workspace <PATH> Runtime workspace root (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
|
||||
"yoi\n\nUsage:\n yoi [OPTIONS] [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 mcp list [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp show <SERVER> [--workspace <PATH>] [--profile <REF>] [--json]\n yoi mcp tools|resources|prompts [SERVER] [--workspace <PATH>] [--profile <REF>] [--json]\n yoi memory lint [OPTIONS]\n\nSurfaces:\n Console Single-Pod chat/client surface (default, --pod, --resume)\n Dashboard Workspace cockpit/action surface (yoi panel)\n TUI Terminal UI implementation umbrella for Console and Dashboard\n\nOptions:\n -r, --resume Open the Pod Console picker and resume/attach a Pod\n --workspace <PATH> Runtime workspace root (defaults to cwd)\n --pod <NAME> Open the Pod Console by name (attach/restore/create)\n --socket <PATH> Attach a Pod Console to a specific socket with --pod\n --session <UUID> Resume a specific session segment in the Pod Console\n --profile <REF> Select a reusable Profile recipe\n -h, --help Print help\n"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -814,6 +968,57 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mcp_commands() {
|
||||
match parse_args_from(["mcp", "list", "--workspace=/tmp/ws", "--json"]).unwrap() {
|
||||
Mode::Mcp(mcp_cli::McpCliCommand::List(options)) => {
|
||||
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
|
||||
assert!(options.json);
|
||||
}
|
||||
_ => panic!("expected MCP list mode"),
|
||||
}
|
||||
|
||||
match parse_args_from(["mcp", "show", "filesystem", "--profile", "project:mcp"]).unwrap() {
|
||||
Mode::Mcp(mcp_cli::McpCliCommand::Show { server, args }) => {
|
||||
assert_eq!(server, "filesystem");
|
||||
assert_eq!(args.profile.as_deref(), Some("project:mcp"));
|
||||
}
|
||||
_ => panic!("expected MCP show mode"),
|
||||
}
|
||||
|
||||
match parse_args_from(["mcp", "tools", "filesystem"]).unwrap() {
|
||||
Mode::Mcp(mcp_cli::McpCliCommand::Tools { server, .. }) => {
|
||||
assert_eq!(server.as_deref(), Some("filesystem"));
|
||||
}
|
||||
_ => panic!("expected MCP tools mode"),
|
||||
}
|
||||
|
||||
match parse_args_from(["mcp", "resources"]).unwrap() {
|
||||
Mode::Mcp(mcp_cli::McpCliCommand::Resources { server, .. }) => {
|
||||
assert!(server.is_none());
|
||||
}
|
||||
_ => panic!("expected MCP resources mode"),
|
||||
}
|
||||
|
||||
match parse_args_from(["mcp", "prompts", "filesystem"]).unwrap() {
|
||||
Mode::Mcp(mcp_cli::McpCliCommand::Prompts { server, .. }) => {
|
||||
assert_eq!(server.as_deref(), Some("filesystem"));
|
||||
}
|
||||
_ => panic!("expected MCP prompts mode"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mcp_rejects_usage_errors() {
|
||||
let err = parse_args_from(["mcp", "show"]).unwrap_err();
|
||||
assert_eq!(err.to_string(), "yoi mcp show requires a server name");
|
||||
let err = parse_args_from(["mcp", "list", "extra"]).unwrap_err();
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"yoi mcp list does not accept positional arguments"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_memory_lint_rejects_usage_errors() {
|
||||
let err = parse_args_from(["memory", "lint", "--workspace"]).unwrap_err();
|
||||
|
|
|
|||
1232
crates/yoi/src/mcp_cli.rs
Normal file
1232
crates/yoi/src/mcp_cli.rs
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -329,6 +329,24 @@ fn static_inspection_diagnostics(
|
|||
});
|
||||
}
|
||||
}
|
||||
for service in &inspection.services {
|
||||
if let Some(message) = &service.diagnostic {
|
||||
diagnostics.push(PluginDiagnosticReport {
|
||||
kind: "grant".to_string(),
|
||||
phase: "resolution".to_string(),
|
||||
message: bound_text(format!("service `{}`: {message}", service.name)),
|
||||
});
|
||||
}
|
||||
}
|
||||
for ingress in &inspection.ingresses {
|
||||
if let Some(message) = &ingress.diagnostic {
|
||||
diagnostics.push(PluginDiagnosticReport {
|
||||
kind: "grant".to_string(),
|
||||
phase: "resolution".to_string(),
|
||||
message: bound_text(format!("ingress `{}`: {message}", ingress.name)),
|
||||
});
|
||||
}
|
||||
}
|
||||
diagnostics
|
||||
}
|
||||
|
||||
|
|
@ -1072,6 +1090,18 @@ fn fill_resolved(builder: &mut ItemBuilder, resolved: &ResolvedPlugin) {
|
|||
.iter()
|
||||
.filter_map(|tool| tool.diagnostic.as_ref()),
|
||||
)
|
||||
.chain(
|
||||
static_runtime
|
||||
.services
|
||||
.iter()
|
||||
.filter_map(|service| service.diagnostic.as_ref()),
|
||||
)
|
||||
.chain(
|
||||
static_runtime
|
||||
.ingresses
|
||||
.iter()
|
||||
.filter_map(|ingress| ingress.diagnostic.as_ref()),
|
||||
)
|
||||
{
|
||||
builder.diagnostics.push(DiagnosticSummary {
|
||||
kind: "static_eligibility".to_string(),
|
||||
|
|
@ -1473,6 +1503,58 @@ mod tests {
|
|||
assert!(show.contains("configured_grants: surfaces.tool, tool.Echo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_only_enablement_ignores_unselected_tool_static_grants() {
|
||||
let dir = tempdir().unwrap();
|
||||
let workspace = dir.path();
|
||||
let digest = write_mixed_tool_service_package(workspace, "mixed");
|
||||
let mut config = PluginConfig::default();
|
||||
config.enabled.push(PluginEnablementConfig {
|
||||
id: "project:mixed".to_string(),
|
||||
digest: Some(digest.clone()),
|
||||
version: Some(PluginExactVersion("0.1.0".to_string())),
|
||||
surfaces: vec![PluginSurface::Service],
|
||||
grants: PluginGrantConfig {
|
||||
id: Some("project:mixed".to_string()),
|
||||
version: Some(PluginExactVersion("0.1.0".to_string())),
|
||||
digest: Some(digest),
|
||||
permissions: vec![
|
||||
PluginPermission::surface(PluginSurface::Service),
|
||||
PluginPermission::service("svc"),
|
||||
],
|
||||
https: Vec::new(),
|
||||
fs: Vec::new(),
|
||||
},
|
||||
config: None,
|
||||
});
|
||||
|
||||
let snapshot = inspect_snapshot(workspace, &config);
|
||||
let item = select_item(&snapshot, "project:mixed").unwrap();
|
||||
|
||||
assert_eq!(item.status, "active");
|
||||
assert!(item.static_eligible);
|
||||
assert_eq!(item.enabled_surfaces, vec!["service"]);
|
||||
assert!(
|
||||
item.tools.is_empty(),
|
||||
"unselected Tool must not be reported"
|
||||
);
|
||||
assert!(
|
||||
item.diagnostics
|
||||
.iter()
|
||||
.all(|diagnostic| !diagnostic.message.contains("tool.Echo")),
|
||||
"unselected Tool grant diagnostics must not affect service-only enablement: {:#?}",
|
||||
item.diagnostics
|
||||
);
|
||||
|
||||
let show_json = serde_json::to_value(item).unwrap();
|
||||
assert_eq!(show_json["status"], "active");
|
||||
assert_eq!(
|
||||
show_json["enabled_surfaces"],
|
||||
serde_json::json!(["service"])
|
||||
);
|
||||
assert_eq!(show_json["tools"], serde_json::json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn human_list_uses_required_status_vocabulary() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
|
@ -2080,6 +2162,61 @@ mod tests {
|
|||
assert!(error.len() < 160);
|
||||
}
|
||||
|
||||
fn write_mixed_tool_service_package(workspace: &Path, id: &str) -> String {
|
||||
let package_dir = workspace.join(".yoi/plugins");
|
||||
fs::create_dir_all(&package_dir).unwrap();
|
||||
let package = package_dir.join(format!("{id}.yoi-plugin"));
|
||||
let manifest = format!(
|
||||
r#"schema_version = 1
|
||||
id = "{id}"
|
||||
name = "{id}"
|
||||
version = "0.1.0"
|
||||
description = "mixed surface package"
|
||||
surfaces = ["tool", "service"]
|
||||
permissions = [
|
||||
{{ kind = "surface", surface = "tool" }},
|
||||
{{ kind = "tool", name = "Echo" }},
|
||||
{{ kind = "surface", surface = "service" }},
|
||||
{{ kind = "service", name = "svc" }},
|
||||
]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
world = "yoi:plugin/instance@1.0.0"
|
||||
component = "plugin.component.wasm"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
description = "unselected tool"
|
||||
input_schema = {{ type = "object" }}
|
||||
|
||||
[[services]]
|
||||
name = "svc"
|
||||
description = "selected service"
|
||||
lifecycle = "host-managed"
|
||||
"#,
|
||||
);
|
||||
write_stored_zip(
|
||||
&package,
|
||||
&[
|
||||
("plugin.toml", manifest.as_bytes()),
|
||||
("plugin.component.wasm", b"placeholder component bytes"),
|
||||
],
|
||||
);
|
||||
let discovery = discover_plugins(&PluginDiscoveryOptions {
|
||||
workspace_root: workspace.to_path_buf(),
|
||||
user_data_home: None,
|
||||
limits: PluginDiscoveryLimits::default(),
|
||||
});
|
||||
discovery
|
||||
.packages
|
||||
.iter()
|
||||
.find(|package| package.identity.local_id == id)
|
||||
.unwrap()
|
||||
.digest
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn inspect_snapshot(workspace: &Path, config: &PluginConfig) -> PluginInspectionSnapshot {
|
||||
let discovery = discover_plugins(&PluginDiscoveryOptions {
|
||||
workspace_root: workspace.to_path_buf(),
|
||||
|
|
|
|||
|
|
@ -179,3 +179,26 @@ semantics while moving package authors onto WIT/canonical ABI bindings.
|
|||
Structured WIT records for Tool requests/responses/errors and host HTTPS/FS
|
||||
payloads are deferred to a follow-up API-design step rather than accidentally
|
||||
omitted.
|
||||
|
||||
## Instance lifecycle surface
|
||||
|
||||
The first instance-capable world is `yoi:plugin/instance@1.0.0`. It moves
|
||||
runtime ownership from per-Tool artifact execution to a host-managed
|
||||
`PluginInstance`. The same instance handles Tool, Service, and Ingress surfaces,
|
||||
so Plugin state/config/diagnostics can be shared without bypassing Yoi's normal
|
||||
authority model.
|
||||
|
||||
Important boundaries:
|
||||
|
||||
- Tool calls still enter through `ToolRegistry` and return ordinary `ToolOutput`
|
||||
that is visible in the Worker history path.
|
||||
- Service and Ingress grants are separate from Tool grants. Sharing an instance
|
||||
does not authorize a surface that lacks its own `surface.*` and per-surface
|
||||
permission/grant.
|
||||
- Ingress delivery accepts bounded typed untrusted events and returns explicit
|
||||
JSON to the host. It does not call model Tools or mutate LLM context/history.
|
||||
- Legacy raw-wasm and `yoi:plugin/tool@1.0.0` component packages are adapted
|
||||
behind `PluginInstanceRegistry` for compatibility rather than executed through
|
||||
a separate authority path.
|
||||
- Host APIs such as `https` and `fs` remain independently grant-gated and still
|
||||
reject ambient filesystem/network authority.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
[workspace]
|
||||
|
||||
[package]
|
||||
name = "example-yoi-instance-plugin"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
yoi-plugin-pdk = { path = "../../../../crates/plugin-pdk" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Yoi instance Plugin template
|
||||
|
||||
This template targets `yoi:plugin/instance@1.0.0`. The host creates one
|
||||
`PluginInstance` for the package; Tool, Service, and Ingress surfaces share that
|
||||
instance state while each surface keeps separate permissions/grants.
|
||||
|
||||
Tools still run only through ordinary model/user-initiated Tool calls. Ingress
|
||||
handlers receive bounded typed untrusted events and must return explicit JSON
|
||||
for host-mediated visible/durable paths.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Build with:
|
||||
# cargo component build --release
|
||||
# cp target/wasm32-wasip1/release/example_yoi_instance_plugin.wasm plugin.component.wasm
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
schema_version = 1
|
||||
id = "example.rust_instance_plugin"
|
||||
name = "Rust Instance Plugin Template"
|
||||
version = "0.1.0"
|
||||
description = "Example instance-oriented Yoi Plugin with shared Tool/Ingress state."
|
||||
surfaces = ["tool", "service", "ingress"]
|
||||
permissions = [
|
||||
{ kind = "surface", surface = "tool" },
|
||||
{ kind = "tool", name = "example_instance_tool" },
|
||||
{ kind = "surface", surface = "service" },
|
||||
{ kind = "service", name = "example_instance_service" },
|
||||
{ kind = "surface", surface = "ingress" },
|
||||
{ kind = "ingress", name = "example_instance_ingress" },
|
||||
]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
world = "yoi:plugin/instance@1.0.0"
|
||||
component = "plugin.component.wasm"
|
||||
|
||||
[[tools]]
|
||||
name = "example_instance_tool"
|
||||
description = "Return the input and increment shared instance state."
|
||||
input_schema = { type = "object" }
|
||||
|
||||
[[services]]
|
||||
name = "example_instance_service"
|
||||
description = "Reports shared plugin instance lifecycle status."
|
||||
lifecycle = "host-managed"
|
||||
|
||||
[[ingresses]]
|
||||
name = "example_instance_ingress"
|
||||
description = "Accepts bounded in-process ingress events."
|
||||
event_kinds = ["example"]
|
||||
input_schema = { type = "object" }
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
use serde_json::{json, Value};
|
||||
use yoi_plugin_pdk::wit_bindgen;
|
||||
use yoi_plugin_pdk::{export_plugin_instance, Plugin, PluginIngressEvent, PluginStatus, ToolOutput};
|
||||
|
||||
wit_bindgen::generate!({
|
||||
world: "instance",
|
||||
path: "../../../../resources/plugin/wit",
|
||||
generate_all,
|
||||
runtime_path: "yoi_plugin_pdk::wit_bindgen::rt",
|
||||
});
|
||||
|
||||
struct ExamplePlugin {
|
||||
calls: u64,
|
||||
}
|
||||
|
||||
impl Plugin for ExamplePlugin {
|
||||
fn start(_config: Value) -> yoi_plugin_pdk::Result<Self> {
|
||||
Ok(Self { calls: 0 })
|
||||
}
|
||||
|
||||
fn handle_tool(&mut self, name: &str, input: Value) -> yoi_plugin_pdk::Result<ToolOutput> {
|
||||
self.calls += 1;
|
||||
ToolOutput::json(
|
||||
format!("{name} handled by shared instance"),
|
||||
json!({
|
||||
"tool": name,
|
||||
"calls": self.calls,
|
||||
"input": input
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_ingress(
|
||||
&mut self,
|
||||
name: &str,
|
||||
event: PluginIngressEvent,
|
||||
) -> yoi_plugin_pdk::Result<Value> {
|
||||
Ok(json!({
|
||||
"ingress": name,
|
||||
"kind": event.kind,
|
||||
"source": event.source,
|
||||
"calls": self.calls,
|
||||
"accepted": true
|
||||
}))
|
||||
}
|
||||
|
||||
fn status(&self) -> yoi_plugin_pdk::Result<PluginStatus> {
|
||||
Ok(PluginStatus::ready(json!({ "calls": self.calls })))
|
||||
}
|
||||
}
|
||||
|
||||
export_plugin_instance!(ExamplePluginComponent, ExamplePlugin);
|
||||
12
resources/plugin/wit/yoi-plugin-instance-v1.wit
Normal file
12
resources/plugin/wit/yoi-plugin-instance-v1.wit
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package yoi:plugin@1.0.0;
|
||||
|
||||
world instance {
|
||||
import yoi:host/https@1.0.0;
|
||||
import yoi:host/fs@1.0.0;
|
||||
|
||||
export start: func(config-json: string) -> string;
|
||||
export handle-tool: func(name: string, input-json: string) -> string;
|
||||
export handle-ingress: func(name: string, event-json: string) -> string;
|
||||
export status: func() -> string;
|
||||
export stop: func() -> string;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user