DeepSeek Harness 沙箱与审批:让 Agent 危险动作可控 菜鸟教程
上一章节讲了工具执行流水线,其中两个环节最让人关心:沙箱怎么约束命令,审批怎么决定「能不能做」。
本章节将深入 ctx.sandbox 与 ctx.approval 两个服务,讲清它们各自的分工与故障关闭原则。
一句话:沙箱管「命令跑在什么边界里」,审批管「这个具体操作是否被允许」,两者都默认失败关闭。
先看整体:沙箱决策 + 审批流程
下面的图把两个服务放在一起看:左边是沙箱如何包装 argv,右边是审批如何做出一次性决策。

它们共同回答了同一个问题:Agent 想做一件有风险的事,怎么把它约束住。
沙箱把「进程能碰哪些文件」圈起来,审批把「是否放行这一次操作」交给应答者决定。
ctx.sandbox:按策略包装 argv
进程沙箱的模型是:消费方交出确切的 argv,后端按文件效果策略把它包装起来。
关键点是「确切 argv」而不是 shell 字符串——一个 shell 形态的消费方要传 ['bash', '-c', command]。
ctx.sandbox.confine(argv, policy) 返回一个 ConfinedArgv,也就是「替换后的 argv 加上后端的强制执行事实」。
SandboxMode(沙箱模式)只管控文件系统效果,不含网络与进程可见性。
read-only:只允许必需的数据接收端(如
/dev/null),拒绝写入。workspace-write:还允许在工作区根目录及后端承诺的临时区域下写入。
danger-full-access:绕过隔离,消费方直接 spawn 原始 argv,不调用 ctx.sandbox。
只有前两种模式会发给提供方,danger-full-access 根本不进沙箱。
这就保证了一个安全性质:受限执行必然到达 ctx.sandbox,静默的无隔离透传永远不合法。
强制执行程度与回退规则
后端报告它实际达成的强制执行完整度:enforcement: ‘full’ | ‘partial’。
full 表示后端管控了该模式承诺的所有文件效果;partial 表示只管控了子集。
当前的部分强制执行情形包括较旧的 Landlock ABI,以及 Windows ACL runner 的 Everyone 与硬链接边界。
要求绝对边界的消费方必须把 partial 当作「不够」处理,拒绝或向上暴露这一区别。
策略的解析有明确的回退顺序,官方文档把它归纳为三层:
| 优先级 | 来源 | 说明 |
|---|---|---|
| 最高 | 已批准的显式模式 | 一次性提权重试时传入的 mode,胜过会话策略 |
| 其次 | 会话最后一次 sandbox/mode 事件 | 随会话日志持久化,可回放重建 |
| 回退 | 部署默认模式 | 无 agent 的调用与没有 cwd 的会话使用配置的根目录 |
普通工具调用从调用会话的不可变 cwd 派生 workspaceRoot。
root 会先按文件系统语义规范化,再做词法规范化,因此包含 symlink/.. 的 cwd 会标识进程实际运行的目录。
故障关闭:当没有可用后端时,
ctx.sandbox.confine会抛出SandboxUnavailableError,错误码SANDBOX_UNAVAILABLE。受限策略下,静默的无隔离透传永远不合法。
动手示例:受限模式下跑一次 bash
用代码把「沙箱化 bash」的调用路径串起来看看。
实例
// 文件路径:my-plugins/sandbox-demo/src/index.ts
// 演示沙箱化消费方:先解析策略,再让 ctx.sandbox 包装 argv。
import type { Context } from ‘@deepseek-ai/cordis’
import { defineTool } from ‘@deepseek-ai/dsh-tools’
export const name = ‘sandbox-demo’
export const inject = [’tools’, ‘sandbox’, ‘sandboxPolicy’]
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: ‘sandboxed_echo’,
description: ‘Run echo inside the sandbox. The runoob demo command.’,
parameters: {
text: { type: ‘string’, required: true, description: ‘Text to echo’ },
},
output: { schema: { type: ‘string’ } },
async execute(args, exec) {
// 1. 解析本次调用的完整策略:会话 cwd 就是工作区边界。
const policy = ctx.sandboxPolicy.resolve({ session: exec.agent?.session })
// 2. 消费方交出确切 argv(程序加参数),不是 shell 字符串。
const argv = [‘bash’, ‘-c’, `echo ${JSON.stringify(args.text)}`]
// 3. danger-full-access 直接 spawn;其余交给沙箱包装。
if (policy.mode === ‘danger-full-access’) {
const { spawn } = await import(’node:child_process’)
// … spawn(argv) 并收集输出
return `echo ${args.text}` // 演示:正常返回
}
// 4. 受限模式:confine 返回替换后的 argv,无后端则抛 SANDBOX_UNAVAILABLE。
const confined = ctx.sandbox.confine(argv, { mode: policy.mode, workspaceRoot: policy.workspaceRoot })
// 5. 消费方再 spawn confined.argv,并按 confined.enforcement 决定是否要求 full。
if (confined.enforcement === ‘partial’ && policy.mode !== ‘read-only’) {
return { isError: true, error: { message: ‘partial enforcement is not acceptable for runoob demo’ } }
}
return `confined echo ${args.text} (enforcement: ${confined.enforcement})`
},
}))
}
说明:上面代码是演示性质的参考写法,省略了 spawn 与收集输出的细节。
生产级的消费方是
dsh-bash-sandbox,它同时负责 spawn 与结果归因。真正的难点在结果分类:要把「沙箱 runner 失败」与「沙箱正常工作但拒绝」区分开。
沙箱还带回两种正交的 stderr 分类器:
denialSignatures:识别沙箱正常工作时、受限命令被阻止的情况(bwrap 的 EROFS 文本、Landlock 的 EACCES、Seatbelt 的 EPERM)。
runnerFailureRules:识别 runner 在执行命令之前拒绝或失败的情况(Landlock 失败要求退出码 125 加一行致命诊断)。
消费方应先把 runner 失败作为沙箱基础设施故障上报,而不是普通任务失败。
ctx.approval:一次性权限决策
审批服务回答一个很窄的问题:这个具体操作是否可以继续?
它通过 approval/request waterfall 把问题分发给应答者(answerer)。
应答者在负责处理该请求时返回结果,否则调用 next() 委托;第一个应答者占据唯一的决策槽位。
结果类型 ApprovalOutcome 是闭合的:
| 结果 | 含义 | 调用方的行为 |
|---|---|---|
allowed-once | 一次性放行 | 唯一放行结果,只授权所询问的那一个操作 |
rejected | 明确拒绝 | 拒绝 |
cancelled | 请求被撤回(如 signal 中止) | 拒绝 |
unavailable | 无应答者、应答者抛异常或返回值不合规 | 失败关闭,一律拒绝 |
关键安全性质:缺失、不负责该请求、抛异常或不合规的应答者都会产生 unavailable,而不是放行。
调用方对 rejected、cancelled 和 unavailable 一律执行拒绝。
按会话审批策略:ask 与 never
ApprovalPolicy 决定在交互式应答者运行之前发生什么。
| 策略 | 行为 | 典型场景 |
|---|---|---|
ask(默认) | 委托给组合的应答者链;无应答者时回退为 unavailable | 交互式 UI,需要人做决定 |
never | 确定性返回 rejected,不分发任何应答者 | CI、无人值守运行的严格无头姿态 |
生效值是会话日志中最后一条 approval/policy 事件,回退到服务配置。
setApprovalPolicy(session, policy) 是唯一的写入路径,因此回放能重建覆盖值。
never 在服务内部、waterfall 分发之前强制执行,所以即使后来用 prepend 注册的应答者也无法绕过它。
审计与模型可见性的区别:审批的审计事件对(
approval/asked与approval/decided)只写日志,不进入模型 transcript。模型可见的行为是调用方派生的工具结果与当前运行时上下文快照。
权限预设:两个 knob 捆绑成具名预设
沙箱模式(sandbox/mode)与审批策略(approval/policy)是两个相互独立的 knob。
权限预设层 ctx.permissionPresets 把它们捆绑成具名预设,供客户端作为单个「权限」选择器提供。
| 预设 | 沙箱模式 | 审批策略 | 含义 |
|---|---|---|---|
workspace-write | workspace-write | ask | 可写工作区,敏感操作问用户 |
danger-full-access | danger-full-access | never | 绕过沙箱、不询问,只能跑在可丢弃环境 |
预设表是配置驱动的,默认表自带上面两个;名字 custom 被保留给「派生的非预设状态」。
current(events) 从两个 knob 折叠实际生效的预设,而不只是看预设自己的事件。
两个预设共享同一个 knob 组合时,permission/preset 事件让 current() 仍能保住用户选的究竟是哪一个。
重要:权限预设只描述它实际管辖的能力。
官方事故复盘 0002 就提醒过:组合时的文件系统访问无法安全地跟随运行时的 bash-only 预设。
在 runoob 生产环境里,
danger-full-access只应配给一次性、可丢弃的沙箱环境。
小结自测
沙箱与审批把「能力边界」与「决策授权」分开:沙箱用文件效果策略包装 argv,审批用瀑布式应答者做一次性放行。
自测题:
| 问题 | 参考 |
|---|---|
消费方想跑 danger-full-access 模式,它会调用 ctx.sandbox 吗? | 不会,直接 spawn 原始 argv,不进沙箱 |
| 审批链没有任何应答者,结果是什么? | unavailable,失败关闭,调用方拒绝 |
| 在 CI 里想「绝不询问、确定性拒绝所有审批」,用哪个策略? | approval/policy: never |
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/gpt/post/20260818/DeepSeek-Harness-%E6%B2%99%E7%AE%B1%E4%B8%8E%E5%AE%A1%E6%89%B9%E8%AE%A9-Agent-%E5%8D%B1%E9%99%A9%E5%8A%A8%E4%BD%9C%E5%8F%AF%E6%8E%A7-%E8%8F%9C%E9%B8%9F%E6%95%99%E7%A8%8B/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com

