feat(protocol): support AdCP 3.1.1 schemas#965
Conversation
There was a problem hiding this comment.
Clean 3.1.0→3.1.1 regen. The tmp→trusted_match rename stays entirely on the internal generated_poc module paths — no public name churns on the wire.
Things I checked
- No public-surface removals. The rename rewrites only
_generated.pyandclient.pyimport paths;client.py:303-306swapsgenerated_poc.tmp.*→generated_poc.trusted_match.*. Public export names (IdentityMatchRequest/Response,ContextMatch*,TmpError) are preserved. Zero-lines in any__all__or intests/fixtures/public_api_snapshot.json.feat(protocol)is the right semver signal — this is purely additive. - Collision-alias plumbing is consistent across all six surfaces.
IdentityMatchTmpxMacro/ProviderRegistrationTmpxMacroland in lockstep inconsolidate_exports.py,aliases.py,_generated.py,_eager.py,types/__init__.py,__init__.py, and the snapshot.FreshnessandTmpxProvidersare correctly scoped to_generated.pyonly (internal), so they never leak into_eager's named-import namespace or trip the lazy-surface test.code-reviewer: no blockers, no issues. TmpxMacrodual-shape split is the right shape. Two genuinely distinct wire types collide on local class name —IdentityMatchTmpxMacrois the emitted{name, value}object,ProviderRegistrationTmpxMacrois theRootModel[str]registered name. Splitting them into distinct aliases follows the existingProvenanceDeclaredBy/SiSponsoredContextDeclaredByconvention.tests/test_collision_aliases.py:208-211asserts the field-shape divergence.- Injected validators are structurally sound.
fix_trusted_match_runtime_validatorsstring-injectsfield_validator/model_validatorinto bothTmpProviderRegistration1/2andIdentityMatchResponse; every referenced field exists, forward-ref return annotations resolve underfrom __future__ import annotations, the fixer is idempotent (sentinel guards) and fails closed (source = \"\"+if source:). The_require_identity_match_dimensionscheck exactly mirrors the schemaif {identity_match: const true} then {required: [countries, uid_types]}, and the_validate_tmpx_provider_idsregex^[A-Za-z0-9_]{1,64}$is equivalent to the schema's^[A-Za-z0-9_]+$+ min/max.code-reviewerandad-tech-protocol-expertboth verified against the on-branch generated files. - No forward-compat regression. No discriminator arm dropped, no
additionalPropertiesnarrowed on a published variant, no enum member removed.IdentityMatchResponsedeprecates rather than removestmpx, keepsadditionalProperties: true.manifest_schemaswaps thetmp→trusted-matchenum value cleanly.
Follow-ups (non-blocking — file as issues)
_require_https_endpointis stricter than the 3.1.1 wire schema.ad-tech-protocol-expert: sound-with-caveats. The normativeendpoint(schemas/cache/3.1/trusted-match/provider-registration.json) isformat: uriwith prose "MUST be HTTPS in production" — a deployment posture, not a schema invariant. The injected validator (scripts/post_generate_fixes.py,fix_trusted_match_runtime_validators) raisesValueErroron anyscheme != 'https', so a wire-legalhttp://localhost/ staging registration parses per spec but is rejected by the SDK.tests/test_trusted_match_validation.py::test_provider_registration_rejects_non_https_endpointbakes it in as intended. Not a block — happy path (production https) is unchanged, and Trusted Match isx-status: experimental— but consider gating the scheme check on a non-local host, or downgrading to a warning, so TMP conformance testbeds aren't locked out.RegistryFeedResponse.freshnessis now required. Upstream 3.1.1 addedfreshnessto the schema'srequiredlist withFreshness(extra='forbid')— the SDK mirrors it faithfully (not SDK-side narrowing). But feed consumers deserializing pre-3.1.1 payloads (nofreshness) will nowValidationError. Worth aCHANGELOGcallout so adopters know the read path tightened.
Minor nits (non-blocking)
SCHEMA_DELTAS.mdremovals read as churn, not loss. The regenerated deltas file drops 106 lines of prior cumulative-baseline content and keeps only the 3.1.0→3.1.1 window. Consistent with the codegen guidance — a reviewer skimming the-block should not read it as dropped types. Interesting choice to regenerate the whole file rather than append, but harmless.
LGTM. Follow-ups noted below.
There was a problem hiding this comment.
Clean regeneration to AdCP 3.1.1. Right shape: the tmp/ → trusted_match/ namespace move is internal (generated_poc/), the public adcp.* surface only gains names, and the tmpx_providers shape break is sanctioned under x-status: experimental — so feat: is the correct semver signal, not feat!:.
Things I checked
- Additive public surface, no breaking removal. New exports
IdentityMatchTmpxMacro,ProviderRegistrationTmpxMacro,Freshness,TmpxProvidersland consistently across_generated.py,_eager.py,types/__init__.py,adcp/__init__.py,aliases.py, and the snapshot (tests/fixtures/public_api_snapshot.json). No existing name removed or retyped.feat:holds. - Regenerated, not hand-edited.
ADCP_VERSION3.1.0→3.1.1, generated timestamps all move to 2026-07-01T04:30:58Z, schema cache$schema/generatedAtbumped in lockstep. TheTmpxMacrocollision is registered inconsolidate_exports.py:100and disambiguated inaliases.py— the alias layer covers the two distinct wire shapes ({name,value}vs root string). tmp/fully removed, not orphaned.tmp/identity_match_response.pyis deleted (not left behind); the other eight modules are git-renamed intotrusted_match/. No stalegenerated_poc.tmpimport survives —client.py:303-306and_generated.pyboth repoint.- The RootModel access in the new test is safe.
TmpProviderRegistration(RootModel[...])carries the injected__getattr__proxy (post_generate_fixes.py:834), soregistration.provider_idresolves throughself.root.test_provider_registration_accepts_valid_identity_registrationwill pass. (code-reviewerconfirmed by reading the proxy; tests not run in-sandbox — see Follow-up.) - Injected validators reference real fields.
fix_trusted_match_runtime_validators(post_generate_fixes.py:1857-1971) targetsendpoint,identity_match,countries,uid_typeson bothTmpProviderRegistration1/2; anchors are unique,count=1. Theself.identity_match is Trueguard is correct, not a truthy bug — the field isbool | Noneand Pydantic coerces before themode='after'check fires. - No credential-echo regression.
security-reviewer:IdentityMatchResponseisextra='allow', but it's a response shape — nothing routes it intoRequestContext.metadata/_build_request_context, and the response-side scrubber is gated to credential-bearing account methods.tmpx*tokens are opaque HPKE pass-through, designed to round-trip. No High. - HTTPS validator is fail-closed.
_require_https_endpointchecksvalue.scheme != 'https'on anAnyUrl; Pydantic lowercases the scheme at parse and a schemeless string raises before the validator runs. No case/normalization bypass.
Follow-ups (non-blocking — file as issues)
- HTTPS validator is stricter than the spec text.
ad-tech-protocol-expert:provider_registration.endpointisformat: uriin the source schema with a description of "MUST be HTTPS in production." The injected validator rejects all non-httpsunconditionally, sohttp://localhostdev/staging registrations that the "in production" qualifier tolerates won't validate through this SDK. Consider gating on an env/x-flag or downgrading to a warning. tmpx_providerskey pattern is a client-side tightening. The_validate_tmpx_provider_idsregex^[A-Za-z0-9_]{1,64}$enforces apropertyNamesconstraint the response schema only states in prose. Semantically justified (mirrorsprovider_id), but a schema-compliant-but-unusual key would be rejected by this SDK and accepted by the wire. If it's meant to be normative it belongs upstream in the response schema.make lint-allreported red on merge. PR body notes it fails on pre-existing, unrelated test-lint. The core gate (ruff check src/,mypy, the pinned pytest set) is what matters here — confirm CI's core matrix is green before merge, since the validators were verified by reading, not by a sandbox run.
Minor nits (non-blocking)
- Un-abbreviated the namespace, kept the abbreviation on the types.
tmp/becametrusted_match/, butTmpError,TmpProviderRegistration, and theTmpx*prefixes ride along unchanged. Consistent with codegen output — noting it, not asking for a rename.
LGTM. Follow-ups noted below.
There was a problem hiding this comment.
3.1.0 → 3.1.1 regeneration, additive public surface, tmp/ → trusted_match/ on the internal generated layer. Approving — the wire-shaped changes track upstream 3.1.1 and the two new exports are consistent everywhere they need to be.
Things I checked
- Public surface is additive. Two new collision aliases
IdentityMatchTmpxMacro/ProviderRegistrationTmpxMacroland consistently acrossaliases.py(import +__all__),_eager.py,types/__init__.py,adcp/__init__.py(both__all__blocks +TYPE_CHECKING), andtests/fixtures/public_api_snapshot.json.consolidate_exports.pyregisters theTmpxMacrocollision correctly.feat(protocol):is the right semver signal — nothing stable was removed or narrowed. - No discriminated-union regression.
identity_match_response.pykeepsextra='allow', thetype: Literal['identity_match_response']discriminator, and the deprecatedtmpxfield (nullable, "Removed in 4.0") alongside the newtmpx_providers: dict[str, TmpxProviders]shape.core/format.pyis pureAssetsN → AssetsMrenumber churn — the 15-arm union,Field(discriminator='asset_type'), and theAssets94back-compat alias are intact, just repointed.registry_feed_response.pyadds a requiredfreshnessbut keepsextra='allow'.ad-tech-protocol-expert: sound-with-caveats, no fallback arm or open-union hatch dropped. tmp/fully removed, imports repointed.client.pynow importsgenerated_poc.trusted_match.*. That direct generated import is sanctioned —client.pyis on the layering exemption list (tests/test_import_layering.py:71), so this is the pre-existing pattern, not a new breach.- Injected validators are logically sound.
_PROVIDER_ID_PATTERN({1,64}) matches theprovider_idmax_length=64; theidentity_match is Truegating andAnyUrl.schemecheck reference fields that exist;IdentityMatchResponseis the last class so the appended validator attaches.tests/test_trusted_match_validation.pyexercises the real generated output on both happy and failure paths. _generated.pyrenumbering is codegen churn, exactly the instability CLAUDE.md tells us to discard, not a schema delta.
Follow-ups (non-blocking — file as issues)
_require_https_endpointfail-closes on wire-legal input.post_generate_fixes.pyhard-rejects any non-httpsendpoint, but the 3.1.1 schema types itformat: uriand the HTTPS requirement is prose-scoped to production. A schema-legalhttp://localhost:8080dev/staging registration now raisesValueErroron deserialization. Fail-closed beats fail-open, but a parsing SDK rejecting documents the schema accepts is the wrong cut here — scope it to non-loopback hosts or downgrade to a validation-mode check. (ad-tech-protocol-expert: the one genuine divergence.)tmpx_providerskey pattern is stricter than the wire. The response schema has nopropertyNamesconstraint — the^[A-Za-z0-9_]{1,64}$check is reconstructed fromprovider_id. Defensible (provider_id is the only sanctioned key), but it will fail-closed if a future revision namespaces keys. Worth a comment noting the pattern is inferred, not normative.Protocolenum drops thetmpwire value (tmp='tmp'→trusted_match='trusted-match'inmanifest_schema.py). A manifest declaringprotocol: "tmp"will no longer deserialize. Sanctioned — this is thex-status: experimentalTMP surface that upstream 3.1.1 itself renamed, sofeatunder the experimental carve-out is correct. Flagging it so the changelog prose calls out the experimental rename for anyone who prototyped againsttmp.
Minor nits (non-blocking)
- Unchecked
.replace()anchors infix_trusted_match_runtime_validators.post_generate_fixes.pyinserts the provider_registration validators by anchoring on"\n\nclass TmpProviderRegistration2(AdCPBaseModel):"etc. If codegen drifts either anchor,.replace()returns the string unchanged, the file is written, and it prints"added runtime validators"— a silent no-op.test_trusted_match_validation.pycatches it in CI, but the failure surfaces in the test suite, not the generator. Assert the content changed (as the import-shape branch already does) so it fails loudly at generation time. dict\[StringConstraints\((.*?)\),non-greedy truncation. AStringConstraintsargument whose pattern literally contains),would truncate and emit broken Python. No 3.1.1 schema hits it — latent footgun, forward-looking normalization only.
Approving on the strength of the additive public surface plus a clean forward-compat audit. Follow-ups noted.
There was a problem hiding this comment.
Clean 3.1.0→3.1.1 regen with the experimental tmp surface renamed to trusted_match. Additive on the wire — every public export name is stable, the snapshot only gains symbols — so feat(protocol): is the right semver signal.
Things I checked
- Public surface is additive.
tests/fixtures/public_api_snapshot.jsongains onlyIdentityMatchTmpxMacroandProviderRegistrationTmpxMacro; no name removed.ContextMatchRequest/Response,IdentityMatchRequest/Responsekeep their class names — only the module path movedtmp→trusted_match. Non-breaking;feat:holds. - Collision aliases wired through every layer.
TmpxMacrohas two distinct wire shapes (emitted{name,value}pairs vs. registered-nameRootModel[root]).consolidate_exports.py,_generated.py(disambiguated_TmpxMacroFrom*, no bare flat import),aliases.py,_eager.py,types/__init__.py,__init__.py, and the snapshot all agree.test_collision_aliases.py:206pins the distinct field sets. Right shape. - Injected validators match the schema.
provider_registration.pyhttps enforcement and theidentity_match is True→countries/uid_typesrequirement land on both union arms (TmpProviderRegistration1/2);identity_match_response.py:_validate_tmpx_provider_idsiteratesdict[str, TmpxProviders]keys against^[A-Za-z0-9_]{1,64}$, faithfully reproducingprovider-registration.json'sprovider_idmin/max.ad-tech-protocol-expert: sound — every wire point matches upstream 3.1.1. - Regen, not hand-edit. Timestamps bumped to 2026-07-01,
$schemarefs to 3.1.1,ADCP_VERSIONand README to 3.1.1. Validators are applied bypost_generate_fixes.fix_trusted_match_runtime_validators()in the pipeline, consistent with the repo's post-gen architecture — not source edits togenerated_poc/. tmpis fully gone.generated_poc/tmp/*andschemas/cache/3.1/tmp/*removed,manifest_schema.Protocolenumtmp→trusted-match,client.pyimport rename complete. No dangling reference.identity_match_responseshape change is sanctioned.tmpx_providerswentMap<provider_id,string>→Map<provider_id,{macros:[TmpxMacro]}>and roottmpxis deprecated (removed in 4.0) — the schema carriesx-status: experimentaland documents the change inline, citing upstream #5689. Not a stable-contract break.- Variant-number de-pin.
test_decisioning_capabilities_submodule.pydrops the brittleIdempotency3assertion for an identity + prefix check. Correct per the codegen-numbering-is-unstable rule.
Follow-ups (non-blocking — file as issues)
- https validator is stricter than the spec prose.
provider-registration.json:endpointisformat: uriwith "MUST be HTTPS in production";_require_https_endpointrejects any non-https scheme unconditionally, so a wire-validhttp://staging endpoint is refused. Defensible SSRF hardening —test_provider_registration_rejects_non_https_endpointasserts it by design — but it's an SDK constraint beyond the wire shape. Document it inSCHEMA_DELTAS.mdso it's an explicit decision, not a silent one.
Minor nits (non-blocking)
- Unguarded pattern-insert anchor.
scripts/post_generate_fixes.py,identity_match_responsebranch: the_PROVIDER_ID_PATTERNinsertion via.replace("...AdcpVersionEnvelope\n\n\n", ...)is unguarded, but the validator that references it is appended unconditionally. If a future codegen import reorder breaks that exact anchor, the pattern is silently not defined while the validator still lands —NameErrorat import instead of a clean skip. The sibling pydantic-import replace right above guards withelse: return; mirror it. Anchor matches at head, so no active bug. _hold_unused_bundled_dirsruns before thetry.scripts/generate_types.py: the holdshutil.movesits outside thetry/finally, so a raise mid-hold strandsbundled/in..._bundled_heldwith no restore, and the next run'srmtree(held)deletes it. Dev-time only (input reconstitutes fromschemas/cache), but folding the hold into thetrypairs it with the restore.
Approving on the strength of the additive public surface plus the faithful validator reproduction — two expert passes, no blocker. Follow-ups noted.
Summary
tmpx_providersprovider IDsExpert review notes
tests/test_trusted_match_validation.pyandscripts/post_generate_fixes.py.Validation
make lintmake typecheck-allmake validate-generatedgit diff --checkPYTHONPATH=src pytest tests/test_schemas_version_pin.py tests/test_validation_version.py tests/test_collision_guard.py tests/test_collision_aliases.py tests/test_public_api.py::test_public_api_surface_matches_snapshot tests/test_trusted_match_validation.py tests/test_client.py tests/test_mcp_schema_drift.py tests/test_lazy_types.py -qNote:
make lint-allcurrently fails on unrelated pre-existing test lint issues outside this change.