Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docs/acceptance-policy-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Seller acceptance-policy discovery

AdCP 3.2 sellers can advertise an `acceptance_policy_discovery` catalog in
their media-buy capabilities. Products can add
`acceptance_policy_profile_ids`. The Python SDK can resolve those profiles and
produce a conservative advisory assessment before a buyer sends a task.

Discovery never replaces the seller's task response. An `allowed` assessment
means only that the verified, published profiles allow the contemplated action.
An absent catalog, a partial profile, an unavailable registry policy, a missing
buyer fact, or an invalid digest produces `unknown` rather than permission.

```python
from adcp import AcceptancePolicyOutcome, AcceptancePolicyResolver

discovery = capabilities.media_buy.acceptance_policy_discovery
product_profiles = product.acceptance_policy_profile_ids or []

async with AcceptancePolicyResolver() as resolver:
assessment = await resolver.assess(
discovery,
{
"subjects": [
{
"subject_category": "political",
"subject_facets": ["candidate_election"],
}
],
"advertiser_roles": ["political_actor"],
"delivery_jurisdictions": ["US"],
},
applies_to="media_buy",
product_profile_ids=product_profiles,
cache_ttl_seconds=capabilities.capability_changes.cache_ttl_seconds,
capabilities_version=capabilities.capability_changes.capabilities_version,
)

if assessment.outcome is AcceptancePolicyOutcome.prohibited:
# Do not submit this configuration.
...
elif assessment.outcome is AcceptancePolicyOutcome.unknown:
# Ask the seller or submit the exact task and handle its authoritative result.
...
```

The other outcomes identify the coarse next step:

- `requires_disclosure`: add a declaration, disclosure, funding statement, or
transparency information described by the typed requirements.
- `requires_setup`: complete advertiser verification, eligibility, account
setup, licensing, or certification.
- `requires_review`: obtain authorization or seller review, or satisfy a typed
targeting, creative, destination, format, or time restriction.

Do not execute the free-text `description` fields in catalogs, profiles, rules,
or policies. They are display-only. Use typed rule dimensions and requirements;
the exact obligations remain in the digest-pinned registry policies.

## Cache and invalidation

Pass only the TTL advertised by `capability_changes.cache_ttl_seconds`. The
resolver keys cached catalogs by canonical URL and exact digest, bounds the
cache, and never caches failures. Call `invalidate_capabilities()` after a
`capabilities.changed` notification, then re-read seller capabilities before
assessing again.

