Skip to content

feat(spec): draft provenance records MCP build-door writes and the OAuth client that made them - #27220

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-e4rirs
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-e4rirs

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #27176

What changes

A pending draft's per-key provenance (draftChanges, served on the strict ?state=draft read) can now say that a key was written by an AI agent outside the console, through a deployment's MCP build door, and which OAuth client that agent was.

Spec (@objectstack/spec/api)

  • MetadataAuthoringSurfaceSchema gains mcp. All the describes that list the set (draftChanges[].by, the save's surface, the docblocks) now name it.
  • New MetadataAuthoringClientSchema ({ id, name? }): the OAuth client the person consented to. id is the verified token's client id (opaque). name is its registered client_name, and it is never the id standing in for a name.
  • draftChanges[] gains an optional client, present only beside by: 'mcp'.
  • SaveMetaItemRequestSchema gains an optional client. It is stated only by the MCP build door, in-process, from the token it verified. No HTTP door reads it.
  • Pinned as isomorphic in type-alias-convention.pin.test.ts (798 → 799). Generated artifacts are regenerated: api-surface, export-origins, declaration-map, authorable-surface and json-schema manifest, reference docs, and the strictness ledger.

Writer (@objectstack/metadata-protocol)

  • A draft save with surface: 'mcp' stamps by: 'mcp' and its client on every key path it changes (draftChangesClient on the repository put, beside draftChangesBy). actor stays the consenting person.
  • A client beside any other surface (or with none), or of the wrong shape, is refused 400 INVALID_REQUEST before anything is read or written. authoringClientOf is the one shape check, shared by the save and the stored read.
  • storedDraftChanges reads mcp entries with or without a client. A stored client beside a non-mcp surface, or a malformed one, is malformed, like any other bad entry.

HTTP doors (@objectstack/rest, runtime dispatcher)

  • ?surface=mcp is refused 400 VALIDATION_ERROR with its own sentence, as ?surface=copilot is. Both doors share metaSaveRequestOptions.

Console

  • @object-ui/data-objectstack: MetadataClientSaveOptions.surface excludes mcp as it excludes copilot. The type now has a @ts-expect-error pin for mcp.
  • Studio properties (StudioDraftMark): the draft chip and the 来源 line mark mcp writes as they mark the copilot's. The line names the client: 「由 MCP 客户端「Claude」在 2 分钟前作为草稿加入」 / "Added as a draft by the MCP client "Claude" 2 min ago". A client with no registered name reads as an unnamed MCP client, never by its id.
    • copilotDraftMarkOf → agentDraftMarkOf. Its marks carry by and client.
    • The source keys take an {agent} hole, with three new engine.studio.draftMark.agent.* rows (en + zh, the designer table's two languages).
  • Changes drawer (DraftChangesPanel): an entry's drill-in already sends the strict draft read. It now reads that read's draftChanges too, and lists which AI tools drafted part of the item: the copilot once, and each MCP client once. Copy is in all ten locale packs (preview.changes.draftedBy*).

Why

cloud#3302 opened the MCP build door: an agent with a metadata:write grant drafts metadata through the same authoring tools the copilot uses. Its writes were recorded as api, so a person reviewing the draft could not tell an agent's edit from any other caller's, or tell which agent it was. The cloud half (the door passing surface: 'mcp' and the verified client from its principal into the save) follows once the pin carries this change.

Verification

  • pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec check:generated --fix: the five stale artifacts were regenerated and re-checked green. check:test-typecheck is OK after updating meta-item-response-shapes.test.ts (the surface set and the client type are pinned; the client shape is refused at draftChanges[n].client).
  • spec: meta-item-response-shapes.test.ts and type-alias-convention.pin.test.ts pass (26).
  • metadata-protocol: tsc --noEmit is clean. draft-changes.test.ts and protocol.packageless-save-address.test.ts pass (103). The new cases cover the client stamp and keep rule, the unnamed client, an mcp save without a client, and refusal of a client beside another surface or of the wrong shape, with nothing written.
  • rest: rest-server-meta-door-closed-sets.test.ts passes (29), with ?surface=mcp refused. runtime: meta-save-preconditions-parity.test.ts passes (23), with ?surface=mcp refused on the dispatcher too.
  • ui:
    • tsc --noEmit is clean for app-shell and data-objectstack.
    • StudioDraftMark.test.ts, the new DraftChangesPanel.agentSource-27176.test.tsx, and draftChanges-12725.test.ts pass.
    • Every DraftChangesPanel.* suite and every copilot-mark suite (52 files) passes.
    • check:i18n-keys, check:i18n-drift, check:i18n-designer-parity and check:i18n-dead-keys show nothing for the new keys.
  • pnpm lint on the touched files: 0 errors.

Clause-②: yes (widening)
Metadata-protocol: added

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…provenance, and render them in Studio

Claude-Session: https://claude.ai/code/session_01QUs3BthV9Hg8gmo8r35XcV
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/rest, @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/api/metadata-api.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/automation/webhooks.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/concepts/metadata-lifecycle.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/data-modeling/import-mappings.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/deployment/validating-metadata.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/cluster.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/authentication.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/permissions/authorization.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/sso.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/ui/forms.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/releases/v17/17-6.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/releases/v18/18-0.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/releases/v18/18-1.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))
  • content/docs/releases/v18/18-2.mdx (via INVALID_REQUEST (literal, a string literal in saveMetaItem))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/declaration-map/api.json, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 56 of 208 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 152 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json cdb949a288e1bd3350f09527c5f2169eb4752c23 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from aa497a2e3109443cfe0a44332bf2d34e659089c6 — the merge of head 532985f37f9aa4460526df86699de0a4f7cbde91 into base cdb949a288e1bd3350f09527c5f2169eb4752c23, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin aa497a2e3109443cfe0a44332bf2d34e659089c6 && git checkout aa497a2e3109443cfe0a44332bf2d34e659089c6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cdb949a288e1bd3350f09527c5f2169eb4752c23 532985f37f9aa4460526df86699de0a4f7cbde91 && git checkout -B drift-repro cdb949a288e1bd3350f09527c5f2169eb4752c23 && git merge --no-ff 532985f37f9aa4460526df86699de0a4f7cbde91

node scripts/docs-audit/affected-docs.mjs --json cdb949a288e1bd3350f09527c5f2169eb4752c23

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs cdb949a288e1bd3350f09527c5f2169eb4752c23 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6ef6a96021bda9ee4776a897e03c2528392afb20
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/metadata-protocol/src/draft-changes.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/sys-metadata-repository.ts, packages/rest/src/meta-save-request.ts, packages/rest/src/meta-item-write-routes.ts, packages/runtime/src/domains/meta.ts, packages/spec/src/kernel/execution-context.zod.ts, ui/packages/data-objectstack/src/metadata-client.ts, ui/packages/app-shell/src/views/studio-design/StudioDraftMark.tsx

① Derived judgments

Check-runs on the head at read time (2026-10-28): 46 runs; Governed Surface Queue Guard, PR-body guards, Spec property liveness, Check Documentation Links, Flag docs affected, Check PR Size, Auto Label, Dogfood Regression Gate, Plan Test Core shards are success; Test Core (1-8/8), Build Core, Build UI Deps, Build Docs, Check Changeset, every Lint · lane (ESLint, docs & code guards, repo & tooling gates, PM dispatch-gates, CI/release/engine gates, three scoped lanes) and every Type Check · lane (workspace, debt ledger, source gates, consumer gates) are still in progress, so the derived gate families below (generated artifacts, strictness ledger, type-alias pin count, UI type check, changeset level) have no conclusion yet. The judgments here are read off the tree; the in-progress runs are the gate verdicts once they conclude.

  • Public surface, @objectstack/spec/api — MetadataAuthoringSurfaceSchema widens ['copilot','studio','api'] to ['copilot','mcp','studio','api']; new export MetadataAuthoringClientSchema / MetadataAuthoringClient ({ id: string(min 1), name?: string(min 1) }); GetMetaItemResponseSchema.draftChanges[].client optional; SaveMetaItemRequestSchema.client optional. Every consumer of MetadataAuthoringSurface is a z.infer type (DraftChange['by'], MetaSaveRequestMembers, MetadataClientSaveOptions), so a widening that adds one enum member is additive and non-breaking. Right. The index re-exports ./protocol.zod wholesale, so the new schema is published without an index edit. Right.
  • Accept set, in-process save — saveMetaItem now accepts surface: 'mcp' (through MetadataAuthoringSurfaceSchema.safeParse, so the spec is the source) and client only beside surface: 'mcp', refused 400 INVALID_REQUEST otherwise or malformed, before any read or write (protocol.ts ~20245, tested with metadataWrites() empty). Right.
  • Accept set, HTTP doors — metaSaveRequestOptions (packages/rest/src/meta-save-request.ts) refuses ?surface=mcp with its own sentence; HTTP_SAVE_SURFACES stays ['studio','api']. Both doors spread ...saveOptions.request, whose type MetaSaveRequestMembers is a Pick of exactly parentVersion, mode and surface, into a request built member by member (meta-item-write-routes.ts ~283-345; runtime/src/domains/meta.ts ~1599-1612), so no wire client can reach the save. The describe's "no HTTP door reads it" claim holds in the tree. Right. The narrowed set the UI client may send (MetadataClientSaveOptions.surface, the spec's by set with copilot and mcp excluded) tracks the doors. Right.
  • Stored form and read — mergeDraftChanges stamps client only when the stamp has one; storedDraftChanges admits mcp entries with or without a client and treats a client beside a non-mcp surface, or a malformed one, as malformed (whole draftChanges dropped, as for any bad entry today). The draft read serves the entries unchanged (protocol.ts ~10999 / ~11034), so a stored client reaches GetMetaItemResponseSchema and parses (pinned end-to-end in protocol.packageless-save-address.test.ts). Right.
  • Generated artifacts — api-surface, export-origins, declaration-map, authorable-surface (MetadataAuthoringClient:id/name, SaveMetaItemRequest:client), json-schema.manifest, content/docs/references/api/protocol.mdx, references index and the untriaged api/ strictness count 448 → 449 are all regenerated in the diff. The type-alias pin count 798 → 799 adds Iso_api_protocol__MetadataAuthoringClientSchema (no default, no transform; z.input equals z.infer). Right, pending the in-progress Lint/Type Check lanes.
  • ADR alignment — ADR-0033's provenance item ("attribute each AI draft to the acting user + mark it AI-sourced") is carried at the key grain with actor unchanged and an added attribution member; ADR-0090 D10 rule 4 / ADR-0118 D1-D5 forbid moving actor and answer "which non-user acted" with an added field, which client is. No ADR is contradicted; none needs amending.

