DSH plugin 工具参数被拒?Invalid schema、oneOf 丢类型与必填误判排查

故障排查发布于 2026-09-12作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH pluginInvalid schema for functiononeOf工具参数校验
插件注册工具后每轮对话都报 Invalid schema for function?三种拒法:parameters 根形状缺 type:"object"、type:"json" 投影丢类型、Codex 把可选参数 materialize。逐条给出定位与修法。

如果你给 DeepSeek Harness 写了一个工具插件,装完却让每一轮对话都报 Invalid schema for function 'xxx',那基本可以先怀疑 parameters 的根形状——它必须是 { type: "object", properties: {...} },而不是裸属性表。 同一句「工具参数被拒」其实包含三种互不相同的拒法:注册期 parameters 根形状不合法(模型 API 直接拒掉每一个请求)、type: "json" 在 schema 投影里丢掉线格式导致 oneOf 匹配零分支、以及 Codex 会把声明但非必填的顶层属性 materialize 成值而被误判为「可选被当必填」。三者定位与修法完全不同,混为一谈只会浪费时间。

DSH plugin 的三种拒法:Invalid schema for function、oneOf matched 0、可选被当必填

先按报错原文分流,三句话对应三条完全不同的链路。 分别看:

  1. 注册期根形状不合法——Invalid schema for function 'vision_query':插件 dsh-image-vision 注册 vision_query 工具时,parameters 传的是裸属性表({ images: {...}, prompt: {...} }),没有 JSON Schema 要求的 object 根。模型函数调用 API 要求参数 schema 根必须是 type: "object",于是 DeepSeek API 拒掉每一个请求,报 schema must be a JSON Schema of 'type: "object"', got 'type: null'。表现不是「这个工具不能用」,而是整个会话每一轮都失败——用户当时的描述就是「把 dsh 搞崩了」「整个不能工作了」(#297)。同类还有 Invalid schema for function 'cmd'(#447),同一根因。
  2. 对象参数被字符串化——must match exactly one oneOf branch (matched 0)cordis_defineplugin 参数是判别式 oneOf(两支均为 object:kind:"new" + idPrefix / kind:"existing" + pluginId)。从 Web GUI 调用时,{"kind":"new","idPrefix":"hello"} 这种完全合法的载荷却稳定报 matched 0cordis_inspect_queryinput(也是对象类型)同样失配。用户环境是 @deepseek-ai/[email protected](npm)+ Node v26.4.0 + Windows + Chrome(#1122)。
  3. 可选属性被实体化——write / bash 被 escalation 校验卡住:GPT-5.6-sol 走 openai-codex-responses 时,每一次 write / bash 调用都必须提交 sandbox_permissionsjustification 才能通过校验,否则直接失败。观察到的实际载荷是 {"sandbox_permissions": "workspace-write", "justification": ""},而用户根本没要求提权(#1149)。
  4. 三种拒法的判别速查:报错里出现 Invalid schema for function → 看插件 parameters 根形状;出现 oneOf ... matched 0 / must match exactly one → 看该字段是否被投影成了无类型字段或被字符串化;出现「某个可选字段缺了就失败」→ 看该模型路径是否 materialize 声明属性,以及宿主是否把它当成控制型字段。把这三类混在一起猜,是最常见的排查弯路。

DeepSeek Harness 机制:parameters 根形状未校验、type:"json" 投影丢类型、Codex materialize 声明属性

三条机制彼此独立,唯一共同点只是「工具参数在到达校验器之前已经变形」。 逐层拆解:

  1. 宿主侧不校验 parameters(第一类的根因)ctx.tools.register() 只校验了 output.schema,对 parameters 从不校验、原样透传给模型 API。后果是任何一个第三方插件注册了不合法的工具 schema,都会让整个会话的每一轮请求全部 400,而且报错既不点名出错的工具、也不说明原因——表面现象就是「装了个插件,整个跑不动了」(#297)。
  2. type: "json" 在投影里变成纯注解(第二类的一半)dsh-toolsparameterSchemaSpecToJsonSchematype: "json" 当作 annotation-only,发给模型 API 的字面内容就是 "input": { "description": "Optional query input; ..." }——处处没有 type。没有类型信息,API 层就把嵌套对象以 JSON 字符串发出;本地复现验证器也确认:JSON.parse 后的结果能通过 isPlainJsonRecord,原始字符串不行(#1122)。
  3. 下游没有任何补偿(第二类的另一半)dsh-cordis-host-runnervalidateInput 会拿 args.input 逐个方法比对 inputSchema(全都要求 type: "object"),而从传输层到校验层之间没有任何环节会重新 parse 一个被字符串化的值。两半合起来,就是「精确查询全挂、目录模式正常」——因为在已装包的 dsh-tool-* 里扫一遍,这个 input唯一一个输入侧 type: "json" 参数(#1122)。
  4. oneOf 校验器本身是对的(第二类的关键反证)cordis_define.plugin 声明的是两个 object 分支的 oneOf,投影完整保留了它,线格式上带着 type: "object"(直接调用导出的投影函数可以核验),并不像 type: "json" 那样丢类型。所以 matched 0 不是 oneOf 语义问题,而是 Web 路径把错误的运行时类型交给了校验器——matched 0 只是下游症状。校验器统计命中分支的逻辑本身很简单:恰好 1 支 → 通过,0 支 → matched 0,2 支以上 → 重叠非法(#1122)。
  5. Codex 会 materialize 已声明的顶层可选属性(第三类的根因):一组绕过宿主的直接探针把这点钉死了。向本机 CC Switch 的 Anthropic Messages 入口发一个 schema:required 只有 required_value,另有 optional_text(string)与 optional_mode(string + enum: ["alpha","beta"]),prompt 明确要求「只填 required_value,不要编造可选值」。返回结果仍是 {"required_value":"ok","optional_text":"","optional_mode":"alpha"}——optional string 被填成空串、optional enum 被填成第一个枚举值。这与宿主内观察到的 {"sandbox_permissions":"workspace-write","justification":""} 完全对应(#1149)。
  6. nullable 对照实验证明「未提供」是可表达的:把两个可选字段改成 oneOf: [{type:"string"},{type:"null"}](enum 同理加一支 null)后,同一条路由返回 {"required_value":"ok","optional_text":null,"optional_mode":null}。也就是说,Codex 路径表达「未提供」,只是需要用 nullable 表达;而 DeepSeek Harness 当时的 escalation 参数只接受 string,所以仍需一侧做归一化(#1149)。
  7. 归因修正:strict: null 不是必要条件:上游 pi#8105 把问题定位到 openai-codex-responsesstrict: null,但复现链路 DSH plugin pi-ai anthropic-messages → CC Switch 3.19.2 → Codex ResponsesapiFormat=openai_responses → Codex OAuth → chatgpt.com/backend-api/codex/responses → gpt-5.6-sol)没有经过 pi-ai 的 codex 适配器,却复现了同样的 optional materialization。更准确的表述是:问题在 Codex Responses 工具调用与 Anthropic optional-property 语义之间的兼容,strict: null 可能是某条路径上的触发或放大因素,但不是该行为成立的必要条件(#1149)。
  8. 跨路径反证:同一模型同一条账号也能正常省略可选参数:在 stock @deepseek-ai/[email protected] 上走 pi2dsh 内置的 OpenAI-Codex 路由、对一个干净工作区执行真实 write,从会话日志读回的提交参数恰好只有 ["file_path","content"]sandbox_permissionsjustification 都没有被 materialize;校验通过、文件落盘、回合 completed 收尾。这说明故障不在「模型不会省略可选参数」,而在那条特定线路如何呈现 schema#1149)。

DeepSeek Harness 按 JSON Schema 修根形状与三种后续规避

修法分三档:插件侧把根形状写对、宿主侧在注册期就响亮失败、调用侧为已知缺陷选择合适的绕行。 具体如下:

  1. 插件侧:parameters 必须是 object 根(第一类最直接的修法):
js
ctx.tools.register({
  name: 'vision_query',
  parameters: {
    type: 'object',
    additionalProperties: false,
    required: ['images'],
    properties: {
      images: { type: 'array', items: { type: 'string' } },
      prompt: { type: 'string' },
    },
  },
})

同时要检查属性级的 required 位置——把 required: true 写在属性对象里是无效的 JSON Schema,required 只能是对象层级的字符串数组(image-vision 插件当时就踩了这个坑,修复版本升到 0.1.1)。不写 type: "object" 的裸属性表一定会让每一次请求都被拒。 2. 宿主侧:注册期校验根契约,响亮失败register() 现在会在注册时校验 parameters 的根契约(必须是 object 根、type: "object"),违规立即抛 JsonSchemaError,在插件加载时就点名出问题,而不是污染之后所有请求。只校验根契约是有意为之:MCP 服务器经 SDK 的 zod 转换会输出 $schema / definitions 等关键字,属于强制子集之外的合法 JSON Schema 词汇,做完整子集校验会误伤它们。装上这一版后,坏插件会在加载阶段直接失败,而不是让整个会话崩掉。 3. 应急绕过:重置 profile 补丁文件:如果已经被坏插件卡住、连界面都进不去,把 profile 的 cordis.patch.yml 改回空数组再重启即可——重置之所以有效,是因为它让注册了坏 schema 的插件不再被加载

sh
# ~/.dsh/profiles/web/cordis.patch.yml 内容改为:
[]

这只是绕过表象,根因仍在插件 schema;确认是哪个插件后应升级到修好的版本,或用 DSH Plugin Hub 先卸载它。 4. type: "json" 参数:先加防御性再解析,长期改投影:第三类的一半(cordis_inspect_query.input)可以在 execute 里做防御性 JSON.parse——解析失败就自然落到原有校验错误,畸形字符串仍会被正常报出:

diff
 		async execute(args, exec) {
-			const data = await ctx.cordisInspect.query(args.platform, args.provider, args.method, args.input, requireAgent(exec), exec.signal);
+			let input = args.input;
+			if (typeof input === "string") {
+				try { input = JSON.parse(input); } catch { /* fall through */ }
+			}
+			const data = await ctx.cordisInspect.query(args.platform, args.provider, args.method, input, requireAgent(exec), exec.signal);
 			return {

更干净的长期方案是给 type: "json" 参数在投影阶段补上显式线格式类型,让 API 层从一开始就不必猜。已装包里这个 input 是唯一一个输入侧 type: "json" 参数,所以影响面可控。 5. oneOf 被字符串化:在 defineTool 层归一化(社区可用解):有人把修复放在 dsh-toolsdefineTool 包装里,新增一个递归 _coerceArgs,把「以 {[ 开头的字符串」递归解析回对象/数组,再交给 validate(在 dsh-tools/lib/index.jslib/types/schema.js 两处替换 execute 包装):

js
async execute(args, exec) {
    function _coerceArgs(val) {
        if (typeof val === "string") {
            if (val.charCodeAt(0) === 123 || val.charCodeAt(0) === 91) {
                try { return JSON.parse(val); } catch { return val; }
            }
            return val;
        }
        if (Array.isArray(val)) return val.map(_coerceArgs);
        if (val !== null && typeof val === "object") {
            const out = {};
            for (const k of Object.keys(val)) out[k] = _coerceArgs(val[k]);
            return out;
        }
        return val;
    }
    const coerced = _coerceArgs(args);
    const violations = validate(coerced);
    if (violations.length > 0) throw new ToolArgsError(violations);
    return userExecute(coerced, exec);
}

_coerceArgs 会新建对象而非改动被冻结的 args。实机验证环境为 [email protected] + Windows + Chrome,cordis_define 恢复正常、其他工具不受影响。但要注意这个解法的副作用:一个合法字符串参数本来就可能故意包含 JSON 文本,无条件递归解析会在校验前悄悄改变它的类型;更稳妥的做法是让归一化由 schema 引导——只在声明为 object/array/JSON 值或命中某个 oneOf 分支时才对字符串做解析,并补一条覆盖 cordis_define.plugincordis_inspect_query.input 两条路径的 Web 回归测试。 6. 可选参数被 materialize:换 Code Mode 或做 nullable 归一化#1149 的可用绕过是把该会话的工具呈现方式从原生 function calling 切到 PTC / Code Mode——模型只看到外层 run_code,其 code / description 都是真正必填,嵌套的 write / bash 在内部按 canonical schema 派发,因此不会出现可选字段被补值的问题,文件创建与写入恢复可用。隔离证据也表明文件系统后端、沙箱策略、canonical 工具运行时与 write 执行器本身都是好的,故障只限于原生 function 工具的 schema/调用路径。 7. 可选参数的正解:允许 null 并在校验前归一化:最小兼容修复是让 sandbox_permissionsjustification 接受显式 null,并在进入 escalation 校验前把 null 归一化为「未提供」;涉及 packages/shell/tool-bashpackages/shell/tool-pwshpackages/fs/tool-fspackages/fs/tool-fs/src/sandbox。也可以把「已知目标等于或窄于当前有效模式」的成对字段视为冗余 metadata 走 no-op(例如 danger-full-access + 请求 workspace-write),但真正变宽的请求必须保持现有流程read-only → workspace-writeread-only → danger-full-accessworkspace-write → danger-full-access),不得削弱 strictly-wider 校验、非空 justification、approval 与 never 下的 fail-closed 行为——这套语义在本站 sandbox 提权校验排查 里有更细的展开。 8. 更稳的长期方向是「denial-bound」:不要仅凭模型提供了 sandbox_permissions 就认定这是一次合法提权重试——模型输出是请求,不是授权。可以考虑:普通调用先发生真实 sandbox denial → denial 返回一次性 escalation_token → 只有携带该 token 的精确重试才能请求更宽模式 → 没有有效 token 的冗余 escalation 参数不进审批路径 → 或者干脆把普通工具与 escalation retry 拆成两个独立工具。eligibility 应绑定到会话、工具、归一化后的命令/操作、workdir 或目标路径与当前有效模式,并且短时效、一次性使用,避免并行或无关调用继承它。想自己写这类工具插件,可先看 DSH plugin 开发入门,把 parameters 根形状与 required 分层写对是从一开始就该守住的约定。

DSH plugin 排查注意事项

报错分流优先于猜测——三句报错原文对应三条完全不同的链路,混在一起猜是最常见的排查弯路。 七条要点:

  1. 报错分流优先于猜测Invalid schema for functionparameters 根形状;oneOf matched 0 看字段是否被字符串化;「可选字段缺了就失败」看模型路径是否 materialize 声明属性。
  2. 裸属性表必炸:无论自研的 DSH插件 还是第三方 DeepSeek插件,parameters 不写 type: "object" 都会让每一轮请求被拒,表现为「整个不能工作了」,而不是单个工具不可用。
  3. required 只在对象层级:属性内部的 required: true 是无效 JSON Schema,必须写成对象级的字符串数组。
  4. 只校验根契约是有意的:完整子集校验会误伤 MCP 服务器经 zod 转换产生的合法关键字。
  5. 谨慎做递归解析:无条件的「看起来像 JSON 就 parse」会改变合法字符串参数的类型,应让归一化由 schema 引导。
  6. 降级方案不等于根修:PTC / Code Mode 与重置 cordis.patch.yml 都只是绕过,escalation 相关语义不应为绕过而放松。
  7. 同现象未必同根因cordis_define.plugincordis_inspect_query.input 是两条独立链路,未经核验不要合并归因。
DSH Plugin Hub 已安装插件列表:定位并卸载 schema 不合法的插件

来源:Discussion #297Discussion #1122Discussion #1149earendil-works/pi#8105

常见问题

为什么 DSH plugin 工具参数会报 Invalid schema for function,而不是插件加载失败?

DeepSeek Harness 的函数调用 API 要求工具参数 schema 的根必须是 type: "object",所以坏 schema 会在每一次请求上被拒,而不是在加载插件时被拦下。如果插件的 parameters 传的是裸属性表,根上就没有 type,API 会拒掉**每一个**请求并回 Invalid schema for function 'xxx': schema must be a JSON Schema of 'type: "object"', got 'type: null'。早期宿主在 ctx.tools.register() 里只校验 output.schema、不校验 parameters,坏 schema 被原样透传下去,表现为整个会话每一轮都 400(来源:Discussion #297)。

DSH plugin 报 oneOf matched 0,是我的参数写错了吗?

不一定,这类 matched 0 在 DSH plugin 里多半是对象参数被字符串化造成的,而不是参数本身写错。以 cordis_define 为例,它的 plugin 参数确实是两个对象分支的 oneOfkind:"new" + idPrefix / kind:"existing" + pluginId),{"kind":"new","idPrefix":"hello"} 本该命中第一支。真正的原因是 Web 路径把嵌套对象当成了**字符串**交下来,两个对象分支自然都不匹配,matched 0 只是下游症状(来源:Discussion #1122)。

DeepSeek Harness 里 cordis_inspect_query 的 input 为什么也失配,和插件的问题是一回事吗?

两者不是一回事,但在 DeepSeek Harness 里同属参数 schema 在投影或校验阶段变形。cordis_inspect_query.input 声明为 type: "json",而 schema 投影把 type: "json" 当成纯注解,发给模型 API 的字面内容只有 "input": { "description": "Optional query input; ..." }——**完全没有 type**。模型无从判断形状,API 层就把嵌套对象以 JSON 字符串发出;下游 dsh-cordis-host-runnervalidateInput 又按各方法 inputSchema(都要求 type:"object")校验,中间没有任何环节把它 JSON.parse 回来。已装包里这个 input 是唯一一个输入侧 type:"json" 参数,这正好解释了「精确查询全挂、目录模式正常」(来源:Discussion #1122)。

DeepSeek Harness 下 Codex 真的把可选参数标成必填了吗?

准确说不是 DeepSeek Harness 把 optional 标成 required,而是 Codex 路径会主动 materialize 已声明的可选属性。canonical tool schema 只把真正必填的字段放进 required,pi-ai / Anthropic Messages 路径也完整保留该数组。真正发生的是 GPT/Codex 会为 schema 中**已声明但不在 required 里**的顶层属性主动 materialize 类型合法的默认值——optional string 变成 ""、optional enum 变成第一个枚举值——而 DeepSeek Harness 又把 escalation 字段的「存在」解释为一次真实的提权请求,于是普通 write / bash 调用被 escalation 校验卡住。换成 PTC / Code Mode 后文件写入立即可用,因为模型只看到外层 run_code(真正必填的只有 code / description),嵌套调用在内部按 canonical schema 派发(来源:Discussion #1149)。

相关术语

parameters root shape(parameters 根形状)
parameters 根形状是工具参数 schema 的顶层契约。函数调用 API 要求根必须是 `type: "object"`,插件若传裸属性表就会让每一轮请求都被拒。只校验根契约是有意为之:MCP 服务器经 SDK 的 zod 转换会带 `$schema` 等关键字,完整子集校验会误伤它们。https://github.com/deepseek-ai/deepseek-harness/discussions/297
schema 投影(schema projection)
schema projection 是把工具内部的参数规格转换成发给模型 API 的 JSON Schema 的过程。`type: "json"` 目前在此步被当成纯注解,线格式上不带 `type`,因此模型无法判断形状、嵌套对象容易以字符串形态往返。https://github.com/deepseek-ai/deepseek-harness/discussions/1122
materialize(属性实体化)
materialize 是 Codex 路径在工具调用时为 schema 中已声明却不在 `required` 里的顶层可选属性自动生成类型合法默认值的行为(string→""、enum→首个枚举值),也是「可选被当必填」现象的直接触发因素。https://github.com/deepseek-ai/deepseek-harness/discussions/1149

来源