Skip to content

Commit 408d6ee

Browse files
vvillait88claude
andcommitted
fix(identity): regenerate cross-lang fixture corpus for spec-compliant shape
Mirror of node-commerce. Updated regen script to use the spec-compliant input (services / capabilities / payment_handlers as dicts keyed by reverse-DNS name) and regenerated all 10 py-* fixtures + synced the 10 node-* fixtures from the node sibling. Also corrected the stale docstring in examples/signed_ucp_merchant.py that claimed "UCP §6 trust-mode requires JWS signature" — UCP doesn't mandate signing; that's an AgentScore vendor extension for opt-in trust-mode verifiers (Pura Vida and Shopify-backed UCP merchants ship unsigned). test_ucp_cross_lang.py: 21/21 pass. The corpus now actually tests cross-language byte-parity for the spec-compliant shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bcd1060 commit 408d6ee

22 files changed

Lines changed: 839 additions & 550 deletions

examples/signed_ucp_merchant.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"""Signed UCP profile example — ``/.well-known/ucp`` + ``/.well-known/jwks.json``.
22
3-
UCP §6 trust-mode verification (Google AI Mode, Gemini commerce) requires the
4-
profile to carry a JWS signature and the merchant to publish a JWKS endpoint
5-
verifiers can fetch the public key from. This example wires both routes against
6-
a persistent signing key (env-loaded for prod, ephemeral for dev).
3+
AgentScore's ``agentscore-profile+jws`` is a vendor extension layered on top of
4+
the UCP profile for trust-mode verifiers (Visa AP2 pilots, regulated-commerce
5+
verifiers) that opt into auditable cryptographic provenance. UCP §6 itself does
6+
NOT mandate profile-body signing — Pura Vida and other Shopify-backed UCP
7+
merchants ship unsigned in production today, and live UCP-aware agents (Google
8+
AI Mode, Gemini commerce, Microsoft Copilot, Perplexity) accept unsigned
9+
profiles. This example wires both routes against a persistent signing key
10+
(env-loaded for prod, ephemeral for dev) for verifiers that DO opt into the
11+
signed envelope.
712
813
Run::
914

scripts/regenerate_cross_lang_fixtures.py

Lines changed: 136 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
"""Regenerate the full cross-lang fixture corpus (Python side).
22
3-
Writes all ``py-*.json`` fixtures under ``tests/fixtures/cross-lang/``. Used
4-
after a canonicalization-relevant change (typ rename, capability-name rename,
5-
schema-URL rename, key-sort tweak, etc.) where every JWS in the corpus needs
3+
Writes all ``py-*.json`` fixtures under ``tests/fixtures/cross-lang/``. Used after a
4+
canonicalization-relevant change (typ rename, capability-name rename, schema-URL
5+
rename, key-sort tweak, profile-shape change) where every JWS in the corpus needs
66
to be re-signed.
77
8-
Each scenario hand-crafts the profile body, signs with a fresh keypair, and
9-
writes the ``{profile, jwks, alg, kid, generator}`` envelope. Cross-lang
10-
verify in ``tests/test_ucp_cross_lang.py`` (and the Node sibling) pulls these
11-
in alongside the ``node-*`` fixtures generated by the Node sibling.
8+
Each scenario hand-crafts the profile body using the spec-compliant input shape
9+
(``services`` / ``capabilities`` / ``payment_handlers`` as MAPS keyed by reverse-DNS
10+
service / capability / handler name), signs with a fresh keypair, and writes the
11+
``{profile, jwks, alg, kid, generator}`` envelope. Cross-lang verify in
12+
``tests/test_ucp_cross_lang.py`` (and the Node sibling) pulls these in alongside the
13+
``node-*`` fixtures generated by the Node sibling.
14+
15+
Run: ``uv run python scripts/regenerate_cross_lang_fixtures.py``
1216
"""
1317

1418
from __future__ import annotations
@@ -20,9 +24,9 @@
2024
from agentscore_commerce.identity import (
2125
AssessResult,
2226
OperatorVerification,
23-
UCPCapability,
24-
UCPPaymentHandler,
25-
UCPService,
27+
UCPCapabilityBinding,
28+
UCPPaymentHandlerBinding,
29+
UCPServiceBinding,
2630
UCPSigningKey,
2731
build_ucp_profile,
2832
)
@@ -51,82 +55,139 @@ def _envelope(signed: dict[str, Any], public_jwk: dict[str, Any], alg: str, kid:
5155
}
5256

5357

58+
# Spec-compliant binding helpers — each scenario uses these (or variants) so the
59+
# fixtures cover the full set of canonical UCP fields per binding type.
60+
61+
62+
def _shop_service_mcp(host: str) -> UCPServiceBinding:
63+
return UCPServiceBinding(
64+
version="2026-04-08",
65+
spec="https://ucp.dev/2026-04-08/specification/overview",
66+
transport="mcp",
67+
endpoint=f"{host}/api/ucp/mcp",
68+
schema="https://ucp.dev/services/shopping/openrpc.json",
69+
)
70+
71+
72+
def _shop_service_a2a(host: str) -> UCPServiceBinding:
73+
return UCPServiceBinding(
74+
version="2026-04-08",
75+
spec="https://ucp.dev/2026-04-08/specification/overview",
76+
transport="a2a",
77+
endpoint=f"{host}/.well-known/agent-card.json",
78+
)
79+
80+
81+
def _tempo_handler(config: dict[str, Any] | None = None) -> UCPPaymentHandlerBinding:
82+
h = UCPPaymentHandlerBinding(
83+
id="tempo",
84+
version="2026-04-08",
85+
spec="https://agentscore.sh/specification/payment-handlers/tempo",
86+
schema="https://agentscore.sh/schemas/payment-handlers/tempo.json",
87+
)
88+
if config is not None:
89+
h.config = config
90+
return h
91+
92+
93+
def _x402_handler(networks: list[str]) -> UCPPaymentHandlerBinding:
94+
return UCPPaymentHandlerBinding(
95+
id="x402",
96+
version="2026-04-08",
97+
spec="https://agentscore.sh/specification/payment-handlers/x402",
98+
schema="https://agentscore.sh/schemas/payment-handlers/x402.json",
99+
config={"networks": networks},
100+
)
101+
102+
103+
def _stripe_handler(config: dict[str, Any]) -> UCPPaymentHandlerBinding:
104+
return UCPPaymentHandlerBinding(
105+
id="stripe",
106+
version="2026-04-08",
107+
spec="https://agentscore.sh/specification/payment-handlers/stripe-spt",
108+
schema="https://agentscore.sh/schemas/payment-handlers/stripe-spt.json",
109+
config=config,
110+
)
111+
112+
54113
def main() -> None:
55-
# py-minimal
114+
# py-minimal — empty maps; just metadata + signing keys.
56115
kid = "py-minimal-EdDSA"
57116
key = generate_ucp_signing_key(kid=kid)
58117
profile = build_ucp_profile(
59-
name="Minimal Merchant",
60-
services=[UCPService(type="rest", url="https://m.example.com")],
61-
payment_handlers=[],
118+
services={"dev.ucp.shopping": [_shop_service_mcp("https://m.example.com")]},
62119
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
120+
name="Minimal Merchant",
63121
)
64122
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid)
65123
_write("py-minimal", _envelope(signed, key.public_jwk, "EdDSA", kid))
66124

67-
# py-es256-rails
125+
# py-es256-rails — multi-transport service + multi-rail + ES256 signing key.
68126
kid = "py-es256-rails-ES256"
69127
key = generate_ucp_signing_key(kid=kid, alg="ES256")
70128
profile = build_ucp_profile(
71-
name="ES256 Merchant",
72-
services=[
73-
UCPService(type="rest", url="https://a.example.com"),
74-
UCPService(type="a2a", url="https://a.example.com/agent-card.json"),
75-
],
76-
payment_handlers=[
77-
UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet", "chain_id": 4217}),
78-
UCPPaymentHandler(name="x402", config={"networks": ["base-8453"]}),
79-
],
129+
services={
130+
"dev.ucp.shopping": [
131+
_shop_service_mcp("https://a.example.com"),
132+
_shop_service_a2a("https://a.example.com"),
133+
],
134+
},
80135
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
136+
payment_handlers={
137+
"sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet", "chain_id": 4217})],
138+
"sh.agentscore.payment.x402": [_x402_handler(["base-8453"])],
139+
},
140+
name="ES256 Merchant",
81141
)
82142
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid, alg="ES256")
83143
_write("py-es256-rails", _envelope(signed, key.public_jwk, "ES256", kid))
84144

85-
# py-extras-int
145+
# py-extras-int — payment_handler config with int + string fields.
86146
kid = "py-extras-int-EdDSA"
87147
key = generate_ucp_signing_key(kid=kid)
88148
profile = build_ucp_profile(
89-
name="Extras Merchant",
90-
services=[UCPService(type="rest", url="https://e.example.com")],
91-
payment_handlers=[UCPPaymentHandler(name="stripe", config={"profile_id": "abc", "count": 7})],
149+
services={"dev.ucp.shopping": [_shop_service_mcp("https://e.example.com")]},
92150
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
151+
payment_handlers={
152+
"sh.agentscore.payment.stripe-spt": [_stripe_handler({"profile_id": "abc", "count": 7})],
153+
},
154+
name="Extras Merchant",
93155
)
94156
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid)
95157
_write("py-extras-int", _envelope(signed, key.public_jwk, "EdDSA", kid))
96158

97-
# py-capability — hand-crafted vendor capability (renamed to
98-
# sh.agentscore.identity to match the new namespace; the in-fixture name
99-
# is independent of the SDK's auto-injection but consistency keeps the
100-
# corpus honest about what callers should publish).
159+
# py-capability — hand-crafted vendor capability under sh.agentscore.identity.
101160
kid = "py-capability-EdDSA"
102161
key = generate_ucp_signing_key(kid=kid)
162+
custom_capability = UCPCapabilityBinding(
163+
version="1",
164+
spec="https://agentscore.sh/specification/identity",
165+
schema="https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json",
166+
# `extras` flat on the binding — kyc_required is a vendor field on this binding.
167+
extras={"kyc_required": True},
168+
)
103169
profile = build_ucp_profile(
104-
name="Capability Merchant",
105-
services=[UCPService(type="rest", url="https://c.example.com")],
106-
capabilities=[
107-
UCPCapability(
108-
name="sh.agentscore.identity",
109-
schema="https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json",
110-
version="1",
111-
extras={"kyc_required": True},
112-
),
113-
],
114-
payment_handlers=[
115-
UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet", "chain_id": 4217}),
116-
],
170+
services={"dev.ucp.shopping": [_shop_service_mcp("https://c.example.com")]},
117171
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
172+
capabilities={"sh.agentscore.identity": [custom_capability]},
173+
payment_handlers={
174+
"sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet", "chain_id": 4217})],
175+
},
176+
name="Capability Merchant",
118177
)
119178
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid)
120179
_write("py-capability", _envelope(signed, key.public_jwk, "EdDSA", kid))
121180

122-
# py-unicode
181+
# py-unicode — multi-byte UTF-8 in name / endpoint / config.
123182
kid = "py-unicode-EdDSA"
124183
key = generate_ucp_signing_key(kid=kid)
125184
profile = build_ucp_profile(
126-
name="Café 日本 🍷 Merchant",
127-
services=[UCPService(type="rest", url="https://日本.example.com")],
128-
payment_handlers=[UCPPaymentHandler(name="tempo", config={"note": "メモ"})],
185+
services={"dev.ucp.shopping": [_shop_service_mcp("https://日本.example.com")]},
129186
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
187+
payment_handlers={
188+
"sh.agentscore.payment.tempo": [_tempo_handler({"note": "メモ"})],
189+
},
190+
name="Café 日本 🍷 Merchant",
130191
)
131192
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid)
132193
_write("py-unicode", _envelope(signed, key.public_jwk, "EdDSA", kid))
@@ -135,13 +196,15 @@ def main() -> None:
135196
old_key = generate_ucp_signing_key(kid="py-multikey-old")
136197
new_key = generate_ucp_signing_key(kid="py-multikey-new")
137198
profile = build_ucp_profile(
138-
name="Multi-Key Merchant",
139-
services=[UCPService(type="rest", url="https://mk.example.com")],
140-
payment_handlers=[UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet"})],
199+
services={"dev.ucp.shopping": [_shop_service_mcp("https://mk.example.com")]},
141200
signing_keys=[
142201
UCPSigningKey.from_jwk(old_key.public_jwk),
143202
UCPSigningKey.from_jwk(new_key.public_jwk),
144203
],
204+
payment_handlers={
205+
"sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet"})],
206+
},
207+
name="Multi-Key Merchant",
145208
)
146209
signed = sign_ucp_profile(profile.to_dict(), signing_key=new_key.private_key, kid="py-multikey-new")
147210
_write(
@@ -155,33 +218,35 @@ def main() -> None:
155218
},
156219
)
157220

158-
# py-emoji-keys — extras with non-ASCII object keys (BMP private use, CJK
159-
# compatibility, supplementary plane). Exercises codepoint-vs-UTF-16 sort.
221+
# py-emoji-keys — extras at top-level (outside the `ucp` envelope) with non-ASCII
222+
# object keys (BMP private use, CJK compatibility, supplementary plane).
223+
# Exercises codepoint-vs-UTF-16 sort.
160224
kid = "py-emoji-keys-EdDSA"
161225
key = generate_ucp_signing_key(kid=kid)
162226
profile = build_ucp_profile(
163-
name="Emoji Keys Merchant",
164-
services=[UCPService(type="rest", url="https://emoji.example.com")],
165-
payment_handlers=[UCPPaymentHandler(name="tempo", config={})],
227+
services={"dev.ucp.shopping": [_shop_service_mcp("https://emoji.example.com")]},
166228
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
229+
payment_handlers={
230+
"sh.agentscore.payment.tempo": [_tempo_handler()],
231+
},
232+
name="Emoji Keys Merchant",
167233
extras={
168234
"a": 1,
169235
"豈": 2,
170-
"": 3,
236+
"": 3,
171237
"🍷": 4,
172238
},
173239
)
174240
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid)
175241
_write("py-emoji-keys", _envelope(signed, key.public_jwk, "EdDSA", kid))
176242

177-
# py-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip.
243+
# py-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip via top-level extras.
178244
kid = "py-int-boundary-EdDSA"
179245
key = generate_ucp_signing_key(kid=kid)
180246
profile = build_ucp_profile(
181-
name="Int Boundary Merchant",
182-
services=[UCPService(type="rest", url="https://i.example.com")],
183-
payment_handlers=[],
247+
services={"dev.ucp.shopping": [_shop_service_mcp("https://i.example.com")]},
184248
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
249+
name="Int Boundary Merchant",
185250
extras={
186251
"max_safe_int": 9007199254740991,
187252
"min_safe_int": -9007199254740991,
@@ -193,9 +258,9 @@ def main() -> None:
193258
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid)
194259
_write("py-int-boundary", _envelope(signed, key.public_jwk, "EdDSA", kid))
195260

196-
# py-data-driven-claims — exercises the build_ucp_profile data path with
197-
# API-shape "missing" sentinels (empty string + None). Both languages MUST
198-
# emit identical canonical bytes for this input.
261+
# py-data-driven-claims — exercises build_ucp_profile data path with API-shape
262+
# "missing" sentinels (empty string + None). Both languages MUST emit identical
263+
# canonical bytes for this input.
199264
kid = "py-data-driven-claims-EdDSA"
200265
key = generate_ucp_signing_key(kid=kid)
201266
result = AssessResult(
@@ -213,17 +278,16 @@ def main() -> None:
213278
},
214279
)
215280
profile = build_ucp_profile(
216-
name="Data Driven Claims Merchant",
217-
services=[UCPService(type="rest", url="https://d.example.com")],
218-
payment_handlers=[],
281+
services={"dev.ucp.shopping": [_shop_service_mcp("https://d.example.com")]},
219282
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
283+
name="Data Driven Claims Merchant",
220284
data=result,
221285
)
222286
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid)
223287
_write("py-data-driven-claims", _envelope(signed, key.public_jwk, "EdDSA", kid))
224288

225-
# py-typed-claims — exercises the typed AssessResult fields (no raw
226-
# fallback). Cross-lang parity check for the typed-field-only call site.
289+
# py-typed-claims — exercises typed AssessResult fields (no raw fallback).
290+
# Cross-lang parity check for the typed-field-only call site.
227291
kid = "py-typed-claims-EdDSA"
228292
key = generate_ucp_signing_key(kid=kid)
229293
result = AssessResult(
@@ -245,10 +309,9 @@ def main() -> None:
245309
raw=None,
246310
)
247311
profile = build_ucp_profile(
248-
name="Typed Claims Merchant",
249-
services=[UCPService(type="rest", url="https://t.example.com")],
250-
payment_handlers=[],
312+
services={"dev.ucp.shopping": [_shop_service_mcp("https://t.example.com")]},
251313
signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)],
314+
name="Typed Claims Merchant",
252315
data=result,
253316
)
254317
signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid)

0 commit comments

Comments
 (0)