feat(plugins): load activation scope from plugin manifest

Plugins can now declare activationScope ("app" | "project") in their
manifest; loader/runtime honor it to defer activation of project-scoped
plugins until a project is focused instead of activating everything at
app bootstrap. Bumps sdk/IdeaSDK to the commit that adds the field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-03 14:44:08 +02:00
parent 863d9b7277
commit e2da2d911e
16 changed files with 513 additions and 34 deletions

View File

@ -754,6 +754,7 @@ mod tests {
icon: None,
trust_level: PluginTrustLevel::Full,
capabilities: Vec::new(),
activation_scope: domain::PluginActivationScope::default(),
contributes: PluginContributionSet::default(),
})
}

View File

@ -49,6 +49,7 @@ fn runtime_catalog_dto_carries_bundle_hash_and_contributions() {
icon_url: None,
content_hash: "abc".to_owned(),
capabilities: vec![PluginCapability::Ui, PluginCapability::Tooling],
activation_scope: domain::PluginActivationScope::Project,
contributes: PluginContributionSet::default(),
}],
};
@ -63,6 +64,7 @@ fn runtime_catalog_dto_carries_bundle_hash_and_contributions() {
value["plugins"][0]["capabilities"],
serde_json::json!(["ui", "tooling"])
);
assert_eq!(value["plugins"][0]["activationScope"], "project");
assert!(value["plugins"][0]["contributes"]["menus"]
.as_array()
.unwrap()

View File

@ -153,6 +153,8 @@ pub struct PluginRuntimePlugin {
pub content_hash: String,
/// Public manifest capabilities.
pub capabilities: Vec<domain::PluginCapability>,
/// Manifest-declared activation scope.
pub activation_scope: domain::PluginActivationScope,
/// Contributions.
pub contributes: PluginContributionSet,
}
@ -2207,6 +2209,7 @@ impl ListPlugins {
icon: None,
trust_level: PluginTrustLevel::Full,
capabilities: Vec::new(),
activation_scope: domain::PluginActivationScope::default(),
contributes: PluginContributionSet::default(),
};
out.push(admin_from_descriptor(
@ -2793,6 +2796,7 @@ async fn runtime_plugin_from_entry(
icon_url,
content_hash: descriptor.registry.content_hash.as_str().to_owned(),
capabilities: descriptor.manifest.capabilities,
activation_scope: descriptor.manifest.activation_scope,
contributes: descriptor.manifest.contributes,
})
}
@ -3006,6 +3010,8 @@ struct RawManifest {
trust_level: String,
#[serde(default)]
capabilities: Vec<String>,
#[serde(default)]
activation_scope: domain::PluginActivationScope,
contributes: RawContributes,
}
@ -3158,6 +3164,7 @@ impl PluginManifestValidator for JsonPluginManifestValidator {
icon,
trust_level: PluginTrustLevel::Full,
capabilities,
activation_scope: raw.activation_scope,
contributes,
})
}

View File

@ -242,6 +242,8 @@ pub struct PluginRuntimePluginDto {
pub content_hash: String,
/// Public manifest capabilities.
pub capabilities: Vec<domain::PluginCapability>,
/// Manifest-declared activation scope.
pub activation_scope: domain::PluginActivationScope,
/// Contributions.
pub contributes: domain::PluginContributionSet,
}
@ -269,6 +271,7 @@ impl From<application::PluginRuntimePlugin> for PluginRuntimePluginDto {
icon_url: value.icon_url,
content_hash: value.content_hash,
capabilities: value.capabilities,
activation_scope: value.activation_scope,
contributes: value.contributes,
}
}

View File

@ -217,13 +217,13 @@ pub use system_permissions::{
};
pub use plugin::{
ContentHash, CustomPluginLayout, PluginBundleUrl, PluginCapability, PluginCommandId,
PluginContributionSet, PluginDescriptor, PluginError, PluginId, PluginInstallSource,
PluginLayoutContribution, PluginLayoutType, PluginLifecycleState, PluginManifest,
PluginMcpServerContribution, PluginMcpServerId, PluginMcpServerSpec, PluginMcpStatus,
PluginMcpStatusSet, PluginMenuItemContribution, PluginPackageRef, PluginRegistry,
PluginRegistryEntry, PluginTopLevelMenuContribution, PluginTrustLevel, PluginVersion,
RelativePath, RemovalOutcome, StagedPluginPackage,
ContentHash, CustomPluginLayout, PluginActivationScope, PluginBundleUrl, PluginCapability,
PluginCommandId, PluginContributionSet, PluginDescriptor, PluginError, PluginId,
PluginInstallSource, PluginLayoutContribution, PluginLayoutType, PluginLifecycleState,
PluginManifest, PluginMcpServerContribution, PluginMcpServerId, PluginMcpServerSpec,
PluginMcpStatus, PluginMcpStatusSet, PluginMenuItemContribution, PluginPackageRef,
PluginRegistry, PluginRegistryEntry, PluginTopLevelMenuContribution, PluginTrustLevel,
PluginVersion, RelativePath, RemovalOutcome, StagedPluginPackage,
};
pub use sandbox::{

View File

@ -288,6 +288,22 @@ pub enum PluginCapability {
Tooling,
}
/// Manifest-declared runtime activation scope.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PluginActivationScope {
/// Activate at app bootstrap, without requiring a focused project.
App,
/// Wait until a project is focused before the first activation.
Project,
}
impl Default for PluginActivationScope {
fn default() -> Self {
Self::App
}
}
/// Plugin command id.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
@ -495,6 +511,9 @@ pub struct PluginManifest {
/// Capabilities.
#[serde(default)]
pub capabilities: Vec<PluginCapability>,
/// Activation scope. Missing in older manifests means app-level activation.
#[serde(default)]
pub activation_scope: PluginActivationScope,
/// Contributions.
pub contributes: PluginContributionSet,
}
@ -693,4 +712,13 @@ mod tests {
serde_json::json!(["ui", "mcp", "tooling"])
);
}
#[test]
fn plugin_activation_scope_defaults_to_app_and_serializes_public_names() {
assert_eq!(PluginActivationScope::default(), PluginActivationScope::App);
assert_eq!(
serde_json::to_value(PluginActivationScope::Project).unwrap(),
serde_json::json!("project")
);
}
}