From 627c8f36ffc6db3a4b3625bbeb188278bd127fbf Mon Sep 17 00:00:00 2001 From: Hare Date: Sun, 21 Jun 2026 00:12:55 +0900 Subject: [PATCH] plugin: filter static enabled surfaces --- crates/pod/src/feature/plugin.rs | 166 +++++++++++++++++-------------- crates/yoi/src/plugin_cli.rs | 119 ++++++++++++++++++++++ 2 files changed, 209 insertions(+), 76 deletions(-) diff --git a/crates/pod/src/feature/plugin.rs b/crates/pod/src/feature/plugin.rs index 33e88730..6ff66327 100644 --- a/crates/pod/src/feature/plugin.rs +++ b/crates/pod/src/feature/plugin.rs @@ -303,87 +303,101 @@ pub fn inspect_resolved_plugin_static(record: &ResolvedPluginRecord) -> PluginSt .collect(); let duplicate_tool_names = duplicate_tool_names(record); - let tools = record - .manifest - .tools - .iter() - .map(|tool| { - let permission = PluginPermission::tool(&tool.name); - let requested = permission_requested(record, &permission); - let granted = grant_allows(record, &permission); - let mut diagnostics = validate_plugin_tool_definition(tool, &duplicate_tool_names); - if let Err(error) = authorize_plugin_tool(record, tool) { - diagnostics.push(error.bounded_message()); - } - let diagnostic = join_tool_diagnostics(diagnostics); - PluginToolEligibility { - name: tool.name.clone(), - permission: permission.label(), - requested, - granted, - eligible: diagnostic.is_none(), - external_write: tool.external_write, - diagnostic, - } - }) - .collect(); + let tools = if surface_enabled(record, PluginSurface::Tool) { + record + .manifest + .tools + .iter() + .map(|tool| { + let permission = PluginPermission::tool(&tool.name); + let requested = permission_requested(record, &permission); + let granted = grant_allows(record, &permission); + let mut diagnostics = validate_plugin_tool_definition(tool, &duplicate_tool_names); + if let Err(error) = authorize_plugin_tool(record, tool) { + diagnostics.push(error.bounded_message()); + } + let diagnostic = join_tool_diagnostics(diagnostics); + PluginToolEligibility { + name: tool.name.clone(), + permission: permission.label(), + requested, + granted, + eligible: diagnostic.is_none(), + external_write: tool.external_write, + diagnostic, + } + }) + .collect() + } else { + Vec::new() + }; let instance_world = record.manifest.runtime.as_ref().is_some_and(|runtime| { runtime.kind == PLUGIN_RUNTIME_COMPONENT_KIND && runtime.world.as_deref() == Some(PLUGIN_COMPONENT_INSTANCE_WORLD) }); - let services = record - .manifest - .services - .iter() - .map(|service| { - let permission = PluginPermission::service(&service.name); - let requested = permission_requested(record, &permission); - let granted = grant_allows(record, &permission); - let mut diagnostics = Vec::new(); - if !instance_world { - diagnostics.push("service requires instance-capable component world".to_string()); - } - if let Err(error) = authorize_plugin_service(record, &service.name) { - diagnostics.push(error.bounded_message()); - } - let diagnostic = join_tool_diagnostics(diagnostics); - PluginSurfaceEligibility { - name: service.name.clone(), - permission: permission.label(), - requested, - granted, - eligible: diagnostic.is_none(), - diagnostic, - } - }) - .collect(); - let ingresses = record - .manifest - .ingresses - .iter() - .map(|ingress| { - let permission = PluginPermission::ingress(&ingress.name); - let requested = permission_requested(record, &permission); - let granted = grant_allows(record, &permission); - let mut diagnostics = Vec::new(); - if !instance_world { - diagnostics.push("ingress requires instance-capable component world".to_string()); - } - if let Err(error) = authorize_plugin_ingress(record, &ingress.name) { - diagnostics.push(error.bounded_message()); - } - let diagnostic = join_tool_diagnostics(diagnostics); - PluginSurfaceEligibility { - name: ingress.name.clone(), - permission: permission.label(), - requested, - granted, - eligible: diagnostic.is_none(), - diagnostic, - } - }) - .collect(); + let services = if surface_enabled(record, PluginSurface::Service) { + record + .manifest + .services + .iter() + .map(|service| { + let permission = PluginPermission::service(&service.name); + let requested = permission_requested(record, &permission); + let granted = grant_allows(record, &permission); + let mut diagnostics = Vec::new(); + if !instance_world { + diagnostics + .push("service requires instance-capable component world".to_string()); + } + if let Err(error) = authorize_plugin_service(record, &service.name) { + diagnostics.push(error.bounded_message()); + } + let diagnostic = join_tool_diagnostics(diagnostics); + PluginSurfaceEligibility { + name: service.name.clone(), + permission: permission.label(), + requested, + granted, + eligible: diagnostic.is_none(), + diagnostic, + } + }) + .collect() + } else { + Vec::new() + }; + let ingresses = if surface_enabled(record, PluginSurface::Ingress) { + record + .manifest + .ingresses + .iter() + .map(|ingress| { + let permission = PluginPermission::ingress(&ingress.name); + let requested = permission_requested(record, &permission); + let granted = grant_allows(record, &permission); + let mut diagnostics = Vec::new(); + if !instance_world { + diagnostics + .push("ingress requires instance-capable component world".to_string()); + } + if let Err(error) = authorize_plugin_ingress(record, &ingress.name) { + diagnostics.push(error.bounded_message()); + } + let diagnostic = join_tool_diagnostics(diagnostics); + PluginSurfaceEligibility { + name: ingress.name.clone(), + permission: permission.label(), + requested, + granted, + eligible: diagnostic.is_none(), + diagnostic, + } + }) + .collect() + } else { + Vec::new() + }; PluginStaticInspection { runtime, diff --git a/crates/yoi/src/plugin_cli.rs b/crates/yoi/src/plugin_cli.rs index f93b4d98..6ea65fde 100644 --- a/crates/yoi/src/plugin_cli.rs +++ b/crates/yoi/src/plugin_cli.rs @@ -1090,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(), @@ -1491,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(); @@ -2098,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(),