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

@ -2,7 +2,7 @@ use app_tauri_lib::dto::{
PluginAdminDto, PluginContributionSummaryDto, PluginRuntimeContributionCatalogDto,
PluginRuntimePluginDto,
};
use domain::{PluginContributionSet, PluginLifecycleState, PluginTrustLevel};
use domain::{PluginCapability, PluginContributionSet, PluginLifecycleState, PluginTrustLevel};
#[test]
fn plugin_admin_dto_serialises_exact_contract_shape() {
@ -48,6 +48,7 @@ fn runtime_catalog_dto_carries_bundle_hash_and_contributions() {
bundle_url: "idea-plugin://dev.acme.gitgraph/1.2.3/abc/dist/index.js".to_owned(),
icon_url: None,
content_hash: "abc".to_owned(),
capabilities: vec![PluginCapability::Ui, PluginCapability::Tooling],
contributes: PluginContributionSet::default(),
}],
};
@ -58,6 +59,10 @@ fn runtime_catalog_dto_carries_bundle_hash_and_contributions() {
"idea-plugin://dev.acme.gitgraph/1.2.3/abc/dist/index.js"
);
assert_eq!(value["plugins"][0]["contentHash"], "abc");
assert_eq!(
value["plugins"][0]["capabilities"],
serde_json::json!(["ui", "tooling"])
);
assert!(value["plugins"][0]["contributes"]["menus"]
.as_array()
.unwrap()

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()));

View File

@ -239,6 +239,8 @@ pub struct PluginRuntimePluginDto {
pub icon_url: Option<String>,
/// Content hash.
pub content_hash: String,
/// Public manifest capabilities.
pub capabilities: Vec<domain::PluginCapability>,
/// Contributions.
pub contributes: domain::PluginContributionSet,
}
@ -265,6 +267,7 @@ impl From<application::PluginRuntimePlugin> for PluginRuntimePluginDto {
bundle_url: value.bundle_url,
icon_url: value.icon_url,
content_hash: value.content_hash,
capabilities: value.capabilities,
contributes: value.contributes,
}
}

View File

@ -284,6 +284,8 @@ pub enum PluginCapability {
Ui,
/// External MCP server declarations.
Mcp,
/// Public tooling/build/debug services.
Tooling,
}
/// Plugin command id.
@ -678,4 +680,17 @@ mod tests {
assert!(!PluginLifecycleState::PendingUninstall.is_runtime_active());
assert!(PluginLifecycleState::Enabled.is_runtime_active());
}
#[test]
fn plugin_capabilities_serialize_public_manifest_names() {
assert_eq!(
serde_json::to_value([
PluginCapability::Ui,
PluginCapability::Mcp,
PluginCapability::Tooling
])
.unwrap(),
serde_json::json!(["ui", "mcp", "tooling"])
);
}
}