
🚩 2026 年「术哥无界」系列实战文档 X 篇原创计划 第 201 篇,DeepSeek Harness最佳实战「2026」系列第 04 篇
大家好,欢迎来到 术哥无界 | ShugeX | 运维有术。
我是术哥,一名专注于 AI 编程、AI 智能体、Agent Skills、MCP、云原生、AIOps、Milvus 向量数据库的技术实践者与开源布道者!
Talk is cheap, let's explore。无界探索,有术而行。

我的判断先放这儿:dsh 里的审批,不是安全机制。它是人机之间的交接班制度。
先给你看一个具体的反差点。源码里,一次审批请求的结果一共四个取值:
type ApprovalOutcome = 'allowed-once' | 'rejected' | 'cancelled' | 'unavailable'换句话说:放行、拒绝、撤回、答不了。
四种里只有第一种能放行。剩下三种,都是拒绝。

注意最后一个:unavailable 没有应答者,没人回答这个问题。它也算拒绝,不是放行。
这是 dsh 整个审批体系的态度:拿不准,就往拒绝那边倒。
dsh 里问人的机制至少有两套,别混。
第一套是通用审批。文档把它定义为一个回答“这个具体操作是否可以继续?”的 seam。它有一套共享的请求/结果词汇、一个分发服务、一串应答者,还有按会话设置的 ask/never 策略。两个消费方是 dsh-tools 和 dsh-tool-bash;它们拿到审批结果后,除非结果是 allowed-once,否则一律拒绝。
什么时候会真的问?拿 bash 工具举例子,最典型的一条:沙箱拒绝了一条命令(比如 workspace-write 模式不让写工作区外的文件),agent 想重试一次更宽权限的版本。这时工具会先走审批,人点了同意才执行。这个参数的描述写得很死:只允许作为“沙箱刚拒绝之后同一条命令的一次性重试”,必须带一句话理由,必须经过用户批准。
第二套,是 Cordis 动态插件那一路。dsh 允许 agent 现场写代码、定义成一个动态包。注意,写代码本身不触发审批:cordis_define 只校验参数和语法、把源码登记在案,文档里写得明明白白:“不申请审批、不执行 apply”。真正可能触发审批的是下一步:cordis_run,把这个包跑起来,且仅当这个包带浏览器代码、又没被批准过。
所以什么操作会触发审批,答案一半在工具层(高危/提权操作先问人),一半在运行层(带浏览器代码的动态包,激活要问人)。
为什么 agent 不能直接执行? 我的判断是,机制上只有一个原因:代码要跑的地方,是人的地盘。动态包要是带浏览器半(client half),代码就要进浏览器页面执行,落在你的会话、你的页面上,所以它必须等人签字。
agent 能写任何代码。但让这段代码跑到你的浏览器里,这个动作决定权不在它。
通用审批那条路,把拿不准就拒绝贯彻到了骨头里。
先看策略。每个会话有两个策略:ask(默认)和 never。ask 就是正常问人:请求进一条应答者链,第一个能应答的接走,没人应答就落到 unavailable。never 更绝,根本不分发应答者,直接确定性地返回 rejected。文档里把它叫“严格的无人值守姿态(CI、无人值守运行)”,一个不用问就知道结果的策略。
注意顺序:never 是在瀑布分发之前、在服务内部强制执行的。也就是说,就算后来有人注册了应答者,也绕不过它。策略是会话级的,写入路径只有一条(setApprovalPolicy),回放的时候能重建。
再看闭环。每次审批都是成对的审计事件:approval/asked 和 approval/decided。一对一对齐,只写日志,不进模型 transcript,也就是说,模型看不到谁问了、结果如何的原始记录,它只能看到调用方派生出来的工具结果。
还有一个细节特别体现态度:发起审批的会话必须处在一个尚未结束的轮次里,否则请求直接拒掉。中止信号一到,请求立刻 settle 成 cancelled,晚到的应答作废。
整条链上,没有任何一个环节默认放行。放行是唯一需要明确证据的结果。
回到 Cordis 那一路,因为它的运行手感最特别:审批是异步的,工具不等。
源码里,cordis_run 调进 runner 的 run(),先算一个布尔值:
const requiresApproval = !clientVersionUpdatesApproved
&& !approvedClientPackages.has(packageId)换句话说:这个包(这个版本)既没被单次批准过,用户也没勾过未来版本都批准,而且这个包带浏览器代码,那就得审批。
需要审批时,cordis_run 返回这样一个东西(源码返回结构):
{
"ok": true,
"status": "awaiting-approval",
"pluginId": "xxx",
"packageId": "pkg-1",
"pluginRunId": "run-1",
"mode": "run",
"waitingFor": [],
"nextPackageId": "pkg-1"
}awaiting-approval:正在等审批。注意 ok 是 true,这不是错误,这是一个状态。
然后呢?然后工具调用就结束了。模型拿到的就是这个占位结果,系统提示词明确要求它:不要等、不要重试、不要声称正在跑。审批卡片在页面上挂着,等一个人。

