spec(envelope): require status on every task response envelope (closes #4832)#4876
Conversation
The protocol envelope (core/protocol-envelope.json) declares `status` as required, formalizing the wire contract the docs and conformance storyboards already assume. Every task response — including synchronous read-only metadata calls like get_adcp_capabilities — MUST carry a top-level `status` field. Synchronous calls emit `status: "completed"`; async calls emit `submitted`, `working`, `input-required`, etc. per the task-status enum. Why this is a clarification, not a new requirement: sdk-stack.mdx, mcp-response-extraction.mdx, webhooks.mdx, and error-handling.mdx already treat envelope `status` as the canonical protocol-layer field. The v3_envelope_integrity storyboard already asserts presence via envelope_field_present. Only the schema left `status` declared but not required on the envelope, which let SDKs ship without emitting it on some sync responses. Resolves #4832 (adopter on @adcp/sdk@7.7.0 hit the storyboard failure because the auto-registered get_adcp_capabilities handler in adcp-client builds the response without `status`). Follow-ups (filed separately): - adcp-client SDK: emit `status: "completed"` on auto-registered get_adcp_capabilities handler - 3.1-cycle schema fold: extend the 64 task response schemas to $ref protocol-envelope.json in addition to version-envelope.json, so per-task response_schema validators catch envelope omissions directly
There was a problem hiding this comment.
Direction is right. One line-level contradiction blocks merge.
The schema now says two things at once. Line 60-62 requires status. Line 181 still reads:
"
statusandpayloadare intentionally NOT required on this schema.statusmay be carried via transport-native state on A2A (task.status.state);payloadis a documentary grouping construct, never a required wire field."
That note isn't stale prose — it is the carve-out that explained how the envelope contract reconciles with A2A's transport-native state carrier. Per docs/building/by-layer/L0/a2a-response-extraction.mdx:30-43, A2A receivers read status.state from the task object, not a body-level status key. After this PR, an A2A sender reading the schema cannot tell whether they must now emit body-level status in the DataPart or whether task.status.state satisfies required: ["status"]. The carve-out got deleted by silence.
What to fix in this PR
Rewrite line 181 to (a) drop the "status … intentionally NOT required" claim and (b) state the A2A reconciliation explicitly — on A2A, required: ["status"] is satisfied by task.status.state carrying the value, even when the body DataPart omits the key. On MCP/REST it is satisfied by a root-level status field. Mirror the existing Transport serialization framing at lines 176-180.
The rest of the PR is clean — this is a one-line fix.
Things I checked
- Schema structure (lines 60-62):
requiredlands as a top-level sibling ofproperties/additionalProperties/not/examples. Draft-07 compliant. not.anyOf(lines 64-69): orthogonal to the newrequired— different keys, different predicates. No interaction under draft-07.- All five
examples[](lines 75, 99, 121, 145, 158) already carrystatus; they validate against the tightened schema. - Cascade: confirmed the PR's claim that no other schema
$refsprotocol-envelope.json. Onlystatic/schemas/source/index.json:257-258(registry entry) andversion-envelope.json:5(prose pointer). The newrequiredis normative-only until the per-task fold lands. - Changeset front-matter (
adcontextprotocol: minor) is valid; body matches the diff.minoris defensible — consistent with.changeset/2911-mcp-envelope-flat-serialization.mdprecedent for the same envelope. - No drift against
sdk-stack.mdx:114,mcp-response-extraction.mdx:77-87,webhooks.mdx, orerror-handling.mdx. The drift is internal to this one file.
Follow-ups (non-blocking — file as issues)
- No negative test added. PR description claims
npm run test:schemas8/8 passing, but the diff adds zero test files. A constraint-coverage case (envelope withoutstatus→ expected schema rejection) would close the loop and prevent future regressions when the per-task fold lands. - Per-task schema fold (PR's own follow-up #1) — until the 64 task response schemas
$refprotocol-envelope.json, thisrequiredis documentary. Worth landing before 3.1 GA so per-task validators actually catch envelope omissions instead of relying on the storyboard. - SDK companion in
adcp-clientfor the auto-registeredget_adcp_capabilitieshandler — sibling-repo PR per the description.
Minor nits (non-blocking)
- Changeset adopter specificity.
.changeset/4832-envelope-status-required.md:13names@adcp/sdk@7.7.0and "production seller." Public changeset bodies usually anonymize the adopter SDK version. Fine to leave; flagging for the next one.
Request changes pending the line 181 reconciliation. Everything else is the right shape.
The prior note 'status and payload are intentionally NOT required on this schema' directly contradicts the new required: [status] this PR introduces. Rewrite to clarify that status is required on the conceptual envelope across all transports, and that on A2A the wire carrier is task.status.state (mapped 1:1 to the in-memory envelope status via the canonical extraction algorithm). payload remains non-required. Catches the self-contradiction flagged by ad-tech-protocol-expert review.
There was a problem hiding this comment.
Sound clarification. Closes the schema-vs-storyboard gap where v3_envelope_integrity/no_legacy_status_fields asserted envelope_field_present: status but the schema let permissive producers slip through.
Things I checked
- Cascade scope. Only
static/schemas/source/index.json:258$refsprotocol-envelope.json, and it's a catalog entry, not anallOfcomposition. Per-task response schemas (create-media-buy-response.jsonet al.)$refversion-envelope.jsononly. Newrequired: ["status"]does not silently propagate into per-task validators. The per-task fold is correctly deferred in the changeset. - Examples still validate. All 5 examples at
protocol-envelope.json:75 / :99 / :121 / :145 / :158carrystatus(completed / submitted / input-required / replayed-completed / failed). Examples-against-schema stays green. - Internal coherence. Four prose touchpoints describe one contract without contradiction: top-level description (
:5),statusproperty description (:23), the newrequiredarray (:60-62), and the notes paragraph at:181that explicitly bridges A2A — schema-levelrequired: [status]enforces the post-extraction in-memory shape, transport-nativetask.status.statesatisfies it on the A2A wire. - task-status enum coverage.
static/schemas/source/enums/task-status.jsoncarriescompleted, so sync metadata calls have a valid value to emit. - Legacy-fields constraint untouched.
not.anyOfrejectingtask_status/response_statusat:64-69still does its job. - Storyboard alignment.
static/compliance/source/universal/v3-envelope-integrity.yamlalready requiredstatusviaenvelope_field_present— this PR is the schema catching up to the storyboard, not the other way around. - Changeset.
minoris the right call within the unreleased 3.1 window. Would bemajorif 3.1 were GA. Within-minor amendment of #2911 is fine; calling itmajorwould imply the post-#2911 permissive state was the intended contract, which the docs and storyboard contradict.
Follow-ups (non-blocking — file as issues)
- Release-notes drift.
docs/reference/release-notes.mdx:81and:108still describe the post-#2911 state ("schema dropsrequired: [status, payload]") and the adopter matrix tells strict-validator consumersstatuswill flip required→optional. After this lands, those lines actively misinform. Must update before 3.1 GA.docs/reference/whats-new-in-3-1.mdx:197was already half-fixed (sayspayload.requiredonly) but the line still treats #2911 in isolation — worth a hand-edit pass at the same time. - A2A raw-payload validators. Worth adding one clause to the
notesarray making explicit that raw A2Ataskpayloads validated directly againstprotocol-envelope.jsonare out of scope — the schema enforces the post-extraction in-memory envelope, not the A2A wire shape. The reasoning is already present at:181; just hoist the conclusion. Closes the only ambiguity an adopter running raw-task validation against the envelope schema might hit. - SDK companion (already in changeset):
@adcp/client's auto-registeredget_adcp_capabilitieshandler needs to emitstatus: "completed"to close kapoost's storyboard failure end-to-end. - Per-task schema fold (already in changeset): folding the 64 task response schemas to
$refprotocol-envelope.jsonin addition toversion-envelope.jsonis the right long-term shape — lets per-taskresponse_schemavalidators catch envelope omissions directly.
Minor nits (non-blocking)
- Top-level description hedge.
protocol-envelope.json:5reads "Agents shipping responses without a top-levelstatusare non-conformant regardless of whether the task body schema would otherwise validate." For A2A the wire-native carrier istask.status.state, not a literal top-level key.notes[5]at:181resolves it, but a one-clause hedge in the top-level description ("...top-levelstatus— or its transport-native equivalent per Transport serialization below...") would remove the apparent tension on first read.
LGTM. Follow-ups noted below.
status on check_governance and report_plan_outcome (#4897)
#4902
…report_plan_outcome (#4897) (#4902) Frees the top-level `status` key for envelope task-status (TaskStatus) under MCP flat-on-the-wire serialization. The body-level governance field and the envelope share a root key but carry different enums; whichever wins on the wire, the other is silently destroyed and no validator catches it. WG-recommended Option A per the issue triage (both ad-tech-protocol-expert and adtech-product-expert): - check-governance-response.json: status → verdict (enum unchanged: approved | denied | conditions). Updates the three if/then discriminator blocks, the required[], and prose threads on findings/conditions/expires_at. - report-plan-outcome-response.json: status → outcome_state (enum unchanged: accepted | findings). Updates required[] and the findings prose thread. - get-plan-audit-logs-response.json: entries[].status → entries[].verdict (cascade for vocabulary consistency with check-governance-response). Other status fields on the same response (plans[].status, governed_actions[].status) are lifecycle states, not verdicts — left unchanged. Docs swept (~25 example bodies + table descriptions + prose): - docs/governance/overview.mdx - docs/governance/campaign/tasks/{check_governance,report_plan_outcome,get_plan_audit_logs}.mdx - docs/governance/campaign/{audit-trail,specification}.mdx - docs/reference/whats-new-in-3-1.mdx (migration note in Final-spec clarifications batch) Storyboards swept — the triage scoped this as "no yaml renames needed" but during implementation found four storyboards with field_present / field_value assertions keyed on path: "status" against check-governance-response. Updated to path: "verdict": - specialisms/governance-spend-authority/{index,denied}.yaml - specialisms/governance-delivery-monitor/index.yaml - protocols/governance/index.yaml (plus a stale expected block referencing status: recorded — not in the enum — corrected to outcome_state: accepted) Test fixture: composed-schema-validation report_plan_outcome case updated to use outcome_state. Adopter impact: wire-shape change on three experimental governance schemas (x-status: experimental). Buyers and sellers rename one property name per emitter / consumer; enum values unchanged. SDK regen required. Related: - #4876 — envelope status REQUIRED (beta.2) - #4895 — companion media-buy collision (separate PR) - #4896 — per-task envelope fold; carve-outs for these two schemas can be removed once this lands Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pdate success responses (#4895) Under MCP flat-on-the-wire serialization, the envelope task-status (`status`, TaskStatus) and the body-level media-buy lifecycle status (`status`, MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and `UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope stamps a TaskStatus at the same path. No validator catches it. WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal). **Strictly additive — no schema is renamed and no `required[]` constraint changes.** - create-media-buy-response.json `CreateMediaBuySuccess`: adds `media_buy_status: $ref media-buy-status.json` alongside the existing `status`. The legacy `status` is marked `deprecated: true` and slated for removal in 4.0. Neither field is in `required[]` (both optional in 3.1). `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }` is the TaskStatus discriminator. - update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric. Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`, `get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and `core/media-buy.json` `status` are NOT renamed. These fields live nested at depth ≥ 1 inside arrays, so the envelope `status` at the response root does not collide with them on the wire. Renaming them would require either a breaking `required[]` swap or a double-field transition for no wire-collision payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns `media_buy_status` at root, the next returns `status` inside an array) is the price of keeping this strictly additive. Resolve in 4.0 alongside the legacy-`status` removal, when a clean cascade rename is on the table. `cancel_media_buy` is performed via update_media_buy with cancel intent — there is no dedicated cancel tool. Inherits the rename from UpdateMediaBuySuccess; no separate handling. Storyboards swept: - protocols/media-buy/state-machine.yaml — three field_present path:"status" assertions on update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two field_value assertions checking MediaBuyStatus values on create-media-buy-response.json / update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/create_media_buy_async.yaml — left as path:"status" (this checks the submitted-arm TaskStatus discriminator). Docs: - task-reference/update_media_buy.mdx — cancellation success example shows the canonical media_buy_status form. - reference/whats-new-in-3-1.mdx — migration note in Final-spec clarifications batch. Adopter impact: - Sellers (3.1+) SHOULD emit media_buy_status on create/update success; MAY continue emitting the deprecated legacy `status` during the window. - Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to legacy `status` for compatibility. - 3.0 sellers and buyers continue to work unchanged. No required-field swap, no rename, no breakage. - 4.0: deprecated top-level `status` removed from create/update success branches. The nested `status` fields on get-media-buys-response items, get-media-buy-delivery-response items, and core/media-buy.json should be addressed in the same 4.0 release as a coherent cascade. - SDK regen required for @adcp/client, adcp-go, Python. Related: - #4876 — envelope status REQUIRED (beta.2) - #4897 — companion governance schema rename (separate PR #4902) - adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pdate success responses (#4895) Under MCP flat-on-the-wire serialization, the envelope task-status (`status`, TaskStatus) and the body-level media-buy lifecycle status (`status`, MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and `UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope stamps a TaskStatus at the same path. No validator catches it. WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal). **Strictly additive — no schema is renamed and no `required[]` constraint changes.** - create-media-buy-response.json `CreateMediaBuySuccess`: adds `media_buy_status: $ref media-buy-status.json` alongside the existing `status`. The legacy `status` is marked `deprecated: true` and slated for removal in 4.0. Neither field is in `required[]` (both optional in 3.1). `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }` is the TaskStatus discriminator. - update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric. Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`, `get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and `core/media-buy.json` `status` are NOT renamed. These fields live nested at depth ≥ 1 inside arrays, so the envelope `status` at the response root does not collide with them on the wire. Renaming them would require either a breaking `required[]` swap or a double-field transition for no wire-collision payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns `media_buy_status` at root, the next returns `status` inside an array) is the price of keeping this strictly additive. Resolve in 4.0 alongside the legacy-`status` removal, when a clean cascade rename is on the table. `cancel_media_buy` is performed via update_media_buy with cancel intent — there is no dedicated cancel tool. Inherits the rename from UpdateMediaBuySuccess; no separate handling. Storyboards swept: - protocols/media-buy/state-machine.yaml — three field_present path:"status" assertions on update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two field_value assertions checking MediaBuyStatus values on create-media-buy-response.json / update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/create_media_buy_async.yaml — left as path:"status" (this checks the submitted-arm TaskStatus discriminator). Docs: - task-reference/update_media_buy.mdx — cancellation success example shows the canonical media_buy_status form. - reference/whats-new-in-3-1.mdx — migration note in Final-spec clarifications batch. Adopter impact: - Sellers (3.1+) SHOULD emit media_buy_status on create/update success; MAY continue emitting the deprecated legacy `status` during the window. - Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to legacy `status` for compatibility. - 3.0 sellers and buyers continue to work unchanged. No required-field swap, no rename, no breakage. - 4.0: deprecated top-level `status` removed from create/update success branches. The nested `status` fields on get-media-buys-response items, get-media-buy-delivery-response items, and core/media-buy.json should be addressed in the same 4.0 release as a coherent cascade. - SDK regen required for @adcp/client, adcp-go, Python. Related: - #4876 — envelope status REQUIRED (beta.2) - #4897 — companion governance schema rename (separate PR #4902) - adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pdate success responses (#4895) Under MCP flat-on-the-wire serialization, the envelope task-status (`status`, TaskStatus) and the body-level media-buy lifecycle status (`status`, MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and `UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope stamps a TaskStatus at the same path. No validator catches it. WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal). **Strictly additive — no schema is renamed and no `required[]` constraint changes.** - create-media-buy-response.json `CreateMediaBuySuccess`: adds `media_buy_status: $ref media-buy-status.json` alongside the existing `status`. The legacy `status` is marked `deprecated: true` and slated for removal in 4.0. Neither field is in `required[]` (both optional in 3.1). `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }` is the TaskStatus discriminator. - update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric. Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`, `get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and `core/media-buy.json` `status` are NOT renamed. These fields live nested at depth ≥ 1 inside arrays, so the envelope `status` at the response root does not collide with them on the wire. Renaming them would require either a breaking `required[]` swap or a double-field transition for no wire-collision payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns `media_buy_status` at root, the next returns `status` inside an array) is the price of keeping this strictly additive. Resolve in 4.0 alongside the legacy-`status` removal, when a clean cascade rename is on the table. `cancel_media_buy` is performed via update_media_buy with cancel intent — there is no dedicated cancel tool. Inherits the rename from UpdateMediaBuySuccess; no separate handling. Storyboards swept: - protocols/media-buy/state-machine.yaml — three field_present path:"status" assertions on update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two field_value assertions checking MediaBuyStatus values on create-media-buy-response.json / update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/create_media_buy_async.yaml — left as path:"status" (this checks the submitted-arm TaskStatus discriminator). Docs: - task-reference/update_media_buy.mdx — cancellation success example shows the canonical media_buy_status form. - reference/whats-new-in-3-1.mdx — migration note in Final-spec clarifications batch. Adopter impact: - Sellers (3.1+) SHOULD emit media_buy_status on create/update success; MAY continue emitting the deprecated legacy `status` during the window. - Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to legacy `status` for compatibility. - 3.0 sellers and buyers continue to work unchanged. No required-field swap, no rename, no breakage. - 4.0: deprecated top-level `status` removed from create/update success branches. The nested `status` fields on get-media-buys-response items, get-media-buy-delivery-response items, and core/media-buy.json should be addressed in the same 4.0 release as a coherent cascade. - SDK regen required for @adcp/client, adcp-go, Python. Related: - #4876 — envelope status REQUIRED (beta.2) - #4897 — companion governance schema rename (separate PR #4902) - adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pdate success responses (#4895) Under MCP flat-on-the-wire serialization, the envelope task-status (`status`, TaskStatus) and the body-level media-buy lifecycle status (`status`, MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and `UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope stamps a TaskStatus at the same path. No validator catches it. WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal). **Strictly additive — no schema is renamed and no `required[]` constraint changes.** - create-media-buy-response.json `CreateMediaBuySuccess`: adds `media_buy_status: $ref media-buy-status.json` alongside the existing `status`. The legacy `status` is marked `deprecated: true` and slated for removal in 4.0. Neither field is in `required[]` (both optional in 3.1). `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }` is the TaskStatus discriminator. - update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric. Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`, `get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and `core/media-buy.json` `status` are NOT renamed. These fields live nested at depth ≥ 1 inside arrays, so the envelope `status` at the response root does not collide with them on the wire. Renaming them would require either a breaking `required[]` swap or a double-field transition for no wire-collision payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns `media_buy_status` at root, the next returns `status` inside an array) is the price of keeping this strictly additive. Resolve in 4.0 alongside the legacy-`status` removal, when a clean cascade rename is on the table. `cancel_media_buy` is performed via update_media_buy with cancel intent — there is no dedicated cancel tool. Inherits the rename from UpdateMediaBuySuccess; no separate handling. Storyboards swept: - protocols/media-buy/state-machine.yaml — three field_present path:"status" assertions on update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two field_value assertions checking MediaBuyStatus values on create-media-buy-response.json / update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/create_media_buy_async.yaml — left as path:"status" (this checks the submitted-arm TaskStatus discriminator). Docs: - task-reference/update_media_buy.mdx — cancellation success example shows the canonical media_buy_status form. - reference/whats-new-in-3-1.mdx — migration note in Final-spec clarifications batch. Adopter impact: - Sellers (3.1+) SHOULD emit media_buy_status on create/update success; MAY continue emitting the deprecated legacy `status` during the window. - Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to legacy `status` for compatibility. - 3.0 sellers and buyers continue to work unchanged. No required-field swap, no rename, no breakage. - 4.0: deprecated top-level `status` removed from create/update success branches. The nested `status` fields on get-media-buys-response items, get-media-buy-delivery-response items, and core/media-buy.json should be addressed in the same 4.0 release as a coherent cascade. - SDK regen required for @adcp/client, adcp-go, Python. Related: - #4876 — envelope status REQUIRED (beta.2) - #4897 — companion governance schema rename (separate PR #4902) - adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pdate success responses (#4895) Under MCP flat-on-the-wire serialization, the envelope task-status (`status`, TaskStatus) and the body-level media-buy lifecycle status (`status`, MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and `UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope stamps a TaskStatus at the same path. No validator catches it. WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal). **Strictly additive — no schema is renamed and no `required[]` constraint changes.** - create-media-buy-response.json `CreateMediaBuySuccess`: adds `media_buy_status: $ref media-buy-status.json` alongside the existing `status`. The legacy `status` is marked `deprecated: true` and slated for removal in 4.0. Neither field is in `required[]` (both optional in 3.1). `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }` is the TaskStatus discriminator. - update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric. Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`, `get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and `core/media-buy.json` `status` are NOT renamed. These fields live nested at depth ≥ 1 inside arrays, so the envelope `status` at the response root does not collide with them on the wire. Renaming them would require either a breaking `required[]` swap or a double-field transition for no wire-collision payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns `media_buy_status` at root, the next returns `status` inside an array) is the price of keeping this strictly additive. Resolve in 4.0 alongside the legacy-`status` removal, when a clean cascade rename is on the table. `cancel_media_buy` is performed via update_media_buy with cancel intent — there is no dedicated cancel tool. Inherits the rename from UpdateMediaBuySuccess; no separate handling. Storyboards swept: - protocols/media-buy/state-machine.yaml — three field_present path:"status" assertions on update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two field_value assertions checking MediaBuyStatus values on create-media-buy-response.json / update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/create_media_buy_async.yaml — left as path:"status" (this checks the submitted-arm TaskStatus discriminator). Docs: - task-reference/update_media_buy.mdx — cancellation success example shows the canonical media_buy_status form. - reference/whats-new-in-3-1.mdx — migration note in Final-spec clarifications batch. Adopter impact: - Sellers (3.1+) SHOULD emit media_buy_status on create/update success; MAY continue emitting the deprecated legacy `status` during the window. - Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to legacy `status` for compatibility. - 3.0 sellers and buyers continue to work unchanged. No required-field swap, no rename, no breakage. - 4.0: deprecated top-level `status` removed from create/update success branches. The nested `status` fields on get-media-buys-response items, get-media-buy-delivery-response items, and core/media-buy.json should be addressed in the same 4.0 release as a coherent cascade. - SDK regen required for @adcp/client, adcp-go, Python. Related: - #4876 — envelope status REQUIRED (beta.2) - #4897 — companion governance schema rename (separate PR #4902) - adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ditive-deprecate, #4895) (#4904) * spec(media-buy): additive-deprecate body-level `status` on create / update success responses (#4895) Under MCP flat-on-the-wire serialization, the envelope task-status (`status`, TaskStatus) and the body-level media-buy lifecycle status (`status`, MediaBuyStatus) share the same root key on `CreateMediaBuySuccess` and `UpdateMediaBuySuccess`. Enums overlap on completed | canceled | rejected and diverge elsewhere — a MediaBuyStatus is silently destroyed when the envelope stamps a TaskStatus at the same path. No validator catches it. WG-recommended Option E (additive-deprecate, 3.1 minor → 4.0 removal). **Strictly additive — no schema is renamed and no `required[]` constraint changes.** - create-media-buy-response.json `CreateMediaBuySuccess`: adds `media_buy_status: $ref media-buy-status.json` alongside the existing `status`. The legacy `status` is marked `deprecated: true` and slated for removal in 4.0. Neither field is in `required[]` (both optional in 3.1). `CreateMediaBuySubmitted` branch unchanged — its `status: { const: "submitted" }` is the TaskStatus discriminator. - update-media-buy-response.json `UpdateMediaBuySuccess`: symmetric. Out of scope (deliberate): `get-media-buys-response.json` `media_buys[].status`, `get-media-buy-delivery-response.json` `media_buy_deliveries[].status`, and `core/media-buy.json` `status` are NOT renamed. These fields live nested at depth ≥ 1 inside arrays, so the envelope `status` at the response root does not collide with them on the wire. Renaming them would require either a breaking `required[]` swap or a double-field transition for no wire-collision payoff. The nested-vocabulary inconsistency in 3.1 (one buyer call returns `media_buy_status` at root, the next returns `status` inside an array) is the price of keeping this strictly additive. Resolve in 4.0 alongside the legacy-`status` removal, when a clean cascade rename is on the table. `cancel_media_buy` is performed via update_media_buy with cancel intent — there is no dedicated cancel tool. Inherits the rename from UpdateMediaBuySuccess; no separate handling. Storyboards swept: - protocols/media-buy/state-machine.yaml — three field_present path:"status" assertions on update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/pending_creatives_to_start.yaml — two field_value assertions checking MediaBuyStatus values on create-media-buy-response.json / update-media-buy-response.json → path:"media_buy_status". - protocols/media-buy/scenarios/create_media_buy_async.yaml — left as path:"status" (this checks the submitted-arm TaskStatus discriminator). Docs: - task-reference/update_media_buy.mdx — cancellation success example shows the canonical media_buy_status form. - reference/whats-new-in-3-1.mdx — migration note in Final-spec clarifications batch. Adopter impact: - Sellers (3.1+) SHOULD emit media_buy_status on create/update success; MAY continue emitting the deprecated legacy `status` during the window. - Buyers (3.1+) MUST prefer media_buy_status when present; MAY fall back to legacy `status` for compatibility. - 3.0 sellers and buyers continue to work unchanged. No required-field swap, no rename, no breakage. - 4.0: deprecated top-level `status` removed from create/update success branches. The nested `status` fields on get-media-buys-response items, get-media-buy-delivery-response items, and core/media-buy.json should be addressed in the same 4.0 release as a coherent cascade. - SDK regen required for @adcp/client, adcp-go, Python. Related: - #4876 — envelope status REQUIRED (beta.2) - #4897 — companion governance schema rename (separate PR #4902) - adcontextprotocol/adcp-client#1898 — SDK-side audit + transport precedence Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * spec(media-buy): fold #4908 — MUST-equality clause + storyboard assertions for deprecation window During the 3.1 deprecation window both `status` (deprecated) and `media_buy_status` (canonical) coexist on CreateMediaBuySuccess / UpdateMediaBuySuccess. The prior schema was silent on whether sellers emitting both fields must carry the same value; JSON Schema validation passed even for divergent values (e.g., status: active, media_buy_status: paused). - Schema descriptions: add MUST-identical-values clause to `status` and `media_buy_status` on both create-media-buy-response.json and update-media-buy-response.json. - Storyboard (pending_creatives_to_start.yaml): add `field_value_or_absent` assertions on the deprecated `status` path at the two steps that already check `media_buy_status` — passes when status is absent (conformant 3.1 sellers), fails only when status is present with a diverging value. - Migration doc: document the equality requirement and explain why if/then JSON Schema constraint was deferred (short window, codegen compat uncertainty, storyboard is sufficient). Resolves #4908. Refs #4895. * spec(media-buy): align schema description with 3.2 removal timeline (#4904 follow-up) The schema descriptions on CreateMediaBuySuccess / UpdateMediaBuySuccess for the deprecated top-level `status` and the new `media_buy_status` both still said "removed in 4.0" while every other surface (migration page, changeset adopter section, whats-new bullet, task-reference tables) was updated to "removed in 3.2 (#4906)" when the cascade was split. aao-release-bot caught the drift. Three replacements per file ("removed in 4.0" → "removed in 3.2 (#4906)") on: - create-media-buy-response.json: media_buy_status description, deprecated status description - update-media-buy-response.json: same Schema↔doc parity restored. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #4832.
Summary
core/protocol-envelope.jsonnow declaresstatusin itsrequiredarray. Every task response — including synchronous read-only metadata calls likeget_adcp_capabilities— MUST carry a top-levelstatusfield. Synchronous calls emit"completed"; async calls emitsubmitted,working,input-required, etc. per thetask-statusenum.Why this is a clarification, not a new requirement
The docs and storyboards already treat envelope
statusas canonical:sdk-stack.mdx:114listsstatusas a Layer 1 response envelope responsibilitymcp-response-extraction.mdx:77-87showsstatusas a top-level structuredContent field in normative exampleswebhooks.mdxanderror-handling.mdxdiscussstatusvalues matter-of-factly across the sync/async/webhook lifecyclev3_envelope_integrity/no_legacy_status_fieldsstoryboard step assertsenvelope_field_present: status— currently the only enforcement layerThe schema design just left
statusdeclared inpropertiesbut absent fromrequired, which let SDKs ship without emitting it on some sync responses. This PR closes the ambiguity at the schema layer.How the gap surfaced
Adopter @kapoost (production seller agent on
@adcp/sdk@7.7.0, 117/121 storyboards passing) hitv3_envelope_integrity/no_legacy_status_fieldsfailure (#4832). The SDK's auto-registeredget_adcp_capabilitieshandler inadcp-client/src/lib/server/create-adcp-server.ts:4294-4376buildscapabilitiesDatafromadcp,supported_protocols,media_buy,account,creative,library_version— and never setsstatus.capabilitiesResponseinresponses.ts:133runs the payload throughtoStructuredContent, which is a type-only passthrough. The handler bypasses the v6 envelope-threading helpers, so nostatusis added.The storyboard's expectation was correct in spirit but the schema didn't enforce it, leaving the SDK gap latent.
What this PR changes
core/protocol-envelope.json: addsrequired: ["status"]and tightens the schema description and thestatusproperty description to spell out the contract for sync metadata responses.minor(stricter wire contract).What this PR does NOT change
create-media-buy-response.json,sync-creatives-response.json, etc.) still only$refversion-envelope.json. Folding them to also$refprotocol-envelope.jsonis mechanical cleanup that lets per-taskresponse_schemavalidators catch envelope omissions directly. Filed as a separate follow-up before 3.1 GA.adcp-clientauto-registeredget_adcp_capabilitieshandler still needs to emitstatus: "completed". Filed as a sibling-repo follow-up.The two follow-ups together close the loop on the SDK side; this PR locks the spec contract.
Adopter impact
statusare now non-conformant.@adcp/client's auto-registeredget_adcp_capabilities(sibling follow-up).statusbecause the SDK threads the envelope around typed platform returns.status: "completed"to any sync response missing it.Validation
npm run test:schemas— 8/8 passing including the new schema statenpm run lint:schema-links— passing$refprotocol-envelope.json, so the newrequireddoesn't cascade through allOf chainsCross-references