沙箱管"命令跑在什么边界里",审批管"这个具体操作是否被允许",两者都默认失败关闭。
ctx.sandbox:按策略包装 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 根本不进沙箱。强制执行程度与回退规则
后端报告 enforcement: 'full' | 'partial'。partial 情形包括:较旧的 Landlock ABI、Windows ACL runner 的 Everyone 与硬链接边界。要求绝对边界的消费方必须把 partial 当"不够"处理。
策略解析回退顺序(三层):
| 优先级 | 来源 | 说明 |
|---|---|---|
| 最高 | 已批准的显式模式 | 一次性提权重试时传入的 mode,胜过会话策略 |
| 其次 | 会话最后一次 sandbox/mode 事件 | 随会话日志持久化,可回放重建 |
| 回退 | 部署默认模式 | 无 agent 的调用与没有 cwd 的会话使用配置的根目录 |
故障关闭:无可用后端时 ctx.sandbox.confine 抛 SandboxUnavailableError,错误码 SANDBOX_UNAVAILABLE。
动手示例:受限模式下跑一次 bash
1// 文件路径:my-plugins/sandbox-demo/src/index.ts
2import type { Context } from '@deepseek-ai/cordis'
3import { defineTool } from '@deepseek-ai/dsh-tools'
4
5export const name = 'sandbox-demo'
6export const inject = ['tools', 'sandbox', 'sandboxPolicy']
7
8export function apply(ctx: Context) {
9 ctx.tools.register(defineTool({
10 name: 'sandboxed_echo',
11 description: 'Run echo inside the sandbox. The runoob demo command.',
12 parameters: {
13 text: { type: 'string', required: true, description: 'Text to echo' },
14 },
15 output: { schema: { type: 'string' } },
16 async execute(args, exec) {
17 const policy = ctx.sandboxPolicy.resolve({ session: exec.agent?.session })
18
19 const argv = ['bash', '-c', `echo ${JSON.stringify(args.text)}`]
20
21 if (policy.mode === 'danger-full-access') {
22 const { spawn } = await import('node:child_process')
23 // spawn(argv) 并收集输出
24 return `echo ${args.text}`
25 }
26
27 const confined = ctx.sandbox.confine(argv, { mode: policy.mode, workspaceRoot: policy.workspaceRoot })
28
29 if (confined.enforcement === 'partial' && policy.mode !== 'read-only') {
30 return { isError: true, error: { message: 'partial enforcement is not acceptable for runoob demo' } }
31 }
32 return `confined echo ${args.text} (enforcement: ${confined.enforcement})`
33 },
34 }))
35}
生产级消费方是 dsh-bash-sandbox(负责 spawn 与结果归因)。结果分类要区分"沙箱 runner 失败"与"沙箱正常工作但拒绝":两种正交 stderr 分类器 denialSignatures(识别受限命令被阻止)与 runnerFailureRules(识别 runner 执行前拒绝/失败)。消费方应先把 runner 失败作为沙箱基础设施故障上报,而非普通任务失败。
ctx.approval:一次性权限决策
通过 approval/request waterfall 分发问题给应答者;第一个应答者占据唯一决策槽位。结果类型 ApprovalOutcome 是闭合的:
| 结果 | 含义 | 调用方行为 |
|---|---|---|
allowed-once | 一次性放行 | 唯一放行结果,只授权所询问的那一个操作 |
rejected | 明确拒绝 | 拒绝 |
cancelled | 请求被撤回(如 signal 中止) | 拒绝 |
unavailable | 无应答者、应答者抛异常或返回值不合规 | 失败关闭,一律拒绝 |
unavailable 而不是放行。按会话审批策略:ask 与 never
| 策略 | 行为 | 典型场景 |
|---|---|---|
ask(默认) | 委托给应答者链;无应答者时回退为 unavailable | 交互式 UI,需要人做决定 |
never | 确定性返回 rejected,不分发任何应答者 | CI、无人值守的严格无头姿态 |
生效值是会话日志中最后一条 approval/policy 事件,回退到服务配置;setApprovalPolicy(session, policy) 是唯一写入路径。never 在服务内部、waterfall 分发前强制执行,prepend 注册的应答者也无法绕过。
审计与模型可见性区别:approval/asked 与 approval/decided 只写日志,不进入模型 transcript;模型可见的是调用方派生的工具结果与当前运行时上下文快照。
权限预设:两个 knob 捆绑成具名预设
| 预设 | 沙箱模式 | 审批策略 | 含义 |
|---|---|---|---|
workspace-write | workspace-write | ask | 可写工作区,敏感操作问用户 |
danger-full-access | danger-full-access | never | 绕过沙箱、不询问,只能跑在可丢弃环境 |
danger-full-access 只应配给一次性、可丢弃的沙箱环境。自测
- danger-full-access 是否调用 ctx.sandbox?→ 不调用,直接 spawn 原始 argv
- 审批链无应答者会怎样?→
unavailable,失败关闭、调用方拒绝 - CI 里"绝不询问、确定性拒绝"用什么?→
approval/policy: never