Skip to content

fix(canonical-formats): compare every parameter a format reference declares - #1518

Open
KonstantinMirin wants to merge 1 commit into
adcontextprotocol:mainfrom
KonstantinMirin:fix/canonical-format-params
Open

KonstantinMirin wants to merge 1 commit into
adcontextprotocol:mainfrom
KonstantinMirin:fix/canonical-format-params

Conversation

@KonstantinMirin

Copy link
Copy Markdown
Collaborator

Closes #1517.

core/format-id.json declares four parameters beyond the (agent_url, id) identity —
width, height, duration_ms and pixel_ratio. Three helpers in
adcp/canonical_formats/compat_helpers.py each named three of them in a hand-written
tuple, so none of them ever looked at pixel_ratio.

What was wrong

Measured on d5195a952, before this change:

from adcp.canonical_formats import (
    format_is_supported, formats_are_equivalent, upgrade_legacy_format_id,
)
from adcp.types.legacy import LegacyFormatId as FormatId

a = "https://creative.adcontextprotocol.org"
base = dict(agent_url=a, id="display_image", width=300, height=250)

format_is_supported(FormatId(**base), FormatId(**base, pixel_ratio=2))
# -> True    a product requiring 2x accepts a creative that claims no density

format_is_supported(FormatId(**base, pixel_ratio=3), FormatId(**base, pixel_ratio=2))
# -> True    and accepts one that claims a conflicting density

formats_are_equivalent(FormatId(**base, pixel_ratio=3), FormatId(**base, pixel_ratio=2))
# -> True    two references that explicitly disagree compare equal

legacy = FormatId(agent_url=a, id="display_300x250", width=300, height=250, pixel_ratio=2)
upgrade_legacy_format_id(legacy).pixel_ratio
# -> None    the legacy-size upgrade discards the density it was handed

The first line is the one with a delivery consequence: a seller publishing a format that
requires a 2x asset accepts a creative that never claims one, so a 1x image is served into
a 2x slot. The directional rule format_is_supported already implements correctly for the
other three parameters — if the supported side fixes a parameter, the request must state and
match it — simply never ran for this one.

What changed

The parameter set is now derived: set(FormatId.model_fields) - {"agent_url", "id"},
read once into _PARAMETER_FIELDS and walked by all three sites.

Deriving rather than appending a fourth name is the point of the change. An independent
server-side implementation of this same comparison hit the identical bug from its own
hand-written list of the same three names — two implementations, one list each, the same
parameter missing from both. A derived set means the next parameter added to
core/format-id.json is compared without anyone remembering to name it.

One rule stays per-field, because the schema gives it one. On pixel_ratio:

Omit for the backward-compatible 1x variant.

So an absent density is a value, not silence — unlike an absent width, which means
"template, unparameterized". _IMPLIED_WHEN_ABSENT carries that: a product fixed at 2x
refuses a request that says nothing, while one fixed at 1x still serves it. Without it,
closing the gap would have introduced the mirror-image false rejection, refusing every
buyer who uses the omitted form against a seller who spells pixel_ratio: 1 explicitly.

Relationship to #1364

#1364 was a different defect in format_is_supported — it refused the
FormatReferenceStructuredObject that generated model fields carry — and its fix landed on
2026-10-04. These gaps survived it untouched: the input coercion changed, the comparison
body did not.

Tests

tests/test_canonical_formats_compatibility.py gains five tests. Four of them are the
measured cases above; the fifth is the guard that would have caught the original bug:

@pytest.mark.parametrize("parameter", sorted(_CONFLICTING_VALUES))
def test_the_helpers_refuse_a_conflict_in_every_declared_parameter(parameter: str) -> None:

Its parameter list comes from get_named_schema_document("core/format-id.json"), not from
the model, so it fails when a helper stops comparing something the spec declares rather
than only when helper and model disagree. A companion test asserts the conflict table
covers every parameter the schema declares, so a new one cannot slip past the parametrize.

Red before the change, on the commit this branch is based on:

$ python -m pytest tests/test_canonical_formats_compatibility.py -q
FAILED ...::test_the_helpers_refuse_a_conflict_in_every_declared_parameter[pixel_ratio]
FAILED ...::test_format_is_supported_refuses_a_request_that_omits_a_required_pixel_ratio
FAILED ...::test_upgrade_legacy_format_id_keeps_every_parameter_it_was_given
3 failed, 20 passed in 10.99s

Green after:

$ python -m pytest tests/test_canonical_formats_compatibility.py -q
23 passed in 8.87s

test_format_is_supported_accepts_the_omitted_form_of_a_1x_product passes on both sides —
it passed vacuously before (the field was ignored), and it is here to hold the 1x rule
above in place, not as evidence of the fix.

