DSH plugin tool schema rejected: fix parameters and oneOf

TroubleshootingPublished 2026-09-12Author: DeepSeek Plugin Market
DeepSeek HarnessDSH pluginInvalid schema for functiononeOftool argument validation
Every turn fails with Invalid schema for function? Three distinct rejections: a parameters root missing type:"object", json losing its wire type.

If you wrote a tool plugin for DeepSeek Harness and every turn now fails with Invalid schema for function 'xxx', suspect the parameters root contract first — it must be { type: "object", properties: {...} }, not a bare property map. The single phrase "tool parameters rejected" actually covers three unrelated failures: an invalid parameters root at registration time (the model API rejects every request), type: "json" losing its wire type during schema projection so oneOf matches zero branches, and Codex materializing declared-but-optional top-level properties so they look like optionals forced into required. Each one has a completely different diagnosis and fix; conflating them only wastes time.

DSH plugin: three rejections — Invalid schema for function, oneOf matched 0, optionals as required

Start by splitting on the exact error text — each message maps to a different chain. Taking them in turn:

  1. Invalid root at registration time — Invalid schema for function 'vision_query': the dsh-image-vision plugin registered a vision_query tool whose parameters was a bare property map ({ images: {...}, prompt: {...} }) with no object root. The function-calling API requires the parameter schema root to be type: "object", so the API rejected every request with schema must be a JSON Schema of 'type: "object"', got 'type: null'. The symptom was not "this one tool is unavailable" but "the whole session fails on every turn" — users described it as crashing DSH and making it completely unusable (#297). The same root cause produces Invalid schema for function 'cmd' (#447).
  2. Object parameter stringified — must match exactly one oneOf branch (matched 0): cordis_define's plugin parameter is a discriminated oneOf over two object branches (kind:"new" + idPrefix, kind:"existing" + pluginId). Called from the Web GUI, the perfectly valid payload {"kind":"new","idPrefix":"hello"} reliably reports matched 0, and cordis_inspect_query's object-valued input fails the same way. Reported on @deepseek-ai/[email protected] (npm) with Node v26.4.0 on Windows and Chrome (#1122).
  3. Optional properties materialized — write / bash stuck in escalation validation: with GPT-5.6-sol on openai-codex-responses, every write / bash call must submit both sandbox_permissions and justification to pass validation. The observed payload was {"sandbox_permissions": "workspace-write", "justification": ""}, even though the user never asked for escalation (#1149).
  4. Quick triage rule: Invalid schema for function means check the plugin's parameters root; oneOf ... matched 0 or must match exactly one means check whether that field was projected without a type or handed over as a string; "a call fails whenever an optional field is missing" means check whether the model path materializes declared properties and whether the host treats them as control fields. Guessing across all three categories is the most common detour.

DeepSeek Harness mechanism: unvalidated parameters root, type:"json" losing its type, Codex materializing declared properties

The three mechanisms are independent; their only common trait is that the arguments get deformed before they ever reach the validator. Layer by layer:

  1. The host never validated parameters (root cause of the first failure): ctx.tools.register() validated only output.schema and passed parameters through to the model API untouched. As a result, any third-party plugin registering an illegal tool schema makes every request of the entire session 400, with an error that names neither the offending tool nor the reason — which is why it looks like "I installed a plugin and everything stopped" (#297).
  2. type: "json" becomes annotation-only in the projection (half of the second failure): parameterSchemaSpecToJsonSchema in dsh-tools treats type: "json" as annotation-only, so what reaches the model API is "input": { "description": "Optional query input; ..." } — no type anywhere. With no type information, the API layer emits the nested object as a JSON string; local validator replication confirms the distinction: JSON.parse output passes isPlainJsonRecord, a raw string does not (#1122).
  3. Nothing downstream compensates (the other half): validateInput in dsh-cordis-host-runner checks args.input against each method's inputSchema (all requiring type: "object"), and nothing between transport and validation ever re-parses a stringified value. Together the two halves explain "all exact queries break while catalog mode works": scanning the shipped dsh-tool-* packages, this input is the only input-side type: "json" parameter (#1122).
  4. The oneOf validator itself is correct (the key counter-evidence): cordis_define.plugin is declared as a oneOf over two object branches, and the projection keeps it intact, including type: "object" on the wire (verifiable by calling the exported projection function directly) — unlike type: "json", it does not lose its type. So matched 0 is not a oneOf semantics problem but the Web path handing the validator the wrong runtime type. The validator's branch counting is straightforward: exactly 1 branch validates, 0 branches gives matched 0, 2+ branches is an illegal overlap (#1122).
  5. Codex materializes declared top-level optionals (root cause of the third failure): a set of direct probes bypassing the harness pinned this down. A schema sent straight to the local CC Switch Anthropic Messages endpoint had required: ["required_value"] plus optional_text (string) and optional_mode (string with enum: ["alpha","beta"]), and the prompt explicitly asked to call the tool with only required_value and not to invent optional values. The response still came back as {"required_value":"ok","optional_text":"","optional_mode":"alpha"} — the optional string filled with an empty string and the optional enum with its first value. That matches the in-harness observation {"sandbox_permissions":"workspace-write","justification":""} exactly (#1149).
  6. A nullable control experiment shows "not provided" is expressible: rewriting both optionals as oneOf: [{type:"string"},{type:"null"}] (and the enum with a null branch) made the same route return {"required_value":"ok","optional_text":null,"optional_mode":null}. In other words the Codex path can express absence — it just needs nullability to do so — while DeepSeek Harness's escalation parameters only accepted strings at the time, so one side still had to normalize (#1149).
  7. Attribution corrected: strict: null is not necessary: upstream pi#8105 located the problem in openai-codex-responses's strict: null, but the reproduction chain DSH plugin pi-ai anthropic-messages → CC Switch 3.19.2 → Codex Responses (apiFormat=openai_responses → Codex OAuth → chatgpt.com/backend-api/codex/responses → gpt-5.6-sol) never passes through pi-ai's codex adapter and still reproduces the same optional materialization. The more accurate framing is a compatibility gap between Codex Responses tool calling and Anthropic optional-property semantics; strict: null may trigger or amplify it on one path, but it is not a necessary condition (#1149).
  8. Cross-path counterexample: the same model and account can omit optionals fine: on a stock @deepseek-ai/[email protected] driving pi2dsh's built-in OpenAI-Codex route, a real write against a clean workspace submitted arguments read back from the session log as exactly ["file_path","content"] — neither sandbox_permissions nor justification was materialized. Validation accepted the call, the file landed on disk, and the turn closed completed. So the failure is not "the model cannot omit optional parameters" but how that specific wire presents the schema (#1149).

DeepSeek Harness: fix the JSON Schema root, then three ways around the rest

The fixes come in three tiers: write the root correctly in the plugin, fail loudly at registration in the host, and pick the right workaround on the calling side. Specifically:

  1. Plugin side: parameters needs an object root (the direct fix for the first failure):
js
ctx.tools.register({
  name: 'vision_query',
  parameters: {
    type: 'object',
    additionalProperties: false,
    required: ['images'],
    properties: {
      images: { type: 'array', items: { type: 'string' } },
      prompt: { type: 'string' },
    },
  },
})

Also check where required sits: putting required: true inside a property object is invalid JSON Schema — required may only be an object-level array of strings (the image-vision plugin hit exactly this, with the fix released as version 0.1.1). A bare property map without type: "object" guarantees every request is rejected. 2. Host side: validate the root contract at registration and fail loudly: register() now checks parameters's root contract (must be an object root with type: "object") at registration and immediately throws JsonSchemaError, naming the problem while the plugin loads instead of poisoning every later request. Root-only validation is deliberate: MCP servers converted through the SDK's zod layer emit keywords such as $schema / definitions, which are legal JSON Schema vocabulary outside the enforced subset — full-subset validation would wrongly reject them. With this in place, a bad plugin fails at load time rather than taking the whole session down. 3. As a stopgap: reset the profile patch file: if a bad plugin has already wedged you so badly that the UI will not come up, set the profile's cordis.patch.yml back to an empty array and restart — that works because it stops the plugin registering the bad schema from being loaded at all:

sh
# set the contents of ~/.dsh/profiles/web/cordis.patch.yml to:
[]

This only bypasses the symptom; the root cause is still the plugin schema, so once you know which plugin it is, upgrade to a fixed version or uninstall it through DSH Plugin Hub. 4. type: "json" parameters: add a defensive re-parse now, fix the projection later: for half of the second failure (cordis_inspect_query.input) you can defensively JSON.parse inside execute — a parse failure falls through to the normal validator error, so malformed strings are still reported properly:

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 {

The cleaner long-term fix is to give type: "json" parameters an explicit wire type during projection, so the API layer never has to guess. Scope is contained: this input is the only input-side type: "json" parameter in the shipped packages. 5. Stringified oneOf: normalize at the defineTool layer (community-usable fix): one reported fix replaces the execute wrapper inside defineTool in dsh-tools (both lib/index.js and lib/types/schema.js) with a recursive _coerceArgs that parses strings beginning with { or [ back into objects/arrays before validate runs:

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 builds new objects rather than mutating the frozen args. It was verified on [email protected] with Windows and Chrome: cordis_define succeeded and other tools were unaffected. Mind the side effect, though: a legitimate string argument may intentionally contain JSON text, and unconditional recursive parsing would silently change its type before validation. The safer route is to make coercion schema-guided — only parse when the declared schema expects an object, array, JSON value, or a matching oneOf branch — and to add a Web regression covering both cordis_define.plugin and cordis_inspect_query.input. 6. Materialized optionals: switch to Code Mode or normalize nullable values: the usable workaround in #1149 is to change the session's tool presentation from native function calling to PTC / Code Mode — the model only sees the outer run_code, whose code / description are genuinely required, and nested write / bash calls are dispatched internally against the canonical schemas, so no optional field gets auto-filled and file creation and writing work again. The isolation evidence also shows the filesystem backend, sandbox policy, canonical tool runtime, and write executor are all functional; the failure is confined to the native function-tool schema/calling path. 7. The proper fix for optionals: allow null and normalize before validation: the minimal compatibility fix is to let sandbox_permissions and justification accept an explicit null and to normalize null back to "not provided" before escalation validation; the surface spans packages/shell/tool-bash, packages/shell/tool-pwsh, packages/fs/tool-fs, and packages/fs/tool-fs/src/sandbox. Alternatively, treat a pair of fields whose known target equals or is narrower than the current effective mode as redundant metadata and no-op it (for example danger-full-access + requested workspace-write). But genuinely widening requests must keep the existing flow (read-only → workspace-write, read-only → danger-full-access, workspace-write → danger-full-access), and strictly-wider checks, non-empty justification, approval, and fail-closed behavior under never must not be weakened — that contract is expanded in more detail in our sandbox escalation validation write-up. 8. The more robust long-term direction is denial-bound: do not treat the mere presence of sandbox_permissions as a legitimate escalation retry — model output is a request, not authorization. Consider: a normal call first hits a real sandbox denial → the denial returns a one-shot escalation_token → only an exact retry carrying that token may request a wider mode → redundant escalation fields without a valid token never enter the approval path → or simply split the normal tool and the escalation retry into two separate tools. Eligibility should be bound to the session, tool, normalized command/operation, workdir or target path, and effective sandbox mode, and it should be short-lived and single-use so parallel or unrelated calls cannot inherit it. If you want to build tools like these yourself, start with our DSH plugin development primer — getting the parameters root and object-level required right from day one is the convention to hold.

DSH plugin troubleshooting notes

Triage on the exact error text before guessing — those three messages map to three entirely different chains, and lumping them together is the most common detour. Seven points to keep in mind when a DeepSeek Harness plugin tool schema is rejected:

  1. Triage on the exact error text before guessing: Invalid schema for function points at the parameters root; oneOf matched 0 points at a stringified or untyped field; "a call fails whenever an optional field is missing" points at a model path that materializes declared properties.
  2. A bare property map always breaks: whether you are writing a DeepSeek Harness plugin or auditing third-party DSH plugins, omitting type: "object" in parameters rejects every turn, which looks like the whole harness dying rather than one tool being unavailable.
  3. required lives at the object level only: required: true inside a property is invalid JSON Schema; write an object-level array of strings.
  4. Root-only validation is intentional: full-subset validation would wrongly reject legal keywords produced by MCP servers through zod conversion.
  5. Be careful with recursive parsing: an unconditional "looks like JSON, parse it" rule changes the type of legitimate string arguments; keep coercion schema-guided.
  6. Workarounds are not root fixes: PTC / Code Mode and resetting cordis.patch.yml are bypasses, and escalation semantics should not be relaxed to accommodate them.
  7. Same symptom, different root cause: cordis_define.plugin and cordis_inspect_query.input are two independent chains; do not merge their attributions without verification.
DSH Plugin Hub installed plugins: locate and remove a plugin with an invalid schema

Sources: Discussion #297, Discussion #1122, Discussion #1149, earendil-works/pi#8105.

FAQ

Why does a DSH plugin fail with Invalid schema for function instead of a plugin load error?

In DeepSeek Harness the function-calling API requires the parameter schema root to be type: "object", so a bad root is rejected on every request rather than at plugin load time. If a plugin passes a bare property map, the root carries no type, so the API rejects **every** request with Invalid schema for function 'xxx': schema must be a JSON Schema of 'type: "object"', got 'type: null'. Early hosts only validated output.schema inside ctx.tools.register() and never checked parameters, so the bad schema was passed through verbatim and the whole session 400'd on every turn (Source: Discussion #297).

A DSH plugin reports oneOf matched 0. Is my payload wrong?

Not necessarily — in a DSH plugin this matched 0 usually comes from an object argument being stringified rather than from a wrong payload. cordis_define's plugin parameter really is a oneOf over two object branches (kind:"new" + idPrefix / kind:"existing" + pluginId), and {"kind":"new","idPrefix":"hello"} should match the first branch. What actually happens is that the Web path hands the validator the nested object as a **string**, so both object branches fail and matched 0 is merely the downstream symptom (Source: Discussion #1122).

In DeepSeek Harness, cordis_inspect_query.input fails too. Is that the same bug?

No — the two are separate chains in DeepSeek Harness, though both come from a parameter schema being deformed during projection or validation. cordis_inspect_query.input is declared as type: "json", and the schema projection treats that as annotation-only: what goes to the model API is literally "input": { "description": "Optional query input; ..." } — **no type at all**. The model cannot infer the shape, so the API layer emits the nested object as a JSON string; downstream, validateInput in dsh-cordis-host-runner checks it against each method's inputSchema (all requiring type: "object"), and nothing in between ever re-parses it. In the shipped packages this input is the only input-side type:"json" parameter, which is exactly why exact inspect queries break while catalog mode works (Source: Discussion #1122).

On DeepSeek Harness, does Codex really mark optional parameters as required?

DeepSeek Harness does not mislabel optionals; the Codex path is what actively materializes declared optional properties. The canonical tool schema only puts genuinely required fields into required, and the pi-ai / Anthropic Messages path preserves that array intact. What happens is that GPT/Codex actively materializes type-valid defaults for declared-but-not-required top-level properties — an optional string becomes "", an optional enum becomes its first value — and DeepSeek Harness then reads the mere *presence* of escalation fields as a real privilege request, so ordinary write / bash calls get stuck in escalation validation. Switching to PTC / Code Mode makes file writes work again, because the model only sees the outer run_code (whose code / description are genuinely required) and nested calls are dispatched internally against the canonical schemas (Source: Discussion #1149).

Related Terms

parameters root contract
The top-level shape of a tool's parameter schema. The function-calling API requires the root to be `type: "object"`; a bare property map gets every request rejected. Validating only the root is deliberate: MCP servers converted through the SDK's zod layer carry keywords such as `$schema`, which full-subset validation would wrongly reject.https://github.com/deepseek-ai/deepseek-harness/discussions/297
schema projection
The step that converts a tool's internal parameter spec into the JSON Schema sent to the model API. `type: "json"` is currently treated as annotation-only, so the wire schema carries no `type` and nested objects tend to travel as strings.https://github.com/deepseek-ai/deepseek-harness/discussions/1122
materialize
On the Codex path, the model auto-generates type-valid defaults for declared-but-optional top-level properties (string to "", enum to its first value). It is the direct trigger of the 'optional treated as required' symptom.https://github.com/deepseek-ai/deepseek-harness/discussions/1149

Sources