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

@ -57,6 +57,9 @@ import type {
PluginProjectStructureEntry,
PluginReview,
PluginRuntimeContributionCatalog,
ProjectPluginManifest,
ProjectPluginSettings,
ProjectPluginSettingsView,
PluginToolchainDiagnostic,
PluginUninstallResult,
PluginPublicEvent,
@ -138,6 +141,8 @@ import type {
PluginEventSubscribeInput,
PluginEventUnsubscribeInput,
PluginGateway,
MutateAgentPluginSkillInput,
MutateAgentPluginToolInput,
PluginStorageGateway,
PluginStorageGetInput,
PluginStorageSetInput,
@ -155,6 +160,7 @@ import type {
ReattachAgentChatResult,
RemoteGateway,
ReviewPluginPackageInput,
SetProjectPluginEnabledInput,
SaveOpenCodeProviderProfileInput,
SendAgentChatOptions,
SkillGateway,
@ -4004,6 +4010,8 @@ function isBlanketRule(rule: PermissionSet["rules"][number]) {
*/
export class MockPluginGateway implements PluginGateway {
private plugins: PluginAdmin[] = [];
private projectManifests: ProjectPluginManifest[] = [];
private projectSettings = new Map<string, ProjectPluginSettings>();
private seq = 0;
private summaryFor(_pluginId: string): PluginContributionSummary {
@ -4015,6 +4023,24 @@ export class MockPluginGateway implements PluginGateway {
this.plugins.push(plugin);
}
/** Test/dev helper: seed a full manifest for project capability settings. */
_seedProjectPluginManifest(manifest: ProjectPluginManifest): void {
this.projectManifests.push(structuredClone(manifest));
}
/** Test/dev helper: seed activation/assignment tombstones. */
_seedProjectPluginSettings(projectId: string, settings: ProjectPluginSettings): void {
this.projectSettings.set(projectId, structuredClone(settings));
}
private settingsFor(projectId: string): ProjectPluginSettings {
const existing = this.projectSettings.get(projectId);
if (existing) return existing;
const created: ProjectPluginSettings = { version: 1, plugins: [], agents: [] };
this.projectSettings.set(projectId, created);
return created;
}
async listPlugins(): Promise<PluginAdmin[]> {
return structuredClone(this.plugins);
}
@ -4099,6 +4125,68 @@ export class MockPluginGateway implements PluginGateway {
return { plugins: [] };
}
async listProjectSettings(projectId: string): Promise<ProjectPluginSettingsView> {
return {
settings: structuredClone(this.settingsFor(projectId)),
installed: structuredClone(this.projectManifests),
};
}
async setProjectEnabled(input: SetProjectPluginEnabledInput): Promise<ProjectPluginSettings> {
if (input.enabled && !this.projectManifests.some((plugin) => plugin.id === input.pluginId)) {
throw { code: "NOT_FOUND", message: `plugin ${input.pluginId} not found` } satisfies GatewayError;
}
const settings = this.settingsFor(input.projectId);
const activation = settings.plugins.find((entry) => entry.pluginId === input.pluginId);
if (activation) activation.enabled = input.enabled;
else settings.plugins.push({ pluginId: input.pluginId, enabled: input.enabled });
return structuredClone(settings);
}
private assignmentsFor(projectId: string, agentId: string) {
const settings = this.settingsFor(projectId);
let assignments = settings.agents.find((entry) => entry.agentId === agentId);
if (!assignments) {
assignments = { agentId, skills: [], tools: [] };
settings.agents.push(assignments);
}
return assignments;
}
async assignAgentSkill(input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
const assignments = this.assignmentsFor(input.projectId, input.agentId);
if (!assignments.skills.some((ref) => ref.pluginId === input.pluginId && ref.skillId === input.skillId)) {
assignments.skills.push({ pluginId: input.pluginId, skillId: input.skillId });
}
return structuredClone(this.settingsFor(input.projectId));
}
async unassignAgentSkill(input: MutateAgentPluginSkillInput): Promise<ProjectPluginSettings> {
const assignments = this.assignmentsFor(input.projectId, input.agentId);
assignments.skills = assignments.skills.filter(
(ref) => ref.pluginId !== input.pluginId || ref.skillId !== input.skillId,
);
return structuredClone(this.settingsFor(input.projectId));
}
async assignAgentTool(input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
const assignments = this.assignmentsFor(input.projectId, input.agentId);
if (!assignments.tools.some((ref) =>
ref.pluginId === input.pluginId && ref.serverId === input.serverId && ref.toolName === input.toolName
)) {
assignments.tools.push({ pluginId: input.pluginId, serverId: input.serverId, toolName: input.toolName });
}
return structuredClone(this.settingsFor(input.projectId));
}
async unassignAgentTool(input: MutateAgentPluginToolInput): Promise<ProjectPluginSettings> {
const assignments = this.assignmentsFor(input.projectId, input.agentId);
assignments.tools = assignments.tools.filter((ref) =>
ref.pluginId !== input.pluginId || ref.serverId !== input.serverId || ref.toolName !== input.toolName
);
return structuredClone(this.settingsFor(input.projectId));
}
async openPluginsFolder(_pluginId?: string): Promise<void> {
// No filesystem in the mock — no-op.
}