spec(accounts): buyer-agent identity model + billing error-code coverage for sync_accounts#3831
Merged
Merged
Conversation
…age for sync_accounts Adds the spec/doc backing that adcp-client #1269 (BuyerAgentRegistry) needs to land without inventing wire behavior. Registers four billing/account error codes referenced by sync_accounts but missing from the canonical enum, plus one new code for the per-buyer-agent commercial gate (BILLING_NOT_PERMITTED_FOR_AGENT). Adds two error-details schemas locking the recovery shapes — the per-agent schema clamps additionalProperties: false to prevent full-subset commercial-state disclosure in a single probe; the per-supported schema requires omitting scope on the unauthenticated path so the hint cannot itself become a per-account oracle. New "Buyer-agent identity" section in accounts-and-agents.mdx names the two-layer identity model (signed-request agent_url derivation or seller credential map, plus brand.json/authorized_operators) the spec already implies. security.mdx tightens agent_url derivation as the agents[] entry whose jwks_uri resolved the keyid — not a JWK/JWS/envelope claim — and forbids introducing an envelope-side buyer_agent_url on the bearer/OAuth path. Tier 3 follow-up (conformance fixtures, Python BrandAuthorizationResolver naming alignment) tracked as #3828. Reviewed by ad-tech-protocol-expert, adtech-product-expert, security-reviewer, docs-expert, and code-reviewer; all MUST FIX and SHOULD FIX findings addressed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build:schemas script regenerates static/schemas/source/index.json from package.json. Pre-push hook caught the drift between the registry's adcp_version (3.0.4) and the package version (3.0.3); rerunning the build aligns them and populates published_version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 tasks
This was referenced May 2, 2026
bokelley
added a commit
to adcontextprotocol/adcp-client
that referenced
this pull request
May 2, 2026
…1269) (#1293) * feat(server): BuyerAgentRegistry Phase 1 Stage 1 — types + factory functions (#1269) Adds the durable buyer-agent identity surface: `BuyerAgent` record, kind- discriminated `AdcpCredential`, and `BuyerAgentRegistry` with three factory functions (`signingOnly`, `bearerOnly`, `mixed`) that encode the implementer posture at construction. The credential answers "who signed?" / "who holds this token?"; the `BuyerAgent` answers "who is this counterparty in our books?" — a durable commercial relationship distinct from the per-request credential. Same pattern as an SSP's `buyer_id` row keyed to a DSP regardless of credential type. This stage ships the types and factories only. Stage 2 wires the resolve seam in the dispatcher; Stage 3 ships the `ResolvedAuthInfo` migration shim; Stage 4 adds status enforcement / multi-credential conflict / log redaction; Stage 5 adds the caching decorator and conformance fixtures. All Phase 1 stages are no-wire-emission and ship in 3.0.x. Phase 2 (#1292) — framework-level `billing_capability` enforcement and the AdCP-3.1 error-code emission from adcontextprotocol/adcp#3831 — is gated on the SDK's 3.1 cutover. 19 unit tests cover factory contracts (kind-routing, null on unhonored kinds, construction-time validation, resolver-throw propagation), `BuyerAgent` shape semantics, and `BillingMode` Set-membership. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(buyer-agent): tighten bearerOnly foot-gun + reject malformed http_sig credentials Three fixes from expert review on PR #1293 (no blockers, all in-scope for Stage 1): 1. Doc-clarify that the legacy `ResolvedAuthInfo` synthesis is Stage 3's job, not Stage 1's. The previous comment promised behavior the code didn't implement. 2. `ResolveBuyerAgentByCredential` JSDoc now requires implementations to switch on `credential.kind` and reject unrecognized kinds. Closes the foot-gun where a `WHERE token = $1` lookup against an api-key table could mis-resolve when handed an `http_sig` credential whose `keyid` collides with an existing api-key value — the credential variants share no key namespace. 3. Runtime guard against malformed `http_sig` credentials. A misbehaving authenticator could produce `kind: 'http_sig'` without a non-empty `agent_url`; without the guard, `signingOnly` and `mixed` would silently pass `undefined` to the resolver. `signingOnly` now returns null on malformed; `mixed` rejects rather than falling through to the bearer path (which would make `mixed` strictly weaker than `signingOnly` — a malformed http_sig could otherwise bypass signed- path enforcement by routing through the bearer table). Test coverage extended to 22 cases (+3): malformed-http_sig refusal in both `signingOnly` and `mixed`; empty-agent_url refusal. Deferred items (Stages 2, 3, Phase 2) recorded on #1269 / #1292. 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 was referenced May 2, 2026
Merged
bokelley
added a commit
that referenced
this pull request
May 2, 2026
…ionResolver naming guidance Tier-3 follow-up to #3828 / #3831 (BuyerAgentRegistry spec backing). New universal storyboard `billing-gate-dispatch` exercises the two-gate dispatch contract on `sync_accounts.billing` rejection: seller-wide capability gate (`BILLING_NOT_SUPPORTED` with `error.details.scope: "capability"` and supported_billing echo) and per-buyer-agent commercial-relationship gate (`BILLING_NOT_PERMITTED_FOR_AGENT` with the clamped rejected_billing + optional suggested_billing shape). The recover phase chains off the per-agent reject under the same idempotency_key (matching the spec contract that recovery is a retry of the same upsert with a different `billing` value), capturing suggested_billing into context so the seller's nominated fallback is what actually provisions. Negative-shape assertions actively forbid permitted_billing, rate_card, payment_terms, credit_limit, billing_entity, and account_id from leaking through error.details — the per-agent commercial-state oracles that error-details/billing-not-permitted-for-agent.json's additionalProperties: false is meant to close. New test kit `test-kits/billing-gate-runner.yaml` documents the coordination contract: account.unsupported_billing_probe gates the capability phase; commercial_relationship gates the per-agent phases. Both are precondition-gated rather than capability-gated — sellers without the precondition grade not_applicable on those phases. Conformance and compliance-catalog docs updated; doc-parity lint clean. SDK naming guidance added to accounts-and-agents.mdx Buyer-agent identity section: SDKs SHOULD name the brand-operator authorization Protocol BrandAuthorizationResolver (after the file consulted, brand.json/authorized_operators), NOT AdagentsResolver (adagents.json is publisher-side and models a different relationship). Cross-coordination filed as adcp-client-python#346. Reviewed by ad-tech-protocol-expert and nodejs-testing-expert; all MUST FIX findings addressed (idempotency-key reuse, test-kit fields declared, suggested_billing not strict-pinned to operator), plus key SHOULD FIX findings (action/status/recovery assertions, response_schema on error responses, SDK naming softened to SHOULD). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
May 2, 2026
…ERMITTED_FOR_AGENT gates (#3851) * feat(training-agent): implement BILLING_NOT_SUPPORTED + BILLING_NOT_PERMITTED_FOR_AGENT gates Reference implementation of the two billing gates from #3831 (BuyerAgentRegistry spec backing). Lands first on the legacy /mcp route via handleSyncAccounts; v6 per-tenant routes have no accounts.upsert yet, so wiring there follows in a separate PR. Capability gate: validates input.billing against SUPPORTED_BILLINGS and emits BILLING_NOT_SUPPORTED with error.details.scope: "capability" and an echoed supported_billing list per error-details/billing-not-supported.json. Also fixes the stale advertisement (was 'agent'-only, handler accepted all three) so get_adcp_capabilities and the gate agree. Per-buyer-agent gate: new commercial-relationships.ts module maps the authenticated principal to a commercial-relationship enum (passthrough_only or agent_billable). Two demo bearer prefixes recognized — demo-billing-passthrough-* and demo-billing-agent-billable-* — matching the existing DEMO_TEST_KIT_KEY_PATTERN authenticator. Emits BILLING_NOT_PERMITTED_FOR_AGENT with the clamped details shape per error-details/billing-not-permitted-for-agent.json (additionalProperties: false — rejected_billing + suggested_billing only). Strict toEqual assertion in the test enforces the clamp at the wire level. Bright-line uniform-response rule: principals without an onboarded record return undefined from getCommercialRelationship and fall through to the seller-wide capability gate only — preventing the per-agent code from acting as an onboarding oracle for unrecognized callers. tenantId reserved on getCommercialRelationship signature so the v6 follow-up doesn't force a signature break across call sites. 20 unit tests pass (14 existing + 6 new): capability-gate rejection, passthrough rejection of agent and advertiser, autonomous recovery via operator, agent-billable acceptance of all three values, uniform-response for unrecognized principal, and dry_run does not suppress billing-gate rejections. Reviewed by code-reviewer; SHOULD FIX (tenantId plumbing) and both NICE TO HAVE items (toEqual clamp assertion, dry_run test) addressed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * review: export SUPPORTED_BILLINGS, document spoofable test principals, multi-account batching test Addresses SHOULD FIX findings from second-pass review of #3851: - Export SUPPORTED_BILLINGS from account-handlers.ts and import it in task-handlers.ts so the gate constant and the supported_billing advertisement are mechanically locked rather than comment-coupled. - Document the security posture of the static:demo:* principal namespace explicitly in commercial-relationships.ts: spoofable by design (any caller with the bearer self-stamps the principal — fine for conformance runners deterministically targeting each branch), with a forward-looking guard that future production billing stores must not key on this namespace. - Add a multi-account batching test: [passing, rejected, passing] sequence proves the per-account `continue` loop does not poison passing siblings and result ordering matches request ordering — closes the test surface around the load-bearing per-account semantics of sync_accounts. 22 tests pass; full server unit suite (3088 tests) clean except for one pre-existing flaky LLM test in addie-router.test.ts unrelated to this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(training-agent): wire bearer→principal in standalone for local gate testing Mirrors server/src/training-agent/index.ts:84 — any bearer matching the demo-test-kit pattern stamps `static:demo:<token>` as the principal. Lets the standalone runner exercise the per-buyer-agent billing gate locally over real HTTP without spinning up the full server's auth chain. Used to validate #3839 against #3851 end-to-end: npx tsx server/src/training-agent/standalone.ts & curl -X POST http://localhost:4100/mcp \\ -H 'Authorization: Bearer demo-billing-passthrough-v1' \\ -d '{"jsonrpc":"2.0",...,"params":{"name":"sync_accounts",...}}' Confirmed end-to-end: the auth chain → principal → gate path produces BILLING_NOT_PERMITTED_FOR_AGENT with the right error.details shape on the wire (not just in the in-process simulateCallTool harness). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
May 2, 2026
…ionResolver naming guidance (#3839) * spec(accounts): billing-gate conformance storyboard + BrandAuthorizationResolver naming guidance Tier-3 follow-up to #3828 / #3831 (BuyerAgentRegistry spec backing). New universal storyboard `billing-gate-dispatch` exercises the two-gate dispatch contract on `sync_accounts.billing` rejection: seller-wide capability gate (`BILLING_NOT_SUPPORTED` with `error.details.scope: "capability"` and supported_billing echo) and per-buyer-agent commercial-relationship gate (`BILLING_NOT_PERMITTED_FOR_AGENT` with the clamped rejected_billing + optional suggested_billing shape). The recover phase chains off the per-agent reject under the same idempotency_key (matching the spec contract that recovery is a retry of the same upsert with a different `billing` value), capturing suggested_billing into context so the seller's nominated fallback is what actually provisions. Negative-shape assertions actively forbid permitted_billing, rate_card, payment_terms, credit_limit, billing_entity, and account_id from leaking through error.details — the per-agent commercial-state oracles that error-details/billing-not-permitted-for-agent.json's additionalProperties: false is meant to close. New test kit `test-kits/billing-gate-runner.yaml` documents the coordination contract: account.unsupported_billing_probe gates the capability phase; commercial_relationship gates the per-agent phases. Both are precondition-gated rather than capability-gated — sellers without the precondition grade not_applicable on those phases. Conformance and compliance-catalog docs updated; doc-parity lint clean. SDK naming guidance added to accounts-and-agents.mdx Buyer-agent identity section: SDKs SHOULD name the brand-operator authorization Protocol BrandAuthorizationResolver (after the file consulted, brand.json/authorized_operators), NOT AdagentsResolver (adagents.json is publisher-side and models a different relationship). Cross-coordination filed as adcp-client-python#346. Reviewed by ad-tech-protocol-expert and nodejs-testing-expert; all MUST FIX findings addressed (idempotency-key reuse, test-kit fields declared, suggested_billing not strict-pinned to operator), plus key SHOULD FIX findings (action/status/recovery assertions, response_schema on error responses, SDK naming softened to SHOULD). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(storyboard): validate billing-gate-dispatch end-to-end against training-agent (#3851) Three bugs surfaced by running the storyboard against the running training-agent reference implementation. Lint couldn't catch any of them — exactly the brittleness the validation was meant to surface before shipping cold. 1. check: error_code does not accept a `path` parameter for per-account error extraction. The runner pulls the code from default paths (adcp_error.code / errors[0].code at root). For sync_accounts' per-account errors at accounts[0].errors[0].code, switch to check: field_value with the explicit path. Two occurrences fixed. 2. expect_error: true requires a transport-level error marker (MCP isError / A2A failed). sync_accounts produces transport-level success with per-account errors inside the success envelope — accounts[].errors[] is the per-account-error shape, not transport failure. Removed expect_error from both gate phases with explanatory comments. 3. Idempotency-key reuse across reject/recover phases produced IDEMPOTENCY_CONFLICT. The recover phase submits a different payload (different `billing` value), and per error-handling.mdx, same key + different payload within the replay window MUST reject. Recover phase now uses a fresh idempotency_key with a distinct `#billing_gate_dispatch_passthrough_recover` stability tag. Also corrected three places in the narrative + storyboard structure that previously asserted the wrong "retry under same key" semantics. Plus a runner-side documentation gap: today's storyboard runner does not auto-extract auth.api_key from the test kit. Callers pass it explicitly via --auth. The test-kit comment now spells out the canonical CLI invocation: npx adcp storyboard run <agent-url> \\ --auth demo-billing-passthrough-v1 \\ --file static/compliance/source/universal/billing-gate-dispatch.yaml Test-kit api_key updated to demo-billing-passthrough-v1 to match the training-agent's recognized prefix family. commercial_relationship: "passthrough_only" uncommented as the default-active precondition. Storyboard now passes 3/3 strict assertions against the training-agent per-agent-gate flow (capability_discovery + per_agent_gate_reject + per_agent_gate_recover); capability_gate phase grades not_applicable correctly when the seller advertises all three billing values. 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 was referenced May 2, 2026
Closed
Closed
bokelley
added a commit
that referenced
this pull request
May 2, 2026
…-scope enum (closes #3883) (#3887) * spec(errors): register agent-permission-denied details + shared error-scope enum (closes #3883) - New error-details/agent-permission-denied.json with scope:agent + oneOf exclusivity on status:[suspended,blocked] vs reason:[sandbox_only], additionalProperties:false. Per-agent commercial gate recovery shape so cross-language SDKs can dispatch on the discriminator without parsing prose. - New enums/error-scope.json registering the shared discriminator vocabulary (capability | account | agent). Each per-shape error-details schema constrains its own subset; this enum is the single source of truth for the union. - error-code.json PERMISSION_DENIED description references the new shape and carries the cross-tenant onboarding oracle clamp (scope:agent only on established-identity path; full channel-coverage rules in error-handling.mdx). - billing-not-supported.json scope description cross-references the shared vocabulary. - error-handling.mdx adds a Per-Agent Authorization Gate subsection with the comprehensive *_NOT_FOUND-style channel coverage list (response shape, HTTP/A2A/MCP status, headers, side effects, observability, latency parity, retry-counter side channel, cache warmth) plus dispatch example. Builds on #3831 (BILLING_NOT_PERMITTED_FOR_AGENT precedent + cross-tenant clamp). Cross-references #3871 (open follow-up to elevate suspended/blocked to dedicated error codes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(errors): fix kebab→snake in dispatch comment + clarify wire-level recovery field on per-agent gate DX/protocol expert review of #3887 caught two issues: - Stale `'sandbox-only'` (kebab) in the JS dispatch example comment after the wire enum value was renamed to `'sandbox_only'` (snake) for cross-language SDK compat. SDK authors copy-pasting the comment would write a never-matches branch. - Envelope example shows `recovery: "correctable"` (matches registered enumMetadata) while prose says terminal-pending-onboarding — readers who branch on `recovery` would auto-retry against the no-retry MUST. Added prose adjacent to the envelope clarifying that buyer agents MUST branch on `details.scope` itself, not the wire-level `recovery` field, and noting that conditional-recovery support is a follow-up (either dedicated codes per #3871 or an enumMetadata extension). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
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
BILLING_NOT_PERMITTED_FOR_AGENT) for the per-buyer-agent commercial gate, plus twoerror-details/schemas locking the recovery shapes.accounts-and-agents.mdxnames the two-layer identity model the spec already implies (signed-requestagent_urlderivation OR seller credential map, plusbrand.json/authorized_operators).security.mdx"Agent identity" tightensagent_urlderivation as theagents[]entry whosejwks_uriresolved thekeyid— not a JWK/JWS/envelope claim — and forbids introducing an envelope-sidebuyer_agent_urlon the bearer/OAuth path. Tier-3 follow-up tracked as Tier 3 follow-ups for #1269 BuyerAgentRegistry: conformance fixtures + cross-language naming #3828.Why these changes
Reviewing #1269 surfaced three classes of missing spec backing the JS SDK would otherwise paper over:
BILLING_NOT_SUPPORTEDwas referenced insync_accounts.mdxbut never registered in the canonicalerror-code.jsonenum. Same forPAYMENT_TERMS_NOT_SUPPORTEDandBRAND_REQUIRED. The doc table also referencedPAYMENT_REQUIREDwhere the registered code isACCOUNT_PAYMENT_REQUIRED— corrected.BILLING_NOT_PERMITTED_FOR_AGENTis the new code, distinct fromBILLING_NOT_SUPPORTEDso agents can dispatch on autonomous-retry vs surface-to-human without parsing prose.agent_urlis established.security.mdxmentionedverified_signer.agent_urlin the request context but didn't name the derivation. Spelling it out closes a loophole where an SDK could surface a buyer-assertedagent_urlfrom the envelope.Security clamps applied during review
The change went through ad-tech-protocol-expert, adtech-product-expert, security-reviewer, docs-expert, and code-reviewer. Notable hardenings:
BILLING_NOT_PERMITTED_FOR_AGENTonly when agent identity has been established via signed-request derivation or a credential-to-agent mapping; in all other cases (including bearer credentials not mapped to a specific agent record) sellers MUST returnBILLING_NOT_SUPPORTEDand MUST omiterror.details.scope. Same shape as the*_NOT_FOUNDuniform-response rule.error-details/billing-not-permitted-for-agent.jsonisadditionalProperties: false—rejected_billing(echo) plus optional singlesuggested_billingretry value. The schema description normatively forbids carrying the agent's full permitted-billing subset, rate cards, payment terms, credit limit, billing entity, or any other per-agent commercial state.BILLING_NOT_PERMITTED_FOR_AGENTon the suggested fallback MUST surface to a human, not loop.verify_agent.agent_urlaside insecurity.mdxkeeps the new prohibition from being read as banning legitimate buyer-asserted verifier references.Files
static/schemas/source/enums/error-code.json— 4 new codes, dual-surface (enumDescriptions + enumMetadata).static/schemas/source/error-details/billing-not-permitted-for-agent.json— new. Clamped recovery shape.static/schemas/source/error-details/billing-not-supported.json— new.scopediscriminator + optionalsupported_billingecho, with the omit-on-unauthenticated-path rule.docs/accounts/tasks/sync_accounts.mdx— normative line onbillingtwo-gate model; cross-linked error rows;PAYMENT_REQUIREDtypo fix.docs/building/implementation/error-handling.mdx— new "Billing and Account Setup" subsection with the uniform-response rule, clamp normative line, one-shot-retry rule, side-by-side recovery snippet, example envelope.docs/building/integration/accounts-and-agents.mdx— new "Buyer-agent identity" section.docs/building/implementation/security.mdx—agent_urlderivation paragraph + bearer/OAuth tightening + verify_agent aside.Tier-3 follow-up
#3828 tracks: (a) conformance fixtures upstream for the per-buyer-agent billing gate so other-language SDKs inherit the same enforcement contract, (b) cross-language naming alignment with the in-flight Python
v3-identity-bundle-design.md(spec-side:BrandAuthorizationResolver, notAdagentsResolver—adagents.jsonis publisher-side and models a different relationship).Test plan
node scripts/lint-error-codes.cjs— clean for new codes (3 pre-existing storyboard errors onBRAND_NOT_FOUNDare unrelated).npm run build:schemas— regenerates dist; manifest count updates from new codes.🤖 Generated with Claude Code