What
ResourceEditPage's edit door hands the client Zod gate a draft that still carries the server's own read-time decoration _diagnostics. Every wired metadata type whose schema is .strict() — 14 of the 15, by the count already recorded in ResourceEditPage.tsx — then reports a body the server accepts as unrecognized_keys.
Found while implementing #6982 (out of that card's scope, which is clientValidation.ts's two named cures). Filed unassigned for PM triage.
The ingress, measured
ResourceEditPage.tsx builds the edit baseline from the layered read and then merges the pending draft over it:
const baseline = (lay.effective ?? lay.code ?? {}) as Record...;
const rawInitial = draftReal ? { ...baseline, ...draftReal } : baseline;
lay.effective is safe: getMetaItemLayered serves three RAW layers and decoration never reaches it (protocol.ts states this in its own words).
draftReal is client.getDraft(...)'s resp.item, taken verbatim by extractDraftBody. That item is decorated — the strict draft branch returns item: decorateMetadataItem(type, ...), and decorateMetadataItem attaches _diagnostics whenever the type has a registered Zod schema.
Nothing on the objectui side strips it: validateMetadataDraft(type, draft, ...) receives draft as-is.
So the misfire needs one condition: the item has a pending draft. Without one, draftReal is null and the baseline alone is clean — which is why this has stayed invisible.
Measured, on the real read path
Driving the real protocol over a real engine (save a sharing_rule, read it back):
GET /meta/sharing_rule/NAME item keys += _diagnostics
GET /meta/sharing_rule/NAME?state=draft item keys += _diagnostics
layered `effective` (raw, undecorated) clean
And through objectui's own gate, on flow (a type that gates BOTH doors today):
flow, clean body ACCEPT
flow, body + _diagnostics REJECT unrecognized_keys: `_diagnostics`
Why the server is the one that is right
The spec names _diagnostics a read decoration on purpose, and says a served body "is therefore NOT a valid input to the schema that produced it until these are removed" (kernel/metadata-read-decorations.ts, METADATA_READ_DECORATIONS = ['_diagnostics', '_draft']). The write path strips them, so the same body PUTs back byte-identical. The envelope keys are deliberately excluded from that list — they are allowlisted by the closed schemas precisely so provenance survives a re-parse. The decorations are not.
The framework has already paid for this class twice: a served _diagnostics 400ing every saved dataset, and the cold-boot flow bind (cloud#971) throwing unrecognized_keys: ["_diagnostics"] for every flow.
Blast radius
Not a Save blocker and nothing is exploitable — schema issues are advisory on this page (ResourceEditPage.schemaAdvisory.test.tsx), and the server has the last word. The cost is a false "this item is invalid" banner plus inline field errors on a legitimate item, on exactly the workflow where an author is mid-edit. _draft: true is the second decoration and reaches the same place through the preview-draft branch.
sharing_rule is accidentally the only type shielded, because AUTHOR_SHAPE_ONLY_TYPES switches its edit gate off for an unrelated reason — see #6982, where that opt-out is kept for exactly this measurement.
Suggested shape, not a decision
Strip the read decorations where the draft is assembled, deriving the key list from the spec's exported METADATA_READ_DECORATIONS rather than restating it — a second hand-maintained copy of that list in objectui is the thing the spec's own header warns about. ⛔ Not by loosening any schema.
Generated by Claude Code
What
ResourceEditPage's edit door hands the client Zod gate a draft that still carries the server's own read-time decoration_diagnostics. Every wired metadata type whose schema is.strict()— 14 of the 15, by the count already recorded inResourceEditPage.tsx— then reports a body the server accepts asunrecognized_keys.Found while implementing #6982 (out of that card's scope, which is
clientValidation.ts's two named cures). Filed unassigned for PM triage.The ingress, measured
ResourceEditPage.tsxbuilds the edit baseline from the layered read and then merges the pending draft over it:lay.effectiveis safe:getMetaItemLayeredserves three RAW layers and decoration never reaches it (protocol.ts states this in its own words).draftRealisclient.getDraft(...)'sresp.item, taken verbatim byextractDraftBody. That item is decorated — the strict draft branch returnsitem: decorateMetadataItem(type, ...), anddecorateMetadataItemattaches_diagnosticswhenever the type has a registered Zod schema.Nothing on the objectui side strips it:
validateMetadataDraft(type, draft, ...)receivesdraftas-is.So the misfire needs one condition: the item has a pending draft. Without one,
draftRealis null and the baseline alone is clean — which is why this has stayed invisible.Measured, on the real read path
Driving the real protocol over a real engine (save a
sharing_rule, read it back):And through objectui's own gate, on
flow(a type that gates BOTH doors today):Why the server is the one that is right
The spec names
_diagnosticsa read decoration on purpose, and says a served body "is therefore NOT a valid input to the schema that produced it until these are removed" (kernel/metadata-read-decorations.ts,METADATA_READ_DECORATIONS = ['_diagnostics', '_draft']). The write path strips them, so the same body PUTs back byte-identical. The envelope keys are deliberately excluded from that list — they are allowlisted by the closed schemas precisely so provenance survives a re-parse. The decorations are not.The framework has already paid for this class twice: a served
_diagnostics400ing every saved dataset, and the cold-boot flow bind (cloud#971) throwingunrecognized_keys: ["_diagnostics"]for every flow.Blast radius
Not a Save blocker and nothing is exploitable — schema issues are advisory on this page (
ResourceEditPage.schemaAdvisory.test.tsx), and the server has the last word. The cost is a false "this item is invalid" banner plus inline field errors on a legitimate item, on exactly the workflow where an author is mid-edit._draft: trueis the second decoration and reaches the same place through the preview-draft branch.sharing_ruleis accidentally the only type shielded, becauseAUTHOR_SHAPE_ONLY_TYPESswitches its edit gate off for an unrelated reason — see #6982, where that opt-out is kept for exactly this measurement.Suggested shape, not a decision
Strip the read decorations where the draft is assembled, deriving the key list from the spec's exported
METADATA_READ_DECORATIONSrather than restating it — a second hand-maintained copy of that list in objectui is the thing the spec's own header warns about. ⛔ Not by loosening any schema.Generated by Claude Code