From a960d43ed1fa17d97eabb7c6c00dfbf804479844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Mon, 30 Mar 2026 09:22:26 +0200 Subject: [PATCH] feat(assistant): align tool visibility with route audiences --- .../src/__tests__/assistant-router.test.ts | 62 +++++++++++++++++++ packages/api/src/router/assistant.ts | 21 +++++++ 2 files changed, 83 insertions(+) diff --git a/packages/api/src/__tests__/assistant-router.test.ts b/packages/api/src/__tests__/assistant-router.test.ts index 5531129..3f6903e 100644 --- a/packages/api/src/__tests__/assistant-router.test.ts +++ b/packages/api/src/__tests__/assistant-router.test.ts @@ -375,6 +375,68 @@ describe("assistant router tool gating", () => { expect(userNames).not.toContain("get_project_computation_graph"); }); + it("keeps planning read tools behind the explicit planning permission", () => { + const userWithoutPlanning = getToolNames([], SystemRole.USER); + const userWithPlanning = getToolNames([PermissionKey.VIEW_PLANNING], SystemRole.USER); + + expect(userWithoutPlanning).not.toContain("list_allocations"); + expect(userWithoutPlanning).not.toContain("list_demands"); + expect(userWithoutPlanning).not.toContain("check_resource_availability"); + expect(userWithPlanning).toContain("list_allocations"); + expect(userWithPlanning).toContain("list_demands"); + expect(userWithPlanning).toContain("check_resource_availability"); + }); + + it("keeps controller-only project and dashboard reads hidden from plain users", () => { + const controllerNames = getToolNames([ + PermissionKey.VIEW_COSTS, + PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS, + ], SystemRole.CONTROLLER); + const userNames = getToolNames([ + PermissionKey.VIEW_COSTS, + PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS, + PermissionKey.VIEW_PLANNING, + ], SystemRole.USER); + + expect(controllerNames).toContain("search_projects"); + expect(controllerNames).toContain("get_project"); + expect(controllerNames).toContain("get_statistics"); + expect(controllerNames).toContain("get_dashboard_detail"); + expect(controllerNames).toContain("get_skill_gaps"); + expect(controllerNames).toContain("get_project_health"); + expect(controllerNames).toContain("get_budget_forecast"); + expect(userNames).not.toContain("search_projects"); + expect(userNames).not.toContain("get_project"); + expect(userNames).not.toContain("get_statistics"); + expect(userNames).not.toContain("get_dashboard_detail"); + expect(userNames).not.toContain("get_skill_gaps"); + expect(userNames).not.toContain("get_project_health"); + expect(userNames).not.toContain("get_budget_forecast"); + }); + + it("requires both controller role and advanced assistant access for timeline detail tools", () => { + const controllerWithAdvanced = getToolNames([ + PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS, + ], SystemRole.CONTROLLER); + const controllerWithoutAdvanced = getToolNames([], SystemRole.CONTROLLER); + const userWithAdvanced = getToolNames([ + PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS, + ], SystemRole.USER); + + expect(controllerWithAdvanced).toContain("get_timeline_entries_view"); + expect(controllerWithAdvanced).toContain("get_timeline_holiday_overlays"); + expect(controllerWithAdvanced).toContain("get_project_timeline_context"); + expect(controllerWithAdvanced).toContain("preview_project_shift"); + expect(controllerWithoutAdvanced).not.toContain("get_timeline_entries_view"); + expect(controllerWithoutAdvanced).not.toContain("get_timeline_holiday_overlays"); + expect(controllerWithoutAdvanced).not.toContain("get_project_timeline_context"); + expect(controllerWithoutAdvanced).not.toContain("preview_project_shift"); + expect(userWithAdvanced).not.toContain("get_timeline_entries_view"); + expect(userWithAdvanced).not.toContain("get_timeline_holiday_overlays"); + expect(userWithAdvanced).not.toContain("get_project_timeline_context"); + expect(userWithAdvanced).not.toContain("preview_project_shift"); + }); + it("keeps timeline write parity tools behind manager/admin role, manageAllocations, and advanced assistant access", () => { const managerNames = getToolNames([ PermissionKey.MANAGE_ALLOCATIONS, diff --git a/packages/api/src/router/assistant.ts b/packages/api/src/router/assistant.ts index 97b167a..89383ea 100644 --- a/packages/api/src/router/assistant.ts +++ b/packages/api/src/router/assistant.ts @@ -224,8 +224,26 @@ const COST_TOOLS = new Set([ "find_best_project_resource", ]); +/** Tools that follow planningReadProcedure access rules in the main API. */ +const PLANNING_READ_TOOLS = new Set([ + "list_allocations", + "list_demands", + "check_resource_availability", +]); + /** Tools that follow controllerProcedure access rules in the main API. */ const CONTROLLER_ONLY_TOOLS = new Set([ + "search_projects", + "get_project", + "get_timeline_entries_view", + "get_timeline_holiday_overlays", + "get_project_timeline_context", + "preview_project_shift", + "get_statistics", + "get_dashboard_detail", + "get_skill_gaps", + "get_project_health", + "get_budget_forecast", "query_change_history", "get_entity_timeline", "export_resources_csv", @@ -356,6 +374,9 @@ export function getAvailableAssistantTools(permissions: Set, user if (CONTROLLER_ONLY_TOOLS.has(toolName) && !hasControllerAccess) { return false; } + if (PLANNING_READ_TOOLS.has(toolName) && !permissions.has(PermissionKey.VIEW_PLANNING)) { + return false; + } if (COST_TOOLS.has(toolName) && !permissions.has(PermissionKey.VIEW_COSTS)) { return false; }