Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0884d10
feat(identity): UCP profile signing helpers (joserfc optional extra)
vvillait88 May 8, 2026
bacfc5e
chore(deps): refresh transitive deps
vvillait88 May 8, 2026
7e6d6ed
hardening(identity): UCP signing security + ergonomics fixes
vvillait88 May 8, 2026
df383ff
hardening: round-2 reviewer findings (security + test gaps)
vvillait88 May 8, 2026
afc7c3c
hardening: round-3 reviewer findings (HIGH JWS header guard + parity)
vvillait88 May 8, 2026
757dedf
hardening: round-4 reviewer findings (warning scope + edge tests + RE…
vvillait88 May 8, 2026
4373205
hardening: round-5 reviewer findings (alg-mismatch + warning scope + …
vvillait88 May 8, 2026
9c2e098
hardening: round-6 reviewer findings (cross-lang corpus + extras + re…
vvillait88 May 9, 2026
2fed413
fix(ucp): round-7 review parity fixes
vvillait88 May 9, 2026
ea80759
hardening: round-9 reviewer findings (int-boundary cross-lang parity)
vvillait88 May 9, 2026
2042ed2
hardening: round-11 reviewer findings (typed-error contract + dict-ke…
vvillait88 May 9, 2026
a4fd2f5
hardening: round-17 reviewer findings (crit precedence cross-lang par…
vvillait88 May 9, 2026
81ffd00
fix(ucp): treat JWS crit=null as malformed_jws
vvillait88 May 9, 2026
3013392
hardening(identity): reject U+2028/U+2029 in profile canonicalization
vvillait88 May 9, 2026
7fe7f32
docs(identity): fix sign_ucp_profile docstring example constructor
vvillait88 May 9, 2026
acda956
hardening(identity): align build_ucp_profile claims coalescing with n…
vvillait88 May 9, 2026
48bced1
hardening(identity): typed-field fallback + per-element extras guard …
vvillait88 May 9, 2026
1bc9ef3
hardening(identity): round-26 UCP signing reviewer findings
vvillait88 May 9, 2026
2f598ca
hardening(identity): reject crit array with non-string elements per R…
vvillait88 May 9, 2026
4dfee92
hardening(identity): align UCP read order to typed-first + reject bytes
vvillait88 May 9, 2026
c00d51b
hardening(identity): make AssessResult.account_verification a typed f…
vvillait88 May 9, 2026
63a7ee2
hardening(identity): typed-empty wins over raw + preserve empty payme…
vvillait88 May 9, 2026
3198c84
docs(ucp): clarify raw fallback as Python-only legacy escape hatch
vvillait88 May 9, 2026
e0aedc1
chore(deps): refresh uv.lock
vvillait88 May 9, 2026
4b9571e
docs: scrub em dashes and refresh examples README
vvillait88 May 9, 2026
7144c52
hardening(examples): sanitize UCP self-test exception, drop unused An…
vvillait88 May 9, 2026
6745320
feat(identity): vendor-namespace UCP signing typ + capability name (1…
vvillait88 May 9, 2026
09e0f71
fix(identity): align hand-crafted capability fixture schema URL with SDK
vvillait88 May 9, 2026
bd81dfe
fix(identity): unwrap doubly-nested extras in py emoji-keys + int-bou…
vvillait88 May 9, 2026
644fdc2
fix(identity): align UCPPaymentHandler config emission with Node opti…
vvillait88 May 9, 2026
d487d61
chore(scripts): remove standalone fixture generators shadowed by orch…
vvillait88 May 9, 2026
c516f43
fix(identity): coerce both-empty verified_at to None for cross-lang p…
vvillait88 May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ Every helper is extracted from a real consumer, not speculated.
| Submodule | What it is |
|---|---|
| `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware (KYC, age, sanctions, jurisdiction) |
| `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.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.discovery` | Discovery probe, Bazaar wrapper, `/.well-known/mpp.json`, `llms.txt` builder, `skill.md` builder (Claude-Skill-compatible agent-discovery manifest), OpenAPI snippets, `NoindexNonDiscoveryMiddleware` ASGI middleware |
| `agentscore_commerce.challenge` | 402-body builders: accepted_methods, identity_metadata, how_to_pay, agent_instructions, build_402_body, `build_validation_error` (4xx body builder) |
| `agentscore_commerce.stripe_multichain` | Multichain PaymentIntent helper, deposit-address lookup, testnet simulator, mppx Stripe wrapper |
| `agentscore_commerce.api` | Re-exports `AgentScore` from `agentscore` SDK |

## 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_assess_data`, `get_gate_degraded_state`, `get_gate_quota_info`) with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim).

| Directory | Contents |
|---|---|
Expand All @@ -30,11 +30,11 @@ Single Python package, hatchling-built, published to PyPI as `agentscore-commerc
| `examples/` | Runnable single-file FastAPI apps for each common scenario |
| `tests/` | pytest, one file per surface |

Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtimevendors 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.
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.

## Examples

`examples/` contains full single-file FastAPI apps for the most common merchant scenarios copy-paste templates, not frameworks:
`examples/` contains full single-file FastAPI apps for the most common merchant scenarios; copy-paste templates, not frameworks:

| Example | Scenario |
|---|---|
Expand All @@ -43,28 +43,28 @@ Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime —
| `multi_rail_merchant.py` | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT |
| `stripe_multichain_merchant.py` | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) |
| `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) |
| `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`) |
| `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`) |
| `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`. |

## Identity model

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.
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.

`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.

`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.

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.
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.

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).
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).

