Skip to content

审批流水线、沙箱重构、钩子系统增强、ToolExecutor 改造为 Effect Service#21

Merged
phantom5099 merged 4 commits into
mainfrom
sandbox-toolapproval
May 20, 2026
Merged

审批流水线、沙箱重构、钩子系统增强、ToolExecutor 改造为 Effect Service#21
phantom5099 merged 4 commits into
mainfrom
sandbox-toolapproval

Conversation

@phantom5099
Copy link
Copy Markdown
Owner

@phantom5099 phantom5099 commented May 19, 2026

背景与问题

closes: #19

1. 安全架构的玩具级现状

整个安全链路只有一个点:DefaultSandbox.allowCommand()command.includes() 字符串黑名单检查。

allowCommand(command: string): boolean {
  for (const pattern of this.denyCommands) {
    if (command.includes(pattern)) return false;  // 一行就能绕过
  }
  return true;
}

攻击面:路径规范化绕过(rm -rf /home/./user/../)、参数顺序变换、管道拆分、alias 替换。这不是沙箱,是玩具。

2. 审批模块不存在

当前审批逻辑完全缺失:

  • 没有 deny/allow/ask 三级决策
  • 没有规则引擎(glob/regex 模式匹配)
  • 没有权限模式(只读/默认/跳过)
  • 没有用户确认弹窗
  • 没有"Always/Never"学习机制

3. 钩子系统只有"通知"没有"决策"

问题 详情
8 个点只接入了 3 个 tool.execute.* 已接入;llm.request.*session.save.* 有定义无调用
纯观察性,不能决策 HookHandler = (payload) => void,无法返回 deny/allow/modifiedInput
无 PreToolUse 钩子 当前只有 tool.execute.before(通知性质),没有审批流水线中的决策钩子

4. ToolExecutor 脱离 Effect 体系

ToolExecutor 是普通 class + 构造函数注入:

export class ToolExecutor {
  constructor(
    private registry: ToolService,   // Effect Service
    private hooks: HookService,      // Effect Service
    private sandbox: Sandbox,        // 普通 interface → new DefaultSandbox()
  ) {}
}

连锁问题:

  • HookService 被迫开"后门" — 暴露 emitSync 专门给 ToolExecutor 使用
  • cli.ts 手动 new 对象 — 完全绕过 Layer 体系
  • ServerDeps 全部 any 类型 — 通过 Hono context 传递,零类型安全

方案

总体架构

ToolCallRequest
      │
      ▼
┌─────────────────────────────┐
│ Layer 1: Rule Engine        │  ← 系统硬规则优先(deny rm -rf /、sudo)
└─────────────────────────────┘
      │ (无匹配规则)
      ▼
┌─────────────────────────────┐
│ Layer 2: Read-only Whitelist│  ← Read/Glob/Grep/LSP 自动 allow
└─────────────────────────────┘
      │ (写操作工具)
      ▼
┌─────────────────────────────┐
│ Layer 3: Permission Mode    │  ← plan/bypass/acceptEdits 全局短路策略
└─────────────────────────────┘
      │ (default 模式)
      ▼
┌─────────────────────────────┐
│ Layer 4: Hook PreToolUse    │  ← 用户自定义策略,可 deny/allow/ask/modifyInput
└─────────────────────────────┘
      │ (无决策 或 ask)
      ▼
┌─────────────────────────────┐
│ Layer 5: User Confirmation  │  ← Y/N/A/V 交互弹窗 + 超时 60s → deny
└─────────────────────────────┘
      │
      ▼
┌─────────────────────────────┐
│ Layer 6: Audit / Log        │  ← 无论哪层做的决策,最终记录审计日志
└─────────────────────────────┘

关键设计决策

为什么 Hook 不是 Layer 1? 系统硬规则和只读白名单是不可协商的安全底线。Hook 在系统处理完明确的 deny/allow 之后再介入,职责清晰。

为什么 Permission Mode 在 Layer 3? 它是全局短路策略(bypass 跳过后续所有层、plan 模式直接拒绝写操作),适合在规则引擎和只读白名单之后立即拦截。

为什么 Layer 6 是审计日志而不是 Deny Fallback? "超时拒绝"只是 User Confirmation 层的超时行为,不构成独立语义。审计日志确保无论决策来源,最终都经过统一记录点。


修改范围

新增文件(11 个)

