feat(sdk,plugins): ajoute capability tooling et services runtime publics (workspace/terminal/tasks)

- capability manifeste tooling supportée backend + transport runtime catalog
- ctx.services publique côté SDK/runtime, gated par tooling
- surface honnête: workspace, terminal, et tasks en observation/contrôle uniquement
- docs/exemple/tests mis à jour
- QA PASS sur feature/sdk-plugin-tooling-build-debug-surface
This commit is contained in:
2026-08-02 00:31:15 +02:00
parent efaeb31a38
commit 033e9a86d5
21 changed files with 748 additions and 10 deletions

View File

@ -146,6 +146,8 @@ pub struct PluginRuntimePlugin {
pub icon_url: Option<String>,
/// Content hash.
pub content_hash: String,
/// Public manifest capabilities.
pub capabilities: Vec<domain::PluginCapability>,
/// Contributions.
pub contributes: PluginContributionSet,
}
@ -824,6 +826,7 @@ async fn runtime_plugin_from_entry(
bundle_url: bundle,
icon_url,
content_hash: descriptor.registry.content_hash.as_str().to_owned(),
capabilities: descriptor.manifest.capabilities,
contributes: descriptor.manifest.contributes,
})
}
@ -1170,6 +1173,7 @@ impl PluginManifestValidator for JsonPluginManifestValidator {
.map(|c| match c.as_str() {
"ui" => Ok(domain::PluginCapability::Ui),
"mcp" => Ok(domain::PluginCapability::Mcp),
"tooling" => Ok(domain::PluginCapability::Tooling),
_ => Err(PluginManifestError::Invalid(format!(
"unknown capability: {c}"
))),
@ -1377,7 +1381,7 @@ mod tests {
"engines": {"idea": ">=0.1.0 <1.0.0"},
"main": "dist/index.js",
"trustLevel": "full",
"capabilities": ["ui", "mcp"],
"capabilities": ["ui", "mcp", "tooling"],
"contributes": {
"menus": [{"id":"dev.acme.menu","label":"Graph","topLevel":true}],
"menuItems": [{"id":"dev.acme.open","targetMenuId":"panels","label":"Open","command":"dev.acme.open"}],
@ -1630,10 +1634,39 @@ mod tests {
)
.unwrap();
assert_eq!(m.id.as_str(), "dev.acme.gitgraph");
assert_eq!(
m.capabilities,
vec![
domain::PluginCapability::Ui,
domain::PluginCapability::Mcp,
domain::PluginCapability::Tooling,
]
);
assert_eq!(m.contributes.layouts.len(), 1);
assert_eq!(m.contributes.mcp_servers.len(), 1);
}
#[test]
fn rejects_unknown_manifest_capability() {
let mut value: serde_json::Value = serde_json::from_slice(&valid_manifest()).unwrap();
value["capabilities"] = serde_json::json!(["ui", "android"]);
let err = validator()
.validate(
&serde_json::to_vec(&value).unwrap(),
&domain::PluginPackageRef {
plugin_id: None,
root: "x".into(),
},
)
.unwrap_err();
assert_eq!(
err,
PluginManifestError::Invalid("unknown capability: android".to_owned())
);
}
#[test]
fn rejects_unsafe_main_path_and_non_full_trust() {
let mut value: serde_json::Value = serde_json::from_slice(&valid_manifest()).unwrap();
@ -1695,6 +1728,28 @@ mod tests {
}
}
#[tokio::test]
async fn runtime_catalog_carries_manifest_capabilities() {
let packages = Arc::new(FakePackages::with_manifest(valid_manifest()));
let registry = Arc::new(FakeRegistry {
registry: Mutex::new(registry_with(PluginLifecycleState::Enabled)),
});
let usecase =
ListPluginRuntimeContributions::new(packages, registry, Arc::new(validator()));
let catalog = usecase.execute().await.unwrap();
assert_eq!(catalog.plugins.len(), 1);
assert_eq!(
catalog.plugins[0].capabilities,
vec![
domain::PluginCapability::Ui,
domain::PluginCapability::Mcp,
domain::PluginCapability::Tooling,
]
);
}
#[tokio::test]
async fn runtime_catalog_marks_invalid_active_plugin_and_keeps_bootstrap_alive() {
let packages = Arc::new(FakePackages::with_manifest(br#"{"broken":true}"#.to_vec()));