Skip to content

Commit d644048

Browse files
authored
Release 3.0.0: A2A 1.0 agent cards, UCP 2026-08-25 profiles, MPP payment offers (#148)
## Summary Mirrors node-commerce 3.0.0 (agentscore/node-commerce#141). Requested by Varun after a report that our A2A cards were out of date; a sweep of every hand-written format found two more. - **A2A agent card.** The card declared protocol 1.0 but carried the 0.3 shape. `to_dict()` now emits A2A 1.0 (`specification/a2a.proto` at v1.0.1): the required `supportedInterfaces[]` (url, protocolBinding, protocolVersion), the extended-card flag in `capabilities.extendedAgentCard`, and `securityRequirements` in place of `security` on the card and on skills. `build_a2a_agent_card` keeps `url`, `preferred_transport`, `additional_interfaces` and `security` as inputs. The UCP extension URI moves to 2026-08-25. - **UCP profile.** UCP 2026-08-25 removed `signing_keys` and made `keys` (a JWK Set) the one canonical field. The profile publishes `keys` at version 2026-08-25; `build_ucp_profile(keys=...)`, with `signing_keys` still accepted. `supported_versions` is no longer published. - **MPP payment discovery.** `x-payment-info` now also carries MPP's `offers[]` (integer amount in the rail's smallest unit, `None` when dynamic) beside the x402scan `price` and `protocols`. New `offers_from`. - **Dependency sweep, in the same release.** Relocked (`markupsafe` 3.0.4, `tzdata` 2026.5) and the uv CLI workflow input moves to 0.12.23. The capped requirements (`x402`, `pympp`, `redis`, `cdp-sdk`, `joserfc`, `httpx`, `stripe`) all lock at PyPI latest, no prerelease is locked, and OSV over `uv.lock` (157 packages) is clean. - **A real guard.** `tests/test_a2a_canonical.py` parses the built card as the official A2A `AgentCard` protobuf from `a2a-sdk` (added as a dev dependency only), which refuses any field the spec does not define, plus the negative case on a 0.3-shaped card. ## Type of change - [ ] Bug fix (no breaking change) - [ ] New feature (no breaking change) - [x] Breaking change (existing callers must update) - [ ] Docs, tests, or internal maintenance only ## Public API - Card JSON: `url`, `preferredTransport`, `protocolVersion`, `additionalInterfaces`, `supportsAuthenticatedExtendedCard`, `security` removed; `supportedInterfaces`, `securityRequirements` added. - `build_a2a_agent_card`: `state_transition_history` and `supports_authenticated_extended_card` removed; `extended_agent_card` added. `A2AAgentCardCapabilities` follows the same rename. - `UCPProfile.signing_keys` becomes `keys`; `build_ucp_profile(keys=...)`, `signing_keys` still accepted. - `well_known_ucp_url` on `build_signed_ucp_response` and the `mount_ucp_routes_*` methods becomes optional and is ignored. - New exports: `offers_from`, `to_security_requirements`. Version 3.0.0. ## Test plan - Updated the A2A and UCP tests for the new shapes, including a test that none of the removed 0.3 fields appear. - New: MPP offers per rail through `x_payment_info_from_checkout`, and the protobuf guard with its negative case. - `uv run ruff check .`, `uv run ruff format --check .`, `uv run ty check agentscore_commerce/`, `uv run vulture . --min-confidence 80 --exclude .venv`, `uv run pytest tests` (1891 passed, coverage 95.41% against the 95% gate). What this deliberately does not do: release the package or move core's store, which takes 3.0.0 separately. ## Checklist - [x] Tests cover the new behavior, and the suite passes locally - [x] Lint, format, and type checks pass - [x] Docs and README examples updated if the public surface changed - [x] No secrets, credentials, or personal data in the diff or the tests
1 parent ec1c141 commit d644048

19 files changed

Lines changed: 652 additions & 184 deletions

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Install uv
2525
uses: astral-sh/setup-uv@v10.2.0
2626
with:
27-
version: "0.12.22"
27+
version: "0.12.23"
2828

2929
- name: Set up Python
3030
run: uv python install 3.12
@@ -71,7 +71,7 @@ jobs:
7171
- name: Install uv
7272
uses: astral-sh/setup-uv@v10.2.0
7373
with:
74-
version: "0.12.22"
74+
version: "0.12.23"
7575

7676
- name: Set up Python
7777
run: uv python install ${{ matrix.python }}

‎.github/workflows/publish.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121

2222
- uses: astral-sh/setup-uv@v10.2.0
2323
with:
24-
version: "0.12.22"
24+
version: "0.12.23"
2525

2626
- name: Set version from tag
2727
run: sed -i "s/^version = .*/version = \"${GITHUB_REF_NAME#v}\"/" pyproject.toml

‎.github/workflows/security.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ jobs:
4141
- uses: useblacksmith/checkout@v1
4242
- uses: astral-sh/setup-uv@v10.2.0
4343
with:
44-
version: "0.12.22"
44+
version: "0.12.23"
4545
- uses: actions/setup-python@v7
4646
with:
4747
python-version: "3.13"

‎README.md‎

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ checkout = Checkout(
177177
checkout.mount_ucp_routes_fastapi(
178178
app,
179179
name="Merchant",
180-
well_known_ucp_url="https://merchant.example/.well-known/ucp",
181180
services=default_a2a_services(agent_card_url="https://merchant.example/.well-known/agent-card.json"),
182181
signing_kid="merchant-2026-05",
183182
)
@@ -340,16 +339,16 @@ card = build_a2a_agent_card(
340339

341340
# Google Universal Commerce Protocol. Publish at /.well-known/ucp.
342341
# Output shape: {"ucp": {"version", "services", "capabilities",
343-
# "payment_handlers", "name?", "supported_versions?"}, "signing_keys": [...]}
342+
# "payment_handlers", "name?", "supported_versions?"}, "keys": [...]}
344343
# , services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
345344
# service / capability / handler name (UCP spec §3 + §6).
346345
profile = build_ucp_profile(
347346
name="My Service",
348347
services={
349348
"dev.ucp.shopping": [
350349
UCPServiceBinding(
351-
version="2026-04-08",
352-
spec="https://ucp.dev/2026-04-08/specification/overview",
350+
version="2026-08-25",
351+
spec="https://ucp.dev/2026-08-25/specification/overview",
353352
transport="mcp",
354353
endpoint=f"{base_url}/api/ucp/mcp",
355354
schema="https://ucp.dev/services/shopping/mcp.openrpc.json",
@@ -361,7 +360,7 @@ profile = build_ucp_profile(
361360
**x402_payment_handler(networks=[X402BaseRailSpec(recipient=BASE_ADDR)]),
362361
**stripe_spt_payment_handler(spec=StripeRailSpec(profile_id="profile_5xKvNqM9BaH")),
363362
},
364-
signing_keys=[UCPSigningKey(kid="me", kty="EC", alg="ES256")],
363+
keys=[UCPSigningKey(kid="me", kty="EC", alg="ES256")],
365364
# Optional: declare merchant gate policy as an `com.agentscore.identity` capability
366365
# binding inside the public profile. Static policy declaration only, no per-operator
367366
# claims. Per-operator identity attestation flows through the AP2 risk-signal endpoint.
@@ -395,7 +394,7 @@ profile = build_ucp_profile(
395394
name="My Service",
396395
services={...},
397396
payment_handlers={...},
398-
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
397+
keys=[UCPSigningKey.from_jwk(key.public_jwk)],
399398
)
400399
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=key.public_jwk["kid"], alg="EdDSA")
401400
jwks = build_jwks_response([key.public_jwk])

‎agentscore_commerce/checkout.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,7 +1507,7 @@ def _build_ucp_resp(
15071507
request_headers: Mapping[str, str],
15081508
*,
15091509
name: str,
1510-
well_known_ucp_url: str,
1510+
well_known_ucp_url: str | None = None,
15111511
services: dict[str, Any],
15121512
signing_kid: str,
15131513
agentscore_gate: Any,
@@ -1539,7 +1539,7 @@ def mount_ucp_routes_fastapi(
15391539
app: Any,
15401540
*,
15411541
name: str,
1542-
well_known_ucp_url: str,
1542+
well_known_ucp_url: str | None = None,
15431543
services: dict[str, Any],
15441544
signing_kid: str = "merchant-default",
15451545
agentscore_gate: Any = None,
@@ -1587,7 +1587,7 @@ def mount_ucp_routes_flask(
15871587
app: Any,
15881588
*,
15891589
name: str,
1590-
well_known_ucp_url: str,
1590+
well_known_ucp_url: str | None = None,
15911591
services: dict[str, Any],
15921592
signing_kid: str = "merchant-default",
15931593
agentscore_gate: Any = None,
@@ -1640,7 +1640,7 @@ def mount_ucp_routes_django(
16401640
urlpatterns: list[Any],
16411641
*,
16421642
name: str,
1643-
well_known_ucp_url: str,
1643+
well_known_ucp_url: str | None = None,
16441644
services: dict[str, Any],
16451645
signing_kid: str = "merchant-default",
16461646
agentscore_gate: Any = None,
@@ -1686,7 +1686,7 @@ def mount_ucp_routes_aiohttp(
16861686
app: Any,
16871687
*,
16881688
name: str,
1689-
well_known_ucp_url: str,
1689+
well_known_ucp_url: str | None = None,
16901690
services: dict[str, Any],
16911691
signing_kid: str = "merchant-default",
16921692
agentscore_gate: Any = None,
@@ -1724,7 +1724,7 @@ def mount_ucp_routes_sanic(
17241724
app: Any,
17251725
*,
17261726
name: str,
1727-
well_known_ucp_url: str,
1727+
well_known_ucp_url: str | None = None,
17281728
services: dict[str, Any],
17291729
signing_kid: str = "merchant-default",
17301730
agentscore_gate: Any = None,

‎agentscore_commerce/discovery/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
agentscore_openapi_snippets,
2828
agentscore_payment_required_schema,
2929
agentscore_security_schemes,
30+
offers_from,
3031
siwx_security_scheme,
3132
x_guidance_extension,
3233
x_payment_info_extension,
@@ -129,6 +130,7 @@
129130
"is_discovery_probe_request",
130131
"llms_txt_identity_section",
131132
"llms_txt_payment_section",
133+
"offers_from",
132134
"purchase_mode_note",
133135
"sample_x402_accept_for_network",
134136
"signed_response_aiohttp",

‎agentscore_commerce/discovery/openapi.py‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from dataclasses import dataclass
44
from typing import Any, Literal
55

6+
from agentscore_commerce.payment.amounts import usd_to_atomic
7+
68

79
def agentscore_security_schemes(*, aip: bool = False) -> dict[str, Any]:
810
"""Standard AgentScore identity security schemes for `components.securitySchemes`.
@@ -101,25 +103,57 @@ def x_payment_info_extension(
101103
price: XPaymentInfoPrice,
102104
protocols: list[dict[str, Any]],
103105
description: str | None = None,
106+
offers: list[dict[str, Any]] | None = None,
104107
) -> dict[str, Any]:
105108
"""Wrap a price + protocols block under ``x-payment-info``.
106109
107110
For spreading into an OpenAPI operation object. ``protocols`` is a list of
108111
single-key dicts: ``{"x402": {}}`` for x402, ``{"mpp": {"method": ...,
109112
"intent": ..., "currency": ...}}`` for MPP. Order is preserved.
110113
111-
Emits ``authMode: "payment"`` by default per the x402scan convention.
114+
The block carries both readers' shapes, because MPP and x402scan define the same
115+
``x-payment-info`` extension differently and neither reads the other's keys:
116+
x402scan reads ``price`` + ``protocols`` (and ``authMode: "payment"``), MPP reads
117+
``offers``. ``offers`` defaults to one per MPP protocol entry (:func:`offers_from`).
112118
"""
113119
if isinstance(price, XPaymentInfoFixedPrice):
114120
price_dict: dict[str, Any] = {"mode": "fixed", "currency": price.currency, "amount": price.amount}
115121
else:
116122
price_dict = {"mode": "dynamic", "currency": price.currency, "min": price.min, "max": price.max}
117123
block: dict[str, Any] = {"authMode": "payment", "price": price_dict, "protocols": protocols}
124+
derived = offers if offers is not None else offers_from(price, protocols)
125+
if derived:
126+
block["offers"] = derived
118127
if description is not None:
119128
block["description"] = description
120129
return {"x-payment-info": block}
121130

122131

132+
def offers_from(price: XPaymentInfoPrice, protocols: list[dict[str, Any]]) -> list[dict[str, Any]]:
133+
"""MPP payment offers (``draft-payment-discovery``) for the MPP entries in ``protocols``.
134+
135+
Priced in each method's smallest unit the way the 402 challenge prices it: Stripe in
136+
cents, the token rails (Tempo USDC.e, Solana USDC) in 6-decimal base units. x402
137+
entries have no MPP offer; a dynamic price is ``None`` (``null``), which MPP defines
138+
as "depends on the request".
139+
"""
140+
offers: list[dict[str, Any]] = []
141+
for p in protocols:
142+
mpp = p.get("mpp")
143+
if not isinstance(mpp, dict):
144+
continue
145+
method, _, slash_intent = str(mpp.get("method", "")).partition("/")
146+
intent = "session" if (slash_intent or mpp.get("intent")) == "session" else "charge"
147+
decimals = 2 if method == "stripe" else 6
148+
fixed_usd = isinstance(price, XPaymentInfoFixedPrice) and price.currency.upper() == "USD"
149+
amount = str(usd_to_atomic(price.amount, decimals=decimals)) if fixed_usd else None
150+
offer: dict[str, Any] = {"intent": intent, "method": method, "amount": amount}
151+
if isinstance(mpp.get("currency"), str):
152+
offer["currency"] = mpp["currency"]
153+
offers.append(offer)
154+
return offers
155+
156+
123157
def x_guidance_extension(text: str) -> dict[str, str]:
124158
"""Wrap a prose blurb under ``x-guidance`` for spreading into an OpenAPI ``info`` block.
125159

‎agentscore_commerce/discovery/well_known.py‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def build_signed_ucp_response(
152152
*,
153153
checkout: Checkout,
154154
name: str,
155-
well_known_ucp_url: str,
155+
well_known_ucp_url: str | None = None,
156156
services: dict[str, list[UCPServiceBinding]],
157157
request_headers: Mapping[str, str] | None = None,
158158
signing_kid: str = "merchant-default",
@@ -169,8 +169,10 @@ def build_signed_ucp_response(
169169
Cache-Control) when no payment handlers can be derived from rails.
170170
171171
``services`` is the spec-compliant services map (keyed by reverse-DNS
172-
service name). ``well_known_ucp_url`` is the canonical URL of this profile,
173-
surfaced as the value in ``supported_versions``.
172+
service name). The profile publishes only the current UCP version:
173+
``supported_versions`` maps OLDER versions to complete profiles for them, and
174+
this serves none. ``well_known_ucp_url`` is no longer published and is accepted
175+
so existing callers keep working.
174176
"""
175177
handlers = _compose_handlers(checkout)
176178
if not handlers:
@@ -181,11 +183,10 @@ def build_signed_ucp_response(
181183

182184
profile = build_ucp_profile(
183185
name=name,
184-
supported_versions={"2026-04-08": well_known_ucp_url},
185186
agentscore_gate=agentscore_gate,
186187
services=services,
187188
payment_handlers=handlers,
188-
signing_keys=[signing_key_entry],
189+
keys=[signing_key_entry],
189190
)
190191
signed = sign_ucp_profile(
191192
profile.to_dict(),
@@ -281,13 +282,14 @@ def well_known_preflight_response(
281282
)
282283

283284

284-
_UCP_SHOPPING_SPEC_2026_04_08 = "https://ucp.dev/2026-04-08/specification/overview"
285+
_UCP_VERSION = "2026-08-25"
286+
_UCP_SHOPPING_SPEC = f"https://ucp.dev/{_UCP_VERSION}/specification/overview"
285287

286288

287289
def default_a2a_services(*, agent_card_url: str) -> dict[str, list[UCPServiceBinding]]:
288290
"""Canonical UCP §services map for a merchant publishing an A2A agent card.
289291
290-
Returns ``{"dev.ucp.shopping": [UCPServiceBinding(version="2026-04-08",
292+
Returns ``{"dev.ucp.shopping": [UCPServiceBinding(version="2026-08-25",
291293
spec="<UCP shopping spec>", transport="a2a", endpoint=agent_card_url)]}`` ;
292294
the binding every UCP-publishing merchant declares when their primary agent
293295
surface is the A2A v1.0 ``/.well-known/agent-card.json`` (versus a UCP MCP
@@ -299,8 +301,8 @@ def default_a2a_services(*, agent_card_url: str) -> dict[str, list[UCPServiceBin
299301
return {
300302
"dev.ucp.shopping": [
301303
UCPServiceBinding(
302-
version="2026-04-08",
303-
spec=_UCP_SHOPPING_SPEC_2026_04_08,
304+
version=_UCP_VERSION,
305+
spec=_UCP_SHOPPING_SPEC,
304306
transport="a2a",
305307
endpoint=agent_card_url,
306308
),

0 commit comments

Comments
 (0)