feat(agents,plugins): surface unifiée Capacités de l'agent — skills et tools MCP natifs + plugins #296 (domain: PluginMcpToolMetadata dans PluginMcpServerContribution, validation hôte alignée SDK — name requis non vide, description optionnelle non vide, doublons rejetés; application: McpToolPermissionCatalogue.vue structurée additive tools {name, description, access read|writeAction} enrichie des descriptions canoniques infrastructure orchestrator mcp tools, use case SetAgentMcpToolAllowed — toggle atomique idempotente d'un tool natif dans l'allowlist explicite d'un agent, refus unknown tool sans mutation, overrides vides retirés; backend DTO: catalogue.tools additive + SetAgentMcpToolAllowedRequestDto, wire camelCase; app-tauri + web-server: commande set_agent_mcp_tool_allowed; frontend: AgentPluginCapabilities devient la surface unifiée — skills natifs assign/unassign, tools natifs via setAgentMcpToolAllowed (catalogue structuré avec fallback legacy), skills/tools plugins avec metadata statiques mcpServers[].tools et saisie manuelle fallback pour serveurs sans catalogue, tombstones indisponibles, bannière redémarrage requis si agent actif; AgentsPanel retire l'ancienne section Skills séparée au profit de la surface unifiée; ProjectPluginsPanel recentré enable/disable avec renvoi Capacités des agents; adapters Tauri/HTTP/mock + permission.test.ts; QA verte reconfirmée par Git: application mcp_tool_permissions 5/5 + plugin::tests 37/37, backend ciblés 3/3 + checks domain/application/backend, app-tauri dto 33/33 + dto_agents 30/30 + dto_plugins 2/2, check web-server, npm test 35/35 + 65/65, npm run build; suites complètes rouges classées non bloquantes par QA: web-server parallèle too many open files + 7 PTY série passant individuellement, frontend 32 échecs sur 6 fichiers web non modifiés reproduits isolément)

This commit is contained in:
2026-09-09 19:52:38 +02:00
parent 6e4d9eb47e
commit e283f4a9e4
22 changed files with 1152 additions and 301 deletions

View File

@ -11,6 +11,7 @@ import { HttpInvoker, defaultFetch } from "./httpInvoker";
import {
HttpProjectGateway,
HttpGitGateway,
HttpPermissionGateway,
HttpTemplateGateway,
} from "./requestResponseGateways";
@ -191,6 +192,42 @@ describe("defaultFetch", () => {
});
describe("request/response gateways preserve the Tauri command contract", () => {
it("HttpPermissionGateway sends the granular MCP tool toggle DTO", async () => {
const http = new HttpInvoker({ baseUrl: "https://h" });
const response = {
version: 1,
catalogue: {
readOnlyTools: [],
writeActionTools: ["idea_ticket_update"],
tools: [{
name: "idea_ticket_update",
description: "Update a ticket.",
access: "writeAction",
}],
},
projectDefault: null,
agents: [],
};
const spy = vi.spyOn(http, "invoke").mockResolvedValue(response as never);
await expect(
new HttpPermissionGateway(http).setAgentMcpToolAllowed(
"project-1",
"agent-1",
"idea_ticket_update",
true,
),
).resolves.toEqual(response);
expect(spy).toHaveBeenCalledWith("set_agent_mcp_tool_allowed", {
request: {
projectId: "project-1",
agentId: "agent-1",
toolName: "idea_ticket_update",
allowed: true,
},
});
});
it("HttpProjectGateway.createProject maps to create_project with a request envelope", async () => {
const http = new HttpInvoker({ baseUrl: "https://h" });
const spy = vi.spyOn(http, "invoke").mockResolvedValue({ id: "p1" } as never);

View File

@ -505,6 +505,16 @@ export class HttpPermissionGateway implements PermissionGateway {
request: { projectId, agentId, policy },
});
}
setAgentMcpToolAllowed(
projectId: string,
agentId: string,
toolName: string,
allowed: boolean,
): Promise<ProjectMcpToolPermissions> {
return this.http.invoke<ProjectMcpToolPermissions>("set_agent_mcp_tool_allowed", {
request: { projectId, agentId, toolName, allowed },
});
}
}
export class HttpWorkStateGateway implements WorkStateGateway {

View File

@ -3080,6 +3080,31 @@ export class MockPermissionGateway implements PermissionGateway {
if (policy) doc.agents.push({ agentId, policy: structuredClone(policy) });
return structuredClone(doc);
}
async setAgentMcpToolAllowed(
projectId: string,
agentId: string,
toolName: string,
allowed: boolean,
): Promise<ProjectMcpToolPermissions> {
const doc = this.mcpDoc(projectId);
const known = new Set([
...doc.catalogue.readOnlyTools,
...doc.catalogue.writeActionTools,
]);
if (!known.has(toolName)) {
throw { code: "INVALID", message: `unknown MCP tool: ${toolName}` };
}
const existing = doc.agents.find((entry) => entry.agentId === agentId);
const allowedTools = new Set(existing?.policy.allowedTools ?? []);
if (allowed) allowedTools.add(toolName);
else allowedTools.delete(toolName);
doc.agents = doc.agents.filter((entry) => entry.agentId !== agentId);
if (allowedTools.size > 0) {
doc.agents.push({ agentId, policy: { allowedTools: [...allowedTools] } });
}
return structuredClone(doc);
}
}
export class MockWorkStateGateway implements WorkStateGateway {

View File

@ -0,0 +1,30 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const invoke = vi.fn();
vi.mock("@tauri-apps/api/core", () => ({
invoke: (...args: unknown[]) => invoke(...args),
}));
import { TauriPermissionGateway } from "./permission";
describe("TauriPermissionGateway invoke payloads", () => {
beforeEach(() => invoke.mockReset().mockResolvedValue({}));
it("wraps the granular MCP tool toggle in the request DTO", async () => {
await new TauriPermissionGateway().setAgentMcpToolAllowed(
"project-1",
"agent-1",
"idea_ticket_update",
false,
);
expect(invoke).toHaveBeenCalledWith("set_agent_mcp_tool_allowed", {
request: {
projectId: "project-1",
agentId: "agent-1",
toolName: "idea_ticket_update",
allowed: false,
},
});
});
});

View File

@ -107,4 +107,15 @@ export class TauriPermissionGateway implements PermissionGateway {
{ request: { projectId, agentId, policy } },
);
}
setAgentMcpToolAllowed(
projectId: string,
agentId: string,
toolName: string,
allowed: boolean,
): Promise<ProjectMcpToolPermissions> {
return invoke<ProjectMcpToolPermissions>("set_agent_mcp_tool_allowed", {
request: { projectId, agentId, toolName, allowed },
});
}
}