diff --git a/CLAUDE.md b/CLAUDE.md index 047bec4..fa271a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ Every helper is extracted from a real consumer, not speculated. ## Architecture -Single Python package, hatchling-built, published to PyPI as `agentscore-commerce`. Per-framework identity adapters expose the same surface (`AgentScoreGate`, or `agentscore_gate(app, ...)` for Flask/Sanic; `capture_wallet`, `verify_wallet_signer_match`, `get_assess_data`, `get_gate_degraded_state`, `get_gate_quota_info`) with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim). +Single Python package, hatchling-built, published to PyPI as `agentscore-commerce`. Per-framework identity adapters expose the same surface (`AgentScoreGate`, or `agentscore_gate(app, ...)` for Flask/Sanic; `capture_wallet`, `verify_wallet_signer_match`, `get_agentscore_data`, `get_gate_degraded_state`, `get_gate_quota_info`) with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim). | Directory | Contents | |---|---| diff --git a/README.md b/README.md index dba6fef..d3e6143 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]' | Submodule | What it provides | |---|---| -| `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_assess_data(...)`, `capture_wallet(...)`, `verify_wallet_signer_match(...)`. | +| `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_agentscore_data(...)`, `capture_wallet(...)`, `verify_wallet_signer_match(...)`. | | `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` (for multi-product merchants where each product carries its own compliance config: hard gate vs soft vs none, per-product shipping allowlists). | | `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim that emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (returns `PaymentSigner({address, network})`), `register_x402_schemes_v1_v2`; drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle with one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text). | | `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers like `awal x402 details`), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest; strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces; defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp,jwks.json}`, `/favicon.{png,ico}`; pure helpers `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). | @@ -39,7 +39,7 @@ from fastapi import Depends, FastAPI, Request from agentscore_commerce.identity.fastapi import ( AgentScoreGate, capture_wallet, - get_assess_data, + get_agentscore_data, verify_wallet_signer_match, ) @@ -68,7 +68,7 @@ async def gate_on_settle(request: Request) -> None: @app.post("/purchase", dependencies=[Depends(gate_on_settle)]) -async def purchase(request: Request, assess=Depends(get_assess_data)): +async def purchase(request: Request, assess=Depends(get_agentscore_data)): # ... settle payment ... # After payment, capture the signer wallet for cross-merchant attribution await capture_wallet(request, signer, "evm", idempotency_key=payment_intent_id) @@ -181,20 +181,37 @@ headers = build_payment_headers(BuildPaymentHeadersInput( ```python from agentscore_commerce.identity import ( + AgentScoreGatePolicy, UCPServiceBinding, UCPSigningKey, UCPPaymentHandlerBinding, - A2AAgentCardCapabilities, + A2AAgentSkill, build_a2a_agent_card, build_ucp_profile, ucp_a2a_extension, ) # Google A2A v1.0 Signed Agent Card. Publish at /.well-known/agent-card.json. -# Per UCP §A2A binding the card MUST declare the canonical UCP extension URI; -# pass `ucp_a2a_extension()` with empty capabilities until you bind formal UCP -# capabilities (dev.ucp.shopping.checkout, etc.). -card = build_a2a_agent_card(name="My Service", url=base_url, capabilities=A2AAgentCardCapabilities(...), extensions=[ucp_a2a_extension()], data=assess_result) +# Per UCP §A2A binding the card MUST declare the canonical UCP extension URI in +# `capabilities.extensions[]`; pass `ucp_a2a_extension()` with empty capabilities +# until you bind formal UCP capabilities (dev.ucp.shopping.checkout, etc.). +# Skills are top-level AgentSkill objects; identity claims live in a separate +# AgentCardSignature (RFC 7515 JWS) wrapping the serialized card. +card = build_a2a_agent_card( + name="My Service", + description="Buy products via agent payments.", + url=base_url, + version="1.0.0", + skills=[ + A2AAgentSkill( + id="purchase", + name="Purchase", + description="Buy products via agent payments.", + tags=["commerce", "payment"], + ), + ], + extensions=[ucp_a2a_extension()], +) # Google Universal Commerce Protocol. Publish at /.well-known/ucp. # Output shape: {"ucp": {"version", "services", "capabilities", @@ -210,7 +227,7 @@ profile = build_ucp_profile( spec="https://ucp.dev/2026-04-08/specification/overview", transport="mcp", endpoint=f"{base_url}/api/ucp/mcp", - schema="https://ucp.dev/services/shopping/openrpc.json", + schema="https://ucp.dev/services/shopping/mcp.openrpc.json", ), ], }, @@ -226,7 +243,12 @@ profile = build_ucp_profile( ], }, signing_keys=[UCPSigningKey(kid="me", kty="EC", alg="ES256")], - data=assess_result, + # Optional: declare merchant gate policy as an `sh.agentscore.identity` capability + # binding inside the public profile. Static policy declaration only — no per-operator + # claims. Per-operator identity attestation flows through the AP2 risk-signal endpoint. + agentscore_gate=AgentScoreGatePolicy( + require_kyc=True, min_age=21, allowed_jurisdictions=["US"], + ), ) ``` @@ -415,11 +437,11 @@ The `get_gate_degraded_state` helper is exported by every framework adapter (Fas ## Examples -The [examples/](./examples) directory has 7 runnable single-file FastAPI apps covering common merchant scenarios. See [examples/README.md](./examples/README.md) for the full table. +The [examples/](./examples) directory has 8 runnable single-file FastAPI apps covering common merchant scenarios. See [examples/README.md](./examples/README.md) for the full table. ## Stability -`agentscore-commerce@1.4.0` ships with the full merchant SDK surface stable. Helpers are protocol translations + configurable opinions; most evolution is additive (new optional params, new helpers, new networks/rails). Major bumps are reserved for genuine protocol-mapping bugs. +`agentscore-commerce` ships with the full merchant SDK surface stable. Helpers are protocol translations + configurable opinions; most evolution is additive (new optional params, new helpers, new networks/rails). Major bumps are reserved for genuine protocol-mapping bugs. ## Documentation diff --git a/agentscore_commerce/identity/__init__.py b/agentscore_commerce/identity/__init__.py index fbd1437..c5ef46a 100644 --- a/agentscore_commerce/identity/__init__.py +++ b/agentscore_commerce/identity/__init__.py @@ -16,7 +16,10 @@ A2AAgentCard, A2AAgentCardCapabilities, A2AAgentCardExtension, - A2AAgentCardIdentity, + A2AAgentCardSignature, + A2AAgentInterface, + A2AAgentProvider, + A2AAgentSkill, build_a2a_agent_card, ucp_a2a_extension, ) @@ -33,23 +36,19 @@ ) from agentscore_commerce.identity.signer import extract_x402_signer from agentscore_commerce.identity.types import ( - Activity, AgentIdentity, AgentMemoryHint, AssessResult, - Classification, DenialCode, DenialReason, - Grade, - Identity, OperatorVerification, - ScoreDetail, VerifyWalletSignerMatchOptions, VerifyWalletSignerResult, build_agent_memory_hint, ) from agentscore_commerce.identity.ucp import ( AGENTSCORE_UCP_CAPABILITY, + AgentScoreGatePolicy, UCPCapabilityBinding, UCPPaymentHandlerBinding, UCPProfile, @@ -70,7 +69,7 @@ # ASGI middleware is the default import (re-exported as CreateSessionOnMissing too). # Framework adapters are imported from their own submodules: -# from agentscore_commerce.identity.fastapi import AgentScoreGate, get_assess_data # native Depends() +# from agentscore_commerce.identity.fastapi import AgentScoreGate, get_agentscore_data # native Depends() # from agentscore_commerce.identity.flask import agentscore_gate # from agentscore_commerce.identity.django import AgentScoreMiddleware # from agentscore_commerce.identity.aiohttp import agentscore_gate_middleware @@ -95,13 +94,15 @@ def _load_asgi_middleware() -> tuple[Any, Any]: "A2AAgentCard", "A2AAgentCardCapabilities", "A2AAgentCardExtension", - "A2AAgentCardIdentity", - "Activity", + "A2AAgentCardSignature", + "A2AAgentInterface", + "A2AAgentProvider", + "A2AAgentSkill", "AgentIdentity", "AgentMemoryHint", "AgentScoreGate", + "AgentScoreGatePolicy", "AssessResult", - "Classification", "CreateSessionOnMissing", "DenialCode", "DenialReason", @@ -109,12 +110,9 @@ def _load_asgi_middleware() -> tuple[Any, Any]: "GateClient", "GateResult", "GeneratedUCPKey", - "Grade", - "Identity", "IdentityStatus", "OperatorVerification", "PolicyBlock", - "ScoreDetail", "UCPCapabilityBinding", "UCPPaymentHandlerBinding", "UCPProfile", diff --git a/agentscore_commerce/identity/a2a.py b/agentscore_commerce/identity/a2a.py index bb33783..d281c83 100644 --- a/agentscore_commerce/identity/a2a.py +++ b/agentscore_commerce/identity/a2a.py @@ -1,13 +1,15 @@ -"""Google A2A (Agent-to-Agent) Signed Agent Cards builder. +"""Google A2A (Agent-to-Agent) v1.0 Agent Card builder. -Compose the JSON payload for an A2A v1.0 Signed Agent Card that includes the agent's -AgentScore identity claims. Returned object is the unsigned card body — the merchant -(or agent) signs it with their wallet / signing key before publishing. +Compose the JSON payload for an A2A v1.0 Agent Card per the canonical proto definition +at https://github.com/a2aproject/A2A/blob/main/specification/a2a.proto. Returned object +is the unsigned card body — wrap with an A2A AgentCardSignature (RFC 7515 JWS) to sign +vendor-side before publishing at /.well-known/agent-card.json. -Why publish: A2A is a Linux Foundation standard with broad cross-vendor adoption. -Signed Agent Cards let any A2A-compatible reader discover an agent's verified-identity -claims without per-platform integration. Publishing operator identity in this format -means our identity travels with the agent across A2A-aware ecosystems. +Why publish: A2A is a Linux Foundation standard. Signed Agent Cards let any +A2A-compatible reader discover an agent's capabilities + protocol bindings without +per-platform integration. Per UCP §A2A binding, agents serving UCP via the A2A +transport MUST declare the canonical UCP extension URI in capabilities.extensions[] +so platforms detect UCP support without re-fetching the profile. Spec reference: https://a2a-protocol.org/latest/ """ @@ -15,32 +17,104 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from agentscore_commerce.identity.types import AssessResult +from typing import Any _PROTOCOL_VERSION = "1.0" -_CARD_VERSION = 1 +_DEFAULT_PROTOCOL_BINDING = "HTTP+JSON" +_DEFAULT_INPUT_MODE = "application/json" +_DEFAULT_OUTPUT_MODE = "application/json" UCP_A2A_EXTENSION_URI = "https://ucp.dev/2026-04-08/specification/reference" """Canonical UCP A2A extension URI — verifiers look for this exact URI in -``extensions[]`` to detect UCP support on the agent card.""" +``capabilities.extensions[]`` to detect UCP support on the agent card.""" + + +@dataclass +class A2AAgentInterface: + """Per spec §4.4.6. Each entry advertises one protocol binding the agent supports. + + `supported_interfaces[0]` is the preferred binding (ordered list). + """ + + url: str + protocol_binding: str + """Open string — core values are ``JSONRPC``, ``GRPC``, ``HTTP+JSON``.""" + protocol_version: str + """A2A protocol version, e.g. ``1.0``. Distinct from the agent's own version.""" + tenant: str | None = None + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "url": self.url, + "protocol_binding": self.protocol_binding, + "protocol_version": self.protocol_version, + } + if self.tenant is not None: + out["tenant"] = self.tenant + return out + + +@dataclass +class A2AAgentProvider: + """Per spec §4.4.2. The org/service that provides the agent.""" + + url: str + organization: str + + def to_dict(self) -> dict[str, str]: + return {"url": self.url, "organization": self.organization} + + +@dataclass +class A2AAgentSkill: + """Per spec §4.4.5. A distinct capability or function the agent performs. + + Lives at the TOP LEVEL of AgentCard (not inside ``capabilities``). + """ + + id: str + name: str + description: str + tags: list[str] + examples: list[str] = field(default_factory=list) + input_modes: list[str] = field(default_factory=list) + output_modes: list[str] = field(default_factory=list) + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "id": self.id, + "name": self.name, + "description": self.description, + "tags": self.tags, + } + if self.examples: + out["examples"] = self.examples + if self.input_modes: + out["input_modes"] = self.input_modes + if self.output_modes: + out["output_modes"] = self.output_modes + return out @dataclass class A2AAgentCardExtension: - """Per A2A v1.0: an entry in the card's top-level ``extensions`` array. + """Per spec §4.4.4. A protocol extension the agent supports. - UCP support is declared this way (UCP §A2A binding requires - ``https://ucp.dev/2026-04-08/specification/reference``). + Lives in ``capabilities.extensions[]``. ``description`` and ``required`` are + spec-mandated fields, not optional. """ uri: str + description: str + required: bool = False params: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: - out: dict[str, Any] = {"uri": self.uri} + out: dict[str, Any] = { + "uri": self.uri, + "description": self.description, + "required": self.required, + } if self.params: out["params"] = self.params return out @@ -48,8 +122,10 @@ def to_dict(self) -> dict[str, Any]: def ucp_a2a_extension( capabilities: dict[str, list[dict[str, str]]] | None = None, + *, + required: bool = False, ) -> A2AAgentCardExtension: - """Build the canonical UCP entry for an A2A agent card's ``extensions[]`` array. + """Build the canonical UCP entry for an A2A agent card's extensions[] array. Per UCP §A2A binding: "Businesses supporting UCP must advertise the extension and any optional capabilities in their A2A Agent Card to allow platforms to @@ -58,160 +134,226 @@ def ucp_a2a_extension( list of ``{"version": "..."}`` records. Pass ``None`` (or an empty dict) when you serve UCP at the discovery layer but have no formal capability bindings yet. + + ``required=True`` declares the platform must understand UCP to interoperate + with this agent. Default ``False``: UCP is offered but not mandatory. """ return A2AAgentCardExtension( uri=UCP_A2A_EXTENSION_URI, + description="UCP support: this agent serves Universal Commerce Protocol bindings via the A2A transport.", + required=required, params={"capabilities": capabilities or {}}, ) @dataclass class A2AAgentCardCapabilities: - """Endpoints the agent exposes + skill tags.""" + """Per spec §4.4.3. Optional capabilities the agent supports. + + Per the canonical proto, ``capabilities`` declares: streaming, push_notifications, + extensions (the protocol extensions the agent supports), and extended_agent_card. + REST-style endpoint metadata does NOT belong here — A2A uses ``supported_interfaces`` + on the AgentCard for protocol bindings, and ``skills`` (top-level) for capability + descriptions. + """ - endpoints: list[dict[str, str]] = field(default_factory=list) - skills: list[str] = field(default_factory=list) + streaming: bool | None = None + push_notifications: bool | None = None + extensions: list[A2AAgentCardExtension] = field(default_factory=list) + extended_agent_card: bool | None = None def to_dict(self) -> dict[str, Any]: out: dict[str, Any] = {} - if self.endpoints: - out["endpoints"] = self.endpoints - if self.skills: - out["skills"] = self.skills + if self.streaming is not None: + out["streaming"] = self.streaming + if self.push_notifications is not None: + out["push_notifications"] = self.push_notifications + if self.extensions: + out["extensions"] = [e.to_dict() for e in self.extensions] + if self.extended_agent_card is not None: + out["extended_agent_card"] = self.extended_agent_card return out @dataclass -class A2AAgentCardIdentity: - """AgentScore identity claims embedded in the A2A card.""" - - issuer: str - operator_id: str - kyc_level: str - sanctions_clear: bool - age_bracket: str - jurisdiction: str - verified_at: str | None - verify_url: str +class A2AAgentCardSignature: + """Per spec §4.4.7. JWS signature embedded in an Agent Card. + + Multiple signatures MAY be attached to a single card. Verifiers reconstruct the + card body without ``signatures`` to verify each entry. Format follows RFC 7515 + JSON Web Signature (JWS). + """ + + protected: str + """Base64url-encoded JSON of the protected JWS header. REQUIRED.""" + signature: str + """Base64url-encoded computed signature. REQUIRED.""" + header: dict[str, Any] = field(default_factory=dict) + """Optional unprotected JWS header values.""" def to_dict(self) -> dict[str, Any]: - return { - "issuer": self.issuer, - "operator_id": self.operator_id, - "kyc_level": self.kyc_level, - "sanctions_clear": self.sanctions_clear, - "age_bracket": self.age_bracket, - "jurisdiction": self.jurisdiction, - "verified_at": self.verified_at, - "verify_url": self.verify_url, - } + out: dict[str, Any] = {"protected": self.protected, "signature": self.signature} + if self.header: + out["header"] = self.header + return out @dataclass class A2AAgentCard: - """A2A v1.0 Agent Card body. + """Per spec §4.4.1. A2A v1.0 Agent Card body. - Use :meth:`to_dict` to serialize for signing + publishing. Signing happens - vendor-side (the agent's signing key never leaves their environment). + Use :meth:`to_dict` to serialize for signing + publishing. Per spec §4.4.7, + JWS signatures may be embedded directly in the card via the ``signatures`` field; + verifiers reconstruct the card body without ``signatures`` and verify each entry. + Per-vendor identity attestation can also be expressed via a vendor extension + entry inside ``capabilities.extensions[]``. """ name: str - identity: A2AAgentCardIdentity | None = None - description: str | None = None - url: str | None = None - capabilities: A2AAgentCardCapabilities | None = None - extensions: list[A2AAgentCardExtension] = field(default_factory=list) + description: str + supported_interfaces: list[A2AAgentInterface] + version: str + """Agent's own version, e.g. ``"1.0.0"``. Distinct from the A2A protocol version, + which lives on each ``A2AAgentInterface.protocol_version``.""" + capabilities: A2AAgentCardCapabilities + default_input_modes: list[str] + default_output_modes: list[str] + skills: list[A2AAgentSkill] = field(default_factory=list) + """Per spec §4.4.1 (proto field 12, REQUIRED): the agent must declare ≥1 skill. + The convenience builder :func:`build_a2a_agent_card` enforces non-empty.""" + provider: A2AAgentProvider | None = None + documentation_url: str | None = None + icon_url: str | None = None + """Per spec §4.4.1 (proto field 14, optional): URL to an icon for the agent.""" + signatures: list[A2AAgentCardSignature] = field(default_factory=list) + """Per spec §4.4.1 (proto field 13, optional) + §4.4.7: JWS signatures embedded + in the card. Compute over the canonical card body MINUS this field, then attach.""" + security_schemes: dict[str, Any] = field(default_factory=dict) + security_requirements: list[Any] = field(default_factory=list) extras: dict[str, Any] = field(default_factory=dict) - protocol_version: str = _PROTOCOL_VERSION - card_version: int = _CARD_VERSION def to_dict(self) -> dict[str, Any]: out: dict[str, Any] = { - "protocol_version": self.protocol_version, - "card_version": self.card_version, "name": self.name, - "identity": self.identity.to_dict() if self.identity is not None else None, + "description": self.description, + "supported_interfaces": [i.to_dict() for i in self.supported_interfaces], + "version": self.version, + "capabilities": self.capabilities.to_dict(), + "default_input_modes": self.default_input_modes, + "default_output_modes": self.default_output_modes, } - if self.description is not None: - out["description"] = self.description - if self.url is not None: - out["url"] = self.url - if self.capabilities is not None: - out["capabilities"] = self.capabilities.to_dict() - if self.extensions: - out["extensions"] = [e.to_dict() for e in self.extensions] - if self.extras: - out.update(self.extras) + if self.provider is not None: + out["provider"] = self.provider.to_dict() + if self.documentation_url is not None: + out["documentation_url"] = self.documentation_url + if self.icon_url is not None: + out["icon_url"] = self.icon_url + if self.skills: + out["skills"] = [s.to_dict() for s in self.skills] + if self.signatures: + out["signatures"] = [s.to_dict() for s in self.signatures] + if self.security_schemes: + out["security_schemes"] = self.security_schemes + if self.security_requirements: + out["security_requirements"] = self.security_requirements + for k, v in self.extras.items(): + out[k] = v return out def build_a2a_agent_card( + *, name: str, - description: str | None = None, - url: str | None = None, - capabilities: A2AAgentCardCapabilities | None = None, + description: str, + url: str, + skills: list[A2AAgentSkill], + version: str = "1.0.0", extensions: list[A2AAgentCardExtension] | None = None, - data: AssessResult | None = None, - issuer: str = "https://agentscore.sh", - verify_url: str | None = None, + streaming: bool | None = None, + push_notifications: bool | None = None, + extended_agent_card: bool | None = None, + provider: A2AAgentProvider | None = None, + documentation_url: str | None = None, + icon_url: str | None = None, + signatures: list[A2AAgentCardSignature] | None = None, + default_input_modes: list[str] | None = None, + default_output_modes: list[str] | None = None, + protocol_binding: str = _DEFAULT_PROTOCOL_BINDING, + a2a_protocol_version: str = _PROTOCOL_VERSION, + security_schemes: dict[str, Any] | None = None, + security_requirements: list[Any] | None = None, extras: dict[str, Any] | None = None, ) -> A2AAgentCard: - """Compose an A2A Signed Agent Card body with AgentScore identity claims included. + """Compose an A2A v1.0 Agent Card body per the canonical proto. - Returns the UNSIGNED card. The vendor signs it with their wallet (typically the - same wallet they use for x402 / MPP payments) and publishes the signed envelope - to wherever A2A consumers discover cards. + Returns the UNSIGNED card. To attach identity claims, sign the ``to_dict()`` + output as an RFC 7515 JWS (``AgentCardSignature``). Vendors can also add an + identity-flavored extension to ``capabilities.extensions[]``. - Pass ``data=None`` to emit a card with no identity claims (publishable but - unverified). + The single ``url`` argument becomes the primary ``supported_interfaces[0].url`` + (with ``protocol_binding=HTTP+JSON``, ``protocol_version=1.0`` by default). + Override these via the ``protocol_binding`` and ``a2a_protocol_version`` kwargs, + or build ``A2AAgentInterface`` objects directly via the dataclass for multi-binding + agents (in which case construct the ``A2AAgentCard`` directly). Example:: from agentscore_commerce.identity.a2a import ( - A2AAgentCardCapabilities, + A2AAgentSkill, build_a2a_agent_card, + ucp_a2a_extension, ) - result = client.check(identity) card = build_a2a_agent_card( name="Example Merchant Concierge", description="Buy regulated goods via agent payments.", url="https://agents.example.com", - capabilities=A2AAgentCardCapabilities( - endpoints=[{"name": "purchase", "path": "/purchase", "method": "POST"}], - skills=["product-purchase", "regulated-commerce"], - ), - data=result, + version="1.0.0", + skills=[ + A2AAgentSkill( + id="purchase", + name="Purchase", + description="Buy products via agent payments.", + tags=["commerce", "payment"], + ), + ], + extensions=[ucp_a2a_extension()], ) - signed = your_sign(card.to_dict()) + signed = your_jws_sign(card.to_dict()) """ - identity: A2AAgentCardIdentity | None = None - if data is not None and data.resolved_operator: - raw = data.raw or {} - operator_verification = raw.get("operator_verification") if isinstance(raw, dict) else None - account_verification = raw.get("account_verification") if isinstance(raw, dict) else None - if not isinstance(operator_verification, dict): - operator_verification = {} - if not isinstance(account_verification, dict): - account_verification = {} - identity = A2AAgentCardIdentity( - issuer=issuer, - operator_id=data.resolved_operator, - kyc_level=account_verification.get("kyc_level") or operator_verification.get("level") or "none", - sanctions_clear=account_verification.get("sanctions_clear") is True, - age_bracket=account_verification.get("age_bracket", "unknown"), - jurisdiction=account_verification.get("jurisdiction", ""), - verified_at=account_verification.get("verified_at") or operator_verification.get("verified_at"), - verify_url=verify_url or data.verify_url or f"{issuer}/verify", + if not skills: + msg = ( + "build_a2a_agent_card: `skills` MUST be a non-empty list. Per spec §4.4.1 " + "(proto field 12 [field_behavior=REQUIRED]), every Agent Card must declare " + "at least one AgentSkill. Construct A2AAgentCard directly to bypass." ) - + raise ValueError(msg) + capabilities = A2AAgentCardCapabilities( + streaming=streaming, + push_notifications=push_notifications, + extensions=extensions or [], + extended_agent_card=extended_agent_card, + ) + interface = A2AAgentInterface( + url=url, + protocol_binding=protocol_binding, + protocol_version=a2a_protocol_version, + ) return A2AAgentCard( name=name, - identity=identity, description=description, - url=url, + supported_interfaces=[interface], + version=version, capabilities=capabilities, - extensions=extensions or [], + default_input_modes=default_input_modes or [_DEFAULT_INPUT_MODE], + default_output_modes=default_output_modes or [_DEFAULT_OUTPUT_MODE], + skills=skills, + provider=provider, + documentation_url=documentation_url, + icon_url=icon_url, + signatures=signatures or [], + security_schemes=security_schemes or {}, + security_requirements=security_requirements or [], extras=extras or {}, ) @@ -221,7 +363,10 @@ def build_a2a_agent_card( "A2AAgentCard", "A2AAgentCardCapabilities", "A2AAgentCardExtension", - "A2AAgentCardIdentity", + "A2AAgentCardSignature", + "A2AAgentInterface", + "A2AAgentProvider", + "A2AAgentSkill", "build_a2a_agent_card", "ucp_a2a_extension", ] diff --git a/agentscore_commerce/identity/aiohttp.py b/agentscore_commerce/identity/aiohttp.py index 4fa0e86..7997ee0 100644 --- a/agentscore_commerce/identity/aiohttp.py +++ b/agentscore_commerce/identity/aiohttp.py @@ -71,7 +71,7 @@ def _mark_degraded_aiohttp(request: web.Request, infra_reason: str) -> None: "denial_reason_to_body", "extract_payment_signer", "extract_payment_signer_address", - "get_assess_data", + "get_agentscore_data", "get_gate_degraded_state", "get_gate_quota_info", "is_fixable_denial", @@ -81,7 +81,7 @@ def _mark_degraded_aiohttp(request: web.Request, infra_reason: str) -> None: ] -def get_assess_data(request: web.Request) -> dict[str, Any] | None: +def get_agentscore_data(request: web.Request) -> dict[str, Any] | None: """Return the `/v1/assess` response the middleware stashed on the aiohttp request dict. Returns ``None`` when identity was missing or the gate short-circuited with a diff --git a/agentscore_commerce/identity/django.py b/agentscore_commerce/identity/django.py index bc584eb..31ed987 100644 --- a/agentscore_commerce/identity/django.py +++ b/agentscore_commerce/identity/django.py @@ -66,7 +66,7 @@ def _mark_degraded_django(request: HttpRequest, infra_reason: str) -> None: "denial_reason_to_body", "extract_payment_signer", "extract_payment_signer_address", - "get_assess_data", + "get_agentscore_data", "get_gate_degraded_state", "get_gate_quota_info", "is_fixable_denial", @@ -76,7 +76,7 @@ def _mark_degraded_django(request: HttpRequest, infra_reason: str) -> None: ] -def get_assess_data(request: HttpRequest) -> dict[str, Any] | None: +def get_agentscore_data(request: HttpRequest) -> dict[str, Any] | None: """Return the `/v1/assess` response the middleware stashed on the request. Returns ``None`` when identity was missing or the gate short-circuited with a diff --git a/agentscore_commerce/identity/fastapi.py b/agentscore_commerce/identity/fastapi.py index b51bb09..3ba54b4 100644 --- a/agentscore_commerce/identity/fastapi.py +++ b/agentscore_commerce/identity/fastapi.py @@ -3,7 +3,7 @@ The adapter plugs into FastAPI's dependency-injection system. Unlike the generic ASGI middleware (which gates every request), this adapter lets you scope gating to specific routes via ``dependencies=[Depends(gate)]`` and inject the assess result with -``Depends(get_assess_data)``. +``Depends(get_agentscore_data)``. """ from __future__ import annotations @@ -112,7 +112,7 @@ def get_gate_quota_info(request: Request) -> GateQuotaInfo | None: "denial_reason_to_body", "extract_payment_signer", "extract_payment_signer_address", - "get_assess_data", + "get_agentscore_data", "get_gate_degraded_state", "get_gate_quota_info", "is_fixable_denial", @@ -154,13 +154,13 @@ class AgentScoreGate: Usage:: from fastapi import Depends, FastAPI - from agentscore_commerce.identity.fastapi import AgentScoreGate, get_assess_data + from agentscore_commerce.identity.fastapi import AgentScoreGate, get_agentscore_data app = FastAPI() gate = AgentScoreGate(api_key="ask_...", require_kyc=True, min_age=21) @app.post("/purchase", dependencies=[Depends(gate)]) - async def purchase(assess = Depends(get_assess_data)): + async def purchase(assess = Depends(get_agentscore_data)): # assess is the raw /v1/assess response dict ... """ @@ -306,14 +306,14 @@ async def __call__(self, request: Request) -> None: ) -def get_assess_data(request: Request) -> dict[str, Any] | None: +def get_agentscore_data(request: Request) -> dict[str, Any] | None: """FastAPI dependency that returns the raw ``/v1/assess`` response for the current request. Returns ``None`` when the gate was bypassed via ``fail_open`` or the route wasn't gated. Usage:: @app.post("/purchase", dependencies=[Depends(gate)]) - async def purchase(assess = Depends(get_assess_data)): + async def purchase(assess = Depends(get_agentscore_data)): ... """ return getattr(request.state, ASSESS_STATE_KEY, None) @@ -355,7 +355,7 @@ async def capture_wallet( Usage:: @app.post("/purchase", dependencies=[Depends(gate)]) - async def purchase(request: Request, assess = Depends(get_assess_data)): + async def purchase(request: Request, assess = Depends(get_agentscore_data)): # ... run payment, recover signer wallet from the payload ... await capture_wallet(request, signer, "evm", idempotency_key=payment_intent_id) return {"ok": True} diff --git a/agentscore_commerce/identity/flask.py b/agentscore_commerce/identity/flask.py index cd866d6..33b531c 100644 --- a/agentscore_commerce/identity/flask.py +++ b/agentscore_commerce/identity/flask.py @@ -64,7 +64,7 @@ "denial_reason_to_body", "extract_payment_signer", "extract_payment_signer_address", - "get_assess_data", + "get_agentscore_data", "get_gate_degraded_state", "get_gate_quota_info", "is_fixable_denial", @@ -74,7 +74,7 @@ ] -def get_assess_data() -> dict[str, Any] | None: +def get_agentscore_data() -> dict[str, Any] | None: """Return the `/v1/assess` response the gate stashed on Flask's request-scoped ``g``. Returns ``None`` outside a gated request, when identity was missing, or when the gate diff --git a/agentscore_commerce/identity/middleware.py b/agentscore_commerce/identity/middleware.py index 30dfa1c..7cffa04 100644 --- a/agentscore_commerce/identity/middleware.py +++ b/agentscore_commerce/identity/middleware.py @@ -73,7 +73,7 @@ def _mark_degraded_asgi(scope: Scope, infra_reason: str) -> None: "denial_reason_to_body", "extract_payment_signer", "extract_payment_signer_address", - "get_assess_data", + "get_agentscore_data", "get_gate_degraded_state", "get_gate_quota_info", "is_fixable_denial", @@ -83,7 +83,7 @@ def _mark_degraded_asgi(scope: Scope, infra_reason: str) -> None: ] -def get_assess_data(request: Request) -> dict[str, Any] | None: +def get_agentscore_data(request: Request) -> dict[str, Any] | None: """Return the `/v1/assess` response the middleware stashed on the request scope. Returns ``None`` when identity was missing or the gate short-circuited with a diff --git a/agentscore_commerce/identity/sanic.py b/agentscore_commerce/identity/sanic.py index 7827512..14cc811 100644 --- a/agentscore_commerce/identity/sanic.py +++ b/agentscore_commerce/identity/sanic.py @@ -71,7 +71,7 @@ def _mark_degraded_sanic(request: Request, infra_reason: str) -> None: "denial_reason_to_body", "extract_payment_signer", "extract_payment_signer_address", - "get_assess_data", + "get_agentscore_data", "get_gate_degraded_state", "get_gate_quota_info", "is_fixable_denial", @@ -81,7 +81,7 @@ def _mark_degraded_sanic(request: Request, infra_reason: str) -> None: ] -def get_assess_data(request: Request) -> dict[str, Any] | None: +def get_agentscore_data(request: Request) -> dict[str, Any] | None: """Return the `/v1/assess` response the middleware stashed on ``request.ctx``. Returns ``None`` when identity was missing or the gate short-circuited with a diff --git a/agentscore_commerce/identity/types.py b/agentscore_commerce/identity/types.py index 79d3705..6dce6be 100644 --- a/agentscore_commerce/identity/types.py +++ b/agentscore_commerce/identity/types.py @@ -5,9 +5,10 @@ from agentscore import Network as Network # noqa: TC002 — runtime re-export for vendors -Grade = Literal["A", "B", "C", "D", "F"] - -ScoreStatus = Literal["scored", "stale", "known_unscored"] +# Reputation-API types (Activity, Classification, Identity, Reputation, ScoreDetail, +# Grade, ScoreStatus) live in agentscore-py — not re-exported here. Commerce SDK is +# scoped to gate + payment + 402 + discovery; reputation lookups belong to the +# AgentScore SDK. Import via `from agentscore import Activity, Classification, ...`. DenialCode = Literal[ "wallet_not_trusted", @@ -175,72 +176,6 @@ def build_agent_memory_hint() -> AgentMemoryHint: ) -@dataclass -class ScoreDetail: - """Typed score breakdown from the assess response.""" - - value: float | None - grade: Grade - status: ScoreStatus - confidence: float | None = None - scored_at: str | None = None - version: str | None = None - dimensions: dict[str, Any] | None = None - - -@dataclass -class Activity: - """On-chain activity summary from the assess response.""" - - total_verified_transactions: int = 0 - total_candidate_transactions: int = 0 - counterparties_count: int = 0 - active_days: int = 0 - active_months: int = 0 - as_verified_payer: int = 0 - as_verified_payee: int = 0 - as_candidate_payer: int = 0 - as_candidate_payee: int = 0 - first_verified_tx_at: str | None = None - last_verified_tx_at: str | None = None - first_candidate_tx_at: str | None = None - last_candidate_tx_at: str | None = None - - -@dataclass -class Classification: - """Entity classification from the assess response.""" - - entity_type: str | None = None - confidence: float = 0.0 - is_known: bool = False - is_known_erc8004_agent: bool = False - has_verified_payment_activity: bool = False - has_candidate_payment_activity: bool = False - reasons: list[str] = field(default_factory=list) - - -@dataclass -class Identity: - """Known identity links from the assess response.""" - - ens_name: str | None = None - github_url: str | None = None - website_url: str | None = None - - -@dataclass -class Reputation: - """On-chain reputation feedback summary.""" - - feedback_count: int = 0 - client_count: int = 0 - trust_avg: float | None = None - uptime_avg: float | None = None - activity_avg: float | None = None - last_feedback_at: str | None = None - - @dataclass class OperatorVerification: """Operator verification details from the assess response.""" @@ -309,9 +244,9 @@ class AssessResult: identity_method: str | None = None operator_verification: OperatorVerification | None = None # Account-level verification block (KYC level, age bracket, jurisdiction, - # sanctions verdict). Mirrors node-commerce's typed AgentScoreData.account_verification - # field so a hand-constructed AssessResult emits the same UCP claims in both - # languages without a raw-dict round trip. + # sanctions verdict). Mirrors node-commerce's typed + # AgentScoreData.account_verification field; consumed by the A2A agent-card + # builder when emitting per-card identity claims. account_verification: dict[str, Any] | None = None resolved_operator: str | None = None verify_url: str | None = None diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index b3cedd1..74b0819 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -18,10 +18,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Literal, cast - -if TYPE_CHECKING: - from agentscore_commerce.identity.types import AssessResult +from typing import Any, Literal _DEFAULT_VERSION = "2026-04-08" @@ -32,11 +29,53 @@ """Capability name AgentScore registers in the UCP profile. Consumers filter on this to find verified-buyer claims attached to the profile.""" -_AGENTSCORE_CAPABILITY_VERSION = "1" +_AGENTSCORE_CAPABILITY_VERSION = "2026-04-08" _AGENTSCORE_DEFAULT_SPEC_URL = "https://agentscore.sh/specification/identity" _AGENTSCORE_DEFAULT_SCHEMA_URL = "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json" +@dataclass +class AgentScoreGatePolicy: + """Merchant gate policy declared on the UCP profile via ``sh.agentscore.identity``. + + All fields optional; merchant declares which AgentScore checks the gate enforces. + Snake-case field names match the AgentScore API's ``/v1/assess`` policy contract + verbatim. No conversion layer between this declaration and what the gate enforces. + """ + + require_kyc: bool | None = None + """Gate denies if the operator/account behind the agent is not Stripe-Identity-verified.""" + + require_sanctions_clear: bool | None = None + """Gate denies if the operator/account is flagged by OpenSanctions screening.""" + + min_age: int | None = None + """Gate denies if the verified age (from KYC) is below this threshold. Common: 18, 21.""" + + allowed_jurisdictions: list[str] | None = None + """ISO-3166-1 alpha-2 country codes the gate accepts. Mutually exclusive with + ``blocked_jurisdictions``.""" + + blocked_jurisdictions: list[str] | None = None + """ISO-3166-1 alpha-2 country codes the gate denies. Mutually exclusive with + ``allowed_jurisdictions``.""" + + def to_config(self) -> dict[str, Any]: + """Serialize as the binding's ``config`` object. Omits unset fields.""" + out: dict[str, Any] = {} + if self.require_kyc is not None: + out["require_kyc"] = self.require_kyc + if self.require_sanctions_clear is not None: + out["require_sanctions_clear"] = self.require_sanctions_clear + if self.min_age is not None: + out["min_age"] = self.min_age + if self.allowed_jurisdictions is not None: + out["allowed_jurisdictions"] = self.allowed_jurisdictions + if self.blocked_jurisdictions is not None: + out["blocked_jurisdictions"] = self.blocked_jurisdictions + return out + + @dataclass class UCPSigningKey: """JWK entry for the profile's ``signing_keys`` array. @@ -121,6 +160,16 @@ class UCPServiceBinding: _RESERVED = frozenset({"version", "spec", "transport", "endpoint", "schema", "id", "config"}) def to_dict(self) -> dict[str, Any]: + # Per UCP spec service.json: rest/mcp/a2a transports REQUIRE endpoint; + # embedded does not. Validate at serialization so a misconfigured profile + # fails locally instead of being rejected by spec-strict platforms. + if self.transport in ("rest", "mcp", "a2a") and self.endpoint is None: + msg = ( + f"UCPServiceBinding(transport={self.transport!r}) requires `endpoint`. " + "Per UCP spec service.json business_schema, rest/mcp/a2a bindings MUST " + "carry an endpoint URL." + ) + raise ValueError(msg) out: dict[str, Any] = { "version": self.version, "spec": self.spec, @@ -200,7 +249,10 @@ def to_dict(self) -> dict[str, Any]: "spec": self.spec, "schema": self.schema, } - if self.available_instruments is not None: + # Per UCP spec payment_handler.json: available_instruments has minItems:1. + # Drop the field when empty so a caller passing `[]` doesn't ship an + # invalid profile. + if self.available_instruments: out["available_instruments"] = self.available_instruments if self.config: out["config"] = self.config @@ -295,7 +347,7 @@ def build_ucp_profile( payment_handlers: dict[str, list[UCPPaymentHandlerBinding]] | None = None, name: str | None = None, version: str = _DEFAULT_VERSION, - data: AssessResult | None = None, + agentscore_gate: AgentScoreGatePolicy | None = None, agentscore_schema_url: str | None = None, agentscore_spec_url: str | None = None, supported_versions: dict[str, str] | None = None, @@ -309,13 +361,18 @@ def build_ucp_profile( reverse-DNS name. Pass through :func:`sign_ucp_profile` to attach a JWS signature for trust-mode verifiers. - Auto-injects ``sh.agentscore.identity`` as a vendor capability when ``data`` - carries a resolved operator. Verifiers that recognize the AgentScore namespace - can parse the ``claims`` extra; vanilla UCP agents see a normal capability. + Auto-injects ``sh.agentscore.identity`` as a vendor capability extending both + ``dev.ucp.shopping.checkout`` and ``dev.ucp.shopping.cart`` when + ``agentscore_gate`` is provided. The capability's ``config`` carries the + merchant's static gate policy declaration (require_kyc / require_sanctions_clear + / min_age / allowed_jurisdictions / blocked_jurisdictions). NO per-operator + data is ever placed on the public profile — per-operator identity attestation + flows through the AP2 risk-signal endpoint, not here. Example:: - from agentscore_commerce.identity.ucp import ( + from agentscore_commerce.identity import ( + AgentScoreGatePolicy, UCPServiceBinding, UCPSigningKey, UCPPaymentHandlerBinding, @@ -324,7 +381,6 @@ def build_ucp_profile( @app.get("/.well-known/ucp") async def ucp_profile(): - result = await client.acheck(identity) return build_ucp_profile( services={ "dev.ucp.shopping": [ @@ -333,7 +389,7 @@ async def ucp_profile(): spec="https://ucp.dev/2026-04-08/specification/overview", transport="mcp", endpoint="https://merchant.example/api/ucp/mcp", - schema="https://ucp.dev/services/shopping/openrpc.json", + schema="https://ucp.dev/services/shopping/mcp.openrpc.json", ), ], }, @@ -350,7 +406,9 @@ async def ucp_profile(): ], }, name="Example Merchant", - data=result, + agentscore_gate=AgentScoreGatePolicy( + require_kyc=True, min_age=21, allowed_jurisdictions=["US"], + ), ).to_dict() """ services = services if services is not None else {} @@ -362,59 +420,19 @@ async def ucp_profile(): k: list(bindings) for k, bindings in (capabilities or {}).items() } - if data is not None and data.resolved_operator: - # Read typed AssessResult fields first (canonical path). Fall back to - # ``data.raw["operator_verification"]`` / ``data.raw["account_verification"]`` - # only when the typed field is ``None`` (Python-only legacy escape hatch - # for callers who hand-construct ``AssessResult(raw=..., typed=None)``). - # Node has no raw fallback at all. - typed_op = data.operator_verification - operator_verification: dict[str, Any] - if typed_op is None: - raw = data.raw or {} - raw_op = raw.get("operator_verification") if isinstance(raw, dict) else None - operator_verification = raw_op if isinstance(raw_op, dict) else {} - elif isinstance(typed_op, dict): - operator_verification = cast("dict[str, Any]", typed_op) - else: - operator_verification = { - "level": getattr(typed_op, "level", None), - "operator_type": getattr(typed_op, "operator_type", None), - "verified_at": getattr(typed_op, "verified_at", None), - } - - account_verification: dict[str, Any] - if data.account_verification is None: - raw = data.raw or {} - raw_av = raw.get("account_verification") if isinstance(raw, dict) else None - account_verification = raw_av if isinstance(raw_av, dict) else {} - elif isinstance(data.account_verification, dict): - account_verification = data.account_verification - else: - account_verification = {} - - # `dict.get(k) or DEFAULT` (not `dict.get(k, DEFAULT)`) coerces both a - # missing key AND a present-but-falsy (None / "") value to the default, - # matching the node sibling's `||` semantics. - claims = { - "operator_id": data.resolved_operator, - "kyc_level": account_verification.get("kyc_level") or operator_verification.get("level") or "none", - "sanctions_clear": account_verification.get("sanctions_clear") is True, - "age_bracket": account_verification.get("age_bracket") or "unknown", - "jurisdiction": account_verification.get("jurisdiction") or "", - "verified_at": account_verification.get("verified_at") or operator_verification.get("verified_at") or None, - "verify_url": data.verify_url, - "issuer": "https://agentscore.sh", - } - # Multi-parent extension matching Shopify's `dev.shopify.catalog.storefront` - # and UCP-canonical `dev.ucp.shopping.discount` (extends [checkout, cart]). - # `claims` lives in `extras` so it serializes as a vendor field on the binding. + # Auto-inject `sh.agentscore.identity` capability when the merchant declares a gate + # policy. Static merchant-policy declaration only — no per-operator data on the public + # profile. Per-operator identity attestation flows through the AP2 risk-signal endpoint + # or per-request 4xx response bodies, not here. Multi-parent extension matching + # Shopify's `dev.shopify.catalog.storefront` and UCP-canonical + # `dev.ucp.shopping.discount` (extends [checkout, cart]). + if agentscore_gate is not None: binding = UCPCapabilityBinding( version=_AGENTSCORE_CAPABILITY_VERSION, spec=agentscore_spec_url or _AGENTSCORE_DEFAULT_SPEC_URL, schema=agentscore_schema_url or _AGENTSCORE_DEFAULT_SCHEMA_URL, extends=["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"], - extras={"claims": claims}, + config=agentscore_gate.to_config(), ) if AGENTSCORE_UCP_CAPABILITY in base_capabilities: base_capabilities[AGENTSCORE_UCP_CAPABILITY].append(binding) @@ -440,6 +458,7 @@ async def ucp_profile(): __all__ = [ "AGENTSCORE_UCP_CAPABILITY", + "AgentScoreGatePolicy", "UCPCapabilityBinding", "UCPPaymentHandlerBinding", "UCPProfile", diff --git a/examples/README.md b/examples/README.md index 0a10827..744ec6c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -23,7 +23,7 @@ Runnable, copy-pasteable example integrations covering the most common merchant ## Patterns -All seven examples follow the same rough shape: +All eight examples follow the same rough shape: 1. **Boot:** instantiate FastAPI, identity gate (if any), Stripe / facilitator clients (if any) via commerce factories 2. **Discovery routes:** `/openapi.json` + `/.well-known/mpp.json` + `/llms.txt` (omitted in these focused examples; see node-commerce for the discovery wiring) diff --git a/examples/compliance_merchant.py b/examples/compliance_merchant.py index 0b12034..37e677a 100644 --- a/examples/compliance_merchant.py +++ b/examples/compliance_merchant.py @@ -44,7 +44,7 @@ is_fixable_denial, verification_agent_instructions, ) -from agentscore_commerce.identity.fastapi import AgentScoreGate, get_assess_data, verify_wallet_signer_match +from agentscore_commerce.identity.fastapi import AgentScoreGate, get_agentscore_data, verify_wallet_signer_match SUPPORT_EMAIL = "support@example.com" @@ -135,7 +135,7 @@ async def gate_on_settle(request: Request) -> None: @app.post("/buy", dependencies=[Depends(gate_on_settle)]) -async def buy(request: Request, assess: dict = Depends(get_assess_data)): +async def buy(request: Request, assess: dict = Depends(get_agentscore_data)): # Wallet-auth: verify the payment signer matches the claimed wallet (or a same-operator # linked wallet). No-ops for operator_token requests. Pass `signer=` from your real x402/MPP # credential extraction (use extract_payment_signer from commerce.payment, etc.). diff --git a/examples/identity_only.py b/examples/identity_only.py index d958026..e5d03eb 100644 --- a/examples/identity_only.py +++ b/examples/identity_only.py @@ -19,7 +19,7 @@ from fastapi import Depends, FastAPI -from agentscore_commerce.identity.fastapi import AgentScoreGate, get_assess_data +from agentscore_commerce.identity.fastapi import AgentScoreGate, get_agentscore_data app = FastAPI() @@ -33,7 +33,7 @@ @app.post("/deliver", dependencies=[Depends(gate)]) -async def deliver(assess: dict = Depends(get_assess_data)): +async def deliver(assess: dict = Depends(get_agentscore_data)): """Gated route — only reached when the agent passes the compliance policy. `assess` is the raw `/v1/assess` response. Use it for downstream business logic that diff --git a/examples/multi_rail_merchant.py b/examples/multi_rail_merchant.py index 794eb58..46e3cb5 100644 --- a/examples/multi_rail_merchant.py +++ b/examples/multi_rail_merchant.py @@ -62,7 +62,7 @@ first_encounter_agent_memory, respond_402, ) -from agentscore_commerce.identity.fastapi import AgentScoreGate, get_assess_data +from agentscore_commerce.identity.fastapi import AgentScoreGate, get_agentscore_data from agentscore_commerce.payment import ( USDC, PaymentRequiredHeaderInput, @@ -128,7 +128,7 @@ async def gate_on_settle(request: Request) -> None: @app.post("/purchase", dependencies=[Depends(gate_on_settle)]) -async def purchase(request: Request, assess: dict = Depends(get_assess_data)): +async def purchase(request: Request, assess: dict = Depends(get_agentscore_data)): body = await request.json() # Compute pricing (vendor-specific — wine tax by state, dynamic SKU pricing, etc.) diff --git a/examples/signed_ucp_merchant.py b/examples/signed_ucp_merchant.py index 79b5d93..b16b8aa 100644 --- a/examples/signed_ucp_merchant.py +++ b/examples/signed_ucp_merchant.py @@ -38,6 +38,7 @@ from fastapi.responses import JSONResponse from agentscore_commerce.identity import ( + AgentScoreGatePolicy, UCPPaymentHandlerBinding, UCPServiceBinding, UCPSigningKey, @@ -116,7 +117,7 @@ async def well_known_ucp() -> JSONResponse: spec="https://ucp.dev/2026-04-08/specification/overview", transport="mcp", endpoint="https://agents.example.com/api/ucp/mcp", - schema="https://ucp.dev/services/shopping/openrpc.json", + schema="https://ucp.dev/services/shopping/mcp.openrpc.json", ), ], }, @@ -132,6 +133,14 @@ async def well_known_ucp() -> JSONResponse: ], }, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + # Optional: declare merchant gate policy as an `sh.agentscore.identity` capability + # binding inside the public profile. Static policy declaration only — no per-operator + # claims. Per-operator identity attestation flows through the AP2 risk-signal endpoint. + agentscore_gate=AgentScoreGatePolicy( + require_kyc=True, + min_age=21, + allowed_jurisdictions=["US"], + ), ) signed = sign_ucp_profile( profile.to_dict(), diff --git a/pyproject.toml b/pyproject.toml index 69558c1..202f325 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-commerce" -version = "1.5.1" +version = "1.6.0" description = "Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce." readme = "README.md" license = "MIT" diff --git a/scripts/regenerate_cross_lang_fixtures.py b/scripts/regenerate_cross_lang_fixtures.py index 49c41d9..5c652fb 100644 --- a/scripts/regenerate_cross_lang_fixtures.py +++ b/scripts/regenerate_cross_lang_fixtures.py @@ -22,8 +22,7 @@ from typing import Any from agentscore_commerce.identity import ( - AssessResult, - OperatorVerification, + AgentScoreGatePolicy, UCPCapabilityBinding, UCPPaymentHandlerBinding, UCPServiceBinding, @@ -65,7 +64,7 @@ def _shop_service_mcp(host: str) -> UCPServiceBinding: spec="https://ucp.dev/2026-04-08/specification/overview", transport="mcp", endpoint=f"{host}/api/ucp/mcp", - schema="https://ucp.dev/services/shopping/openrpc.json", + schema="https://ucp.dev/services/shopping/mcp.openrpc.json", ) @@ -104,8 +103,8 @@ def _stripe_handler(config: dict[str, Any]) -> UCPPaymentHandlerBinding: return UCPPaymentHandlerBinding( id="stripe", version="2026-04-08", - spec="https://agentscore.sh/specification/payment-handlers/stripe-spt", - schema="https://agentscore.sh/schemas/payment-handlers/stripe-spt.json", + spec="https://agentscore.sh/specification/payment-handlers/stripe_spt", + schema="https://agentscore.sh/schemas/payment-handlers/stripe_spt.json", config=config, ) @@ -149,7 +148,7 @@ def main() -> None: services={"dev.ucp.shopping": [_shop_service_mcp("https://e.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], payment_handlers={ - "sh.agentscore.payment.stripe-spt": [_stripe_handler({"profile_id": "abc", "count": 7})], + "sh.agentscore.payment.stripe_spt": [_stripe_handler({"profile_id": "abc", "count": 7})], }, name="Extras Merchant", ) @@ -160,7 +159,7 @@ def main() -> None: kid = "py-capability-EdDSA" key = generate_ucp_signing_key(kid=kid) custom_capability = UCPCapabilityBinding( - version="1", + version="2026-04-08", spec="https://agentscore.sh/specification/identity", schema="https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", # `extras` flat on the binding — kyc_required is a vendor field on this binding. @@ -258,64 +257,40 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-int-boundary", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-data-driven-claims — exercises build_ucp_profile data path with API-shape - # "missing" sentinels (empty string + None). Both languages MUST emit identical - # canonical bytes for this input. - kid = "py-data-driven-claims-EdDSA" + # py-agentscore-gate-full — exercises build_ucp_profile with a fully-populated + # merchant gate policy declared via `agentscore_gate`. Both languages MUST emit + # identical canonical bytes so a profile signed in one verifies in the other. + kid = "py-agentscore-gate-full-EdDSA" key = generate_ucp_signing_key(kid=kid) - result = AssessResult( - allow=True, - resolved_operator="op_data_driven", - verify_url="https://agentscore.sh/verify/op_data_driven", - raw={ - "account_verification": { - "kyc_level": "", - "sanctions_clear": False, - "age_bracket": None, - "jurisdiction": None, - "verified_at": None, - }, - }, - ) profile = build_ucp_profile( services={"dev.ucp.shopping": [_shop_service_mcp("https://d.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], - name="Data Driven Claims Merchant", - data=result, + name="AgentScore Gate Full-Policy Merchant", + agentscore_gate=AgentScoreGatePolicy( + require_kyc=True, + require_sanctions_clear=True, + min_age=21, + allowed_jurisdictions=["US"], + ), ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) - _write("py-data-driven-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) + _write("py-agentscore-gate-full", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-typed-claims — exercises typed AssessResult fields (no raw fallback). - # Cross-lang parity check for the typed-field-only call site. - kid = "py-typed-claims-EdDSA" + # py-agentscore-gate-blocked — exercises blocked_jurisdictions + # (mutually exclusive with allowed_jurisdictions) for cross-lang parity. + kid = "py-agentscore-gate-blocked-EdDSA" key = generate_ucp_signing_key(kid=kid) - result = AssessResult( - allow=True, - resolved_operator="op_typed_claims", - verify_url="https://agentscore.sh/verify/op_typed_claims", - operator_verification=OperatorVerification( - level="enhanced", - operator_type="api", - verified_at="2026-04-01T00:00:00Z", - ), - account_verification={ - "kyc_level": "enhanced", - "sanctions_clear": True, - "age_bracket": "21+", - "jurisdiction": "US", - "verified_at": "2026-04-01T00:00:00Z", - }, - raw=None, - ) profile = build_ucp_profile( services={"dev.ucp.shopping": [_shop_service_mcp("https://t.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], - name="Typed Claims Merchant", - data=result, + name="AgentScore Gate Blocked-Jurisdictions Merchant", + agentscore_gate=AgentScoreGatePolicy( + require_kyc=True, + blocked_jurisdictions=["KP", "IR", "CU"], + ), ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) - _write("py-typed-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) + _write("py-agentscore-gate-blocked", _envelope(signed, key.public_jwk, "EdDSA", kid)) if __name__ == "__main__": diff --git a/tests/fixtures/cross-lang/node-agentscore-gate-blocked.json b/tests/fixtures/cross-lang/node-agentscore-gate-blocked.json new file mode 100644 index 0000000..6dc1781 --- /dev/null +++ b/tests/fixtures/cross-lang/node-agentscore-gate-blocked.json @@ -0,0 +1,67 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-08", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://t.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "config": { + "require_kyc": true, + "blocked_jurisdictions": [ + "KP", + "IR", + "CU" + ] + } + } + ] + }, + "payment_handlers": {}, + "name": "AgentScore Gate Blocked-Jurisdictions Merchant" + }, + "signing_keys": [ + { + "kid": "node-agentscore-gate-blocked-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "ffpNqM2SBHyKorvO-lDKzycyMdh4AgMiOZsFjxj-hPU" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtYWdlbnRzY29yZS1nYXRlLWJsb2NrZWQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtYWdlbnRzY29yZS1nYXRlLWJsb2NrZWQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZmZwTnFNMlNCSHlLb3J2Ty1sREt6eWN5TWRoNEFnTWlPWnNGanhqLWhQVSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6eyJzaC5hZ2VudHNjb3JlLmlkZW50aXR5IjpbeyJjb25maWciOnsiYmxvY2tlZF9qdXJpc2RpY3Rpb25zIjpbIktQIiwiSVIiLCJDVSJdLCJyZXF1aXJlX2t5YyI6dHJ1ZX0sImV4dGVuZHMiOlsiZGV2LnVjcC5zaG9wcGluZy5jaGVja291dCIsImRldi51Y3Auc2hvcHBpbmcuY2FydCJdLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL2lkZW50aXR5IiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJuYW1lIjoiQWdlbnRTY29yZSBHYXRlIEJsb2NrZWQtSnVyaXNkaWN0aW9ucyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vdC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9tY3Aub3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.wLBzWguXqHRyyD3md8nfKxdr2ge5eo7UfsYZ8yqArNay_lFx-q41T06FJCu0akan6hrjYoPymMVoDrHLD-GRAg" + }, + "jwks": { + "keys": [ + { + "kid": "node-agentscore-gate-blocked-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "ffpNqM2SBHyKorvO-lDKzycyMdh4AgMiOZsFjxj-hPU" + } + ] + }, + "alg": "EdDSA", + "kid": "node-agentscore-gate-blocked-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-agentscore-gate-full.json b/tests/fixtures/cross-lang/node-agentscore-gate-full.json new file mode 100644 index 0000000..60007ad --- /dev/null +++ b/tests/fixtures/cross-lang/node-agentscore-gate-full.json @@ -0,0 +1,67 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-08", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://d.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "config": { + "require_kyc": true, + "require_sanctions_clear": true, + "min_age": 21, + "allowed_jurisdictions": [ + "US" + ] + } + } + ] + }, + "payment_handlers": {}, + "name": "AgentScore Gate Full-Policy Merchant" + }, + "signing_keys": [ + { + "kid": "node-agentscore-gate-full-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "vdNcZ9OUb60NP-oo7JVSbcknT0UQPNWadgrDUyH0Ouk" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtYWdlbnRzY29yZS1nYXRlLWZ1bGwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtYWdlbnRzY29yZS1nYXRlLWZ1bGwtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoidmROY1o5T1ViNjBOUC1vbzdKVlNiY2tuVDBVUVBOV2FkZ3JEVXlIME91ayJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6eyJzaC5hZ2VudHNjb3JlLmlkZW50aXR5IjpbeyJjb25maWciOnsiYWxsb3dlZF9qdXJpc2RpY3Rpb25zIjpbIlVTIl0sIm1pbl9hZ2UiOjIxLCJyZXF1aXJlX2t5YyI6dHJ1ZSwicmVxdWlyZV9zYW5jdGlvbnNfY2xlYXIiOnRydWV9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwibmFtZSI6IkFnZW50U2NvcmUgR2F0ZSBGdWxsLVBvbGljeSBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9tY3Aub3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.Eqb8g9LSGrBg3GytCXBHdP_kJJiWom1KgsJ9qqkHZA0ZK4qeIuYzTBhSJMvTbbFqEBsipIoNsH8Fy1QMFg0AAA" + }, + "jwks": { + "keys": [ + { + "kid": "node-agentscore-gate-full-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "vdNcZ9OUb60NP-oo7JVSbcknT0UQPNWadgrDUyH0Ouk" + } + ] + }, + "alg": "EdDSA", + "kid": "node-agentscore-gate-full-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-capability.json b/tests/fixtures/cross-lang/node-capability.json index b7eeed7..509fc04 100644 --- a/tests/fixtures/cross-lang/node-capability.json +++ b/tests/fixtures/cross-lang/node-capability.json @@ -9,14 +9,14 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://c.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, "capabilities": { "sh.agentscore.identity": [ { - "version": "1", + "version": "2026-04-08", "spec": "https://agentscore.sh/specification/identity", "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", "kyc_required": true @@ -46,10 +46,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "X2stNKskrWqDT_5KngcAN-RkUu-Ieya3USNRHr6lqPc" + "x": "cIRqpxDKrS5C6Vl5HOk8HLOpj-5VQzMxt4fJB_wDToQ" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJYMnN0Tktza3JXcURUXzVLbmdjQU4tUmtVdS1JZXlhM1VTTlJIcjZscVBjIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.ZGG687JXm2o7NMLsdAi8recA0hYf3wQrmNfa7HAnZh3xowZ2w93DjMKft3RPNSF8ZMicxggyqezskbVMPzPkDQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJjSVJxcHhES3JTNUM2Vmw1SE9rOEhMT3BqLTVWUXpNeHQ0ZkpCX3dEVG9RIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL21jcC5vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.s25npLIHeGubKStp1kT99XSKYyFrV-9w4hh4vHIVKBG3VYCFn8ECfNxa-q0WwjnZTIMR73f-B29lMuuImr0MBA" }, "jwks": { "keys": [ @@ -59,7 +59,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "X2stNKskrWqDT_5KngcAN-RkUu-Ieya3USNRHr6lqPc" + "x": "cIRqpxDKrS5C6Vl5HOk8HLOpj-5VQzMxt4fJB_wDToQ" } ] }, diff --git a/tests/fixtures/cross-lang/node-data-driven-claims.json b/tests/fixtures/cross-lang/node-data-driven-claims.json deleted file mode 100644 index 271de0c..0000000 --- a/tests/fixtures/cross-lang/node-data-driven-claims.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "profile": { - "ucp": { - "version": "2026-04-08", - "services": { - "dev.ucp.shopping": [ - { - "version": "2026-04-08", - "spec": "https://ucp.dev/2026-04-08/specification/overview", - "transport": "mcp", - "endpoint": "https://d.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" - } - ] - }, - "capabilities": { - "sh.agentscore.identity": [ - { - "version": "1", - "spec": "https://agentscore.sh/specification/identity", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "extends": [ - "dev.ucp.shopping.checkout", - "dev.ucp.shopping.cart" - ], - "claims": { - "operator_id": "op_data_driven", - "kyc_level": "none", - "sanctions_clear": false, - "age_bracket": "unknown", - "jurisdiction": "", - "verified_at": null, - "verify_url": "https://agentscore.sh/verify/op_data_driven", - "issuer": "https://agentscore.sh" - } - } - ] - }, - "payment_handlers": {}, - "name": "Data Driven Claims Merchant" - }, - "signing_keys": [ - { - "kid": "node-data-driven-claims-EdDSA", - "alg": "EdDSA", - "use": "sig", - "crv": "Ed25519", - "kty": "OKP", - "x": "Ex__9-UJq57zdUnDjihuTriMCT66Mxp8AlhmPXhxnAw" - } - ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IkV4X185LVVKcTU3emRVbkRqaWh1VHJpTUNUNjZNeHA4QWxobVBYaHhuQXcifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnsic2guYWdlbnRzY29yZS5pZGVudGl0eSI6W3siY2xhaW1zIjp7ImFnZV9icmFja2V0IjoidW5rbm93biIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IiIsImt5Y19sZXZlbCI6Im5vbmUiLCJvcGVyYXRvcl9pZCI6Im9wX2RhdGFfZHJpdmVuIiwic2FuY3Rpb25zX2NsZWFyIjpmYWxzZSwidmVyaWZpZWRfYXQiOm51bGwsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX2RhdGFfZHJpdmVuIn0sImV4dGVuZHMiOlsiZGV2LnVjcC5zaG9wcGluZy5jaGVja291dCIsImRldi51Y3Auc2hvcHBpbmcuY2FydCJdLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL2lkZW50aXR5IiwidmVyc2lvbiI6IjEifV19LCJuYW1lIjoiRGF0YSBEcml2ZW4gQ2xhaW1zIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9kLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.9H0tKbXw99RsDCZXhBZ1ug0StgArOL07Ue-yc4IdJtKmXsoHBaO2bfo2ygVyGz3XK2TQtGvrA4N9wmSdky9MCQ" - }, - "jwks": { - "keys": [ - { - "kid": "node-data-driven-claims-EdDSA", - "alg": "EdDSA", - "use": "sig", - "crv": "Ed25519", - "kty": "OKP", - "x": "Ex__9-UJq57zdUnDjihuTriMCT66Mxp8AlhmPXhxnAw" - } - ] - }, - "alg": "EdDSA", - "kid": "node-data-driven-claims-EdDSA", - "generator": "node" -} diff --git a/tests/fixtures/cross-lang/node-emoji-keys.json b/tests/fixtures/cross-lang/node-emoji-keys.json index 0cc8427..013af76 100644 --- a/tests/fixtures/cross-lang/node-emoji-keys.json +++ b/tests/fixtures/cross-lang/node-emoji-keys.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://emoji.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -33,14 +33,14 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "eW7hE4Mqxlub3PeI4yBjv4gY3om8ld1OIcYSCwTafV8" + "x": "1lyxweR0JJrFaMwYCCur27hwUEZMozZHccwuAaAZ2po" } ], "a": 1, "豈": 2, "": 3, "🍷": 4, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImVXN2hFNE1xeGx1YjNQZUk0eUJqdjRnWTNvbThsZDFPSWNZU0N3VGFmVjgifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnt9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifSwi6LGIIjoyLCLwn423Ijo0fQ.zG0olreIhuT-2zPs2UEcXaKS5ixqpOCpA65GouZvlpcq7O81zst6nJEL7ZxCT4Up-Mk9ZPv2M3nkcO3u7QB-DA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjFseXh3ZVIwSkpyRmFNd1lDQ3VyMjdod1VFWk1velpIY2N3dUFhQVoycG8ifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnt9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9tY3Aub3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In0sIuixiCI6Miwi8J-NtyI6NH0.kaIXBy_GOX_8dfmSEgxKhj2Elr3xfsWGsGHlMl0SVl6Qhf5Ngqi759_4fgRehat_tZ9qJcmt_rmnUkiPzBkKAA" }, "jwks": { "keys": [ @@ -50,7 +50,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "eW7hE4Mqxlub3PeI4yBjv4gY3om8ld1OIcYSCwTafV8" + "x": "1lyxweR0JJrFaMwYCCur27hwUEZMozZHccwuAaAZ2po" } ] }, diff --git a/tests/fixtures/cross-lang/node-es256-rails.json b/tests/fixtures/cross-lang/node-es256-rails.json index ef430ed..427c61f 100644 --- a/tests/fixtures/cross-lang/node-es256-rails.json +++ b/tests/fixtures/cross-lang/node-es256-rails.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://a.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" }, { "version": "2026-04-08", @@ -56,11 +56,11 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "lLlT6hqlccYtAYP4l5tVP9jDETRSocwEh_Z1EEDbjA4", - "y": "IHrRW8GTsA5Jib53LG5B28i9t6PX5nvr86uZrdhhfH0" + "x": "rTEyD9j6Bwmi8cINDuTXIPZJKsOhroiPxNko2lUm4ug", + "y": "g2nukIWNA3RXPNMlOoGqMezC7kHOEsuigivBUXUnkDE" } ], - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJub2RlLWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoibExsVDZocWxjY1l0QVlQNGw1dFZQOWpERVRSU29jd0VoX1oxRUVEYmpBNCIsInkiOiJJSHJSVzhHVHNBNUppYjUzTEc1QjI4aTl0NlBYNW52cjg2dVpyZGhoZkgwIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVTMjU2IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV0sInNoLmFnZW50c2NvcmUucGF5bWVudC54NDAyIjpbeyJjb25maWciOnsibmV0d29ya3MiOlsiYmFzZS04NDUzIl19LCJpZCI6Ing0MDIiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3g0MDIuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3g0MDIiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9hLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifSx7ImVuZHBvaW50IjoiaHR0cHM6Ly9hLmV4YW1wbGUuY29tLy53ZWxsLWtub3duL2FnZW50LWNhcmQuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoiYTJhIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.mYvpnIyTko3x80Yhza-iqIT-ifeVj6qwAkOy3sHeOk-GmOp4hTfyNLgS4NVhfHr-vyYeJcqHyXfGMLhH5GRXyw" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJub2RlLWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiclRFeUQ5ajZCd21pOGNJTkR1VFhJUFpKS3NPaHJvaVB4TmtvMmxVbTR1ZyIsInkiOiJnMm51a0lXTkEzUlhQTk1sT29HcU1lekM3a0hPRXN1aWdpdkJVWFVua0RFIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVTMjU2IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV0sInNoLmFnZW50c2NvcmUucGF5bWVudC54NDAyIjpbeyJjb25maWciOnsibmV0d29ya3MiOlsiYmFzZS04NDUzIl19LCJpZCI6Ing0MDIiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3g0MDIuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3g0MDIiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9hLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL21jcC5vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In0seyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS8ud2VsbC1rbm93bi9hZ2VudC1jYXJkLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6ImEyYSIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.-z0gMcjpn2kWi5q5Z3cADjx56vqVlzuca442pO61E8xUnYtTbwyMXGIicEy052yl6pCmxFKpcV6xddkzFzzmHQ" }, "jwks": { "keys": [ @@ -70,8 +70,8 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "lLlT6hqlccYtAYP4l5tVP9jDETRSocwEh_Z1EEDbjA4", - "y": "IHrRW8GTsA5Jib53LG5B28i9t6PX5nvr86uZrdhhfH0" + "x": "rTEyD9j6Bwmi8cINDuTXIPZJKsOhroiPxNko2lUm4ug", + "y": "g2nukIWNA3RXPNMlOoGqMezC7kHOEsuigivBUXUnkDE" } ] }, diff --git a/tests/fixtures/cross-lang/node-extras-int.json b/tests/fixtures/cross-lang/node-extras-int.json index 77370e8..029f846 100644 --- a/tests/fixtures/cross-lang/node-extras-int.json +++ b/tests/fixtures/cross-lang/node-extras-int.json @@ -9,18 +9,18 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://e.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, "capabilities": {}, "payment_handlers": { - "sh.agentscore.payment.stripe-spt": [ + "sh.agentscore.payment.stripe_spt": [ { "id": "stripe", "version": "2026-04-08", - "spec": "https://agentscore.sh/specification/payment-handlers/stripe-spt", - "schema": "https://agentscore.sh/schemas/payment-handlers/stripe-spt.json", + "spec": "https://agentscore.sh/specification/payment-handlers/stripe_spt", + "schema": "https://agentscore.sh/schemas/payment-handlers/stripe_spt.json", "config": { "profile_id": "abc", "count": 7 @@ -37,10 +37,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "6oWh70ciinshT6hYc8F3bi-LDe2k4awOGxh8Zgvm6_0" + "x": "8bY4p8JAZawYMyaOHz00B9cXv1sLsYEPclIhH56ZU9c" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI2b1doNzBjaWluc2hUNmhZYzhGM2JpLUxEZTJrNGF3T0d4aDhaZ3ZtNl8wIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkV4dHJhcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnN0cmlwZS1zcHQiOlt7ImNvbmZpZyI6eyJjb3VudCI6NywicHJvZmlsZV9pZCI6ImFiYyJ9LCJpZCI6InN0cmlwZSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvc3RyaXBlLXNwdC5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvc3RyaXBlLXNwdCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2UuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.MH9yEhoofwGP55zpFAxxdVVX8UJOI5rj5Y-kEOSAyR-regztzdgLJLL2U7DG2OyAX-K08j_2oHyAWwI9hfMCBw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI4Ylk0cDhKQVphd1lNeWFPSHowMEI5Y1h2MXNMc1lFUGNsSWhINTZaVTljIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkV4dHJhcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnN0cmlwZV9zcHQiOlt7ImNvbmZpZyI6eyJjb3VudCI6NywicHJvZmlsZV9pZCI6ImFiYyJ9LCJpZCI6InN0cmlwZSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvc3RyaXBlX3NwdC5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvc3RyaXBlX3NwdCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2UuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvbWNwLm9wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.9uvrpl0IE82nKBGWyLf5GvszZ3F0HL2TRs3Q9Gr3DlSwMKrS5TP3paVuwWg6b_CUHdDYKdDScwOcbSN4_ELnBw" }, "jwks": { "keys": [ @@ -50,7 +50,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "6oWh70ciinshT6hYc8F3bi-LDe2k4awOGxh8Zgvm6_0" + "x": "8bY4p8JAZawYMyaOHz00B9cXv1sLsYEPclIhH56ZU9c" } ] }, diff --git a/tests/fixtures/cross-lang/node-int-boundary.json b/tests/fixtures/cross-lang/node-int-boundary.json index dea80f9..8377f04 100644 --- a/tests/fixtures/cross-lang/node-int-boundary.json +++ b/tests/fixtures/cross-lang/node-int-boundary.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://i.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -24,7 +24,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "tqMB9yg0wzxt7RAbYRh9G2zJttQYEl1StI-ZUH-TfT4" + "x": "-Oje3qF87VTvybymL6PJdJUmAe2NOxjsbRaOoNNO6Qc" } ], "max_safe_int": 9007199254740991, @@ -32,7 +32,7 @@ "small_int": 42, "neg_small_int": -42, "zero": 0, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWludC1ib3VuZGFyeS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ0cU1COXlnMHd6eHQ3UkFiWVJoOUcyekp0dFFZRWwxU3RJLVpVSC1UZlQ0In1dLCJzbWFsbF9pbnQiOjQyLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkludCBCb3VuZGFyeSBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifSwiemVybyI6MH0.WUAFP4QuWfMtg1oNb3umalpIk8TgA3gZRR61ZeY6i3lhH2QoiaiCoD_i-ZuUX0VacPKROmNPHB7M2P2YEhMMDA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWludC1ib3VuZGFyeS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiItT2plM3FGODdWVHZ5YnltTDZQSmRKVW1BZTJOT3hqc2JSYU9vTk5PNlFjIn1dLCJzbWFsbF9pbnQiOjQyLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkludCBCb3VuZGFyeSBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9tY3Aub3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In0sInplcm8iOjB9.PmLCJdpu-40amNjiOkH28gap2WFsIYmP8TRD3-3jdnrQE5ZibmIyBPegnNzAi8ZvB1W6sl72pNXPDdooFfGjDw" }, "jwks": { "keys": [ @@ -42,7 +42,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "tqMB9yg0wzxt7RAbYRh9G2zJttQYEl1StI-ZUH-TfT4" + "x": "-Oje3qF87VTvybymL6PJdJUmAe2NOxjsbRaOoNNO6Qc" } ] }, diff --git a/tests/fixtures/cross-lang/node-minimal.json b/tests/fixtures/cross-lang/node-minimal.json index 1480878..ffa0239 100644 --- a/tests/fixtures/cross-lang/node-minimal.json +++ b/tests/fixtures/cross-lang/node-minimal.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://m.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -24,10 +24,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "4k9JLt7VYeohDntkv9QWirs8S0rCcY9GRqlo6Q-Om_w" + "x": "uLYv8aI6H5s6nYXHHTKDGdh8yVCQRzDPkM0AO6IhFoI" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI0azlKTHQ3Vlllb2hEbnRrdjlRV2lyczhTMHJDY1k5R1JxbG82US1PbV93In1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6Ik1pbmltYWwgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL20uZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.oaltgn7J8k5mMmcJJDKgO3hFFcuwfmWMtQIjOdqqPLoASC23xEycv7Rxl0Kw8V4Uv-GdRWMZRkZFbEt1lR9XBw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ1TFl2OGFJNkg1czZuWVhISFRLREdkaDh5VkNRUnpEUGtNMEFPNkloRm9JIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6Ik1pbmltYWwgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL20uZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvbWNwLm9wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.fqIGhDyJowQJmEVapVofDGkB6UamNc-YJojcpGWucn7UIxvJiqmdGPjiLhpiKpMrwgzBkXe1hLBzZoNHtAPMCw" }, "jwks": { "keys": [ @@ -37,7 +37,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "4k9JLt7VYeohDntkv9QWirs8S0rCcY9GRqlo6Q-Om_w" + "x": "uLYv8aI6H5s6nYXHHTKDGdh8yVCQRzDPkM0AO6IhFoI" } ] }, diff --git a/tests/fixtures/cross-lang/node-multikey.json b/tests/fixtures/cross-lang/node-multikey.json index dbf18a3..4d4b097 100644 --- a/tests/fixtures/cross-lang/node-multikey.json +++ b/tests/fixtures/cross-lang/node-multikey.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://mk.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -36,7 +36,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "uM5kVUnQNr3wjoJZsZfH_UHvr8eyZ3HkPU8c_byxSkE" + "x": "Xu4Op-Ycm4awJeGFw8b67ouLXQTgOM_fj5LrATF7_es" }, { "kid": "node-multikey-new", @@ -44,10 +44,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "m4yC2wCUwJp4fW9paM1X5WD5BkxrncIQ59FAzD7BE_Q" + "x": "WQUfG6_UAqBPjH81xLn22nzDeq4Z2BgzEk1U5KdsBFY" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbXVsdGlrZXktb2xkIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InVNNWtWVW5RTnIzd2pvSlpzWmZIX1VIdnI4ZXlaM0hrUFU4Y19ieXhTa0UifSx7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im00eUMyd0NVd0pwNGZXOXBhTTFYNVdENUJreHJuY0lRNTlGQXpEN0JFX1EifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnt9LCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.iV40cWm797CZZ-8l8nwkj0hIlNGl4pkC5THFHhSeCrtzCAeRTGgAH2j2dz4u8VF3ig6Fgih1_HzB0cY9lGSIBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbXVsdGlrZXktb2xkIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ilh1NE9wLVljbTRhd0plR0Z3OGI2N291TFhRVGdPTV9majVMckFURjdfZXMifSx7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IldRVWZHNl9VQXFCUGpIODF4TG4yMm56RGVxNFoyQmd6RWsxVTVLZHNCRlkifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnt9LCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9tY3Aub3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.Xv5w2FohwRi0BBg0ulAUA6mc8NUoS5nTlVtLlI6Gq92K7C-2UCWYQGenLteD4HUPTe6aJsZuV7fWuUkBNgDYAw" }, "jwks": { "keys": [ @@ -57,7 +57,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "uM5kVUnQNr3wjoJZsZfH_UHvr8eyZ3HkPU8c_byxSkE" + "x": "Xu4Op-Ycm4awJeGFw8b67ouLXQTgOM_fj5LrATF7_es" }, { "kid": "node-multikey-new", @@ -65,7 +65,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "m4yC2wCUwJp4fW9paM1X5WD5BkxrncIQ59FAzD7BE_Q" + "x": "WQUfG6_UAqBPjH81xLn22nzDeq4Z2BgzEk1U5KdsBFY" } ] }, diff --git a/tests/fixtures/cross-lang/node-typed-claims.json b/tests/fixtures/cross-lang/node-typed-claims.json deleted file mode 100644 index 0694918..0000000 --- a/tests/fixtures/cross-lang/node-typed-claims.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "profile": { - "ucp": { - "version": "2026-04-08", - "services": { - "dev.ucp.shopping": [ - { - "version": "2026-04-08", - "spec": "https://ucp.dev/2026-04-08/specification/overview", - "transport": "mcp", - "endpoint": "https://t.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" - } - ] - }, - "capabilities": { - "sh.agentscore.identity": [ - { - "version": "1", - "spec": "https://agentscore.sh/specification/identity", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "extends": [ - "dev.ucp.shopping.checkout", - "dev.ucp.shopping.cart" - ], - "claims": { - "operator_id": "op_typed_claims", - "kyc_level": "enhanced", - "sanctions_clear": true, - "age_bracket": "21+", - "jurisdiction": "US", - "verified_at": "2026-04-01T00:00:00Z", - "verify_url": "https://agentscore.sh/verify/op_typed_claims", - "issuer": "https://agentscore.sh" - } - } - ] - }, - "payment_handlers": {}, - "name": "Typed Claims Merchant" - }, - "signing_keys": [ - { - "kid": "node-typed-claims-EdDSA", - "alg": "EdDSA", - "use": "sig", - "crv": "Ed25519", - "kty": "OKP", - "x": "wQljNPuL3yoY1Kc1wavS5rlR5R_3FyakZ2dg1R9H5kw" - } - ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IndRbGpOUHVMM3lvWTFLYzF3YXZTNXJsUjVSXzNGeWFrWjJkZzFSOUg1a3cifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnsic2guYWdlbnRzY29yZS5pZGVudGl0eSI6W3siY2xhaW1zIjp7ImFnZV9icmFja2V0IjoiMjErIiwiaXNzdWVyIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoIiwianVyaXNkaWN0aW9uIjoiVVMiLCJreWNfbGV2ZWwiOiJlbmhhbmNlZCIsIm9wZXJhdG9yX2lkIjoib3BfdHlwZWRfY2xhaW1zIiwic2FuY3Rpb25zX2NsZWFyIjp0cnVlLCJ2ZXJpZmllZF9hdCI6IjIwMjYtMDQtMDFUMDA6MDA6MDBaIiwidmVyaWZ5X3VybCI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC92ZXJpZnkvb3BfdHlwZWRfY2xhaW1zIn0sImV4dGVuZHMiOlsiZGV2LnVjcC5zaG9wcGluZy5jaGVja291dCIsImRldi51Y3Auc2hvcHBpbmcuY2FydCJdLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL2lkZW50aXR5IiwidmVyc2lvbiI6IjEifV19LCJuYW1lIjoiVHlwZWQgQ2xhaW1zIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly90LmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.LMMkHYIobuopLGaOx6qvD9cD8bBgqllhZs-oE0vlF3BCLPfhNScs9JAsMNcpQerSET0V488mdyy-gPdeEuIuCA" - }, - "jwks": { - "keys": [ - { - "kid": "node-typed-claims-EdDSA", - "alg": "EdDSA", - "use": "sig", - "crv": "Ed25519", - "kty": "OKP", - "x": "wQljNPuL3yoY1Kc1wavS5rlR5R_3FyakZ2dg1R9H5kw" - } - ] - }, - "alg": "EdDSA", - "kid": "node-typed-claims-EdDSA", - "generator": "node" -} diff --git a/tests/fixtures/cross-lang/node-unicode.json b/tests/fixtures/cross-lang/node-unicode.json index 7ace95d..e8a5b79 100644 --- a/tests/fixtures/cross-lang/node-unicode.json +++ b/tests/fixtures/cross-lang/node-unicode.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://日本.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -36,10 +36,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "5ZjZBll_HGMmMX97BLrmhMNDzjiWeRUIOG9iWFSMxKI" + "x": "D0L5Qdojm2_MWXHiFE7kJTQSqNQyjHeYp7xv-GRDins" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI1WmpaQmxsX0hHTW1NWDk3QkxybWhNTkR6amlXZVJVSU9HOWlXRlNNeEtJIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkNhZsOpIOaXpeacrCDwn423IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJub3RlIjoi44Oh44OiIn0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL-aXpeacrC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.vhSNv0jA-cUXMKVdrUJSlrAaHAX8Xvt-7gTVFi4vo9kN4cS1D8QgBw0gOoDOtdRm0_ln0HWAGxK9qB0oKAx5Cg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJEMEw1UWRvam0yX01XWEhpRkU3a0pUUVNxTlF5akhlWXA3eHYtR1JEaW5zIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkNhZsOpIOaXpeacrCDwn423IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJub3RlIjoi44Oh44OiIn0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL-aXpeacrC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9tY3Aub3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.FidulEjqrdZPyaB4uUXaGYZHfHB3wG3cVI_xu4UXV0Qw7zEJ5VNmMmzLJ1gOji0JBOjNxgM_oCTYCyFG-a0MCw" }, "jwks": { "keys": [ @@ -49,7 +49,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "5ZjZBll_HGMmMX97BLrmhMNDzjiWeRUIOG9iWFSMxKI" + "x": "D0L5Qdojm2_MWXHiFE7kJTQSqNQyjHeYp7xv-GRDins" } ] }, diff --git a/tests/fixtures/cross-lang/py-agentscore-gate-blocked.json b/tests/fixtures/cross-lang/py-agentscore-gate-blocked.json new file mode 100644 index 0000000..ef2cd8e --- /dev/null +++ b/tests/fixtures/cross-lang/py-agentscore-gate-blocked.json @@ -0,0 +1,67 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-08", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://t.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "config": { + "require_kyc": true, + "blocked_jurisdictions": [ + "KP", + "IR", + "CU" + ] + }, + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ] + } + ] + }, + "payment_handlers": {}, + "name": "AgentScore Gate Blocked-Jurisdictions Merchant" + }, + "signing_keys": [ + { + "kid": "py-agentscore-gate-blocked-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "fOtxrTCaPPssNgw4crEHH-LhQ1rYViaxFIcoQKwyCnk" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWFnZW50c2NvcmUtZ2F0ZS1ibG9ja2VkLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWFnZW50c2NvcmUtZ2F0ZS1ibG9ja2VkLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImZPdHhyVENhUFBzc05ndzRjckVISC1MaFExcllWaWF4Rkljb1FLd3lDbmsifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnsic2guYWdlbnRzY29yZS5pZGVudGl0eSI6W3siY29uZmlnIjp7ImJsb2NrZWRfanVyaXNkaWN0aW9ucyI6WyJLUCIsIklSIiwiQ1UiXSwicmVxdWlyZV9reWMiOnRydWV9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwibmFtZSI6IkFnZW50U2NvcmUgR2F0ZSBCbG9ja2VkLUp1cmlzZGljdGlvbnMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL3QuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvbWNwLm9wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.aEUrZSEmCUJeBVQalbGVBHpKszBrD6wXYGsywBBrkw9WXU6MAZ3M5azIMamN50ZcMiBAaa9sAAwMkoRq0kY-AQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "fOtxrTCaPPssNgw4crEHH-LhQ1rYViaxFIcoQKwyCnk", + "kid": "py-agentscore-gate-blocked-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-agentscore-gate-blocked-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-agentscore-gate-full.json b/tests/fixtures/cross-lang/py-agentscore-gate-full.json new file mode 100644 index 0000000..da533c5 --- /dev/null +++ b/tests/fixtures/cross-lang/py-agentscore-gate-full.json @@ -0,0 +1,67 @@ +{ + "profile": { + "ucp": { + "version": "2026-04-08", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://d.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "config": { + "require_kyc": true, + "require_sanctions_clear": true, + "min_age": 21, + "allowed_jurisdictions": [ + "US" + ] + }, + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ] + } + ] + }, + "payment_handlers": {}, + "name": "AgentScore Gate Full-Policy Merchant" + }, + "signing_keys": [ + { + "kid": "py-agentscore-gate-full-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "aBD5Lh0N8r_mgKsde6MWzGc6uucZnrNqWE8oAXRK-XI" + } + ], + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWFnZW50c2NvcmUtZ2F0ZS1mdWxsLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWFnZW50c2NvcmUtZ2F0ZS1mdWxsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImFCRDVMaDBOOHJfbWdLc2RlNk1XekdjNnV1Y1puck5xV0U4b0FYUkstWEkifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnsic2guYWdlbnRzY29yZS5pZGVudGl0eSI6W3siY29uZmlnIjp7ImFsbG93ZWRfanVyaXNkaWN0aW9ucyI6WyJVUyJdLCJtaW5fYWdlIjoyMSwicmVxdWlyZV9reWMiOnRydWUsInJlcXVpcmVfc2FuY3Rpb25zX2NsZWFyIjp0cnVlfSwiZXh0ZW5kcyI6WyJkZXYudWNwLnNob3BwaW5nLmNoZWNrb3V0IiwiZGV2LnVjcC5zaG9wcGluZy5jYXJ0Il0sInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vaWRlbnRpdHkiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sIm5hbWUiOiJBZ2VudFNjb3JlIEdhdGUgRnVsbC1Qb2xpY3kgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2QuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvbWNwLm9wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.5CvH3TLesVMunm0gIdZ6O69XXhBV_whX6ed6HeMCFFHUiVopM2c39H9ZWqjyIQu-CAGJgUgH88nx_UOvDVoEAQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "aBD5Lh0N8r_mgKsde6MWzGc6uucZnrNqWE8oAXRK-XI", + "kid": "py-agentscore-gate-full-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-agentscore-gate-full-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-capability.json b/tests/fixtures/cross-lang/py-capability.json index cc04e06..d50074c 100644 --- a/tests/fixtures/cross-lang/py-capability.json +++ b/tests/fixtures/cross-lang/py-capability.json @@ -9,14 +9,14 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://c.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, "capabilities": { "sh.agentscore.identity": [ { - "version": "1", + "version": "2026-04-08", "spec": "https://agentscore.sh/specification/identity", "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", "kyc_required": true @@ -46,16 +46,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "ooweB0iqaf1pSzkXqgfeXeAMZKrpbN7F8Ny_Nw14kBY" + "x": "Z3bx48mm2PDFs4CMrGQqFLOh3bGQXpyyVlnOdrdhcTw" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoib293ZUIwaXFhZjFwU3prWHFnZmVYZUFNWktycGJON0Y4TnlfTncxNGtCWSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6eyJzaC5hZ2VudHNjb3JlLmlkZW50aXR5IjpbeyJreWNfcmVxdWlyZWQiOnRydWUsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vaWRlbnRpdHkiLCJ2ZXJzaW9uIjoiMSJ9XX0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.s9dfG-oFBGFonZaPCBJy6qRQqzz8n38UR5hJYKILeTO2A9rZAtRD6r0a34AdmOSMSY0VFnWt99bB-SGBzw10Bg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiWjNieDQ4bW0yUERGczRDTXJHUXFGTE9oM2JHUVhweXlWbG5PZHJkaGNUdyJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6eyJzaC5hZ2VudHNjb3JlLmlkZW50aXR5IjpbeyJreWNfcmVxdWlyZWQiOnRydWUsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vaWRlbnRpdHkiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9tY3Aub3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.Ui_HdO8qTMw7F8dqyQesfL1tXjbtliq0XSavarpW82WYu4E6dQ32ufH-f4p5RTbnY0m-Fn1zmqoA6RrZFzvjBA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "ooweB0iqaf1pSzkXqgfeXeAMZKrpbN7F8Ny_Nw14kBY", + "x": "Z3bx48mm2PDFs4CMrGQqFLOh3bGQXpyyVlnOdrdhcTw", "kid": "py-capability-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-data-driven-claims.json b/tests/fixtures/cross-lang/py-data-driven-claims.json deleted file mode 100644 index c04890a..0000000 --- a/tests/fixtures/cross-lang/py-data-driven-claims.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "profile": { - "ucp": { - "version": "2026-04-08", - "services": { - "dev.ucp.shopping": [ - { - "version": "2026-04-08", - "spec": "https://ucp.dev/2026-04-08/specification/overview", - "transport": "mcp", - "endpoint": "https://d.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" - } - ] - }, - "capabilities": { - "sh.agentscore.identity": [ - { - "version": "1", - "spec": "https://agentscore.sh/specification/identity", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "extends": [ - "dev.ucp.shopping.checkout", - "dev.ucp.shopping.cart" - ], - "claims": { - "operator_id": "op_data_driven", - "kyc_level": "none", - "sanctions_clear": false, - "age_bracket": "unknown", - "jurisdiction": "", - "verified_at": null, - "verify_url": "https://agentscore.sh/verify/op_data_driven", - "issuer": "https://agentscore.sh" - } - } - ] - }, - "payment_handlers": {}, - "name": "Data Driven Claims Merchant" - }, - "signing_keys": [ - { - "kid": "py-data-driven-claims-EdDSA", - "kty": "OKP", - "alg": "EdDSA", - "use": "sig", - "crv": "Ed25519", - "x": "UjY11Kj26MtyD01uljIaDpiaMTCK2UT_f6QdlwAmHC0" - } - ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJValkxMUtqMjZNdHlEMDF1bGpJYURwaWFNVENLMlVUX2Y2UWRsd0FtSEMwIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.09ZeEA6nYBxLtmqnyO0SnXLpFiy_78xHgKKLQT1nqXmmZBTUcoOviu1pzF-PS8XTQincVlfo7jXYPbBaNZCMBQ" - }, - "jwks": { - "keys": [ - { - "crv": "Ed25519", - "x": "UjY11Kj26MtyD01uljIaDpiaMTCK2UT_f6QdlwAmHC0", - "kid": "py-data-driven-claims-EdDSA", - "alg": "EdDSA", - "use": "sig", - "kty": "OKP" - } - ] - }, - "alg": "EdDSA", - "kid": "py-data-driven-claims-EdDSA", - "generator": "python" -} diff --git a/tests/fixtures/cross-lang/py-emoji-keys.json b/tests/fixtures/cross-lang/py-emoji-keys.json index 2b74a11..d613ebb 100644 --- a/tests/fixtures/cross-lang/py-emoji-keys.json +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://emoji.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -33,20 +33,20 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "uPJGnZ8E9RNspNvKtNtxZxqZRUqPDZFeLOeuyhauf-A" + "x": "5jwR2Mfvr9NN8HA4unDfW26CeV7NGphYgB99UN30RjA" } ], "a": 1, "豈": 2, "": 3, "🍷": 4, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZW1vamkta2V5cy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ1UEpHblo4RTlSTnNwTnZLdE50eFp4cVpSVXFQRFpGZUxPZXV5aGF1Zi1BIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVtb2ppIEtleXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZW1vamkuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In0sIuixiCI6Miwi8J-NtyI6NH0.L05I_0xvbdh7xf04gsnp8eygU4ru8YAHjQhbqb-tvpYJsWNOuxybfQe6GARFYl-P0F55pnYM2bPDiHqXjBJnDw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZW1vamkta2V5cy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI1andSMk1mdnI5Tk44SEE0dW5EZlcyNkNlVjdOR3BoWWdCOTlVTjMwUmpBIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVtb2ppIEtleXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZW1vamkuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvbWNwLm9wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9LCLosYgiOjIsIvCfjbciOjR9.imiZGrpdtj66SFOnikz57UPVVNK6hIxTUYND7BvTkY9Sb2FA2kVLkpAYpiuC3YJmlsBhgGuRXFm9xI-GozGECQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "uPJGnZ8E9RNspNvKtNtxZxqZRUqPDZFeLOeuyhauf-A", + "x": "5jwR2Mfvr9NN8HA4unDfW26CeV7NGphYgB99UN30RjA", "kid": "py-emoji-keys-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-es256-rails.json b/tests/fixtures/cross-lang/py-es256-rails.json index ff3125f..6f2e4c6 100644 --- a/tests/fixtures/cross-lang/py-es256-rails.json +++ b/tests/fixtures/cross-lang/py-es256-rails.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://a.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" }, { "version": "2026-04-08", @@ -56,18 +56,18 @@ "alg": "ES256", "use": "sig", "crv": "P-256", - "x": "C3ZfMpkic5bJf8t1HREJMrW_46yq7oG4pTelrXa6BTs", - "y": "GpQcHSIQaLCNBpMl7CeGxEpGtu50zYKGiaZUSaKUbtQ" + "x": "ZIZsqfrM8QSJIqSf1VGd4IhebR50aZph8I8NZmJnK78", + "y": "6QkiYzx-xUckq_sH8gIzE8yPcqzTWVdIWp6JLReIMqE" } ], - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJweS1lczI1Ni1yYWlscy1FUzI1NiIsImt0eSI6IkVDIiwidXNlIjoic2lnIiwieCI6IkMzWmZNcGtpYzViSmY4dDFIUkVKTXJXXzQ2eXE3b0c0cFRlbHJYYTZCVHMiLCJ5IjoiR3BRY0hTSVFhTENOQnBNbDdDZUd4RXBHdHU1MHpZS0dpYVpVU2FLVWJ0USJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFUzI1NiBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dLCJzaC5hZ2VudHNjb3JlLnBheW1lbnQueDQwMiI6W3siY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwiaWQiOiJ4NDAyIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy94NDAyLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy94NDAyIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In0seyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS8ud2VsbC1rbm93bi9hZ2VudC1jYXJkLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6ImEyYSIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.JIr_8NYAQNF9VETkmEsRjY_OFjj4h-oHJ3b0SIM2lhDPDB9Cr60xK9AoNtH2u8MNk7W9lx2uK1jcP9YB7PKtZg" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJweS1lczI1Ni1yYWlscy1FUzI1NiIsImt0eSI6IkVDIiwidXNlIjoic2lnIiwieCI6IlpJWnNxZnJNOFFTSklxU2YxVkdkNEloZWJSNTBhWnBoOEk4TlptSm5LNzgiLCJ5IjoiNlFraVl6eC14VWNrcV9zSDhnSXpFOHlQY3F6VFdWZElXcDZKTFJlSU1xRSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFUzI1NiBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dLCJzaC5hZ2VudHNjb3JlLnBheW1lbnQueDQwMiI6W3siY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwiaWQiOiJ4NDAyIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy94NDAyLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy94NDAyIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9tY3Aub3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9LHsiZW5kcG9pbnQiOiJodHRwczovL2EuZXhhbXBsZS5jb20vLndlbGwta25vd24vYWdlbnQtY2FyZC5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJhMmEiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.d1eH6jLjcW-D9evPQT0ZndJh6HLhYhF-6E6Muvl7y7HUezTV6EgVzaI3hWJrnCphfm-eJ5BIXNJLSG6YjFEyyw" }, "jwks": { "keys": [ { "crv": "P-256", - "x": "C3ZfMpkic5bJf8t1HREJMrW_46yq7oG4pTelrXa6BTs", - "y": "GpQcHSIQaLCNBpMl7CeGxEpGtu50zYKGiaZUSaKUbtQ", + "x": "ZIZsqfrM8QSJIqSf1VGd4IhebR50aZph8I8NZmJnK78", + "y": "6QkiYzx-xUckq_sH8gIzE8yPcqzTWVdIWp6JLReIMqE", "kid": "py-es256-rails-ES256", "alg": "ES256", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-extras-int.json b/tests/fixtures/cross-lang/py-extras-int.json index fdc25db..a9cd659 100644 --- a/tests/fixtures/cross-lang/py-extras-int.json +++ b/tests/fixtures/cross-lang/py-extras-int.json @@ -9,18 +9,18 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://e.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, "capabilities": {}, "payment_handlers": { - "sh.agentscore.payment.stripe-spt": [ + "sh.agentscore.payment.stripe_spt": [ { "id": "stripe", "version": "2026-04-08", - "spec": "https://agentscore.sh/specification/payment-handlers/stripe-spt", - "schema": "https://agentscore.sh/schemas/payment-handlers/stripe-spt.json", + "spec": "https://agentscore.sh/specification/payment-handlers/stripe_spt", + "schema": "https://agentscore.sh/schemas/payment-handlers/stripe_spt.json", "config": { "profile_id": "abc", "count": 7 @@ -37,16 +37,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "68yvy7SRtwNF91vk7bJwCfZF0llVVD6fNP4d7X4Zk5I" + "x": "JPj_2h7DSZ8tnCrNQFCr3mLfWTI5KASQJ6MZpd9HAZw" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNjh5dnk3U1J0d05GOTF2azdiSndDZlpGMGxsVlZENmZOUDRkN1g0Wms1SSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFeHRyYXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC5zdHJpcGUtc3B0IjpbeyJjb25maWciOnsiY291bnQiOjcsInByb2ZpbGVfaWQiOiJhYmMifSwiaWQiOiJzdHJpcGUiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3N0cmlwZS1zcHQuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3N0cmlwZS1zcHQiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.MKHlKWBvkWZK_2USAzWyuAS6jtz8EAUSqlhNr1pQR5n39njsoT98EjPXr9EF0YidkH6i12OCFv88GoI0gKE_Dg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiSlBqXzJoN0RTWjh0bkNyTlFGQ3IzbUxmV1RJNUtBU1FKNk1acGQ5SEFadyJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFeHRyYXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC5zdHJpcGVfc3B0IjpbeyJjb25maWciOnsiY291bnQiOjcsInByb2ZpbGVfaWQiOiJhYmMifSwiaWQiOiJzdHJpcGUiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3N0cmlwZV9zcHQuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3N0cmlwZV9zcHQiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL21jcC5vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.Y2-Ji_NtxMY1CFY8oiQiac4NBXvYWr4hrza54Tzx6OU85ZCiZPk_lKjdaEiSc2YA1e8aihc9-x_ZXZZ4_xrhCA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "68yvy7SRtwNF91vk7bJwCfZF0llVVD6fNP4d7X4Zk5I", + "x": "JPj_2h7DSZ8tnCrNQFCr3mLfWTI5KASQJ6MZpd9HAZw", "kid": "py-extras-int-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-int-boundary.json b/tests/fixtures/cross-lang/py-int-boundary.json index dabf87f..f8115f2 100644 --- a/tests/fixtures/cross-lang/py-int-boundary.json +++ b/tests/fixtures/cross-lang/py-int-boundary.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://i.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -24,7 +24,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "0afNQAp3MfPjli1PWg6-iYAjoNnESv96e5bJ8irRcD0" + "x": "9tbm2c88gZyFzzDyfI0A-yfbB4xzu6m9pAWy0Ln3lNI" } ], "max_safe_int": 9007199254740991, @@ -32,13 +32,13 @@ "small_int": 42, "neg_small_int": -42, "zero": 0, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiMGFmTlFBcDNNZlBqbGkxUFdnNi1pWUFqb05uRVN2OTZlNWJKOGlyUmNEMCJ9XSwic21hbGxfaW50Ijo0MiwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2kuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In0sInplcm8iOjB9.SZ9CIjxAV6kQCp6eeYtzqu3KETakw_rEYTEr71xee5kaP5kLsO_5CEHcQ0AyNaGxSi9FjZMGhfWqmiVC0w3hBg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiOXRibTJjODhnWnlGenpEeWZJMEEteWZiQjR4enU2bTlwQVd5MExuM2xOSSJ9XSwic21hbGxfaW50Ijo0MiwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2kuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvbWNwLm9wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9LCJ6ZXJvIjowfQ.oOfju-MfP_EIzK4iOzY2S0pGqz71o8UGTqbYRTOnsZ9IDKgjEv0XhKvudk4vzcoDFZ5xW-Rr7rf9fQiJtHMxCw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "0afNQAp3MfPjli1PWg6-iYAjoNnESv96e5bJ8irRcD0", + "x": "9tbm2c88gZyFzzDyfI0A-yfbB4xzu6m9pAWy0Ln3lNI", "kid": "py-int-boundary-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-minimal.json b/tests/fixtures/cross-lang/py-minimal.json index 19d9ed6..dedfe5b 100644 --- a/tests/fixtures/cross-lang/py-minimal.json +++ b/tests/fixtures/cross-lang/py-minimal.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://m.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -24,16 +24,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "metZlz1oZFmvRiVvfL3ajCVdWy_-RK2PerjKSsgviLg" + "x": "29lgJhbXQc6T--febg1OwvqYSP2RU7xQndmHl9HStN4" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoibWV0Wmx6MW9aRm12UmlWdmZMM2FqQ1ZkV3lfLVJLMlBlcmpLU3NndmlMZyJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNaW5pbWFsIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.z76YEZSH1zGeNTXVzbH8gP1Rzer8LpeFtDhrSCSn-wV8eWc135f9BrfEEYQGaEqiYzNlPIynfA1-iVQLqzOJDw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiMjlsZ0poYlhRYzZULS1mZWJnMU93dnFZU1AyUlU3eFFuZG1IbDlIU3RONCJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNaW5pbWFsIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL21jcC5vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.H7ELZTV8BseCkX3RJUvNHAw0loE0M7iUmB4m2YsJ_k4rmYYLJ5m_8q2Ahh6OV6dOAlUfL5w5I3gKudP5rWzMDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "metZlz1oZFmvRiVvfL3ajCVdWy_-RK2PerjKSsgviLg", + "x": "29lgJhbXQc6T--febg1OwvqYSP2RU7xQndmHl9HStN4", "kid": "py-minimal-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-multikey.json b/tests/fixtures/cross-lang/py-multikey.json index c0a3f70..e9d8d7b 100644 --- a/tests/fixtures/cross-lang/py-multikey.json +++ b/tests/fixtures/cross-lang/py-multikey.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://mk.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -36,7 +36,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "VvkX3wnJHn26gnTmWF8GRFlVH_iaMNLAaieBn-5ovEU" + "x": "bmtxztAfkN74Ww0GbLWUvb8XWejGK4O2EK5zxKyMa9U" }, { "kid": "py-multikey-new", @@ -44,16 +44,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "jA1y92GEbvAeS1OZJPyr7HYpGu2FwQjoo22M_TzorcA" + "x": "6wXFCH8GTAnmlgr5TUd2W56-BxWQmJE_qYzoOQRsEWY" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJWdmtYM3duSkhuMjZnblRtV0Y4R1JGbFZIX2lhTU5MQWFpZUJuLTVvdkVVIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1uZXciLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiakExeTkyR0VidkFlUzFPWkpQeXI3SFlwR3UyRndRam9vMjJNX1R6b3JjQSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNdWx0aS1LZXkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL21rLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.kUnGFkqkSj0iCX2cxIoh99qtvtklfUGaWeuxoX7Y3v1C2bNsB1EDNB-u0ZDPIn44O4FJy-aUxX3jAVhqhQADAg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJibXR4enRBZmtONzRXdzBHYkxXVXZiOFhXZWpHSzRPMkVLNXp4S3lNYTlVIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1uZXciLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNndYRkNIOEdUQW5tbGdyNVRVZDJXNTYtQnhXUW1KRV9xWXpvT1FSc0VXWSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNdWx0aS1LZXkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL21rLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL21jcC5vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.Au5GjUltaxdK2vhazvJ5VqpuM5wtxQgD7YtkPA3ij3_glwQ3jtqK-9qwP_I4fg35jJa2EH9SuQkCv2MVa4dJCg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "VvkX3wnJHn26gnTmWF8GRFlVH_iaMNLAaieBn-5ovEU", + "x": "bmtxztAfkN74Ww0GbLWUvb8XWejGK4O2EK5zxKyMa9U", "kid": "py-multikey-old", "alg": "EdDSA", "use": "sig", @@ -61,7 +61,7 @@ }, { "crv": "Ed25519", - "x": "jA1y92GEbvAeS1OZJPyr7HYpGu2FwQjoo22M_TzorcA", + "x": "6wXFCH8GTAnmlgr5TUd2W56-BxWQmJE_qYzoOQRsEWY", "kid": "py-multikey-new", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-typed-claims.json b/tests/fixtures/cross-lang/py-typed-claims.json deleted file mode 100644 index 396bf54..0000000 --- a/tests/fixtures/cross-lang/py-typed-claims.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "profile": { - "ucp": { - "version": "2026-04-08", - "services": { - "dev.ucp.shopping": [ - { - "version": "2026-04-08", - "spec": "https://ucp.dev/2026-04-08/specification/overview", - "transport": "mcp", - "endpoint": "https://t.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" - } - ] - }, - "capabilities": { - "sh.agentscore.identity": [ - { - "version": "1", - "spec": "https://agentscore.sh/specification/identity", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "extends": [ - "dev.ucp.shopping.checkout", - "dev.ucp.shopping.cart" - ], - "claims": { - "operator_id": "op_typed_claims", - "kyc_level": "enhanced", - "sanctions_clear": true, - "age_bracket": "21+", - "jurisdiction": "US", - "verified_at": "2026-04-01T00:00:00Z", - "verify_url": "https://agentscore.sh/verify/op_typed_claims", - "issuer": "https://agentscore.sh" - } - } - ] - }, - "payment_handlers": {}, - "name": "Typed Claims Merchant" - }, - "signing_keys": [ - { - "kid": "py-typed-claims-EdDSA", - "kty": "OKP", - "alg": "EdDSA", - "use": "sig", - "crv": "Ed25519", - "x": "yjdkLTwZHaA2QJ8QK6dZ-TXj3b2vKWGxd5Wn8jXaP2I" - } - ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ5amRrTFR3WkhhQTJRSjhRSzZkWi1UWGozYjJ2S1dHeGQ1V244alhhUDJJIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vdC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMDgifX0.dEad_hdel2vcUDbqf2dwq_VXtQvk71Fo-RJHfoE0z8cUWNAktZudAxbxKpOAIXgPwlnG1jh2m6mo7IpSiLuuBw" - }, - "jwks": { - "keys": [ - { - "crv": "Ed25519", - "x": "yjdkLTwZHaA2QJ8QK6dZ-TXj3b2vKWGxd5Wn8jXaP2I", - "kid": "py-typed-claims-EdDSA", - "alg": "EdDSA", - "use": "sig", - "kty": "OKP" - } - ] - }, - "alg": "EdDSA", - "kid": "py-typed-claims-EdDSA", - "generator": "python" -} diff --git a/tests/fixtures/cross-lang/py-unicode.json b/tests/fixtures/cross-lang/py-unicode.json index 7ae8586..6b9fdce 100644 --- a/tests/fixtures/cross-lang/py-unicode.json +++ b/tests/fixtures/cross-lang/py-unicode.json @@ -9,7 +9,7 @@ "spec": "https://ucp.dev/2026-04-08/specification/overview", "transport": "mcp", "endpoint": "https://日本.example.com/api/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/openrpc.json" + "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" } ] }, @@ -36,16 +36,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "2yFUhSLrMIm1yc59Gg7xtDo89yYDXEptKkzh7jQyj6A" + "x": "4z111Qj_1ipW6SujAVVBGqYsL6maY-BB_WwZx5ZZVp0" } ], - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiMnlGVWhTTHJNSW0xeWM1OUdnN3h0RG84OXlZRFhFcHRLa3poN2pReWo2QSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJDYWbDqSDml6XmnKwg8J-NtyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly_ml6XmnKwuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTA4In19.bEMMVR7wkLpEs-DFvE75RqEpgaWaDsWb_J2uZUG3bX79W8RpERhquF5tIkYaVadCcCeXkKNJcY2vdnEM0XdVBQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNHoxMTFRal8xaXBXNlN1akFWVkJHcVlzTDZtYVktQkJfV3daeDVaWlZwMCJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJDYWbDqSDml6XmnKwg8J-NtyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly_ml6XmnKwuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvbWNwLm9wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9fQ.f9TnlaP_fgDx4pRw-7j6EHvMRUp4vAsuhfXJjV3mpuc2NP6yDhjJQENqu0E-g6aqArD0uiAFaYKT5XSS48w3AQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "2yFUhSLrMIm1yc59Gg7xtDo89yYDXEptKkzh7jQyj6A", + "x": "4z111Qj_1ipW6SujAVVBGqYsL6maY-BB_WwZx5ZZVp0", "kid": "py-unicode-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/test_a2a.py b/tests/test_a2a.py index fdebc97..a8a21a3 100644 --- a/tests/test_a2a.py +++ b/tests/test_a2a.py @@ -1,153 +1,441 @@ -"""Tests for build_a2a_agent_card.""" +"""Tests for build_a2a_agent_card (A2A v1.0 spec compliance).""" + +import pytest from agentscore_commerce.identity import ( UCP_A2A_EXTENSION_URI, A2AAgentCardCapabilities, - AssessResult, + A2AAgentCardExtension, + A2AAgentCardSignature, + A2AAgentInterface, + A2AAgentProvider, + A2AAgentSkill, build_a2a_agent_card, ucp_a2a_extension, ) +from agentscore_commerce.identity.a2a import A2AAgentCard + +_DEFAULT_SKILL = A2AAgentSkill( + id="purchase", + name="Purchase", + description="Buy products via agent payments.", + tags=["commerce", "payment"], +) + + +def test_minimum_required_fields_emitted(): + card = build_a2a_agent_card( + name="Example Merchant", + description="Buy regulated goods via agent payments.", + url="https://agents.example.com", + skills=[_DEFAULT_SKILL], + ) + d = card.to_dict() + # Per spec §4.4.1: name, description, supported_interfaces, version, + # capabilities, default_input_modes, default_output_modes, skills are REQUIRED. + assert d["name"] == "Example Merchant" + assert d["description"] == "Buy regulated goods via agent payments." + assert isinstance(d["supported_interfaces"], list) + assert len(d["supported_interfaces"]) == 1 + assert d["supported_interfaces"][0]["url"] == "https://agents.example.com" + assert d["supported_interfaces"][0]["protocol_binding"] == "HTTP+JSON" + assert d["supported_interfaces"][0]["protocol_version"] == "1.0" + assert d["version"] == "1.0.0" + assert d["capabilities"] == {} + assert d["default_input_modes"] == ["application/json"] + assert d["default_output_modes"] == ["application/json"] + assert len(d["skills"]) == 1 + + +def test_skills_required_non_empty(): + """Per spec §4.4.1 (proto field 12 [field_behavior=REQUIRED]): skills MUST be non-empty.""" + with pytest.raises(ValueError, match="MUST be a non-empty list"): + build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[], + ) + + +def test_does_not_emit_invented_fields(): + """Confirms we no longer emit `protocol_version` / `card_version` / `endpoints` / + top-level `identity` / top-level `extensions` — none of these exist in the A2A proto.""" + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + ) + d = card.to_dict() + assert "protocol_version" not in d + assert "card_version" not in d + assert "endpoints" not in d + assert "identity" not in d + assert "extensions" not in d # extensions live INSIDE capabilities -def _full_result() -> AssessResult: - return AssessResult( - allow=True, - resolved_operator="op_abc", - verify_url="https://agentscore.sh/verify", - raw={ - "account_verification": { - "kyc_level": "enhanced", - "sanctions_clear": True, - "age_bracket": "21+", - "jurisdiction": "US", - "verified_at": "2026-04-01T00:00:00Z", - }, +def test_skills_serialize_as_top_level_objects_not_strings(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[ + A2AAgentSkill( + id="purchase", + name="Purchase", + description="Buy products via agent payments.", + tags=["commerce", "payment"], + ), + ], + ) + d = card.to_dict() + assert d["skills"] == [ + { + "id": "purchase", + "name": "Purchase", + "description": "Buy products via agent payments.", + "tags": ["commerce", "payment"], }, + ] + + +def test_extensions_live_inside_capabilities_not_top_level(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + extensions=[ucp_a2a_extension()], ) + d = card.to_dict() + assert "extensions" not in d # NOT at top level + assert "extensions" in d["capabilities"] + assert len(d["capabilities"]["extensions"]) == 1 + assert d["capabilities"]["extensions"][0]["uri"] == UCP_A2A_EXTENSION_URI -def test_card_with_identity_when_data_provided(): +def test_extensions_omitted_when_empty(): card = build_a2a_agent_card( - name="Example Merchant", - url="https://agents.example.com", - data=_full_result(), + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + extensions=[], + ) + assert "extensions" not in card.to_dict()["capabilities"] + + +def test_capability_flags_emitted_when_set(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + streaming=True, + push_notifications=False, + extended_agent_card=True, + ) + caps = card.to_dict()["capabilities"] + assert caps["streaming"] is True + assert caps["push_notifications"] is False + assert caps["extended_agent_card"] is True + + +def test_capability_flags_omitted_when_unset(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + ) + caps = card.to_dict()["capabilities"] + assert "streaming" not in caps + assert "push_notifications" not in caps + assert "extended_agent_card" not in caps + + +def test_provider_emitted_when_set(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + provider=A2AAgentProvider(url="https://acme.example", organization="Acme"), ) - assert card.protocol_version == "1.0" - assert card.card_version == 1 - assert card.name == "Example Merchant" - assert card.url == "https://agents.example.com" - assert card.identity is not None - assert card.identity.operator_id == "op_abc" - assert card.identity.kyc_level == "enhanced" - assert card.identity.sanctions_clear is True - assert card.identity.age_bracket == "21+" - assert card.identity.jurisdiction == "US" + assert card.to_dict()["provider"] == {"url": "https://acme.example", "organization": "Acme"} -def test_card_with_no_identity_when_data_omitted(): - card = build_a2a_agent_card(name="X") - assert card.identity is None - assert card.name == "X" +def test_documentation_url_emitted_when_set(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + documentation_url="https://docs.example", + ) + assert card.to_dict()["documentation_url"] == "https://docs.example" -def test_card_with_no_identity_when_no_resolved_operator(): - card = build_a2a_agent_card(name="X", data=AssessResult(allow=True, resolved_operator=None)) - assert card.identity is None +def test_icon_url_emitted_when_set(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + icon_url="https://x.example/icon.png", + ) + assert card.to_dict()["icon_url"] == "https://x.example/icon.png" -def test_passes_through_capabilities_description_extras(): +def test_signatures_emitted_when_set(): + sig = A2AAgentCardSignature(protected="eyJhbGciOiJFZERTQSJ9", signature="abc") card = build_a2a_agent_card( name="X", - description="test agent", - capabilities=A2AAgentCardCapabilities( - endpoints=[{"name": "pay", "method": "POST"}], - skills=["wine"], - ), - extras={"custom": "value"}, + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + signatures=[sig], ) - assert card.description == "test agent" - assert card.capabilities is not None - assert card.capabilities.endpoints == [{"name": "pay", "method": "POST"}] - assert card.capabilities.skills == ["wine"] - assert card.extras == {"custom": "value"} + assert card.to_dict()["signatures"] == [ + {"protected": "eyJhbGciOiJFZERTQSJ9", "signature": "abc"}, + ] -def test_to_dict_round_trip(): +def test_signatures_omitted_when_empty(): card = build_a2a_agent_card( name="X", - description="test", + description="y", url="https://x.example", - capabilities=A2AAgentCardCapabilities(skills=["a", "b"]), - data=_full_result(), - extras={"foo": 1}, + skills=[_DEFAULT_SKILL], + ) + assert "signatures" not in card.to_dict() + + +def test_default_input_output_modes_overridable(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + default_input_modes=["text/plain", "application/json"], + default_output_modes=["text/plain"], ) d = card.to_dict() - assert d["protocol_version"] == "1.0" - assert d["card_version"] == 1 - assert d["name"] == "X" - assert d["description"] == "test" - assert d["url"] == "https://x.example" - assert d["capabilities"] == {"skills": ["a", "b"]} - assert d["identity"]["operator_id"] == "op_abc" - assert d["foo"] == 1 + assert d["default_input_modes"] == ["text/plain", "application/json"] + assert d["default_output_modes"] == ["text/plain"] -def test_respects_issuer_and_verify_url_overrides(): +def test_protocol_binding_overridable(): card = build_a2a_agent_card( name="X", - data=_full_result(), - issuer="https://other.example", - verify_url="https://other.example/v", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + protocol_binding="GRPC", + a2a_protocol_version="1.0", + ) + iface = card.to_dict()["supported_interfaces"][0] + assert iface["protocol_binding"] == "GRPC" + + +def test_extras_merge_at_top_level(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + extras={"vendor_field": 42}, ) - assert card.identity is not None - assert card.identity.issuer == "https://other.example" - assert card.identity.verify_url == "https://other.example/v" + assert card.to_dict()["vendor_field"] == 42 + + +def test_security_schemes_emitted_when_set(): + card = build_a2a_agent_card( + name="X", + description="y", + url="https://x.example", + skills=[_DEFAULT_SKILL], + security_schemes={"bearer": {"type": "http", "scheme": "bearer"}}, + ) + assert card.to_dict()["security_schemes"] == {"bearer": {"type": "http", "scheme": "bearer"}} + + +# ---- AgentExtension shape ---- -def test_ucp_a2a_extension_uri_pinned_to_2026_04_08(): +def test_agent_extension_emits_required_fields(): + """Per spec §4.4.4: AgentExtension MUST carry uri, description, required.""" + ext = A2AAgentCardExtension(uri="https://example/ext", description="test", required=True) + d = ext.to_dict() + assert d["uri"] == "https://example/ext" + assert d["description"] == "test" + assert d["required"] is True + + +def test_agent_extension_required_defaults_false(): + ext = A2AAgentCardExtension(uri="https://example/ext", description="test") + assert ext.to_dict()["required"] is False + + +def test_agent_extension_params_omitted_when_empty(): + ext = A2AAgentCardExtension(uri="u", description="d") + assert "params" not in ext.to_dict() + + +def test_agent_extension_params_emitted_when_set(): + ext = A2AAgentCardExtension(uri="u", description="d", params={"k": "v"}) + assert ext.to_dict()["params"] == {"k": "v"} + + +# ---- ucp_a2a_extension helper ---- + + +def test_ucp_a2a_extension_uri_pinned(): assert UCP_A2A_EXTENSION_URI == "https://ucp.dev/2026-04-08/specification/reference" -def test_ucp_a2a_extension_no_args_produces_empty_capabilities_entry(): +def test_ucp_a2a_extension_default_args_emit_empty_capabilities(): ext = ucp_a2a_extension() - assert ext.uri == UCP_A2A_EXTENSION_URI - assert ext.params == {"capabilities": {}} + d = ext.to_dict() + assert d["uri"] == UCP_A2A_EXTENSION_URI + assert d["description"] # non-empty per spec + assert d["required"] is False + assert d["params"] == {"capabilities": {}} -def test_ucp_a2a_extension_wraps_capabilities_map_under_params(): +def test_ucp_a2a_extension_passes_capabilities_under_params(): ext = ucp_a2a_extension( { "dev.ucp.shopping.checkout": [{"version": "2026-04-08"}], "dev.ucp.shopping.cart": [{"version": "2026-04-08"}], }, ) - assert ext.params == { - "capabilities": { - "dev.ucp.shopping.checkout": [{"version": "2026-04-08"}], - "dev.ucp.shopping.cart": [{"version": "2026-04-08"}], - }, + assert ext.params["capabilities"] == { + "dev.ucp.shopping.checkout": [{"version": "2026-04-08"}], + "dev.ucp.shopping.cart": [{"version": "2026-04-08"}], } -def test_build_a2a_agent_card_emits_extensions_when_passed(): - card = build_a2a_agent_card( +def test_ucp_a2a_extension_required_kwarg(): + ext = ucp_a2a_extension(required=True) + assert ext.required is True + + +# ---- AgentInterface ---- + + +def test_agent_interface_emits_required_fields(): + iface = A2AAgentInterface( + url="https://x.example", + protocol_binding="JSONRPC", + protocol_version="1.0", + ) + d = iface.to_dict() + assert d == { + "url": "https://x.example", + "protocol_binding": "JSONRPC", + "protocol_version": "1.0", + } + + +def test_agent_interface_tenant_emitted_when_set(): + iface = A2AAgentInterface( + url="https://x.example", + protocol_binding="JSONRPC", + protocol_version="1.0", + tenant="tenant-123", + ) + assert iface.to_dict()["tenant"] == "tenant-123" + + +# ---- AgentSkill ---- + + +def test_agent_skill_required_fields(): + s = A2AAgentSkill(id="x", name="X", description="d", tags=["t"]) + d = s.to_dict() + assert d == {"id": "x", "name": "X", "description": "d", "tags": ["t"]} + + +def test_agent_skill_optional_fields_omitted_when_empty(): + s = A2AAgentSkill(id="x", name="X", description="d", tags=["t"]) + d = s.to_dict() + assert "examples" not in d + assert "input_modes" not in d + assert "output_modes" not in d + + +def test_agent_skill_optional_fields_emitted_when_set(): + s = A2AAgentSkill( + id="x", name="X", - extensions=[ucp_a2a_extension()], + description="d", + tags=["t"], + examples=["buy a wine"], + input_modes=["application/json"], + output_modes=["text/plain"], ) - d = card.to_dict() - assert "extensions" in d - assert len(d["extensions"]) == 1 - assert d["extensions"][0]["uri"] == UCP_A2A_EXTENSION_URI - assert d["extensions"][0]["params"] == {"capabilities": {}} + d = s.to_dict() + assert d["examples"] == ["buy a wine"] + assert d["input_modes"] == ["application/json"] + assert d["output_modes"] == ["text/plain"] + + +# ---- AgentCardSignature ---- + + +def test_agent_card_signature_required_fields(): + sig = A2AAgentCardSignature(protected="eyJ...", signature="abc") + d = sig.to_dict() + assert d == {"protected": "eyJ...", "signature": "abc"} + + +def test_agent_card_signature_unprotected_header_emitted_when_set(): + sig = A2AAgentCardSignature( + protected="eyJ...", + signature="abc", + header={"kid": "key-1"}, + ) + assert sig.to_dict()["header"] == {"kid": "key-1"} + +# ---- Direct AgentCard construction (multi-binding agents) ---- -def test_build_a2a_agent_card_omits_extensions_when_not_passed(): - card = build_a2a_agent_card(name="X") - assert "extensions" not in card.to_dict() + +def test_direct_agent_card_construction_with_multiple_interfaces(): + card = A2AAgentCard( + name="X", + description="y", + supported_interfaces=[ + A2AAgentInterface(url="https://x.example/jsonrpc", protocol_binding="JSONRPC", protocol_version="1.0"), + A2AAgentInterface(url="https://x.example/grpc", protocol_binding="GRPC", protocol_version="1.0"), + ], + version="1.0.0", + capabilities=A2AAgentCardCapabilities(), + default_input_modes=["application/json"], + default_output_modes=["application/json"], + skills=[_DEFAULT_SKILL], + ) + d = card.to_dict() + assert len(d["supported_interfaces"]) == 2 + assert d["supported_interfaces"][0]["protocol_binding"] == "JSONRPC" + assert d["supported_interfaces"][1]["protocol_binding"] == "GRPC" -def test_build_a2a_agent_card_omits_extensions_when_passed_empty_list(): - # Parity with node: both SDKs skip the extensions field when empty so - # cross-language profiles canonicalize to identical bytes when both omit. - card = build_a2a_agent_card(name="X", extensions=[]) - assert "extensions" not in card.to_dict() +@pytest.mark.parametrize( + "missing_kwarg", + ["name", "description", "url", "skills"], +) +def test_required_kwargs_enforced(missing_kwarg: str) -> None: + """Per spec §4.4.1: name, description, supported_interfaces (built from url), and + skills (≥1) are REQUIRED. Missing one raises TypeError or ValueError.""" + kwargs: dict = { + "name": "X", + "description": "y", + "url": "https://x.example", + "skills": [_DEFAULT_SKILL], + } + del kwargs[missing_kwarg] + with pytest.raises((TypeError, ValueError)): + build_a2a_agent_card(**kwargs) diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index 929be3f..c299a45 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -15,7 +15,7 @@ GATE_STATE_KEY, agentscore_gate_middleware, capture_wallet, - get_assess_data, + get_agentscore_data, ) from agentscore_commerce.identity.sessions import CreateSessionOnMissing @@ -67,11 +67,11 @@ async def test_allows_trusted_wallet(self): @pytest.mark.asyncio @respx.mock - async def test_get_assess_data_returns_assess_after_pass(self): + async def test_get_agentscore_data_returns_assess_after_pass(self): _mock_assess("allow") async def handler(request: web.Request) -> web.Response: - return web.json_response({"assess": get_assess_data(request)}) + return web.json_response({"assess": get_agentscore_data(request)}) client = await _client(_make_app(handler=handler)) async with client: diff --git a/tests/test_django.py b/tests/test_django.py index 3f40661..34adb12 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -23,7 +23,7 @@ from django.test import RequestFactory from agentscore_commerce.identity.client import PaymentRequiredError, QuotaExceededError -from agentscore_commerce.identity.django import AgentScoreMiddleware, get_assess_data +from agentscore_commerce.identity.django import AgentScoreMiddleware, get_agentscore_data from agentscore_commerce.identity.types import AssessResult # Minimal URL conf for Django test runner. @@ -221,16 +221,16 @@ def test_attaches_data_to_request(self) -> None: assert hasattr(request, "agentscore") assert request.agentscore["score"] == 80 # type: ignore[attr-defined] - def test_get_assess_data_returns_assess_after_pass(self) -> None: + def test_get_agentscore_data_returns_assess_after_pass(self) -> None: mw = self._make_middleware() request = self.factory.get("/", HTTP_X_WALLET_ADDRESS="0xabc") with patch("agentscore_commerce.identity.django.GateClient.check", return_value=_mock_result()): mw(request) - assert get_assess_data(request) == {"score": 80, "grade": "B"} + assert get_agentscore_data(request) == {"score": 80, "grade": "B"} - def test_get_assess_data_returns_none_for_ungated_request(self) -> None: + def test_get_agentscore_data_returns_none_for_ungated_request(self) -> None: request = self.factory.get("/") - assert get_assess_data(request) is None + assert get_agentscore_data(request) is None def test_compliance_params_passed_to_client(self) -> None: with patch("agentscore_commerce.identity.django.GateClient") as mock_cls: diff --git a/tests/test_fastapi.py b/tests/test_fastapi.py index 4c70827..1c96497 100644 --- a/tests/test_fastapi.py +++ b/tests/test_fastapi.py @@ -13,7 +13,7 @@ from agentscore_commerce.identity.fastapi import ( AgentScoreGate, capture_wallet, - get_assess_data, + get_agentscore_data, ) from agentscore_commerce.identity.sessions import CreateSessionOnMissing @@ -32,7 +32,7 @@ def _make_app(gate: AgentScoreGate) -> FastAPI: app = FastAPI() @app.get("/", dependencies=[Depends(gate)]) - async def index(assess=Depends(get_assess_data)): + async def index(assess=Depends(get_agentscore_data)): return {"ok": True, "assess": assess} @app.post("/purchase", dependencies=[Depends(gate)]) diff --git a/tests/test_flask.py b/tests/test_flask.py index 4647d90..2d0484e 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -9,7 +9,7 @@ from flask import Flask from agentscore_commerce.identity.client import PaymentRequiredError, QuotaExceededError -from agentscore_commerce.identity.flask import agentscore_gate, get_assess_data +from agentscore_commerce.identity.flask import agentscore_gate, get_agentscore_data from agentscore_commerce.identity.types import AssessResult @@ -44,24 +44,24 @@ def test_allows_trusted_wallet(self) -> None: assert data["ok"] is True assert data["agentscore"] is not None - def test_get_assess_data_returns_assess_after_pass(self) -> None: + def test_get_agentscore_data_returns_assess_after_pass(self) -> None: app = Flask(__name__) app.config["TESTING"] = True agentscore_gate(app, api_key="test-key") @app.route("/") def index() -> dict[str, object]: - return {"assess": get_assess_data()} + return {"assess": get_agentscore_data()} with patch("agentscore_commerce.identity.flask.GateClient.check", return_value=_mock_result()): resp = app.test_client().get("/", headers={"x-wallet-address": "0xabc"}) assert resp.status_code == 200 assert resp.get_json()["assess"] == {"score": 80, "grade": "B"} - def test_get_assess_data_returns_none_outside_request(self) -> None: + def test_get_agentscore_data_returns_none_outside_request(self) -> None: app = Flask(__name__) with app.app_context(): - assert get_assess_data() is None + assert get_agentscore_data() is None def test_blocks_untrusted_wallet(self) -> None: app = _make_app() diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 2a9007e..a9f888f 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -12,7 +12,7 @@ from starlette.routing import Route from starlette.testclient import TestClient -from agentscore_commerce.identity.middleware import AgentScoreGate, CreateSessionOnMissing, get_assess_data +from agentscore_commerce.identity.middleware import AgentScoreGate, CreateSessionOnMissing, get_agentscore_data if TYPE_CHECKING: from starlette.requests import Request @@ -38,7 +38,7 @@ def _homepage(request: Request) -> JSONResponse: def _homepage_via_getter(request: Request) -> JSONResponse: - return JSONResponse({"assess": get_assess_data(request)}) + return JSONResponse({"assess": get_agentscore_data(request)}) def _make_app(**gate_kwargs: object) -> Starlette: @@ -139,7 +139,7 @@ def test_falls_back_to_missing_identity_on_network_error(self): assert data["error"]["code"] == "missing_identity" @respx.mock - def test_get_assess_data_returns_assess_after_pass(self): + def test_get_agentscore_data_returns_assess_after_pass(self): _mock_assess() app = Starlette(routes=[Route("/", _homepage_via_getter)]) gated = AgentScoreGate(app, api_key="ask_test_key") diff --git a/tests/test_sanic.py b/tests/test_sanic.py index 6a9c9fc..df6acd6 100644 --- a/tests/test_sanic.py +++ b/tests/test_sanic.py @@ -11,7 +11,7 @@ from sanic import Sanic, response -from agentscore_commerce.identity.sanic import agentscore_gate, capture_wallet, get_assess_data +from agentscore_commerce.identity.sanic import agentscore_gate, capture_wallet, get_agentscore_data from agentscore_commerce.identity.sessions import CreateSessionOnMissing from agentscore_commerce.identity.types import AssessResult, DenialReason @@ -53,13 +53,13 @@ def test_allows_trusted_wallet(self): assert resp.status == 200 assert resp.json["ok"] is True - def test_get_assess_data_returns_assess_after_pass(self): - app = Sanic.get_app("sanic_get_assess_data", force_create=True) + def test_get_agentscore_data_returns_assess_after_pass(self): + app = Sanic.get_app("sanic_get_agentscore_data", force_create=True) agentscore_gate(app, api_key="ask_test") @app.get("/") async def handler(request): - return response.json({"assess": get_assess_data(request)}) + return response.json({"assess": get_agentscore_data(request)}) with patch( "agentscore_commerce.identity.sanic.GateClient.acheck_identity", diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 95be163..ab0462e 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -1,13 +1,10 @@ """Tests for build_ucp_profile (spec-compliant shape).""" -from typing import cast - import pytest from agentscore_commerce.identity import ( AGENTSCORE_UCP_CAPABILITY, - AssessResult, - OperatorVerification, + AgentScoreGatePolicy, UCPCapabilityBinding, UCPPaymentHandlerBinding, UCPProfileBody, @@ -17,30 +14,13 @@ ) -def _full_result() -> AssessResult: - return AssessResult( - allow=True, - resolved_operator="op_abc", - verify_url="https://agentscore.sh/verify", - raw={ - "account_verification": { - "kyc_level": "enhanced", - "sanctions_clear": True, - "age_bracket": "21+", - "jurisdiction": "US", - "verified_at": "2026-04-01T00:00:00Z", - }, - }, - ) - - def _sample_service() -> UCPServiceBinding: return UCPServiceBinding( version="2026-04-08", spec="https://ucp.dev/2026-04-08/specification/overview", transport="mcp", endpoint="https://agents.example/api/ucp/mcp", - schema="https://ucp.dev/services/shopping/openrpc.json", + schema="https://ucp.dev/services/shopping/mcp.openrpc.json", ) @@ -72,26 +52,76 @@ def test_emits_spec_envelope_with_ucp_body_and_outer_signing_keys(): assert d["signing_keys"][0]["kid"] == "me" -def test_appends_agentscore_capability_when_data_provided(): - profile = build_ucp_profile(**_base_kwargs(), data=_full_result()) +def test_skips_agentscore_capability_when_gate_not_provided(): + profile = build_ucp_profile(**_base_kwargs()) + d = profile.to_dict() + assert AGENTSCORE_UCP_CAPABILITY not in d["ucp"]["capabilities"] + + +def test_appends_agentscore_capability_when_gate_provided(): + profile = build_ucp_profile( + **_base_kwargs(), + agentscore_gate=AgentScoreGatePolicy( + require_kyc=True, + require_sanctions_clear=True, + min_age=21, + allowed_jurisdictions=["US"], + ), + ) d = profile.to_dict() cap = _agentscore_cap(d) - assert cap["version"] == "1" + # Date-format version (UCP convention; matches every other binding's version field). + assert cap["version"] == "2026-04-08" assert "sh-agentscore-identity-v1.json" in cap["schema"] # Multi-parent extends — matches Shopify's dev.shopify.catalog.storefront pattern # and UCP-canonical dev.ucp.shopping.discount (extends [checkout, cart]). assert cap["extends"] == ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"] - claims = cap["claims"] - assert claims["operator_id"] == "op_abc" - assert claims["kyc_level"] == "enhanced" - assert claims["sanctions_clear"] is True - assert claims["jurisdiction"] == "US" + # Config is the merchant's policy declaration, NOT per-operator data. Public + # /.well-known/ucp profiles must never carry per-operator KYC claims. + assert cap["config"] == { + "require_kyc": True, + "require_sanctions_clear": True, + "min_age": 21, + "allowed_jurisdictions": ["US"], + } -def test_skips_agentscore_capability_when_no_resolved_operator(): - profile = build_ucp_profile(**_base_kwargs(), data=AssessResult(allow=True, resolved_operator=None)) +def test_capability_present_with_omitted_config_when_caller_passes_empty_policy(): + """When the caller passes AgentScoreGatePolicy() with no fields set, the capability + binding is still injected (signals that the merchant is AgentScore-gated), but the + `config` field is omitted from serialization for cross-lang parity (the underlying + UCPCapabilityBinding.to_dict skips empty config consistently with how + UCPPaymentHandlerBinding skips empty config).""" + profile = build_ucp_profile( + **_base_kwargs(), + agentscore_gate=AgentScoreGatePolicy(), + ) d = profile.to_dict() - assert AGENTSCORE_UCP_CAPABILITY not in d["ucp"]["capabilities"] + cap = _agentscore_cap(d) + # Capability IS present + assert cap["version"] == "2026-04-08" + # Config IS omitted (no policy fields set) + assert "config" not in cap + + +def test_emits_only_the_policy_fields_caller_set(): + profile = build_ucp_profile( + **_base_kwargs(), + agentscore_gate=AgentScoreGatePolicy(require_kyc=True), + ) + cap_config = _agentscore_cap(profile.to_dict())["config"] + assert cap_config == {"require_kyc": True} + assert "min_age" not in cap_config + assert "allowed_jurisdictions" not in cap_config + + +def test_blocked_jurisdictions_serializes_as_inverse_of_allowed(): + profile = build_ucp_profile( + **_base_kwargs(), + agentscore_gate=AgentScoreGatePolicy(blocked_jurisdictions=["KP", "IR", "CU"]), + ) + cap_config = _agentscore_cap(profile.to_dict())["config"] + assert cap_config == {"blocked_jurisdictions": ["KP", "IR", "CU"]} def test_preserves_caller_capabilities_and_appends_agentscore(): @@ -103,11 +133,11 @@ def test_preserves_caller_capabilities_and_appends_agentscore(): profile = build_ucp_profile( **_base_kwargs(), capabilities={"dev.ucp.shopping.checkout": [checkout_binding]}, - data=_full_result(), + agentscore_gate=AgentScoreGatePolicy(require_kyc=True), ) d = profile.to_dict() assert d["ucp"]["capabilities"]["dev.ucp.shopping.checkout"][0]["version"] == "2026-04-08" - assert _agentscore_cap(d)["version"] == "1" + assert _agentscore_cap(d)["version"] == "2026-04-08" def test_passes_through_name_payment_handlers_extras(): @@ -156,7 +186,7 @@ def test_respects_version_override(): def test_respects_agentscore_schema_url_override(): profile = build_ucp_profile( **_base_kwargs(), - data=_full_result(), + agentscore_gate=AgentScoreGatePolicy(), agentscore_schema_url="https://custom.example/schema.json", ) cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] @@ -166,7 +196,7 @@ def test_respects_agentscore_schema_url_override(): def test_respects_agentscore_spec_url_override(): profile = build_ucp_profile( **_base_kwargs(), - data=_full_result(), + agentscore_gate=AgentScoreGatePolicy(), agentscore_spec_url="https://custom.example/spec", ) cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] @@ -215,142 +245,6 @@ def test_ucp_extras_reserved_collision_rejected(key: str) -> None: profile.to_dict() -# Empty-string and null normalization: the API can emit `account_verification` with -# either null or "" for un-set fields, and the node + python siblings must produce -# the SAME canonical claims block for either shape so a profile signed in one -# language verifies in the other. - - -def _claims_of(account_verification: dict, operator_verification: dict | None = None) -> dict: - raw: dict = {"account_verification": account_verification} - if operator_verification is not None: - raw["operator_verification"] = operator_verification - result = AssessResult(allow=True, resolved_operator="op_abc", raw=raw) - profile = build_ucp_profile(**_base_kwargs(), data=result) - d = profile.to_dict() - return _agentscore_cap(d)["claims"] - - -def test_coerces_empty_string_kyc_level_to_none() -> None: - assert _claims_of({"kyc_level": ""})["kyc_level"] == "none" - - -def test_coerces_null_age_bracket_to_unknown() -> None: - assert _claims_of({"age_bracket": None})["age_bracket"] == "unknown" - - -def test_coerces_empty_string_age_bracket_to_unknown() -> None: - assert _claims_of({"age_bracket": ""})["age_bracket"] == "unknown" - - -def test_coerces_null_jurisdiction_to_empty_string() -> None: - assert _claims_of({"jurisdiction": None})["jurisdiction"] == "" - - -def test_coerces_empty_string_jurisdiction_to_empty_string() -> None: - assert _claims_of({"jurisdiction": ""})["jurisdiction"] == "" - - -def test_coerces_null_verified_at_to_none() -> None: - assert _claims_of({"verified_at": None})["verified_at"] is None - - -def test_coerces_empty_string_verified_at_to_none() -> None: - assert _claims_of({"verified_at": ""})["verified_at"] is None - - -def test_both_empty_string_verified_at_normalizes_to_none() -> None: - """Both account_verification + operator_verification with verified_at="" - must normalize to None for cross-language byte parity with Node SDK. - """ - assert ( - _claims_of( - {"verified_at": ""}, - operator_verification={"verified_at": ""}, - )["verified_at"] - is None - ) - - -# Typed-field fallback: production callers populate `data.raw`, but a -# hand-constructed AssessResult (no raw) should still surface the verification -# block via the typed `AssessResult.operator_verification` / -# `AssessResult.account_verification` fields. Mirrors the node sibling's -# typed-field read path. - - -def test_typed_operator_verification_fallback_when_raw_is_none() -> None: - result = AssessResult( - allow=True, - resolved_operator="op_typed", - operator_verification=OperatorVerification( - level="enhanced", - operator_type="api", - verified_at="2026-04-01T00:00:00Z", - ), - raw=None, - ) - profile = build_ucp_profile(**_base_kwargs(), data=result) - d = profile.to_dict() - claims = _agentscore_cap(d)["claims"] - assert claims["operator_id"] == "op_typed" - assert claims["kyc_level"] == "enhanced" - assert claims["verified_at"] == "2026-04-01T00:00:00Z" - - -def test_typed_account_verification_fallback_when_raw_is_none() -> None: - result = AssessResult( - allow=True, - resolved_operator="op_typed", - operator_verification=OperatorVerification(level="verified"), - account_verification={ - "kyc_level": "verified", - "age_bracket": "21+", - "jurisdiction": "US", - "sanctions_clear": True, - }, - raw=None, - ) - profile = build_ucp_profile(**_base_kwargs(), data=result) - d = profile.to_dict() - claims = _agentscore_cap(d)["claims"] - assert claims["kyc_level"] == "verified" - assert claims["age_bracket"] == "21+" - assert claims["jurisdiction"] == "US" - assert claims["sanctions_clear"] is True - - -def test_typed_takes_precedence_over_raw() -> None: - result = AssessResult( - allow=True, - resolved_operator="op_xyz", - operator_verification=OperatorVerification(level="verified"), - account_verification={"kyc_level": "verified"}, - raw={ - "operator_verification": {"level": "none"}, - "account_verification": {"kyc_level": "none"}, - }, - ) - profile = build_ucp_profile(**_base_kwargs(), data=result) - cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] - assert cap.extras["claims"]["kyc_level"] == "verified" - - -def test_raw_fallback_used_when_typed_missing() -> None: - result = AssessResult( - allow=True, - resolved_operator="op_raw", - operator_verification=None, - raw={ - "operator_verification": {"level": "enhanced"}, - "account_verification": {"kyc_level": "enhanced"}, - }, - ) - profile = build_ucp_profile(**_base_kwargs(), data=result) - cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] - assert cap.extras["claims"]["kyc_level"] == "enhanced" - - # Per-element to_dict reserved-key collision guard. Vendor extras can't silently # overwrite a canonical field on the new binding dataclasses. @@ -360,12 +254,51 @@ def test_ucp_service_binding_extras_collision_rejected() -> None: version="2026-04-08", spec="https://ucp.dev/spec", transport="rest", + endpoint="https://x.example", extras={"transport": "different"}, ) with pytest.raises(ValueError, match=r"UCPServiceBinding\.extras key 'transport' collides"): svc.to_dict() +@pytest.mark.parametrize("transport", ["rest", "mcp", "a2a"]) +def test_ucp_service_binding_rejects_missing_endpoint_for_required_transports(transport: str) -> None: + """Per UCP spec service.json: rest/mcp/a2a transports MUST carry an endpoint URL.""" + svc = UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/spec", + transport=transport, # type: ignore[arg-type] + ) + with pytest.raises(ValueError, match="requires `endpoint`"): + svc.to_dict() + + +def test_ucp_service_binding_embedded_does_not_require_endpoint() -> None: + """Per spec service.json: embedded transport MAY omit endpoint.""" + svc = UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/spec", + transport="embedded", + schema="https://ucp.dev/schemas/embedded.json", + ) + out = svc.to_dict() + assert "endpoint" not in out + + +def test_ucp_payment_handler_drops_empty_available_instruments() -> None: + """Per spec payment_handler.json: available_instruments has minItems:1. + Drop the field when empty so callers passing `[]` don't ship an invalid profile.""" + h = UCPPaymentHandlerBinding( + id="tempo", + version="2026-04-08", + spec="https://x", + schema="https://x", + available_instruments=[], + ) + out = h.to_dict() + assert "available_instruments" not in out + + def test_ucp_service_binding_extras_non_reserved_pass_through() -> None: svc = UCPServiceBinding( version="2026-04-08", @@ -381,7 +314,7 @@ def test_ucp_service_binding_extras_non_reserved_pass_through() -> None: def test_ucp_capability_binding_extras_collision_rejected() -> None: cap = UCPCapabilityBinding( - version="1", + version="2026-04-08", spec="https://x/spec", schema="https://x/schema", extras={"schema": "https://attacker"}, @@ -390,21 +323,21 @@ def test_ucp_capability_binding_extras_collision_rejected() -> None: cap.to_dict() -def test_ucp_capability_binding_claims_extra_passes_through() -> None: +def test_ucp_capability_binding_extras_pass_through() -> None: cap = UCPCapabilityBinding( - version="1", + version="2026-04-08", spec="https://x/spec", schema="https://x/schema", - extras={"claims": {"k": "v"}}, + extras={"vendor_field": {"k": "v"}}, ) out = cap.to_dict() - assert out["claims"] == {"k": "v"} + assert out["vendor_field"] == {"k": "v"} def test_ucp_payment_handler_binding_omits_default_empty_config() -> None: h = UCPPaymentHandlerBinding( id="tempo", - version="1", + version="2026-04-08", spec="https://x", schema="https://x", ) @@ -416,7 +349,7 @@ def test_ucp_payment_handler_binding_omits_default_empty_config() -> None: def test_ucp_payment_handler_binding_omits_explicit_empty_config() -> None: h = UCPPaymentHandlerBinding( id="tempo", - version="1", + version="2026-04-08", spec="https://x", schema="https://x", config={}, @@ -427,7 +360,7 @@ def test_ucp_payment_handler_binding_omits_explicit_empty_config() -> None: def test_ucp_payment_handler_binding_preserves_populated_config() -> None: h = UCPPaymentHandlerBinding( id="tempo", - version="1", + version="2026-04-08", spec="https://x", schema="https://x", config={"recipient": "0xabc"}, @@ -448,30 +381,6 @@ def test_ucp_signing_key_extras_non_reserved_pass_through() -> None: assert out == {"kid": "me", "kty": "EC", "alg": "ES256", "crv": "P-256", "x": "abc", "y": "def"} -def test_typed_empty_account_verification_wins_over_raw() -> None: - result = AssessResult( - allow=True, - resolved_operator="op_xyz", - account_verification={}, - raw={"account_verification": {"kyc_level": "verified"}}, - ) - profile = build_ucp_profile(**_base_kwargs(), data=result) - cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] - assert cap.extras["claims"]["kyc_level"] == "none" - - -def test_typed_empty_operator_verification_wins_over_raw() -> None: - result = AssessResult( - allow=True, - resolved_operator="op_xyz", - operator_verification=cast("OperatorVerification", {}), - raw={"operator_verification": {"level": "enhanced", "verified_at": "2026-01-01T00:00:00Z"}}, - ) - profile = build_ucp_profile(**_base_kwargs(), data=result) - cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] - assert cap.extras["claims"]["verified_at"] is None - - def test_ucp_profile_body_can_be_constructed_directly() -> None: """UCPProfileBody is exported so callers can pre-build the body if they want.""" body = UCPProfileBody(version="2026-04-08") diff --git a/tests/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py index 2a737b1..baff144 100644 --- a/tests/test_ucp_cross_lang.py +++ b/tests/test_ucp_cross_lang.py @@ -46,15 +46,13 @@ def test_corpus_covers_canonical_scenarios() -> None: "multikey", "emoji-keys", "int-boundary", - # `data-driven-claims` exercises the raw-dict fallback read path - # (`AssessResult(raw={"account_verification": {...}})`) that - # production callers populate. `typed-claims` exercises the typed - # field path (`AssessResult(account_verification={...}, raw=None)`) - # that hand-constructed callers use — Node's `buildUCPProfile` - # reads typed fields directly without consulting raw, so both - # paths must produce byte-identical canonical bytes across - # languages or cross-lang verify silently drifts. - "data-driven-claims", - "typed-claims", + # `agentscore-gate-full` exercises a fully-populated merchant policy + # (require_kyc + require_sanctions_clear + min_age + allowed_jurisdictions). + # `agentscore-gate-blocked` exercises blocked_jurisdictions (the inverse + # jurisdiction policy). Both languages serialize the merchant gate config + # identically; a drift in either's JCS-canonical output breaks cross-lang + # verify silently. + "agentscore-gate-full", + "agentscore-gate-blocked", ): assert f"{lang}-{scenario}.json" in names, f"missing fixture {lang}-{scenario}.json" diff --git a/uv.lock b/uv.lock index 62e5dc5..0493cc2 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ [[package]] name = "agentscore-commerce" -version = "1.5.1" +version = "1.6.0" source = { editable = "." } dependencies = [ { name = "agentscore-py" }, @@ -122,14 +122,14 @@ dev = [ [[package]] name = "agentscore-py" -version = "2.1.0" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/aa/d857b03cd36cbc44cdce45f180fb639762a658bec492767c11aa97936bf1/agentscore_py-2.1.0.tar.gz", hash = "sha256:4fc811703ccc463dcd689b90fede67b016ea62d6a344e209d9669cd66ec5463c", size = 57894, upload-time = "2026-05-02T04:51:19.978Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/82/65c94878455fca417374c04f9f35c9bbdcee0fe98d43cb1cde5d072b20f6/agentscore_py-2.1.2.tar.gz", hash = "sha256:8830765d89aaea102c092084d24a542c1eb7f83b2b29370eae31417d41c22a41", size = 58027, upload-time = "2026-05-11T00:45:40.84Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/85/cccf18bbf9752edbc39f5dcc718afcdf4973a8be62067582a3d385085894/agentscore_py-2.1.0-py3-none-any.whl", hash = "sha256:11e6fb0c346540d68c11526b01046b6bdbdb0db66ae3a424490587206ce03af1", size = 19086, upload-time = "2026-05-02T04:51:18.503Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/e80ca6f484f198347ec48aa8a190511827b591679da309ee162d43e17856/agentscore_py-2.1.2-py3-none-any.whl", hash = "sha256:c7220cab0de7957e91632634c4502c562e2235548a37f367473548001ee3af38", size = 19087, upload-time = "2026-05-11T00:45:39.067Z" }, ] [[package]] @@ -1569,11 +1569,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.14" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/b1/efac073e0c297ecf2fb33c346989a529d4e19164f1759102dee5953ee17e/idna-3.14.tar.gz", hash = "sha256:466d810d7a2cc1022bea9b037c39728d51ae7dad40d480fc9b7d7ecf98ba8ee3", size = 198272, upload-time = "2026-05-10T20:32:15.935Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3c/3f62dee257eb3d6b2c1ef2a09d36d9793c7111156a73b5654d2c2305e5ce/idna-3.14-py3-none-any.whl", hash = "sha256:e677eaf072e290f7b725f9acf0b3a2bd55f9fd6f7c70abe5f0e34823d0accf69", size = 72184, upload-time = "2026-05-10T20:32:14.295Z" }, ] [[package]] diff --git a/vulture_whitelist.py b/vulture_whitelist.py index ef2b8b7..9c9315c 100644 --- a/vulture_whitelist.py +++ b/vulture_whitelist.py @@ -9,16 +9,9 @@ ex # noqa: F821 # Public API exports -Activity # noqa: F821 AgentScoreGate # noqa: F821 AssessResult # noqa: F821 -Classification # noqa: F821 DenialReason # noqa: F821 -Grade # noqa: F821 -Identity # noqa: F821 -Reputation # noqa: F821 -ScoreDetail # noqa: F821 -ScoreStatus # noqa: F821 OperatorVerification # noqa: F821 # TYPE_CHECKING imports referenced inside string-literal cast() calls