dsh-sop-guard
一个 DeepSeek Harness 插件。它把 skill 目录里的 SOP.json 清单变成真正的分步白名单:skill 带了清单之后,模型必须先申报自己在第几步,然后每一步允许改文件、跑命令、跑代码的范围,由插件在工具真正执行前卡住。被拒的调用没有副作用。
解决什么
skill 告诉模型该怎么做,但挡不住模型不照着做。两种失控很常见:跳过分析步骤直接给一个看起来很完整的答案;为了省时间抄近路,绕开清单里写明的必经步骤。这类问题改提示词没用,因为提示词只是建议。
这个插件把流程从提示词里搬到工具闸门上。当前步骤不允许写文件,模型就写不进去。
安装
还没发到 npm。从仓库装:
npm install github:ExpertKT/dsh-sop-guard
然后把它加进 profile 的 bundle 列表。插件通过 cordis.patch.yml 挂载:
- insert:
- id: sop-guard
name: dsh-sop-guard
config: {}
配置项都不填也能跑:
| 配置 | 默认值 | 说明 |
|---|---|---|
mode |
enforce |
enforce 直接拒绝;warn 只把同样的拒绝文本写进日志然后放行。填别的值会在加载时报错,不猜。 |
stateDir |
~/.dsh/sop-guard/flows |
流程按会话持久化的位置。填 false 只在内存里存。 |
清单
把 SOP.json 放在 skill 的 SKILL.md 旁边。加载这个 skill 就会为当前会话武装流程。
{
"version": 1,
"steps": [
{
"id": "inspect",
"title": "看仓库现状",
"tools": ["pwsh"],
"commands": ["^git status\\b", "^git branch\\b"]
},
{
"id": "review",
"title": "读失败的测试和它覆盖的模块",
"tools": [],
"witness": true
},
{
"id": "await-approval",
"title": "把方案给人看,等人点头",
"tools": [],
"approval": true
},
{
"id": "write",
"title": "改代码",
"tools": ["write", "edit"]
},
{
"id": "commit",
"title": "提交",
"tools": ["pwsh"],
"commands": ["^git add\\b", "^git commit\\b"]
}
]
}
步骤字段:
id:小写字母、数字、连字符,最长 24 字符,done是保留字。title:出现在拒信和状态输出里。tools:这一步允许的工具名。整个字段省略表示这一步不做工具限制。commands:匹配 shell 命令文本的正则。声明了pwsh/bash却不写commands,这一步的所有 shell 调用都会被拒——没有白名单就没有可核对的东西。witness:这一步不许改任何东西,但必须先用read/read_image/glob/grep读过东西。顺带读文件的 shell 命令不算。approval:这一步要人运行/sop approve才能推进。确认只管这一步,不跨步骤,也不写进状态文件——会话恢复后要重新问人。
顶层的 gatedTools 数组可以把额外工具名加进闸门的监视范围,对所有步骤生效。
模型怎么配合
插件只注册一个工具 sop_step:
sop_step({ step: "inspect" })申报当前步骤。申报管整步,不按调用次数消耗。sop_step({ step: "review", evidence: "..." })推进。evidence 至少 8 个字符,写清上一步看到了什么。上一步声明了工具时,还要求宿主真观察到一次成功的执行——所以一条退回码非零的失败命令不算数。sop_step({ skill: "名称" })在多个已装载的清单之间切换。sop_step({})查看当前状态。sop_step({ step: "done", evidence: "..." })提前结束流程。结束会丢掉整条闸门,所以它和推进一样:本步要先申报,声明了工具的步要有一次成功执行,标了witness的步要真读过依据,标了approval的步要人确认。宿主会记下是谁在第几步、用什么理由结束的。
推进之后新的一步是未申报状态,得再申报一次才能用工具。少了这条,一次申报就等于放开后面所有步骤。
当前步骤也会出现在模型的运行时上下文里,步骤一变就重播一遍。这一条针对的是插件管不住的那种情况:模型忘了清单存在,下一次调用前能看到自己在第几步。
人的出口
/sop 打印当前流程和位置。清单里标了 "approval": true 的步骤,要人运行 /sop approve 才放行;没有这一步,模型会停在原地,确认也只对当前步骤有效。/sop off 丢掉这个会话的流程并删掉状态文件,清单写错、或者模型被一条你写坏的规则顶住时用它。这两个出口只留给人。
到底拦住了什么
每一次受管调用都按当前申报的步骤比对:
write、edit、delete、move、copy、apply_patch只有在步骤里点名时才放行。pwsh、bash要求步骤点名,并且每一段命令都命中这一步的正则。- 命令串在引号外按
;、&、|、换行切成段,逐段核对。2>&1认作描述符复制,不切段。 - 自带命令的 shell(
pwsh -Command "..."、bash -c "...")核对内层命令,不核对pwsh这个词。让 shell 从文件或标准输入读命令的直接拒绝——没有可核对的内容。套两层 shell 同样拒绝。 -EncodedCommand、-enc一律拒绝。- 不允许改文件的步骤里,
Set-Content、Out-File、Remove-Item、cp、mv、mkdir、tee、dd这类命令同样过不去,换条路写文件也不行。 run_code的程序体会被扫模块字面量。不允许改文件的步骤里出现node:fs会被拒;声明了工具的步骤里出现node:child_process会被拒。- 标了
approval的步骤,人没运行/sop approve之前,sop_step推进不过去。
让这个闸门在实际使用中不至于把正常干活也卡死的有三点:
- 拒信写清步骤、原因、允许的工具、允许的命令、下一步和清单路径。
- 拒绝发生在工具执行之前,不会留半截文件。
- 推进和结束都要 evidence,也要宿主真观察到成功执行。只申报不干活,走不完流程,也退不出流程。
它不是什么
这一段请在信任它之前读完。
它不是沙箱。 命令核对是词法层的,看的是模型写下的命令文本,不是展开之后真正跑的东西。$HOME 是在核对之后才展开的。模型先前用写文件工具写下的脚本,运行时想干什么干什么。
evidence 是自述。 插件能证明某个工具跑过,证明不了模型读的是不是该读的文件,也证明不了它的结论是从读到的内容推出来的。witness 把门槛从「有工具成功」抬到「有读取工具成功」,这是把洞改小,不是堵上。想混过去的模型随便 read 一个文件就能过。
程序体扫描是文本匹配。 运行时拼出来的模块名,比如 "node:" + "fs",扫不到。
确认步骤要人坐在键盘前。 框架不会把自己的批准结果报给插件,有些安装里批准弹窗还是自动放行的。插件用自己的握手:标了 "approval": true 的步骤,人没输入 /sop approve 就推进不了。确认按步骤算,只存在内存里,会话恢复后要重新问。插件能证明有人敲了这条命令,证明不了这个人看过模型给出的方案。
子 agent 继承流程,不继承申报和干过的活。 子 agent 从父 agent 的当前位置开始,但要自己重新申报、自己跑出成功的工具调用;观察记录和 /sop approve 的确认都分开算。
运行时文本是中文。 拒信、sop_step 的工具说明、系统提示里的规则都用中文写,中文是这套东西的首要语言。英文清单能跑,但「你哪里做错了」仍然是中文。这份 README 是中文版,英文那份是配套参考。
持久化是每会话一个 JSON 文件。 两个进程恢复同一个会话 id 时靠锁文件仲裁。家目录不可写会关掉持久化并打一条警告,闸门本身照常工作。
如果你要的是「挡住一个主动想逃的模型」,请把允许名单放进操作系统或容器里。这个插件面向的是想好好干活、偶尔抄近路的模型。
状态
1.0.0。拒绝路径、命令白名单、evidence 门槛、观察执行门槛在真实 harness 里对着真清单跑过,但跑的是最后几次改动之前的构建。运行时上下文播报、/sop off、/sop approve、witness 的读取规则、run_code 程序体检查只有单元测试。线上验证还欠着,别当成已经验过。改代码之后从仓库副本先跑测试:
node test/guard.test.mjs
设计取舍和已知边界写在 DESIGN.md。examples/sop-demo/ 里有一份能直接加载的 skill 和清单,测试会核对它和 README 示例一致。测试与示例也打进 npm 包,装完可以自己跑 node node_modules/dsh-sop-guard/test/guard.test.mjs。
许可证
MIT。
No comments yet. Be the first to write one.