Catalog requests use HTTPS with no caller credentials, no redirects, an
IP-pinned public-address transport, a five-second default timeout, and a 1 MiB
decoded-body limit. The catalog digest is checked against the exact decoded
representation bytes before JSON parsing or schema validation. Local profile
digests and registry policy content use RFC 8785 JCS as specified by AdCP.
83 changes: 83 additions & 0 deletions src/adcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ def _resolve_version() -> str:
# names and caches the result. Grouped by module (and mirrored verbatim in the
# ``TYPE_CHECKING`` block at the bottom) so it stays readable and auditable.
_LAZY_MODULES: dict[str, tuple[str, ...]] = {
"adcp.acceptance": (
"AcceptancePolicyAssessment",
"AcceptancePolicyDiagnostic",
"AcceptancePolicyDiagnosticCode",
"AcceptancePolicyOutcome",
"AcceptancePolicyRegistry",
"AcceptancePolicyResolution",
"AcceptancePolicyResolver",
"AcceptancePolicySurface",
"DEFAULT_ACCEPTANCE_CATALOG_CACHE_ENTRIES",
"DEFAULT_ACCEPTANCE_CATALOG_MAX_BYTES",
"DEFAULT_ACCEPTANCE_CATALOG_TIMEOUT_SECONDS",
),
"adcp.adagents": (
"AdagentsCacheEntry",
"AdagentsEntryError",
Expand Down Expand Up @@ -238,6 +251,15 @@ def _resolve_version() -> str:
"AcceptProposalRequest",
"AcceptProposalResponse",
"AcceptedLoss",
"AcceptanceContext",
"AcceptancePolicyCatalog",
"AcceptancePolicyDiscovery",
"AcceptancePolicyProfile",
"AcceptancePolicyProfileId",
"AcceptancePolicyProfileIds",
"AcceptancePolicyRequirement",
"AcceptancePolicyRule",
"RegistryAcceptancePolicyProfileReference",
"BuyProductsRequest",
"BuyProductsResponse",
"ControlMediaBuyRequest",
Expand Down Expand Up @@ -880,6 +902,27 @@ def get_adcp_version() -> str:
"get_tracer",
"inject_trace_headers",
"is_tracing_available",
# Seller acceptance-policy discovery and advisory assessment
"AcceptancePolicyAssessment",
"AcceptancePolicyDiagnostic",
"AcceptancePolicyDiagnosticCode",
"AcceptancePolicyOutcome",
"AcceptancePolicyRegistry",
"AcceptancePolicyResolution",
"AcceptancePolicyResolver",
"AcceptancePolicySurface",
"DEFAULT_ACCEPTANCE_CATALOG_CACHE_ENTRIES",
"DEFAULT_ACCEPTANCE_CATALOG_MAX_BYTES",
"DEFAULT_ACCEPTANCE_CATALOG_TIMEOUT_SECONDS",
"AcceptanceContext",
"AcceptancePolicyCatalog",
"AcceptancePolicyDiscovery",
"AcceptancePolicyProfile",
"AcceptancePolicyProfileId",
"AcceptancePolicyProfileIds",
"AcceptancePolicyRequirement",
"AcceptancePolicyRule",
"RegistryAcceptancePolicyProfileReference",
# Buyer OAuth authorization-code helpers
"InMemoryPendingOAuthFlowStore",
"OAuthAuthorizationError",
Expand Down Expand Up @@ -1526,6 +1569,19 @@ def get_adcp_version() -> str:
if TYPE_CHECKING:
# Eager re-exports for type checkers / IDEs. Resolved lazily at runtime
# via ``__getattr__`` so ``import adcp`` does not build the schema graph.
from adcp.acceptance import ( # noqa: F401
DEFAULT_ACCEPTANCE_CATALOG_CACHE_ENTRIES,
DEFAULT_ACCEPTANCE_CATALOG_MAX_BYTES,
DEFAULT_ACCEPTANCE_CATALOG_TIMEOUT_SECONDS,
AcceptancePolicyAssessment,
AcceptancePolicyDiagnostic,
AcceptancePolicyDiagnosticCode,
AcceptancePolicyOutcome,
AcceptancePolicyRegistry,
AcceptancePolicyResolution,
AcceptancePolicyResolver,
AcceptancePolicySurface,
)
from adcp.adagents import (
AdagentsCacheEntry,
AdagentsEntryError,
Expand Down Expand Up @@ -1657,6 +1713,30 @@ def get_adcp_version() -> str:
test_agent_client,
test_agent_no_auth,
)
from adcp.types import (
AcceptanceContext as AcceptanceContext,
)
from adcp.types import (
AcceptancePolicyCatalog as AcceptancePolicyCatalog,
)
from adcp.types import (
AcceptancePolicyDiscovery as AcceptancePolicyDiscovery,
)
from adcp.types import (
AcceptancePolicyProfile as AcceptancePolicyProfile,
)
from adcp.types import (
AcceptancePolicyProfileId as AcceptancePolicyProfileId,
)
from adcp.types import (
AcceptancePolicyProfileIds as AcceptancePolicyProfileIds,
)
from adcp.types import (
AcceptancePolicyRequirement as AcceptancePolicyRequirement,
)
from adcp.types import (
AcceptancePolicyRule as AcceptancePolicyRule,
)
from adcp.types import (
AcceptedLoss,
AcceptProposalRequest,
Expand Down Expand Up @@ -1973,6 +2053,9 @@ def get_adcp_version() -> str:
ZipAsset,
aliases,
)
from adcp.types import (
RegistryAcceptancePolicyProfileReference as RegistryAcceptancePolicyProfileReference,
)
from adcp.types import _generated as generated
from adcp.types.aliases import (
AccountReferenceById,
Expand Down
Loading
Loading