Skip to content

feat(decisioning): property_list resolver + intersection helper for get_products #494

@bokelley

Description

@bokelley

Motivation

GetProductsRequest.property_list is {agent_url, list_id, auth_token?} — a reference to an externally-managed list of properties the buyer has been authorized to spend on. The seller is expected to fetch the list from the buyer's agent, intersect against each product's publisher_properties, and set property_list_applied: true in the response.

This is directly capability-gated already: RequiredFeatures.property_list_filtering (src/adcp/types/generated_poc/core/media_buy_features.py:18) exists on both buyer-side filter and seller capabilities response. Every adopter that wants to claim this capability writes the same fetch + intersect + flag-echo code. Salesagent already has it inline; the helper should live in the SDK.

Parent tracker: #491.

Current state

Salesagent has the canonical implementation inline:

  • src/core/tools/products.py:65-91extract_product_property_ids (reads selection_type all/by_id/by_tag)
  • src/core/tools/products.py:94-126should_include_product_for_property_list (handles property_targeting_allowed permissive vs strict mode)
  • src/core/tools/products.py:129-142filter_products_by_property_list
  • src/core/tools/products.py:404-424 — invocation site (calls resolve_property_list from src/core/property_list_resolver.py:42, then filters, sets the flag implicitly via the wire schema's property_list_applied)

SDK: nothing. MediaBuyHandler.get_products (src/adcp/decisioning/handler.py:1015-1033) doesn't touch property_list. The capability RequiredFeatures.property_list_filtering is wire-defined but the framework doesn't gate on it.

Proposed API

Capability-gated, framework intercepts post-adapter (mirrors auto-emit pattern):

# src/adcp/decisioning/property_list.py (new)

async def resolve_property_list(
    ref: PropertyList,
    *,
    fetcher: PropertyListFetcher,
) -> set[str]:
    \"\"\"Fetch the list from `ref.agent_url` and return the set of allowed
    property_ids. Adopter supplies a `PropertyListFetcher` to plug in
    their HTTP client / auth.\"\"\"

def filter_products_by_property_list(
    products: list[Product],
    allowed_property_ids: set[str],
) -> list[Product]:
    \"\"\"Mirrors salesagent's logic: respects publisher_properties.selection_type
    ('all' = always include, 'by_id' = intersect, 'by_tag' = exclude),
    and respects product.property_targeting_allowed (False = strict subset
    required, True = any intersection suffices).\"\"\"

Capability declaration on DecisioningCapabilities:

caps = DecisioningCapabilities(
    media_buy=MediaBuy(
        execution=Execution(
            features=Features(property_list_filtering=True),
        ),
    ),
)
# When True, framework runs filter post-adapter and sets
# response.property_list_applied = True. When False, framework
# leaves request.property_list untouched and the adopter handles it
# (or ignores it).

Acceptance criteria

  • filter_products_by_property_list matches salesagent's three-mode logic (all / strict / permissive) — port the salesagent test suite
  • Framework runs the filter only when capability is declared AND req.property_list is present
  • Sets response.property_list_applied = True when filter applied
  • Adopter-supplied PropertyListFetcher protocol (no hidden HTTP client)
  • Fetch failures surface as AdcpError with recovery='transient', mirroring salesagent's behavior
  • validate_platform warns when property_list_filtering=True is declared but no fetcher is wired
  • Test: product with selection_type='all' always passes
  • Test: product with property_targeting_allowed=False requires full subset
  • Test: auth_token threaded through to the fetcher

Out of scope

  • The list_property_lists / create_property_list task family (already wired in handler at lines 1814-1909)
  • Caching the fetched list across requests (out of scope for v1; framework calls fetcher every time)
  • Seller-side property-list publishing (separate from the buyer-supplied filter)

Cross-references

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions