Repository navigation
feat(protocol): adopt AdCP 3.2.0-rc.3 - #2907
Conversation
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean mechanical adoption of AdCP 3.2.0-rc.3.
This PR pins the ADCP version, adds a correct minor changeset, and regenerates the generated type files. The one hand-written change (the content_mismatch/mismatch_code parity block in generate-zod-from-ts.ts) is correct, matches the changeset, and is covered by the differential Ajv-vs-Zod test with six new cases. No library version was hand-edited in package.json, and the changeset type accurately reflects impact.
No blocking findings and no Medium findings were reported.
Decision-table walk-through:
- Row 1 (critical/high): no such findings.
- Row 2 (gated_paths + not APPROVED): gated_paths is false — does not fire.
- Rows 3–5 (high_risk deletions/schema/infra mediums): high_risk is false, no mediums.
- Row 6 (sticky escalate): no prior decision.
- Row 7 (no-auto-approve team): no team match.
- Row 8 (≥3 mediums): zero mediums.
- Row 9: approve.
Note: review_decision is REVIEW_REQUIRED, but since gated_paths is false, row 2 does not apply and the deprecated protected-branch rule no longer forces a comment. Clean diff falls through to approve.
|
Pushed 1. Package budget — raised 5,995 → 6,025 with itemization. rc.3 publishes 16 new source schemas (scoped + media-buy frequency-cap families, 2. Version-negotiation fixtures — fixed, but the cause is worth recording because it is a trap for the next bump too. These are fail-closed tests whose premise is that the seller advertises or serves something newer than the buyer pin. The blanket rc.2 → rc.3 sweep rewrote the buyer pin as well, so "newer" became "equal" and the assertions inverted. Rather than re-pin them to rc.3, both sides now derive from the SDK's own constant: const SDK_PIN = ADCP_VERSION;
const ONE_BEYOND_SDK_PIN = /* rc.N -> rc.N+1, or X.Y.Z -> X.(Y+1).0 */;Three tests converted ( 3. Targeting Input codegen bug (not in the triage). Typechecking the rc.3 bundle against the existing surface surfaced a real defect in device_platform?: DevicePlatform | null; // scalar enum — WRONG
device_type?: DeviceType | null; // scalar enum — WRONGThe wire carries arrays in both cases.
4. Null-clear durability (#7466 / DR-0020) folded in, so no PR D. The codegen bug made this non-optional here — the strict/loose split is unusable while one side is the wrong shape, and the On the tag: Moving to PR B. |
|
Ladon cannot review this PR until merge conflicts are resolved. |
219b945 to
3fceb4b
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — AdCP 3.2.0-rc.3 adoption, clean pass.
Applied the decision table top-down:
- Row 1 (critical/high): no such findings.
- Row 2 (gated_paths + not APPROVED): gated_paths is false, does not fire.
- Row 3 (high_risk deleted): high_risk is false, does not fire.
- Rows 4–5 (medium data-loss/schema/infra or high_risk modified + medium): no medium findings.
- Row 6 (sticky escalate): prior decision was
approve, not escalate. - Row 7 (no-auto-approve team): no team match.
- Row 8 (≥3 medium): zero medium findings.
- Row 9: approve.
Reviewer reported a clean pass. Checked the delta's runtime changes: the new null-clear projection in targeting-input.ts (prototype-pollution defended, correct three-state semantics), its use in media-buy-store.ts (persist/new_packages leak paths closed, whole-clear preserved), the generate-types.ts cardinality-restore codegen, and the zod content_mismatch parity block. Two changesets are present and correctly typed for the additive exports and widened store input types — changeset-vs-wire-impact obligation satisfied. No witness-not-translator or transport-client concerns. This is a net-neutral subsequent pass over the prior clean approve.
Note: review_decision is REVIEW_REQUIRED, but with gated_paths false this does not block auto-approval per row 2.
Bump ADCP_VERSION and the default wire pin to 3.2.0-rc.3, resync the signed schema and compliance bundles, and regenerate every derived surface (core/tools/enums/inline-enums/manifest/schemas, wire-spec fields, entity hydration, media-buy update fields, agent docs, TYPE-SUMMARY). The bundle swap is not purely mechanical in one place. The hand-written Zod parity block in scripts/generate-zod-from-ts.ts encodes the context-sensitive consumer-status constraints that the JSON Schema -> TS -> Zod projection drops, and it was written against rc.2. Left alone, the rc.3 pin would have produced a validator that rejects `mismatch_code` as an unrecognized key while accepting a `content_mismatch` statement with no code at all — exactly backwards. The block now covers the rc.3 arm: `content_mismatch` requires the obligation id, the revision id, the recomputed `observed_revision_content_sha256`, and `mismatch_code`, and forbids `failure_code`; the four pre-existing statuses additionally forbid `mismatch_code`. The differential test in test/generate-zod-consumer-status.test.js gains six cases pinning generated-Zod against the published schema for that arm. `ReportingLedgerConsumerStatementV1` gains `content_mismatch` and the closed `mismatch_code` so the ledger types can carry what the wire now accepts. The projection semantics (severity, grace window, escalation, obligation counts) are deliberately not in this commit. rc.3 also publishes core/media-buy-available-action-id.json, so the generated update dispatch table gains the structured-only `update_media_buy_frequency_cap` action and its `frequency_cap` binding. Media-buy preflight already resolved that binding from the generated table rather than hardcoding it, so the shared media-buy frequency cap becomes dispatchable with no adopter change. 3.2 prereleases stay exact pins: rc.3 replaces rc.2 in COMPATIBLE_ADCP_VERSIONS and only the current prerelease's bundle ships. `adcpVersion: '3.2-rc'` follows whichever release candidate a build carries; a superseded exact pin raises a configuration error at schema load rather than silently validating against a different contract. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four things, all consequences of the rc.3 bundle meeting the existing surface.
**Package budget.** rc.3 publishes sixteen new source schemas (the scoped and
media-buy frequency-cap families, `core/media-buy-available-action-id.json`,
and the shared frequency-cap duration/control/mutable-field enums) plus two
new compliance storyboards. With bundled and compact-projection copies that is
+26 published files; the Targeting Input helper module adds +4 more. Budget
moves 5,995 -> 6,025 with the itemization in the comment. Unpacked is 101.7 MiB
against a 120 MiB ceiling.
**Version-negotiation fixtures.** The blanket rc.2 -> rc.3 pin sweep inverted
three fail-closed tests: their whole point is that the seller advertises or
serves something *newer* than the buyer pin, and rewriting the buyer pin to
match made "newer" equal. They now derive both sides from `ADCP_VERSION` and a
`ONE_BEYOND_SDK_PIN` computed from it, so the relationship survives the next
bump instead of silently degrading into "seller advertises the same version".
**Targeting Input codegen.** `TargetingOverlayInput.device_platform` and
`.device_type` were emitted as the scalar `DevicePlatform` / `DeviceType`
enums where the wire carries arrays. `core/targeting-input.json` reaches those
dimensions through a JSON-pointer `$ref` into
`core/targeting.json#/properties/<dimension>`, and for an array with no
`title` of its own json-schema-to-typescript names the result after the items'
canonical `$ref`. Verified against a standalone jsts compile, which produces
the correct tuple — the loss is specific to the aggregate pass.
`alignTargetingInputArrayCardinality` repairs it from the invariant the two
schemas actually satisfy (identical property sets, so Input is Overlay plus
`| null` per dimension), and only where the Input side is provably the item
type of the Overlay side's array, so a correct alias is never rewritten and
per-property JSDoc is preserved.
**Null-clear durability (#7466 / DR-0020).** `resolveTargetingInput`,
`applyTargetingInput`, and `hasTargetingClears` project between the
request-only Targeting Input and the strict overlay used by snapshots,
responses, and readback. `createMediaBuyStore` had two paths that could write
a clear command into durable state — `persistFromCreate` falling back to the
request overlay verbatim when the seller echoed none, and the `new_packages`
arm of `mergeFromUpdate` assigning the incoming overlay directly — after which
`backfill` would echo `null` into a `get_media_buys` response whose schema
forbids it. Both resolve clears away now, and clearing the last surviving
dimension drops the tracked overlay rather than persisting `{}`. The store's
input types widen to the request shape so a real payload typechecks; persisted
and echoed values stay strict. `examples/seller-3.2-starter.ts` projects the
same way when building its accepted proposal, which is what CI's example
typecheck was failing on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects the review stack converged on — flagged independently by all three
codex personas, the code reviewer, and the security reviewer.
**Prototype swap via a wire-supplied `__proto__`.** `targeting_overlay` comes
off `JSON.parse`, which produces a real *own* `__proto__` key that
`Object.entries` yields. Assigning it with `=` invoked the inherited setter
instead of creating an own property, swapping the accumulator's prototype.
Reproduced against the built package before the fix:
resolveTargetingInput(JSON.parse('{"__proto__":{"geo_regions":["INJECTED"]},"geo_countries":["US"]}'))
Object.keys(result) -> ['geo_countries']
result.geo_regions -> ['INJECTED']
Global `Object.prototype` was never touched, but that is not the interesting
part: an own-key validator and a dot-access enforcement path disagree about the
same object, and the attacker-chosen dimension is invisible to `Object.keys`
and `JSON.stringify` while still reading back off the overlay the seller
persists and echoes. A payload whose only key is `__proto__` also resolved to
`undefined`, which made the store silently drop the package's stored overlay.
The accumulator is now null-prototype, `__proto__` / `constructor` / `prototype`
are dropped outright so a later spread cannot reintroduce the problem, and the
result is rehydrated as an ordinary object so adopters see normal behaviour.
`hasTargetingClears` filters the same key set, because otherwise the two
functions disagreed about whether `{"__proto__": null}` is a clear command.
**Stale slice offsets in `alignTargetingInputArrayCardinality`.** The block
offsets were computed once from the pre-edit string, then the string was
rewritten inside the loop. Every repair grows it by two characters, so the
replacement window progressively under-covers the tail of the block: a
declaration past the truncated boundary is never found, `.replace()` no-ops,
and `corrections.push()` still reports success. Today's pin needs two repairs
and both sit well before the block end, so the committed output happens to be
correct — latent, not manifesting, until an rc bump adds a third nullable array
dimension or moves one to the end.
The block is now repaired in isolation and spliced back once, and a replacement
that fails to land throws instead of being logged as applied. Silently
reporting a correction that did not happen is exactly the failure this function
exists to prevent.
Both are covered: 6 codegen cases including the four-repair shape that exposes
the offset bug (verified failing against the previous implementation), and 2
prototype cases driving real `JSON.parse` output.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3fceb4b to
c9dbdcd
Compare
There was a problem hiding this comment.
Ladon verdict: Approve
Approve — clean delta review, no blocking findings.
This is a subsequent pass on #2907 (3fceb4b→c9dbdcd), following a prior approve. The delta reviewed the schema-derivation changes: generate-zod-from-ts.ts now derives the consumer-status parity block from the pinned reporting-consumer-status.json with fail-closed guards (generated output verified byte-equivalent to the prior hardcoded rules); ReportingLedgerConsumerStatementV1 unified with the generated wire type plus compile-time Equal<> assertions; and the SHA-binding check in status-ingest.ts correctly tightened from received-only to any statement carrying observed_revision_content_sha256. Coverage is schema-driven and differential; changesets are correct.
Gate checks: no no-auto-approve team match; gated_paths is false; high_risk is false. review_decision is REVIEW_REQUIRED, but row 2 only fires when gated_paths is true — it is false here, so no escalation applies. No critical/high/medium findings reported.
Decision table: rows 1–8 do not fire (no findings, no team gate, no high-risk deletion/modification, no gated paths). Falls through to row 9 → approve.
Green, and re-verified against the published tagCI run 34877997417 is green on Release verification after the tag landedThe tag was published at 18:11Z; I first synced at 14:20Z. A re-cut artifact would have silently invalidated every generated file in this PR, so I checked rather than assumed: Byte-identical. A full To close the loop on the original brief: no signature-skip workaround was ever needed or added. The Sigstore sidecars were already present at 14:20Z, so the first sync verified cleanly and every sync since has too. The two red lanes, and why they were not this PRBoth were investigated rather than re-run on faith.
No meaningful regression, against a 30 s timeout that the test's own comment says is tuned to sit above startup latency under parallel load. The subtest completes in 2.4 s locally on this head. Runner contention, not the bundle. Green on re-run.
Scope noteAfter #2911 merged I dropped everything it already covers rather than resolving conflicts in my favour — its bundle-derived Zod arms and derived ledger statement type are self-maintaining where mine were hand-written, so they're strictly better. This PR's diff to 🤖 Generated with Claude Code |
|
Thanks for the thorough verification write-up. Noted: CI green on Generated by Claude Code |
|
Acknowledged — thanks for the review pass, @bokelley. Standing by for final review. Generated by Claude Code |
First PR in the Reliable Reporting rc.3 series. Mechanical bundle + pin adoption, plus the one place where the bundle swap is not mechanical.
What's in it
ADCP_VERSION/package.json#adcp_version/ default wire pin →3.2.0-rc.3, from the published v3.2.0-rc.3 release (tagged 18:11Z). Signed bundle verified —sha256 1cd940f76516b43327cbfa6c17c0befd4c093ca85b53a81a4062d38bb522d831, cosign signature verified against theadcontextprotocol/adcprelease workflow identity.Re-verified after the tag went live, because I first synced at 14:20Z and a re-cut artifact would have invalidated every generated file here. That digest is byte-identical across all three sources — the live
/protocol/3.2.0-rc.3.tgz, the asset attached to the release tag, and what I originally generated from — and a fullsync-schemasre-run against the tagged release verifies the signature and produces zero drift in tracked files. No signature check was skipped or worked around at any point.npm run sync-schemas -- 3.2.0-rc.3,generate-types,generate-wire-spec-fields,generate-registry-types --sync(no registry delta),generate-agent-docs→docs/llms.txt+docs/TYPE-SUMMARY.md.Every rc.2-pinned test/script expectation moved to rc.3 the same way feat(protocol): adopt AdCP 3.2.0-rc.2 #2888 did for rc.2, plus doc pins in
WIRE-VERSION-COMPAT.md,MEDIA-BUY-3.2-COMPATIBILITY.md,REPORTING-LEDGER.md,migration-12-to-14.md,migration-13-to-14.md.The part that isn't mechanical
scripts/generate-zod-from-ts.tscarries a hand-written parity block that restores the context-sensitivesync_reporting_statusconstraints the JSON Schema → TS → Zod projection drops. It was written against rc.2. Swapping the bundle alone would have shipped a validator that rejectsmismatch_codeas an unrecognized key while accepting acontent_mismatchstatement with no code at all — precisely inverted. Fixed here:content_mismatchobserved_revision_content_sha256,mismatch_code; forbidsfailure_codereceived/obligation_missing/revision_missing/unreadablemismatch_codetest/generate-zod-consumer-status.test.jsgains six cases. That test is differential — it compiles the published JSON Schema with Ajv and the generated Zod withtsx, then asserts both agree — so it pins the generated validator against the signed schema rather than against my reading of it.ReportingLedgerConsumerStatementV1gainscontent_mismatch+ the closedmismatch_code. Projection semantics are deliberately not here — severity, the stale-received grace window, escalation, andobligation_counts.consumer_status_pendingland in PR B.Other rc.3 changesets, audited for runtime impact
add-media-buy-frequency-cap-scope(#7449) /add-scoped-frequency-cap-support(#7438) — no runtime change needed. rc.3 is the first bundle to publishcore/media-buy-available-action-id.json, so codegen now emitsSTRUCTURED_ONLY_MEDIA_BUY_ACTIONS = ["update_media_buy_frequency_cap"]and thefrequency_cap → update_media_buy_frequency_capbinding inACTIONS_BY_FIELD.src/lib/media-buy/preflight.ts:218already resolved that action from the generated table instead of hardcoding it (fix(media-buy): merge structured-only action metadata into update-field dispatch #2889), so the shared cap becomes dispatchable with zero adopter change.overlay_support.frequency_cap_supportand the constraint-aware discovery objects reach buyers through existing passthrough typing (required_overlay_supportis forwarded as-is innegotiation/buyer.tsandmedia-buy/compatibility.ts), so constraint-aware discovery needs no new code either.targeting-input-null-clear-semantics(#7466) — needs a runtime change, and it is not in this PR.PackageRequest/PackageUpdate.targeting_overlaynow resolve toTargetingOverlayInput, whose dimensions are individually nullable.createMediaBuyStore'smergeOverlayalready implements DR-0020's three states correctly at runtime (omit → preserve,null→ clear, value → replace), but two gaps open up once nulls can actually arrive on the wire: the store's input types are still the strict readbackTargetingOverlay, andpersistFromCreate/ thenew_packagesarm ofmergeFromUpdateassign the incoming overlay without stripping null dimensions, so a null command can land in durable state and then be echoed bybackfillinto aget_media_buysresponse whose schema forbids null. Split into PR D to keep this PR a reviewable bundle swap.rc.1 / rc.2 retention — measured, and it does not fit
The ask was to keep rc.2 and rc.1 selectable for pinned peers. I implemented the measurement rather than the change, because the two constraints in the brief collide:
dist/lib/schemas-data/. Retaining rc.1 + rc.2 adds ~72 MB and ~2,100 files, against enforced limits ofMAX_UNPACKED_PACKAGE_BYTES = 120 MB(dist is already 127 MB pre-filter) andMAX_PACKED_FILE_COUNT = 5,995. Both break.COMPATIBLE_ADCP_VERSIONSwithout shipping bundles — is a trap, not a feature.resolveBundleKeyreturns a prerelease pin verbatim andresolveSchemaRootthrows when the directory is absent; no silent fallback. Verified against this build:So I kept the repo's existing exact-pin policy (what
sync-version.tsdoes by default, and what rc.2 did to rc.1) and documented the one zero-cost affordance that does work:adcpVersion: '3.2-rc'fuzzy-matches the newest cached3.2.0-rc.*bundle, analogous to3.1-beta.migration-13-to-14.mdnow says this explicitly, including that a superseded exact pin errors at schema load rather than silently validating against a different contract.Your call on the follow-up, if you want more than that: (i) raise the size/file budgets and ship rc.2 only (+36 MB), or (ii) add a validation-only compact side bundle for prior 3.2 rcs — much smaller than a full bundle, but new machinery. Say which and I'll add it to the stack.
🤖 Generated with Claude Code
Open workspace in Conductor