阶段三验证清单:工具系统源码逐条验证报告
验证范围:第三阶段精读笔记(4.1-4.10)覆盖的原书第 4 章内容及
tools/工具目录源码验证方法:逐条对照原书描述与实际源码(
claude-code-sourcemap/restored-src/src/),给出源码函数名和代码片段作为证据
验证项 1:BashTool 是否真的有 7 层防御(命令验证→路径检查→Shell 注入→超时→沙箱→权限→输出截断)
结论:✅ 存在七层防御,但源码实际有 10 步检查,比原书 7 层更细粒度
详细验证
原书 4.7 节描述的七层防御与源码 bashToolHasPermission() 函数的对应关系:
parseForSecurityFromAst()checkSemantics() | utils/bash/ast.ts | ||
bashToolCheckExactMatchPermission() | bashPermissions.ts | ||
bashPermissionRule()matchWildcardPattern() | bashPermissions.ts | ||
checkPathConstraints() | pathValidation.ts | ||
checkSedConstraints() | sedValidation.ts | ||
validateReadOnlyCommand() | readOnlyValidation.ts | ||
classifyBashCommand() | bashPermissions.tsbashClassifier.ts |
1.1 用户描述的 7 项与源码对照
checkPathConstraints() | -- 端标志 + 危险路径检测 + cd+write 组合检测 + 重定向目标验证 | ||
bashSecurity.ts | |||
runShellCommand 的 AsyncGenerator 流式执行),不是权限决策链的一环。源码中不存在"超时防御层" | |||
shouldUseSandbox.tsSandboxManager | shouldUseSandbox() 在权限检查中调用),不是独立防御层。沙箱自动放行是步骤 3(模式验证后、精确匹配前)的旁路 | ||
maxResultSizeChars |
1.2 源码实际的 10 步检查链
比原书 7 层更细粒度的完整检查链:
步骤 1: Tree-sitter AST 解析(六道预检查 + PARSE_ABORTED fail-closed + 语义检查)步骤 2: 模式验证(acceptEdits 自动放行 7 种文件命令 / bypassPermissions/dontAsk 跳过)步骤 3: 沙箱自动放行(shouldUseSandbox + checkSandboxAutoAllow)步骤 4: 精确匹配(deny → 拒绝 / ask → 询问 / allow → 放行)步骤 5: 前缀/通配符匹配(deny: stripAllLeadingEnvVars 激进剥离 / allow: stripSafeWrappers 保守剥离)步骤 6: 路径约束(35 种 PathCommand + -- 处理 + 危险路径 + cd+write 组合 + 重定向目标)步骤 7: Sed 约束(双模式白名单: 行打印 + 替换 / denylist: w/W/e/E/非ASCII/花括号/换行/注释)步骤 8: 只读自动放行(GIT/GH/RIPGREP/PYRIGHT/DOCKER/EXTERNAL 6 组白名单 + flag 白名单 + UNC 检测)步骤 9: 分类器审批(LLM 分类器 + pendingClassifierCheck + 用户审批 UI)步骤 10: 破坏性命令警告(15 种模式,纯信息性,不影响权限决策)
1.3 关键差异总结
用户描述中的"Shell 注入"是 Layer 1 的子步骤,不是独立层 "超时"和"输出截断"是执行管理机制,不属于七层安全防御链 "沙箱"是 Layer 7 的子步骤,在权限决策链内调用 源码实际有 10 步(含模式验证和沙箱自动放行),比原书描述的 7 层更精细 防御链的核心设计是 Fail-Closed:AST 解析失败 → too-complex→ ask;Tree-sitter 超时 →PARSE_ABORTED→ ask(不降级到 legacy)
1.4 源码证据
AST 解析失败 fail-closed(utils/bash/ast.ts):
// Tree-sitter 解析超时或失败时返回 PARSE_ABORTED,不降级到 legacy parserif (parseResult === PARSE_ABORTED) {return { behavior: 'ask', reason: 'parse-aborted' }; // Fail-Closed}
不对称剥离策略(bashPermissions.ts):
// deny 规则:激进剥离(stripAllLeadingEnvVars — 剥离所有前导环境变量)const denyResult = matchDenyRules(input, stripAllLeadingEnvVars(command));// allow 规则:保守剥离(stripSafeWrappers — 仅剥离安全的包裹命令)const allowResult = matchAllowRules(input, stripSafeWrappers(command));
验证项 2:Fail-Closed 是否体现在"工具默认不可用,必须显式注册"
结论:❌ 不是。Fail-Closed 体现在安全属性的默认值选最严格选项,而非工具的可用性
详细验证
2.1 TOOL_DEFAULTS 的实际默认值
源码 Tool.ts 中的 TOOL_DEFAULTS 对象:
const TOOL_DEFAULTS = {isEnabled: () => true, // ✅ 默认启用(不是不可用)isConcurrencySafe: (_input?: unknown) => false, // 🔒 保守:假设不安全isReadOnly: (_input?: unknown) => false, // 🔒 保守:假设会写入isDestructive: (_input?: unknown) => false, // 默认非破坏性checkPermissions: (input, _ctx?) =>Promise.resolve({ behavior: 'allow', updatedInput: input }), // 默认放行给通用权限系统toAutoClassifierInput: (_input?: unknown) => '', // 默认跳过分类器userFacingName: (_input?: unknown) => '', // 默认空}
2.2 各安全属性的默认值语义
isConcurrencySafe | false | ||
isReadOnly | false | ||
isDestructive | false | ||
checkPermissions | allow | ||
isEnabled | true |
2.3 工具注册的实际机制
tools.ts 的 getAllBaseTools() 函数:
export function getAllBaseTools(): Tools {return [AgentTool, // ← 默认注册BashTool, // ← 默认注册FileReadTool, // ← 默认注册FileEditTool, // ← 默认注册// ... 40+ 工具默认注册...(isTodoV2Enabled() ? [TaskCreateTool, ...] : []), // 条件注册...(isAgentSwarmsEnabled() ? [getTeamCreateTool(), ...] : []),]}
工具默认在注册表中,但通过三种机制过滤:
isEnabled()feature flag:如 isTodoV2Enabled()、isAgentSwarmsEnabled()、isBriefEnabled()等- Deny 规则过滤
: filterToolsByDenyRules()在组装时移除被 deny 的工具 - 四维模式过滤
:Simple 模式仅保留 Bash/Read/Edit;REPL 模式隐藏原始工具强制走 REPL;Coordinator 模式额外暴露 Agent/TaskStop
2.4 原书的精准表述
原书 4.3.1 节明确指出:
不对称的代价——一次误判"安全"可能导致数据丢失,而一次误判"危险"最多让用户多点一次确认按钮。
Fail-Closed 的本质是安全属性的默认值选最严格选项,而非工具的可用性控制:
isConcurrencySafe: false | ||
isReadOnly: false | ||
too-complex | ||
PARSE_ABORTED | ||
SAFE_SKILL_PROPERTIES 白名单外) |
2.5 在 30+ 工具中的广泛体现
countWorktreeChanges() | ExitWorktreeTool.ts | |
ExitPlanModeV2Tool.ts | ||
SAFE_SKILL_PROPERTIES 白名单 → 默认需权限 | SkillTool.ts | |
SendMessageTool.ts | ||
MAX_JOBS = 50 | CronCreateTool.ts |
验证项 3:工具基类是否统一了 inputSchema/execute/validate 三段式
结论:✅ 是,但实际是四段式(inputSchema → validateInput → checkPermissions → call)
详细验证
源码中 Tool<Input, Output, P> 泛型接口统一了所有工具的生命周期契约,包含 30+ 方法/属性。核心的执行管线是四段式:
3.1 四段式管线
| Schema 定义 | inputSchema: ZodType | Tool.ts | |
| 自定义验证 | validateInput(input, context): Promise<ValidationResult> | Tool.ts | |
| 权限决策 | checkPermissions(input, context): Promise<PermissionResult> | Tool.ts | |
| 执行 | call(args, context, canUseTool, parentMessage, onProgress): Promise<ToolResult> | data + contextModifier 的结果 | Tool.ts |
3.2 九步执行管线中的映射
toolExecution.ts 中的完整九步执行管线:
步骤 1: findToolByName(含别名回退) ← 工具发现步骤 2: Abort 检查(用户中断) ← 前置检查步骤 3: Zod Schema 验证 ← inputSchema ← 第 1 段:Schema 定义步骤 4: tool.validateInput() ← 第 2 段:自定义验证步骤 5: Bash 分类器投机预检 ← 优化(不阻塞)步骤 6: backfillObservableInput(路径回填) ← 输入预处理步骤 7: PreToolUse Hooks ← Hook 拦截步骤 8: 权限决策解析 ← checkPermissions ← 第 3 段:权限决策步骤 9: tool.call() + PostToolUse Hooks ← 第 4 段:执行
3.3 三段式 vs 四段式的辨析
- "三段式"描述
(inputSchema / execute / validate)将 checkPermissions归入validate的广义范畴,因为权限检查也是一种验证(验证是否有权执行)。这是概念层面的合理简化。 - "四段式"实际
:源码将 validateInput(业务逻辑验证)和checkPermissions(权限验证)分为两个独立方法,因为它们的决策路径不同: validateInput返回 ValidationResult(通过/不通过)— 二值决策checkPermissions返回 PermissionResult(allow/deny/ask)— 三值决策,决策空间更大
3.4 buildTool() 工厂函数的统一机制
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {return {...TOOL_DEFAULTS, // 1. 先铺七个默认值userFacingName: () => def.name, // 2. name 作为默认 userFacingName...def, // 3. 开发者定义覆盖} as BuiltTool<D>}
所有 40+ 工具(BashTool、FileEditTool、AgentTool、TodoWriteTool、AskUserQuestionTool、SkillTool…)都通过 buildTool() 创建,统一了:
- 输入定义
: inputSchema使用 Zod schema(z.strictObject()/z.object().passthrough()) - 输入验证
: validateInput可选的自定义验证 - 权限检查
: checkPermissions可选的工具特定权限逻辑 - 执行逻辑
: call()核心执行方法 - 结果格式化
: mapToolResultToToolResultBlockParam结果到 LLM 可读文本的转换
3.5 编译期类型安全
ToolDef → BuiltTool<D> 的类型体操:
// 开发者侧:7 个默认方法可选type ToolDef<Input, Output, P> =Omit<Tool<Input, Output, P>, DefaultableToolKeys> & // 非默认方法必须提供Partial<Pick<Tool<Input, Output, P>, DefaultableToolKeys>> // 默认方法可选// 调用方侧:7 个默认方法必需(-? 移除可选性)type BuiltTool<D> = Omit<D, DefaultableToolKeys> & {[K in DefaultableToolKeys]-?: // 所有默认键变为必需K extends keyof D? undefined extends D[K] ? ToolDefaults[K] : D[K]: ToolDefaults[K]}
这意味着调用方永远不需要 null 检查——tool.checkPermissions 一定存在(要么是开发者提供的实现,要么是 TOOL_DEFAULTS 的默认值),编译期类型系统保证了这一点。
3.6 统一契约在各工具中的实例
z.object({ command, timeout, ... }) | bashToolHasPermission() | runShellCommand() | ||
z.strictObject({ filePath, oldString, newString, ... }) | allow | |||
z.object({ description, prompt, subagent_type?, ... }) | allow | runAgent() | ||
z.object({ action, discard_changes? }) | allow | |||
z.object({ skill, args? }) | SAFE_SKILL_PROPERTIES | |||
z.object({ to, summary?, message }) | ask (bypass-immune) / 其他 → allow |
3.7 补充:工具描述即 Prompt
除四段式执行管线外,工具还有两个重要的"可选但推荐"的属性:
// description 字段 — 本质是写给 LLM 的 Promptdescription: string | AsyncDynamicDescription// mapToolResultToToolResultBlockParam — 工具结果到 LLM 可读文本的转换mapToolResultToToolResultBlockParam: (toolResult: ToolResult,context: ToolContext) => Promise<ToolResultBlockParam[]>
这两个属性不参与安全决策,但直接影响 LLM 的工具选择行为和结果消费效率,属于原书 4.2.2 节"工具描述即 Prompt"设计模式的实现。
总结速查表
isEnabled: true);Fail-Closed 体现在安全属性默认值:isConcurrencySafe: false、isReadOnly: false——“不对称的代价”:误判安全可能导致数据丢失,误判危险最多多点一次按钮 | ||
buildTool() + TOOL_DEFAULTS + ToolDef/BuiltTool 编译期类型体操实现"运行时防御转移到编译期约束",调用方永远不需要 null 检查 |
参考笔记:
4.1-4.2-tool-registration.md(工具注册表 + buildTool 泛型接口) 4.3-bashtool-defense-chain.md(BashTool 七层防御链 + 10 步检查) 4.4-file-edit-security.md(FileEditTool 双重时间戳校验) 4.5-shared-security-logic.md(九步执行管线 + checkPermissions/validateInput 语义) 4.6-readonly-tools.md(只读工具安全标记) 4.7-mcp-tool.md(MCP 工具延迟加载) 4.8-agent-tool.md(AgentTool 子代理执行) 4.9-ssrf-protection.md(WebFetch SSRF 防护) 4.10-tool-taxonomy.md(30+ 工具分类 + 安全属性矩阵 + 设计模式提炼)
夜雨聆风