feat(plugins): paramètres projet plugins + attribution skills/tools aux agents — #282 (manifeste accepte contributes.skills IdeA-native multi-harness; store .ideai/project-plugins.json v1: enable plugin par projet + assignments skills/tools par agent; refs + use cases settings/assign/unassign; resolver skills pour idea_skill_read et injection catalogue lancement agent; permissions MCP dynamiques projetées par harness Claude Code/Codex/OpenCode; tombstones uninstall, hot reload recalcule catalogue et marque assignments cassés; frontend: Settings projet Plugins, détail capabilities, attribution agent, catalogue permissions; bump pointeur IdeaSDK 6eb8db3; QA verte: cargo test -p application, project_plugin_settings 3/3, dto_plugins 2/2, structured_launch_d3 26/26, catalogue mcp 7/7, frontend 5 tests ProjectPluginSettings/plugin, npm build frontend)
This commit is contained in:
@ -27,6 +27,8 @@ use crate::{AppError, SpawnBackgroundCommand, SpawnBackgroundCommandInput};
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PluginContributionSummary {
|
||||
/// Agent skill count.
|
||||
pub skills: usize,
|
||||
/// Top-level menu count.
|
||||
pub top_level_menus: usize,
|
||||
/// Menu item count.
|
||||
@ -42,6 +44,7 @@ pub struct PluginContributionSummary {
|
||||
impl From<&PluginContributionSet> for PluginContributionSummary {
|
||||
fn from(c: &PluginContributionSet) -> Self {
|
||||
Self {
|
||||
skills: c.skills.len(),
|
||||
top_level_menus: c.menus.len(),
|
||||
menu_items: c.menu_items.len(),
|
||||
slash_commands: c.slash_commands.len(),
|
||||
@ -3310,6 +3313,8 @@ struct RawEngines {
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RawContributes {
|
||||
#[serde(default)]
|
||||
skills: Vec<RawPluginSkill>,
|
||||
#[serde(default)]
|
||||
menus: Vec<RawMenu>,
|
||||
#[serde(default)]
|
||||
@ -3322,6 +3327,17 @@ struct RawContributes {
|
||||
mcp_servers: Vec<RawMcpServer>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RawPluginSkill {
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
#[serde(default)]
|
||||
kind: domain::SkillKind,
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct RawMenu {
|
||||
@ -3480,6 +3496,33 @@ fn validate_contributes(raw: RawContributes) -> Result<PluginContributionSet, Pl
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
let skills = raw
|
||||
.skills
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
insert(&s.id)?;
|
||||
if s.name.trim().is_empty() || s.description.trim().is_empty() {
|
||||
return Err(PluginManifestError::Invalid(
|
||||
"skills[].name and skills[].description are required".to_owned(),
|
||||
));
|
||||
}
|
||||
let path = domain::RelativePath::new(s.path)
|
||||
.map_err(|e| PluginManifestError::Invalid(e.to_string()))?;
|
||||
if !path.as_str().ends_with(".md") {
|
||||
return Err(PluginManifestError::Invalid(
|
||||
"skills[].path must point to a .md file".to_owned(),
|
||||
));
|
||||
}
|
||||
Ok(domain::PluginSkillContribution {
|
||||
id: domain::PluginSkillId::new(s.id)
|
||||
.map_err(|e| PluginManifestError::Invalid(e.to_string()))?,
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
kind: s.kind,
|
||||
path,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let menus = raw
|
||||
.menus
|
||||
.into_iter()
|
||||
@ -3617,6 +3660,7 @@ fn validate_contributes(raw: RawContributes) -> Result<PluginContributionSet, Pl
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(PluginContributionSet {
|
||||
skills,
|
||||
menus,
|
||||
menu_items,
|
||||
slash_commands,
|
||||
@ -3688,6 +3732,7 @@ mod tests {
|
||||
"trustLevel": "full",
|
||||
"capabilities": ["ui", "mcp", "tooling"],
|
||||
"contributes": {
|
||||
"skills": [{"id":"build-debug","name":"build-debug","description":"Diagnose build failures.","kind":"workflow","path":"skills/build-debug/SKILL.md"}],
|
||||
"menus": [{"id":"dev.acme.menu","label":"Graph","topLevel":true}],
|
||||
"menuItems": [{"id":"dev.acme.open","targetMenuId":"panels","label":"Open","command":"dev.acme.open"}],
|
||||
"slashCommands": [{"name":"/graph","shortDescription":"Open graph tools","command":"dev.acme.open","requiresConfirmation":true}],
|
||||
@ -4020,12 +4065,35 @@ mod tests {
|
||||
]
|
||||
);
|
||||
assert_eq!(m.contributes.layouts.len(), 1);
|
||||
assert_eq!(m.contributes.skills.len(), 1);
|
||||
assert_eq!(m.contributes.skills[0].id.as_str(), "build-debug");
|
||||
assert_eq!(m.contributes.skills[0].name, "build-debug");
|
||||
assert_eq!(
|
||||
m.contributes.skills[0].path.as_str(),
|
||||
"skills/build-debug/SKILL.md"
|
||||
);
|
||||
assert_eq!(m.contributes.mcp_servers.len(), 1);
|
||||
assert_eq!(m.contributes.slash_commands.len(), 1);
|
||||
assert_eq!(m.contributes.slash_commands[0].name, "/graph");
|
||||
assert!(m.contributes.slash_commands[0].requires_confirmation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_plugin_skill_outside_markdown_contract() {
|
||||
let mut value: serde_json::Value = serde_json::from_slice(&valid_manifest()).unwrap();
|
||||
value["contributes"]["skills"][0]["path"] = serde_json::json!("skills/build.txt");
|
||||
let error = validator()
|
||||
.validate(
|
||||
&serde_json::to_vec(&value).unwrap(),
|
||||
&domain::PluginPackageRef {
|
||||
plugin_id: None,
|
||||
root: "x".into(),
|
||||
},
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("must point to a .md file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_manifest_capability() {
|
||||
let mut value: serde_json::Value = serde_json::from_slice(&valid_manifest()).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user