Skip to content

Contract changes for v0.6.0: spec-conformant name bounds, negotiation sender identity, IAB-aligned curation object - #3

Open
atc964 wants to merge 2 commits into
mainfrom
feat/contract-v0.6-changes
Open

atc964 wants to merge 2 commits into
mainfrom
feat/contract-v0.6-changes

Conversation

@atc964

@atc964 atc964 commented Sep 14, 2026 •

Copy link
Copy Markdown
Collaborator

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 -> 100
  • Organization.name: max length 128 -> 120
  • Account.name: max length 128 -> 120

These three were the only max_length=128 fields 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 lifecycle name fields on Order/Line (200).

2. NegotiationMessage sender identity

Added optional agent_url: str | None to NegotiationMessage, documented as the sender's registered A2A agent URL for registry trust-tier verification. Follows the exact convention already used for agent_url elsewhere in the contract (protocol/quotes.py's QuoteRequest.agent_url, protocol/agent_card.py's AgentTrustVerification.agent_url), plain str, not AnyUrl; the codebase does not use pydantic.AnyUrl anywhere. Optional, so existing callers are unaffected.

3. Curated packages: an IAB-aligned Curation object, not a fourth deal type

Decision trail on this one: PMP -> CUR -> Curation object.

  1. First proposed: add DealType.PMP for curated packages, grounded in an SSP reconciliation spike (Index Exchange, PubMatic, Magnite all independently ship a curated-package deal construct beyond PG/PD/PA).
  2. At review: renamed PMP -> CUR, because bare "PMP" is ambiguous in industry usage — it's also commonly used for the PA private-auction tier this contract already encodes, and reusing it risked exactly that collision.
  3. Final decision (this PR): removed the fourth DealType value entirely. Instead, curated packages are represented by an optional Curation object that rides alongside a deal, modeled on the published IAB Deals API v1.0 Curation object (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 typically PA-mechanics (floor + competitive bid) but can equally be PG — so collapsing curation into the DealType enum, under either CUR or PMP, 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, in primitives.pricing): curator (canonical domain of the packaging entity, spec curator), curator_deal_id (the curator's own deal id, spec cdealid), curation_fee_type (spec curfeetype), and an ext slot — 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 way Deal.supply_chain does. DealBookingResponse wraps Deal directly (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 that CUR and "PMP" were both considered and rejected as wire values for the reasons above.

Breaking change

Tightening Product/Organization/Account name 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_url and Deal.curation are additive and backward compatible.

What changed

  • src/iab_agentic_primitives/primitives/identity.py: Organization.name, Account.name bounds
  • src/iab_agentic_primitives/primitives/inventory.py: Product.name bound
  • src/iab_agentic_primitives/primitives/pricing.py: Curation, CurationFeeType; DealType docstring (curation disambiguation)
  • src/iab_agentic_primitives/primitives/lifecycle.py: Deal.curation
  • src/iab_agentic_primitives/protocol/negotiation.py: NegotiationMessage.agent_url
  • src/iab_agentic_primitives/conformance/standards.py: iab-deals-api-1.0 entry now notes Curation is modeled
  • spec/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: coverage

Repo notes (things that were useful to know)

  • There are two separate schema generators, both needed here: spec/generate_schemas.py (primitives, e.g. Product.json, Organization.json, Deal.json) and spec/jsonschema/protocol/generate.py (protocol messages, e.g. NegotiationMessage.json, QuoteRequest.json). Curation/CurationFeeType don't get their own top-level schema file, they're not in WIRE_PRIMITIVES (same treatment as ProductRef/QuotePricing), so they show up embedded in $defs wherever Deal is rendered.
  • There's also a third generator: python -m iab_agentic_primitives.conformance --render-md STANDARDS_GAP_REPORT.md, guarded by its own drift test, needed because this PR touched conformance/standards.py.
  • Conformance golden fixtures are regenerated via the conformance module's regenerate entry point and are byte-pinned (deterministic timestamps/ids); the optional agent_url field added "agent_url": null to every NegotiationMessage golden vector, and Deal.curation added "curation": null to every Deal golden vector, even though nothing else changed in either.
  • The repo's formatter currently flags a number of pre-existing files repo-wide as unformatted at the same baseline commit this branch was cut from (confirmed against a clean worktree at origin/main before any of these edits). That drift is unrelated to this change and out of scope here; the linter itself is clean.

Test plan

  • Dependency sync from the lockfile
  • Full suite: 587 passed, 8 skipped (up from a 569-passed/8-skipped pre-PR baseline; no regressions)
  • Linter: all checks passed
  • New/updated tests cover: Product name 100 accepted / 101 rejected; Organization/Account name 120 accepted / 121 rejected; Creative 255 and Order/Line 200 bounds unchanged (regression guard); agent_url optional round-trip (absent and present); Curation round-trip on a Deal, all-fields-optional default, curation_fee_type in-bounds values and out-of-bounds rejection, and that Curation can accompany any DealType; a direct check that the DealType docstring disclaims curation as a deal type and records the CUR/PMP rejection
  • Schema, fixture, and gap-report drift tests pass

@atc964
atc964 requested a review from Sirajmx September 14, 2026 20:16
…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.
@atc964
atc964 force-pushed the feat/contract-v0.6-changes branch from fc24b42 to 4e983dc Compare September 14, 2026 20:29
@atc964 atc964 changed the title Contract changes for v0.6.0: spec-conformant name bounds, negotiation sender identity, PMP deal type Contract changes for v0.6.0: spec-conformant name bounds, negotiation sender identity, CUR curated-package deal type Sep 14, 2026
…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).
@atc964 atc964 changed the title Contract changes for v0.6.0: spec-conformant name bounds, negotiation sender identity, CUR curated-package deal type Contract changes for v0.6.0: spec-conformant name bounds, negotiation sender identity, IAB-aligned curation object Sep 14, 2026

@Sirajmx Sirajmx left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 main in CSV mode with one 104-character product name, served on v0.5.1 and then on this branch, with the buyer's OpenDirectClient listing 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 Product from 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.1 pin: 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

  • Curation carries the fee structure (curation_fee_type) but not the amount, which is correct per the spec. The seller's curated-deal path computes curator_fee_cpm, so when it adopts Deal.curation that amount should stay seller-internal or go in ext, not into a new contract field.
  • test_dealtype_docstring_disclaims_curation_as_a_deal_type asserts on docstring wording, so it pins prose rather than behaviour. A test that DealType has exactly PG/PD/PA would 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants