dsh-arg-guard — 工具入参 schema 守卫(DSH 插件)
DSH 校验工具输出:
output.schema对不上就抛ToolOutputError。 它从不校验工具输入:parameters只是渲染进模型提示词的一张纸。 于是错类型一路走到工具函数体里,变成一句裸 Node TypeError——The "path" argument must be of type string、p.replace is not a function、slug.toLowerCase is not a function。对 5 个已发布插件做 148 次参数模糊调用, 47 次以这种裸错误结束,来自 12 个不同入口点。
这个插件补上那道缺口:在 host 的 tools/pre-execute 瀑布里,按工具自己声明的
parameters 校验入参;违规就拒绝,并把确切的 JSON 路径 + 期望类型交给模型。
模型看到的不再是崩溃,而是一句能照着改的错。
缺口的位置(读 host 源码确认,不是推测)
| 位置 | 事实 |
|---|---|
_dsh-tools.js:3321 |
const gate = await this.ctx.waterfall(carrier, "tools/pre-execute", exec, () => ({ kind: "allow" })) —— 唯一的入参关卡 |
_dsh-tools.js:3345 |
拒绝理由原样变成模型看到的 Error: <reason> |
_dsh-tools.js:3091 / 3107 |
get(name, scope) / resolveExecution(name, scope, nested) 拿得到定义,定义上带 parameters |
| 输出侧 | output.schema 有强制校验,会抛 ToolOutputError |
也就是说:schema 写给模型看,但没人照着检查。 每个插件作者只能自己手写
typeof 守卫,而手写的总会漏——上面 47 次里没有一次是"作者知道但选择不防"。
它做什么
注册一个 tools/pre-execute 处理器,对每次工具调用:
{"dir":123} → {"kind":"deny","reason":"publish_gate.dir: expected string, got number (123)"}
{"mode":"fix"} → {"kind":"deny","reason":"publish_gate.dir: required but missing"}
{"dir":"E:/p","mode":"slow"} → {"kind":"deny","reason":"publish_gate.mode: expected one of \"check\", \"fix\", got string (\"slow\")"}
{"dir":"E:/p","extra":1} → {"kind":"deny","reason":"publish_gate.extra: unexpected property (allowed: dir, mode)"}
{"dir":"E:/p","mode":"fix"} → {"kind":"allow"}
(上面是真实输出,用 publish_gate 的 schema 驱动。)
拒绝理由的格式固定为 <工具名>.<JSON 路径>: expected <类型>, got <类型> (<值>),
根级错误省掉点号:publish_gate: expected object, got string ("…")。
安装
dsh plugin --profile desktop add @bushizhanghan/dsh-arg-guard
重启 DSH 后生效。只读:不写磁盘、不发网络请求,计数器只在内存里。
配置
- id: arg-guard
name: "@bushizhanghan/dsh-arg-guard"
config:
enabled: true # 总开关,默认 true
mode: deny # deny = 拦截;warn = 只记录、放行
skip: [] # 不检查的工具名,例如 [bash]
上线顺序建议:先 mode: warn 跑一段时间,用 arg_guard_status 看
violations 里有没有误报(工具声明的 schema 比真实契约更严时会误报),
确认干净再切 deny。
Agent 工具
| 工具 | 作用 |
|---|---|
arg_guard_status |
hook 是否挂上、mode、skip、计数器(checked/allowed/denied/warned/skipped/errors)、最近 10 条违规 |
arg_guard_check |
{tool, args} 干跑:返回和真实 hook 完全一致的 verdict 与理由,不执行、不计数 |
arg_guard_check 的典型用法:某次调用被拒了,把当时的参数丢进去看为什么;
或者发请求前先自检一遍,省一次往返。
校验哪些规则
| 规则 | 例 |
|---|---|
| 原始类型 | type: string / number / integer / boolean / null |
| 类型联合 | type: ["string","null"] |
| 必填 | required: ["dir"](传 undefined 等同于没传) |
| 枚举 / 常量 | enum、const |
| 嵌套对象 | properties 递归,路径用点号连起来 |
| 数组元素 | items(对象或元组形式) |
| 封闭对象 | additionalProperties: false(patternProperties 命中的键放行) |
| 变体 | oneOf / anyOf / allOf |
不校验什么(明确的边界)
- 不做类型强转:schema 说
integer,"3"就是错的。守卫的职责是让模型改, 不是替它猜。 - 不认识的 JSON Schema 关键字一律忽略:
$ref、if/then/else、format、dependencies、自定义关键字——看不懂就不判定,因为看不懂不等于调用是错的。 - 不校验输出:host 已经在做了。
- 不做跨进程:只作用于注册在同一个 host 里的工具。
为什么它不会把工具调用搞坏
这是这个插件的第一原则:守卫弄坏的调用,比没有守卫更糟。
- 整个处理器包在
try/catch里,任何异常都 fall through 到allow; - 解析不到定义(工具没注册、被 scope 限制、
resolveExecution抛错)→allow; - 工具没声明
parameters.properties→allow(没有契约就不存在违约); - 类型名不认识 → 不判定;
- schema 本身畸形(不是对象、是数组)→
allow; - 有
skip名单,随时能放行某个工具; - 有
mode: warn,可以先只观测。
validateArguments 本身也是纯函数 + 全 try/catch:它是 38 个测试里唯一被直接
断言的对象,包括"定义查询抛错仍然放行"这条。
开发
npm test # node --test,38 个用例
lib/engine.js 是纯函数(validateArguments(parameters, args) → null 或一句理由),
lib/index.js 只负责挂 hook、查定义、记账、注册两个只读工具。
License
MIT
No comments yet. Be the first to write one.