生命不息,折腾不止。上一篇让 Agent 学会「给自己造工具」,这一篇补上最关键的那道闸:造出来的工具、跑起来的命令,到底谁能写文件、谁得先问你一句——两个旋钮,一个叫沙箱、一个叫审批,今天就把它俩彻底盘明白。

接上一篇的尾。Creator 模式聊到最后,收在了一句挺吓人的话:「沙箱 ≠ 安全边界,信任这套工具集,等价于信任 bash」。那句话其实只讲了 cordis_* 那七个工具自带的 node:vm 沙箱。今天要聊的,是 dsh 每个 Agent 每次跑命令都会过的那道文件边界——权限系统(Permissions)。

这套东西看着简单,其实最容易被两拨人同时误解:一拨人以为「开了 danger-full-access 就百无禁忌」,另一拨人以为「开了沙箱就刀枪不入」。两个都是错的。咱们一层层拆。

一、先分清:权限(Permission)和沙箱(Sandbox)不是一回事

这是官方文档开篇就警告的点,但 90% 的人会把它俩混成一个词。它俩的分工清清楚楚:

权限(Permission) 沙箱(Sandbox)
管什么 操作的「模式档」+ 要不要批准 命令执行那一刻的文件边界
在哪一层 预设(preset)+ 审批策略 ctx.sandbox.confine
举个例 写文件要不要先问用户 这条命令是在只读下跑,还是随便写

一句话记住:预设选模式,沙箱执行边界。这里有个官方表述特别容易读反——「设成 danger 不等于绕过沙箱」。它的本意是:预设这个层本身不会偷偷绕开沙箱子系统,命令照样会走 confine 这条调用路径。但别理解反了:danger-full-access 这个模式会让沙箱后端直接短路,把命令原样交给底层执行器,等于彻底放弃文件边界——它是「没有边界」,不是「还关在笼子里」。反过来,沙箱只管文件、管不住网络,这个坑第三节细说。

所以官方那句总结要背下来:权限是「预设」,不是「沙箱」。 真想限制文件系统访问,得把权限档和沙箱配置配合起来用。

二、两个旋钮 + 一张表 = 三个预设

dsh 的权限体系,本质是两个正交的旋钮,然后用一张「预设表」把它们打包成用户能一键选的东西:

  • 旋钮一:sandbox/mode(沙箱模式)——允许写到哪。
  • 旋钮二:approval/policy(审批策略)——动手前要不要问人。

出厂默认表里就三个预设(preset):

预设 沙箱模式 审批策略 行为
workspace-write workspace-write ask 只写当前工作区,写操作需批准
read-only read-only ask 只读沙箱,操作仍需确认
danger-full-access danger-full-access never 全部操作直接执行,无批准门

其中 workspace-write(+ ask)是出厂默认,剩下两个都要你显式去切或者去配。

两个旋钮如果拼出来的组合不在这张表里,当前状态会显示成 custom——只能显示、不能选中。它是个「派生态」,不是你能挑的第四个预设,别以为出了 bug。

会话里切换用一条斜杠命令:/permission。裸打 /permission 是报告当前值,带上预设名就是切换(比如 /permission read-only 切只读、/permission danger-full-access 切全开)。两个小细节:切换只会记一条日志事件,而且只有在有效值真的变了才会落地——你已经在这个档上,再切一次等于没动。

三、三档沙箱逐一看:文件边界到底画在哪

三个档,从紧到松:

  • read-only(只读):完全不让你写。POSIX 后端会额外放行一个例外——写 /dev/null(因为有些命令默认就往那吐日志,不写会报错)。适合审代码、看仓库、查问题这类「只读不下手」的活。
  • workspace-write(工作区可写):写操作被限制在当前会话的工作区根目录 + 平台允许的临时目录内。这就是默认档,日常写代码用它正好。但注意下面这个坑。
  • danger-full-access(全开):没有任何隔离,想写哪写哪。

最大的坑在这:workspace-write 只管文件,不管网络和进程可见性。 一个 workspace-write 的会话照样能对外发任意外联请求。所以别拿它当「防数据外传」的保险——它防的是「手滑把系统文件改了」,不是「手滑把内网数据传出去」。真要做外联管控,得另外配 egress 策略,这是另一件事了。

再补一层底:这三个档是「文件效果」的抽象,真正落地靠的是各平台的后端:

平台 后端
Linux bwrap / Landlock
macOS Seatbelt
Windows ACL 受限令牌(dsh-sandbox-windows-acl)
云(可选) E2B 云端沙箱,换成远程隔离容器

这里也有个实战要点:强制执行强度在各平台并不均匀。老内核的 Landlock ABI、还有 Windows 的 ACL 边界,官方文档都标了「部分生效(partial)」。所以别只看模式名字就放心,得确认后端到底有没有真关住——这个咱们下一篇展开。

另外,沙箱档之间有个「只升不降」的升级链:read-only → workspace-write → danger-full-access。当 workspace-write 会话里的某个工具想写工作区外的文件时,会触发一条沙箱升级路径——它得带上 sandbox_permissions 和 justification(理由),然后走下面要讲的审批缝,批了才放行这一次。

四、审批策略:只有 ask 和 never,没有 full-access

第二个旋钮,就两档,别的都是幻觉:

策略 行为
ask 操作执行前询问用户
never 不询问,直接执行(危险)

注意一个反直觉的点:never 不是在「问都不问直接放行」之前装死,而是在进入交互分发之前就主动拒绝。 换句话说,never 档下,凡是需要审批的操作,答案一律是「不批」——它把「要不要问」这件事整个短路了,而不是「问完默许」。

