Repository navigation
Conversation
…dentity, add CUR curated-package deal type - Product.name max length 128 -> 100, Organization.name and Account.name max length 128 -> 120, matching the OpenDirect 2.1 spec. Bounds already at spec (Creative.name 255, lifecycle name fields at 200) are untouched. - NegotiationMessage gains optional agent_url: the sender's registered A2A agent URL, for registry trust-tier verification. Optional for backward compatibility. - DealType gains CUR: curated package deal, floor-priced and bid-based, bundling curated inventory under one deal ID via an SSP-native curation product (Index Inventory/Auction Packages, PubMatic Auction Packages, Magnite Curate). Distinct from PA (private auction, the general private-marketplace tier). Named CUR rather than reusing the industry term "PMP" on the wire, since bare "PMP" is ambiguous and commonly refers to the PA tier instead; seller-internal "PMP curated deals" terminology maps to CUR here. The docstring spells out the disambiguation. Regenerated spec/jsonschema and spec/fixtures golden vectors from the models via spec/generate_schemas.py and spec/jsonschema/protocol/generate.py, plus python -m iab_agentic_primitives.conformance --regenerate.
fc24b42 to
4e983dc
Compare
…not a deal type - DealType reverts to PG/PD/PA only. Its docstring now states explicitly that curated packages are NOT a fourth deal type: pricing mechanic and packaging structure are orthogonal, and a curated deal can accompany any DealType (typically PA-mechanics, but can be PG). Both CUR and the industry term "PMP" were considered as wire values for a fourth deal type and rejected, in favor of aligning with the published IAB Deals API v1.0 model, which keeps the two concerns separate. - Add Curation (and CurationFeeType) to primitives.pricing, modeling the IAB Deals API v1.0 Curation object (deal-api spec, deal1.0.md): curator (canonical domain of the packaging entity), curator_deal_id (the curator's own deal id, spec cdealid), curation_fee_type (spec curfeetype: 0=undisclosed, 1=percent of spend, 2=flat fee, 3=CPM, 4=no fee), and an ext slot. All fields optional, matching the published spec's own all-optional Curation object. - Deal gains an optional curation field. DealBookingResponse wraps Deal directly, so it picks up curation automatically with no separate field. Regenerated spec/jsonschema and spec/fixtures golden vectors via spec/generate_schemas.py and spec/jsonschema/protocol/generate.py plus python -m iab_agentic_primitives.conformance --regenerate, and refreshed STANDARDS_GAP_REPORT.md (the iab-deals-api-1.0 standards entry now notes Curation is modeled, still unverified against the published examples).
Sirajmx
left a comment
There was a problem hiding this comment.
Thanks @atc964. The contract changes are well reasoned, and modelling curated packages as the IAB Deals API Curation object rather than a fourth DealType is the right call. I checked out a4a5238 and tested it against both agents.
What I ran
- This repo: 587 passed, 8 skipped (the live registry tests, credentials not provisioned). The schema and golden-fixture drift tests pass, so the regenerated JSON Schemas match the models.
- seller-agent
main(ba54aac) with this branch installed: 1670 unit tests passed. - buyer-agent
main(47ac319) with this branch installed: 3528 passed, 1 skipped. - Live: seller
mainin CSV mode with one 104-character product name, served on v0.5.1 and then on this branch, with the buyer'sOpenDirectClientlisting products from it on both versions.
1. Document agent_url as an unverified claim (blocking)
The new NegotiationMessage.agent_url is described as being "for registry trust verification", the same wording as the existing QuoteRequest.agent_url. Both are plain body fields the sender fills in. A receiver that raises the sender's trust tier based on this value alone is trusting a self-asserted identifier: the URL says which agent the sender claims to be, not that the sender is that agent. The contract wording currently invites that use. I've seen this pattern on the seller side and will follow up there separately.
Requested change: update the description on both fields to say this is the sender's claimed agent URL. Receivers may use it to look up or reject an agent (for example a blocked one), but must not use it to raise a trust tier unless it is bound to an authenticated credential, such as a key issued to that agent or a registry-issued token whose subject is that URL.
2. Name-length bounds (non-blocking here; seller follow-up needed before it bumps the pin)
The 100/120 bounds match OpenDirect 2.1, and the buyer's OpenDirect models already enforce them (ad_buyer/models/opendirect.py: Organization.name 120, Product.name 100). So this aligns the contract with what one consumer already does.
The seller enforces no bound. Product names come through unchanged from the CSV name column and GAM displayName into the shared Product (contract_mappers.py:521, via products_to_list_response). With this branch installed:
| Seller primitives | GET /products (16 products, one with a 104-char name) |
|---|---|
| v0.5.1 | 200, all 16 |
| this PR | 500 for the whole list: ValidationError: String should have at most 100 characters |
On the buyer side nothing changes. On either version the strict list_products fails on that product, and list_products_tolerant returns 15 plus one reject. So the mismatch (seller serves up to 128, buyer accepts up to 100) already exists on main; this PR moves the failure into the seller.
Suggestions:
- Extend the CHANGELOG "Breaking" entry: a server that builds
Productfrom upstream data must bound the name at ingestion, or one long name fails the entire list response. - On the seller side, before it moves off the
v0.5.1pin: bound names at sync/CSV ingestion, and build the product list per product so one bad record can't fail the whole response (matching the buyer's tolerant parser). - None of the three test suites has a name near the bound; the longest name in the seller's sample inventory is 38 characters.
3. Commit history (non-blocking)
The first commit's title says "add CUR curated-package deal type", which the second commit removes. Please squash on merge so the public history doesn't announce a CUR value that never shipped.
4. Smaller notes
Curationcarries the fee structure (curation_fee_type) but not the amount, which is correct per the spec. The seller's curated-deal path computescurator_fee_cpm, so when it adoptsDeal.curationthat amount should stay seller-internal or go inext, not into a new contract field.test_dealtype_docstring_disclaims_curation_as_a_deal_typeasserts on docstring wording, so it pins prose rather than behaviour. A test thatDealTypehas exactlyPG/PD/PAwould protect the decision more directly.- No version bump here, which fits the stated release gating. Both agents pin
v0.5.1, so nothing changes for them until the pin moves.
Summary
Three approved contract changes (decisions D2 and D6, 2026-09-14), preparing the ground for a v0.6.0 minor release. No version bump in this PR; that is a separately gated release step.
1. Name-length bounds conformed to OpenDirect 2.1
Product.name: max length 128 -> 100Organization.name: max length 128 -> 120Account.name: max length 128 -> 120These three were the only
max_length=128fields in the codebase (identity.py:59,identity.py:99,inventory.py:68, matching the architecture review's citations at v0.5.1).ProductRef.name(the lightweight summary embedded in quotes/deals) is unbounded and was left alone, there was no 128 bound on it to conform. Bounds that already matched spec were not touched:Creative.name(255) and the lifecyclenamefields onOrder/Line(200).2. NegotiationMessage sender identity
Added optional
agent_url: str | NonetoNegotiationMessage, documented as the sender's registered A2A agent URL for registry trust-tier verification. Follows the exact convention already used foragent_urlelsewhere in the contract (protocol/quotes.py'sQuoteRequest.agent_url,protocol/agent_card.py'sAgentTrustVerification.agent_url), plainstr, notAnyUrl; the codebase does not usepydantic.AnyUrlanywhere. Optional, so existing callers are unaffected.3. Curated packages: an IAB-aligned
Curationobject, not a fourth deal typeDecision trail on this one: PMP -> CUR -> Curation object.
DealType.PMPfor curated packages, grounded in an SSP reconciliation spike (Index Exchange, PubMatic, Magnite all independently ship a curated-package deal construct beyond PG/PD/PA).PMP->CUR, because bare "PMP" is ambiguous in industry usage — it's also commonly used for thePAprivate-auction tier this contract already encodes, and reusing it risked exactly that collision.DealTypevalue entirely. Instead, curated packages are represented by an optionalCurationobject that rides alongside a deal, modeled on the published IAB Deals API v1.0Curationobject (IABTechLab/deal-api,deal1.0.md). That spec treats packaging/curation provenance and deal pricing-mechanic (PG/PD/PA) as orthogonal concerns — a curated deal is typicallyPA-mechanics (floor + competitive bid) but can equally bePG— so collapsing curation into theDealTypeenum, under eitherCURorPMP, would have fought the shape of the standard we're supposed to be aligning with. This is the version that ships.What was added, verified against the live spec table at
raw.githubusercontent.com/IABTechLab/deal-api/master/deal1.0.md:Curation(new primitive, inprimitives.pricing):curator(canonical domain of the packaging entity, speccurator),curator_deal_id(the curator's own deal id, speccdealid),curation_fee_type(speccurfeetype), and anextslot — matching the spec's own object, which marks every field Optional.CurationFeeType(new int enum):0=undisclosed,1=percent of spend,2=flat fee,3=CPM,4=no fee — verbatim from the published enumeration.Deal.curation: Curation | None = None— optional, so it composes the same wayDeal.supply_chaindoes.DealBookingResponsewrapsDealdirectly (deal: Deal), so curation flows through automatically with no separate field needed there.DealType's docstring now states explicitly that curation is not a deal type, and records thatCURand "PMP" were both considered and rejected as wire values for the reasons above.Breaking change
Tightening
Product/Organization/Accountname bounds is breaking for any integrator currently sending names in the 101-128 (Product) or 121-128 (Organization/Account) character range, those payloads will start failing validation. Per this repo's 0.x semver convention (README "Versioning policy": a breaking schema change is a version bump; while major is pinned at 0, that bump lands on minor), this ships as v0.6.0 at release time with disclosure in the changelog, not in this PR (version files are untouched here; the release cut is a separately gated step). Downstream, both the buyer and seller agents re-pin in the same release train, and the seller adds mapper-side enforcement of the new bounds in a separate PR.NegotiationMessage.agent_urlandDeal.curationare additive and backward compatible.What changed
src/iab_agentic_primitives/primitives/identity.py:Organization.name,Account.nameboundssrc/iab_agentic_primitives/primitives/inventory.py:Product.nameboundsrc/iab_agentic_primitives/primitives/pricing.py:Curation,CurationFeeType;DealTypedocstring (curation disambiguation)src/iab_agentic_primitives/primitives/lifecycle.py:Deal.curationsrc/iab_agentic_primitives/protocol/negotiation.py:NegotiationMessage.agent_urlsrc/iab_agentic_primitives/conformance/standards.py:iab-deals-api-1.0entry now notesCurationis modeledspec/jsonschema/**,spec/fixtures/**,STANDARDS_GAP_REPORT.md: regenerated from the models (see below)CHANGELOG.md:[Unreleased]entries (bounds,agent_url,Curation)tests/test_curation.py(new),tests/test_name_bounds.py,tests/test_enum_unification.py,tests/test_protocol.py: coverageRepo notes (things that were useful to know)
spec/generate_schemas.py(primitives, e.g.Product.json,Organization.json,Deal.json) andspec/jsonschema/protocol/generate.py(protocol messages, e.g.NegotiationMessage.json,QuoteRequest.json).Curation/CurationFeeTypedon't get their own top-level schema file, they're not inWIRE_PRIMITIVES(same treatment asProductRef/QuotePricing), so they show up embedded in$defswhereverDealis rendered.python -m iab_agentic_primitives.conformance --render-md STANDARDS_GAP_REPORT.md, guarded by its own drift test, needed because this PR touchedconformance/standards.py.agent_urlfield added"agent_url": nullto everyNegotiationMessagegolden vector, andDeal.curationadded"curation": nullto everyDealgolden vector, even though nothing else changed in either.origin/mainbefore any of these edits). That drift is unrelated to this change and out of scope here; the linter itself is clean.Test plan
agent_urloptional round-trip (absent and present);Curationround-trip on aDeal, all-fields-optional default,curation_fee_typein-bounds values and out-of-bounds rejection, and thatCurationcan accompany anyDealType; a direct check that theDealTypedocstring disclaims curation as a deal type and records theCUR/PMPrejection