From dc71add9d80e652c358ed3c8b47eb290019b9400 Mon Sep 17 00:00:00 2001 From: James Feng <47167674+GhostDragon124@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:10:05 +0800 Subject: [PATCH] feat: register LocalMemoryRecallTool + VaultHttpFetchTool wiring (cherry-pick 5bb0306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream: 5bb0306 — feat: 添加 LocalMemoryRecallTool 和 VaultHttpFetchTool Tool sources were already present in CCP (pulled in via 39ba9a56). This commit adds the wiring: - Register LOCAL_MEMORY_RECALL_TOOL_NAME in ALL_AGENT_DISALLOWED_TOOLS - Add agentToolFilter utility for fork subagent tool inheritance - Add agentToolFilter tests Build: 561 files, bun run build ✔ Runtime: ccp --version → 2.6.5 ✔ --- src/constants/tools.ts | 7 +- src/utils/__tests__/agentToolFilter.test.ts | 108 ++++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 src/utils/__tests__/agentToolFilter.test.ts diff --git a/src/constants/tools.ts b/src/constants/tools.ts index 48b8054d1..16312eb5d 100644 --- a/src/constants/tools.ts +++ b/src/constants/tools.ts @@ -28,6 +28,7 @@ import { ENTER_WORKTREE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ import { EXIT_WORKTREE_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/ExitWorktreeTool/constants.js' import { WORKFLOW_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/WorkflowTool/constants.js' import { VAULT_HTTP_FETCH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/VaultHttpFetchTool/constants.js' +import { LOCAL_MEMORY_RECALL_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/LocalMemoryRecallTool/constants.js' import { CRON_CREATE_TOOL_NAME, CRON_DELETE_TOOL_NAME, @@ -44,6 +45,8 @@ export const ALL_AGENT_DISALLOWED_TOOLS = new Set([ TASK_STOP_TOOL_NAME, // Prevent recursive workflow execution inside subagents. ...(feature('WORKFLOW_SCRIPTS') ? [WORKFLOW_TOOL_NAME] : []), + // Keep local-memory recall on the main thread only. + LOCAL_MEMORY_RECALL_TOOL_NAME, // Prevent subagents from accessing LocalVault secrets. VAULT_HTTP_FETCH_TOOL_NAME, ]) @@ -85,7 +88,9 @@ export const IN_PROCESS_TEAMMATE_ALLOWED_TOOLS = new Set([ SEND_MESSAGE_TOOL_NAME, // Teammate-created crons are tagged with the creating agentId and routed to // that teammate's pendingUserMessages queue (see useScheduledTasks.ts). - CRON_CREATE_TOOL_NAME, CRON_DELETE_TOOL_NAME, CRON_LIST_TOOL_NAME, + CRON_CREATE_TOOL_NAME, + CRON_DELETE_TOOL_NAME, + CRON_LIST_TOOL_NAME, ]) /* diff --git a/src/utils/__tests__/agentToolFilter.test.ts b/src/utils/__tests__/agentToolFilter.test.ts new file mode 100644 index 000000000..f081e67c7 --- /dev/null +++ b/src/utils/__tests__/agentToolFilter.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, test } from 'bun:test' +import { filterParentToolsForFork } from '../agentToolFilter.js' +import { ALL_AGENT_DISALLOWED_TOOLS } from '../../constants/tools.js' +import type { Tool } from '../../Tool.js' + +// L6 fix: synthetic tool factory typed precisely. filterParentToolsForFork +// only reads .name; if the filter ever needed more (e.g. .isEnabled()), +// the cast site would surface the missing fields rather than silently +// pass through `as Tool`. +function fakeTool(name: string): Tool { + return { name } as unknown as Tool +} + +describe('filterParentToolsForFork', () => { + test('strips tools that are in ALL_AGENT_DISALLOWED_TOOLS', () => { + // Pick any disallowed tool name for a deterministic test. + const disallowed = Array.from(ALL_AGENT_DISALLOWED_TOOLS)[0]! + const parent: Tool[] = [fakeTool('AllowedTool'), fakeTool(disallowed)] + const result = filterParentToolsForFork(parent) + expect(result.map(t => t.name)).toEqual(['AllowedTool']) + }) + + test('strips LocalMemoryRecall (registered as disallowed in PR-1)', () => { + const parent: Tool[] = [ + fakeTool('LocalMemoryRecall'), + fakeTool('Bash'), + fakeTool('FileRead'), + ] + const result = filterParentToolsForFork(parent) + expect(result.map(t => t.name)).toEqual(['Bash', 'FileRead']) + }) + + test('passes through tools that are not in the disallow set', () => { + const parent: Tool[] = [ + fakeTool('Bash'), + fakeTool('Read'), + fakeTool('WebFetch'), + ] + const result = filterParentToolsForFork(parent) + expect(result).toEqual(parent) + }) + + test('handles empty input', () => { + expect(filterParentToolsForFork([])).toEqual([]) + }) + + test('preserves order of allowed tools', () => { + const parent: Tool[] = [ + fakeTool('A'), + fakeTool('LocalMemoryRecall'), + fakeTool('B'), + fakeTool('C'), + ] + const result = filterParentToolsForFork(parent) + expect(result.map(t => t.name)).toEqual(['A', 'B', 'C']) + }) + + test('strips multiple disallowed tools in one pass', () => { + const disallowed = Array.from(ALL_AGENT_DISALLOWED_TOOLS).slice(0, 2) + const parent: Tool[] = [ + fakeTool('Keep1'), + fakeTool(disallowed[0]!), + fakeTool('Keep2'), + fakeTool(disallowed[1]!), + fakeTool('Keep3'), + ] + const result = filterParentToolsForFork(parent) + expect(result.map(t => t.name)).toEqual(['Keep1', 'Keep2', 'Keep3']) + }) +}) + +describe('AC11a: ALL_AGENT_DISALLOWED_TOOLS contains LocalMemoryRecall', () => { + test('layer 1 gate registration is in place', () => { + expect(ALL_AGENT_DISALLOWED_TOOLS.has('LocalMemoryRecall')).toBe(true) + }) +}) + +describe('AC11b: layer 2 fork-path filter integration semantics', () => { + // Both AgentTool.tsx (new fork) and resumeAgent.ts (resumed fork) must + // call filterParentToolsForFork before passing tools to runAgent. We + // verify the wiring via grep snapshot - a missing call is the only way + // for layer 2 to silently fail. The actual fork execution pathway + // requires a full Ink REPL and is exercised in REPL AC. + test('AgentTool.tsx fork path uses filterParentToolsForFork', async () => { + const fs = await import('node:fs') + const path = await import('node:path') + // Resolve relative to the test worker's cwd, which is the project root. + const file = path.resolve( + 'packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx', + ) + const src = fs.readFileSync(file, 'utf8') + expect(src).toContain( + 'filterParentToolsForFork(toolUseContext.options.tools)', + ) + }) + + test('resumeAgent.ts resumed-fork path uses filterParentToolsForFork', async () => { + const fs = await import('node:fs') + const path = await import('node:path') + const file = path.resolve( + 'packages/builtin-tools/src/tools/AgentTool/resumeAgent.ts', + ) + const src = fs.readFileSync(file, 'utf8') + expect(src).toContain( + 'filterParentToolsForFork(toolUseContext.options.tools)', + ) + }) +})