mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
feat: 添加 LocalMemoryRecallTool 和 VaultHttpFetchTool
- LocalMemoryRecallTool: 跨会话本地笔记召回,权限门控,大小限制 - VaultHttpFetchTool: 使用 vault 密钥的认证 HTTP 请求,ACL 规则 - agentToolFilter: 子 agent 工具继承过滤层 - ALL_AGENT_DISALLOWED_TOOLS 白名单更新 Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
108
src/utils/__tests__/agentToolFilter.test.ts
Normal file
108
src/utils/__tests__/agentToolFilter.test.ts
Normal file
@@ -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)',
|
||||
)
|
||||
})
|
||||
})
|
||||
23
src/utils/agentToolFilter.ts
Normal file
23
src/utils/agentToolFilter.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* filterParentToolsForFork — gate layer 2 for subagent tool inheritance.
|
||||
*
|
||||
* The fork path of AgentTool (and its sibling resumeAgent) sets
|
||||
* `useExactTools: true` and passes `toolUseContext.options.tools` to
|
||||
* `runAgent` as `availableTools`. With `useExactTools=true`, runAgent
|
||||
* skips `resolveAgentTools`, which means the gate layer 1
|
||||
* (`ALL_AGENT_DISALLOWED_TOOLS`) — which only takes effect inside
|
||||
* `filterToolsForAgent` — is bypassed entirely on fork paths.
|
||||
*
|
||||
* This filter applies the same disallow-list to the parent tool array
|
||||
* before it reaches the fork. Both new-fork (AgentTool.tsx) and
|
||||
* resumed-fork (resumeAgent.ts) paths must call this.
|
||||
*
|
||||
* See docs/jira/LOCAL-WIRING-DESIGN.md §4.5 / §5.5 for design rationale.
|
||||
*/
|
||||
|
||||
import { ALL_AGENT_DISALLOWED_TOOLS } from '../constants/tools.js'
|
||||
import type { Tool } from '../Tool.js'
|
||||
|
||||
export function filterParentToolsForFork(parentTools: readonly Tool[]): Tool[] {
|
||||
return parentTools.filter(t => !ALL_AGENT_DISALLOWED_TOOLS.has(t.name))
|
||||
}
|
||||
Reference in New Issue
Block a user