feat(frontend): surface MCP tool permissions per agent — Permissions panel (#82 lot UX/F)

Adds the "Tools MCP IdeA" tab to the project Permissions panel, alongside
the existing "Système" (file/command) tab. Lets the user grant/revoke MCP
tool capabilities per agent or project-wide default, grouped by domain
(Lecture projet, Lecture tickets, Délégation agents, Contexte et mémoire,
Tickets, Travail et exécution, Skills) instead of a flat 25-checkbox list,
per the UX conception in carnet #82.

- domain/ports/adapters (Tauri, HTTP, mock): wire get_mcp_tool_permissions,
  update_project_mcp_tool_permissions, update_agent_mcp_tool_permissions
  (already merged backend API, #82 lots B1-B4) onto PermissionGateway.
- useMcpToolPermissions: view-model owning the durable MCP tool policy
  document, distinct from the file/command permissions in usePermissions.
- McpToolPermissionsPanel: target selector (Défaut projet + agents with
  Hérité/Override badges) and grouped editor — inherited agents are
  read-only until "Créer un override" (prefilled with the effective
  allowlist), per-row Ajouté/Retiré diffing against the project default,
  inline confirmation before granting a write tool at project-default
  level, and unsaved-draft protection on target change.
- mcpToolGroups.ts: presentational-only domain grouping and short French
  labels — the read/write classification itself always comes from the
  backend-provided catalogue, never hardcoded here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 23:36:02 +02:00
parent 2db37a50fa
commit c3078875f4
10 changed files with 1381 additions and 2 deletions

View File

@ -32,9 +32,11 @@ import type {
MemoryIndexEntry,
MemoryLink,
MemoryType,
McpToolPolicy,
ModelServerCommandPreview,
PermissionSet,
Project,
ProjectMcpToolPermissions,
ProjectPermissions,
ProjectWorkState,
ProfileAvailability,
@ -344,6 +346,26 @@ export class HttpPermissionGateway implements PermissionGateway {
request: { projectId, agentId },
});
}
getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions> {
return this.http.invoke<ProjectMcpToolPermissions>("get_mcp_tool_permissions", { projectId });
}
updateProjectMcpToolPermissions(
projectId: string,
policy: McpToolPolicy | null,
): Promise<ProjectMcpToolPermissions> {
return this.http.invoke<ProjectMcpToolPermissions>("update_project_mcp_tool_permissions", {
request: { projectId, policy },
});
}
updateAgentMcpToolPermissions(
projectId: string,
agentId: string,
policy: McpToolPolicy | null,
): Promise<ProjectMcpToolPermissions> {
return this.http.invoke<ProjectMcpToolPermissions>("update_agent_mcp_tool_permissions", {
request: { projectId, agentId, policy },
});
}
}
export class HttpWorkStateGateway implements WorkStateGateway {

View File

@ -31,11 +31,14 @@ import type {
MemoryIndexEntry,
MemoryLink,
MemoryType,
McpToolCatalogue,
McpToolPolicy,
EffectivePermissions,
PairedDevice,
PairingCode,
PermissionSet,
Project,
ProjectMcpToolPermissions,
ProjectPermissions,
ProjectWorkState,
ProfileAvailability,
@ -2151,6 +2154,79 @@ export class MockPermissionGateway implements PermissionGateway {
fallback: mostRestrictive(project?.fallback, agent?.fallback),
};
}
// ── MCP tool permissions (ticket #82) — mirrors the backend catalogue in
// `crates/infrastructure/src/orchestrator/mcp/tools.rs`, a separate durable
// document from the file/command permissions above. ─────────────────────
private mcpCatalogue: McpToolCatalogue = {
readOnlyTools: [
"idea_list_agents",
"idea_context_read",
"idea_memory_read",
"idea_skill_read",
"idea_workstate_read",
"idea_ticket_read",
"idea_ticket_list",
"idea_ticket_read_carnet",
"idea_sprint_list",
],
writeActionTools: [
"idea_ask_agent",
"idea_run_in_background",
"idea_launch_agent",
"idea_stop_agent",
"idea_update_context",
"idea_context_propose",
"idea_memory_write",
"idea_workstate_set",
"idea_create_skill",
"idea_ticket_create",
"idea_ticket_update",
"idea_ticket_update_status",
"idea_ticket_update_priority",
"idea_ticket_update_carnet",
"idea_ticket_link",
"idea_ticket_unlink",
],
};
private mcpDocs = new Map<string, ProjectMcpToolPermissions>();
private mcpDoc(projectId: string): ProjectMcpToolPermissions {
if (!this.mcpDocs.has(projectId)) {
this.mcpDocs.set(projectId, {
version: 1,
catalogue: this.mcpCatalogue,
projectDefault: null,
agents: [],
});
}
return this.mcpDocs.get(projectId)!;
}
async getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions> {
return structuredClone(this.mcpDoc(projectId));
}
async updateProjectMcpToolPermissions(
projectId: string,
policy: McpToolPolicy | null,
): Promise<ProjectMcpToolPermissions> {
const doc = this.mcpDoc(projectId);
doc.projectDefault = policy ? structuredClone(policy) : null;
return structuredClone(doc);
}
async updateAgentMcpToolPermissions(
projectId: string,
agentId: string,
policy: McpToolPolicy | null,
): Promise<ProjectMcpToolPermissions> {
const doc = this.mcpDoc(projectId);
doc.agents = doc.agents.filter((entry) => entry.agentId !== agentId);
if (policy) doc.agents.push({ agentId, policy: structuredClone(policy) });
return structuredClone(doc);
}
}
export class MockWorkStateGateway implements WorkStateGateway {

View File

@ -2,7 +2,9 @@ import { invoke } from "@tauri-apps/api/core";
import type {
EffectivePermissions,
McpToolPolicy,
PermissionSet,
ProjectMcpToolPermissions,
ProjectPermissions,
} from "@/domain";
import type { PermissionGateway } from "@/ports";
@ -40,4 +42,31 @@ export class TauriPermissionGateway implements PermissionGateway {
request: { projectId, agentId },
});
}
getMcpToolPermissions(projectId: string): Promise<ProjectMcpToolPermissions> {
return invoke<ProjectMcpToolPermissions>("get_mcp_tool_permissions", {
projectId,
});
}
updateProjectMcpToolPermissions(
projectId: string,
policy: McpToolPolicy | null,
): Promise<ProjectMcpToolPermissions> {
return invoke<ProjectMcpToolPermissions>(
"update_project_mcp_tool_permissions",
{ request: { projectId, policy } },
);
}
updateAgentMcpToolPermissions(
projectId: string,
agentId: string,
policy: McpToolPolicy | null,
): Promise<ProjectMcpToolPermissions> {
return invoke<ProjectMcpToolPermissions>(
"update_agent_mcp_tool_permissions",
{ request: { projectId, agentId, policy } },
);
}
}