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

@ -236,14 +236,15 @@ pub use system_permissions::{
};
pub use plugin::{
ContentHash, CustomPluginLayout, PluginActivationScope, PluginBundleUrl, PluginCapability,
PluginCommandId, PluginContributionSet, PluginDescriptor, PluginError, PluginId,
PluginInstallSource, PluginLayoutContribution, PluginLayoutType, PluginLifecycleState,
PluginManifest, PluginMcpServerContribution, PluginMcpServerId, PluginMcpServerSpec,
PluginMcpStatus, PluginMcpStatusSet, PluginMenuItemContribution, PluginPackageRef,
PluginRegistry, PluginRegistryEntry, PluginSlashCommandContribution,
PluginTopLevelMenuContribution, PluginTrustLevel, PluginVersion, RelativePath, RemovalOutcome,
StagedPluginPackage,
AgentPluginAssignments, ContentHash, CustomPluginLayout, PluginActivationScope,
PluginBundleUrl, PluginCapability, PluginCommandId, PluginContributionSet, PluginDescriptor,
PluginError, PluginId, PluginInstallSource, PluginLayoutContribution, PluginLayoutType,
PluginLifecycleState, PluginManifest, PluginMcpServerContribution, PluginMcpServerId,
PluginMcpServerSpec, PluginMcpStatus, PluginMcpStatusSet, PluginMenuItemContribution,
PluginPackageRef, PluginRegistry, PluginRegistryEntry, PluginSkillContribution, PluginSkillId,
PluginSkillRef, PluginSlashCommandContribution, PluginToolRef, PluginTopLevelMenuContribution,
PluginTrustLevel, PluginVersion, ProjectPluginActivation, ProjectPluginSettings, RelativePath,
RemovalOutcome, StagedPluginPackage,
};
pub use sandbox::{
@ -270,8 +271,9 @@ pub use ports::{
PluginManifestError, PluginManifestValidator, PluginMcpError, PluginMcpSupervisor,
PluginPackageStore, PluginRegistryError, PluginRegistryStore, PluginStorageError,
PluginStorageStore, PluginStoreError, PreparedContext, ProcessError, ProcessSpawner,
ProfileStore, ProjectStore, ProviderModelCatalogue, PtyError, PtyHandle, PtyPort, RemoteError,
RemoteHost, RemotePath, RuntimeError, RuntimePermissionProbe, ScheduledTask, Scheduler,
SpawnSpec, SprintStore, SprintStoreError, StoreError, StructuredSessionEnvironment,
StructuredSessionEnvironmentPreparer, SystemPermissionStore, TemplateStore, WindowStateStore,
ProfileStore, ProjectPluginSettingsStore, ProjectStore, ProviderModelCatalogue, PtyError,
PtyHandle, PtyPort, RemoteError, RemoteHost, RemotePath, RuntimeError, RuntimePermissionProbe,
ScheduledTask, Scheduler, SpawnSpec, SprintStore, SprintStoreError, StoreError,
StructuredSessionEnvironment, StructuredSessionEnvironmentPreparer, SystemPermissionStore,
TemplateStore, WindowStateStore,
};

View File

@ -17,8 +17,20 @@ use crate::conversation::ConversationParty;
use crate::ids::{AgentId, NodeId, SkillId};
use crate::live_state::WorkStatus;
use crate::mailbox::TicketId;
use crate::plugin::{PluginId, PluginSkillId};
use crate::skill::{SkillKind, SkillScope};
/// Plugin origin for a resolved assigned skill. `None` on
/// [`AssignedSkillSnapshot::plugin`] denotes a native IdeA skill.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AssignedPluginSkillSource {
/// Owning plugin.
pub plugin_id: PluginId,
/// Manifest-local skill id.
pub skill_id: PluginSkillId,
}
/// Current schema version for orchestration capabilities injected into an
/// agent's effective runtime context.
pub const ORCHESTRATION_CAPABILITY_SNAPSHOT_VERSION: u32 = 1;
@ -35,6 +47,9 @@ pub struct AssignedSkillSnapshot {
pub skill_id: SkillId,
/// Store scope used to resolve the skill body.
pub scope: SkillScope,
/// Plugin origin. Absent for native IdeA skills.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plugin: Option<AssignedPluginSkillSource>,
/// Display name shown to the model and accepted by `idea_skill_read`.
pub name: String,
/// One-line affordance description shown in the model context.

View File

@ -370,6 +370,41 @@ impl PluginMcpServerId {
}
}
/// Stable id of a skill contributed by a plugin manifest.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PluginSkillId(String);
impl PluginSkillId {
/// Validates and creates a plugin skill id.
pub fn new(raw: impl Into<String>) -> Result<Self, PluginError> {
PluginCommandId::new(raw).map(|v| Self(v.0))
}
/// Returns the raw id.
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
/// Read-only skill contribution whose body lives inside the plugin package.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginSkillContribution {
/// Stable id within the plugin.
pub id: PluginSkillId,
/// Agent-facing name accepted by `idea_skill_read`.
pub name: String,
/// Short affordance shown in the injected skill catalogue.
pub description: String,
/// Capability nature.
#[serde(default)]
pub kind: crate::SkillKind,
/// Package-relative Markdown file.
pub path: RelativePath,
}
/// Top-level menu contribution.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -484,6 +519,9 @@ pub struct PluginMcpServerContribution {
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginContributionSet {
/// Read-only agent skills shipped in the package.
#[serde(default)]
pub skills: Vec<PluginSkillContribution>,
/// Top-level menus.
#[serde(default)]
pub menus: Vec<PluginTopLevelMenuContribution>,
@ -501,6 +539,94 @@ pub struct PluginContributionSet {
pub mcp_servers: Vec<PluginMcpServerContribution>,
}
/// Project-scoped plugin activation entry. It is retained when a plugin is
/// temporarily uninstalled so reinstalling does not lose user intent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectPluginActivation {
/// Globally-installed plugin id.
pub plugin_id: PluginId,
/// Whether this project may expose the plugin's capabilities.
pub enabled: bool,
}
/// Reference to one plugin skill assigned to an agent.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginSkillRef {
/// Owning plugin.
pub plugin_id: PluginId,
/// Skill id within that plugin.
pub skill_id: PluginSkillId,
}
/// Stable namespace of one externally-provided MCP tool.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PluginToolRef {
/// Owning plugin.
pub plugin_id: PluginId,
/// MCP server id within that plugin.
pub server_id: PluginMcpServerId,
/// Tool name advertised by the server.
pub tool_name: String,
}
/// Plugin capabilities explicitly assigned to one project agent.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentPluginAssignments {
/// Agent receiving the capabilities.
pub agent_id: crate::AgentId,
/// Assigned plugin skills, in injection order.
#[serde(default)]
pub skills: Vec<PluginSkillRef>,
/// Assigned external MCP tools.
#[serde(default)]
pub tools: Vec<PluginToolRef>,
}
/// Versioned project-owned plugin settings persisted under
/// `.ideai/project-plugins.json`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectPluginSettings {
/// Schema version.
pub version: u32,
/// Explicit project activation choices.
#[serde(default)]
pub plugins: Vec<ProjectPluginActivation>,
/// Per-agent assignments. Dangling entries are intentional tombstones.
#[serde(default)]
pub agents: Vec<AgentPluginAssignments>,
}
impl Default for ProjectPluginSettings {
fn default() -> Self {
Self {
version: 1,
plugins: Vec::new(),
agents: Vec::new(),
}
}
}
impl ProjectPluginSettings {
/// Whether a plugin is explicitly enabled for this project.
#[must_use]
pub fn is_enabled(&self, plugin_id: &PluginId) -> bool {
self.plugins
.iter()
.any(|p| &p.plugin_id == plugin_id && p.enabled)
}
/// Assignments for an agent, if any.
#[must_use]
pub fn assignments_for(&self, agent_id: crate::AgentId) -> Option<&AgentPluginAssignments> {
self.agents.iter().find(|a| a.agent_id == agent_id)
}
}
/// Validated plugin manifest.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]

