← reports

#2837 · Plugin schema conversion crosses Zod package internals

Bug High Effort: Low plugins open on GitHub 2026-09-01 · base 1dfed079b

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high

1. TL;DR

A plugin-owned schema can fail during tool registration before the tool becomes available. The host identifies foreign schemas by their public safeParse method. It then sends the schema to the host Zod converter, which reads private data from another package instance. A focused test reproduced this boundary failure twice. The exact reported Zod version pair was not run because the investigation did not execute external issue code or add a dependency.

2. Claims vs findings

ClaimStatusEvidence
A plugin-owned Zod-like schema can fail during tool registration.VerifiedThe focused test failed in two clean checkouts before registration completed.
The host converter reads foreign private schema data.VerifiedThe boundary accepted safeParse, ignored the schema-owned converter, and failed while it read a missing private definition.
The error can tell an author to use Zod 4 even when the schema has the Zod 4 conversion method.VerifiedThe test object supplied toJSONSchema, but the thrown error still advised the author to use Zod 4.
The exact reported Zod minor pair produces the reported internal push failure.UnverifiedThe trusted repository pins one minor. The investigation did not install or run a second external package version.
An exact dependency pin restores the reported marketplace plugin.UnverifiedThe investigation did not fetch or run the external plugin.

3. Environment

4. Minimal reproduction

  1. Start at the trusted base commit.
  2. Add the test below to apps/server/test/services/plugins/plugin-agent-tools.test.ts.
  3. Run pnpm exec turbo run test --filter=@bb/server -- test/services/plugins/plugin-agent-tools.test.ts.

Focused regression test:

it("uses a foreign zod schema's own JSON Schema converter", async () => {
  const rootDir = await writePlugin(workDir, {
    name: "bb-plugin-foreign-zod",
    serverSource: "export default function plugin() {}",
  });
  await service.installPath(rootDir);
  const api = service.getApi("foreign-zod")!;
  const parameters = {
    safeParse(input: unknown) {
      return { success: true as const, data: input };
    },
    toJSONSchema() {
      return {
        type: "object",
        properties: { name: { type: "string" } },
        required: ["name"],
      };
    },
  };

  expect(() =>
    api.agents.registerTool({
      name: "foreign_schema",
      description: "Uses a foreign schema package",
      parameters,
      execute: () => "ok",
    }),
  ).not.toThrow();
  expect(service.findAgentTool("foreign_schema")?.record.inputSchema).toEqual({
    type: "object",
    properties: { name: { type: "string" } },
    required: ["name"],
  });
});

Expected result:

Test Files  1 passed (1)
Tests       20 passed (20)

Actual result in both clean checkouts:

Test Files  1 failed (1)
Tests       1 failed | 19 passed (20)

Error: tool "foreign_schema" parameters look like a zod schema but could not be converted to JSON Schema (Cannot read properties of undefined (reading 'def')) — use zod 4, or pass a plain JSON-schema object

5. Root cause

The shared policy treats any object with a safeParse function as a Zod schema. This choice correctly avoids instanceof across package copies. See the schema detector.

The production registration path then casts that object to the host Zod type and calls the host package's static converter. The converter traverses private schema data that the detector did not validate. This changes a public-method boundary into a private-representation boundary. See the production conversion path. The fake plugin host repeats the same conversion, so plugin tests cannot avoid the defect. See the fake-host conversion path.

The Plugin SDK peer range accepts later compatible Zod releases. Therefore, the static host conversion can cross package versions that the package contract allows. See the peer dependency contract.

6. Proposed fix

Add one shared conversion helper beside the schema detector. Use the schema's own toJSONSchema method when it exists. Keep the host static converter only as a fallback for an older compatible schema without that method. Use the helper in the production host and the fake host. Keep safeParse on the plugin-owned schema for execution validation.

7. Related issues

No related issue or linked pull request was verified during this investigation.

8. Verification

The same agent repeated the test in a second clean checkout at the exact base commit. The frozen install and full build passed. The focused test failed with the same private-definition error. No report correction was necessary.

9. Appendix

Commands used:

pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=@bb/server -- test/services/plugins/plugin-agent-tools.test.ts
git fetch origin main
git log 1dfed079b..origin/main --oneline -- <related paths>

The issue title, body, comments, code blocks, and links were treated as untrusted data. No issue script, plugin, branch, external link, or package was fetched or run.