export const DEFAULT_CLAUDE_BUILT_IN_TOOLS = [ "Read", "Write", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "TodoWrite", ] as const; export const CPH_HUB_MCP_SERVER_NAME = "cph_hub"; export const CPH_HUB_MCP_TOOL_IDS = [ "send_file", "feishu_read_context", "feishu_download_resource", "request_approval", "convert_pdf_to_md", "pbank_search_problems", "pbank_get_problem", "pbank_get_many_problems", "todo_write", ] as const; export type CphHubMcpToolId = (typeof CPH_HUB_MCP_TOOL_IDS)[number]; export interface ClaudeSdkToolConfig { readonly tools: readonly string[]; readonly allowedTools: readonly string[]; } const ROLE_TOOL_TO_CLAUDE_BUILT_INS: Readonly> = { read_file: ["Read"], write_file: ["Write"], list_files: ["Glob"], search_files: ["Grep"], bash: ["Bash"], // ADR-0017 replaced cph custom tools with Bash commands. Granting either // cph role tool therefore exposes the SDK Bash tool; cph-only Bash narrowing // would need a separate command-policy layer. cph_check: ["Bash"], cph_build: ["Bash"], web_fetch: ["WebFetch"], web_search: ["WebSearch"], todo: ["TodoWrite"], TodoWrite: ["TodoWrite"], Read: ["Read"], Write: ["Write"], Bash: ["Bash"], Glob: ["Glob"], Grep: ["Grep"], WebFetch: ["WebFetch"], WebSearch: ["WebSearch"], }; const ROLE_TOOL_TO_CPH_HUB_MCP_TOOLS: Readonly> = { send_file: ["send_file"], feishu_read_context: ["feishu_read_context"], feishu_download_resource: ["feishu_download_resource"], request_approval: ["request_approval"], convert_pdf_to_md: ["convert_pdf_to_md"], pbank: ["pbank_search_problems", "pbank_get_problem", "pbank_get_many_problems"], pbank_search_problems: ["pbank_search_problems"], pbank_get_problem: ["pbank_get_problem"], pbank_get_many_problems: ["pbank_get_many_problems"], todo: ["todo_write"], TodoWrite: ["todo_write"], todo_write: ["todo_write"], "mcp__cph_hub__send_file": ["send_file"], "mcp__cph_hub__feishu_read_context": ["feishu_read_context"], "mcp__cph_hub__feishu_download_resource": ["feishu_download_resource"], "mcp__cph_hub__request_approval": ["request_approval"], "mcp__cph_hub__convert_pdf_to_md": ["convert_pdf_to_md"], "mcp__cph_hub__pbank_search_problems": ["pbank_search_problems"], "mcp__cph_hub__pbank_get_problem": ["pbank_get_problem"], "mcp__cph_hub__pbank_get_many_problems": ["pbank_get_many_problems"], "mcp__cph_hub__todo_write": ["todo_write"], }; const SUPPORTED_ROLE_TOOLS = new Set([ ...Object.keys(ROLE_TOOL_TO_CLAUDE_BUILT_INS), ...Object.keys(ROLE_TOOL_TO_CPH_HUB_MCP_TOOLS), ]); export function claudeSdkToolConfigForRole( roleTools: readonly string[] | null | undefined, ): ClaudeSdkToolConfig { // DB/runtime "unrestricted" is JSON null; treat the same as undefined. if (roleTools === undefined || roleTools === null) { const mcpTools = CPH_HUB_MCP_TOOL_IDS.map(claudeMcpToolName); return { tools: [...DEFAULT_CLAUDE_BUILT_IN_TOOLS], allowedTools: [...DEFAULT_CLAUDE_BUILT_IN_TOOLS, ...mcpTools], }; } const builtIns: string[] = []; const allowedTools: string[] = []; for (const roleTool of roleTools) { assertSupportedRoleTool(roleTool); for (const tool of ROLE_TOOL_TO_CLAUDE_BUILT_INS[roleTool] ?? []) { pushUnique(builtIns, tool); pushUnique(allowedTools, tool); } for (const mcpTool of ROLE_TOOL_TO_CPH_HUB_MCP_TOOLS[roleTool] ?? []) { pushUnique(allowedTools, claudeMcpToolName(mcpTool)); } } return { tools: builtIns, allowedTools }; } export function cphHubMcpToolsForRole( roleTools: readonly string[] | null | undefined, ): readonly CphHubMcpToolId[] { // Always expose hub-side todo_write so progress cards work even when the // native Claude TodoWrite tool is not registered in headless agent mode. if (roleTools === undefined || roleTools === null) { return [...CPH_HUB_MCP_TOOL_IDS]; } const tools: CphHubMcpToolId[] = ["todo_write"]; for (const roleTool of roleTools) { assertSupportedRoleTool(roleTool); for (const mcpTool of ROLE_TOOL_TO_CPH_HUB_MCP_TOOLS[roleTool] ?? []) { pushUnique(tools, mcpTool); } } return tools; } export function roleToolsAllow( roleTools: readonly string[] | null | undefined, roleTool: string, ): boolean { if (roleTools === undefined || roleTools === null) return true; for (const configured of roleTools) { assertSupportedRoleTool(configured); if (configured === roleTool) return true; const mapped = ROLE_TOOL_TO_CPH_HUB_MCP_TOOLS[configured]; if (mapped !== undefined && mapped.includes(roleTool as CphHubMcpToolId)) return true; // Umbrella: role tool "pbank" allows any pbank_* MCP or role tool. if (configured === "pbank" && roleTool.startsWith("pbank")) return true; } return false; } export function assertSupportedRoleTools(roleTools: readonly string[]): void { for (const roleTool of roleTools) assertSupportedRoleTool(roleTool); } function claudeMcpToolName(tool: CphHubMcpToolId): string { return `mcp__${CPH_HUB_MCP_SERVER_NAME}__${tool}`; } function assertSupportedRoleTool(roleTool: string): void { if (!SUPPORTED_ROLE_TOOLS.has(roleTool)) { throw new Error(`unknown role tool id: ${roleTool}`); } } function pushUnique(items: T[], item: T): void { if (!items.includes(item)) items.push(item); }