Skip to content

Add SafeGuard Privacy vendor approval gate for deal requests#82

Open
tdanielcox wants to merge 2 commits intoIABTechLab:mainfrom
SafeguardPrivacy:feature/safeguard-privacy-approval-gate
Open

Add SafeGuard Privacy vendor approval gate for deal requests#82
tdanielcox wants to merge 2 commits intoIABTechLab:mainfrom
SafeguardPrivacy:feature/safeguard-privacy-approval-gate

Conversation

@tdanielcox
Copy link
Copy Markdown

Summary

Adds an optional integration with the SafeGuard Privacy (SGP) platform that lets buyers gate Deal ID generation on the iabBuyerAgentApproval flag they maintain for each vendor in their SGP tenant. The integration is off by default. When SGP_API_KEY is empty, the buyer agent behaves exactly as before — no calls, no warnings, no behavior change.

The approval is the buyer's own mark on their vendors; SGP is the system of record. This PR wires the buyer agent to consult that record before committing to a programmatic deal.

Why

Privacy-conscious buyers want programmatic tooling to refuse deals against sellers they haven't explicitly cleared. Today that check happens out-of-band (SGP dashboard, email approvals, spreadsheets) and breaks down when agents choose inventory autonomously. This PR brings the check inline — both as a hard gate at Deal ID time and as an agent-callable capability during inventory reasoning.

What's new

Client and model

File Purpose
src/ad_buyer/clients/sgp_client.py SGPClient — async httpx client for GET /api/v1/integrations/iab/buyer-agent-approval. Domain normalization, dedupe, batching to 10/request, per-domain TTL cache, full HTTP status handling. Transport errors wrap as SGPClientError so callers catch one type.
src/ad_buyer/models/sgp.py ApprovalRecord pydantic model mirroring IabBuyerAgentResource.

Tools

File Purpose
src/ad_buyer/tools/research/sgp_vendor_approval.py SGPVendorApprovalTool — CrewAI tool (name check_sgp_vendor_approval) wired into the Buyer Deal Specialist so the agent can consult approval status during product selection, not only at Deal ID time. Class is SGP-prefixed so future vendor-approval integrations can coexist.

Integration points (existing tools extended)

  • DiscoverInventoryTool — accepts an optional SGPClient. When provided, each returned product is annotated with APPROVED / NOT APPROVED / UNKNOWN. Discovery fails open on SGP transport errors so outages never break browsing.
  • RequestDealTool — optional pre-flight gate after product lookup and before Deal ID generation. Fails closed on transport errors when enforcement is on. Unknown-vendor behavior is governed by SGP_UNKNOWN_VENDOR_POLICY (block / warn / allow).
  • BuyerDealFlow — auto-instantiates SGPClient from settings when SGP_API_KEY is set and threads it through to the tools above. Logs a warning if enforcement is configured without an API key (catches the silent-bypass misconfiguration).

Configuration

Five new env vars in .env.example and src/ad_buyer/config/settings.py:

Variable Type Default Description
SGP_API_KEY str "" API key with the iab:buyerAgent scope. Empty = integration disabled.
SGP_BASE_URL str https://api.safeguardprivacy.com Production endpoint. Staging environment: https://api.safeguardprivacy-demo.com.
SGP_ENFORCE_ON_DEAL_REQUEST bool false When true, RequestDealTool blocks Deal ID generation for unapproved vendors.
SGP_UNKNOWN_VENDOR_POLICY str block Behavior when the vendor is not in the buyer's SGP portfolio (HTTP 404).
SGP_CACHE_TTL_SECONDS int 900 Per-domain cache lifetime for approval lookups.

Behavior matrix

With enforcement on (SGP_ENFORCE_ON_DEAL_REQUEST=true and SGP_API_KEY set):

SGP response block (default) warn allow
iabBuyerAgentApproval: true Proceeds, banner Same Same
iabBuyerAgentApproval: false Blocked Blocked Blocked
404 (not onboarded in SGP) Blocked Proceeds, warning Proceeds silently
Transport error Fails closed Fails closed Fails closed
Product has no seller-domain field Blocked Blocked Blocked

An explicit iabBuyerAgentApproval: false is always fatal — the policy only governs the unknown-vendor case.

Zero-config compatibility

A default install (no SGP_API_KEY, no env overrides) produces exactly the same behavior as before this PR:

  • No SGP client is constructed
  • No HTTP calls to SafeGuard Privacy
  • No annotations in discovery output
  • RequestDealTool skips the gate entirely
  • SGPVendorApprovalTool is not instantiated or added to any agent's toolbox

All new constructor parameters are optional with backward-compatible defaults. No existing call sites require changes.

Documentation

  • New: docs/integration/safeguard-privacy.md — endpoint contract, config, behavior matrix, troubleshooting
  • Updated: docs/guides/configuration.md (SGP env var table), mkdocs.yml (nav entry), README.md (Key Features)

Test plan

  • 31 new unit tests (19 client + 12 gate and flow wiring)
  • Full suite green: 2704 passed, 41 skipped, 0 failed
  • Lint on our files clean: ruff check and ruff format --check pass for all SGP-authored code
  • Manual smoke: set SGP_API_KEY against a live SGP tenant, run a deal request against an approved vs. unapproved seller domain