View File

@ -446,6 +446,19 @@ pub trait PluginPackageStore: Send + Sync {
package: &PluginPackageRef,
) -> Result<PluginManifestBytes, PluginStoreError>;
/// Reads a package-relative file from an installed plugin, confined to its
/// canonical package root.
async fn read_installed_file(
&self,
plugin_id: &crate::PluginId,
path: &crate::RelativePath,
) -> Result<Vec<u8>, PluginStoreError> {
let _ = (plugin_id, path);
Err(PluginStoreError::Invalid(
"installed package file reads are not supported by this adapter".to_owned(),
))
}
/// Stages an archive for validation.
async fn install_from_archive(
&self,
@ -496,6 +509,23 @@ pub trait PluginRegistryStore: Send + Sync {
async fn save_registry(&self, registry: &PluginRegistry) -> Result<(), PluginRegistryError>;
}
/// Store for one project's plugin activation and agent assignments.
#[async_trait]
pub trait ProjectPluginSettingsStore: Send + Sync {
/// Loads settings, returning an empty version-1 document when missing.
async fn load(
&self,
project_root: &crate::ProjectPath,
) -> Result<crate::ProjectPluginSettings, StoreError>;
/// Atomically persists the complete document.
async fn save(
&self,
project_root: &crate::ProjectPath,
settings: &crate::ProjectPluginSettings,
) -> Result<(), StoreError>;
}
/// Manifest parser and validator.
pub trait PluginManifestValidator: Send + Sync {
/// Validates raw manifest bytes.