You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Summary
- Add four helpers for UCP §6 trust-mode profiles:
`generate_ucp_signing_key`, `sign_ucp_profile`, `verify_ucp_profile`,
`build_jwks_response`. Profiles are JWS-signed (Compact Serialization)
over a JCS-canonicalized body. Both EdDSA (Ed25519, default) and ES256
supported.
- Cross-language byte parity with `@agent-score/commerce` Node SDK 1.3.4
— profiles signed by either SDK verify in the other.
- `joserfc` is an optional extra (`pip install
agentscore-commerce[ucp]`); vendors install it only when publishing
signed UCP profiles. Unsigned `build_ucp_profile` keeps working.
- `/.well-known/jwks.json` added to the default discovery-path
allowlist.
- Bumps to **1.3.7**.
## Test plan
- [x] Round-trip sign+verify for Ed25519 and ES256
- [x] Multi-key JWKS resolves by `kid`
- [x] Tamper detection via byte-equal canonical-body check after JWS
verify
- [x] Missing key in JWKS rejects with `InvalidKeyIdError`
- [x] Profile without signature rejects
- [x] Key-order-independent verification (canonicalization)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|`agentscore_commerce.payment`| Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps `x402[evm]>=2.9` + `cdp-sdk` for `facilitator="coinbase"`; install via the `coinbase` extra), `build_x402_accepts_for_402` (build the 402's `accepts[]` from the registered scheme — derives the right `extra.name` per network), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header |
12
+
|`agentscore_commerce.payment`| Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps `x402[evm]>=2.9` + `cdp-sdk` for `facilitator="coinbase"`; install via the `coinbase` extra), `build_x402_accepts_for_402` (build the 402's `accepts[]` from the registered scheme; derives the right `extra.name` per network), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header |
|`agentscore_commerce.api`| Re-exports `AgentScore` from `agentscore` SDK |
17
17
18
18
## Architecture
19
19
20
-
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).
20
+
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).
21
21
22
22
| Directory | Contents |
23
23
|---|---|
@@ -30,11 +30,11 @@ Single Python package, hatchling-built, published to PyPI as `agentscore-commerc
30
30
|`examples/`| Runnable single-file FastAPI apps for each common scenario |
31
31
|`tests/`| pytest, one file per surface |
32
32
33
-
Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime — vendors install only what they use via extras (`pip install agentscore-commerce[fastapi,stripe]` etc.). Underlying packages: `x402[evm]`, `pympp[server,tempo,stripe]`, `stripe`, `cdp-sdk` (the `coinbase` extra — only needed when `facilitator="coinbase"`). Missing peer dep raises a guiding `ImportError` with the install command.
33
+
Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime; vendors install only what they use via extras (`pip install agentscore-commerce[fastapi,stripe]` etc.). Underlying packages: `x402[evm]`, `pympp[server,tempo,stripe]`, `stripe`, `cdp-sdk` (the `coinbase` extra; only needed when `facilitator="coinbase"`). Missing peer dep raises a guiding `ImportError` with the install command.
34
34
35
35
## Examples
36
36
37
-
`examples/` contains full single-file FastAPI apps for the most common merchant scenarios — copy-paste templates, not frameworks:
37
+
`examples/` contains full single-file FastAPI apps for the most common merchant scenarios; copy-paste templates, not frameworks:
|`variable_cost_merchant.py`| Pay-per-actual-usage on **two protocols**: x402 upto (Permit2 + Settlement-Overrides) AND MPP tempo session (channel + SSE + mid-stream vouchers) |
46
-
|`compliance_merchant.py`| Regulated-goods merchant — full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`) |
46
+
|`compliance_merchant.py`| Regulated-goods merchant: full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`) |
47
47
|`per_product_policy_merchant.py`| Multi-product merchant where each row carries its own compliance policy. One product hard-gates KYC + age + state; another is anonymous; a third uses `enforcement="soft"` (request KYC but don't block sale). Demonstrates `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. |
48
+
|`signed_ucp_merchant.py`| Signed UCP profile (`/.well-known/ucp`) + JWKS endpoint (`/.well-known/jwks.json`). AgentScore's `agentscore-profile+jws` is a vendor extension on top of UCP for trust-mode verifiers (Visa AP2 pilots, regulated-commerce verifiers) that opt into auditable cryptographic provenance — UCP §6 itself does NOT mandate signing; Pura Vida and other Shopify-backed UCP merchants ship unsigned in production. Wires ephemeral-for-dev / env-JWK-for-prod signing, kid rotation, and `Cache-Control` posture. Uses `generate_ucp_signing_key`, `sign_ucp_profile`, `build_jwks_response`, `UCPSigningKey.from_jwk`, `UCPVerificationError`. |
48
49
49
50
## Identity model
50
51
51
-
Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-Token`). Default checks operator-token first, then wallet. Address normalization is network-aware via `agentscore_commerce/identity/address.py`: EVM lowercased, Solana base58 preserved verbatim — used for cache keys, wallet→operator resolves, and signer-match comparisons.
52
+
Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-Token`). Default checks operator-token first, then wallet. Address normalization is network-aware via `agentscore_commerce/identity/address.py`: EVM lowercased, Solana base58 preserved verbatim. Used for cache keys, wallet→operator resolves, and signer-match comparisons.
52
53
53
54
`DenialReason` codes (`missing_identity`, `identity_verification_required`, `token_expired`, `invalid_credential`, `wallet_signer_mismatch`, `wallet_auth_requires_wallet_signing`, `wallet_not_trusted`, `api_error`, `payment_required`) each carry a structured `agent_instructions` JSON block describing concrete recovery actions. See `agentscore_commerce/identity/_response.py` for the canned action copy.
54
55
55
56
`create_session_on_missing` auto-mints a verification session when no identity is present and returns 403 with `verify_url` + poll instructions. `verify_wallet_signer_match` (per-adapter) compares the recovered signer against `linked_wallets[]` for cross-chain wallet-stack matching.
56
57
57
-
Captured wallets: `capture_wallet(...)` is fire-and-forget — reads`operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests.
58
+
Captured wallets: `capture_wallet(...)` is fire-and-forget. Reads`operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests.
58
59
59
-
Wallet-signer-match: `verify_wallet_signer_match` / `averify_wallet_signer_match` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response — collapses the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signer_match_by_signer` sub-dict and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety).
60
+
Wallet-signer-match: `verify_wallet_signer_match` / `averify_wallet_signer_match` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response, collapsing the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signer_match_by_signer` sub-dict and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety).
60
61
61
62
### Fail-open (opt-in)
62
63
63
-
`fail_open=True` on `AgentScoreGate(...)` (or `agentscore_gate(app, ...)`) flips infra-failure handling: 429 / 5xx / network-timeout pass through to the handler with the gate state stamped `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"`. `get_gate_degraded_state(request)` (Flask: `get_gate_degraded_state()` — reads from `g`) returns `{"degraded": bool, "infra_reason"?: str}` for merchant logging/alerting. Default stays `fail_open=False` — regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag. The gate's `try` wraps only the AgentScore call — never the downstream user handler.
64
+
`fail_open=True` on `AgentScoreGate(...)` (or `agentscore_gate(app, ...)`) flips infra-failure handling: 429 / 5xx / network-timeout pass through to the handler with the gate state stamped `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"`. `get_gate_degraded_state(request)` (Flask: `get_gate_degraded_state()`, reads from `g`) returns `{"degraded": bool, "infra_reason"?: str}` for merchant logging/alerting. Default stays `fail_open=False`; regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag. The gate's `try` wraps only the AgentScore call, never the downstream user handler.
64
65
65
66
### Mount posture: gate-first vs gate-conditional
66
67
67
-
`AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic) is mounted directly when the route is AgentScore-only — every request runs identity + policy. To support **anonymous discovery by any spec-compliant x402 wallet** (Coinbase awal, Phantom, Solflare, …), wrap the gate so it fires only when a payment credential is attached:
68
+
`AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic) is mounted directly when the route is AgentScore-only; every request runs identity + policy. To support **anonymous discovery by any spec-compliant x402 wallet** (Coinbase awal, Phantom, Solflare, ...), wrap the gate so it fires only when a payment credential is attached:
@@ -87,20 +88,20 @@ Anonymous POST flows through to the handler unauthenticated and gets a 402 with
87
88
88
89
### `compatible_clients` field on emitted 402s
89
90
90
-
`build_agent_instructions` emits a `compatible_clients` field in the 402 body, derived automatically from `how_to_pay` — per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `BuildAgentInstructionsInput(compatible_clients={...})` to add their own tested clients. Set to an empty dict `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands.
91
+
`build_agent_instructions` emits a `compatible_clients` field in the 402 body, derived automatically from `how_to_pay`: per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `BuildAgentInstructionsInput(compatible_clients={...})` to add their own tested clients. Set to an empty dict `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands.
91
92
92
93
## Tooling
93
94
94
-
-**uv** — package manager.
95
-
-**ruff** — linting + formatting.
96
-
-**ty** — type checker (Astral).
97
-
-**vulture** — dead code detection.
98
-
-**pytest** — tests.
99
-
-**Lefthook** — pre-commit ruff, pre-push ty + vulture (parallel).
95
+
-**uv**: package manager.
96
+
-**ruff**: linting + formatting.
97
+
-**ty**: type checker (Astral).
98
+
-**vulture**: dead code detection.
99
+
-**pytest**: tests.
100
+
-**Lefthook**: pre-commit ruff, pre-push ty + vulture (parallel).
100
101
101
102
```bash
102
103
uv sync --all-extras
103
-
uv run lefthook install # one-time per clone — wires pre-commit + pre-push
104
+
uv run lefthook install # one-time per clone; wires pre-commit + pre-push
104
105
uv run ruff check .
105
106
uv run ruff format .
106
107
uv run ty check agentscore_commerce/
@@ -112,16 +113,16 @@ uv run pytest tests/
112
113
1. Create a branch
113
114
2. Make changes
114
115
3. Lefthook runs ruff on commit, ty + vulture on push
115
-
4. Open a PR — CI runs automatically
116
+
4. Open a PR (CI runs automatically)
116
117
5. Merge (squash)
117
118
118
119
## Rules
119
120
120
121
-**No silent refactors**
121
122
-**Never commit .env files or secrets**
122
-
-**Use PRs** — never push directly to main
123
+
-**Use PRs**: never push directly to main
123
124
-**Helpers are protocol translations + configurable opinions, not opinionated frameworks**
124
-
-**Cross-language API parity** — keep the surface area identical between the node and python flavors so vendors switching languages have the same mental model
125
+
-**Cross-language API parity**: keep the surface area identical between the node and python flavors so vendors switching languages have the same mental model
0 commit comments