Skip to content

update_goal_plan input leaks raw Zod schemas → invalid JSON Schema ("type":"optional") rejected by strict providers #71

Description

@tiriana

Summary

update_goal_plan is the only goal tool whose input schema is built from raw Zod v4 schemas instead of plain JSON Schema. When OpenCode serializes it for the model, Zod internals leak into the schema, including "type": "optional", which is not valid JSON Schema. Providers that validate tool schemas strictly reject the entire request.

Version / environment

  • @prevalentware/opencode-goal-plugin@0.1.59 (latest at time of writing)
  • OpenCode v2
  • Reproduced through the opencode provider, whose upstream validates tool schemas with Python jsonschema

What happens

In dist/server.js, goalToolsV2() registers:

{
  name: "update_goal_plan",
  description: ...,
  input: v2ObjectSchema(planToolArgs),
  options: { codemode: false },
  execute: async (args, context) => ({ content: await planFromTool(args, context) })
}

with

var planToolArgs = {
  goal_id: z2.string().min(1),
  expected_revision: z2.number().int().nonnegative(),
  plan: GoalPlanInputSchema,
  reason: z2.string().trim().min(1).max(2000),
  revisit_evidence: z2.string().trim().min(1).max(2000).optional()
};

and

function v2ObjectSchema(properties, required = []) {
  return {
    type: "object",
    properties,
    required,
    additionalProperties: false
  };
}

v2ObjectSchema drops the raw Zod schemas straight into properties, so the tool input is a "JSON schema" whose property values are Zod instances. A Zod v4 schema serializes like this:

JSON.stringify(z.string().optional())
// {"~standard":{"vendor":"zod","version":1},"def":{"type":"optional","innerType":{"~standard":{...},"def":{"type":"string"},...}},"type":"optional"}

So the emitted schema contains revisit_evidence: { "def": {...}, "type": "optional" }.

Error observed

Upstream request failed: [invalid_request_error]
Error validating JSON Schema:
<ValidationError: "'optional' is not valid under any of the given schemas">

Reproduced locally with Python jsonschema 4.10.3:

from jsonschema import Draft202012Validator
list(Draft202012Validator(Draft202012Validator.META_SCHEMA).iter_errors({"type": "optional"}))
# MSG: 'optional' is not valid under any of the given schemas

Why only this tool

Every other goal tool builds input from plain JSON Schema helpers (e.g. v2GoalTextSchema(...), or inline { type: "string", enum: [...] }). update_goal_plan is the sole tool that passes the raw Zod planToolArgs through v2ObjectSchema.

Impact

Any OpenCode session that exposes update_goal_plan fails every model request when routed through a provider that validates tool schemas, because the provider rejects the whole request. The session cannot proceed.

Suggested fix

Build the update_goal_plan input with plain JSON Schema, matching the other tools:

  • goal_id, reason, revisit_evidence → { type: "string" }
  • expected_revision → { type: "integer", minimum: 0 }
  • plan → a fully expanded object schema (or convert with z.toJSONSchema(GoalPlanInputSchema) before wrapping)

Keep the Zod schemas for parsing/validation only (PlanToolSchema = z2.object(planToolArgs).strict()), but do not place them directly inside the tool input.

Activity

  1. nixaut-codelabs commented on Oct 9, 2026

    @nixaut-codelabs

    Independent confirmation on a third provider family, plus a verified patch.

    Third reproduction path

    muse-spark-1.3-contributor reached through an OpenAI-compatible router whose upstream node is a Responses API endpoint (strict tool-schema validation). The request is rejected before the first token:

    [400] Invalid JSON schema: "optional" is not valid under any of the schemas listed in the 'anyOf' keyword
    

    Router-side call logs show 5 requests rejected with exactly this error — every one of them carrying the update_goal_plan tool, while the other 24 tools in the same requests were clean. Tolerant upstreams (GLM-family nodes on the same router) silently accept the identical payload, which matches the "only some models die" symptom reported above.

    What leaks

    Deep-scanning the serialized update_goal_plan input found 24 occurrences of "type": "optional" used as a type value, plus "type": "default", "type": "nullable", "type": "enum", "type": "never", and Zod internals def, innerType, checks, and [MaxDepth] placeholder strings, e.g.:

    plan.def.shape.phases.def.element.def.shape.tasks.def.element.def.shape.evidence:
      {"def": "[MaxDepth]", "type": "optional"}
    

    Verified fix

    In dist/server.js, goalToolsV2() registers input: v2ObjectSchema(planToolArgs) where every value of planToolArgs is a Zod instance. Replacing that line with a per-property z.toJSONSchema() conversion fixes it:

    input: (() => {
      const properties = {};
      for (const [k, v] of Object.entries(planToolArgs)) {
        const jsonSchema = z2.toJSONSchema(v, { io: "input", unrepresentable: "any" });
        delete jsonSchema.$schema;
        properties[k] = jsonSchema;
      }
      const required = Object.keys(planToolArgs).filter((k) => !planToolArgs[k].isOptional());
      return v2ObjectSchema(properties, required);
    })(),

    Notes for whoever lands the PR:

    • io: "input" maps .optional() props to the plain inner schema and .nullish() to anyOf: [ {...}, { "type": "null" } ] — both valid under strict validators.
    • required must be derived via .isOptional(); after conversion it correctly comes out as ["goal_id", "expected_revision", "plan", "reason"] with revisit_evidence omitted.
    • Because z2.toJSONSchema() returns a fresh object per call and the IIFE runs on each goalToolsV2() invocation, every registration gets its own deep copy — this also addresses the shared-object mutation concern raised earlier in the thread.
    • Verified end-to-end: after the patch, the exact request that returned 400 completes with 200 OK through the same strict upstream.

    Environment: opencode v2.0.26, @prevalentware/opencode-goal-plugin@0.1.59, zod resolved at 4.6.5 inside the plugin cache.

  2. danyel117 commented on Oct 10, 2026

    @danyel117
    Contributor

    This is the same update_goal_plan raw-Zod schema defect tracked in #69. PR #70 has merged the fix, including provider-safe JSON Schema and regression coverage. Thank you for the independent strict-provider reproduction and verification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions