Skip to content
My Blog
Go back

第 09 课 · 工具执行管线与安全三旋钮

前置:第 03、04、05 课 | 预计时长:60–90 分钟 | 动手环节:必须完成

你将学会

问题引入

第 04 课的工具调用事件流跳过了执行本身。这一课补上最敏感的一段:模型说「帮我删这个文件」,从决策到落地之间有几道闸门?谁有权拦?拦了之后怎么审计? dsh 的答案:四道关卡 + 三个正交旋钮 + 一条铁律——fail-closed。

正文

9.1 模型看到什么:只有投影

工具对模型只投影 name / description / parameters 三个字段(packages/core/tools/src/index.ts:1255-1266schemaOf);执行回调、超时等绝不发给模型。schema 由 ToolRuntime 构造时挂接 ctx.systemPrompt.tools(...) 自动进入提示词组装(tools/src/index.ts:833)——你在第 10 课注册工具时不需要手动同步提示词。

9.2 四道关卡

对应 prepareExecutionpackages/core/tools/src/index.ts:1341-1506):

execute(exec) {
  gate = await waterfall('tools/pre-execute', exec, () => ({kind:'allow'}))  // :1474-1477 关卡①
  if (gate.kind === 'ask') gate = await serviceAsk(exec, gate)               // 无审批通道即拒绝
  if (gate.kind === 'deny') return errorResult(gate.reason)
  denial = guardReason(exec)              // 关卡②:单调守卫再过一遍 :1485-1487
  result = await waterfall('tools/execute', exec, runBody)   // 关卡③:around 包裹 :1572-1575
      // 包装器只能替换 signal;体内把原始信号重新熔断(fuseToolSignals :1888-1915)
  result = await waterfall('tools/post-execute', ...)        // 关卡④:可替换/阻断 :1741-1780
  emit('tools/result', finalize(result))  // 失败一律物化为错误结果,不抛出
}
关卡语义谁在用
tools/pre-executeallow / deny / ask 闭集决策审批策略、外部 hooks 桥、UI
② 单调守卫guard() 注册的守卫只能拒绝、不能放行,且在可扩展瀑布之后运行循环卫生、安全兜底
tools/executearound 包裹:包装器只能替换 signal(如上截止时间);体内重新熔断原始信号防甩脱guard 包的 timeout-policy
tools/post-execute可替换 / 阻断结果spill-policy 把超大输出换成引用(第 06 课)

最后一条纪律:失败一律物化为错误结果写进日志,不向调用方抛出——工具失败是领域事实,不是基础设施崩溃。

9.3 审批:闭集 + 日志先行

对应 core/tools/src/index.ts:1474-1498packages/interaction/user-approval/

// ApprovalService.request(user-approval/src/index.ts:222-241)
require(处于打开的 turn)
session.append('approval/asked', { id, toolName, ... })   // 先记日志
outcome = await decide(req, session)
   // 'never' 策略在服务自身路径确定性拒绝(防监听器抢注绕过)
   // 否则走 approval/request scoped waterfall,兜底 'unavailable'(fail-closed)
session.append('approval/decided', { id, outcome })        // 再记结果

三个要点:

  1. 决策是闭集PreToolDecision = allow | deny | askcore/tools/src/index.ts:589-592),ask 走一次性人工批准;客户端(ui-approval)与 ACP 自动化都通过监听 approval/request 瀑布来回答。
  2. 审计可回溯approval/asked → 裁决 → approval/decided 都在会话日志里(第 05 课的事件溯源在这里兑现安全价值)。
  3. fail-closed 全程:无审批服务或无 agent → 拒绝;审批瀑布异常 → 归一为 unavailablenever 策略不依赖监听器自觉,在服务自身路径确定性拒绝。

9.4 三个正交旋钮

dsh 的权限模型没有 allow/deny 规则表,核心是两个旋钮的组合,第三个是逐次批准:

ApprovalPolicy = 'ask' | 'never'        ← 每个工具调用要不要人点头
× SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
× 逐次人工批准(ask 的一次性裁决)

预设把两旋钮打包(如 read-only + ask)。沙箱模式解析优先级:批准过的显式模式 > 会话 sandbox/mode 事件 > 部署默认(默认 read-only,fail-safe)。

9.5 sandbox 抽象与 landlock

Service Definition 只有一个方法SandboxProvider.confine(argv, policy) → 包装后 argvpackages/sandbox/sandbox/src/index.ts:158-176)。禁止静默直通——没有沙箱就明说 SANDBOX_UNAVAILABLE,绝不假装执行。

本地后端选择(sandbox-local/src/index.ts:159-166):平台链优先、探测其次——linux: ['bwrap','landlock'],darwin: ['seatbelt'],win32: ['windows-acl'];唯一候选直接选中,多候选按链序功能探测(真跑一次 read-only profile),全部不可用 → unavailable(fail-closed)。

landlock 启动器(native/landlock-run/main.c)是 self-restrict-then-exec

restrict_self() {                      // main.c:230-262
  abi = 查询内核 ABI;按 abi 收缩 handled mask
  for (path of 规则) add_rule(...)      // --ro 只给读+执行位;--rw 给全部访问位
  prctl(PR_SET_NO_NEW_PRIVS, 1)         // :254 封死 setuid 提权
  landlock_restrict_self(ruleset_fd)    // 失败即退出,绝不 exec
  execvp(wrapped_command)               // :295 最后才 exec
}

规则集随 exec 继承,调用者不受限;不接受任何环境变量覆盖(哪个二进制约束进程不能被环境决定);无安装期构建回退——没有匹配平台包就探测失败、消费方 fail closed。

9.6 guard 与 hooks 的准确分工

动手环节

  1. 画关卡图:照 9.2 画一张从 tool/calltool/result 的序列图,标注四个 waterfall 的名字与每关的「否决权」来源;对照源码行号核对。
  2. 在快照里找审计:翻一个含工具调用的录制会话,找出 approval/asked / approval/decided 事件对(若没有,说明该用例走的是 allow 直通;解释为什么直通也安全——提示:旋钮组合)。
  3. 推演题:某插件想在 tools/pre-execute 里把别人的 deny 改成 allow。为什么做不到?至少给出两层拦截(提示:单调守卫的位置与语义)。

自检清单

常见误解

延伸阅读


Share this post:

Previous Post
第 08 课 · 能力接缝:三角色与两种形态
Next Post
第 10 课 · 毕业项目:写一个完整插件