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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user