|
| 1 | +"""Regenerate the full cross-lang fixture corpus (Python side). |
| 2 | +
|
| 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 |
| 6 | +to be re-signed. |
| 7 | +
|
| 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. |
| 12 | +""" |
| 13 | + |
| 14 | +from __future__ import annotations |
| 15 | + |
| 16 | +import json |
| 17 | +from pathlib import Path |
| 18 | +from typing import Any |
| 19 | + |
| 20 | +from agentscore_commerce.identity import ( |
| 21 | + AssessResult, |
| 22 | + OperatorVerification, |
| 23 | + UCPCapability, |
| 24 | + UCPPaymentHandler, |
| 25 | + UCPService, |
| 26 | + UCPSigningKey, |
| 27 | + build_ucp_profile, |
| 28 | +) |
| 29 | +from agentscore_commerce.identity.ucp_jwks import ( |
| 30 | + build_jwks_response, |
| 31 | + generate_ucp_signing_key, |
| 32 | + sign_ucp_profile, |
| 33 | +) |
| 34 | + |
| 35 | +OUT_DIR = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" |
| 36 | + |
| 37 | + |
| 38 | +def _write(name: str, env: dict[str, Any]) -> None: |
| 39 | + out = OUT_DIR / f"{name}.json" |
| 40 | + out.write_text(json.dumps(env, indent=2, ensure_ascii=False) + "\n") |
| 41 | + print(f"wrote {out}") |
| 42 | + |
| 43 | + |
| 44 | +def _envelope(signed: dict[str, Any], public_jwk: dict[str, Any], alg: str, kid: str) -> dict[str, Any]: |
| 45 | + return { |
| 46 | + "profile": signed, |
| 47 | + "jwks": build_jwks_response([public_jwk]), |
| 48 | + "alg": alg, |
| 49 | + "kid": kid, |
| 50 | + "generator": "python", |
| 51 | + } |
| 52 | + |
| 53 | + |
| 54 | +def main() -> None: |
| 55 | + # py-minimal |
| 56 | + kid = "py-minimal-EdDSA" |
| 57 | + key = generate_ucp_signing_key(kid=kid) |
| 58 | + profile = build_ucp_profile( |
| 59 | + name="Minimal Merchant", |
| 60 | + services=[UCPService(type="rest", url="https://m.example.com")], |
| 61 | + payment_handlers=[], |
| 62 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 63 | + ) |
| 64 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) |
| 65 | + _write("py-minimal", _envelope(signed, key.public_jwk, "EdDSA", kid)) |
| 66 | + |
| 67 | + # py-es256-rails |
| 68 | + kid = "py-es256-rails-ES256" |
| 69 | + key = generate_ucp_signing_key(kid=kid, alg="ES256") |
| 70 | + 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 | + ], |
| 80 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 81 | + ) |
| 82 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid, alg="ES256") |
| 83 | + _write("py-es256-rails", _envelope(signed, key.public_jwk, "ES256", kid)) |
| 84 | + |
| 85 | + # py-extras-int |
| 86 | + kid = "py-extras-int-EdDSA" |
| 87 | + key = generate_ucp_signing_key(kid=kid) |
| 88 | + 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})], |
| 92 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 93 | + ) |
| 94 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) |
| 95 | + _write("py-extras-int", _envelope(signed, key.public_jwk, "EdDSA", kid)) |
| 96 | + |
| 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). |
| 101 | + kid = "py-capability-EdDSA" |
| 102 | + key = generate_ucp_signing_key(kid=kid) |
| 103 | + 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/schema/identity/1", |
| 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 | + ], |
| 117 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 118 | + ) |
| 119 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) |
| 120 | + _write("py-capability", _envelope(signed, key.public_jwk, "EdDSA", kid)) |
| 121 | + |
| 122 | + # py-unicode |
| 123 | + kid = "py-unicode-EdDSA" |
| 124 | + key = generate_ucp_signing_key(kid=kid) |
| 125 | + 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": "メモ"})], |
| 129 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 130 | + ) |
| 131 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) |
| 132 | + _write("py-unicode", _envelope(signed, key.public_jwk, "EdDSA", kid)) |
| 133 | + |
| 134 | + # py-multikey — JWKS with two keys, signed by the newer one. |
| 135 | + old_key = generate_ucp_signing_key(kid="py-multikey-old") |
| 136 | + new_key = generate_ucp_signing_key(kid="py-multikey-new") |
| 137 | + 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"})], |
| 141 | + signing_keys=[ |
| 142 | + UCPSigningKey.from_jwk(old_key.public_jwk), |
| 143 | + UCPSigningKey.from_jwk(new_key.public_jwk), |
| 144 | + ], |
| 145 | + ) |
| 146 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=new_key.private_key, kid="py-multikey-new") |
| 147 | + _write( |
| 148 | + "py-multikey", |
| 149 | + { |
| 150 | + "profile": signed, |
| 151 | + "jwks": build_jwks_response([old_key.public_jwk, new_key.public_jwk]), |
| 152 | + "alg": "EdDSA", |
| 153 | + "kid": "py-multikey-new", |
| 154 | + "generator": "python", |
| 155 | + }, |
| 156 | + ) |
| 157 | + |
| 158 | + # py-emoji-keys — extras with non-ASCII object keys (BMP private use, CJK |
| 159 | + # compatibility, supplementary plane). Exercises codepoint-vs-UTF-16 sort. |
| 160 | + kid = "py-emoji-keys-EdDSA" |
| 161 | + key = generate_ucp_signing_key(kid=kid) |
| 162 | + 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={})], |
| 166 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 167 | + extras={ |
| 168 | + "extras": { |
| 169 | + "a": 1, |
| 170 | + "豈": 2, |
| 171 | + "": 3, |
| 172 | + "🍷": 4, |
| 173 | + }, |
| 174 | + }, |
| 175 | + ) |
| 176 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) |
| 177 | + _write("py-emoji-keys", _envelope(signed, key.public_jwk, "EdDSA", kid)) |
| 178 | + |
| 179 | + # py-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip. |
| 180 | + kid = "py-int-boundary-EdDSA" |
| 181 | + key = generate_ucp_signing_key(kid=kid) |
| 182 | + profile = build_ucp_profile( |
| 183 | + name="Int Boundary Merchant", |
| 184 | + services=[UCPService(type="rest", url="https://i.example.com")], |
| 185 | + payment_handlers=[], |
| 186 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 187 | + extras={ |
| 188 | + "extras": { |
| 189 | + "max_safe_int": 9007199254740991, |
| 190 | + "min_safe_int": -9007199254740991, |
| 191 | + "small_int": 42, |
| 192 | + "neg_small_int": -42, |
| 193 | + "zero": 0, |
| 194 | + }, |
| 195 | + }, |
| 196 | + ) |
| 197 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) |
| 198 | + _write("py-int-boundary", _envelope(signed, key.public_jwk, "EdDSA", kid)) |
| 199 | + |
| 200 | + # py-data-driven-claims — exercises the build_ucp_profile data path with |
| 201 | + # API-shape "missing" sentinels (empty string + None). Both languages MUST |
| 202 | + # emit identical canonical bytes for this input. |
| 203 | + kid = "py-data-driven-claims-EdDSA" |
| 204 | + key = generate_ucp_signing_key(kid=kid) |
| 205 | + result = AssessResult( |
| 206 | + allow=True, |
| 207 | + resolved_operator="op_data_driven", |
| 208 | + verify_url="https://agentscore.sh/verify/op_data_driven", |
| 209 | + raw={ |
| 210 | + "account_verification": { |
| 211 | + "kyc_level": "", |
| 212 | + "sanctions_clear": False, |
| 213 | + "age_bracket": None, |
| 214 | + "jurisdiction": None, |
| 215 | + "verified_at": None, |
| 216 | + }, |
| 217 | + }, |
| 218 | + ) |
| 219 | + profile = build_ucp_profile( |
| 220 | + name="Data Driven Claims Merchant", |
| 221 | + services=[UCPService(type="rest", url="https://d.example.com")], |
| 222 | + payment_handlers=[], |
| 223 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 224 | + data=result, |
| 225 | + ) |
| 226 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) |
| 227 | + _write("py-data-driven-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) |
| 228 | + |
| 229 | + # py-typed-claims — exercises the typed AssessResult fields (no raw |
| 230 | + # fallback). Cross-lang parity check for the typed-field-only call site. |
| 231 | + kid = "py-typed-claims-EdDSA" |
| 232 | + key = generate_ucp_signing_key(kid=kid) |
| 233 | + result = AssessResult( |
| 234 | + allow=True, |
| 235 | + resolved_operator="op_typed_claims", |
| 236 | + verify_url="https://agentscore.sh/verify/op_typed_claims", |
| 237 | + operator_verification=OperatorVerification( |
| 238 | + level="enhanced", |
| 239 | + operator_type="api", |
| 240 | + verified_at="2026-04-01T00:00:00Z", |
| 241 | + ), |
| 242 | + account_verification={ |
| 243 | + "kyc_level": "enhanced", |
| 244 | + "sanctions_clear": True, |
| 245 | + "age_bracket": "21+", |
| 246 | + "jurisdiction": "US", |
| 247 | + "verified_at": "2026-04-01T00:00:00Z", |
| 248 | + }, |
| 249 | + raw=None, |
| 250 | + ) |
| 251 | + profile = build_ucp_profile( |
| 252 | + name="Typed Claims Merchant", |
| 253 | + services=[UCPService(type="rest", url="https://t.example.com")], |
| 254 | + payment_handlers=[], |
| 255 | + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], |
| 256 | + data=result, |
| 257 | + ) |
| 258 | + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) |
| 259 | + _write("py-typed-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) |
| 260 | + |
| 261 | + |
| 262 | +if __name__ == "__main__": |
| 263 | + main() |
0 commit comments