@steelunion/dsh-confirm-gate

securitynpmMIT

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.

下载量 / Downloads
230

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。

兼容性 — 在 DSH 0.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

Links

GitHub · Homepage · npm

Same category