From 2c3e9dfbb751d1343fdc0889c2f89b3391654608 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 2 May 2026 20:53:46 -0400 Subject: [PATCH] fix(decisioning): strip permitted_billing leak from INVALID_BILLING_MODEL details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The billing-not-permitted-for-agent details schema forbids carrying the agent's full permitted-billing subset on the wire. Drop the field from `details` and remove the enumerated modes from the human message; the recognized caller already knows its own capabilities. Partial fix for #375 — the code-rename portion (AGENT_SUSPENDED / AGENT_BLOCKED / REQUEST_AUTH_UNRECOGNIZED_AGENT / INVALID_BILLING_MODEL → spec-conformant codes) is deferred behind v3.1 of the AdCP spec, which needs to add AGENT_SUSPENDED/AGENT_BLOCKED/BILLING_NOT_PERMITTED codes with proper recovery semantics before the SDK can switch. Refs #375 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/adcp/decisioning/registry.py | 19 +++++++++++-------- tests/test_buyer_agent_registry.py | 11 ++++++++++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/adcp/decisioning/registry.py b/src/adcp/decisioning/registry.py index 6e697a8ca..904141a20 100644 --- a/src/adcp/decisioning/registry.py +++ b/src/adcp/decisioning/registry.py @@ -346,24 +346,27 @@ def validate_billing_for_agent( # close on import-load order). from adcp.decisioning.types import AdcpError + # The spec's billing-not-permitted-for-agent details schema forbids + # carrying the agent's full permitted-billing subset on the wire — + # `details` MUST NOT enumerate the permitted modes. The human + # message similarly drops the permitted set; the requested mode is + # the only safe-to-echo value. raise AdcpError( "INVALID_BILLING_MODEL", message=( f"Buyer agent '{agent.agent_url}' is not authorized for " - f"billing={requested_billing!r}; permitted modes are " - f"{sorted(agent.billing_capabilities)!r}. Common cause: " - "this agent has no payments relationship with the seller " - "(passthrough only) — accounts under this agent must be " - "operator-billed. Sellers extending the agent's billing " - "capabilities update the BuyerAgent.billing_capabilities " - "frozenset in their durable store." + f"billing={requested_billing!r}. Common cause: this agent " + "has no payments relationship with the seller (passthrough " + "only) — accounts under this agent must be operator-billed. " + "Sellers extending the agent's billing capabilities update " + "the BuyerAgent.billing_capabilities frozenset in their " + "durable store." ), field="billing", recovery="terminal", details={ "agent_url": agent.agent_url, "requested_billing": requested_billing, - "permitted_billing": sorted(agent.billing_capabilities), }, ) diff --git a/tests/test_buyer_agent_registry.py b/tests/test_buyer_agent_registry.py index fa00e4933..46dd6f8bb 100644 --- a/tests/test_buyer_agent_registry.py +++ b/tests/test_buyer_agent_registry.py @@ -298,7 +298,16 @@ def test_validate_billing_rejects_passthrough_only_with_agent_billing() -> None: details = exc.value.details assert details["agent_url"] == "https://passthrough/" assert details["requested_billing"] == "agent" - assert details["permitted_billing"] == ["operator"] + # Spec forbids leaking the permitted-billing subset on the wire + # (billing-not-permitted-for-agent details schema). The recognized + # caller already knows its own capabilities; structured echo would + # be redundant and is non-conformant. + assert "permitted_billing" not in details + # Human message must not enumerate the permitted set either — + # check no list-like rendering leaks (the message may still + # reference billing modes contextually, e.g. "operator-billed"). + assert "['operator']" not in str(exc.value) + assert "permitted modes" not in str(exc.value) def test_validate_billing_rejects_advertiser_when_not_in_capabilities() -> None: