Skip to content

feat(decisioning): add full stack registry factory#862

Merged
bokelley merged 1 commit into
mainfrom
bokelley/issue-697-pg-registry-full-stack
May 26, 2026
Merged

feat(decisioning): add full stack registry factory#862
bokelley merged 1 commit into
mainfrom
bokelley/issue-697-pg-registry-full-stack

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

Summary

Replaces #765 on a repo-owned branch, stacked on top of #861 / #696 so required base-repo checks can run normally.

Closes #697.

This adds PgBuyerAgentRegistry.with_full_stack() as the one-call factory for the documented production wrapper order: Caching(RateLimited(Auditing(pg))).

What changed

  • Added with_full_stack() to PgBuyerAgentRegistry.
  • Builds the canonical cache -> rate-limit -> audit stack while returning the outer CachingBuyerAgentRegistry.
  • Threads audit_sink and sink_timeout_seconds through all three layers.
  • Shares time_source between cache and rate limiter for deterministic tests.
  • Wires Pg mutations through this registry instance to clear the returned cache.
  • Documents the fixed layer order and points adopters with non-default ordering needs to manual composition.

Local validation

  • PYTHONPATH=src ruff check src/adcp/decisioning/pg/buyer_agent_registry.py tests/conformance/decisioning/test_pg_buyer_agent_registry.py
  • PYTHONPATH=src pytest tests/conformance/decisioning/test_pg_buyer_agent_registry.py -q (skips locally: no Postgres fixture in this workspace)

Copy link
Copy Markdown

@aao-ipr-bot aao-ipr-bot Bot left a comment

Choose a reason for hiding this comment

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

LGTM. Right shape — thin composition over already-reviewed primitives, with the canonical layer order documented in registry_cache.py:37-62 materialized as a one-call factory.

Things I checked

  • Layer order matches the docstring claim: Caching(RateLimited(Auditing(self))), cache outermost so hits skip rate-limit accounting and DB work (buyer_agent_registry.py:464-486).
  • audit_sink / sink_timeout_seconds threaded through all three wrappers; time_source threaded through the two that accept it (Cache, RateLimited) — AuditingBuyerAgentRegistry.__init__ doesn't take time_source and the factory correctly omits it (registry_cache.py:613-619).
  • Observer wiring at buyer_agent_registry.py:486 is the same lambda _op, _agent_url: cache.clear_sync() pattern as with_caching at L419. Same add_mutation_observer lock guard, same single-instance scope caveat.
  • Defaults track the wrapper classes: ttl_seconds=60.0, max_entries=4096, rps_per_tenant=100.0, burst=None→rps, time_source=time.monotonic — all match CachingBuyerAgentRegistry.__init__ and RateLimitedBuyerAgentRegistry.__init__.
  • Test outcome accounting is consistent with the "terminating layer emits, fallthrough does not" rule (registry_cache.py:27-31): Test 1 produces [resolved, cached_hit, resolved], Test 2 produces [miss, rate_limited]. Both assertions match.
  • Type-only AuditSink import is correctly guarded by TYPE_CHECKING; the registry_cache import sits inside the method to dodge the same import cycle with_caching dodges.
  • Public-API audit: purely additive new method on a public class. feat: is the correct conventional-commit prefix — no ! needed.
  • security-reviewer: no High/Critical findings. _credential_key emits api_key:<key_id> / oauth:<client_id> — non-secret identifiers; no new credential-leak path versus pre-factory composition. Tenant isolation preserved (both wrappers key on (tenant_id, lookup_key) from _current_tenant_id()).

Follow-ups (non-blocking — file as issues)

  • security-reviewer flagged a doc-framing Medium: the rate limiter is bucketed per-(tenant_id, lookup_key), so an attacker rotating agent_urls within one tenant context sustains ~100 RPS of miss audit events per probed URL. Already documented behavior of RateLimitedBuyerAgentRegistry, but with_full_stack's "canonical production" framing oversells it. Worth a sentence in the with_full_stack docstring spelling out the per-key keyspace, and considering a future per-tenant-global knob.
  • Calling with_full_stack() more than once on the same pg instance stacks two clear_sync observers and two caches — fine semantically (each lambda clears its own cache) but worth a one-line "call once at startup" note in the docstring to head off the per-request footgun.
  • CapturingAuditSink and FakeClock at test_pg_buyer_agent_registry.py:47-63 look duplicative of patterns already in tests/conformance/decisioning/test_registry_cache.py. Shared fixture would be tidier; not blocking.

Approving. Tests are conformance-gated on ADCP_PG_TEST_URL so the CI Postgres job is the real validation.

@bokelley bokelley merged commit 28c4f64 into main May 26, 2026
23 checks passed
@bokelley bokelley deleted the bokelley/issue-697-pg-registry-full-stack branch May 26, 2026 01:34
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.

feat(buyer-agent-registry): with_full_stack() — compose caching + rate-limiting + auditing in one factory

2 participants