Skip to content

metadata-protocol: /meta/types serves an EMPTY JSON Schema for action — the output-mode derivation of its ZodPipe has no properties, and the hand-crafted fallback never fires #17501

Description

@os-bill

Found while surveying #17232 (PR #17500), which needed the exact JSON Schema GET /meta/types serves for each metadata type. Out of that card's scope: #17232 is about missing title metadata on repeater ITEM schemas, this is the whole action schema going missing.

The defect

packages/metadata-protocol/src/protocol.ts derives the schema it serves with:

const result = z.toJSONSchema(schema, { unrepresentable: 'any' }) as Record<string, unknown>;

no io, so zod's default 'output'. ActionSchema is a ZodPipelazySchema(() => actionObject().refine(...).refine(...)) in packages/spec/src/ui/action.zod.ts — and the OUTPUT derivation of a pipe carries no properties. Measured on origin/main at e758131b39:

z.toJSONSchema(getMetadataTypeSchema('action'), { unrepresentable: 'any' })
  -> {"$schema":"https://json-schema.org/draft/2020-12/schema"}

z.toJSONSchema(getMetadataTypeSchema('action'), { unrepresentable: 'any', io: 'input' })
  -> 47 top-level properties (name, label, objectName, ..., params, locations, ...)

Lit and dark control across the whole registry: of the fifteen metadata types that have a *.form.ts, action is the ONLY one whose output derivation degenerates. Thirteen answer identically under both io settings (agent 26, app 30, dashboard 21, dataset 16, field 74, flow 23, hook 22, object 43, page 24, position 12, report 21, skill 17, tool 14 properties), and view answers a four-arm union under both. action alone answers props:47 under input and an empty object under output.

Why the hand-crafted fallback does not cover it

HAND_CRAFTED_SCHEMAS in the same file declares an action entry precisely for this case. It never fires:

const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : undefined)
    ?? HAND_CRAFTED_SCHEMAS[singular];

toJsonSchemaSafe falls back only when the conversion THROWS. Here it succeeds and returns { "$schema": ... }, which is truthy, so the ?? never reaches the fallback. The failure mode is a silent empty schema rather than a loud one — and it is a pipe away from every other type, so nothing about action looks different at the call site.

Consequence

GET /api/v1/meta and /meta/types advertise action with a schema that names no property. Studio's metadata-admin engine renders forms from that schema, and the *.form.ts layout addresses fields by name against it. Whatever the panel does with an empty schema, it is not reading the 47 keys ActionSchema actually accepts.

Not fixed by PR #17500

That PR titles thirteen repeater item schemas, action.params among them. Those titles ride both derivations identically, so they are correct either way — but under the served output derivation there is no params node for them to sit on. #17500's pin derives with io: 'input' and says so in a comment, deliberately, so this hole stays visible rather than being hidden behind the pin.

Suggested handling

Two candidate shapes, both needing a decision this finding does not make:

  1. serve the AUTHORING shape — io: 'input' — which is what a property panel is for, and which is already what packages/metadata-protocol/src/reference-sites.ts passes;
  2. make the degenerate case loud in toJsonSchemaSafe, so an empty derivation takes the HAND_CRAFTED_SCHEMAS arm instead of being served.

They are not exclusive, and (2) alone would leave action on a hand-written schema that no gate holds equal to ActionSchema.

Carrier: packages/metadata-protocol/src/protocol.ts (toJsonSchemaSafe, HAND_CRAFTED_SCHEMAS, the getMetaTypes entry builder) and packages/spec/src/ui/action.zod.ts.

Back-links: #17232, PR #17500.


Generated by Claude Code

Activity

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

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions