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

@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { Button, Panel, Spinner, cn } from "@/shared";
import type { PermissionSet, PermissionPosture } from "@/domain";
import { McpToolPermissionsPanel } from "./McpToolPermissionsPanel";
import {
type CapabilityChoice,
type PolicyDraft,
@ -14,6 +15,13 @@ export interface PermissionsPanelProps {
projectId: string;
}
type PermissionsTab = "system" | "mcpTools";
const TABS: { id: PermissionsTab; label: string }[] = [
{ id: "system", label: "Système" },
{ id: "mcpTools", label: "Tools MCP IdeA" },
];
type EditorTarget =
| { type: "project" }
| { type: "agent"; agentId: string; agentName: string };
@ -37,6 +45,7 @@ const POSTURE_LABELS: Record<PermissionPosture, string> = {
export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
const vm = usePermissions(projectId);
const [target, setTarget] = useState<EditorTarget>({ type: "project" });
const [tab, setTab] = useState<PermissionsTab>("system");
const selectedAgent = target.type === "agent"
? vm.rows.find((row) => row.agent.id === target.agentId) ?? null
@ -79,9 +88,39 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
Refresh
</Button>
}
className="flex flex-col"
className="flex min-h-0 flex-1 flex-col"
flush
>
<div role="tablist" aria-label="Permissions" className="flex gap-1 border-b border-border px-4 pt-2">
{TABS.map((t) => (
<button
key={t.id}
type="button"
role="tab"
id={`permissions-tab-${t.id}`}
aria-selected={tab === t.id}
aria-controls={`permissions-tabpanel-${t.id}`}
tabIndex={tab === t.id ? 0 : -1}
onClick={() => setTab(t.id)}
onKeyDown={(e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
e.preventDefault();
const i = TABS.findIndex((x) => x.id === tab);
const next = e.key === "ArrowRight" ? (i + 1) % TABS.length : (i - 1 + TABS.length) % TABS.length;
setTab(TABS[next].id);
}}
className={cn(
"min-h-[32px] rounded-t-md border-b-2 px-3 py-1.5 text-sm font-medium transition-colors",
tab === t.id
? "border-primary text-content"
: "border-transparent text-muted hover:text-content",
)}
>
{t.label}
</button>
))}
</div>
{vm.error && (
<p
role="alert"
@ -91,7 +130,13 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
</p>
)}
<div className="flex flex-col gap-4 p-4">
<div
role="tabpanel"
id="permissions-tabpanel-system"
aria-labelledby="permissions-tab-system"
hidden={tab !== "system"}
className="flex flex-col gap-4 p-4"
>
<PolicyCard
title="Project defaults"
subtitle={hasProjectDefaults ? "Configured" : "Native CLI behavior"}
@ -158,6 +203,18 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
onClear={() => void handleClear()}
/>
</div>
<div
role="tabpanel"
id="permissions-tabpanel-mcpTools"
aria-labelledby="permissions-tab-mcpTools"
hidden={tab !== "mcpTools"}
className="flex min-h-0 flex-1 flex-col"
>
{tab === "mcpTools" && (
<McpToolPermissionsPanel projectId={projectId} agents={vm.agents} />
)}
</div>
</Panel>
);
}