Checks run locally

  • ruff check src/ — clean
  • black — clean on both changed files
  • mypy src/adcp/ — no issues in 1534 source files
  • mypy --strict tests/type_checks/ — no issues in 73 source files
  • scripts/run_pyright_type_checks.py — 0 errors over 62 fixtures
  • scripts/check_type_ignore_contract.py — clean
  • make validate-generated — generated types up to date
  • pytest -k 'format or canonical or compat or projection or creative' — 2953 passed, 310
    skipped, 0 failed

Noted, deliberately not included

_looks_like_legacy_format_tuple in adcp/types/canonical_creative.py carries the same
three-name list as a key-subset test, so sanitize_canonical_schema leaves agent_url in
properties and required for a node that also carries pixel_ratio. Its only caller is
pydantic schema generation, where the node always has a class-name title containing
"format" and the first branch matches — so there is a latent inconsistency but no
reachable consequence I could demonstrate, and no behavioral test to pin a change by. It is
described in #1517 for a separate look rather than patched speculatively here.

…clares

`core/format-id.json` declares four parameters beyond the `(agent_url, id)`
identity — `width`, `height`, `duration_ms` and `pixel_ratio`. Three helpers in
`compat_helpers.py` each named three of them in a hand-written tuple, so none of
them looked at `pixel_ratio`:

- `format_is_supported` accepted a creative that claimed no pixel density for a
  product requiring 2x, and accepted one claiming a conflicting density;
- `formats_are_equivalent` called two references with conflicting densities
  equal;
- `_upgrade_legacy_format_id` discarded the `pixel_ratio` it was handed.

Derive the set from the model's fields instead of listing it, so a parameter
added to the schema is compared without anyone remembering to name it. A fourth
literal would only reset the clock: an independent implementation of the same
comparison hit this bug from its own hand-written list of the same three names.

`pixel_ratio` keeps one deliberate per-field rule, because the schema gives it
one: omitting it selects "the backward-compatible 1x variant", so an absent
density reads as 1x rather than as unspecified. A product fixed at 2x therefore
refuses a silent request while one fixed at 1x still serves it — without that,
closing the gap would introduce the mirror-image false rejection.

The new parametrized test reads the parameter list from the bundled schema
rather than from the model, so it fails when a helper stops comparing something
the spec declares.

Closes adcontextprotocol#1517
if getattr(req, field) != supported_value:
requested_value = getattr(req, field)
if requested_value is None:
requested_value = _IMPLIED_WHEN_ABSENT.get(field)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Medium: The implied-1x rule runs on the requested side only. A supported format that omits pixel_ratio hits supported_value is None: continue three lines up and accepts a request stating pixel_ratio: 2. But the schema sentence this fix leans on — "Omit for the backward-compatible 1x variant" — makes an omitted density a value on both sides: a product declaring {display_image, 300x250} is the 1x variant, so matching a 2x request into it is the mirror of the bug you fixed (a 2x asset served against a 1x-declared product). Untested — the suite covers request-omits-vs-supported-2x but not request-2x-vs-supported-omits. If broad-on-density is the intended semantics for the supported side (the width/height template rule), state it; pixel_ratio's omission carries a value where width's does not, so the analogy isn't free.

@aao-secretariat aao-secretariat Bot 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.

Ladon verdict: Approve

Approve — clean, well-tested fix with one non-blocking medium.

The change derives the format-parameter set from FormatId.model_fields and walks it at all three comparison sites, correctly closing the pixel_ratio gap (1x asset served into a 2x slot, conflicting densities compared equal, legacy-upgrade dropping the density). Commit prefix is correctly fix:; no public model shape changes, no import-layering violations, no generated-code hand-edits.

Medium findings

  • src/adcp/canonical_formats/compat_helpers.py:191 — Implied-1x rule applied only to the requested side of format_is_supported, so a supported format omitting pixel_ratio still accepts a 2x request (mirror of the fixed bug, untested).

Decision path: no critical/high findings (row 1 no); gated_paths false (row 2 no); high_risk false (rows 3/5 no); the single medium is not data-loss/schema/infra (row 4 no); no prior escalation (row 6 no); no no-auto-approve team match (row 7 no); only 1 medium, fewer than 3 (row 8 no) → row 9 approve. Worth a follow-up to apply the implied-1x rule symmetrically, but not blocking.

Medium findings

  • src/adcp/canonical_formats/compat_helpers.py:191 — Implied-1x rule applied only to requested side of format_is_supported

This branch has not been deployed

No deployments
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.

The canonical format helpers compare three of the four parameters a format reference declares

1 participant