@steelunion/dsh-confirm-gate
Counts persistent operations within a turn and escalates on the total: a reminder first, then an explicit stop instruction, then an approval prompt. Reads the session sandbox mode on every call, and stays silent under read-only and workspace-write.
Install
dsh plugin --profile web add @steelunion/dsh-confirm-gate
Sandbox test
This plugin has not been tested yet. Submit a test →
README
dsh-confirm-gate
你的 AI 助手一口气改 20 个文件、烧掉一整天额度之前,会先停下来问你一句。
装上它,模型每动一次手就记一笔。额度快用完时它开始提醒自己刹车,
到硬上限就停下来等你回一句 confirm。
不用改 DSH 内核,没有必填配置。
*Stops your AI agent before it rewrites 20 files and burns a day of budget —
it counts, warns, then asks you to type confirm before going further.*
[English](README.md) | 中文 · v0.3.0
v0.3.0 更新了什么
沙盒模式下,门控现在也照样工作。
以前在 read-only 和 workspace-write 下它会自动让开,理由是「沙盒已经把模型能碰的东西框住了」。但那个理由管的是能碰什么,不是碰多少次:workspace-write 会话里连改 40 个文件,还是 40 次改动,第 41 次之前照样值得提醒一句。
所以 SANDBOX_MANAGED_MODES 现在是空集合,任何模式下都计数。计数、三档提醒、confirm 放行这些都没有变。如果你依赖旧的让开行为,把 lib/index.js 里的 SANDBOX_MANAGED_MODES 改回 ['read-only', 'workspace-write'] 即可。另外修了一处正确性问题:模式检测改成每次调用现读,不再缓存,会话中途切换权限模式会立刻跟上。
先说人话
你刚装好 dsh。你手上多了个很能干的东西,但它没有方向盘。
它自己干很久,中间一句话不说,你回来才发现它干错了。同一个功能,上一轮写过,这一轮又新建一个文件重写一遍。你想让它接着做,却不知道它做到哪。
这个插件就是那个方向盘。
- 翻文件、搜代码、看目录,都不算次数。真的动手才算一次。
- 每算一次,它都会收到一句话,要它回来找你确认。
- 到上限就停下来,等你回一句
confirm。
装完不用配。想松一点紧一点,改两个数字。连确认用的词都能改成你自己的。
下面是想知道它怎么实现的人看的。
它跑起来是这样的
模型发起第 11 次文件修改
↓
confirm-gate 记一笔,超了免确认额度
↓
往上下文注入:「立刻停止,向用户索要 confirm」
↓
第 15 次(硬上限)→ 工具失败,等用户回 confirm
↓
用户回一句 confirm → 整轮放行,计数器清零
它是一个会说话的计数器:每算一次都会注入一句话。前 10 次是提醒,
11 到 14 次加重成指令要它停下,15 次才真拦。实测多数模型在第一两次提醒后就会自己停,走不到上限。
三十秒上手
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:SteelUnion/dsh-confirm-gate
装完重启 DSH 就生效。想调额度就改 cordis.patch.yml:
- id: confirm-gate
name: dsh-confirm-gate
config:
freeLimit: 10 # 会提醒,但放行
hardLimit: 15 # 第 15 次停下来问你
confirmWord: confirm
它替你省什么
场景 · 没有它 · 有它
模型陷入循环、反复改同一个文件 · 一路烧到额度用完 · 第 11 次开始提醒,第 15 次停下
你离开电脑半小时 · 回来发现改了两百个文件 · 停在你回来之前
想让它继续 · 不知道它跑到哪了 · 回一句 confirm,整轮放行
它不是安全沙箱——沙箱是 DSH 自己的事。它管的是成本与方向:
让模型在烧钱和跑偏之前,有能力自己刹车。
下面开始是细节
在完全权限的主机上,让 AI Agent 可控运行的次数闸框架。
是什么 — 按轮统计持久化操作。给模型一个刹车,不是一堵墙。
已验证 — Windows 真机热切换(full → workspace-write → read-only → full),全程不重启;模型在第 11/15 次自己停下来了。
成本 — 约 0.27 USD 模型花费。零外部依赖,[dist/](dist/) 里有可直接安装的包。
沙盒 / 只读模式下它自动让位 —— 与 DSH 自带沙盒互补,不是替代。
微信通道里拦掉中途选项 —— 回合制通道承载不了半路弹卡,见[第九节](#九微信通道拦截中途交互选项)。
一个给 DeepSeek Harness 用的运行时插件。提供每轮对话独立的持久化操作计数器,在免确认额度用尽后向模型注入停止指令,达到硬上限时索要用户 confirm。
兼容性 — 在 DSH0.2.0-rc.2上开发并验证;peer 范围写成>=0.2.0-rc.2,所以0.2.1-rc.2及之后的 RC 版本也能装。如果将来某个 DSH 版本改动了本插件绑定的工具派发钩子,它会明确报错,不会静默失效——见[第五节验证方法](#七怎么确认装好了)。
这不是安全边界,是成本边界和方向纠偏机制。
它假设 Agent 运行在没有沙盒接管边界的环境里,目的是让模型在烧 token 和乱改文件之前有能力自己刹车。真正的沙箱隔离由 DSH 自己的 sandbox 提供,与本插件互补而非替代。
成本数字是作者估算,见 [dist/COSTS.md](dist/COSTS.md)。当天其余的钱花在环境排障和研究上,不在这个插件里。
生效范围:沙盒下保持沉默
插件在每一次工具调用时现判要不要计数。模式有两个数据源,按优先级:
1. ctx.sessionProjections.stateOf(session, 'permissions').sandbox —— 会话当前的旋钮值。热切换权限模式时,变的是它。
2. ctx.shell.sandboxMode —— executor 的默认模式。官方注释原话是 "the sandbox mode this executor applies by default",它不跟随热切换,所以只作回退。
这个区分是必须的:只看默认值的话,切进沙盒再切回来,插件读到的永远是启动时那个值。
两个数据源都通过 ctx.get(name) 软探测,绝不直接读 ctx.shell。cordis 的依赖注入是惰性的,碰没声明的服务属性会抛注入门错误 —— 这条路径上抛一次异常,插件就会彻底静默失效。软探测不会抛。
沙箱模式 · 是否计数 · 为什么
read-only · ❌ 不计数 · 边界已被沙盒全部接管
workspace-write · ❌ 不计数 · 同上
danger-full-access · ✅ 计数 · 边界没人管
custom · ✅ 计数 · 自定义组合下边界仍然没人管
undefined / 无 shell 服务 · ✅ 计数 · 这个 executor 不沙盒
两个数据源都读不到 · ✅ 计数 · 倾向生效:静默失效比多提醒糟得多
最后一行是刻意的。这不是安全边界,是记账工具,所以两种失效不对称:
- 该沉默时计数 → 多一条提醒,看得见、改得掉
- 该计数时沉默 → 插件装着却什么都不做,日志里还看不出异常
后者严重得多。所以任何读不到的模式,一律按「不沙盒」处理。
说白了:沙盒下这个插件是多余的 —— 更糟的是,它会把正常操作再拦一次。 所以它在已经有人记账的地方自动让开。
钩子始终注册,判定按次进行。这就是热切换能生效的原因:会话切进沙盒,下一次调用就安静;切回来,计数立刻恢复,不用重启。
### ✅ 现状:已验证,含实时热切换
在 Windows、DSH 0.2.0-rc.2 上手工跑过。权限模式是在会话运行期间实时切换的,全程没重启 DSH,顺序为 完全 → 工作区 → 只读 → 完全:
| 模式 | 写文件 | pwsh | 插件是否计数 |
|---|---|---|---|
| danger-full-access | ✅ 成功 | ✅ 能跑 | ✅ 1/10 → 2/10 → 3/10 |
|workspace-write| ✅ 成功 | ❌0xC0000142| ❌ 沉默(2 次) |
| read-only | ❌ 沙盒拒绝 | ✅ 能跑 | ❌ 沉默(2 次) |
| danger-full-access(切回) | ✅ 成功 | ✅ 能跑 | ✅ 1/10,计数恢复 |
两处值得看。中间两行的「沉默」说明分工正确——沙盒该拒绝的拒绝了、该限制的限制了,门控一声不吭。最后一行说明门控在同一个进程里回来了,这正是「按次判定而非加载时判定」的意义。
老实交代一点:在这个部署里ctx.get('shell')返回undefined,所以「不沙盒」的结论来自读不到 → 倾向生效这条兜底分支,而不是真的读到了模式。行为是对的,走的是回退那条路。如果某个部署暴露了shell,走的会是主路径。
适合谁
场景 · 是否适合
你给了 Agent 完全权限,怕它无限试错烧 token · ✅ 正是为此
你希望 Agent 在做满 N 次写操作后主动问你 · ✅
你希望 read / grep / 联网查完全不被打断 · ✅ 非持久化永不计数
你想用它当沙箱、防住恶意模型 · ❌ 用 DSH 自带 sandbox
你用的是别的 Agent 框架 · ❌ 这是 DSH 插件
取舍与已知局限
先说取舍,因为那才是有意思的部分。
1. 前 N 次完全没有闸。 默认 freeLimit: 10,第 1~10 次只提醒不拦。这个窗口是故意留的 —— 给模型自己纠错的空间。要零容忍就把 freeLimit 设成 0,插件会在第一次持久化操作前就索要确认。
2. confirm 防的是误操作,不是注入。 威胁模型是「信任的用户 + 会犯错的模型」,要防的失败是无限试错。提示注入是另一层的事 —— 那是 DSH 的审批通道和沙箱负责的。别把它当成不信任输入和你文件系统之间唯一的东西。
3. 放行规则粗。 任何含识别语的消息放行整轮,包括只是在讨论这个词的消息。这是故意的:歧义按用户打出来的字解决,不靠插件猜意图。
4. 命令判定是名字比对,不是解析器。 Get-ChildItem $p.GetFiles() 会被判成持久化,因为插件不解析管道。错误方向是选过的:宁可多计,不可漏拦。
5. 一次工具调用算一次,不管它动了几个文件。
为什么没选别的
另一个方案 · 为什么不用
硬拦截,不计数 · 第一版在 OpenClaw 里试过。一堵墙会把每次写操作变成用户没要求的打断。改成计数 + 明确的停止指令后,模型自己会算账 —— 第 11/15 次它自己停了。刹车比墙好用。
纯插件、不挂 DSH 的钩子 · 每轮计数必须来自工具派发层。包一层模型 API 看不到单次工具调用,也就数不了。计数得放在调用发生的地方。
塞进沙箱里 · 两个问题、两层东西。沙箱回答「这件事许不许」,这个回答「已经做了多少」。塞进沙箱,预算就会绑在一个随时会变的权限模型上。
一、设计思路
给 Agent 完全权限是必要的——沙箱会让 pwsh 这类工具直接崩掉(见[附录](#十附录为什么需要完全权限))。但完全权限意味着没有任何东西拦得住它。
这个插件的假设是:
不给硬墙,给计数器和刹车信号。
因为实测发现:模型在收到明确的「立即停止」指令后会停手(见[实测记录](#八实测记录))。所以不必在第一次就越权拦住,只要把额度标上刻度,模型自己会算。
二、三档行为
每轮对话开始,计数归零
│
▼
用户消息含 confirmWord ?
…(truncated)Rendered from the README published on npm. Formatted only, not edited.
Versions
Published on npm::2,最后更新 2026-11-01