② Semver level

Changeset .changeset/27176-mcp-draft-provenance.md: @objectstack/spec minor, @objectstack/metadata-protocol minor, @objectstack/rest patch, @object-ui/data-objectstack / app-shell / i18n patch. Body carries Clause-②: yes (widening) and Metadata-protocol: added; the PR body carries the same two lines. A new accepted enum member on a published authoring schema is added, which owes at least minor on at least one moved package (check-changeset-no-major.mjs, ruling A) — spec and metadata-protocol are the packages whose published source the kind moves, both minor; the HTTP doors and the console only track the set, patch. Nothing is removed or renamed, so no BREAKING banner, no ADR-0087 disposition, no tombstone. Level and both declaration lines match what the diff publishes. Check Changeset is in progress; its conclusion is the gate verdict.

③ Boundary flags

  • No open_questions and no dev flag in the PR body or on the card.
  • The cloud half (the MCP build door passing surface: 'mcp' and the verified client from its principal into the save) is declared as a follow-up on cloud#3302, which the card's "Done looks like" item 3 already scopes that way. Read in the cloud checkout: the door's tool context carries authoringDoor: 'mcp' and the execution context's performedBy.clientId (assemble-execution-context.ts:352); a registered display name must be read off sys_oauth_application by that door — the spec's name is "as registered, absent when none", which that producer can honour. Not a flag on this PR.
  • The @ts-expect-error pin for mcp sits in ui/packages/data-objectstack/src/draftChanges-12725.test.ts beside the existing copilot pin (same file, same compile program), so it is not a phantom check by the AGENTS.md test. UI remainder is the lane seat's ACCEPT at TIER_DEFAULT; Type Check · consumer gates answers it.
  • Docblocks cite [#27176] the way the same file cites [#24298]; the describe() strings (runtime prose) carry no tracker number.

④ Design judgment

packages/spec/src/api/protocol.zod.ts, judged against the card (attribution only: which door, which OAuth client, on whose behalf), ADR-0033 / ADR-0090 D10 rule 4, and its first consumers (the writer in @objectstack/metadata-protocol, the two HTTP doors, StudioDraftMark / DraftChangesPanel, and the cloud MCP door to come).

Holds:

  • Separating by: 'mcp' (the door) from client (the agent) from actor (the person) answers all three of the card's questions without a second actor vocabulary; two clients of one person on one draft are told apart by client.id, which is what the card asked for.
  • client is server-stated like writeFace, and the tree keeps it so: the only two HTTP producers of SaveMetaItemRequest build it from a Pick that has no client; ?surface=mcp is refused on both doors with the same mapping. An HTTP caller cannot label its write as an agent's, nor attribute it to another client.
  • "Present only beside by: 'mcp'" is enforced where it can be: refused at the save and dropped as malformed at the stored read; the schema's prose matches the enforcement (Prime Directive chore: version packages #10).
  • name optional with min(1) and "never the id standing in for it" is the right shape for Studio's line: draftAgentText renders the registered name or "an unnamed MCP client", never client.id (read in StudioDraftMark.tsx:84-90).
  • z.input equals z.infer, so the new type carries no …Parsed twin; the pin count moves by one.

Tried to break it:

  • A client on an active (publish) mcp save: accepted and recorded nowhere — stated in the describe, same posture as surface on an active save. Holds.
  • A stored mcp entry whose client was later hand-edited beside studio: the read drops the whole draftChanges as malformed. Same as every other bad entry; holds.
  • A wire body carrying client with ?surface=studio: item is the body, the request is built member by member, client never read. Holds.

Should-fix (not blocking): the spec and the writer disagree on an unknown member. MetadataAuthoringClientSchema is a plain z.object, so MetadataAuthoringClientSchema.safeParse({ id: 'cli_1', secret: 's' }) succeeds and strips secret; authoringClientOf refuses the same value and the save answers 400 INVALID_REQUEST ("malformed"). The surface member is validated through the spec schema itself; the client member is validated by a hand-rolled check that is stricter than the published contract, which is the drift Prime Directive #12 warns against (one contract, owned at the spec). Either mark the schema .strict() (so the spec's own refusal is the one the writer applies, and the strictness ledger counts it as such) or have authoringClientOf parse through the spec schema; the test rows an extra member / { id, secret } then pin the spec's verdict rather than a second one. Nit in the same file: MetadataAuthoringClientSchema's own .describe says "(see MetadataAuthoringClientSchema)", a self-reference.

No must-fix.

Implemented-by: claude/project-thread-e4rirs
Reviewed-by: session_0123kRqqdpAJWYMgsgdAJX4G

VERDICT: PASS

…lient by it

The writer's authoringClientOf refused an unknown member that the published
schema stripped; the schema is now .strict() and the writer parses through it,
so the contract and the enforcement give one verdict. Its describe no longer
names itself.

Claude-Session: https://claude.ai/code/session_01QUs3BthV9Hg8gmo8r35XcV
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2b1028a55ae009cc51266f0696e29088d215d458
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/metadata-protocol/src/draft-changes.ts, packages/metadata-protocol/src/protocol.ts, packages/spec/src/api/meta-item-response-shapes.test.ts, content/docs/references/api/protocol.mdx, packages/spec/json-schema.manifest/api.json, docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md

Delta review. This head is 6ef6a96021bda9ee4776a897e03c2528392afb20 plus one commit, 2b1028a55a ("fix(spec): close MetadataAuthoringClientSchema and judge the save's client by it"), which touches four files. The 6ef6a96021 record (PASS, one should-fix, one nit) stands for everything the delta does not touch; the sections below judge the delta and restate only what it changes.

① Derived judgments

Check-runs on 2b1028a55a at read time: 23 runs; 4 success, 3 skipped, and 16 still in progress — every Lint · lane, every Type Check · lane, Check Changeset, Spec property liveness, Flag docs affected, Auto Label. The Test Core shards and the Build jobs had not yet been planned on this head. The judgments below are read off the tree; the in-progress runs are the gate verdicts once they conclude.

  • Public surface — MetadataAuthoringClientSchema gains .strict(); its .describe is rewritten. The members (id min 1, name optional min 1) and the exported type are unchanged, and z.input still equals z.infer (strictness adds no default or transform), so the Iso_api_protocol__MetadataAuthoringClientSchema pin and the 799 count stand. The only observable narrowing is on an unknown member of client: stripped at 6ef6a96021, refused now. That member was never published as accepted, no consumer exists yet (the cloud door is a follow-up), and the in-process writer already refused it — so the published-surface effect is to make the spec say what the writer did. Right.
  • Writer — authoringClientOf now delegates to MetadataAuthoringClientSchema.safeParse and returns parsed.data, so the save door (protocol.ts ~20245) and the stored read (storedDraftChanges) judge a client by the spec's own verdict, one source. safeParse of a strict object returns the declared members only, so the stored form is unchanged. isPlainObject keeps its two other callers. The 6ef6a96021 writer tests ({ id, secret } refused, metadataWrites() empty) are untouched and still describe the behaviour. Right.
  • Generated artifacts — content/docs/references/api/protocol.mdx carries the new describe. json-schema.manifest/api.json is a name list (api/MetadataAuthoringClient, unchanged) and the strictness count for api/ is an object-site total that a posture change does not move (strictness-ledger.ts: sites are counted, posture is classified), so neither needed a regen in this commit. api-surface / export-origins / declaration-map / authorable-surface carry no posture and are unchanged. Right, pending the in-progress Lint lanes (check:generated, check:strictness-ledger).
  • No accept-set change on the HTTP doors, no new export, no ADR touched.

② Semver level

Unchanged from 6ef6a96021: spec minor, metadata-protocol minor, rest and console packages patch; Clause-②: yes (widening) / Metadata-protocol: added on both PR body and changeset. The delta refuses an unknown member on a schema that first ships in this same release, so nothing published on main is narrowed and no arm or banner changes. Matches. Check Changeset is in progress.

③ Boundary flags

  • No open_questions and no dev flag in the delta or on the card.
  • .strict() is the spelling this file already uses (two sites at the head); no gate prefers the campaign's strictObject( helper, and the api/ ledger is untriaged, so no verdict row is owed.

④ Design judgment

packages/spec/src/api/protocol.zod.ts, judged against the two findings of the 6ef6a96021 record and the writer it cites.

  • Should-fix closed. The spec and the writer now give one verdict on an unknown member: the schema is .strict() ("refused, never stripped" in its describe), and authoringClientOf parses through it instead of holding a second shape check. meta-item-response-shapes.test.ts pins { id: 'cli_1', secret: 's' } refused at draftChanges[0].client, so the spec's own test now carries the row the writer test carried alone. Contract-first holds (Prime Directive Add comprehensive test suite for Zod schema validation #12: the contract owned at the spec, the consumer applying it).
  • Nit closed. The describe no longer names its own schema; it now says what the client is (the consented client, as the door verified it from the token) and that it is closed.
  • Tried to break it: a stored mcp entry whose client carries an extra member (hand-edited row) is still dropped as malformed by storedDraftChanges, now by the spec's verdict; a served draft read therefore never carries a client the schema would refuse, so any response-side parse against GetMetaItemResponseSchema keeps passing. An in-process caller that passes a client with a stray member is refused 400 INVALID_REQUEST exactly as before. Nothing new on the spec face.

No must-fix, no should-fix.

Implemented-by: claude/project-thread-e4rirs
Reviewed-by: session_0123kRqqdpAJWYMgsgdAJX4G

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Seat review (repo:cloud#2, routed domain:cloud): ACCEPT (head 2b1028a55a, 2026-10-28T11:19Z)

  • Contract review: PASS at CONTRACT_REVIEW_TIER on 6ef6a96021. The delta to 2b1028a55a closes the should-fix (MetadataAuthoringClientSchema is now .strict(), and the writer parses through it) and the nit, also PASS. Both records are on this PR. needs:contract-review is removed.
  • Rest of the diff at TIER_DEFAULT:
    • metadata-protocol: the draft save records surface: 'mcp' plus client, accepted only from an in-process caller with surface mcp. Anything else is refused 400 before any write.
    • rest and runtime: ?surface=mcp is refused on both HTTP doors through the shared closed set.
    • ui: StudioDraftMark and DraftChangesPanel render the surface and client, in all 10 locales for the drawer and en/zh for the designer table. The new tests pin both.
  • Governed: no. 870 lines.
  • Declarations: Clause-②: yes (widening), Metadata-protocol: added, spec minor. They match the diff.

Landing via queue once every check on this head is green, not just the required subset. The cloud half (the MCP door passes surface: 'mcp' and the client into the save) follows the pin round that carries this. cloud#3302 stays open until then.

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Seat 2 review: ACCEPT carried to head 532985f (2026-10-28T12:00Z)

The only change since the ACCEPT at 2b1028a55a is test-only. It moves the two de-quote-pairing-3876.test.ts pins from 97 to 98 for the new correctly paired preview.changes.draftedByMcpClient string, and the de string itself is unchanged. No packages/spec/src/** file changed, so contract review record2 (delta PASS) still covers the spec diff. Every check on 532985f is green or skipped.

Landing via queue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 12:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 12:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 8d79bbd Oct 28, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-e4rirs branch October 28, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants