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:
@ -162,6 +162,20 @@ describe("TauriAgentGateway invoke payloads", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("update_agent_contactable wraps the dedicated boolean in the request DTO", async () => {
|
||||
invoke.mockResolvedValueOnce({ id: "agent-2", contactable: true });
|
||||
|
||||
await new TauriAgentGateway().updateAgentContactable(
|
||||
"proj-1",
|
||||
"agent-2",
|
||||
true,
|
||||
);
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("update_agent_contactable", {
|
||||
request: { projectId: "proj-1", agentId: "agent-2", contactable: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("cancelAgentChat invokes cancel_agent_chat without closing the session", async () => {
|
||||
await new TauriAgentGateway().cancelAgentChat("chat-session-1");
|
||||
|
||||
|
||||
@ -144,6 +144,16 @@ export class TauriAgentGateway implements AgentGateway {
|
||||
});
|
||||
}
|
||||
|
||||
updateAgentContactable(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
contactable: boolean,
|
||||
): Promise<Agent> {
|
||||
return invoke<Agent>("update_agent_contactable", {
|
||||
request: { projectId, agentId, contactable },
|
||||
});
|
||||
}
|
||||
|
||||
readContext(projectId: string, agentId: string): Promise<string> {
|
||||
return invoke<AgentContextDocument>("read_agent_context", {
|
||||
projectId,
|
||||
|
||||
@ -119,6 +119,28 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("updates agent contactability through the dedicated request DTO", async () => {
|
||||
const response = { id: "agent-1", contactable: true };
|
||||
const { gw, calls } = httpAgentGateway(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => response,
|
||||
text: async () => JSON.stringify(response),
|
||||
}));
|
||||
|
||||
await expect(
|
||||
gw.updateAgentContactable("proj-1", "agent-1", true),
|
||||
).resolves.toEqual(response);
|
||||
|
||||
const init = calls[0].init as { body: string };
|
||||
expect(JSON.parse(init.body)).toEqual({
|
||||
command: "update_agent_contactable",
|
||||
args: {
|
||||
request: { projectId: "proj-1", agentId: "agent-1", contactable: true },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("launch → attached: conforming agent.launch frame, assignedConversationId consumed", async () => {
|
||||
const { gw, sockets } = gateway();
|
||||
const chunks: Uint8Array[] = [];
|
||||
|
||||
@ -264,7 +264,8 @@ describe("request/response gateways preserve the Tauri command contract", () =>
|
||||
contentMd: "# Body",
|
||||
version: 1,
|
||||
defaultProfileId: "p1",
|
||||
defaultSystemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
defaultSystemPermissions: { network: "allow" },
|
||||
defaultMcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
};
|
||||
const spy = vi
|
||||
@ -276,7 +277,8 @@ describe("request/response gateways preserve the Tauri command contract", () =>
|
||||
|
||||
await expect(gw.listTemplates()).resolves.toMatchObject([
|
||||
{
|
||||
systemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
systemPermissions: { network: "allow" },
|
||||
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
},
|
||||
]);
|
||||
@ -285,11 +287,13 @@ describe("request/response gateways preserve the Tauri command contract", () =>
|
||||
name: "Permissioned",
|
||||
content: "# Body",
|
||||
defaultProfileId: "p1",
|
||||
systemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
systemPermissions: { network: "allow" },
|
||||
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
systemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
systemPermissions: { network: "allow" },
|
||||
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
});
|
||||
await expect(
|
||||
@ -297,11 +301,13 @@ describe("request/response gateways preserve the Tauri command contract", () =>
|
||||
name: "Permissioned",
|
||||
content: "# Body",
|
||||
defaultProfileId: "p1",
|
||||
systemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
systemPermissions: { network: "allow" },
|
||||
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
systemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
systemPermissions: { network: "allow" },
|
||||
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
});
|
||||
|
||||
@ -311,7 +317,8 @@ describe("request/response gateways preserve the Tauri command contract", () =>
|
||||
content: "# Body",
|
||||
defaultProfileId: "p1",
|
||||
defaultAgentName: null,
|
||||
defaultSystemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
defaultSystemPermissions: { network: "allow" },
|
||||
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
},
|
||||
});
|
||||
@ -322,7 +329,8 @@ describe("request/response gateways preserve the Tauri command contract", () =>
|
||||
content: "# Body",
|
||||
defaultProfileId: "p1",
|
||||
defaultAgentName: null,
|
||||
defaultSystemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
defaultSystemPermissions: { network: "allow" },
|
||||
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
},
|
||||
});
|
||||
|
||||
@ -293,6 +293,7 @@ export class HttpTemplateGateway implements TemplateGateway {
|
||||
content: input.content,
|
||||
defaultProfileId: input.defaultProfileId,
|
||||
defaultAgentName: input.defaultAgentName ?? null,
|
||||
defaultContactable: input.defaultContactable ?? false,
|
||||
defaultSystemPermissions: input.systemPermissions ?? null,
|
||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||
},
|
||||
@ -306,6 +307,7 @@ export class HttpTemplateGateway implements TemplateGateway {
|
||||
content: input.content,
|
||||
defaultProfileId: input.defaultProfileId,
|
||||
defaultAgentName: input.defaultAgentName ?? null,
|
||||
defaultContactable: input.defaultContactable ?? false,
|
||||
defaultSystemPermissions: input.systemPermissions ?? null,
|
||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||
},
|
||||
|
||||
@ -249,6 +249,15 @@ export class HttpAgentGateway implements AgentGateway {
|
||||
request: { projectId, agentId, effort },
|
||||
});
|
||||
}
|
||||
updateAgentContactable(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
contactable: boolean,
|
||||
): Promise<Agent> {
|
||||
return this.http.invoke<Agent>("update_agent_contactable", {
|
||||
request: { projectId, agentId, contactable },
|
||||
});
|
||||
}
|
||||
readContext(projectId: string, agentId: string): Promise<string> {
|
||||
return this.http
|
||||
.invoke<AgentContextDocument>("read_agent_context", { projectId, agentId })
|
||||
|
||||
@ -702,6 +702,24 @@ export class MockAgentGateway implements AgentGateway {
|
||||
return structuredClone(next);
|
||||
}
|
||||
|
||||
async updateAgentContactable(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
contactable: boolean,
|
||||
): Promise<Agent> {
|
||||
const list = this.getAgents(projectId);
|
||||
const idx = list.findIndex((a) => a.id === agentId);
|
||||
if (idx === -1) {
|
||||
const err: GatewayError = {
|
||||
code: "NOT_FOUND",
|
||||
message: `agent ${agentId} not found in project ${projectId}`,
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
list[idx] = { ...list[idx], contactable };
|
||||
return structuredClone(list[idx]);
|
||||
}
|
||||
|
||||
// ── Internal helpers for MockTemplateGateway (same-package use only) ──
|
||||
|
||||
/**
|
||||
@ -2232,6 +2250,7 @@ export class MockTemplateGateway implements TemplateGateway {
|
||||
version: 1,
|
||||
defaultProfileId: input.defaultProfileId,
|
||||
defaultAgentName: input.defaultAgentName ?? null,
|
||||
defaultContactable: input.defaultContactable ?? false,
|
||||
systemPermissions: input.systemPermissions ?? null,
|
||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||
};
|
||||
@ -2259,6 +2278,7 @@ export class MockTemplateGateway implements TemplateGateway {
|
||||
content: input,
|
||||
defaultProfileId: current.defaultProfileId,
|
||||
defaultAgentName: current.defaultAgentName ?? null,
|
||||
defaultContactable: current.defaultContactable ?? false,
|
||||
systemPermissions: current.systemPermissions ?? null,
|
||||
mcpToolPermissions: current.mcpToolPermissions ?? null,
|
||||
}
|
||||
@ -2269,6 +2289,7 @@ export class MockTemplateGateway implements TemplateGateway {
|
||||
contentMd: update.content,
|
||||
defaultProfileId: update.defaultProfileId,
|
||||
defaultAgentName: update.defaultAgentName ?? null,
|
||||
defaultContactable: update.defaultContactable ?? false,
|
||||
systemPermissions: update.systemPermissions ?? null,
|
||||
mcpToolPermissions: update.mcpToolPermissions ?? null,
|
||||
version: current.version + 1,
|
||||
@ -2329,6 +2350,7 @@ export class MockTemplateGateway implements TemplateGateway {
|
||||
},
|
||||
synchronized,
|
||||
skills: [],
|
||||
contactable: template.defaultContactable ?? false,
|
||||
};
|
||||
|
||||
this.agentGateway._insertAgent(projectId, agent, template.contentMd);
|
||||
@ -2940,6 +2962,9 @@ export class MockPermissionGateway implements PermissionGateway {
|
||||
// document from the file/command permissions above. ─────────────────────
|
||||
private mcpCatalogue: McpToolCatalogue = {
|
||||
readOnlyTools: [
|
||||
"idea_project_list",
|
||||
"idea_project_read_context",
|
||||
"idea_project_list_agents",
|
||||
"idea_list_agents",
|
||||
"idea_context_read",
|
||||
"idea_memory_read",
|
||||
@ -2948,9 +2973,15 @@ export class MockPermissionGateway implements PermissionGateway {
|
||||
"idea_ticket_read",
|
||||
"idea_ticket_list",
|
||||
"idea_ticket_read_carnet",
|
||||
"idea_ticket_attachment_read",
|
||||
"idea_sprint_list",
|
||||
"idea_template_list",
|
||||
"idea_template_read",
|
||||
"idea_managed_temp_resolve",
|
||||
],
|
||||
writeActionTools: [
|
||||
"idea_project_ask_agent",
|
||||
"idea_project_cancel_agent",
|
||||
"idea_ask_agent",
|
||||
"idea_ask_agents",
|
||||
"idea_run_in_background",
|
||||
@ -2958,6 +2989,7 @@ export class MockPermissionGateway implements PermissionGateway {
|
||||
"idea_stop_agent",
|
||||
"idea_update_context",
|
||||
"idea_context_propose",
|
||||
"idea_context_update",
|
||||
"idea_memory_write",
|
||||
"idea_workstate_set",
|
||||
"idea_create_skill",
|
||||
@ -2965,14 +2997,30 @@ export class MockPermissionGateway implements PermissionGateway {
|
||||
"idea_ticket_update",
|
||||
"idea_ticket_update_status",
|
||||
"idea_ticket_update_priority",
|
||||
"idea_ticket_bulk_update_status",
|
||||
"idea_ticket_bulk_update_priority",
|
||||
"idea_ticket_bulk_delete",
|
||||
"idea_ticket_update_carnet",
|
||||
"idea_ticket_attachment_add",
|
||||
"idea_ticket_attachment_mark_summarized",
|
||||
"idea_ticket_link",
|
||||
"idea_ticket_unlink",
|
||||
"idea_template_create",
|
||||
"idea_template_update",
|
||||
"idea_template_delete",
|
||||
"idea_managed_temp_allocate",
|
||||
"idea_managed_temp_release",
|
||||
"idea_managed_temp_gc",
|
||||
],
|
||||
};
|
||||
|
||||
private mcpDocs = new Map<string, ProjectMcpToolPermissions>();
|
||||
|
||||
setMcpToolCatalogue(catalogue: McpToolCatalogue): void {
|
||||
this.mcpCatalogue = structuredClone(catalogue);
|
||||
this.mcpDocs.clear();
|
||||
}
|
||||
|
||||
private mcpDoc(projectId: string): ProjectMcpToolPermissions {
|
||||
if (!this.mcpDocs.has(projectId)) {
|
||||
this.mcpDocs.set(projectId, {
|
||||
|
||||
@ -28,6 +28,7 @@ export class TauriTemplateGateway implements TemplateGateway {
|
||||
content: input.content,
|
||||
defaultProfileId: input.defaultProfileId,
|
||||
defaultAgentName: input.defaultAgentName ?? null,
|
||||
defaultContactable: input.defaultContactable ?? false,
|
||||
defaultSystemPermissions: input.systemPermissions ?? null,
|
||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||
},
|
||||
@ -42,6 +43,7 @@ export class TauriTemplateGateway implements TemplateGateway {
|
||||
content: input.content,
|
||||
defaultProfileId: input.defaultProfileId,
|
||||
defaultAgentName: input.defaultAgentName ?? null,
|
||||
defaultContactable: input.defaultContactable ?? false,
|
||||
defaultSystemPermissions: input.systemPermissions ?? null,
|
||||
mcpToolPermissions: input.mcpToolPermissions ?? null,
|
||||
},
|
||||
|
||||
@ -9,6 +9,7 @@ export function normalizeTemplate(template: TemplateDto): Template {
|
||||
const { defaultSystemPermissions, defaultMcpToolPermissions, ...rest } = template;
|
||||
return {
|
||||
...rest,
|
||||
defaultContactable: rest.defaultContactable ?? false,
|
||||
systemPermissions: rest.systemPermissions ?? defaultSystemPermissions ?? null,
|
||||
mcpToolPermissions: rest.mcpToolPermissions ?? defaultMcpToolPermissions ?? null,
|
||||
};
|
||||
|
||||
@ -820,7 +820,6 @@ export type NetworkPolicy = "allow" | "deny" | "ask";
|
||||
/** Optional system permission bundle, distinct from file/bash permissions. */
|
||||
export interface SystemPermissionSet {
|
||||
network?: NetworkPolicy;
|
||||
crossProjectContactable?: boolean;
|
||||
}
|
||||
|
||||
/** One sparse agent system-permission override. */
|
||||
@ -1316,6 +1315,8 @@ export interface Agent {
|
||||
skills: SkillRef[];
|
||||
/** Per-agent effort override. Omitted for older manifests/profile default. */
|
||||
effort?: EffortSelection;
|
||||
/** Whether agents from other projects may contact this agent. Legacy absence means false. */
|
||||
contactable?: boolean;
|
||||
}
|
||||
|
||||
/** Response DTO returned by `read_agent_context`; adapters unwrap `content`. */
|
||||
@ -1502,6 +1503,8 @@ export interface Template {
|
||||
version: number;
|
||||
defaultProfileId: string;
|
||||
defaultAgentName?: string | null;
|
||||
/** Initial cross-project contactability for agents created from this template. */
|
||||
defaultContactable?: boolean;
|
||||
systemPermissions?: SystemPermissionSet | null;
|
||||
mcpToolPermissions?: McpToolPolicy | null;
|
||||
}
|
||||
|
||||
@ -121,6 +121,39 @@ describe("PermissionsPanel — Tools MCP IdeA tab", () => {
|
||||
})).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows cross-project tools in their UX group and catalogued unmapped tools in Autres", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
const permission = new MockPermissionGateway();
|
||||
permission.setMcpToolCatalogue({
|
||||
readOnlyTools: ["idea_project_list", "idea_future_read"],
|
||||
writeActionTools: ["idea_project_ask_agent"],
|
||||
});
|
||||
const gateways = {
|
||||
agent,
|
||||
permission,
|
||||
profile: new MockProfileGateway(),
|
||||
} as unknown as Gateways;
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<PermissionsPanel projectId={PROJECT_ID} />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await openMcpToolsTab();
|
||||
|
||||
expect(screen.getByRole("button", { name: /IdeA inter-projets/ })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole("checkbox", {
|
||||
name: /Demander à un agent d'un autre projet, idea_project_ask_agent$/,
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: /Autres/ })).toBeTruthy();
|
||||
const fallback = screen.getByRole("checkbox", {
|
||||
name: /idea_future_read, idea_future_read$/,
|
||||
}) as HTMLInputElement;
|
||||
expect(fallback.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("creating an override prefills the draft with the current effective allowlist", async () => {
|
||||
await renderPanel();
|
||||
await openMcpToolsTab();
|
||||
@ -156,14 +189,14 @@ describe("PermissionsPanel — Tools MCP IdeA tab", () => {
|
||||
await selectAgentTarget("DevFrontend");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Créer un override" }));
|
||||
|
||||
expect(screen.getByText(/9 lecture autorisés · 0 écriture autorisé/)).toBeTruthy();
|
||||
expect(screen.getByText(/16 lecture autorisés · 0 écriture autorisé/)).toBeTruthy();
|
||||
|
||||
const writeCheckbox = screen.getByRole("checkbox", {
|
||||
name: /idea_ticket_update_carnet$/,
|
||||
});
|
||||
fireEvent.click(writeCheckbox);
|
||||
|
||||
expect(screen.getByText(/9 lecture autorisés · 1 écriture autorisé/)).toBeTruthy();
|
||||
expect(screen.getByText(/16 lecture autorisés · 1 écriture autorisé/)).toBeTruthy();
|
||||
expect(screen.getByText("Modifications non enregistrées")).toBeTruthy();
|
||||
expect(screen.getByText("Ajouté")).toBeTruthy();
|
||||
});
|
||||
@ -196,7 +229,7 @@ describe("PermissionsPanel — Tools MCP IdeA tab", () => {
|
||||
"idea_ticket_update_carnet",
|
||||
]),
|
||||
);
|
||||
expect(calledPolicy?.allowedTools).toHaveLength(10);
|
||||
expect(calledPolicy?.allowedTools).toHaveLength(17);
|
||||
|
||||
// Persisted: the left-column badge flips to Override, and Réinitialiser appears.
|
||||
const nav = screen.getByRole("navigation", { name: "cibles des permissions MCP" });
|
||||
|
||||
@ -17,7 +17,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { Agent, McpToolCatalogue } from "@/domain";
|
||||
import { Button, Panel, cn } from "@/shared";
|
||||
import { MCP_TOOL_GROUPS, mcpToolLabel } from "./mcpToolGroups";
|
||||
import {
|
||||
mcpToolGroupsForCatalogue,
|
||||
mcpToolLabel,
|
||||
} from "./mcpToolGroups";
|
||||
import { useMcpToolPermissions } from "./useMcpToolPermissions";
|
||||
|
||||
export interface McpToolPermissionsPanelProps {
|
||||
@ -243,9 +246,13 @@ function McpToolEditor({
|
||||
const [pendingConfirm, setPendingConfirm] = useState(false);
|
||||
const [savedFlash, setSavedFlash] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const toolGroups = useMemo(
|
||||
() => mcpToolGroupsForCatalogue(catalogue),
|
||||
[catalogue],
|
||||
);
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>(() =>
|
||||
Object.fromEntries(
|
||||
MCP_TOOL_GROUPS.map((group) => [
|
||||
mcpToolGroupsForCatalogue(catalogue).map((group) => [
|
||||
group.id,
|
||||
READ_GROUP_IDS.has(group.id) ? targetKind === "project" : true,
|
||||
]),
|
||||
@ -375,7 +382,7 @@ function McpToolEditor({
|
||||
<h5 className="text-xs font-semibold uppercase tracking-wide text-faint">
|
||||
Détail des tools
|
||||
</h5>
|
||||
{MCP_TOOL_GROUPS.map((group) => (
|
||||
{toolGroups.map((group) => (
|
||||
<ToolGroupSection
|
||||
key={group.id}
|
||||
group={group}
|
||||
|
||||
@ -57,13 +57,9 @@ const NETWORK_LABELS: Record<NetworkPolicy, string> = {
|
||||
const CROSS_PROJECT_HELP =
|
||||
"Autorise les agents d’autres projets IdeA à solliciter cet agent via l’orchestration inter-projets. L’agent reste soumis à ses permissions et à son contexte.";
|
||||
|
||||
function systemPermissionsFromDraft(
|
||||
network: NetworkPolicy | "",
|
||||
crossProjectContactable: boolean,
|
||||
): SystemPermissionSet | null {
|
||||
function systemPermissionsFromDraft(network: NetworkPolicy | ""): SystemPermissionSet | null {
|
||||
const permissions: SystemPermissionSet = {};
|
||||
if (network) permissions.network = network;
|
||||
if (crossProjectContactable) permissions.crossProjectContactable = true;
|
||||
return Object.keys(permissions).length > 0 ? permissions : null;
|
||||
}
|
||||
|
||||
@ -110,11 +106,24 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveNetwork(permissions: SystemPermissionSet | null) {
|
||||
async function handleSaveNetwork(
|
||||
permissions: SystemPermissionSet | null,
|
||||
contactable?: boolean,
|
||||
) {
|
||||
if (target.type === "project") {
|
||||
await vm.saveProjectSystemPermissions(permissions);
|
||||
} else {
|
||||
await vm.saveAgentSystemPermissions(target.agentId, permissions);
|
||||
const networkChanged =
|
||||
(permissions?.network ?? "") !== (selectedAgent?.systemOverride?.network ?? "");
|
||||
const contactableChanged =
|
||||
contactable !== undefined &&
|
||||
contactable !== (selectedAgent?.agent.contactable ?? false);
|
||||
if (networkChanged) {
|
||||
await vm.saveAgentSystemPermissions(target.agentId, permissions);
|
||||
}
|
||||
if (contactableChanged) {
|
||||
await vm.saveAgentContactable(target.agentId, contactable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -261,8 +270,11 @@ export function PermissionsPanel({ projectId }: PermissionsPanelProps) {
|
||||
: null
|
||||
}
|
||||
showCrossProjectContactable={target.type === "agent"}
|
||||
contactable={selectedAgent?.agent.contactable ?? false}
|
||||
busy={vm.busy}
|
||||
onSave={(permissions) => void handleSaveNetwork(permissions)}
|
||||
onSave={(permissions, contactable) =>
|
||||
void handleSaveNetwork(permissions, contactable)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -335,8 +347,9 @@ interface NetworkPermissionEditorProps {
|
||||
inherited: SystemPermissionSet | null;
|
||||
resolved: ResolvedAgentSystemPermissions | null;
|
||||
showCrossProjectContactable: boolean;
|
||||
contactable: boolean;
|
||||
busy: boolean;
|
||||
onSave: (permissions: SystemPermissionSet | null) => void;
|
||||
onSave: (permissions: SystemPermissionSet | null, contactable?: boolean) => void;
|
||||
}
|
||||
|
||||
function NetworkPermissionEditor({
|
||||
@ -345,12 +358,12 @@ function NetworkPermissionEditor({
|
||||
inherited,
|
||||
resolved,
|
||||
showCrossProjectContactable,
|
||||
contactable,
|
||||
busy,
|
||||
onSave,
|
||||
}: NetworkPermissionEditorProps) {
|
||||
const current = source?.network ?? "";
|
||||
const currentCrossProjectContactable =
|
||||
source?.crossProjectContactable ?? false;
|
||||
const currentCrossProjectContactable = contactable;
|
||||
const [local, setLocal] = useState<NetworkPolicy | "">(current);
|
||||
const [localCrossProjectContactable, setLocalCrossProjectContactable] =
|
||||
useState(currentCrossProjectContactable);
|
||||
@ -484,7 +497,7 @@ function NetworkPermissionEditor({
|
||||
busy ||
|
||||
readOnly ||
|
||||
(!source?.network &&
|
||||
!(showCrossProjectContactable && source?.crossProjectContactable))
|
||||
!(showCrossProjectContactable && currentCrossProjectContactable))
|
||||
}
|
||||
onClick={() => onSave(null)}
|
||||
>
|
||||
@ -498,10 +511,10 @@ function NetworkPermissionEditor({
|
||||
loading={busy && changed}
|
||||
onClick={() =>
|
||||
onSave(
|
||||
systemPermissionsFromDraft(
|
||||
local,
|
||||
showCrossProjectContactable && localCrossProjectContactable,
|
||||
),
|
||||
systemPermissionsFromDraft(local),
|
||||
showCrossProjectContactable
|
||||
? localCrossProjectContactable
|
||||
: undefined,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import type { McpToolCatalogue } from "@/domain";
|
||||
|
||||
/**
|
||||
* Presentational grouping of the MCP tool catalogue (ticket #82).
|
||||
*
|
||||
@ -15,6 +17,17 @@ export interface McpToolGroupDef {
|
||||
|
||||
/** Domain groups, in display order (carnet #82 — Conception UX/F). */
|
||||
export const MCP_TOOL_GROUPS: McpToolGroupDef[] = [
|
||||
{
|
||||
id: "crossProject",
|
||||
label: "IdeA inter-projets",
|
||||
tools: [
|
||||
"idea_project_list",
|
||||
"idea_project_read_context",
|
||||
"idea_project_list_agents",
|
||||
"idea_project_ask_agent",
|
||||
"idea_project_cancel_agent",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "readProject",
|
||||
label: "Lecture projet",
|
||||
@ -78,6 +91,11 @@ export const MCP_TOOL_GROUPS: McpToolGroupDef[] = [
|
||||
|
||||
/** Short, human French label per exact MCP tool name. */
|
||||
export const MCP_TOOL_LABELS: Record<string, string> = {
|
||||
idea_project_list: "Lister les projets IdeA",
|
||||
idea_project_read_context: "Lire le contexte d'un autre projet",
|
||||
idea_project_list_agents: "Lister les agents d'un autre projet",
|
||||
idea_project_ask_agent: "Demander à un agent d'un autre projet",
|
||||
idea_project_cancel_agent: "Annuler une demande inter-projet",
|
||||
idea_list_agents: "Lister les agents",
|
||||
idea_context_read: "Lire le contexte d'un agent",
|
||||
idea_memory_read: "Lire la mémoire projet",
|
||||
@ -115,3 +133,27 @@ export function mcpToolLabel(tool: string): string {
|
||||
export function mcpToolGroupId(tool: string): string | undefined {
|
||||
return MCP_TOOL_GROUPS.find((group) => group.tools.includes(tool))?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the exhaustive presentation groups from the backend catalogue.
|
||||
* Local groups only choose placement/labels; every catalogued tool remains
|
||||
* configurable, with unmapped names collected under the visible fallback.
|
||||
*/
|
||||
export function mcpToolGroupsForCatalogue(
|
||||
catalogue: McpToolCatalogue,
|
||||
): McpToolGroupDef[] {
|
||||
const catalogued = Array.from(
|
||||
new Set([...catalogue.readOnlyTools, ...catalogue.writeActionTools]),
|
||||
);
|
||||
const cataloguedSet = new Set(catalogued);
|
||||
const mapped = new Set<string>();
|
||||
const groups = MCP_TOOL_GROUPS.flatMap((group) => {
|
||||
const tools = group.tools.filter((tool) => cataloguedSet.has(tool));
|
||||
tools.forEach((tool) => mapped.add(tool));
|
||||
return tools.length > 0 ? [{ ...group, tools }] : [];
|
||||
});
|
||||
const otherTools = catalogued.filter((tool) => !mapped.has(tool));
|
||||
return otherTools.length > 0
|
||||
? [...groups, { id: "other", label: "Autres", tools: otherTools }]
|
||||
: groups;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
|
||||
import {
|
||||
@ -103,7 +103,9 @@ describe("PermissionsPanel", () => {
|
||||
});
|
||||
|
||||
it("defaults cross-project contactability off and persists the agent checkbox", async () => {
|
||||
const { permission } = await renderPanel();
|
||||
const { agent, permission } = await renderPanel();
|
||||
const contactableSpy = vi.spyOn(agent, "updateAgentContactable");
|
||||
const systemSpy = vi.spyOn(permission, "updateAgentSystemPermissions");
|
||||
|
||||
fireEvent.click(screen.getByText("Builder"));
|
||||
|
||||
@ -117,16 +119,24 @@ describe("PermissionsPanel", () => {
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
await waitFor(() => expect(checkbox.disabled).toBe(false));
|
||||
fireEvent.click(checkbox);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save network" }));
|
||||
const save = screen.getByRole("button", { name: "Save network" });
|
||||
await waitFor(() => expect(save).toHaveProperty("disabled", false));
|
||||
fireEvent.click(save);
|
||||
|
||||
await waitFor(async () => {
|
||||
const doc = await permission.getProjectSystemPermissions(PROJECT_ID);
|
||||
expect(doc.agents?.[0]).toEqual({
|
||||
agentId: expect.any(String),
|
||||
permissions: { crossProjectContactable: true },
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(contactableSpy).toHaveBeenCalledWith(
|
||||
PROJECT_ID,
|
||||
expect.any(String),
|
||||
true,
|
||||
);
|
||||
});
|
||||
await waitFor(async () => {
|
||||
const agents = await agent.listAgents(PROJECT_ID);
|
||||
expect(agents[0].contactable).toBe(true);
|
||||
});
|
||||
expect(systemSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders the network control read-only when persisted control says so", async () => {
|
||||
|
||||
@ -54,6 +54,7 @@ export interface PermissionsViewModel {
|
||||
agentId: string,
|
||||
permissions: SystemPermissionSet | null,
|
||||
) => Promise<void>;
|
||||
saveAgentContactable: (agentId: string, contactable: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
const DEFAULT_DRAFT: PolicyDraft = {
|
||||
@ -342,6 +343,30 @@ export function usePermissions(projectId: string): PermissionsViewModel {
|
||||
[permission, projectId, refresh],
|
||||
);
|
||||
|
||||
const saveAgentContactable = useCallback(
|
||||
async (agentId: string, contactable: boolean) => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const updated = await agent.updateAgentContactable(
|
||||
projectId,
|
||||
agentId,
|
||||
contactable,
|
||||
);
|
||||
setAgents((current) =>
|
||||
current.map((candidate) =>
|
||||
candidate.id === updated.id ? updated : candidate,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
setError(describe(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
},
|
||||
[agent, projectId],
|
||||
);
|
||||
|
||||
return {
|
||||
agents,
|
||||
rows,
|
||||
@ -357,5 +382,6 @@ export function usePermissions(projectId: string): PermissionsViewModel {
|
||||
clearAgentOverride,
|
||||
saveProjectSystemPermissions,
|
||||
saveAgentSystemPermissions,
|
||||
saveAgentContactable,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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) => (
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import type { AgentProfile, Template } from "@/domain";
|
||||
import type { AgentProfile, McpToolCatalogue, Template } from "@/domain";
|
||||
import { Button, Panel, Spinner } from "@/shared";
|
||||
import { useGateways } from "@/app/di";
|
||||
import { TemplateEditor, type TemplateEditorSaveInput } from "./TemplateEditor";
|
||||
@ -33,6 +33,11 @@ export interface TemplatesPanelProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
const EMPTY_MCP_CATALOGUE: McpToolCatalogue = {
|
||||
readOnlyTools: [],
|
||||
writeActionTools: [],
|
||||
};
|
||||
|
||||
/** Editor open-state: "create" or edit-mode for a specific template. */
|
||||
type EditorState =
|
||||
| { mode: "create" }
|
||||
@ -40,11 +45,12 @@ type EditorState =
|
||||
|
||||
export function TemplatesPanel({ projectId }: TemplatesPanelProps) {
|
||||
const vm = useTemplates();
|
||||
const { profile } = useGateways();
|
||||
const { permission, profile } = useGateways();
|
||||
|
||||
// Profiles list for the editor's default-profile selector
|
||||
const [profiles, setProfiles] = useState<AgentProfile[]>([]);
|
||||
const [profilesLoaded, setProfilesLoaded] = useState(false);
|
||||
const [mcpCatalogue, setMcpCatalogue] = useState<McpToolCatalogue | null>(null);
|
||||
|
||||
const [editorState, setEditorState] = useState<EditorState | null>(null);
|
||||
const [editorBusy, setEditorBusy] = useState(false);
|
||||
@ -69,6 +75,25 @@ export function TemplatesPanel({ projectId }: TemplatesPanelProps) {
|
||||
};
|
||||
}, [profile, profilesLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!permission) {
|
||||
setMcpCatalogue(EMPTY_MCP_CATALOGUE);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
void permission
|
||||
.getMcpToolPermissions(projectId)
|
||||
.then((doc) => {
|
||||
if (!cancelled) setMcpCatalogue(doc.catalogue);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setMcpCatalogue(EMPTY_MCP_CATALOGUE);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [permission, projectId]);
|
||||
|
||||
/** Lazily load profiles when the editor is first opened. */
|
||||
async function openEditor(state: EditorState) {
|
||||
if (!profilesLoaded) {
|
||||
@ -105,6 +130,7 @@ export function TemplatesPanel({ projectId }: TemplatesPanelProps) {
|
||||
<TemplateEditor
|
||||
template={editorState.mode === "edit" ? editorState.template : null}
|
||||
profiles={profiles}
|
||||
mcpCatalogue={mcpCatalogue ?? EMPTY_MCP_CATALOGUE}
|
||||
onSave={handleSave}
|
||||
onClose={() => setEditorState(null)}
|
||||
busy={editorBusy}
|
||||
|
||||
@ -17,7 +17,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
||||
|
||||
import { MockAgentGateway, MockProfileGateway, MockTemplateGateway } from "@/adapters/mock";
|
||||
import {
|
||||
MockAgentGateway,
|
||||
MockPermissionGateway,
|
||||
MockProfileGateway,
|
||||
MockTemplateGateway,
|
||||
} from "@/adapters/mock";
|
||||
import type { Gateways } from "@/ports";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { TemplatesPanel } from "./TemplatesPanel";
|
||||
@ -33,6 +38,7 @@ interface RenderOpts {
|
||||
agent?: MockAgentGateway;
|
||||
template?: MockTemplateGateway;
|
||||
profile?: MockProfileGateway;
|
||||
permission?: MockPermissionGateway;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,11 +49,13 @@ function renderTemplatesPanel(opts: RenderOpts = {}) {
|
||||
const agent = opts.agent ?? new MockAgentGateway();
|
||||
const tmpl = opts.template ?? new MockTemplateGateway(agent);
|
||||
const profile = opts.profile ?? new MockProfileGateway();
|
||||
const gateways = { agent, profile, template: tmpl } as unknown as Gateways;
|
||||
const permission = opts.permission ?? new MockPermissionGateway();
|
||||
const gateways = { agent, permission, profile, template: tmpl } as unknown as Gateways;
|
||||
return {
|
||||
agent,
|
||||
tmpl,
|
||||
profile,
|
||||
permission,
|
||||
...render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<TemplatesPanel projectId={PROJECT_ID} />
|
||||
@ -187,11 +195,40 @@ describe("TemplatesPanel (with MockTemplateGateway)", () => {
|
||||
expect(templates[0]).toMatchObject({
|
||||
defaultAgentName: "Main",
|
||||
defaultProfileId: "profile-main",
|
||||
systemPermissions: { network: "allow", crossProjectContactable: true },
|
||||
defaultContactable: true,
|
||||
systemPermissions: { network: "allow" },
|
||||
mcpToolPermissions: { allowedTools: ["idea_ticket_read"] },
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves defaultContactable after save and reopen", async () => {
|
||||
const { tmpl } = renderTemplatesPanel();
|
||||
await waitForTemplatesIdle();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "create template" }));
|
||||
await screen.findByLabelText("template name");
|
||||
fireEvent.change(screen.getByLabelText("template name"), {
|
||||
target: { value: "Contactable" },
|
||||
});
|
||||
fireEvent.click(screen.getByLabelText("Contactable depuis d’autres projets"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save template" }));
|
||||
|
||||
await screen.findByText("Contactable");
|
||||
expect((await tmpl.listTemplates())[0]).toMatchObject({
|
||||
defaultContactable: true,
|
||||
systemPermissions: null,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "edit Contactable" }));
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
(screen.getByLabelText(
|
||||
"Contactable depuis d’autres projets",
|
||||
) as HTMLInputElement).checked,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults template cross-project contactability off when the field is absent", async () => {
|
||||
renderTemplatesPanel();
|
||||
await waitForTemplatesIdle();
|
||||
@ -211,6 +248,28 @@ describe("TemplatesPanel (with MockTemplateGateway)", () => {
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows cross-project and ungrouped catalogued MCP tools in the template editor", async () => {
|
||||
const permission = new MockPermissionGateway();
|
||||
permission.setMcpToolCatalogue({
|
||||
readOnlyTools: ["idea_project_list", "idea_future_read"],
|
||||
writeActionTools: ["idea_project_cancel_agent"],
|
||||
});
|
||||
renderTemplatesPanel({ permission });
|
||||
await waitForTemplatesIdle();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "create template" }));
|
||||
|
||||
expect(await screen.findByText("IdeA inter-projets")).toBeTruthy();
|
||||
expect(
|
||||
screen.getByLabelText("template MCP tool idea_project_cancel_agent"),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText("Autres")).toBeTruthy();
|
||||
const fallback = screen.getByLabelText(
|
||||
"template MCP tool idea_future_read",
|
||||
) as HTMLInputElement;
|
||||
expect(fallback.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("the Create button is disabled when the name is empty", async () => {
|
||||
renderTemplatesPanel();
|
||||
await waitForTemplatesIdle();
|
||||
|
||||
@ -271,6 +271,12 @@ export interface AgentGateway {
|
||||
agentId: string,
|
||||
effort: EffortSelection | null,
|
||||
): Promise<Agent>;
|
||||
/** Changes whether this agent may be contacted from another project. */
|
||||
updateAgentContactable(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
contactable: boolean,
|
||||
): Promise<Agent>;
|
||||
/** Reads an agent's `.md` context by agent id. */
|
||||
readContext(projectId: string, agentId: string): Promise<string>;
|
||||
/** Overwrites an agent's `.md` context. */
|
||||
@ -630,6 +636,7 @@ export interface CreateTemplateInput {
|
||||
content: string;
|
||||
defaultProfileId: string;
|
||||
defaultAgentName?: string | null;
|
||||
defaultContactable?: boolean;
|
||||
systemPermissions?: SystemPermissionSet | null;
|
||||
mcpToolPermissions?: McpToolPolicy | null;
|
||||
}
|
||||
@ -640,6 +647,7 @@ export interface UpdateTemplateInput {
|
||||
content: string;
|
||||
defaultProfileId: string;
|
||||
defaultAgentName?: string | null;
|
||||
defaultContactable?: boolean;
|
||||
systemPermissions?: SystemPermissionSet | null;
|
||||
mcpToolPermissions?: McpToolPolicy | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user