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
7 changes: 6 additions & 1 deletion examples/v3_reference_seller/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,13 @@ def main() -> None:
asyncio.run(_bootstrap_schema(engine))

router = SqlSubdomainTenantRouter(sessionmaker=sessionmaker)
buyer_registry = make_buyer_registry(sessionmaker)
audit_sink = make_audit_sink(sessionmaker)
# The buyer registry composes cache + rate-limit + audit around
# the SQL-backed lookup. Wiring the same audit_sink at every
# layer means cached_hit / cached_miss / rate_limited / resolved
# / miss outcomes ALL land in the audit trail; SecOps can
# reconstruct every resolve attempt.
buyer_registry = make_buyer_registry(sessionmaker, audit_sink=audit_sink)
# Anti-façade traffic recorder. The reference seller is a dev /
# storyboard target, so we wire the in-memory recorder and flip
# ``enable_debug_endpoints=True`` below to expose
Expand Down
52 changes: 49 additions & 3 deletions examples/v3_reference_seller/src/buyer_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,21 @@

from adcp.decisioning import (
ApiKeyCredential,
AuditingBuyerAgentRegistry,
BuyerAgent,
BuyerAgentDefaultTerms,
BuyerAgentRegistry,
CachingBuyerAgentRegistry,
OAuthCredential,
RateLimitedBuyerAgentRegistry,
)
from adcp.server import current_tenant

if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import async_sessionmaker

from adcp.audit_sink import AuditSink

from .models import BuyerAgent as BuyerAgentRow

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -130,11 +135,52 @@ def _row_to_agent(row: BuyerAgentRow) -> BuyerAgent:
)


def make_registry(sessionmaker: async_sessionmaker) -> BuyerAgentRegistry:
def make_registry(
sessionmaker: async_sessionmaker,
*,
audit_sink: AuditSink | None = None,
ttl_seconds: float = 60.0,
rps_per_tenant: float = 100.0,
max_entries: int = 4096,
) -> BuyerAgentRegistry:
"""Factory for the tenant-scoped registry. Returns a
Protocol-typed handle — adopters wire it into
:func:`adcp.decisioning.serve` via ``buyer_agent_registry=``."""
return TenantScopedBuyerAgentRegistry(sessionmaker=sessionmaker)
:func:`adcp.decisioning.serve` via ``buyer_agent_registry=``.

Composes three wrappers around the SQL-backed registry so the
Tier 2 commercial-identity gate has production-grade properties:

* **Cache** (outermost) — TTL + LRU. Both positive and negative
resolutions cached so an enumeration probe at the lookup
endpoint hits the DB at most once per ``(tenant, key)`` per
``ttl_seconds`` window.
* **Rate limit** (middle) — token bucket per
``(tenant, lookup_key)``. On exhaustion raises
``PERMISSION_DENIED`` with no ``details`` so the wire shape
matches the registry-miss path (no enumeration oracle).
* **Audit** (innermost) — emits one
:class:`~adcp.audit_sink.AuditEvent` per DB outcome
(``resolved`` / ``miss``); the cache and rate-limit layers
add ``cached_hit`` / ``cached_miss`` / ``rate_limited``
events to the same sink so compliance teams reconstruct
every resolve attempt.

Adopters needing different defaults pass ``ttl_seconds`` /
``rps_per_tenant`` / ``max_entries`` overrides.
"""
sql_backed = TenantScopedBuyerAgentRegistry(sessionmaker=sessionmaker)
audited = AuditingBuyerAgentRegistry(sql_backed, audit_sink=audit_sink)
rate_limited = RateLimitedBuyerAgentRegistry(
audited,
rps_per_tenant=rps_per_tenant,
audit_sink=audit_sink,
)
return CachingBuyerAgentRegistry(
rate_limited,
ttl_seconds=ttl_seconds,
max_entries=max_entries,
audit_sink=audit_sink,
)


__all__ = ["TenantScopedBuyerAgentRegistry", "make_registry"]
8 changes: 8 additions & 0 deletions src/adcp/decisioning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ def create_media_buy(
signing_only_registry,
validate_billing_for_agent,
)
from adcp.decisioning.registry_cache import (
AuditingBuyerAgentRegistry,
CachingBuyerAgentRegistry,
RateLimitedBuyerAgentRegistry,
)
from adcp.decisioning.resolve import (
CollectionList,
Format,
Expand Down Expand Up @@ -173,13 +178,15 @@ def __init__(self, *args: object, **kwargs: object) -> None:
"AdcpError",
"ApiKeyCredential",
"AudiencePlatform",
"AuditingBuyerAgentRegistry",
"AuthInfo",
"BillingMode",
"BrandRightsPlatform",
"BuyerAgent",
"BuyerAgentDefaultTerms",
"BuyerAgentRegistry",
"BuyerAgentStatus",
"CachingBuyerAgentRegistry",
"CampaignGovernancePlatform",
"CollectionList",
"CollectionListsPlatform",
Expand Down Expand Up @@ -207,6 +214,7 @@ def __init__(self, *args: object, **kwargs: object) -> None:
"PropertyList",
"PropertyListReference",
"PropertyListsPlatform",
"RateLimitedBuyerAgentRegistry",
"RequestContext",
"ResourceResolver",
"SalesPlatform",
Expand Down
Loading
Loading