Conversation
…dentity store Postgres-backed BuyerAgentRegistry implementation for adopters who need durable commercial-identity gating without forking the SQLAlchemy reference example. Mirrors the design of adcp.signing.pg.PgReplayStore — caller-owned ConnectionPool, table-name validation, idempotent ``create_schema()``, raw DDL ships separately for migration tools (Alembic, Flyway, psql). * ``src/adcp/decisioning/pg/buyer_agent_registry.py`` — sync psycopg-pool implementation; the async BuyerAgentRegistry Protocol methods bridge via ``asyncio.to_thread`` so the framework's dispatch event loop stays responsive. * ``src/adcp/decisioning/pg/buyer_agent_registry.sql`` — canonical schema for migration tools. JSONB columns for billing_capabilities, default_terms, allowed_brands, ext. CHECK constraint on status. Partial index on api_key_id for the bearer-credential lookup path. Partial index on status WHERE status <> 'active' for admin tools. * Admin CRUD: upsert(), set_status(), delete(). Status validated against the framework's literal enum. * SQL injection / Unicode-homoglyph defenses on the table_name kwarg matching PgReplayStore's posture. * 15 conformance tests gated on ADCP_PG_TEST_URL — all 15 pass against postgres:16. CI's existing pg job (renamed pg-replay-store → pg-conformance) now runs them too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
May 3, 2026
) (#393) * fix(decisioning): rename Tier 2 codes to spec-conformant PERMISSION_DENIED + parity (#375) The Tier 2 commercial-identity gate previously raised four error codes absent from the AdCP spec's `error-code.json` 51-entry vocabulary: `AGENT_SUSPENDED`, `AGENT_BLOCKED`, `REQUEST_AUTH_UNRECOGNIZED_AGENT`, `INVALID_BILLING_MODEL`. The cross-tenant onboarding-oracle clamp in the spec requires the unrecognized-agent path and the recognized-but- denied path to be observably indistinguishable to an external attacker — distinct codes per status leak which `agent_url`s are onboarded with which sellers, enabling enumeration of commercial relationships. Code changes: * `_resolve_buyer_agent` raises `PERMISSION_DENIED` on all four denial paths. Recognized-but-denied paths (suspended / blocked) carry `details.scope="agent"` + `details.status`; unrecognized paths (registry miss, no credential, unknown status) OMIT `details` per the spec's omit-on-unestablished-identity rule. * All four paths share a single `_denied_message` constant so `error.message` is not a side channel. * `validate_billing_for_agent` raises `BILLING_NOT_PERMITTED_FOR_AGENT` with `details.rejected_billing` (required) and an optional `details.suggested_billing` (the alphabetically-first permitted mode). The full `permitted_billing` subset is no longer leaked — surfacing it on every rejection let a misconfigured buyer probe and exfiltrate the billing matrix. Recovery semantic: `PERMISSION_DENIED` is treated as `terminal` for the commercial-identity gate (resolution path is operator-onboarding, not request-side correction). This overrides the spec's `enumMetadata` default of `correctable` for the code; the override is documented in `_resolve_buyer_agent`'s docstring. Spec status of `BILLING_NOT_PERMITTED_FOR_AGENT`: this code is not in the 51-entry standard enum and lacks the `X_` vendor prefix required by `vendor-error-codes.json`. The user-facing issue (#375) specifies this code; raising the spec conflict in the PR body for sign-off. Tests: * `tests/test_tier2_spec_conformance.py` — pins the wire shape: - All four removed codes are no longer raised. - Recognized-but-denied paths carry `scope` + `status` in details. - Unrecognized paths omit `details`. - Billing path carries `rejected_billing` + optional `suggested_billing`; never leaks `permitted_billing` or `agent_url`. * Existing `tests/test_decisioning_buyer_agent_dispatch.py` and `tests/test_buyer_agent_registry.py` updated to the new wire shape. Deferred to follow-up: The latency / headers / side-effects / observability parity contract between the four denial paths is a larger dispatch-path refactor (single emit point, deliberate latency padding, identical audit / metric side-effects) that does not fit in this PR. Tracking issue to follow. The eager-raise pattern in `_resolve_buyer_agent` still completes the unrecognized path on a different code path than the recognized one — this PR closes the wire-code mismatch only. Backwards compatibility: this is a behavior change for adopters who match on the old codes. The codes were just introduced in PRs #364 / #372, so blast radius is small. Adopters need to migrate to matching on `code == "PERMISSION_DENIED"` + reading `details.scope` and `details.status` to discriminate recognized-but-denied paths. Closes #375 (rename portion). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(decisioning): recovery=correctable per spec enumMetadata (PR #393 fix-pack) Flip wire-level recovery from terminal to correctable on the four PERMISSION_DENIED denial paths in _resolve_buyer_agent (handler.py) and on BILLING_NOT_PERMITTED_FOR_AGENT in validate_billing_for_agent (registry.py) to match the spec's enumMetadata defaults. The details.scope == "agent" discriminator (when present) is the signal callers surface to a human operator rather than auto-retry — that semantic stays in details, not in the recovery hint. Also annotates docs/proposals/v3-identity-bundle-design.md with a status note pointing readers to the test suite for current behavior; the proposal still uses the pre-rename code names but is preserved as a historical doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
adcp.decisioning.pg.PgBuyerAgentRegistry— durable Postgres-backed BuyerAgentRegistry for adopters who need real commercial-identity storage without forking the SQLAlchemy reference example.PgReplayStoredesign: caller-ownedConnectionPool, table-name validation, idempotentcreate_schema(), separate.sqlDDL for migration tools.upsert(),set_status(),delete().ADCP_PG_TEST_URL; CI's existing pg job (renamedpg-replay-store→pg-conformance) runs them. All 15 pass against postgres:16 locally.Test plan
pytest tests/(no PG): 3054 passed, 18 skipped, 0 failedADCP_PG_TEST_URL=... pytest tests/conformance/decisioning/: 15/15 pass against postgres:16pg-conformancejob runs the new test filePG_AVAILABLE=False, ImportError with install hint at construction)🤖 Generated with Claude Code