Test coverage breakdown

Client (tests/unit/test_sgp_client.py)

  • Domain normalization: scheme, www, port, casing, whitespace (8 parametrized cases)
  • Success paths: single vendor, multi-domain single call, batch-of-10 chunking with 25 domains, dedupe
  • Unknown-vendor: HTTP 404 for whole batch, partial batch (SGP omits unknown domains)
  • Errors: 401 raises SGPAuthError, 400 raises SGPClientError, 503 raises SGPClientError with status code
  • Transport errors: real httpx.ConnectError wrapped via MockTransport
  • Cache: second lookup for same domain is a hit; 404 (unknown) also cached

Gate (tests/unit/test_sgp_gate.py)

  • No SGP client / sgp_enforce=False both bypass cleanly
  • Approved vendor proceeds with banner
  • Denied vendor is blocked
  • Unknown vendor: block / warn / allow each honored
  • Transport error fails closed when enforcing
  • Missing seller domain is blocked (cannot evaluate)
  • Invalid policy name raises ValueError at construction
  • BuyerDealFlow wires SGPVendorApprovalTool into the agent when SGP is configured, omits when not

CI status

Gate Status
ruff check src/ tests/ Clean on our files. Pre-existing errors remain in unrelated files on main.
ruff format --check src/ tests/ Clean on our files. Pre-existing format failures remain in unrelated files on main.
pytest tests/ --cov=src/ad_buyer Passing: 2704 passed, 41 skipped.
docker build -f infra/docker/Dockerfile . Pre-existing failure on main. Introduced in ab66e6f feat: add IaC deployment infrastructure. The runtime stage copies src/ and pyproject.toml but not README.md; hatchling's editable install then fails with OSError: Readme file does not exist: README.md. Reproducible from a clean checkout of main without these changes. Out of scope for this PR.

Daniel Cox added 2 commits April 22, 2026 16:01
Introduces an optional integration with the SafeGuard Privacy (SGP)
platform that lets buyers gate Deal ID generation on the
`iabBuyerAgentApproval` flag they maintain for each vendor in their
SGP tenant. The integration is off by default — when SGP_API_KEY is
empty the buyer agent behaves exactly as before.

New components
- SGPClient: async httpx client for the SGP buyer-agent-approval
  endpoint. Domain normalization, dedupe, batching to 10 per request,
  TTL cache, and full HTTP status handling. Transport errors are
  wrapped as SGPClientError so the deal-request gate fails closed.
- ApprovalRecord pydantic model mirroring IabBuyerAgentResource.
- SGPVendorApprovalTool: CrewAI tool wired into the Buyer Deal
  Specialist so the agent can consult approval status during product
  selection, not just at Deal ID generation. Class is SGP-prefixed to
  leave room for future vendor-approval sources.

Integration points
- DiscoverInventoryTool annotates each product row with APPROVED /
  NOT APPROVED / UNKNOWN when an SGPClient is provided. Discovery
  fails open on SGP transport errors so outages do not break browsing.
- RequestDealTool adds a pre-flight gate. When SGP_ENFORCE_ON_DEAL_REQUEST
  is true and an SGPClient is configured, Deal IDs are not issued for
  unapproved vendors. Unknown-vendor behavior is governed by
  SGP_UNKNOWN_VENDOR_POLICY (block | warn | allow; default block).
- BuyerDealFlow auto-instantiates the client from settings and logs a
  warning if enforcement is configured without an API key.

Configuration
- SGP_API_KEY, SGP_BASE_URL, SGP_ENFORCE_ON_DEAL_REQUEST,
  SGP_UNKNOWN_VENDOR_POLICY, SGP_CACHE_TTL_SECONDS. Defaults point at
  the production SGP endpoint; staging is noted inline.

Docs
- docs/integration/safeguard-privacy.md covers endpoint contract,
  config, behavior matrix, and troubleshooting. Added to mkdocs nav,
  the configuration guide, and the README Key Features section.

Tests
- 31 new unit tests covering the client (normalization, batching, all
  HTTP statuses, transport errors, caching), the gate (approved,
  denied, unknown policies, fail-closed on errors, missing seller
  domain), and flow-level tool wiring.
Scope is limited to lines introduced by this PR:
- Trailing newlines added to new files (W292)
- Optional[X] → X | None on our new type annotations (UP045)
- Split an over-long logger.warning line in discover_inventory.py (E501)
- Minor whitespace/formatting adjustments from `ruff format` on our five
  new files only

Pre-existing lint issues in files we also modified (unused imports,
datetime.timezone.utc → datetime.UTC, E501 on the pre-existing
RequestDealInput description, I001 import-group order caused by
upstream `events.*` placement) are intentionally left alone — they
are out of scope for this PR.

Tests: 2704 passed, 41 skipped.
The client calls a single endpoint on the SafeGuard Privacy platform:

```
GET /api/v1/integrations/iab/buyer-agent-approval?domain=a.com,b.com
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why does the buyer agent need to make the choices and status of their vendor approvals public? is this a public endpoint available to anyone that can query the buyer agent or is it private?

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.

2 participants