fix(permissions): tools MCP inter-projets exposés (idea_project_*) + contactable agent/template persisté — update_agent_contactable (application/web-server/app-tauri), defaultContactable TemplateEditor save/reopen, groupes MCP 'IdeA inter-projets' avec fallback, permissions agent Contactable — #267 (QA verte)

This commit is contained in:
2026-09-07 18:13:27 +02:00
parent 9266d121cd
commit f6101a8249
30 changed files with 632 additions and 89 deletions

View File

@ -4,12 +4,16 @@ import remarkGfm from "remark-gfm";
import type {
AgentProfile,
McpToolCatalogue,
McpToolPolicy,
NetworkPolicy,
SystemPermissionSet,
Template,
} from "@/domain";
import { MCP_TOOL_GROUPS, mcpToolLabel } from "@/features/permissions/mcpToolGroups";
import {
mcpToolGroupsForCatalogue,
mcpToolLabel,
} from "@/features/permissions/mcpToolGroups";
import { Button, Input, SmallDropdown, cn } from "@/shared";
import { profileLabel } from "./templatePresentation";
@ -18,6 +22,7 @@ export interface TemplateEditorSaveInput {
content: string;
defaultProfileId: string;
defaultAgentName: string | null;
defaultContactable: boolean;
systemPermissions: SystemPermissionSet | null;
mcpToolPermissions: McpToolPolicy | null;
}
@ -25,6 +30,7 @@ export interface TemplateEditorSaveInput {
export interface TemplateEditorProps {
template?: Template | null;
profiles: AgentProfile[];
mcpCatalogue: McpToolCatalogue;
onSave: (input: TemplateEditorSaveInput) => Promise<void>;
onClose: () => void;
busy?: boolean;
@ -42,13 +48,10 @@ const NETWORK_OPTIONS: { value: "" | NetworkPolicy; label: string }[] = [
const TEMPLATE_CROSS_PROJECT_HELP =
"Les agents créés avec ce template reprendront cette permission.";
const ALL_MCP_TOOLS = Array.from(
new Set(MCP_TOOL_GROUPS.flatMap((group) => group.tools)),
);
export function TemplateEditor({
template,
profiles,
mcpCatalogue,
onSave,
onClose,
busy = false,
@ -64,13 +67,21 @@ export function TemplateEditor({
const [network, setNetwork] = useState<"" | NetworkPolicy>(
template?.systemPermissions?.network ?? "",
);
const [crossProjectContactable, setCrossProjectContactable] = useState(
template?.systemPermissions?.crossProjectContactable ?? false,
const [defaultContactable, setDefaultContactable] = useState(
template?.defaultContactable ?? false,
);
const [allowedTools, setAllowedTools] = useState<string[]>(
template?.mcpToolPermissions?.allowedTools ?? [],
);
const [tab, setTab] = useState<EditorTab>("edit");
const toolGroups = useMemo(
() => mcpToolGroupsForCatalogue(mcpCatalogue),
[mcpCatalogue],
);
const allMcpTools = useMemo(
() => toolGroups.flatMap((group) => group.tools),
[toolGroups],
);
const allowed = useMemo(() => new Set(allowedTools), [allowedTools]);
const canSave = name.trim().length > 0 && !busy;
@ -91,15 +102,8 @@ export function TemplateEditor({
content,
defaultProfileId,
defaultAgentName: defaultAgentName.trim() || null,
systemPermissions:
network || crossProjectContactable
? {
...(network ? { network } : {}),
...(crossProjectContactable
? { crossProjectContactable: true }
: {}),
}
: null,
defaultContactable,
systemPermissions: network ? { network } : null,
mcpToolPermissions:
allowedTools.length > 0 ? { allowedTools: [...allowedTools] } : null,
});
@ -275,8 +279,8 @@ export function TemplateEditor({
<input
type="checkbox"
aria-label="Contactable depuis d’autres projets"
checked={crossProjectContactable}
onChange={(e) => setCrossProjectContactable(e.target.checked)}
checked={defaultContactable}
onChange={(e) => setDefaultContactable(e.target.checked)}
disabled={busy}
className="mt-0.5 h-4 w-4 shrink-0 accent-primary disabled:cursor-not-allowed disabled:opacity-50"
/>
@ -298,7 +302,7 @@ export function TemplateEditor({
</h3>
<span className="text-xs text-faint">{allowedTools.length} autorisé(s)</span>
</div>
{ALL_MCP_TOOLS.length === 0 ? (
{allMcpTools.length === 0 ? (
<p className="text-sm text-muted">Aucun tool MCP IdeA disponible.</p>
) : (
<>
@ -308,7 +312,7 @@ export function TemplateEditor({
</p>
)}
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{MCP_TOOL_GROUPS.map((group) => (
{toolGroups.map((group) => (
<div key={group.id} className="flex min-w-0 flex-col gap-2">
<h4 className="text-xs font-semibold text-content">{group.label}</h4>
{group.tools.map((tool) => (