plugin: add cli inspection
This commit is contained in:
@@ -18,6 +18,7 @@ use manifest::plugin::{
|
||||
PluginConfig, PluginDiscoveryLimits, PluginHostApi, PluginPermission, PluginSurface,
|
||||
PluginToolManifest, ResolvedPluginRecord, read_resolved_plugin_runtime_module,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::{
|
||||
@@ -76,6 +77,159 @@ impl PluginToolFeature {
|
||||
}
|
||||
}
|
||||
|
||||
/// Static, read-only eligibility information for a resolved plugin package.
|
||||
///
|
||||
/// This inspection mirrors the registration-time permission checks without
|
||||
/// loading the WASM module, calling a plugin Tool, or executing plugin code.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct PluginStaticInspection {
|
||||
pub runtime: PluginRuntimeEligibility,
|
||||
pub host_apis: Vec<PluginPermissionEligibility>,
|
||||
pub tools: Vec<PluginToolEligibility>,
|
||||
}
|
||||
|
||||
impl PluginStaticInspection {
|
||||
pub fn statically_eligible(&self) -> bool {
|
||||
self.runtime.eligible
|
||||
&& self.host_apis.iter().all(|api| api.eligible)
|
||||
&& self.tools.iter().all(|tool| tool.eligible)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct PluginRuntimeEligibility {
|
||||
pub eligible: bool,
|
||||
pub status: String,
|
||||
pub diagnostic: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct PluginPermissionEligibility {
|
||||
pub permission: String,
|
||||
pub requested: bool,
|
||||
pub granted: bool,
|
||||
pub eligible: bool,
|
||||
pub diagnostic: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct PluginToolEligibility {
|
||||
pub name: String,
|
||||
pub permission: String,
|
||||
pub requested: bool,
|
||||
pub granted: bool,
|
||||
pub eligible: bool,
|
||||
pub external_write: bool,
|
||||
pub diagnostic: Option<String>,
|
||||
}
|
||||
|
||||
/// Inspect static plugin runtime/tool eligibility without executing plugin code.
|
||||
pub fn inspect_resolved_plugin_static(record: &ResolvedPluginRecord) -> PluginStaticInspection {
|
||||
let runtime = match &record.manifest.runtime {
|
||||
Some(runtime)
|
||||
if runtime.kind == "wasm" && runtime.abi.as_deref() == Some("yoi-plugin-wasm-1") =>
|
||||
{
|
||||
PluginRuntimeEligibility {
|
||||
eligible: true,
|
||||
status: "wasm/yoi-plugin-wasm-1".to_string(),
|
||||
diagnostic: None,
|
||||
}
|
||||
}
|
||||
Some(runtime) if runtime.kind == "wasm" => {
|
||||
let status = runtime
|
||||
.abi
|
||||
.as_deref()
|
||||
.map(|abi| format!("wasm/{abi}"))
|
||||
.unwrap_or_else(|| "wasm/<missing-abi>".to_string());
|
||||
PluginRuntimeEligibility {
|
||||
eligible: false,
|
||||
status,
|
||||
diagnostic: Some("unsupported or missing plugin runtime ABI".to_string()),
|
||||
}
|
||||
}
|
||||
Some(runtime) => PluginRuntimeEligibility {
|
||||
eligible: false,
|
||||
status: runtime.kind.clone(),
|
||||
diagnostic: Some(format!(
|
||||
"unsupported plugin runtime kind `{}`",
|
||||
runtime.kind
|
||||
)),
|
||||
},
|
||||
None => PluginRuntimeEligibility {
|
||||
eligible: false,
|
||||
status: "none".to_string(),
|
||||
diagnostic: Some("plugin runtime is not declared".to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
let host_apis = [PluginHostApi::Https, PluginHostApi::Fs]
|
||||
.into_iter()
|
||||
.filter_map(|api| {
|
||||
let permission = PluginPermission::host_api(api);
|
||||
let requested = permission_requested(record, &permission);
|
||||
let granted = grant_allows(record, &permission);
|
||||
if !requested && !granted {
|
||||
return None;
|
||||
}
|
||||
let diagnostic = authorize_plugin_host_api(record, api)
|
||||
.err()
|
||||
.map(|error| error.bounded_message());
|
||||
Some(PluginPermissionEligibility {
|
||||
permission: permission.label(),
|
||||
requested,
|
||||
granted,
|
||||
eligible: diagnostic.is_none(),
|
||||
diagnostic,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
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 diagnostic = authorize_plugin_tool(record, tool)
|
||||
.err()
|
||||
.map(|error| error.bounded_message());
|
||||
PluginToolEligibility {
|
||||
name: tool.name.clone(),
|
||||
permission: permission.label(),
|
||||
requested,
|
||||
granted,
|
||||
eligible: diagnostic.is_none(),
|
||||
external_write: tool.external_write,
|
||||
diagnostic,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
PluginStaticInspection {
|
||||
runtime,
|
||||
host_apis,
|
||||
tools,
|
||||
}
|
||||
}
|
||||
|
||||
fn permission_requested(record: &ResolvedPluginRecord, permission: &PluginPermission) -> bool {
|
||||
record
|
||||
.manifest
|
||||
.permissions
|
||||
.iter()
|
||||
.any(|requested| requested == permission)
|
||||
}
|
||||
|
||||
fn grant_allows(record: &ResolvedPluginRecord, permission: &PluginPermission) -> bool {
|
||||
record
|
||||
.grants
|
||||
.permissions
|
||||
.iter()
|
||||
.any(|granted| granted == permission)
|
||||
}
|
||||
|
||||
impl FeatureModule for PluginToolFeature {
|
||||
fn descriptor(&self) -> FeatureDescriptor {
|
||||
let mut descriptor =
|
||||
@@ -1665,6 +1819,47 @@ input_schema = { type = "object", additionalProperties = true }
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_inspection_does_not_read_or_execute_package() {
|
||||
let mut record = record(vec![tool("Echo")]);
|
||||
record.package_path = std::path::PathBuf::from("/no/such/plugin.wasm");
|
||||
record.manifest.runtime = Some(PluginRuntimeManifest {
|
||||
kind: "wasm".to_string(),
|
||||
entry: "plugin.wasm".to_string(),
|
||||
abi: Some("yoi-plugin-wasm-1".to_string()),
|
||||
});
|
||||
|
||||
let inspection = inspect_resolved_plugin_static(&record);
|
||||
|
||||
assert!(inspection.runtime.eligible);
|
||||
assert_eq!(inspection.tools.len(), 1);
|
||||
assert!(inspection.tools[0].eligible);
|
||||
assert!(inspection.statically_eligible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn static_inspection_reports_missing_tool_grant() {
|
||||
let mut record = record(vec![tool("Echo")]);
|
||||
record.manifest.runtime = Some(PluginRuntimeManifest {
|
||||
kind: "wasm".to_string(),
|
||||
entry: "plugin.wasm".to_string(),
|
||||
abi: Some("yoi-plugin-wasm-1".to_string()),
|
||||
});
|
||||
record.grants.permissions = vec![PluginPermission::surface(PluginSurface::Tool)];
|
||||
|
||||
let inspection = inspect_resolved_plugin_static(&record);
|
||||
|
||||
assert!(!inspection.statically_eligible());
|
||||
assert!(!inspection.tools[0].eligible);
|
||||
assert!(
|
||||
inspection.tools[0]
|
||||
.diagnostic
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("grant")
|
||||
);
|
||||
}
|
||||
|
||||
fn write_stored_zip(path: &Path, files: &[(&str, &[u8])]) {
|
||||
let mut out = Vec::new();
|
||||
let mut central = Vec::new();
|
||||
|
||||
Reference in New Issue
Block a user