### Fail-open (opt-in)

`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.
`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.

### Mount posture: gate-first vs gate-conditional

`AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic) is mounted directly when the route is AgentScore-onlyevery 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:
`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:

```python
_gate = AgentScoreGate(api_key=..., require_kyc=True, ...)
Expand All @@ -87,20 +87,20 @@ Anonymous POST flows through to the handler unauthenticated and gets a 402 with

### `compatible_clients` field on emitted 402s

`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.
`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.

## Tooling

- **uv** package manager.
- **ruff** linting + formatting.
- **ty** type checker (Astral).
- **vulture** dead code detection.
- **pytest** tests.
- **Lefthook** pre-commit ruff, pre-push ty + vulture (parallel).
- **uv**: package manager.
- **ruff**: linting + formatting.
- **ty**: type checker (Astral).
- **vulture**: dead code detection.
- **pytest**: tests.
- **Lefthook**: pre-commit ruff, pre-push ty + vulture (parallel).

```bash
uv sync --all-extras
uv run lefthook install # one-time per clone wires pre-commit + pre-push
uv run lefthook install # one-time per clone; wires pre-commit + pre-push
uv run ruff check .
uv run ruff format .
uv run ty check agentscore_commerce/
Expand All @@ -112,16 +112,16 @@ uv run pytest tests/
1. Create a branch
2. Make changes
3. Lefthook runs ruff on commit, ty + vulture on push
4. Open a PR CI runs automatically
4. Open a PR (CI runs automatically)
5. Merge (squash)

## Rules

- **No silent refactors**
- **Never commit .env files or secrets**
- **Use PRs** never push directly to main
- **Use PRs**: never push directly to main
- **Helpers are protocol translations + configurable opinions, not opinionated frameworks**
- **Cross-language API parity** keep the surface area identical between the node and python flavors so vendors switching languages have the same mental model
- **Cross-language API parity**: keep the surface area identical between the node and python flavors so vendors switching languages have the same mental model

## Releasing

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Thanks for your interest in contributing! Here's how to get started.

- All PRs require 1 approval before merging
- Squash merge to `main` is the standard
- Keep PRs focused one feature or fix per PR
- Keep PRs focused: one feature or fix per PR
- Include tests for new functionality
- Make sure CI passes before requesting review

Expand Down
Loading
Loading