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:
claude-code-best
2026-05-09 23:04:12 +08:00
parent a2ea69c05e
commit 5bb0306da6
18 changed files with 3815 additions and 0 deletions

View 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)',
)
})
})

View 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))
}