这就带出了 danger-full-access 为什么默认关掉审批的真相:那个预设是「零隔离 + 永不询问」的组合,追求的是「什么都不拦、什么都别问」。它适合的是一次性容器、临时 checkout 这类炸了也无所谓的场景,而不是你正在用的真机。

还有一条铁律必须记住:fail-closed(失败即拒绝)。ask 档下,如果当前环境里没有可用的审批人(answerer)——比如你 ssh 进去 headless 跑、没有 UI 弹窗、也没配 ACP 机器决策——那么审批结果一律按「拒绝」收场,不放行。ask 的语义是「有 answerer 才问,没有就拒绝」。所以无人值守场景直接套 ask,等于把自己锁死,这个第七节重点讲。

五、上手实操:命令、配置文件、环境变量

把上面的理论落到三个可执行的操作上。

1. 会话内切换(最常用)

1
2
3
4
/permission                  # 裸命令:报告当前值
/permission read-only # 切只读
/permission workspace-write # 切回默认
/permission danger-full-access # 切全开(慎用)

2. 配置文件改默认(全局)

1
2
3
# ~/.dsh/settings.yaml
permission:
defaultPreset: danger-full-access

出厂默认是 workspace-write(+ ask),这里显式覆盖成别的档。

3. 环境变量兜底(进程级)

1
export DSH_PERMISSION_MODE=workspace-write

这条在启动时生效,优先级最高。覆盖优先级(从高到低):

层 说明
DSH_PERMISSION_MODE 进程级 fallback,改写沙箱模式并推导审批策略(danger-full-access → never,其余 → ask)
permission.defaultPreset 未来会话的默认;省略时按已组合的沙箱/审批默认反推
出厂默认 workspace-write(+ ask)

最后一个关键语义,很多人在这里踩坑:默认值只在「创建会话」那一刻读取。创建时会把 permission/preset、sandbox/mode、approval/policy 一起钉进这个会话,之后你再怎么改配置文件、改环境变量,都不会动到已经存在的会话;恢复的种子会话也保留它原有的权限。所以要改档,要么切新会话,要么在会话里 /permission 现场切。

六、审批的细节:一次性授权 + 模型也能反过来问你

审批不是「记住你的选择」,而是一次性授权。所有需要批准的操作,都走 ctx.approval.request(req) 这一条缝,返回值只有四种:

返回 含义
allowed-once 本次批准,只对这一条请求动作有效,不形成任何记忆规则
rejected 拒绝
cancelled 请求被中止
unavailable 没有可用 answerer,fail-closed(视为拒绝)

划重点:词汇里只有 allowed-once,没有 allow-always,也没有撤销、没有授权存储。 你点一次「允许」,它就只放行眼前这一下,下次同类操作照样再问。这跟 Creator 模式里 cordis 插件的「单勾 / 双勾」授权粒度是一脉相承的设计哲学——宁可烦一点,也不留一条「我以后都不问你了」的隐患。

除了「批准不批准」,模型还能主动问你。官方挂了个模型可见工具叫 ask_user_question,用来确认、二选一、或者补信息:

  • 参数 questions 是非空数组,每项必填 id 和 question,可选 header、options、multi_select;
  • 推荐项放第一位,并在 label 后追加 (Recommended);
  • 返回 { answers: [{ id, selected, custom? }] },单选是覆盖、多选是补充;
  • 约束:待答问题会阻塞工具调用直到人回答;运行期被别的 agent 拥有的子代理不能问人(DELEGATED_CALLER),得把没决的问题写进自己的最终结果。

这条尤其适合做「半自动」流水线:让 Agent 碰到岔路口别瞎猜,把问题抛回来给你点。

七、无人值守 CI 到底该不该开 danger-full-access

回到上一篇结尾埋的那个问题。官方最佳实践表直接给了答案:

场景 建议
日常使用 workspace-write(写操作有确认,防误操作)
全自动跑批 / CI 临时切 danger-full-access
敏感项目 安全预设 + 配合沙箱 read-only
headless / 无人值守 组合一个终点 answerer 或 ACP 机器决策,否则 ask 一律 fail-closed

拆开说:

  1. CI 里可以开 danger-full-access,但前提是「炸了无所谓」。 一次性容器、临时 checkout、跑完就销毁的环境,开全开很合理——反正容器撕了就没了。但绝不该让碰真机、碰真凭据的会话默认开全开。 这是条红线。

  2. 无人值守又想保留 ask,得给它配个「机器人 answerer」。 也就是官方说的「组合一个终点 answerer」或接 ACP 自动化桥做一次性机器决策。不配的话,ask 在 headless 环境下每一个需要批准的操作都会 fail-closed 直接拒绝,等于 Agent 被绑住手脚。

  3. 警惕 Python SDK 的默认值。 官方 SDK 的示例代码里,DeepSeekHarness(...) 直接带了 danger-full-access 的默认——因为示例要开箱即跑通。你照着抄进生产,等于没看档就全开。自己写 SDK 脚本时,务必显式确认权限档。

最后补一句跟上一篇的呼应:不管用哪个模型跑批(上一篇文章讲过自定义 Provider,想接 Claude/GPT 走中转站 ai.aklibk.com,国内直连、免绑卡、人民币按量付费),模型和权限档是两码事——换模型是 provider 的活儿,权限档是 permission 的活儿,别搅在一起调。

生命不息,折腾不止。下一篇我们把这道「文件边界」扒到底——《DeepSeek Harness 沙箱与安全:confine 到底怎么把命令关进笼子》——讲 Linux 的 bwrap / Landlock、macOS 的 Seatbelt、Windows 的 ACL 三套后端到底怎么落地文件隔离,为什么老内核和 Windows 上可能只是「部分生效」,以及怎么用一条命令验证沙箱是真关住了、还是只挂了个名字。