Skip to content

manifest.permissions hides the structured block's named unknown-key refusal under invalid_union — a third door in the same class as #14722 #16328

Description

@huangyiirene

Found while sweeping #14721 (the "ManifestSchema is an open object" prose sweep). Filed unassigned and unlabelled for triage. ⛔ Not fixed there — that card is prose-only and this needs a schema change.

What was measured

Against packages/spec built at 7a265fbb49, through dist/kernel/index.mjs:

permissions clean (control): ACCEPTED
permissions + near-miss key: REFUSED (1 issue(s))
   code=invalid_union path=["permissions"] :: Invalid input

The fixture is otherwise valid — { services: ['object'], hoooks: ['x'] } — so unrecognized_keys is what should fire. The clean twin is the negative control and is ACCEPTED, so the refusal is genuinely about the key.

An author who transposes hooks as hoooks is told Invalid input at permissions, with no offending key, no surface name, and no rename suggestion.

Why this is a defect and not a preference

Every other closed block on ManifestSchema names the key. Measured in the same run:

block reading
manifest root Unrecognized key(s) on this package manifest: 'namesapce'. Did you mean 'namesapce' → 'namespace'?
contributes Unrecognized key(s) on the 'contributes' block ...: 'kind'. Did you mean 'kind' → 'kinds'?
contributes.kinds[] entry Unrecognized key(s) on a 'contributes.kinds' entry ...: 'descriptio'. Did you mean ... → 'description'?
engines Unrecognized key(s) on the 'engines' block ...: 'protocl'. Did you mean 'protocl' → 'protocol'?
engine Unrecognized key(s) on the legacy 'engine' block ...: 'bogusKey'.
permissions Invalid input

permissions is the one door on this schema where the refusal carries nothing actionable.

Cause

ManifestPermissionsSchema (packages/spec/src/kernel/manifest.zod.ts) is a union:

export const ManifestPermissionsSchema = z.union([
  z.array(z.string()),          // legacy flat list
  PluginPermissionsSchema,      // structured block, .strict()
]);

PluginPermissionsSchema is closed (.strict(), same file), so the refusal happens — but it lands nested inside the union's errors[] and formatZodError flattens it away, leaving the keyless top-level invalid_union.

This is a class, not a one-off

This is the third known door. Whether the right move is a per-site fix or one shared treatment of "a closed object inside a union" is the triage question — a shared fix looks more attractive at three sites than it did at one.

Note on severity

permissions decides which services, hooks, network hosts and filesystem paths a plugin may reach. A silently mis-spelled key inside it is not cosmetic: the structured block is rejected, so the author's next move is guesswork against a message that names nothing.

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