人,是异步的。人点了批准,流程才继续:先启动 host 半(starting-host),再等浏览器半(client-pending),浏览器把代码装载成功,才提交为 waiting 或 running。人点了拒绝,状态落成 rejected,host 半根本没启动。
审批的结果是怎么回到模型的?不是通过那次已经结束的 cordis_run,而是通过一条异步注入的消息。成功就报“已完成,currentPackageId 是 X,继续用”;拒绝就报“用户拒绝了这次激活,除非用户主动要求,不要再申请同一激活”。
一个插件同时只能有一个在途的激活请求。想再 run 一次?得等这个请求被批准、被拒绝、或者被 stop 取消。
人点头,有两种点法。这是单次批准和未来版本批准的区别,也是这个机制里最精细的授权粒度。
看源码,一个插件身上挂着两个授权记录:
approvedClientPackages: Set<packageId> // 单次:批准过的包
clientVersionUpdatesApproved: boolean // 未来:这个插件以后的新版本用户批准时,被批准的那个包 ID 会被记进第一个集合。以后同一个包再 run,直接放行,不用再问,因为 approvedClientPackages.has(packageId) 已经成立。
但动态包是按版本走的:改一次代码,define 出的是一个新的不可变包(新的 packageId)。如果你只勾了单次批准,新版本来了,还是要再问一次。
所以 UI 上给了两个选择,系统提示词里原话:“一个勾只授权当前的包;两个勾授权同一插件的未来版本。”双勾一开,clientVersionUpdatesApproved 置位,之后这个插件定义出的任何新版本,激活都不再问人。

这里有个细节值得你品:授权是记在包(packageId)上的,而且技术失败后依然有效。 你批准过的东西,就算它跑挂了、你修正重跑,也不用重新批准。cordis_stop 只停运行、保留授权;要把授权整个清掉,只有 cordis_undefine(把整个插件删掉),文档里说它会“删除全部 Package、授权和版本指针”。
host 与 client 两侧权限有什么区别?答案就一句:审不审,不看它危不危险,看它要跑在哪。
Host 半:跑在 DSH 的 Node.js 进程里,node:vm 沙箱求值。它适合碰文件、网络、命令、服务、模型工具,全是服务端资源。它要不要审批?不要。纯 host 包直接激活,源码测试里有这个断言:host-only 包 run 直接返回 running,没有请求、没有审批。
Client 半:跑在浏览器页面里。它适合碰主题、布局、页面状态、工具卡片。它拿到的全局就那么几个固定的名字:React、console、styles、host;fetch、setTimeout 这种浏览器全局都被藏起来了。它要不要审批?要。因为它要进你的页面,你的会话环境。
甚至源码的给码逻辑都跟着这个边界走:浏览器半的源码不是常驻页面的,README 原话是“激活时什么都不装载,刷新后也不恢复”,页面只在被要求运行时才装载它。页面刷新之后,一切归零:host 还握着定义,但页面手上什么都没有,直到有人再次运行。
这解释了一个反直觉的现象:为什么最危险的 host 半(能碰文件、能执行命令)反而免审,而看起来只是渲染个界面的 client 半要审?因为 host 半有服务端沙箱和守卫兜着,它能碰到的都是受控的;client 半要去的是你的浏览器,那是你人格的延伸。
那 sandbox 和审批是什么关系?在 dsh 里,它们是两个正交的旋钮。
旋钮一,审批策略:ask 还是 never。管谁拍板。
旋钮二,沙箱模式:read-only、workspace-write、danger-full-access 三档。管能碰到哪。注意,沙箱只管文件系统效果,文档里说明网络与进程可见性不在这个定义范围内。danger-full-access 这一档直接绕过隔离,连沙箱都不调用,原始命令直接跑。
dsh 把这俩旋钮打包成了权限预设,就是 UI 上一个下拉框。预设本身没有任何强制执行,它只是把两个值写进各自的规范 setter,仅此而已。默认预设表就两行:
workspace-write:沙箱 workspace-write + 审批 askdanger-full-access:沙箱全开 + 审批 never第二行,你自己品。权限全开,同时关掉审批。这不是这里不危险所以不用审,而是我选择不问。无人值守的 CI、定时任务,就是这个姿态。

两个旋钮之间还有一张提权白名单表(源码里的 WIDER_MODES),管 bash 提权那件事:read-only 可以直接升到 workspace-write 或 danger-full-access;workspace-write 只能升到 danger-full-access。它管的不是升几档,而是这次升级是不是严格变宽。源码里写明:非严格变宽的请求永远不会提示人类。问人有代价,能不问就不问。
现在可以说主判断了。
审批不是安全边界。安全边界是沙箱(文件效果隔离)、是执行环境的守卫(host 半的 vm 陷阱、client 半的全局白名单)、是注册边界(schema 白名单、运行时守卫)。这些管的是代码能碰到什么。
审批管的是另一件事:谁让这件事发生。它解决不了恶意代码,dsh 自己承认得干脆利落,系统提示词里白纸黑字:“受限执行环境防止的是意外误用;它不是针对恶意代码的安全边界。动态代码拿到的服务连的是真实运行时。”
所以你回头看那四个审批结果,就明白了:allowed-once 之外全是拒绝,不是因为系统多疑,而是因为审批的全部职责就是把这个动作的决定权交到一个具体的人手里,然后 fail-closed 兜底。人签了字,动作发生;人没签字,或者根本没人能签字,动作就不发生。签字不保证动作正确,但它保证有一个具体的人,为让它发生负责。
最后叠个甲:我通篇只读了授权范围内的源码和文档,没有跑过真实审批会话;上面所有的运行手感,都来自源码路径和测试断言,不是我实测的。cordis 那套审批和通用审批在 UI 层会不会汇合,授权材料里看不到,我不下结论。你当机制图看,别当我亲测报告看。
行动出口,就一行:去源码里 grep -n "requiresApproval" packages/extensions/cordis-host-runner/src/index.ts,你会看见整个审批体系的支点:两行布尔运算,一行 awaiting-approval。
人机之间那道停下来问一问,本质就是这两行。想清楚它,比背十句 AI 安全都管用。
去读一下。
然后,回来吵架。
好啦,谢谢你观看我的文章,如果喜欢可以点赞转发给需要的朋友,我们下一期再见!敬请期待!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。