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-91 — extract_product_property_ids (reads selection_type all/by_id/by_tag)
src/core/tools/products.py:94-126 — should_include_product_for_property_list (handles property_targeting_allowed permissive vs strict mode)
src/core/tools/products.py:129-142 — filter_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
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
Motivation
GetProductsRequest.property_listis{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'spublisher_properties, and setproperty_list_applied: truein 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-91—extract_product_property_ids(readsselection_typeall/by_id/by_tag)src/core/tools/products.py:94-126—should_include_product_for_property_list(handlesproperty_targeting_allowedpermissive vs strict mode)src/core/tools/products.py:129-142—filter_products_by_property_listsrc/core/tools/products.py:404-424— invocation site (callsresolve_property_listfromsrc/core/property_list_resolver.py:42, then filters, sets the flag implicitly via the wire schema'sproperty_list_applied)SDK: nothing.
MediaBuyHandler.get_products(src/adcp/decisioning/handler.py:1015-1033) doesn't touchproperty_list. The capabilityRequiredFeatures.property_list_filteringis wire-defined but the framework doesn't gate on it.Proposed API
Capability-gated, framework intercepts post-adapter (mirrors auto-emit pattern):
Capability declaration on
DecisioningCapabilities:Acceptance criteria
filter_products_by_property_listmatches salesagent's three-mode logic (all / strict / permissive) — port the salesagent test suitereq.property_listis presentresponse.property_list_applied = Truewhen filter appliedPropertyListFetcherprotocol (no hidden HTTP client)AdcpErrorwithrecovery='transient', mirroring salesagent's behaviorvalidate_platformwarns whenproperty_list_filtering=Trueis declared but no fetcher is wiredselection_type='all'always passesproperty_targeting_allowed=Falserequires full subsetauth_tokenthreaded through to the fetcherOut of scope
list_property_lists/create_property_listtask family (already wired in handler at lines 1814-1909)Cross-references
src/adcp/types/generated_poc/core/media_buy_features.py:18(property_list_filtering)src/adcp/types/generated_poc/bundled/media_buy/get_products_request.py:1178-1191(PropertyList)src/adcp/types/generated_poc/bundled/media_buy/get_products_response.py:4538-4543(property_list_applied)src/core/tools/products.py:65-142src/core/property_list_resolver.pysrc/adcp/decisioning/webhook_emit.py