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:
2026-09-08 15:48:57 +02:00
parent bdc20a134a
commit 86ed0aa00b
38 changed files with 2127 additions and 89 deletions

View File

@ -16,6 +16,7 @@ use domain::{
};
use crate::error::AppError;
use crate::ProjectPluginResolver;
// ---------------------------------------------------------------------------
// CreateSkill
@ -293,13 +294,24 @@ impl ResolveAgentCapabilities {
/// context rendering and `idea_skill_read` authorization.
pub struct AssignedSkillResolver {
skills: Arc<dyn SkillStore>,
plugin_skills: Option<Arc<ProjectPluginResolver>>,
}
impl AssignedSkillResolver {
/// Builds the resolver from the existing skill store port.
#[must_use]
pub fn new(skills: Arc<dyn SkillStore>) -> Self {
Self { skills }
Self {
skills,
plugin_skills: None,
}
}
/// Adds the provider-agnostic project-plugin resolver.
#[must_use]
pub fn with_plugin_skills(mut self, plugin_skills: Arc<ProjectPluginResolver>) -> Self {
self.plugin_skills = Some(plugin_skills);
self
}
/// Resolves the assigned skills for `agent` in manifest order.
@ -342,6 +354,7 @@ impl AssignedSkillResolver {
let snapshot = AssignedSkillSnapshot {
skill_id: skill.id,
scope: skill.scope,
plugin: None,
name: skill.name,
description,
kind: skill.kind,
@ -353,6 +366,52 @@ impl AssignedSkillResolver {
Err(e) => return Err(e.into()),
}
}
if let Some(resolver) = &self.plugin_skills {
let settings = resolver.settings(root).await?;
if let Some(assignments) = settings.assignments_for(agent.id) {
for (offset, reference) in assignments.skills.iter().enumerate() {
let Some(manifest) = resolver
.enabled_manifest(root, &reference.plugin_id)
.await?
else {
continue;
};
let Some(skill) = manifest
.contributes
.skills
.iter()
.find(|s| s.id == reference.skill_id)
else {
continue;
};
let content = match resolver
.read_skill_body(&reference.plugin_id, &skill.path)
.await
{
Ok(content) => content,
Err(AppError::NotFound(_)) => continue,
Err(error) => return Err(error),
};
assigned.push((
AssignedSkillSnapshot {
// Native fields remain populated for backwards wire compatibility;
// plugin identity is authoritative when `plugin` is present.
skill_id: SkillId::from_uuid(uuid::Uuid::nil()),
scope: SkillScope::Project,
plugin: Some(domain::orchestrator::AssignedPluginSkillSource {
plugin_id: reference.plugin_id.clone(),
skill_id: reference.skill_id.clone(),
}),
name: skill.name.clone(),
description: skill.description.clone(),
kind: skill.kind,
assignment_index: (agent.skills.len() + offset) as u32,
},
content,
));
}
}
}
Ok(assigned)
}
@ -382,6 +441,25 @@ impl AssignedSkillResolver {
)))
}
};
if let Some(plugin) = &selected.plugin {
let resolver = self
.plugin_skills
.as_ref()
.ok_or_else(|| AppError::NotFound(format!("assigned plugin skill `{name}`")))?;
let manifest = resolver
.enabled_manifest(root, &plugin.plugin_id)
.await?
.ok_or_else(|| AppError::NotFound(format!("assigned plugin skill `{name}`")))?;
let contribution = manifest
.contributes
.skills
.iter()
.find(|s| s.id == plugin.skill_id)
.ok_or_else(|| AppError::NotFound(format!("assigned plugin skill `{name}`")))?;
return resolver
.read_skill_body(&plugin.plugin_id, &contribution.path)
.await;
}
let skill = self
.skills
.get(selected.scope, root, selected.skill_id)