HoRain云--DeepSeek Harness 沙箱与审批:让 Agent 危险动作可控

📅 发布时间:2026/8/21 21:59:11
HoRain云--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/cordisimport { defineTool } from deepseek-ai/dsh-toolsexport const name sandbox-demoexport 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/requestwaterfall 把问题分发给应答者answerer。应答者在负责处理该请求时返回结果否则调用next()委托第一个应答者占据唯一的决策槽位。结果类型 ApprovalOutcome 是闭合的结果含义调用方的行为allowed-once一次性放行唯一放行结果只授权所询问的那一个操作rejected明确拒绝拒绝cancelled请求被撤回如 signal 中止拒绝unavailable无应答者、应答者抛异常或返回值不合规失败关闭一律拒绝关键安全性质缺失、不负责该请求、抛异常或不合规的应答者都会产生unavailable而不是放行。调用方对rejected、cancelled和unavailable一律执行拒绝。按会话审批策略ask 与 neverApprovalPolicy 决定在交互式应答者运行之前发生什么。策略行为典型场景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-writeworkspace-writeask可写工作区敏感操作问用户danger-full-accessdanger-full-accessnever绕过沙箱、不询问只能跑在可丢弃环境预设表是配置驱动的默认表自带上面两个名字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