文件 说明
src/approval/types.ts ApprovalDecision、PermissionRule、PermissionMode 等类型定义
src/approval/rule-engine.ts 规则引擎:glob/regex 匹配、优先级排序、运行时 addRule/removeRule
src/approval/presets.ts 默认规则:deny rm -rf /、sudo、curl|sh 等;ask .ssh/.env;只读工具列表
src/approval/confirmation.ts 用户确认弹窗:readline Y/N/A/V 交互,60s 超时默认 deny
src/approval/pipeline.ts 六层流水线串联:Rule Engine → Readonly Whitelist → Permission Mode → Hook PreToolUse → User Confirmation → Audit
src/approval/index.ts ApprovalService (Effect Service),依赖 HookService
test/approval/rule-engine.test.ts 规则引擎测试:匹配、优先级、add/remove
test/approval/pipeline.test.ts 六层流水线测试:每层 allow/deny 路径
test/approval/presets.test.ts 默认规则集验证
test/hooks/decision.test.ts emitDecision 决策钩子测试:优先级、排序、系统/用户标记
test/sandbox/wrap-command.test.ts SandboxService 降级和命令执行测试

修改文件(18 个)

文件 改动
src/sandbox/index.ts 删除 Sandbox interface + DefaultSandbox class,改为 SandboxService (Effect)。动态加载 @anthropic-ai/sandbox-runtime,不可用时降级为传统执行
src/hooks/registry.ts 新增 HookDecision 类型、registerDecision()(支持 priority/source)、emitDecision()(按优先级裁决,首次非 null 即停止)、tool.execute.denied / tool.approval.pre / tool.approval.post 三个新钩子点。删除 emitSync 后门
src/tools/executor.ts 删除 ToolExecutor class,改为 ToolExecutorService (Effect)。插入审批流水线 + PreToolUse 决策钩子。所有依赖通过 yield* 解析
src/agent/agent.ts AgentServiceyield* ToolExecutorService + ToolServicerunStream() 去掉 executor 参数。Bash 工具串行执行(避免竞态),其他工具并行
src/orchestrate.ts sendMessage 去掉 executor/hooks 参数
src/cli.ts 删除 new DefaultSandbox()new ToolExecutor(...),全部走 Layer 装配
src/server/index.ts ServerDeps{ llm, executor, hooks } 缩减为 { llm }
src/server/routes/messages.ts 路由 handler 不再传递 executor/hooks
src/layer.ts 注册 SandboxService、ApprovalService、ToolExecutorService,构建闭合依赖图
src/index.ts 导出 ToolExecutorService 替代 ToolExecutor
README.md 更新沙箱章节,说明两层安全架构和可选安装
CLAUDE.md 更新
package.json 新增 optionalDependencies: { "@anthropic-ai/sandbox-runtime": "^1.0.0" }
4 个测试文件 适配新签名和 Layer 装配方式

文件统计

  • 560 行新增,300 行删除(19 个文件变更)
  • 84 个测试通过,16 个测试文件,零失败

预期收益

安全

  • 六层审批流水线:从单点字符串黑名单升级为多层决策链,任何一层拒绝即阻断
  • 规则引擎支持 glob/regexrm -rf /* 等模式匹配,不可绕过
  • 只读白名单:Read/Glob/Grep 等工具零确认通过,写操作走完整审批
  • 权限模式:plan 模式自动拒绝写操作,bypass 模式放行,acceptEdits 自动允许编辑
  • 用户确认:Y/N/A/V 交互弹窗,支持 Always/Never 学习,60s 超时默认拒绝
  • 审计日志:无论决策来源,最终都经过统一审计点记录

架构

  • ToolExecutor 成为 Effect Service:消除手动依赖注入,消除 emitSync 后门
  • 所有服务统一通过 Layer 装配cli.ts 不再手动 new 对象,依赖图在 layer.ts 集中管理
  • ServerDeps 类型安全:从 any 变为精确类型
  • 沙箱与审批解耦:沙箱负责 OS 级隔离,审批负责应用层决策,互不依赖

开发者体验

  • 新增工具只需注册到 ToolService,无需改 executor/agent
  • 新增审批规则只需添加 PermissionRule,无需改流水线逻辑
  • 新增钩子决策只需 registerDecision,优先级控制执行顺序
  • 测试可替换任意 Service:通过 Layer.succeed 注入 mock

未来扩展点

明确不在本次范围的后续方向:

  1. ML 分类器(Layer 3 预留位置):当前六层流水线中 Rule Engine 和 Readonly Whitelist 之间可以插入 ML 分类器,基于历史决策学习自动判断风险。需要额外的 LLM 调用成本和模型配置,当前不引入。

  2. 子 Agent 委派工具:审批模块就绪后,可以为子 Agent 分配独立的审批策略(如子 Agent 只读、主 Agent 全权)。

  3. cgroup 资源限制@anthropic-ai/sandbox-runtime 只负责隔离,不负责资源限制。后续可按需封装 cgroup v2(memory.max、cpu.max)。

  4. 审批规则持久化:当前 Always/Never 规则仅在运行时内存中生效,后续可持久化到文件(如 .codingcode/approval-rules.yaml)。

@phantom5099 phantom5099 merged commit 08db4cc into main May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

improve sanbox、tool approval and hook module

1 participant