plugin: filter static enabled surfaces

This commit is contained in:
Keisuke Hirata 2026-06-21 00:12:55 +09:00
parent 79ca0f7f81
commit 627c8f36ff
No known key found for this signature in database
2 changed files with 209 additions and 76 deletions

View File

@ -303,87 +303,101 @@ pub fn inspect_resolved_plugin_static(record: &ResolvedPluginRecord) -> PluginSt
.collect(); .collect();
let duplicate_tool_names = duplicate_tool_names(record); let duplicate_tool_names = duplicate_tool_names(record);
let tools = record let tools = if surface_enabled(record, PluginSurface::Tool) {
.manifest record
.tools .manifest
.iter() .tools
.map(|tool| { .iter()
let permission = PluginPermission::tool(&tool.name); .map(|tool| {
let requested = permission_requested(record, &permission); let permission = PluginPermission::tool(&tool.name);
let granted = grant_allows(record, &permission); let requested = permission_requested(record, &permission);
let mut diagnostics = validate_plugin_tool_definition(tool, &duplicate_tool_names); let granted = grant_allows(record, &permission);
if let Err(error) = authorize_plugin_tool(record, tool) { let mut diagnostics = validate_plugin_tool_definition(tool, &duplicate_tool_names);
diagnostics.push(error.bounded_message()); if let Err(error) = authorize_plugin_tool(record, tool) {
} diagnostics.push(error.bounded_message());
let diagnostic = join_tool_diagnostics(diagnostics); }
PluginToolEligibility { let diagnostic = join_tool_diagnostics(diagnostics);
name: tool.name.clone(), PluginToolEligibility {
permission: permission.label(), name: tool.name.clone(),
requested, permission: permission.label(),
granted, requested,
eligible: diagnostic.is_none(), granted,
external_write: tool.external_write, eligible: diagnostic.is_none(),
diagnostic, external_write: tool.external_write,
} diagnostic,
}) }
.collect(); })
.collect()
} else {
Vec::new()
};
let instance_world = record.manifest.runtime.as_ref().is_some_and(|runtime| { let instance_world = record.manifest.runtime.as_ref().is_some_and(|runtime| {
runtime.kind == PLUGIN_RUNTIME_COMPONENT_KIND runtime.kind == PLUGIN_RUNTIME_COMPONENT_KIND
&& runtime.world.as_deref() == Some(PLUGIN_COMPONENT_INSTANCE_WORLD) && runtime.world.as_deref() == Some(PLUGIN_COMPONENT_INSTANCE_WORLD)
}); });
let services = record let services = if surface_enabled(record, PluginSurface::Service) {
.manifest record
.services .manifest
.iter() .services
.map(|service| { .iter()
let permission = PluginPermission::service(&service.name); .map(|service| {
let requested = permission_requested(record, &permission); let permission = PluginPermission::service(&service.name);
let granted = grant_allows(record, &permission); let requested = permission_requested(record, &permission);
let mut diagnostics = Vec::new(); let granted = grant_allows(record, &permission);
if !instance_world { let mut diagnostics = Vec::new();
diagnostics.push("service requires instance-capable component world".to_string()); if !instance_world {
} diagnostics
if let Err(error) = authorize_plugin_service(record, &service.name) { .push("service requires instance-capable component world".to_string());
diagnostics.push(error.bounded_message()); }
} if let Err(error) = authorize_plugin_service(record, &service.name) {
let diagnostic = join_tool_diagnostics(diagnostics); diagnostics.push(error.bounded_message());
PluginSurfaceEligibility { }
name: service.name.clone(), let diagnostic = join_tool_diagnostics(diagnostics);
permission: permission.label(), PluginSurfaceEligibility {
requested, name: service.name.clone(),
granted, permission: permission.label(),
eligible: diagnostic.is_none(), requested,
diagnostic, granted,
} eligible: diagnostic.is_none(),
}) diagnostic,
.collect(); }
let ingresses = record })
.manifest .collect()
.ingresses } else {
.iter() Vec::new()
.map(|ingress| { };
let permission = PluginPermission::ingress(&ingress.name); let ingresses = if surface_enabled(record, PluginSurface::Ingress) {
let requested = permission_requested(record, &permission); record
let granted = grant_allows(record, &permission); .manifest
let mut diagnostics = Vec::new(); .ingresses
if !instance_world { .iter()
diagnostics.push("ingress requires instance-capable component world".to_string()); .map(|ingress| {
} let permission = PluginPermission::ingress(&ingress.name);
if let Err(error) = authorize_plugin_ingress(record, &ingress.name) { let requested = permission_requested(record, &permission);
diagnostics.push(error.bounded_message()); let granted = grant_allows(record, &permission);
} let mut diagnostics = Vec::new();
let diagnostic = join_tool_diagnostics(diagnostics); if !instance_world {
PluginSurfaceEligibility { diagnostics
name: ingress.name.clone(), .push("ingress requires instance-capable component world".to_string());
permission: permission.label(), }
requested, if let Err(error) = authorize_plugin_ingress(record, &ingress.name) {
granted, diagnostics.push(error.bounded_message());
eligible: diagnostic.is_none(), }
diagnostic, let diagnostic = join_tool_diagnostics(diagnostics);
} PluginSurfaceEligibility {
}) name: ingress.name.clone(),
.collect(); permission: permission.label(),
requested,
granted,
eligible: diagnostic.is_none(),
diagnostic,
}
})
.collect()
} else {
Vec::new()
};
PluginStaticInspection { PluginStaticInspection {
runtime, runtime,

View File

@ -1090,6 +1090,18 @@ fn fill_resolved(builder: &mut ItemBuilder, resolved: &ResolvedPlugin) {
.iter() .iter()
.filter_map(|tool| tool.diagnostic.as_ref()), .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 { builder.diagnostics.push(DiagnosticSummary {
kind: "static_eligibility".to_string(), kind: "static_eligibility".to_string(),
@ -1491,6 +1503,58 @@ mod tests {
assert!(show.contains("configured_grants: surfaces.tool, tool.Echo")); 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] #[test]
fn human_list_uses_required_status_vocabulary() { fn human_list_uses_required_status_vocabulary() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
@ -2098,6 +2162,61 @@ mod tests {
assert!(error.len() < 160); 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 { fn inspect_snapshot(workspace: &Path, config: &PluginConfig) -> PluginInspectionSnapshot {
let discovery = discover_plugins(&PluginDiscoveryOptions { let discovery = discover_plugins(&PluginDiscoveryOptions {
workspace_root: workspace.to_path_buf(), workspace_root: workspace.to_path_buf(),