From 2860379d0ad5ba634f7d51b3d98981662a0d0119 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 3 Oct 2026 15:41:28 -0700 Subject: [PATCH] docs: correct A2A signing docs and supported versions; drop em-dashes --- CLAUDE.md | 22 +++--- README.md | 12 +-- SECURITY.md | 4 +- agentscore_commerce/__init__.py | 4 +- agentscore_commerce/_headers.py | 2 +- agentscore_commerce/_mppx_receipt.py | 6 +- agentscore_commerce/_redis.py | 4 +- agentscore_commerce/_warnings.py | 2 +- agentscore_commerce/aip/__init__.py | 2 +- agentscore_commerce/aip/gate.py | 26 +++---- agentscore_commerce/aip/http_signature.py | 30 ++++---- agentscore_commerce/aip/jwks.py | 36 ++++----- agentscore_commerce/aip/request.py | 18 ++--- agentscore_commerce/aip/types.py | 10 +-- agentscore_commerce/aip/verify.py | 28 +++---- agentscore_commerce/api/__init__.py | 2 +- .../challenge/agent_instructions.py | 6 +- agentscore_commerce/challenge/agent_memory.py | 2 +- agentscore_commerce/challenge/body.py | 2 +- agentscore_commerce/challenge/how_to_pay.py | 6 +- agentscore_commerce/challenge/pricing.py | 4 +- agentscore_commerce/challenge/respond_402.py | 10 +-- .../challenge/validation_error.py | 2 +- agentscore_commerce/checkout.py | 76 +++++++++---------- agentscore_commerce/checkout_compute_first.py | 18 ++--- agentscore_commerce/discovery/__init__.py | 2 +- .../discovery/agentscore_content.py | 12 +-- agentscore_commerce/discovery/llms_txt.py | 4 +- agentscore_commerce/discovery/openapi.py | 4 +- agentscore_commerce/discovery/probe.py | 12 +-- agentscore_commerce/discovery/robots_tag.py | 4 +- agentscore_commerce/discovery/skill_md.py | 10 +-- agentscore_commerce/discovery/well_known.py | 2 +- agentscore_commerce/forwarded_proto.py | 4 +- agentscore_commerce/identity/_denial.py | 22 +++--- agentscore_commerce/identity/_response.py | 58 +++++++------- agentscore_commerce/identity/a2a.py | 16 ++-- agentscore_commerce/identity/address.py | 4 +- agentscore_commerce/identity/aiohttp.py | 10 +-- agentscore_commerce/identity/core.py | 44 +++++------ .../identity/default_denied.py | 2 +- agentscore_commerce/identity/django.py | 18 ++--- agentscore_commerce/identity/fastapi.py | 16 ++-- agentscore_commerce/identity/flask.py | 12 +-- agentscore_commerce/identity/middleware.py | 14 ++-- agentscore_commerce/identity/policy.py | 26 +++---- agentscore_commerce/identity/sanic.py | 8 +- agentscore_commerce/identity/sessions.py | 14 ++-- agentscore_commerce/identity/signer.py | 2 +- agentscore_commerce/identity/types.py | 24 +++--- agentscore_commerce/identity/ucp.py | 14 ++-- agentscore_commerce/identity/ucp_jwks.py | 26 +++---- agentscore_commerce/middleware/asgi.py | 10 +-- agentscore_commerce/payment/__init__.py | 2 +- agentscore_commerce/payment/amounts.py | 2 +- agentscore_commerce/payment/compose_rails.py | 4 +- agentscore_commerce/payment/default_rails.py | 4 +- agentscore_commerce/payment/dispatch.py | 4 +- agentscore_commerce/payment/headers.py | 20 ++--- agentscore_commerce/payment/idempotency.py | 8 +- agentscore_commerce/payment/mppx_server.py | 16 ++-- agentscore_commerce/payment/network_kind.py | 2 +- agentscore_commerce/payment/payment_header.py | 10 +-- .../payment/settlement_override.py | 2 +- agentscore_commerce/payment/solana.py | 8 +- .../payment/wwwauthenticate.py | 2 +- agentscore_commerce/payment/x402_server.py | 28 +++---- agentscore_commerce/payment/x402_settle.py | 10 +-- .../payment/x402_validation.py | 12 +-- agentscore_commerce/payment/zero_settle.py | 4 +- agentscore_commerce/quote_cache.py | 6 +- .../stripe_multichain/__init__.py | 2 +- .../stripe_multichain/mppx_stripe.py | 8 +- .../stripe_multichain/pay_to_address.py | 8 +- .../stripe_multichain/pi_cache.py | 18 ++--- .../stripe_multichain/simulate_deposit.py | 12 +-- examples/README.md | 4 +- examples/compliance_merchant.py | 2 +- examples/compute_first_merchant.py | 8 +- examples/identity_only.py | 6 +- examples/multi_rail_merchant.py | 2 +- examples/per_product_policy_merchant.py | 4 +- examples/stripe_multichain_merchant.py | 6 +- osv-scanner.toml | 6 +- pyproject.toml | 2 +- scripts/regenerate_cross_lang_fixtures.py | 24 +++--- tests/test_address.py | 6 +- tests/test_agent_memory_emitter.py | 4 +- tests/test_aiohttp.py | 6 +- tests/test_aip_adapters.py | 4 +- tests/test_aip_checkout.py | 24 +++--- tests/test_aip_gate.py | 2 +- tests/test_aip_http_signature.py | 14 ++-- tests/test_aip_jwks.py | 12 +-- tests/test_aip_request.py | 2 +- tests/test_aip_verify.py | 4 +- tests/test_challenge.py | 2 +- tests/test_checkout.py | 18 ++--- tests/test_checkout_compute_first.py | 6 +- tests/test_checkout_compute_first_settle.py | 6 +- tests/test_checkout_signer_match.py | 8 +- tests/test_checkout_wallet_ofac_default.py | 4 +- tests/test_classify_orchestration_error.py | 4 +- tests/test_core.py | 10 +-- tests/test_default_read_only_on_denied.py | 2 +- tests/test_denial.py | 4 +- tests/test_django.py | 2 +- tests/test_extract_owner_scope.py | 6 +- tests/test_fastapi.py | 14 ++-- tests/test_flask.py | 2 +- tests/test_gate_quota_info.py | 2 +- tests/test_get_signer_verdict.py | 4 +- tests/test_idempotency_helper.py | 2 +- tests/test_lifted_helpers.py | 2 +- tests/test_load_ucp_signing_key_from_env.py | 2 +- tests/test_middleware.py | 12 +-- tests/test_network_kind.py | 2 +- tests/test_pay_to_address.py | 4 +- tests/test_payment_directive.py | 2 +- tests/test_payment_header.py | 2 +- tests/test_payment_servers.py | 6 +- tests/test_payment_signer.py | 10 +-- tests/test_policy.py | 6 +- tests/test_quote_cache_redis.py | 2 +- tests/test_rail_spec.py | 4 +- tests/test_redis_internal.py | 2 +- tests/test_response.py | 6 +- tests/test_robots_tag.py | 2 +- tests/test_sanic.py | 2 +- tests/test_seamless_helpers.py | 8 +- tests/test_signer_match.py | 10 +-- tests/test_signer_verdict_request_scoped.py | 8 +- tests/test_solana.py | 6 +- tests/test_tokens.py | 4 +- tests/test_ucp.py | 4 +- tests/test_ucp_jwks.py | 2 +- tests/test_zero_settle.py | 2 +- 137 files changed, 634 insertions(+), 634 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index aff0954..525662e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,14 +8,14 @@ Every helper is extracted from a real consumer, not speculated. | Submodule | What it is | |---|---| -| `agentscore_commerce` (top-level) | `Checkout` orchestrator (the 2.0 high-level surface): one config object + hooks (pre_validate, compute_pricing, mint_recipients, compose_mppx, on_settled, gate), auto-derived x402+pympp servers, per-framework adapters `handle_fastapi`/`handle_flask`/`handle_django`/`handle_aiohttp`/`handle_sanic`, signed UCP routes via `mount_ucp_routes_{fastapi,flask,django,aiohttp,sanic}`, optional `discovery_probe` config for x402-crawler auto-routing. Plus `compute_first_checkout` — variable-cost pay-per-result helper (compute-first + exact-x402). Scope is exact-mode rails only (x402-exact Base, tempo/charge, solana/charge, Stripe SPT); does NOT use x402-upto (Permit2) or Settlement-Overrides — variable cost is captured by running the work pre-settle and emitting a 402 at the exact computed price. `create_quote_cache` — content-hash quote cache used by the compute-first helper (in-memory by default; pass `redis_url` for distributed deployments). `create_result_cache` — the neutral primitive under it: keyed JSON-value cache with the same body-hash key builder; use it to cache any probe-leg result a Checkout-class merchant replays on the settle leg (e.g. a paid upstream call made in `pre_validate`). `create_default_on_denied` — canonical `on_denied(reason)` factory matching `Checkout`'s gate hook (handles `wallet_signer_mismatch`, `wallet_not_trusted` unfixable fallback, `payment_required`, `token_expired`/`invalid_credential`/`api_error`); merchants pass `merchant_name` + `support_email` and override `wallet_not_trusted_message` / `payment_required_message` / `support_context` for vendor-specific copy. `should_run_conditional_gate` / `requests_verification_session` / `has_identity_header` / `VERIFICATION_SESSION_HEADER`: the conditional-gate predicate (payment credential or an opt-in pre-payment verification-session request) and its parts; `build_identity_bootstrap`: the matching `identity_bootstrap` 402 block. `has_payment_header` — discriminator that splits discovery legs (no payment credential → 402) from settle legs (`payment-signature` / `x-payment` / `Authorization: Payment `); `has_x402_header` / `has_mppx_header` — granular dispatch helpers (x402 vs MPP credential present) for routes that branch on rail. `malformed_payment_credential` — wire-shape gate for payment credentials (not base64 JSON, not token-shaped → reject); `Checkout` runs it before any merchant hook by default (`credential_pre_check=False` opts out). `default_read_only_on_denied(reason)` — canonical `on_denied` for read-only resource gates (`GET /orders/:id`): collapses every denial to 401 `unauthorized` + `Cache-Control: no-store` while still spreading `denial_reason_to_body` so `agent_instructions` / `verify_url` ride through. Returns a `DefaultOnDeniedResult(body, status, headers)` dataclass; FastAPI / Flask / aiohttp / Sanic `on_denied` callbacks accept an optional 3-tuple `(body, status, headers)` — convert with `lambda req, reason: (r := default_read_only_on_denied(reason), (r.body, r.status, r.headers or {}))[1]` or a named wrapper. Django + ASGI middleware adapters return Response objects directly; construct `JsonResponse(r.body, status=r.status, headers=r.headers)` / `JSONResponse(content=r.body, status_code=r.status, headers=r.headers)`. `extract_owner_scope(headers) -> OwnerScope` — pull canonical owner identity from `X-Wallet-Address` / `X-Operator-Token` with safe token hashing; pair with a wallet-or-token-scoped resource query so plaintext tokens never leave the request. Plus factories: `pricing_result` (cents → typed `PricingResult`), `validation_response_{fastapi,flask,django,aiohttp,sanic}` (4xx envelope per framework) | +| `agentscore_commerce` (top-level) | `Checkout` orchestrator: one config object + hooks (pre_validate, compute_pricing, mint_recipients, compose_mppx, on_settled, gate), auto-derived x402+pympp servers, per-framework adapters `handle_fastapi`/`handle_flask`/`handle_django`/`handle_aiohttp`/`handle_sanic`, signed UCP routes via `mount_ucp_routes_{fastapi,flask,django,aiohttp,sanic}`, optional `discovery_probe` config for x402-crawler auto-routing. Plus `compute_first_checkout`: variable-cost pay-per-result helper (compute-first + exact-x402). Scope is exact-mode rails only (x402-exact Base, tempo/charge, solana/charge, Stripe SPT); does NOT use x402-upto (Permit2) or Settlement-Overrides: variable cost is captured by running the work pre-settle and emitting a 402 at the exact computed price. `create_quote_cache`: content-hash quote cache used by the compute-first helper (in-memory by default; pass `redis_url` for distributed deployments). `create_result_cache`: the neutral primitive under it: keyed JSON-value cache with the same body-hash key builder; use it to cache any probe-leg result a Checkout-class merchant replays on the settle leg (e.g. a paid upstream call made in `pre_validate`). `create_default_on_denied`: canonical `on_denied(reason)` factory matching `Checkout`'s gate hook (handles `wallet_signer_mismatch`, `wallet_not_trusted` unfixable fallback, `payment_required`, `token_expired`/`invalid_credential`/`api_error`); merchants pass `merchant_name` + `support_email` and override `wallet_not_trusted_message` / `payment_required_message` / `support_context` for vendor-specific copy. `should_run_conditional_gate` / `requests_verification_session` / `has_identity_header` / `VERIFICATION_SESSION_HEADER`: the conditional-gate predicate (payment credential or an opt-in pre-payment verification-session request) and its parts; `build_identity_bootstrap`: the matching `identity_bootstrap` 402 block. `has_payment_header`: discriminator that splits discovery legs (no payment credential → 402) from settle legs (`payment-signature` / `x-payment` / `Authorization: Payment `); `has_x402_header` / `has_mppx_header`: granular dispatch helpers (x402 vs MPP credential present) for routes that branch on rail. `malformed_payment_credential`: wire-shape gate for payment credentials (not base64 JSON, not token-shaped → reject); `Checkout` runs it before any merchant hook by default (`credential_pre_check=False` opts out). `default_read_only_on_denied(reason)`: canonical `on_denied` for read-only resource gates (`GET /orders/:id`): collapses every denial to 401 `unauthorized` + `Cache-Control: no-store` while still spreading `denial_reason_to_body` so `agent_instructions` / `verify_url` ride through. Returns a `DefaultOnDeniedResult(body, status, headers)` dataclass; FastAPI / Flask / aiohttp / Sanic `on_denied` callbacks accept an optional 3-tuple `(body, status, headers)`: convert with `lambda req, reason: (r := default_read_only_on_denied(reason), (r.body, r.status, r.headers or {}))[1]` or a named wrapper. Django + ASGI middleware adapters return Response objects directly; construct `JsonResponse(r.body, status=r.status, headers=r.headers)` / `JSONResponse(content=r.body, status_code=r.status, headers=r.headers)`. `extract_owner_scope(headers) -> OwnerScope`: pull canonical owner identity from `X-Wallet-Address` / `X-Operator-Token` with safe token hashing; pair with a wallet-or-token-scoped resource query so plaintext tokens never leave the request. Plus factories: `pricing_result` (cents → typed `PricingResult`), `validation_response_{fastapi,flask,django,aiohttp,sanic}` (4xx envelope per framework) | | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware (KYC, age, sanctions on both account name and signer wallet, jurisdiction). Each adapter exports a conditional variant that wraps the gate so it fires only on settle legs (anonymous discovery flows through and gets a 402 with all rails): FastAPI / ASGI expose `ConditionalAgentScoreGate`, Django exposes `ConditionalAgentScoreMiddleware`, Flask + Sanic expose `conditional_agentscore_gate(app, ...)`, aiohttp exposes `conditional_agentscore_gate_middleware(...)`. Adapters export ONLY framework-specific surface (gate classes / fns, accessors, `capture_wallet`); shared helpers like `has_payment_header` / `denial_reason_to_body` import from their canonical home (`agentscore_commerce.payment` and `agentscore_commerce.identity` respectively). The Flask / Sanic `agentscore_gate(app, ...)` function accepts an optional `condition=` callable for inline gating (`AgentScoreGate.__init__` does not). | | `agentscore_commerce.identity.policy` | Per-product compliance helpers: `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`, `validate_shipping_against_policy` (one-call country+state validator that raises `CheckoutValidationError` with the canonical envelope on miss) | -| `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), `build_default_checkout_rails(tempo=, x402_base=, solana_mpp=, stripe=)` (canonical 4-rail `rails` dict factory: merchants pass per-rail overrides instead of redeclaring the recipient sentinel + network/chain_id/token boilerplate. When a caller flips `network` without pinning `token` / `chain_id`, the underlying dataclass derives them from the network: Base Sepolia → Sepolia USDC + chain_id 84532, Solana devnet → devnet USDC mint. Explicit overrides always win. Solana's `network` field accepts both CAIP-2 (`solana:5eykt4UsFv8…` / `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`) AND the raw `@solana/mpp` form (`mainnet-beta` / `devnet` / `localnet`)), `build_mppx_compose_rails(amount_usd=, tempo_recipient=, solana_recipient=, ...)` (per-call intent factory replacing the hand-rolled `[("tempo/charge", {...}), ("solana/charge", {...}), ("stripe/charge", {...})]` list; auto-handles USD→atomic conversion for Solana; auto-drops the `stripe/charge` rail with a one-time `logging.warning` when `amount_usd < 0.50` since Stripe's fixed ~$0.30 fee makes sub-50-cent charges unprofitable — many Stripe accounts also reject PI creation below the floor with `amount_too_small`; sub-50-cent APIs pass `include_stripe=False` explicitly to silence the warning), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), `is_evm_network`/`is_solana_network` (CAIP-2 discriminators that hide the `startswith("eip155:")` / `startswith("solana:")` prefix matching), `has_payment_header` (settle-leg vs discovery-leg discriminator), `parse_did_pkh_address` (parses ``did:pkh:::`` into a `PaymentSigner`), 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), `build_default_checkout_rails(tempo=, x402_base=, solana_mpp=, stripe=)` (canonical 4-rail `rails` dict factory: merchants pass per-rail overrides instead of redeclaring the recipient sentinel + network/chain_id/token boilerplate. When a caller flips `network` without pinning `token` / `chain_id`, the underlying dataclass derives them from the network: Base Sepolia → Sepolia USDC + chain_id 84532, Solana devnet → devnet USDC mint. Explicit overrides always win. Solana's `network` field accepts both CAIP-2 (`solana:5eykt4UsFv8…` / `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`) AND the raw `@solana/mpp` form (`mainnet-beta` / `devnet` / `localnet`)), `build_mppx_compose_rails(amount_usd=, tempo_recipient=, solana_recipient=, ...)` (per-call intent factory replacing the hand-rolled `[("tempo/charge", {...}), ("solana/charge", {...}), ("stripe/charge", {...})]` list; auto-handles USD→atomic conversion for Solana; auto-drops the `stripe/charge` rail with a one-time `logging.warning` when `amount_usd < 0.50` since Stripe's fixed ~$0.30 fee makes sub-50-cent charges unprofitable: many Stripe accounts also reject PI creation below the floor with `amount_too_small`; sub-50-cent APIs pass `include_stripe=False` explicitly to silence the warning), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), `is_evm_network`/`is_solana_network` (CAIP-2 discriminators that hide the `startswith("eip155:")` / `startswith("solana:")` prefix matching), `has_payment_header` (settle-leg vs discovery-leg discriminator), `parse_did_pkh_address` (parses ``did:pkh:::`` into a `PaymentSigner`), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header | | `agentscore_commerce.discovery` | Discovery probe (`is_discovery_probe_request`, `build_discovery_probe_response`; the x402 sample envelope always carries a v2 `resource` and threads optional `extensions`, and `Checkout` fills both from its own `url`/`resource_info`/`discovery_extensions`, because envelope validators (mppx, x402scan's shared engine) hard-require `resource` and read the Bazaar example input to build valid probe bodies), Bazaar wrapper, `/.well-known/mpp.json`, `llms.txt` builder, `skill.md` builder (Claude-Skill-compatible agent-discovery manifest), `build_redemption_skill_md` (delivery-neutral; printed/emailed/API-trial codes all covered via `delivery_intro`/`body_shape`/`body_rules`/`extra_recovery_rows` overrides), `build_merchant_index_json` + `standard_endpoint_descriptions(kind=)` (canonical `/` discovery body for goods or API merchants), `build_success_next_steps` (universal Passport-active success block), `build_agentscore_onboarding_steps`, OpenAPI snippets, `NoindexNonDiscoveryMiddleware` ASGI middleware. Plus the UCP/JWKS publish surface: `build_signed_ucp_response`, `build_signed_jwks_response`, `well_known_preflight_response`, `default_a2a_services`, `bootstrap_ucp_signing_key`, framework-neutral `SignedDiscoveryResponse` + per-framework wrappers `signed_response_{fastapi,flask,django,aiohttp,sanic}` | | `agentscore_commerce.challenge` | 402-body builders: accepted_methods, identity_metadata (auto-attached by `Checkout` when wallet header present), how_to_pay, agent_instructions, build_402_body, pricing, agent_memory, `build_validation_error` (4xx body builder), `Receipt`/`ReceiptNextSteps`/`ProductInfo`/`ShippingAddress` (canonical 200-receipt dataclasses) | -| `agentscore_commerce.stripe_multichain` | Multichain PaymentIntent helper (`create_multichain_payment_intent` returns `MultichainPaymentIntentResult`; read `result.deposit_addresses[network]` directly), `create_pay_to_address_from_stripe_pi(authorization_header=, amount_cents=, stripe=, pi_cache=, networks=, static_recipients=, metadata=, order_id=, preferred_network=)` — one-call per-order payTo resolver matching `Checkout.mint_recipients`: on the settle leg, reuses the buyer's signed-against payTo from the MPP credential (after `pi_cache.has_address` check OR a `static_recipients` match — the static address is always-accepted because the merchant owns it); on the discovery leg, mints a fresh PI for the rails NOT covered by `static_recipients`, caches the merged map, registers static addresses with `pi_cache.cache_address`. `mint_multichain_recipients(...same kwargs) -> MintMultichainRecipientsResult(recipients, payment_intent_id, reused_from_credential)` — structured variant returning the full per-rail map; prefer this when the merchant's `mint_recipients` hook needs all rail addresses (typical multi-rail merchant), and to avoid the "returned-string-is-ambiguous" trap on the settle leg when `static_recipients` is configured. Use `static_recipients={"solana": ""}` for low-margin endpoints where rotating per-PI Solana addresses can't absorb MPP spec §13.6's ~$0.50 ATA rent per call — the SDK skips Stripe minting on that network and reuses the static recipient forever; pair with a one-time external USDC pre-funding of the recipient's ATA and every settle pays only the per-tx fee. Testnet simulator (`simulate_crypto_deposit`, `simulate_deposit_if_test_mode`), `simulate_deposit_for_outcome(outcome=, deposit_address=, get_payment_intent_id=, stripe_secret_key=, stripe_version=)` (dispatches the simulator based on a Checkout / compute_first_checkout settle outcome; replaces the per-merchant rail-switch + thin `simulate_deposit_if_testnet(addr, network)` wrapper), `network_for_outcome` (outcome → simulator network arg, handles both Checkout-shaped `rail_key` and compute-first-shaped `mpp_method`, accepts bare scheme names AND `/charge` forms), `create_pi_cache`, `create_mppx_stripe` | -| `agentscore_commerce.api` | Re-exports `AgentScore` from `agentscore` SDK | +| `agentscore_commerce.stripe_multichain` | Multichain PaymentIntent helper (`create_multichain_payment_intent` returns `MultichainPaymentIntentResult`; read `result.deposit_addresses[network]` directly), `create_pay_to_address_from_stripe_pi(authorization_header=, amount_cents=, stripe=, pi_cache=, networks=, static_recipients=, metadata=, order_id=, preferred_network=)`: one-call per-order payTo resolver matching `Checkout.mint_recipients`: on the settle leg, reuses the buyer's signed-against payTo from the MPP credential (after `pi_cache.has_address` check OR a `static_recipients` match: the static address is always-accepted because the merchant owns it); on the discovery leg, mints a fresh PI for the rails NOT covered by `static_recipients`, caches the merged map, registers static addresses with `pi_cache.cache_address`. `mint_multichain_recipients(...same kwargs) -> MintMultichainRecipientsResult(recipients, payment_intent_id, reused_from_credential)`: structured variant returning the full per-rail map; prefer this when the merchant's `mint_recipients` hook needs all rail addresses (typical multi-rail merchant), and to avoid the "returned-string-is-ambiguous" trap on the settle leg when `static_recipients` is configured. Use `static_recipients={"solana": ""}` for low-margin endpoints where rotating per-PI Solana addresses can't absorb MPP spec §13.6's ~$0.50 ATA rent per call: the SDK skips Stripe minting on that network and reuses the static recipient forever; pair with a one-time external USDC pre-funding of the recipient's ATA and every settle pays only the per-tx fee. Testnet simulator (`simulate_crypto_deposit`, `simulate_deposit_if_test_mode`), `simulate_deposit_for_outcome(outcome=, deposit_address=, get_payment_intent_id=, stripe_secret_key=, stripe_version=)` (dispatches the simulator based on a Checkout / compute_first_checkout settle outcome; replaces the per-merchant rail-switch + thin `simulate_deposit_if_testnet(addr, network)` wrapper), `network_for_outcome` (outcome → simulator network arg, handles both Checkout-shaped `rail_key` and compute-first-shaped `mpp_method`, accepts bare scheme names AND `/charge` forms), `create_pi_cache`, `create_mppx_stripe` | +| `agentscore_commerce.api` | Re-exports `AgentScore`, `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` and `is_agentscore_test_address` from the `agentscore` SDK | | `agentscore_commerce.middleware.{fastapi,flask,django,aiohttp,sanic,asgi}` | Framework-specific rate-limit middleware. FastAPI exposes `rate_limit_fastapi(...)` (FastAPI dependency) plus the ASGI `RateLimitMiddleware` re-export; Flask exposes `rate_limit_flask(app, ...)` (installs a `before_request` hook); Django exposes a class-based async `RateLimitMiddleware` configured via `settings.AGENTSCORE_RATE_LIMIT`; aiohttp exposes `rate_limit_aiohttp(...)` middleware factory; Sanic exposes `rate_limit_sanic(app, ...)` installer; `asgi.RateLimitMiddleware` is a generic ASGI middleware that works with any starlette-compatible app. Shared options: `window_seconds` (default 60), `max_requests` (default 60), `key_resolver` (default first hop of `x-forwarded-for`), `redis_url` (lazy-imports `redis.asyncio` when set, in-memory `dict` fallback otherwise), `key_prefix`. `redis` is an optional peer dep (install via the `redis` extra). | ## Architecture @@ -48,11 +48,11 @@ Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime; ven | `compute_first_merchant.py` | Pay-per-result variable-cost merchant via the compute-first + exact-x402 helper (`compute_first_checkout`). Exact-mode rails only (x402-exact Base, tempo/charge, solana/charge, Stripe SPT); deliberately scoped out of x402-upto (Permit2) and Settlement-Overrides. Probe runs the work + caches by body content-hash; settle replays the cached result at the computed exact price. Pairs with `rate_limit_fastapi` since the probe leg runs work pre-payment. | | `compliance_merchant.py` | Regulated-goods merchant: full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `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`. | -| `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 (regulated-commerce, AP2-aware) that opt into auditable cryptographic provenance — UCP §6 itself does NOT mandate signing; production UCP merchants commonly ship unsigned. 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`. Demonstrates the payment-handler builders (`mpp_payment_handler`, `x402_payment_handler`, `stripe_spt_payment_handler` — see "Payment-handler builders" below). | +| `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 (regulated-commerce, AP2-aware) that opt into auditable cryptographic provenance: UCP §6 itself does NOT mandate signing; production UCP merchants commonly ship unsigned. 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`. Demonstrates the payment-handler builders (`mpp_payment_handler`, `x402_payment_handler`, `stripe_spt_payment_handler`: see "Payment-handler builders" below). | ## Payment-handler builders -The SDK ships protocol-rooted builders for the AgentScore-published payment handlers — vendors compose UCP `payment_handlers` blocks by spreading these helpers instead of hand-writing the verbose binding wrapper: +The SDK ships protocol-rooted builders for the AgentScore-published payment handlers: vendors compose UCP `payment_handlers` blocks by spreading these helpers instead of hand-writing the verbose binding wrapper: ```python from agentscore_commerce import StripeRailSpec, TempoRailSpec, X402BaseRailSpec @@ -73,7 +73,7 @@ build_ucp_profile( ) ``` -Each helper returns `{ : [binding] }` so spreading composes the parent map. The handler `version`, spec URL, and schema URL are owned by the helpers (`agentscore_commerce/identity/ucp.py`) — bumping a handler spec version is a one-line change there. `mpp` takes `TempoRailSpec` / `SolanaMppRailSpec` instances and `x402` takes `X402BaseRailSpec`; each carries a `recipient` — a static address string, or `""` / a factory callable to signal per-order minting (e.g. Stripe-derived deposit addresses), in which case the authoritative recipient ships in the 402 body rather than the static UCP profile. +Each helper returns `{ : [binding] }` so spreading composes the parent map. The handler `version`, spec URL, and schema URL are owned by the helpers (`agentscore_commerce/identity/ucp.py`): bumping a handler spec version is a one-line change there. `mpp` takes `TempoRailSpec` / `SolanaMppRailSpec` instances and `x402` takes `X402BaseRailSpec`; each carries a `recipient`: a static address string, or `""` / a factory callable to signal per-order minting (e.g. Stripe-derived deposit addresses), in which case the authoritative recipient ships in the 402 body rather than the static UCP profile. ## Identity model @@ -81,11 +81,11 @@ Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator- `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 when `wallet_not_trusted` carries fixable reasons (`kyc_required` / `kyc_pending` / `kyc_failed`) — both paths rewrite the denial to `identity_verification_required` before reaching `on_denied`. When the merchant omits `create_session_on_missing` from `CheckoutGateConfig`, `Checkout` auto-defaults it from `gate.api_key` + `gate.base_url` + `gate.context` + `gate.merchant_name`. Merchants that need `on_before_session` side effects (e.g. pre-minting an order_id) supply their own config to override. +`create_session_on_missing` auto-mints a verification session when no identity is present AND when `wallet_not_trusted` carries fixable reasons (`kyc_required` / `kyc_pending` / `kyc_failed`): both paths rewrite the denial to `identity_verification_required` before reaching `on_denied`. When the merchant omits `create_session_on_missing` from `CheckoutGateConfig`, `Checkout` auto-defaults it from `gate.api_key` + `gate.base_url` + `gate.context` + `gate.merchant_name`. Merchants that need `on_before_session` side effects (e.g. pre-minting an order_id) supply their own config to override. **Getting a verify_url before paying.** On an identity-gated `Checkout`, the gate runs only on a settle leg, so a buyer with no identity reached the session 403 only by sending a payment credential first (an SPT buyer had to mint one). The discovery 402 carries an `identity_bootstrap` block (`build_identity_bootstrap()`) naming `X-Verification-Session: create` whenever the request has no identity header (`has_identity_header`), and a request carrying that header, no identity and no payment credential runs the gate, which answers with the same session-bearing 403. It is opt-in on purpose: scanners replay the Bazaar example body on a schedule, so minting on every identity-less 402 would create a session and (on goods stores) a pending order per probe. Gateless merchants neither advertise nor honor it. -`build_verification_required_body(reason, message=?, agent_instructions=?, extra=?)` — canonical body builder for the `identity_verification_required` denial. Spreads `verify_url` / `session_id` / `poll_secret` / `poll_url` / `agent_instructions` from the gate-minted reason into a 4xx envelope with merchant-specific message + optional extras. Saves the per-merchant mapping boilerplate. +`build_verification_required_body(reason, message=?, agent_instructions=?, extra=?)`: canonical body builder for the `identity_verification_required` denial. Spreads `verify_url` / `session_id` / `poll_secret` / `poll_url` / `agent_instructions` from the gate-minted reason into a 4xx envelope with merchant-specific message + optional extras. Saves the per-merchant mapping boilerplate. `get_signer_verdict(request)` (per-adapter) returns the cached `signer_match` + `signer_sanctions` verdicts the gate composed on its primary `/v1/assess` call (single round trip; merchants build a 403 with `build_signer_mismatch_body(result=verdict.signer_match)` when `kind != "pass"`). @@ -103,11 +103,11 @@ Anything that is not a well-formed `oph_` string reads as absent rather than bei 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 + signer-sanctions: the gate adapter calls `extract_payment_signer(x402_header)` pre-evaluate and passes `signer={address, network}` to the SDK's `assess`. The API returns both `signer_match` (wallet-binding) and `signer_sanctions` (OFAC SDN wallet-address) on the same response; commerce caches the raw body alongside the projected verdicts so `get_signer_verdict` is a pure cache read. **Wallet-OFAC SDN enforcement on the `signer` block is unconditional** whenever a signer is present — no `policy.require_sanctions_clear` opt-in required. An SDN hit (or `sanctions_check_unavailable`) flips `decision -> deny` and the gate returns 403 before the handler runs. +Wallet-signer-match + signer-sanctions: the gate adapter calls `extract_payment_signer(x402_header)` pre-evaluate and passes `signer={address, network}` to the SDK's `assess`. The API returns both `signer_match` (wallet-binding) and `signer_sanctions` (OFAC SDN wallet-address) on the same response; commerce caches the raw body alongside the projected verdicts so `get_signer_verdict` is a pure cache read. **Wallet-OFAC SDN enforcement on the `signer` block is unconditional** whenever a signer is present: no `policy.require_sanctions_clear` opt-in required. An SDN hit (or `sanctions_check_unavailable`) flips `decision -> deny` and the gate returns 403 before the handler runs. ### Always-on wallet OFAC default for gateless merchants -Merchants who do NOT configure a `gate` on `Checkout` (or `compute_first_checkout`) still get wallet OFAC SDN enforcement on every settle leg. The SDK resolves the API key from `AGENTSCORE_API_KEY` (or `gate.api_key` if a partial gate config is present), reads `AGENTSCORE_BASE_URL` for staging/dev overrides, extracts the payment signer, and calls `/v1/assess` with the signer block. SDN hit → 403 with `wallet_not_trusted` + `decision_reasons: ["sanctions_flagged"]`. No key → one-time `[checkout]` / `[.compute_first]` warning + skip (dev/testnet pattern; see `agentscore_commerce/_warnings.py`). Stripe SPT (no extractable wallet signer) skips silently — Stripe runs its own OFAC screen at customer creation. `_has_identity_gate()` detects identity-bearing flags (`require_kyc`, `require_sanctions_clear`, `min_age`, `allowed/blocked_jurisdictions`); when none are set, the 402 body omits `agent_memory` and per-rail commands strip the `-H 'X-Operator-Token: ...'` flag (gateless merchants have no operator to identify). Gate configured WITHOUT `api_key` falls through to the wallet-OFAC-only path so the merchant gets the strict-liability floor instead of silently allowing. +Merchants who do NOT configure a `gate` on `Checkout` (or `compute_first_checkout`) still get wallet OFAC SDN enforcement on every settle leg. The SDK resolves the API key from `AGENTSCORE_API_KEY` (or `gate.api_key` if a partial gate config is present), reads `AGENTSCORE_BASE_URL` for staging/dev overrides, extracts the payment signer, and calls `/v1/assess` with the signer block. SDN hit → 403 with `wallet_not_trusted` + `decision_reasons: ["sanctions_flagged"]`. No key → one-time `[checkout]` / `[.compute_first]` warning + skip (dev/testnet pattern; see `agentscore_commerce/_warnings.py`). Stripe SPT (no extractable wallet signer) skips silently: Stripe runs its own OFAC screen at customer creation. `_has_identity_gate()` detects identity-bearing flags (`require_kyc`, `require_sanctions_clear`, `min_age`, `allowed/blocked_jurisdictions`); when none are set, the 402 body omits `agent_memory` and per-rail commands strip the `-H 'X-Operator-Token: ...'` flag (gateless merchants have no operator to identify). Gate configured WITHOUT `api_key` falls through to the wallet-OFAC-only path so the merchant gets the strict-liability floor instead of silently allowing. ### Fail-open (opt-in) diff --git a/README.md b/README.md index bbfce3d..833a33c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]' | Submodule | What it provides | |---|---| -| `agentscore_commerce` (top-level) | `Checkout` orchestrator + `CheckoutContext` + `CheckoutGateConfig` + `CheckoutValidationError` + `DiscoveryProbeConfig` + `SettleOutcome` + `MppxComposeOutcome` + `PricingResult` (the 2.0 high-level surface: one config object, hooks for pre_validate/compute_pricing/on_settled/mint_recipients/compose_mppx, auto-derived x402+mppx servers, per-framework adapters `handle_fastapi`/`handle_flask`/`handle_django`/`handle_aiohttp`/`handle_sanic`, signed UCP routes via `mount_ucp_routes_{fastapi,flask,django,aiohttp,sanic}`); `create_result_cache` (neutral keyed JSON-value cache with a stable body-hash key builder; cache a probe-leg result, e.g. a paid upstream call made in `pre_validate`, and replay it on the settle leg) plus the compute-first-shaped `create_quote_cache` built on it; `pricing_result` (factory: cents-denominated → typed `PricingResult` with embedded `PricingBlock`); `validation_response_{fastapi,flask,django,aiohttp,sanic}` (per-framework 4xx envelope wrappers); `make_mppx_compose_hook` (canonical pympp compose adapter). | +| `agentscore_commerce` (top-level) | `Checkout` orchestrator + `CheckoutContext` + `CheckoutGateConfig` + `CheckoutValidationError` + `DiscoveryProbeConfig` + `SettleOutcome` + `MppxComposeOutcome` + `PricingResult` (one config object, hooks for pre_validate/compute_pricing/on_settled/mint_recipients/compose_mppx, auto-derived x402+mppx servers, per-framework adapters `handle_fastapi`/`handle_flask`/`handle_django`/`handle_aiohttp`/`handle_sanic`, signed UCP routes via `mount_ucp_routes_{fastapi,flask,django,aiohttp,sanic}`); `create_result_cache` (neutral keyed JSON-value cache with a stable body-hash key builder; cache a probe-leg result, e.g. a paid upstream call made in `pre_validate`, and replay it on the settle leg) plus the compute-first-shaped `create_quote_cache` built on it; `pricing_result` (factory: cents-denominated → typed `PricingResult` with embedded `PricingBlock`); `validation_response_{fastapi,flask,django,aiohttp,sanic}` (per-framework 4xx envelope wrappers); `make_mppx_compose_hook` (canonical pympp compose adapter). | | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions (account name + signer wallet), age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_agentscore_data(...)`, `capture_wallet(...)`, `get_signer_verdict(...)`. The gate extracts the payment signer pre-evaluate and passes it to `/v1/assess`, so the API composes both wallet-binding (`signer_match`) and OFAC SDN wallet-address (`signer_sanctions`) verdicts on one round trip. | | `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`. The per-framework adapter modules also expose `get_gate_quota_info(request)` for surfacing X-RateLimit info from gate state. 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`, `validate_shipping_against_policy` (one-call country+state validator that raises `CheckoutValidationError` with the canonical envelope on miss), for multi-product merchants where each product carries its own compliance config: hard gate vs soft vs none, per-product shipping allowlists. Key + token helpers: `load_ucp_signing_key_from_env` (cached env-driven loader for the UCP signing key, reads `UCP_SIGNING_KEY_JWK_PRIVATE` JSON JWK, detects alg from shape, falls back to ephemeral when unset, sanitizes errors so key bytes never reach logs, concurrent-safe via `threading.Lock`; env-var names and `default_kid` / `default_alg` are overridable as kwargs); `hash_operator_token` (sha256 hex of plaintext `opc_...`, for merchants persisting `operator_token_id` to their own DB without ever storing the plaintext); `extract_owner_scope(headers) -> OwnerScope` (canonical owner-identity extractor for caller-scoped resource queries, reads `X-Wallet-Address` / `X-Operator-Token`, hashes the token so plaintext never leaves the request); `default_read_only_on_denied(reason)` (canonical `on_denied` for read-only resource gates: 401 + `Cache-Control: no-store` while still spreading `denial_reason_to_body`. Returns a `DefaultOnDeniedResult(body, status, headers)`; FastAPI / Flask / aiohttp / Sanic `on_denied` callbacks accept an optional 3-tuple `(body, status, headers)` to carry headers through; wrap with `lambda req, reason: (lambda r: (r.body, r.status, r.headers or {}))(default_read_only_on_denied(reason))`). | | `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (opt-in v1↔v2 amount-field shim that adds both `amount` and `maxAmountRequired` to an entry. The 402 builders do NOT apply it by default, strict x402 v2 settlement matches the agent's echoed requirement by exact comparison, so an extra field the server's rebuilt requirement lacks breaks settle; use only when you know a client is hardcoded to read `maxAmountRequired`), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (accepts positional `x402_payment_header` AND/OR `authorization_header=` kwarg; recovers signer from x402 EIP-3009 `payload.authorization.from` OR MPP `Authorization: Payment ` `did:pkh:eip155::` / `did:pkh:solana::` source DID), `detect_rail_from_headers` (returns `"x402"` / `"mpp"` / `None` from inbound headers), `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), `classify_orchestration_error` (same `ClassifiedX402Error` shape but for uncaught exceptions thrown elsewhere in the orchestration; returns `None` for unknown errors so merchants rethrow instead of swallowing); `zero_amount_carve_out` (skip CDP / pympp upstream verify+settle for $0 settles where the upstream rejects value=0 payloads, including credentials signed against a nonzero quote the merchant re-priced to $0 at settle; parses the credential, lifts signer + network, returns a `ZeroSettleResult` shaped identically to the success path so callers branch on rail, not on result shape), `malformed_payment_credential` (wire-shape gate: payment headers that are not base64 JSON and not token-shaped are rejected before merchant hooks run; `Checkout` applies it by default, opt out with `credential_pre_check=False`); `usd_to_atomic` (Decimal-based USD → atomic int, ROUND_HALF_UP, for Tempo / Solana / Base USDC amount construction). | @@ -93,7 +93,7 @@ async def purchase(request: Request, assess=Depends(get_agentscore_data)): return {"ok": True} ``` -## Checkout orchestrator (the 2.0 high-level surface) +## Checkout orchestrator `Checkout` is the canonical merchant surface: one config object, hooks for the merchant-specific pieces, and the SDK handles 402 emit, identity gating, x402 verify+settle, mppx compose, $0 carve-out, and the per-framework adapter. Most merchants reach for `Checkout` first and drop to lower-level helpers only when they need custom flows. @@ -315,12 +315,12 @@ from agentscore_commerce.identity import ( ucp_a2a_extension, ) -# Google A2A v1.0 Signed Agent Card. Publish at /.well-known/agent-card.json. +# A2A v1.0 Agent Card. Publish at /.well-known/agent-card.json. # 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. +# Skills are top-level AgentSkill objects. The card is returned unsigned: to sign it, +# compute RFC 7515 JWS entries over the card body and pass them as `signatures`. card = build_a2a_agent_card( name="My Service", description="Buy products via agent payments.", @@ -340,7 +340,7 @@ card = build_a2a_agent_card( # Google Universal Commerce Protocol. Publish at /.well-known/ucp. # Output shape: {"ucp": {"version", "services", "capabilities", # "payment_handlers", "name?", "supported_versions?"}, "keys": [...]} -# , services / capabilities / payment_handlers are MAPS keyed by reverse-DNS +# services / capabilities / payment_handlers are MAPS keyed by reverse-DNS # service / capability / handler name (UCP spec §3 + §6). profile = build_ucp_profile( name="My Service", diff --git a/SECURITY.md b/SECURITY.md index fe82b97..7e029bb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,5 +19,5 @@ We will acknowledge receipt within 48 hours and aim to release a fix within 7 da | Version | Supported | |---------|-----------| -| 2.x | ✅ | -| < 2.0 | ❌ | +| 3.x | ✅ | +| < 3.0 | ❌ | diff --git a/agentscore_commerce/__init__.py b/agentscore_commerce/__init__.py index e555ad7..373ad72 100644 --- a/agentscore_commerce/__init__.py +++ b/agentscore_commerce/__init__.py @@ -1,4 +1,4 @@ -"""Agentic commerce SDK — identity middleware + payment helpers + 402 builders + discovery + Stripe multichain. +"""Agentic commerce SDK: identity middleware + payment helpers + 402 builders + discovery + Stripe multichain. Submodules: agentscore_commerce.identity - per-framework gate adapters @@ -163,7 +163,7 @@ try: __version__ = _pkg_version("agentscore-commerce") except PackageNotFoundError: - # Editable install or pre-build state — fall back to a sentinel so consumers + # Editable install or pre-build state: fall back to a sentinel so consumers # don't crash on a missing dist-info dir. Real version always comes from # pyproject.toml at install time. __version__ = "0.0.0+local" diff --git a/agentscore_commerce/_headers.py b/agentscore_commerce/_headers.py index 1c49224..fd34e18 100644 --- a/agentscore_commerce/_headers.py +++ b/agentscore_commerce/_headers.py @@ -1,4 +1,4 @@ -"""Internal header helpers — case-normalization for HTTP headers. +"""Internal header helpers: case-normalization for HTTP headers. Replaces hand-rolled ``{k.lower(): v for k, v in headers.items()}`` loops in ``checkout``, ``signer`` and ``challenge.respond_402``. diff --git a/agentscore_commerce/_mppx_receipt.py b/agentscore_commerce/_mppx_receipt.py index b2cc59c..1ce3881 100644 --- a/agentscore_commerce/_mppx_receipt.py +++ b/agentscore_commerce/_mppx_receipt.py @@ -19,11 +19,11 @@ def extract_mppx_receipt_header_from_raw(raw: Any) -> str | None: Covers three shapes hand-rolled hooks commonly return: - * ``raw.receipt_header`` — pympp's current direct-attribute shape. - * ``raw.to_payment_receipt()`` — pympp's older Receipt return-method shape + * ``raw.receipt_header``: pympp's current direct-attribute shape. + * ``raw.to_payment_receipt()``: pympp's older Receipt return-method shape (also reached when ``raw`` is a ``(credential, receipt)`` tuple OR a dict/object carrying ``.receipt``). - * ``raw.with_receipt(response) -> Response`` — a shape that wraps an + * ``raw.with_receipt(response) -> Response``: a shape that wraps an outgoing Response and attaches the header. Returns ``None`` when none match or the underlying call raises. diff --git a/agentscore_commerce/_redis.py b/agentscore_commerce/_redis.py index b3b4a98..7c0e2d3 100644 --- a/agentscore_commerce/_redis.py +++ b/agentscore_commerce/_redis.py @@ -4,7 +4,7 @@ ``stripe_multichain.pi_cache``, and ``middleware._core`` so they don't drift on logging posture, TLS handling, or connect-error semantics. -``redis`` is an optional peer dep — callers pass ``redis_url`` (or rely on +``redis`` is an optional peer dep: callers pass ``redis_url`` (or rely on ``REDIS_URL`` env); when unset or the lazy import fails, this returns ``None`` and the caller falls back to its in-process dict. @@ -80,7 +80,7 @@ def memoized_redis(*, url: str | None, label: str) -> Callable[[], Awaitable[Any First call constructs the client; later calls return the same client (or the same ``None``). - Pairs with the per-caller ``redis_url`` opt — when ``url`` is ``None`` AND + Pairs with the per-caller ``redis_url`` opt: when ``url`` is ``None`` AND ``REDIS_URL`` is unset, the getter resolves to ``None`` once and remains so for the lifetime of the caller. """ diff --git a/agentscore_commerce/_warnings.py b/agentscore_commerce/_warnings.py index bccfcef..96d1d23 100644 --- a/agentscore_commerce/_warnings.py +++ b/agentscore_commerce/_warnings.py @@ -28,7 +28,7 @@ def warn_missing_api_key_once(label: str) -> None: return _warned_no_api_key = True logging.getLogger(__name__).warning( - f"[{label}] AGENTSCORE_API_KEY is not set — wallet OFAC SDN sanctions are NOT being enforced. " + f"[{label}] AGENTSCORE_API_KEY is not set: wallet OFAC SDN sanctions are NOT being enforced. " "Set the env var to enable strict-liability protection on settle." ) diff --git a/agentscore_commerce/aip/__init__.py b/agentscore_commerce/aip/__init__.py index 5ee975a..eb34e07 100644 --- a/agentscore_commerce/aip/__init__.py +++ b/agentscore_commerce/aip/__init__.py @@ -1,4 +1,4 @@ -"""AIP (Agentic Identity Protocol) — AIT verification (verifier role) + RFC 9421 signing. +"""AIP (Agentic Identity Protocol): AIT verification (verifier role) + RFC 9421 signing. This package is the AgentScore verifier for Agent Identity Tokens (AITs): a merchant gate hands a parsed request plus a trusted-issuer :class:`JwksCache` to the orchestrator and gets diff --git a/agentscore_commerce/aip/gate.py b/agentscore_commerce/aip/gate.py index ccc1fcd..ea99f39 100644 --- a/agentscore_commerce/aip/gate.py +++ b/agentscore_commerce/aip/gate.py @@ -2,12 +2,12 @@ ``verify_ait_request`` is the one call a framework adapter makes: hand it a parsed request plus a :class:`~agentscore_commerce.aip.jwks.JwksCache`, and it returns the verified AIT claims -or a typed failure. The helpers here also map that failure onto the AIP wire contract — HTTP -status + error code + an RFC 9457 problem-details body — so every adapter renders denials +or a typed failure. The helpers here also map that failure onto the AIP wire contract: HTTP +status + error code + an RFC 9457 problem-details body: so every adapter renders denials identically. This layer does identity *verification* only (is this a real, key-bound AIT from a trusted -IdP?). Policy enrichment — sanctions, jurisdiction, cross-merchant graph — happens when the +IdP?). Policy enrichment (sanctions, jurisdiction, cross-merchant graph) happens when the merchant additionally feeds the verified claims to ``/v1/assess``; that's the gate's choice, not something this module forces. """ @@ -41,14 +41,14 @@ class AipGateOptions: jwks: JwksCache now: float | None = None max_skew_seconds: float | None = None - # Minimum ``trust_level`` (autonomous < human_present < human_confirmed) the AIT must assert — + # Minimum ``trust_level`` (autonomous < human_present < human_confirmed) the AIT must assert: # the spec's human-presence gate. Insufficient -> 403 weak_auth with ``required_trust_level``. # Enforced by :func:`evaluate_aip_request` / :func:`evaluate_aip_parts`. Unset = any trust level. require_trust_level: TrustLevel | None = None # Acceptable ``auth.amr`` methods (RFC 8176); the AIT must carry >=1. Insufficient -> 403 # weak_auth with ``required_amr``. Unset = not enforced. require_amr: list[str] | None = None - # Identity claims the endpoint needs — surfaced as ``required_claims`` on insufficient_claims + # Identity claims the endpoint needs: surfaced as ``required_claims`` on insufficient_claims # denials so the agent can self-correct. Advisory only (enforce by feeding the verified claims # to your own policy / ``/v1/assess``; this gate does identity + trust_level/amr). required_claims: list[str] | None = None @@ -104,7 +104,7 @@ def aip_error_code(failure: VerifyAitFailure) -> str: if failure == "invalid_claims": return "insufficient_claims" if failure == "key_unavailable": - # The IdP's JWKS could not be fetched/resolved — our infra couldn't reach a trusted + # The IdP's JWKS could not be fetched/resolved: our infra couldn't reach a trusted # issuer, not a client-side auth failure. Distinct code so agents back off + retry # rather than uselessly re-signing. return "idp_unavailable" @@ -173,8 +173,8 @@ def build_aip_error_body( """Build an RFC 9457 problem-details body for an AIP verify failure. Adapters serialize this as ``application/problem+json`` with :func:`aip_error_status`. - Optionally carries the merchant's requirements — ``trusted_issuers`` on untrusted_issuer; - ``required_claims`` / ``required_trust_level`` / ``required_amr`` on insufficient_claims — + Optionally carries the merchant's requirements: ``trusted_issuers`` on untrusted_issuer; + ``required_claims`` / ``required_trust_level`` / ``required_amr`` on insufficient_claims: so the agent learns what would satisfy the gate. """ code = aip_error_code(failure) @@ -206,7 +206,7 @@ def aip_policy_deny_code(code: str) -> tuple[str, int]: codes; the spec's fixed error set expresses each as: - ``token_expired`` -> ``expired_token`` (401) - ``invalid_credential`` -> ``invalid_signature`` (401) - - ``api_error`` -> ``idp_unavailable`` (503, transient — the claims couldn't be evaluated) + - ``api_error`` -> ``idp_unavailable`` (503, transient: the claims couldn't be evaluated) - everything else (compliance: ``wallet_not_trusted`` + ``sanctions_flagged`` / ``age_insufficient`` / ``jurisdiction_restricted`` / ``kyc_*``) -> ``insufficient_claims`` (403): the AIT did not attest (or attested a failing value for) the required compliance claim. @@ -229,8 +229,8 @@ def build_aip_policy_deny_body( """Wrap an AgentScore AIT-path denial body in the RFC 9457 + AIP-spec superset. Reuses :func:`build_aip_error_body`'s SHAPE convention (``type``/``title``/``status``/``detail`` - + escalation extensions) but for the *policy-deny* case — a verified AIT that ``/v1/assess`` - then denied — which carries an AgentScore compliance/credential code, not a verify-failure + + escalation extensions) but for the *policy-deny* case: a verified AIT that ``/v1/assess`` + then denied: which carries an AgentScore compliance/credential code, not a verify-failure reason. The result is a SUPERSET: the canonical ``{ error, agent_instructions, ... }`` body is spread @@ -263,7 +263,7 @@ def build_aip_policy_deny_body( } # Escalation extensions, scoped exactly as the spec mandates: ``required_claims`` / # ``required_trust_level`` / ``required_amr`` on insufficient_claims. ``trusted_issuers`` - # belongs to untrusted_issuer — a VERIFY failure that never reaches the policy-deny path — so it + # belongs to untrusted_issuer (a VERIFY failure that never reaches the policy-deny path) so it # is not emitted here (the edge-deny ``build_aip_error_body`` owns that one). if requirements is not None and spec_code == "insufficient_claims": if requirements.required_claims: @@ -274,7 +274,7 @@ def build_aip_policy_deny_body( superset["required_amr"] = requirements.required_amr # Spread the RFC 9457 envelope LAST so `type` / `title` / `status` / `detail` (and the # escalation extensions) always win: `body` carries merchant `extra` passthrough fields, and a - # buggy or malicious hook must not clobber the problem+json envelope — or the HTTP status the + # buggy or malicious hook must not clobber the problem+json envelope: or the HTTP status the # caller derives from it. The rich AgentScore fields (`error`, `agent_instructions`, `reasons`, # ...) don't collide with the envelope, so they still ride along verbatim. return {**body, **superset} diff --git a/agentscore_commerce/aip/http_signature.py b/agentscore_commerce/aip/http_signature.py index 30122e4..3af29ec 100644 --- a/agentscore_commerce/aip/http_signature.py +++ b/agentscore_commerce/aip/http_signature.py @@ -1,4 +1,4 @@ -"""RFC 9421 HTTP Message Signatures — the AIP-constrained subset. +"""RFC 9421 HTTP Message Signatures: the AIP-constrained subset. AIP (Agentic Identity Protocol) binds an Agent Identity Token (AIT) to the agent that presents it: the agent signs each HTTP request with the private key whose @@ -52,13 +52,13 @@ _DEFAULT_MAX_SKEW_SECONDS = 60 # Hard ceiling on the PoP signature's own declared lifetime (``expires - created``), in seconds. -# Requiring ``created``+``expires`` bounds replay to the declared window — but with no ceiling a +# Requiring ``created``+``expires`` bounds replay to the declared window: but with no ceiling a # malicious trusted-issuer agent could set ``expires = created + (AIT lifetime)`` and replay for the # full window. Cap it tightly so every accepted PoP is short-lived. First-party ``pay`` signs a 60s # window, so it passes; this only bites a signer that declares an over-long PoP. Matches the # authoritative API verifier's ``MAX_POP_WINDOW_SECONDS`` (the AgentScore API verifier) so the # edge (standalone ``aip_gate``) and the API can't drift. (Distinct from the AIT JWT's ``exp - iat`` -# ceiling in verify.py — this is the HTTP-signature layer.) +# ceiling in verify.py: this is the HTTP-signature layer.) MAX_POP_WINDOW_SECONDS = 120 # Verification failure reasons. Mirrors the reference ``VerifyFailureReason`` union exactly. @@ -201,7 +201,7 @@ def build_signature_base( covered component has no available value. When ``raw_params`` is given (the verify path), it is used VERBATIM as the - ``@signature-params`` value — the signer signed over its own serialization, so re-serializing + ``@signature-params`` value: the signer signed over its own serialization, so re-serializing parsed params in a fixed order would break a spec-legal signer that emitted them in a different order. The sign path omits it and serializes canonically. """ @@ -325,7 +325,7 @@ class ParsedSignatureInput: """A selected ``Signature-Input`` member. Carries the dictionary ``label``, parsed ``params``, and the ``raw`` member value - (everything after ``label=``, exactly as received, OWS-trimmed) — the verify path + (everything after ``label=``, exactly as received, OWS-trimmed): the verify path rebuilds the base over the RAW serialization, not a re-serialization. """ @@ -381,7 +381,7 @@ def _calculate_jwk_thumbprint(jwk: Jwk) -> str: """RFC 7638 SHA-256 JWK thumbprint, byte-identical to jose's ``calculateJwkThumbprint``. joserfc's ``OKPKey.thumbprint()`` canonicalizes ``{crv, kty, x}`` with sorted keys and - no whitespace, SHA-256s it, and base64url-no-pad encodes — exactly the RFC 7638 + no whitespace, SHA-256s it, and base64url-no-pad encodes: exactly the RFC 7638 construction jose uses. Verified byte-equal cross-language. Raises on a malformed JWK; the caller catches. """ @@ -410,7 +410,7 @@ def verify_message_signature( 3. REQUIRE both ``created`` and ``expires``, reject an over-long declared window (``expires - created`` > MAX_POP_WINDOW_SECONDS -> ``pop_window_too_long``), then enforce them against ``now`` with skew tolerance. Both are mandatory: an optional time bound is no - time bound — without ``expires`` a captured ``(token, Signature-Input, Signature)`` triple is + time bound: without ``expires`` a captured ``(token, Signature-Input, Signature)`` triple is replayable for the whole AIT lifetime. A signature omitting either is rejected (``created_missing`` / ``expires_missing``). This matches the authoritative API verifier (the AgentScore API verifier) so a merchant running ``aip_gate`` STANDALONE (the @@ -419,7 +419,7 @@ def verify_message_signature( 5. reconstruct the signature base and verify Ed25519 over it Note: - This is a STATELESS verifier — it bounds the replay WINDOW but does not dedupe within it. + This is a STATELESS verifier: it bounds the replay WINDOW but does not dedupe within it. A captured triple can still be replayed until ``expires`` (<= MAX_POP_WINDOW_SECONDS + skew from ``created``). A stateful seen-signature cache (as in the authoritative API) is out of scope for the SDK edge; the tight window bound is the meaningful mitigation here. @@ -446,7 +446,7 @@ def verify_message_signature( # The ``alg`` param is optional in RFC 9421 (the verifier derives the algorithm from the # key); when a signer does include it, the registered HTTP-sig label is ``ed25519``. # Accept that plus the JWS spelling ``EdDSA``, case-insensitively, so a spec-loose - # external signer isn't wrongly rejected — the actual key type is still pinned to + # external signer isn't wrongly rejected: the actual key type is still pinned to # OKP/Ed25519 below, so this only affects the label. if params.alg is not None and params.alg.lower() not in ("ed25519", "eddsa"): return VerifyMessageSignatureResult(ok=False, reason="unsupported_alg") @@ -457,7 +457,7 @@ def verify_message_signature( return VerifyMessageSignatureResult(ok=False, reason="missing_covered_component") # REQUIRE both ``created`` and ``expires``. Treating them as optional leaves an unbounded replay - # window — a captured signature with no ``expires`` is valid for the AIT's full lifetime. Reject + # window: a captured signature with no ``expires`` is valid for the AIT's full lifetime. Reject # when either is absent so every accepted PoP carries an explicit, enforceable time bound. (Our # pay signer always emits both with a 60s window; this only rejects spec-loose external signers.) if params.created is None: @@ -466,7 +466,7 @@ def verify_message_signature( return VerifyMessageSignatureResult(ok=False, reason="expires_missing") # Bound the PoP's own declared lifetime. created+expires alone only bound replay to whatever - # window the SIGNER chose — a malicious trusted-issuer agent could declare a window as wide as + # window the SIGNER chose: a malicious trusted-issuer agent could declare a window as wide as # the AIT lifetime and replay for all of it. Reject an over-long window so every accepted PoP is # short-lived. (pay signs 60s; this only bites a signer declaring > MAX_POP_WINDOW_SECONDS.) # A NEGATIVE window (expires before created) is nonsense and would otherwise slip under the @@ -490,7 +490,7 @@ def verify_message_signature( # binds as Ed25519 (OKP). Validate the key shape BEFORE thumbprinting / importing: a # malformed JWK (missing or non-string ``x``) makes the thumbprint throw, and a non-OKP # key (e.g. a P-256 EC cnf) makes the Ed25519 import throw. Neither call site below - # catches an unguarded throw into a crash — reject with a typed failure instead. + # catches an unguarded throw into a crash: reject with a typed failure instead. # (Note: the JWT alg allowlist permits ES256 for the IDP *issuer* signing key, a # different key.) cnf: dict[str, Any] = cnf_jwk if isinstance(cnf_jwk, dict) else {} @@ -521,7 +521,7 @@ def verify_message_signature( path=path, agent_identity=agent_identity, extra=extra_components, - # Verify over the RAW received member serialization — param order is the signer's. + # Verify over the RAW received member serialization: param order is the signer's. raw_params=selected.raw, ) except _MissingComponentError: @@ -540,7 +540,7 @@ def verify_message_signature( try: raw = OKPKey.import_key(cnf_jwk).raw_value # ``cnf.jwk`` is a public key, but derive the public half defensively so a private - # JWK (which carries ``d``) also verifies — mirrors jose ``importJWK`` + ``subtle``, + # JWK (which carries ``d``) also verifies: mirrors jose ``importJWK`` + ``subtle``, # which verify against the public part regardless. public_key = raw.public_key() if isinstance(raw, Ed25519PrivateKey) else raw # crv=Ed25519 was enforced above, so raw_value is an Ed25519 key; the isinstance @@ -619,7 +619,7 @@ def sign_message( extra=extra_components, ) - # Raw Ed25519 over the signature base bytes — NOT a JWS. Matches node's + # Raw Ed25519 over the signature base bytes: NOT a JWS. Matches node's # ``subtle.sign('Ed25519', key, bytes)`` which emits the 64-byte raw signature, then # standard-base64 (not base64url) encodes it into the ``:...:`` byte-sequence value. raw_private = OKPKey.import_key(private_jwk).raw_value diff --git a/agentscore_commerce/aip/jwks.py b/agentscore_commerce/aip/jwks.py index 5205f3f..4dedd28 100644 --- a/agentscore_commerce/aip/jwks.py +++ b/agentscore_commerce/aip/jwks.py @@ -3,18 +3,18 @@ Verifiers resolve an IdP's public keys from ``https://{iss}/.well-known/agent-identity/jwks.json`` (the spec's well-known path). This module owns: -* **Trusted-issuer enforcement** — only ``iss`` values on the allowlist are fetched, compared +* **Trusted-issuer enforcement**: only ``iss`` values on the allowlist are fetched, compared after URL canonicalization (lowercase scheme+host, no default port, no trailing slash) so ``https://issuer.example`` and ``https://issuer.example/`` match. -* **HTTPS-only** — JWKS over plain HTTP is MITM-vulnerable; we refuse it. -* **Caching with a HARD cap** — we honor ``Cache-Control: max-age`` as advisory but never +* **HTTPS-only**: JWKS over plain HTTP is MITM-vulnerable; we refuse it. +* **Caching with a HARD cap**: we honor ``Cache-Control: max-age`` as advisory but never cache longer than :data:`HARD_MAX_CACHE_SECONDS`, regardless of what the IdP sends. A compromised IdP can't pin stale keys with ``max-age=31536000``. -* **kid-miss refresh (cooldown-bounded)** — a lookup for a ``kid`` not in the cached set triggers +* **kid-miss refresh (cooldown-bounded)**: a lookup for a ``kid`` not in the cached set triggers one refetch (rotation may have published a new key inside the cache window), but at most once per - issuer per :data:`JWKS_REFETCH_COOLDOWN_SECONDS` — a per-issuer cooldown so an unknown-``kid`` + issuer per :data:`JWKS_REFETCH_COOLDOWN_SECONDS`: a per-issuer cooldown so an unknown-``kid`` flood can't amplify into one upstream JWKS GET per request. Concurrent refreshes single-flight. -* **use:"sig" filtering** — only signing keys are returned. +* **use:"sig" filtering**: only signing keys are returned. Pure-ish: the only I/O is the HTTP fetch, injectable for tests via ``fetch_impl``. @@ -49,18 +49,18 @@ #: Cooldown between forced refetches of an issuer triggered by an unknown ``kid``. A kid-miss #: normally forces one refetch (rotation may have published a new key inside the cache window), but -#: an unauthenticated attacker can flood unknown-``kid`` tokens for a trusted issuer — ``kid``/``iss`` +#: an unauthenticated attacker can flood unknown-``kid`` tokens for a trusted issuer: ``kid``/``iss`` #: are decoded BEFORE signature verify, so each would otherwise fan out one upstream JWKS fetch. We #: stamp a per-ISSUER cooldown on every fetch and suppress ALL kid-miss refetches for that issuer #: while it's warm, bounding the amplification to ~one fetch per issuer per cooldown REGARDLESS of #: how many DISTINCT unknown kids are streamed. (A per-(issuer,kid) memo would only bound a repeat -#: of the SAME kid — a distinct-kid flood would still fan out one fetch each.) Mirrors the API +#: of the SAME kid: a distinct-kid flood would still fan out one fetch each.) Mirrors the API #: verifier's jose ``cooldownDuration: 30s`` (``the AgentScore API verifier`` #: ``REFETCH_COOLDOWN_MS``. (30s) JWKS_REFETCH_COOLDOWN_SECONDS = 30 #: AgentScore's own AIT issuer. ALWAYS trusted by every :class:`JwksCache` (and therefore every -#: gate/adapter built on it) without the merchant listing it — this SDK is the AgentScore +#: gate/adapter built on it) without the merchant listing it: this SDK is the AgentScore #: verifier, so a merchant can't accidentally fail to trust AgentScore-issued AITs. #: ``trusted_issuers`` only needs to name ADDITIONAL external issuers. AGENTSCORE_CANONICAL_ISSUER = "https://www.agentscore.com" @@ -75,7 +75,7 @@ class FetchResponse(Protocol): - """Minimal response shape the cache needs — mirrors node's structural ``FetchLike`` return. + """Minimal response shape the cache needs: mirrors node's structural ``FetchLike`` return. The default fetcher adapts :class:`httpx.Response` to this; injected fetchers (tests) implement it directly. ``headers.get`` is case-insensitive for ``cache-control`` lookups, @@ -126,7 +126,7 @@ def canonicalize_issuer(iss: str) -> str | None: Lowercase scheme + host, drop the default port for the scheme, strip a trailing slash on an empty path. Returns ``None`` if the input is not a parseable absolute URL (no scheme/host) or - has a malformed authority (non-numeric/out-of-range port, unbalanced IPv6 bracket) — matching + has a malformed authority (non-numeric/out-of-range port, unbalanced IPv6 bracket): matching node's try/catch around ``new URL()``. """ # ``iss`` comes from the UNVERIFIED JWT payload: urlsplit / .hostname / .port raise ValueError @@ -261,7 +261,7 @@ def __init__( self._now = now if now is not None else (lambda: time.time() * 1000) self._user_agent = user_agent self._cache: dict[str, _CachedKeys] = {} - # Per-issuer refetch cooldown (ms timestamp), stamped on EVERY fetch — success AND failure. + # Per-issuer refetch cooldown (ms timestamp), stamped on EVERY fetch: success AND failure. # This is the per-ISSUER refetch-amplification / DoS guard: it caps JWKS GETs at ~1 per # issuer per cooldown regardless of how many DISTINCT unknown ``kid``s an attacker floods, # and (the failure stamp) keeps a failing issuer from being refetched on every sequential @@ -271,13 +271,13 @@ def __init__( # lookups that land within the cooldown window (a failed fetch leaves no ``_cache`` entry, # so without this the no-cache path would refetch every request). Cleared on success. self._failure: dict[str, JwksLookupResult] = {} - # Per-issuer in-flight refresh future — coalesces CONCURRENT refreshes to ONE upstream + # Per-issuer in-flight refresh future: coalesces CONCURRENT refreshes to ONE upstream # fetch. Without it, a concurrent burst of distinct-kid lookups on a cold/expired cache each # call _refresh() before any has populated the cache → N parallel JWKS GETs (refetch # amplification). The cooldown only suppresses SEQUENTIAL refetches; single-flight suppresses # CONCURRENT ones. Keyed alongside the loop that created the future: sync adapters (Flask / # Django) run ``asyncio.run()`` per request on worker threads, and awaiting a future created - # on ANOTHER thread's loop raises RuntimeError — so coalescing is same-loop only. The entry + # on ANOTHER thread's loop raises RuntimeError: so coalescing is same-loop only. The entry # is cleared once the fetch settles. Mirrors the reference `inflight` map. self._inflight: dict[str, tuple[asyncio.AbstractEventLoop, asyncio.Future[JwksLookupResult]]] = {} @@ -304,7 +304,7 @@ async def get_key(self, iss: str, kid: str | None) -> JwksLookupResult: if hit is not None: return JwksLookupResult(ok=True, key=hit) # kid miss within the cache window. Normally we'd force one refetch (rotation may have - # published a new key) — but only once the per-ISSUER refetch cooldown has elapsed. + # published a new key): but only once the per-ISSUER refetch cooldown has elapsed. # WITHIN the cooldown we return key_not_found WITHOUT refetching. This caps JWKS GETs # at ~1 per issuer per cooldown regardless of how many DISTINCT unknown-kid tokens an # attacker streams (the DoS guard). Once the cooldown passes we fall through to a @@ -331,7 +331,7 @@ async def get_key(self, iss: str, kid: str | None) -> JwksLookupResult: hit = self._select(keys, kid) if hit is not None: return JwksLookupResult(ok=True, key=hit) - # Still missing after a fresh fetch — the cooldown stamped by that fetch suppresses the + # Still missing after a fresh fetch: the cooldown stamped by that fetch suppresses the # next refetch for this issuer anyway. return JwksLookupResult(ok=False, reason="key_not_found") @@ -348,8 +348,8 @@ async def _refresh(self, canon_issuer: str) -> JwksLookupResult: The first caller for an issuer with no in-flight refresh kicks off the fetch and registers the future; concurrent callers ON THE SAME LOOP await that future instead of issuing their own GET. A caller on a DIFFERENT running loop (threaded WSGI: Flask/Django run - ``asyncio.run()`` per request) must NOT await the foreign future — awaiting a future bound - to another thread's loop raises RuntimeError — so it performs its own fetch instead + ``asyncio.run()`` per request) must NOT await the foreign future: awaiting a future bound + to another thread's loop raises RuntimeError: so it performs its own fetch instead (correctness over cross-loop dedupe). Each entry is cleared by its creator once the fetch settles so the next cold/expired lookup can refresh again. Mirrors the reference ``refresh`` + ``inflight`` single-flight. diff --git a/agentscore_commerce/aip/request.py b/agentscore_commerce/aip/request.py index ad3b3d6..d7bb8f1 100644 --- a/agentscore_commerce/aip/request.py +++ b/agentscore_commerce/aip/request.py @@ -8,10 +8,10 @@ Two entry points, mirroring the the reference ``aip/request`` module: -* :func:`build_verify_context_from_request` — for frameworks that expose a request object with +* :func:`build_verify_context_from_request`: for frameworks that expose a request object with ``method`` / ``url`` / ``headers`` (Starlette, FastAPI, aiohttp, Sanic, …). The node analog takes a WHATWG ``Request``. -* :func:`build_verify_context_from_parts` — for frameworks that hand you raw pieces (a header +* :func:`build_verify_context_from_parts`: for frameworks that hand you raw pieces (a header mapping + method + URL/target), e.g. Flask/Django/WSGI. The node analog takes a Node-style header map. @@ -107,7 +107,7 @@ def _read_agent_identity_headers(headers: HeadersLike) -> list[str]: Mirrors the reference ``readAgentIdentityHeaders``. Node reads the WHATWG ``Headers.get`` value (which comma-folds repeats) and splits on ``,``. Starlette's ``Headers.get`` returns only the first match, so we prefer ``getlist`` to recover every repeated header, then split each - on ``,`` as well — this yields the identical AIT set whether the proxy folded the headers + on ``,`` as well: this yields the identical AIT set whether the proxy folded the headers into one line or kept them separate. """ getlist = getattr(headers, "getlist", None) @@ -182,7 +182,7 @@ def _read_mapping_header(headers: Mapping[str, str | list[str] | None], name: st def has_agent_identity_header_parts(headers: Mapping[str, str | list[str] | None]) -> bool: """True when a plain header mapping carries an ``Agent-Identity`` header. - Mirrors the reference ``hasAgentIdentityHeaderNode`` — used by adapters (Flask/Django/WSGI) that + Mirrors the reference ``hasAgentIdentityHeaderNode``: used by adapters (Flask/Django/WSGI) that pass a raw header mapping rather than a request object. """ raw = _read_mapping_header(headers, AGENT_IDENTITY_HEADER) @@ -195,11 +195,11 @@ def build_verify_context_from_parts(parts: VerifyContextParts) -> VerifyRequestC For frameworks that don't expose a request object (Flask/Django/WSGI). The node analog takes Express/Fastify-style parts. ``parts`` carries: - * ``method`` — the HTTP method. - * ``url`` — full request URL, or just the origin-form target (``"/checkout?..."``); used to + * ``method``: the HTTP method. + * ``url``: full request URL, or just the origin-form target (``"/checkout?..."``); used to derive ``@path``. - * ``headers`` — a Node-style header mapping (values ``str`` / ``list[str]`` / ``None``). - * ``authority`` (optional) — authority override; falls back to the ``host`` header. + * ``headers``: a Node-style header mapping (values ``str`` / ``list[str]`` / ``None``). + * ``authority`` (optional): authority override; falls back to the ``host`` header. """ from agentscore_commerce.aip.verify import VerifyRequestContext @@ -215,7 +215,7 @@ def build_verify_context_from_parts(parts: VerifyContextParts) -> VerifyRequestC # ``url`` may be an absolute URL or an origin-form target ("/checkout?...", possibly "//x"). # Build the URL by APPENDING the target to the origin (not resolving it as a reference) so a - # leading "//" is treated as PATH — resolving "//x" against a base mis-reads it as a + # leading "//" is treated as PATH: resolving "//x" against a base mis-reads it as a # protocol-relative authority and drops it, diverging from the signer's ``URL.pathname`` and # failing PoP. Always assigned in both branches below. path: str diff --git a/agentscore_commerce/aip/types.py b/agentscore_commerce/aip/types.py index f3ae21d..aab3802 100644 --- a/agentscore_commerce/aip/types.py +++ b/agentscore_commerce/aip/types.py @@ -12,7 +12,7 @@ Extensibility contract (per spec): the ``identity`` object is open. If a claim is present, the IdP attests to it; verifiers ignore claims they don't recognize. Absence is the -"unknown" signal — IdPs do not ship ``None`` for "not checked". +"unknown" signal: IdPs do not ship ``None`` for "not checked". """ from __future__ import annotations @@ -28,7 +28,7 @@ # Degree of human involvement in issuing this specific AIT. TrustLevel = Literal["autonomous", "human_present", "human_confirmed"] -# Authentication Method Reference values (RFC 8176 / IANA AMR registry). Open set — these +# Authentication Method Reference values (RFC 8176 / IANA AMR registry). Open set: these # are the values relevant to agent identity; others are valid and pass through. AmrValue = Literal["face", "fpt", "hwk", "otp", "pin", "pwd", "sms", "swk", "user", "mfa"] @@ -93,7 +93,7 @@ class LinkedWallet(TypedDict): class IdentityClaim(TypedDict, total=False): """Identity claims (presence == IdP attestation). - Spec-defined fields plus AgentScore compliance extension claims. Open by contract — + Spec-defined fields plus AgentScore compliance extension claims. Open by contract: unknown fields are allowed and ignored. (TypedDict cannot express the open ``[claim: string]: unknown`` index signature node declares; treat this as the documented subset, and rely on ``dict``-level access for any extension key.) @@ -126,7 +126,7 @@ class AitPayload(TypedDict, total=False): Required claims (``aip_version``, ``iss``, ``sub``, ``iat``, ``exp``, ``cnf``, ``agent``) are validated by :func:`validate_ait_payload`; ``total=False`` keeps the type usable for partially-decoded payloads before validation. Like node's interface, - the payload is open — unrecognized claims pass through. + the payload is open: unrecognized claims pass through. """ aip_version: str @@ -209,7 +209,7 @@ def validate_ait_payload(payload: object) -> AitValidationResult: normative conditional in the spec: a ``human_confirmed`` token MUST carry at least one ``auth.amr`` value. - This is shape/contract validation only — it does NOT verify signatures (that's the + This is shape/contract validation only: it does NOT verify signatures (that's the verifier pipeline) and does NOT apply trust policy (that's the gate / ``/v1/assess``). """ if not _is_object(payload): diff --git a/agentscore_commerce/aip/verify.py b/agentscore_commerce/aip/verify.py index 961c7a6..1f492fb 100644 --- a/agentscore_commerce/aip/verify.py +++ b/agentscore_commerce/aip/verify.py @@ -1,11 +1,11 @@ -"""AIP Agent Identity Token (AIT) verification pipeline — the verifier orchestrator. +"""AIP Agent Identity Token (AIT) verification pipeline: the verifier orchestrator. This is the function a merchant gate calls. It executes the spec's verification steps over a presented request, composing the three foundation modules: -* :mod:`~agentscore_commerce.aip.jwks` — trusted-issuer enforcement + key discovery -* :mod:`~agentscore_commerce.aip.http_signature` — RFC 9421 proof-of-possession over the request -* :mod:`~agentscore_commerce.aip.types` — AIT structural contract +* :mod:`~agentscore_commerce.aip.jwks`: trusted-issuer enforcement + key discovery +* :mod:`~agentscore_commerce.aip.http_signature`: RFC 9421 proof-of-possession over the request +* :mod:`~agentscore_commerce.aip.types`: AIT structural contract Steps (per spec): @@ -110,7 +110,7 @@ class VerifiedAit: @dataclass class VerifyAitSuccess: - """Successful AIT verification — ``ait`` holds the verified, key-bound token.""" + """Successful AIT verification: ``ait`` holds the verified, key-bound token.""" ait: VerifiedAit ok: Literal[True] = True @@ -118,7 +118,7 @@ class VerifyAitSuccess: @dataclass class VerifyAitFailureResult: - """Failed AIT verification — ``reason`` names the typed verify-failure (-> wire error code).""" + """Failed AIT verification: ``reason`` names the typed verify-failure (-> wire error code).""" reason: VerifyAitFailure ok: Literal[False] = False @@ -153,7 +153,7 @@ async def verify_ait( return VerifyAitFailureResult(reason="no_token") if not ctx.signature_input or not ctx.signature: return VerifyAitFailureResult(reason="pop_signature_missing") - # Captured post-guard (str, not str | None) — reused for the local fail-fast PoP check and + # Captured post-guard (str, not str | None): reused for the local fail-fast PoP check and # forwarded to /v1/assess so the API can re-verify the same proof-of-possession authoritatively. signature_input = ctx.signature_input signature = ctx.signature @@ -200,7 +200,7 @@ async def verify_ait( idp_key = key_lookup.key if idp_key is None: # `ok=True` guarantees a key per the JwksCache contract; this guard only narrows the - # `Jwk | None` type (and defends a sibling regression) — treat a missing key as unavailable. + # `Jwk | None` type (and defends a sibling regression): treat a missing key as unavailable. last_failure = "key_unavailable" continue @@ -212,7 +212,7 @@ async def verify_ait( _verify_idp_signature( token, idp_key, - # Pin the signature algorithm allowlist (RFC 8725 §3.1) — also rejects `alg:none`. + # Pin the signature algorithm allowlist (RFC 8725 §3.1): also rejects `alg:none`. # Without this, a trusted IdP publishing a non-Ed25519 (e.g. RSA/EC) `use:sig` key # would let an attacker present an RS256/ES256 token that verifies. Matches the # server-side allowlist in the AgentScore API verifier. @@ -242,7 +242,7 @@ async def verify_ait( last_failure = "expired_token" continue - # Step 6 + 7 + 8: PoP — verify the RFC 9421 signature against cnf.jwk. `verify_message_signature` + # Step 6 + 7 + 8: PoP: verify the RFC 9421 signature against cnf.jwk. `verify_message_signature` # is synchronous (joserfc crypto is sync, unlike node's async WebCrypto), so no await. Its # `now`/`max_skew_seconds` are integer seconds (compared against integer `created`/`expires`); # floor a float clock to int seconds, identical to the JWT-path flooring above. @@ -252,7 +252,7 @@ async def verify_ait( path=ctx.path, # The agent-identity covered component is the BARE AIT (a Bearer prefix, if present, is # transport that `_strip_bearer` removed above). Verify over `token`, not `raw`, so the edge - # and the API — which verifies over the forwarded bare aip_token — reconstruct the identical + # and the API (which verifies over the forwarded bare aip_token) reconstruct the identical # base. agent_identity=token, signature_input=signature_input, @@ -344,8 +344,8 @@ def _verify_idp_signature( ``iat`` is deliberately NOT validated here: jose's ``jwtVerify`` does not reject a future ``iat``, but joserfc's ``JWTClaimsRegistry.validate_iat`` does (raising a generic - ``InvalidClaimError``). To stay behavior-exact with node — which checks the future-``iat`` - case itself and maps it to ``expired_token`` (NOT ``idp_signature_invalid``) — drop ``iat`` + ``InvalidClaimError``). To stay behavior-exact with node: which checks the future-``iat`` + case itself and maps it to ``expired_token`` (NOT ``idp_signature_invalid``): drop ``iat`` from the validated claims so joserfc never sees it; the caller does the future-``iat`` check. Raises :class:`_JwtExpiredError` on expiry and :class:`_JwtVerifyError` on any other @@ -363,7 +363,7 @@ def _verify_idp_signature( except JoseError as exc: raise _JwtVerifyError(str(exc)) from exc - # joserfc's `decode` verifies the signature but does NOT validate temporal claims — run the + # joserfc's `decode` verifies the signature but does NOT validate temporal claims: run the # claims registry separately (mirrors jose's `jwtVerify`, which checks `exp`/`nbf` after the # signature). `leeway` == the clock tolerance; an integer `now` pins the comparison clock for # tests. `exp`/`nbf` are integer seconds per spec; floor a float clock to integer seconds. diff --git a/agentscore_commerce/api/__init__.py b/agentscore_commerce/api/__init__.py index 1575968..0f456ba 100644 --- a/agentscore_commerce/api/__init__.py +++ b/agentscore_commerce/api/__init__.py @@ -1,4 +1,4 @@ -"""AgentScore SDK re-export — single import path for the underlying agentscore-py. +"""AgentScore SDK re-export: single import path for the underlying agentscore-py. Vendors install only ``agentscore-commerce`` and reach everything from the underlying ``agentscore-py`` here. Don't add ``agentscore-py`` as a separate dep; the two can diff --git a/agentscore_commerce/challenge/agent_instructions.py b/agentscore_commerce/challenge/agent_instructions.py index b6372b2..6662f5f 100644 --- a/agentscore_commerce/challenge/agent_instructions.py +++ b/agentscore_commerce/challenge/agent_instructions.py @@ -58,7 +58,7 @@ def _default_warnings(how_to_pay: dict[str, Any]) -> list[str]: def compatible_clients_by_rails(rails: Iterable[str]) -> dict[str, list[str]] | None: """Smoke-verified client list for a set of rail keys. - The single source of truth for "which CLIs we've verified end-to-end on each rail" — + The single source of truth for "which CLIs we've verified end-to-end on each rail": consumed both by the 402-body builder (``build_agent_instructions``) and by discovery surfaces (skill.md, llms.txt, etc.). Update here, every surface inherits. """ @@ -101,13 +101,13 @@ def build_agent_instructions( extra_warnings: list[str] | None = None, recommended: str | None = None, # Per-rail list of client names the merchant has verified work end-to-end. - # Vendors set this from their own smoke matrix — defaults to None, in which case + # Vendors set this from their own smoke matrix: defaults to None, in which case # the field is not emitted (avoids vouching for clients the merchant has not tested). # Keys are rail identifiers (e.g. "x402_base", "tempo_mpp"); values are display labels. compatible_clients: dict[str, list[str]] | None = None, extra: dict[str, Any] | None = None, ) -> dict[str, Any]: - """Build the agent_instructions block — combines how_to_pay with tools, warnings, compat note, timeout. + """Build the agent_instructions block: combines how_to_pay with tools, warnings, compat note, timeout. Defaults adapt to the rails declared in ``how_to_pay``: only tempo-relevant warnings/tools appear if ``how_to_pay["tempo"]`` is set, only x402-relevant ones if ``x402_base``/ diff --git a/agentscore_commerce/challenge/agent_memory.py b/agentscore_commerce/challenge/agent_memory.py index 3d8d422..799c856 100644 --- a/agentscore_commerce/challenge/agent_memory.py +++ b/agentscore_commerce/challenge/agent_memory.py @@ -10,7 +10,7 @@ - Merchant tracks per-operator (or per-IP / per-fingerprint) "have I seen this agent before?" in their own DB - On first encounter, include the hint so the agent saves the pattern - - On subsequent encounters, skip — the agent already has it (or never will) + - On subsequent encounters, skip: the agent already has it (or never will) The hint contents come from :func:`build_agent_memory_hint` (re-exported here for convenience). Keep it stateless: AgentScore's pattern doesn't depend on the merchant's diff --git a/agentscore_commerce/challenge/body.py b/agentscore_commerce/challenge/body.py index 39fc338..44f6a4e 100644 --- a/agentscore_commerce/challenge/body.py +++ b/agentscore_commerce/challenge/body.py @@ -1,4 +1,4 @@ -"""build_402_body — full enriched 402 response body builder.""" +"""build_402_body: full enriched 402 response body builder.""" from dataclasses import asdict, dataclass, is_dataclass from typing import Any, Literal diff --git a/agentscore_commerce/challenge/how_to_pay.py b/agentscore_commerce/challenge/how_to_pay.py index 90cfde1..b008a6a 100644 --- a/agentscore_commerce/challenge/how_to_pay.py +++ b/agentscore_commerce/challenge/how_to_pay.py @@ -1,4 +1,4 @@ -"""how_to_pay block builder — per-rail setup/command/what_it_does for 402 agent_instructions.""" +"""how_to_pay block builder: per-rail setup/command/what_it_does for 402 agent_instructions.""" import math from typing import Any @@ -43,7 +43,7 @@ async def build_how_to_pay( retry_body_json: str, total_usd: float | str, rails: dict[str, TempoRailSpec | X402BaseRailSpec | SolanaMppRailSpec | StripeRailSpec], - op_token_placeholder: str | None = "", # noqa: S107 — literal placeholder, not a secret + op_token_placeholder: str | None = "", # noqa: S107 # literal placeholder, not a secret max_spend: float | str | None = None, decimals: int = 2, ) -> dict[str, Any]: @@ -65,7 +65,7 @@ async def build_how_to_pay( ``op_token_placeholder`` defaults to ``""``. Pass ``None`` (gateless merchants) to strip the ``-H 'X-Operator-Token: ...'`` snippet - from every rail command — appropriate when the merchant doesn't run an + from every rail command: appropriate when the merchant doesn't run an identity gate. The always-on wallet OFAC SDN default does NOT need an operator token, so gateless merchants emit cleaner commands. """ diff --git a/agentscore_commerce/challenge/pricing.py b/agentscore_commerce/challenge/pricing.py index 21087d7..5da14dd 100644 --- a/agentscore_commerce/challenge/pricing.py +++ b/agentscore_commerce/challenge/pricing.py @@ -1,8 +1,8 @@ """Pricing block builder + canonical type. Composes cents-denominated price components into the dollar-string shape that 402 -challenge bodies advertise. Standardizes the pricing block so every merchant — -current and future commerce-platform plugins (Commerce7, WooCommerce, Shopify) — +challenge bodies advertise. Standardizes the pricing block so every merchant: +current and future commerce-platform plugins (Commerce7, WooCommerce, Shopify): surfaces the same shape to agents. Shipping is included by default because most physical-goods merchants carry it; pass diff --git a/agentscore_commerce/challenge/respond_402.py b/agentscore_commerce/challenge/respond_402.py index 4d8d0a1..dfd9416 100644 --- a/agentscore_commerce/challenge/respond_402.py +++ b/agentscore_commerce/challenge/respond_402.py @@ -1,14 +1,14 @@ -"""``respond_402`` — single-call 402 emit for merchants who use both pympp + x402. +"""``respond_402``: single-call 402 emit for merchants who use both pympp + x402. Pympp handles tempo + stripe MPP rails; x402 handles Base + Solana. The seam is fiddly enough to get wrong by hand: - pympp's compose returns a 402 response with WWW-Authenticate directives whose ids - pympp's server-side validator REMEMBERS — they round-trip in client credentials. + pympp's server-side validator REMEMBERS: they round-trip in client credentials. Overwriting that header (e.g. with a freshly-built directive) breaks the round-trip. - x402 needs the binary-friendly ``PAYMENT-REQUIRED`` header (base64-encoded JSON of - ``{x402Version, accepts, resource}``) — pympp doesn't emit it. + ``{x402Version, accepts, resource}``): pympp doesn't emit it. - Merchants want a richer JSON body (pricing, identity metadata, agent_instructions, agent_memory, retry_body, accepted_methods cross-reference) than the bare pympp body. @@ -35,7 +35,7 @@ @dataclass class Respond402Result: - """Framework-neutral 402 response shape — body + headers + status.""" + """Framework-neutral 402 response shape: body + headers + status.""" body: dict[str, object] headers: dict[str, str] @@ -55,7 +55,7 @@ def respond_402( ``body`` is the already-built dict from :func:`build_402_body`. ``x402``, when set, carries the PAYMENT-REQUIRED header inputs (``x402_version``, ``accepts``, - ``resource``); omit for merchants that don't accept x402 (Base / Solana) — pympp-only + ``resource``); omit for merchants that don't accept x402 (Base / Solana): pympp-only setups. """ headers = {k.lower(): v for k, v in mppx_challenge_headers.items()} diff --git a/agentscore_commerce/challenge/validation_error.py b/agentscore_commerce/challenge/validation_error.py index bd8945c..b2deb5a 100644 --- a/agentscore_commerce/challenge/validation_error.py +++ b/agentscore_commerce/challenge/validation_error.py @@ -3,7 +3,7 @@ Pairs cleanly with the existing 402 / 403 builders. Every commerce merchant returning helpful ``bad_request`` / ``not_found`` / ``out_of_stock`` errors converges on the same shape: ``{error: {code, message}, ...optional_hints, -next_steps?}``. This builder doesn't choose the HTTP status — vendors wrap the +next_steps?}``. This builder doesn't choose the HTTP status: vendors wrap the returned body in their framework's response (``JSONResponse(body, 400)`` in FastAPI, etc.). Status stays the merchant's call because the same shape works for 400/404/409/422. diff --git a/agentscore_commerce/checkout.py b/agentscore_commerce/checkout.py index dab3d8d..c206a7e 100644 --- a/agentscore_commerce/checkout.py +++ b/agentscore_commerce/checkout.py @@ -364,7 +364,7 @@ class CheckoutContext: for this request. Set by Checkout's internal gate after a successful allow when an ``operator_token`` is present; ``None`` for wallet-authenticated requests (no operator_token to associate) or anonymous discovery legs. - Fire-and-forget — invoke from ``on_settled`` with the recovered signer: + Fire-and-forget: invoke from ``on_settled`` with the recovered signer: ``await ctx.capture_wallet(wallet_address=..., network=..., idempotency_key=...)``. """ @@ -503,13 +503,13 @@ class CheckoutGateConfig: The gate flow has three customization seams: - 1. ``run_gate`` — full escape hatch. Replaces the SDK's gate flow entirely. + 1. ``run_gate``: full escape hatch. Replaces the SDK's gate flow entirely. Used by merchants with custom auth (e.g. enterprise SSO bridges) who need full control. Other fields are ignored when set. - 2. ``per_request_policy`` — reads ``ctx.state`` (populated by pre_validate) + 2. ``per_request_policy``: reads ``ctx.state`` (populated by pre_validate) and returns a dict that overrides static gate policy fields per request. Goods merchants resolve per-product compliance from this. - 3. ``on_denied`` — invoked AFTER the SDK builds the canonical DenialReason. + 3. ``on_denied``: invoked AFTER the SDK builds the canonical DenialReason. Returns a custom denial body shape, or ``None`` to keep the canonical body. ``create_session_on_missing`` auto-mints a verification session when no @@ -863,7 +863,7 @@ class Checkout: * ``credential_pre_check``; reject payment credentials that fail the cheap wire-shape check (not base64 JSON, not a token-shaped value) BEFORE any merchant hook runs, so junk headers never trigger ``pre_validate`` / - pricing / recipient minting / the gate's assess call. Shape only — + pricing / recipient minting / the gate's assess call. Shape only: signature and payTo verification stay on the settle path. Default ``True``; set ``False`` for custom ``compose_mppx`` implementations that accept non-standard credential encodings. @@ -970,7 +970,7 @@ def __init__( self.discovery_probe = discovery_probe """Per-endpoint x402 ``extensions`` block emitted on the 402 body. Merge outputs of ``build_bazaar_discovery_payload({...})`` (or other extension - declarers) here — Checkout forwards verbatim into the 402 response + declarers) here: Checkout forwards verbatim into the 402 response body's ``extensions`` field so Bazaar crawlers and other spec-compliant clients read the route's declared input/output schema.""" @@ -1200,7 +1200,7 @@ async def handle(self, request: CheckoutRequest) -> CheckoutResult: # Credential shape gate: runs BEFORE pre_validate / the identity gate / # pricing / recipient minting so a junk payment header cannot trigger # merchant hooks (which may do paid upstream work) or burn an assess - # call. Shape only — real verification stays on the settle path, which + # call. Shape only: real verification stays on the settle path, which # needs per-request state the hooks produce. Scoped to the credential # channels this Checkout actually dispatches on, so e.g. an x402 header # at a Tempo-only merchant keeps its current discovery-leg behavior. @@ -1255,7 +1255,7 @@ async def handle(self, request: CheckoutRequest) -> CheckoutResult: # - Merchants with an explicit ``gate`` config run the full identity # policy (KYC / age / sanctions / jurisdiction) via ``_run_gate``. # - Merchants WITHOUT a ``gate`` config still get wallet OFAC SDN - # enforcement via ``_run_wallet_sanctions_only`` — the always-on + # enforcement via ``_run_wallet_sanctions_only``: the always-on # strict-liability default. Falls back to AGENTSCORE_API_KEY env # var when set; logs a warning and skips when no key is set # (dev/testnet pattern). @@ -1496,7 +1496,7 @@ def handle_django(self, request: Any, *, body: dict[str, Any] | None = None) -> ) # ───────────────────────────────────────────────────────────────────── - # mount_ucp_routes_ — register `/.well-known/ucp` + `/jwks.json` + # mount_ucp_routes_: register `/.well-known/ucp` + `/jwks.json` # + OPTIONS preflights on the app in one call. Saves merchants the ~40-line # 3-route registration block every UCP-publishing merchant otherwise # hand-rolls. Equivalent across all five Python framework adapters. @@ -1786,7 +1786,7 @@ async def _run_aip_assess( re-verifies PoP authoritatively and evaluates ``eff_policy`` against the token's attested claims. Returns ``None`` on allow (stamping ``identity_status='verified'`` on ``ctx.assess``); a denial :class:`CheckoutResult` otherwise. Compliance fields come from - ``eff_policy`` — the per-issuer override for the verified AIT's issuer when configured, + ``eff_policy``: the per-issuer override for the verified AIT's issuer when configured, else the gate defaults (a whole-policy replacement, mirroring node). """ from agentscore.errors import ( @@ -1886,7 +1886,7 @@ async def _aip_denial_result( ``on_denied`` runs FIRST (node parity): when it returns an override it fully owns the body, so no superset wrapping happens. Otherwise the AgentScore denial body is emitted as an - RFC 9457 + AIP-spec SUPERSET (``application/problem+json``) — both schemes at once: the rich + RFC 9457 + AIP-spec SUPERSET (``application/problem+json``): both schemes at once: the rich AgentScore ``{ error, agent_instructions, ... }`` AND the spec's ``type``/``title``/ ``status``/``detail`` (+ escalation). The wallet / operator-token paths never reach here, so they keep the bare AgentScore body + ``application/json``. @@ -1932,24 +1932,24 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: Three customization seams (in order of precedence): - 1. ``gate.run_gate`` — when set, replaces the SDK's gate flow entirely. - 2. ``gate.per_request_policy`` — per-request policy override merged over + 1. ``gate.run_gate``: when set, replaces the SDK's gate flow entirely. + 2. ``gate.per_request_policy``: per-request policy override merged over static gate fields. Return ``None`` to skip the gate. - 3. ``gate.on_denied`` — invoked after canonical DenialReason is built to + 3. ``gate.on_denied``: invoked after canonical DenialReason is built to reshape the body for the merchant's response contract. """ if self.gate is None: return None gate = self.gate - # 1. run_gate escape hatch — replaces everything else (also bypasses the gate.aip AIP + # 1. run_gate escape hatch: replaces everything else (also bypasses the gate.aip AIP # pre-step below; a custom gate owns AIT verification too, so run_gate and gate.aip # are mutually exclusive). if gate.run_gate is not None: result = await _maybe_await(gate.run_gate(ctx)) return self._coerce_run_gate_result(ctx, result) - # AIP pre-step — runs BEFORE the no-api_key fallback so a present-but-invalid AIT is + # AIP pre-step: runs BEFORE the no-api_key fallback so a present-but-invalid AIT is # always a hard deny, and a cryptographically verified AIT is honored even on an # offline-only gate. The RFC 9421 proof-of-possession can only be checked here at the # edge, where the signed HTTP message lives. A valid AIT becomes the sole identity (wins @@ -2017,7 +2017,7 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: # Enforce the merchant's trust_level / auth.amr requirement (the spec's human-presence # gate). Verification-derived (carried in the verified token), so enforced here at the - # edge — insufficient → weak_auth (403) with required_* so the agent can step up. + # edge: insufficient → weak_auth (403) with required_* so the agent can step up. weak_detail = check_trust_requirements(ait.payload, gate.aip.require_trust_level, gate.aip.require_amr) if weak_detail is not None: body = build_aip_weak_auth_body( @@ -2052,7 +2052,7 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: allowed_jurisdictions=gate.allowed_jurisdictions, ) - # Gate configured without an API key — full policy enforcement requires + # Gate configured without an API key: full policy enforcement requires # /v1/assess access, which we can't reach. Fall through to wallet OFAC # SDN enforcement (the strict-liability default) so the merchant still # gets the basic protection layer instead of silently allowing. @@ -2061,7 +2061,7 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: # A cryptographically verified AIT is a complete offline *identity* check (issuer # signature + RFC 9421 PoP). But compliance *policy* is evaluated against the # token's claims by /v1/assess, which needs an api_key. If the merchant declared - # policy fields without an api_key we cannot enforce them — fail closed rather than + # policy fields without an api_key we cannot enforce them: fail closed rather than # silently allow a verified-but-non-compliant identity. Identity-only gates (no # policy fields) are satisfied by the verified AIT alone. has_policy = bool( @@ -2101,7 +2101,7 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: # 2. per_request_policy resolves per-product compliance (e.g. wine vs # generic merch). Returning None means "no per-product *identity* policy - # for this product" — but it must NOT skip the always-on wallet OFAC SDN + # for this product": but it must NOT skip the always-on wallet OFAC SDN # floor. Route to _run_wallet_sanctions_only so a NULL-enforcement product # still screens its payment signer (identical to the no-gate dispatch). The # floor is a no-op for non-wallet flows (no api_key, or no extractable @@ -2137,7 +2137,7 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: if not merged_policy: merged_policy = {} # `enforcement` is per-product (soft/hard); read it for the soft/hard handling - # below but DO NOT remove it — build_gate_from_policy keys off `enforcement` to + # below but DO NOT remove it: build_gate_from_policy keys off `enforcement` to # decide whether to build a gate at all (no enforcement => no gate), and the gate # constructor reads only specific fields (require_*, min_age, jurisdictions), so # leaving `enforcement` in the dict is harmless. Popping it here previously made @@ -2151,7 +2151,7 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: # So when the merged policy declares ANY compliance gate field but no explicit # enforcement, default to "hard" so the static gate fires. (The per_request_policy # path supplies its own enforcement, including an intentional soft/None.) Node has - # no enforcement abstraction here — it builds the core and calls evaluate whenever + # no enforcement abstraction here: it builds the core and calls evaluate whenever # policy fields are present (the reference gate); this default # restores that always-fire behavior for the static-gate path. enforcement = merged_policy.get("enforcement") if isinstance(merged_policy, dict) else None @@ -2201,7 +2201,7 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: if result.status == "denied": denial_body = result.denial_body or {} denial_status = result.denial_status or 403 - # 3. on_denied callback — let merchants reshape the canonical body. + # 3. on_denied callback: let merchants reshape the canonical body. if gate.on_denied is not None: custom = await _maybe_await(gate.on_denied(ctx, denial_body)) if isinstance(custom, dict) and "body" in custom: @@ -2219,7 +2219,7 @@ async def _run_gate(self, ctx: CheckoutContext) -> CheckoutResult | None: # gate's primary /v1/assess call composed a signer_match verdict when a payment signer # was extracted; a non-`pass` verdict means the payment signer doesn't match the claimed # wallet (or a same-operator linked wallet). Convert it into a 403 here so Checkout - # enforces wallet-signer binding inline — without this, python settles a mismatch that + # enforces wallet-signer binding inline: without this, python settles a mismatch that # node blocks. Enforcement applies ONLY to the wallet identity path: on the AIT path the # identity is the token (PoP-bound, assess keyed by aip_token, no address-keyed verdict), # and on the operator-token path the operator-token wins and signer-match is deliberately @@ -2276,7 +2276,7 @@ async def _enforce_signer_match( Reads the request-local signer verdict the gate composed (``gate_instance._client`` is built fresh per request, so this is race-free) and maps a wallet_signer_mismatch / wallet_auth_requires_wallet_signing verdict onto the canonical 403 body via - ``denial_reason_to_body`` — byte-for-byte the SAME path + shape the reference implementation's + ``denial_reason_to_body``: byte-for-byte the SAME path + shape the reference implementation's ``Checkout.runGate`` emits (an ``agent_instructions`` recovery container, not the standalone ``build_signer_mismatch_body`` helper's ``next_steps`` container). Runs the gate's ``on_denied`` reshaper if configured. Returns ``None`` when the verdict is ``pass`` @@ -2332,9 +2332,9 @@ async def _run_wallet_sanctions_only(self, ctx: CheckoutContext) -> CheckoutResu strict-liability default). Env knobs: - - ``AGENTSCORE_API_KEY`` — required. No key → one-time warning + skip + - ``AGENTSCORE_API_KEY``: required. No key → one-time warning + skip (dev/testnet pattern; production should always configure a key). - - ``AGENTSCORE_BASE_URL`` — optional override for staging/dev API + - ``AGENTSCORE_BASE_URL``: optional override for staging/dev API (e.g. ``https://api.staging.example`` or ``http://localhost:3002``). Stripe SPT (no extractable wallet signer) → skip silently; Stripe runs @@ -2343,7 +2343,7 @@ async def _run_wallet_sanctions_only(self, ctx: CheckoutContext) -> CheckoutResu Calls ``/v1/assess`` with the signer wallet as both the primary address and the signer block. The API enforces signer-sanctions unconditionally when a signer is present (no policy flag needed). Denies on OFAC SDN - hit; fail-closed on unavailable lookup (strict liability — falsely + hit; fail-closed on unavailable lookup (strict liability: falsely allowing a sanctioned settle is an OFAC violation, falsely denying a clean buyer is just bad UX). """ @@ -2366,7 +2366,7 @@ async def _run_wallet_sanctions_only(self, ctx: CheckoutContext) -> CheckoutResu break signer = extract_payment_signer(x402_header, authorization_header=authorization_header) if signer is None: - # Stripe SPT path — no wallet signer, no OFAC check possible. Stripe + # Stripe SPT path: no wallet signer, no OFAC check possible. Stripe # screens its own customer accounts; we have nothing to add here. return None @@ -2392,7 +2392,7 @@ async def _run_wallet_sanctions_only(self, ctx: CheckoutContext) -> CheckoutResu signer={"address": signer.address, "network": signer.network}, ) except (TokenExpiredError, InvalidCredentialError) as err: - # 401 — credential issues map to invalid_credential. Unusual on the + # 401: credential issues map to invalid_credential. Unusual on the # wallet-OFAC-only path (no operator_token) but handled for completeness. reason = DenialReason( code="invalid_credential" if isinstance(err, InvalidCredentialError) else "token_expired", @@ -2406,7 +2406,7 @@ async def _run_wallet_sanctions_only(self, ctx: CheckoutContext) -> CheckoutResu settled=False, ) except (AgentScoreError, Exception) as err: - # 503 — API outage or network failure. Fail-closed: strict-liability. + # 503: API outage or network failure. Fail-closed: strict-liability. reason = DenialReason(code="api_error", message=str(err)) return CheckoutResult( status=denial_reason_status(reason), @@ -2592,7 +2592,7 @@ async def _async_is_cached_address(self, addr: str, ctx: CheckoutContext | None # bind to it. A rail can carry BOTH a static recipient AND mint_recipients (the static # recipient is the discovery/sentinel default; the per-request mint is the real payTo). # Binding to the construction-time static set here would reject the legit minted payTo, - # so the per-request recipient wins — exactly as the compute-first path already does + # so the per-request recipient wins: exactly as the compute-first path already does # (checkout_compute_first ``expected_pay_to = recipients["x402_base"]``). # 3. otherwise (static-treasury rail) → accept ONLY the configured x402_base recipient. # Mirrors the reference payTo-binding fix. @@ -2606,7 +2606,7 @@ async def _async_is_cached_address(self, addr: str, ctx: CheckoutContext | None return addr.lower() == minted.lower() static_recipient = await self._resolve_static_x402_recipient() if static_recipient is None: - # No x402_base rail / no resolvable static recipient — nothing to bind against. Keep + # No x402_base rail / no resolvable static recipient: nothing to bind against. Keep # the prior permissive behavior so non-x402 / dynamically-recipient setups are unaffected. return True return addr.lower() == static_recipient.lower() @@ -2642,7 +2642,7 @@ async def _resolve_recipients(self, ctx: CheckoutContext) -> dict[str, str]: if self.mint_recipients is None: return ctx.recipients # Idempotent: if a prior call (e.g. pre-compose on the discovery leg) - # already minted, skip — re-running would mint fresh Stripe PIs / etc. + # already minted, skip: re-running would mint fresh Stripe PIs / etc. if ctx.recipients: return ctx.recipients ctx.recipients = dict(await _maybe_await(self.mint_recipients(ctx))) @@ -2892,7 +2892,7 @@ async def _emit_402( # emitted accepted_methods + how_to_pay stay consistent with what the # mppx compose layer will actually accept (see build_mppx_compose_rails). # Without this, the 402 body advertises a stripe rail that has no - # matching WWW-Authenticate challenge — agents see it offered but any + # matching WWW-Authenticate challenge: agents see it offered but any # SPT pay attempt fails. The compose-time auto-drop emits the # user-facing warn; here we just strip the slot from the discovery body. if Decimal(str(ctx.pricing.amount_usd)) < STRIPE_MIN_CHARGE_USD and "stripe" in emit_rails: @@ -2917,7 +2917,7 @@ async def _emit_402( rails=how_to_pay_rails, decimals=pricing_decimals, # Merchants without an identity-bearing policy flag get clean commands - # without an X-Operator-Token header — agents don't need one to satisfy + # without an X-Operator-Token header: agents don't need one to satisfy # the always-on wallet OFAC enforcement default. op_token_placeholder=None if not self._has_identity_gate() else "", ) @@ -3206,10 +3206,10 @@ def _apply_recipient_overrides( passed through unchanged (no on-chain recipient; they use ``profile_id``). Drop-empty: when a merchant declares rails with sentinel empty-string - recipients (the per-order-mint pattern — e.g. Stripe-multichain merchants + recipients (the per-order-mint pattern: e.g. Stripe-multichain merchants that mint a fresh deposit address per request) and ``mint_recipients`` only returns addresses for some rails, drop rails that resolve to an empty - recipient — those weren't actually minted for this request and shouldn't be + recipient: those weren't actually minted for this request and shouldn't be advertised in the 402. """ from dataclasses import replace diff --git a/agentscore_commerce/checkout_compute_first.py b/agentscore_commerce/checkout_compute_first.py index d35cc02..3ea3d01 100644 --- a/agentscore_commerce/checkout_compute_first.py +++ b/agentscore_commerce/checkout_compute_first.py @@ -1,4 +1,4 @@ -"""``compute_first_checkout`` — variable-cost pay-per-result merchant helper. +"""``compute_first_checkout``: variable-cost pay-per-result merchant helper. Uses compute-first + exact-x402 (no upto, no Permit2, no Settlement-Overrides). @@ -23,7 +23,7 @@ Works on every exact-mode rail today (x402-exact Base, ``tempo/charge``, ``solana/charge``, Stripe SPT). The tradeoff vs. upto is that the work runs on -the unpaid probe leg — so rate-limiting is load-bearing (use +the unpaid probe leg: so rate-limiting is load-bearing (use ``agentscore_commerce.middleware.fastapi.RateLimitMiddleware`` or the per-framework equivalent). """ @@ -469,7 +469,7 @@ async def _enforce_wallet_sanctions( break signer = extract_payment_signer(x402_header, authorization_header=authorization_header) if signer is None: - return None # Stripe SPT — no wallet signer to screen + return None # Stripe SPT: no wallet signer to screen from agentscore_commerce.api import AgentScore @@ -541,10 +541,10 @@ async def _handle_x402_settle( # Security: the signed ``payTo`` is agent-controlled (it rides in the X-Payment header). If # accepted blindly, an agent can re-point settlement at a wallet it owns and drain funds the # merchant expected to receive. Bind it to the recipient THIS endpoint resolved for the - # request (``recipients["x402_base"]`` — already minted/static-resolved in + # request (``recipients["x402_base"]``: already minted/static-resolved in # _mint_and_resolve_recipients), accepting the payTo only when it matches (case-insensitive # EVM compare). Mirrors the reference payTo-binding fix. When no x402_base recipient was - # resolved (no rail), keep the prior permissive behavior — there's nothing to bind against. + # resolved (no rail), keep the prior permissive behavior: there's nothing to bind against. expected_pay_to = recipients.get("x402_base") async def _bind_pay_to(addr: str) -> bool: @@ -804,7 +804,7 @@ async def handle(self, request: ComputeFirstRequest) -> tuple[int, dict[str, Any {"Content-Type": "application/json"}, ) recipients = quote.recipients if hasattr(quote, "recipients") else {} - # Wallet OFAC SDN enforcement (always-on default — mirrors + # Wallet OFAC SDN enforcement (always-on default: mirrors # Checkout._run_wallet_sanctions_only). Strict-liability check # before the rail-specific settle so funds don't move (x402) or # order doesn't fulfill (MPP) for a sanctioned wallet. @@ -823,7 +823,7 @@ async def handle(self, request: ComputeFirstRequest) -> tuple[int, dict[str, Any try: outcome = await self.run_work(body, ComputeFirstWorkContext(request=request)) except Exception: - # Suppress the upstream exception detail in the wire response — + # Suppress the upstream exception detail in the wire response: # merchant errors may carry stack traces or internal state. The # merchant's own logger is the right channel for the full exception. return ( @@ -933,7 +933,7 @@ async def handle_sanic(self, request: Any, *, body: dict[str, Any] | None = None return sanic_response.json(response_body, status=status, headers=headers) def handle_flask(self, request: Any, *, body: dict[str, Any] | None = None) -> Any: - """Flask adapter — synchronous-callable that runs the async handle. + """Flask adapter: synchronous-callable that runs the async handle. Returns a Flask Response. """ @@ -972,7 +972,7 @@ async def _run() -> tuple[int, dict[str, Any], dict[str, str]]: return response def handle_django(self, request: Any, *, body: dict[str, Any] | None = None) -> Any: - """Django adapter — synchronous-callable matching ``Checkout.handle_django``. + """Django adapter: synchronous-callable matching ``Checkout.handle_django``. Returns a ``JsonResponse``. """ diff --git a/agentscore_commerce/discovery/__init__.py b/agentscore_commerce/discovery/__init__.py index 4e368cf..79afc5b 100644 --- a/agentscore_commerce/discovery/__init__.py +++ b/agentscore_commerce/discovery/__init__.py @@ -1,4 +1,4 @@ -"""Discovery helpers — probe responder, Bazaar payload builder, .well-known/mpp.json, llms.txt, OpenAPI snippets.""" +"""Discovery helpers: probe responder, Bazaar payload builder, .well-known/mpp.json, llms.txt, OpenAPI snippets.""" from agentscore_commerce.discovery.agentscore_content import ( PURCHASE_MODE_NOTES, diff --git a/agentscore_commerce/discovery/agentscore_content.py b/agentscore_commerce/discovery/agentscore_content.py index 68aeef2..5627f23 100644 --- a/agentscore_commerce/discovery/agentscore_content.py +++ b/agentscore_commerce/discovery/agentscore_content.py @@ -15,7 +15,7 @@ from typing import Any, Final, Literal # Whether a paid surface accepts redemption codes. Applies to any merchant -# that bills per-purchase or per-call — goods (catalog rows) and API +# that bills per-purchase or per-call: goods (catalog rows) and API # (per-endpoint or per-tier billing) both use this enum. PurchaseMode = Literal["redemption_only", "coupon_applicable", "paid_only"] @@ -67,7 +67,7 @@ def build_agentscore_onboarding_steps( ``"stripe-spt"``. Unknown rail names are passed through verbatim so future rails work without an SDK bump. - Pass ``vendor_type="api"`` for per-call API providers — the catalog step is + Pass ``vendor_type="api"`` for per-call API providers: the catalog step is dropped and the final step becomes "Make the paid call" instead of "Place the order". """ @@ -195,7 +195,7 @@ def build_merchant_index_json( """Build the canonical AgentScore commerce ``/`` root discovery body. Works for both goods merchants (catalog + purchase + orders) and API - merchants (per-call paid endpoints) — ``endpoints`` and any + merchants (per-call paid endpoints): ``endpoints`` and any merchant-specific fields are passed through ``extra``. Common fields surfaced: ``name``, ``description``, ``docs``, ``endpoints``, @@ -272,15 +272,15 @@ def build_success_next_steps( ) -> dict[str, str]: """Standard ``next_steps`` block emitted in a 200 success body. - Works for both goods-merchant order-success and API-merchant per-call-success - — the ``user_message`` reinforces the cross-merchant Passport pattern + Works for both goods-merchant order-success and API-merchant per-call-success: + the ``user_message`` reinforces the cross-merchant Passport pattern (universal), with merchant-specific copy overridable via ``user_message``. ``order_status_url`` is emitted as ``order_status_url``. API merchants that don't have an order-detail endpoint can pass a usage/dashboard URL or omit the field. - ``fulfillment_eta`` is goods-specific (shipping window) — omit for API or + ``fulfillment_eta`` is goods-specific (shipping window): omit for API or digital-goods merchants. """ out: dict[str, str] = { diff --git a/agentscore_commerce/discovery/llms_txt.py b/agentscore_commerce/discovery/llms_txt.py index 452eebf..0309875 100644 --- a/agentscore_commerce/discovery/llms_txt.py +++ b/agentscore_commerce/discovery/llms_txt.py @@ -1,4 +1,4 @@ -"""llms.txt builders — identity section + payment section + full document assembler.""" +"""llms.txt builders: identity section + payment section + full document assembler.""" import re from typing import Any, TypedDict @@ -72,7 +72,7 @@ def llms_txt_payment_section( ) -> str: """Generate the standard "## Payment" section. - Pass ``verbose=True`` for the rich variant — multi-step setup + full command examples + + Pass ``verbose=True`` for the rich variant: multi-step setup + full command examples + exact-amount warnings. Default is the compact one-bullet-per-rail form. ``tempo_network_name`` / ``tempo_chain_id`` are surfaced in the verbose-mode prerequisites; diff --git a/agentscore_commerce/discovery/openapi.py b/agentscore_commerce/discovery/openapi.py index ee6da62..e465889 100644 --- a/agentscore_commerce/discovery/openapi.py +++ b/agentscore_commerce/discovery/openapi.py @@ -189,7 +189,7 @@ def x_payment_info_from_checkout( ) -> dict[str, Any]: """Derive an ``x-payment-info`` extension from a configured ``Checkout``. - Walks ``checkout.rails`` and emits one entry in ``protocols[]`` per rail — + Walks ``checkout.rails`` and emits one entry in ``protocols[]`` per rail: Tempo MPP, x402 (Base), Solana MPP, Stripe SPT. Saves merchants from enumerating protocols by hand and keeps the OpenAPI doc in sync with the actual rails the Checkout serves. @@ -200,7 +200,7 @@ def x_payment_info_from_checkout( ``stripe``). For Solana MPP, ``currency`` is the SPL mint address per the official - spec (paymentauth.org/draft-solana-charge-00) — read from ``spec.token``. + spec (paymentauth.org/draft-solana-charge-00): read from ``spec.token``. """ from agentscore_commerce.payment.rail_spec import ( SolanaMppRailSpec, diff --git a/agentscore_commerce/discovery/probe.py b/agentscore_commerce/discovery/probe.py index 9aedc1a..d875d22 100644 --- a/agentscore_commerce/discovery/probe.py +++ b/agentscore_commerce/discovery/probe.py @@ -1,4 +1,4 @@ -"""Discovery probe — answers empty-body POSTs from MPP crawlers (mppscan, link-cli) with a sample 402.""" +"""Discovery probe: answers empty-body POSTs from MPP crawlers (mppscan, link-cli) with a sample 402.""" import base64 import json @@ -14,7 +14,7 @@ from agentscore_commerce.payment.usdc import USDC from agentscore_commerce.payment.wwwauthenticate import payment_required_header -# Placeholder payTo for x402 sample accepts in the discovery probe — the probe +# Placeholder payTo for x402 sample accepts in the discovery probe: the probe # exists for crawlers to find that we support x402, not for actual payment. _ZERO_EVM_PAYTO = "0x0000000000000000000000000000000000000000" _ZERO_SOLANA_PAYTO = "11111111111111111111111111111111" @@ -23,7 +23,7 @@ def sample_x402_accept_for_network(caip2: str, amount_atomic: str = "1000000") -> dict[str, Any] | None: """Build a sample x402 accepts entry for a known CAIP-2 network using the USDC registry. - Returns None for networks not in the registry — vendors with custom networks + Returns None for networks not in the registry: vendors with custom networks should construct accepts entries by hand and pass them via ``x402_sample.accepts``. """ if caip2 == networks.base.mainnet.caip2: @@ -80,7 +80,7 @@ class X402SampleProbe: declared ``x402Version`` shape (v2 ``amount``); clients version-route on ``x402Version``. - Pass ``networks`` (shorthand) for the common case — each CAIP-2 network is + Pass ``networks`` (shorthand) for the common case: each CAIP-2 network is mapped to a sample USDC accepts entry via ``sample_x402_accept_for_network``. Or pass ``accepts`` directly for full control over the sample shape. """ @@ -190,9 +190,9 @@ def build_discovery_probe_response( async def is_discovery_probe_request(method: str, authorization: str | None, body_text: str) -> bool: - """Return True for an empty-body POST without a Payment Authorization header — the MPP crawler probe pattern. + """Return True for an empty-body POST without a Payment Authorization header: the MPP crawler probe pattern. - Framework-agnostic — pass extracted method, Authorization header value, and body text. Vendors wire + Framework-agnostic: pass extracted method, Authorization header value, and body text. Vendors wire this against their framework's request object. """ if method.upper() != "POST": diff --git a/agentscore_commerce/discovery/robots_tag.py b/agentscore_commerce/discovery/robots_tag.py index 8234c9b..0e48997 100644 --- a/agentscore_commerce/discovery/robots_tag.py +++ b/agentscore_commerce/discovery/robots_tag.py @@ -3,7 +3,7 @@ Public-by-design endpoints (OpenAPI, llms.txt, MPP/A2A/UCP well-known files) should NOT carry ``X-Robots-Tag: noindex`` since the whole point is for agents and discovery crawlers to find them. Everything else on an agent-only API -should noindex by default — there's no human-shaped HTML to surface, and +should noindex by default: there's no human-shaped HTML to surface, and accidental indexing leaks transactional endpoints into noisy SERPs. This module ships a pure predicate (``is_discovery_path``) that vendors compose @@ -75,7 +75,7 @@ class NoindexNonDiscoveryMiddleware: app.add_middleware(NoindexNonDiscoveryMiddleware, custom_paths={"/sitemap.xml"}) Pure helpers (``is_discovery_path``, ``DEFAULT_DISCOVERY_PATHS``) are exported - for non-ASGI frameworks (Flask, Django sync) — wire them into your own + for non-ASGI frameworks (Flask, Django sync): wire them into your own middleware idiom. """ diff --git a/agentscore_commerce/discovery/skill_md.py b/agentscore_commerce/discovery/skill_md.py index 82f85aa..8b5f10e 100644 --- a/agentscore_commerce/discovery/skill_md.py +++ b/agentscore_commerce/discovery/skill_md.py @@ -1,10 +1,10 @@ -"""skill.md renderer — agentskills.io-compatible agent-discovery surface. +"""skill.md renderer: agentskills.io-compatible agent-discovery surface. Emits a YAML-frontmatter + markdown body manifest describing a merchant's agent-facing contract: payment rails, compatible clients per rail, identity requirements as outcomes, shipping policy, endpoints, triggers, support links. -Renders strictly agent-facing data — only the public payment/identity contract an agent +Renders strictly agent-facing data: only the public payment/identity contract an agent needs to act on. Merchant runtime configuration is not part of this shape. Spec compliance (https://agentskills.io/specification): @@ -17,7 +17,7 @@ The compatible-clients-per-rail table sources from the same SDK constant (``compatible_clients_by_rails`` in ``challenge.agent_instructions``) that drives the live -402 body's ``compatible_clients`` field — single source of truth across surfaces. +402 body's ``compatible_clients`` field: single source of truth across surfaces. """ import re @@ -54,7 +54,7 @@ class SkillMdEndpoint(TypedDict): class SkillMdIdentityRequirements(TypedDict, total=False): """Agent-observable identity requirements only (kyc / age / jurisdictions / sanctions). - Merchant runtime configuration is not part of this shape — agents act on outcomes, + Merchant runtime configuration is not part of this shape: agents act on outcomes, not implementation. """ @@ -380,7 +380,7 @@ def build_skill_md( Output is YAML frontmatter (``name`` / ``description`` / optional ``license`` / ``compatibility`` / ``allowed-tools`` / ``metadata``) followed by markdown sections describing payment rails, identity requirements, endpoints, triggers, and support - links — exactly the agent-facing contract. + links: exactly the agent-facing contract. """ ctx = _SkillCtx( name=name, diff --git a/agentscore_commerce/discovery/well_known.py b/agentscore_commerce/discovery/well_known.py index 83852db..3a6d8b8 100644 --- a/agentscore_commerce/discovery/well_known.py +++ b/agentscore_commerce/discovery/well_known.py @@ -87,7 +87,7 @@ def _compose_handlers(checkout: Checkout) -> dict[str, list[Any]]: """Map rails on the Checkout to a UCP ``payment_handlers`` block. Includes rails with empty-string-sentinel recipients (per-order-mint - pattern) — the static UCP profile drops the recipient field from those + pattern): the static UCP profile drops the recipient field from those entries, and the authoritative per-order recipient ships in the 402 body at request time. Only rails missing the ``recipient`` attribute entirely are excluded. diff --git a/agentscore_commerce/forwarded_proto.py b/agentscore_commerce/forwarded_proto.py index 31d7f4f..55c7421 100644 --- a/agentscore_commerce/forwarded_proto.py +++ b/agentscore_commerce/forwarded_proto.py @@ -13,8 +13,8 @@ def apply_forwarded_proto(url: str, forwarded_proto: str | None) -> str: """Rewrite a URL's scheme to the proxy's original protocol. Behind a TLS-terminating edge proxy (ALB / CloudFront / nginx) the inbound - request arrives as ``http://``, but x402 discovery — and the mppx client's - resource-match check — require the public ``https://``. Honor + request arrives as ``http://``, but x402 discovery: and the mppx client's + resource-match check: require the public ``https://``. Honor ``X-Forwarded-Proto`` (the scheme the client actually used) so the emitted ``resource.url`` matches the URL the client fetched. ``forwarded_proto`` may carry a comma-separated proxy chain (``"https, http"``); the first hop is the diff --git a/agentscore_commerce/identity/_denial.py b/agentscore_commerce/identity/_denial.py index f3cc43d..5681024 100644 --- a/agentscore_commerce/identity/_denial.py +++ b/agentscore_commerce/identity/_denial.py @@ -1,15 +1,15 @@ """Universal denial helpers shared across every adapter. What lives here: - FIXABLE_DENIAL_REASONS / is_fixable_denial — classifier for compliance reasons that can + FIXABLE_DENIAL_REASONS / is_fixable_denial: classifier for compliance reasons that can be resolved by re-completing KYC (vs sanctions / age failures which are permanent). - denial_reason_status — picks the right HTTP status code per denial code (401 for credential + denial_reason_status: picks the right HTTP status code per denial code (401 for credential problems, 503 for transient API errors, 403 for everything else). - build_signer_mismatch_body — produces the standard 403 body for a non-pass signer_match + build_signer_mismatch_body: produces the standard 403 body for a non-pass signer_match verdict (read via get_signer_verdict). - build_contact_support_next_steps — standard `next_steps.action: "contact_support"` shape for + build_contact_support_next_steps: standard `next_steps.action: "contact_support"` shape for unfixable compliance denials. - verification_agent_instructions — the canned `agent_instructions` block for + verification_agent_instructions: the canned `agent_instructions` block for identity-verification 403s. Vendors can override individual fields. Adapters use `denial_reason_status` inside their default `on_denied` so vendors get the right @@ -25,10 +25,10 @@ # Compliance denial reasons that can be resolved by re-completing KYC. The API emits these # when KYC is missing/pending/failed; the user can re-verify and retry. # -# `jurisdiction_restricted` is NOT in this set — the API only emits it AFTER KYC is verified, +# `jurisdiction_restricted` is NOT in this set: the API only emits it AFTER KYC is verified, # meaning the user's KYC'd country is in the merchant's blocked list (or absent from the # allowed list). Re-doing KYC won't change the country, so it's permanent. Same shape as -# `sanctions_flagged` and `age_insufficient` — surface contact_support, don't waste a +# `sanctions_flagged` and `age_insufficient`: surface contact_support, don't waste a # /v1/sessions mint. FIXABLE_DENIAL_REASONS: frozenset[str] = frozenset( { @@ -43,7 +43,7 @@ def is_fixable_denial(reasons: Iterable[str] | None) -> bool: """Return True when every reason is fixable via KYC re-verification. False when any reason is permanent (sanctions, age, jurisdiction_restricted) OR when - reasons is empty/None — without a known reason we can't promise a fix, so default to + reasons is empty/None: without a known reason we can't promise a fix, so default to the bare denial path. """ if not reasons: @@ -158,7 +158,7 @@ def verification_agent_instructions( """Canonical `agent_instructions` block for identity-verification 403s. Tells the agent how to present verify_url, poll for the operator_token, and retry the - original request. Universal across every AgentScore-gated merchant — overrides let vendors + original request. Universal across every AgentScore-gated merchant: overrides let vendors add merchant-specific steps. ``retry_step`` REPLACES the generic "Retry the original merchant request..." step with a @@ -168,7 +168,7 @@ def verification_agent_instructions( the retry (e.g. "After payment the same call returns 200"). """ base_steps = [ - "Present the verify_url directly to the user — it is a complete, ready-to-open URL with the session " + "Present the verify_url directly to the user: it is a complete, ready-to-open URL with the session " "token already embedded (e.g. https://www.agentscore.com/verify?session=sess_...). " "Do NOT modify or construct the URL yourself.", f"Immediately begin polling poll_url every {poll_interval_seconds} seconds with header X-Poll-Secret " @@ -176,7 +176,7 @@ def verification_agent_instructions( "The user visits the URL, signs in, completes identity verification (photo ID + selfie via Stripe Identity), " "and closes the tab. They do NOT need to copy or paste anything back to you.", 'When your poll returns status "verified", extract operator_token from the response. This is a one-time ' - 'value — save it immediately. Subsequent polls return status "consumed" without the token.', + 'value: save it immediately. Subsequent polls return status "consumed" without the token.', retry_step or "Retry the original merchant request with header X-Operator-Token set to the operator_token value.", ] diff --git a/agentscore_commerce/identity/_response.py b/agentscore_commerce/identity/_response.py index 5aa14e8..cbe79e7 100644 --- a/agentscore_commerce/identity/_response.py +++ b/agentscore_commerce/identity/_response.py @@ -1,10 +1,10 @@ """Shared serialization for DenialReason → HTTP body dict. Every adapter (ASGI, FastAPI, Flask, Django, AIOHTTP, Sanic) renders the same -body shape for a denial — this helper keeps them in sync and in one place. +body shape for a denial: this helper keeps them in sync and in one place. Includes the wallet-signer-match fields and the agent_memory payload. -Body shape: ``{"error": {"code": ..., "message": ...}, ...}`` — matches the +Body shape: ``{"error": {"code": ..., "message": ...}, ...}``: matches the canonical AgentScore core API response shape so downstream agents see one consistent ``error.code`` + ``error.message`` pair regardless of which layer produced the denial. @@ -22,7 +22,7 @@ _log = logging.getLogger("agentscore_gate") # Field names the gate claims authority over. Merchant-provided ``extra`` (from the -# on_before_session hook) MUST NOT override these — a buggy or malicious hook could +# on_before_session hook) MUST NOT override these: a buggy or malicious hook could # otherwise replace ``verify_url`` with a phishing URL or drop agent_instructions. _RESERVED_FIELDS = frozenset( { @@ -50,11 +50,11 @@ _MISSING_IDENTITY_BASE_STEPS = [ ( "If you have a wallet AND this merchant accepts a wallet-signing rail " - "(Tempo MPP or x402 — see openapi.json x-payment-info): send X-Wallet-Address " + "(Tempo MPP or x402: see openapi.json x-payment-info): send X-Wallet-Address " "and retry. If the wallet is already linked to an AgentScore operator (via " "prior SIWE claim or prior captureWallet), this passes in one round trip. If " "the wallet is unlinked or the account has no KYC, the 403 will include a " - "verify_url — share it with the user to claim the wallet + complete KYC, " + "verify_url: share it with the user to claim the wallet + complete KYC, " "then retry." ), ( @@ -64,11 +64,11 @@ ( "If neither applies: retry with NO identity header. Merchants that " "auto-create verification sessions (most AgentScore merchants do) return " - "verify_url + session_id + poll_secret in the 403 body — share verify_url " + "verify_url + session_id + poll_secret in the 403 body: share verify_url " "with the user, then poll poll_url every 5s with the X-Poll-Secret header " "until status=verified (the poll returns a one-time operator_token). If the " "retry returns the same bare 403, this merchant does not support self-service " - "session bootstrapping — direct the user to https://www.agentscore.com/sign-up to " + "session bootstrapping: direct the user to https://www.agentscore.com/sign-up to " "create an AgentScore identity and mint an operator_token from their " "dashboard (https://www.agentscore.com/dashboard/verify). The user hands the " "opc_... to you, and you retry with X-Operator-Token." @@ -94,7 +94,7 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) if aip_trusted_issuers: steps.append( f"If you hold an AIP Agent Identity Token from a trusted issuer " - f"({', '.join(aip_trusted_issuers)}): present it — send the JWT in an Agent-Identity " + f"({', '.join(aip_trusted_issuers)}): present it: send the JWT in an Agent-Identity " f"header plus an RFC 9421 HTTP Message Signature (Signature-Input + Signature over " f'@method @authority @path agent-identity, tag="agent-identity") signed with the ' f"token-bound cnf key. This satisfies identity in one round trip without an AgentScore " @@ -116,13 +116,13 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) "steps": [ ( "Preferred: re-submit the payment signed by expected_signer (or any entry in " - "linked_wallets — same-operator wallets are fungible) and retry with the same " + "linked_wallets: same-operator wallets are fungible) and retry with the same " "X-Wallet-Address." ), ( "Alternative: drop X-Wallet-Address and retry with X-Operator-Token. Use a " "stored opc_... if you have one; otherwise retry this request with NO " - "identity header — the merchant will mint a verification session in the " + "identity header: the merchant will mint a verification session in the " "403 body (verify_url + poll_secret). Share verify_url with the user, poll, " "receive a fresh opc_..." ), @@ -139,7 +139,7 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) "action": "switch_to_operator_token", "steps": [ ( - "This payment rail (Stripe SPT, card) carries no wallet signature — " + "This payment rail (Stripe SPT, card) carries no wallet signature: " "X-Wallet-Address cannot be verified against the payment." ), ( @@ -162,7 +162,7 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) "The wallet's operator failed an UNFIXABLE compliance check (sanctions, " "age, or jurisdiction). `reasons` lists which: `sanctions_flagged` / " "`age_insufficient` / `jurisdiction_restricted`. KYC re-verification " - "won't change the outcome — the policy denial is structural." + "won't change the outcome: the policy denial is structural." ), ( "Surface the denial to the user with the merchant's support contact. " @@ -171,12 +171,12 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) ), ( "Fixable compliance reasons (`kyc_required`, `kyc_pending`, " - "`kyc_failed`) do NOT land on this code — the gate auto-mints a " + "`kyc_failed`) do NOT land on this code: the gate auto-mints a " "verification session for those and returns " "`identity_verification_required` with poll endpoints, same shape as " "`missing_identity`. `jurisdiction_restricted` IS in the unfixable " "bucket because the API only emits it after KYC is verified (the " - "user's KYC'd country is in the blocked list — re-doing KYC won't " + "user's KYC'd country is in the blocked list: re-doing KYC won't " "change the country)." ), ], @@ -195,16 +195,16 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) ( "The merchant's AgentScore account does not have the assess endpoint " "enabled, so agent identity cannot be evaluated. This is a merchant-side " - "configuration gap — there is no agent-side recovery." + "configuration gap: there is no agent-side recovery." ), ( - "Contact the merchant (their support channel — typically listed in " + "Contact the merchant (their support channel: typically listed in " "/llms.txt or the OpenAPI servers metadata) so they can resolve the " "configuration on their side." ), ], "user_message": ( - "This merchant's identity gate is misconfigured. Contact the merchant — " + "This merchant's identity gate is misconfigured. Contact the merchant: " "there's nothing to fix on the agent side." ), } @@ -216,7 +216,7 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) { "action": "deliver_verify_url_and_poll", "steps": [ - "Share verify_url with the user — they complete identity verification on AgentScore.", + "Share verify_url with the user: they complete identity verification on AgentScore.", ( "If session_id + poll_secret are present in the body, poll poll_url every " "5 seconds with header `X-Poll-Secret: ` until status=verified. " @@ -236,7 +236,7 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) "steps": [ ( "The operator token is expired or revoked. AgentScore auto-mints a fresh " - "verification session — complete it to receive a new opc_..." + "verification session: complete it to receive a new opc_..." ), ( "Share verify_url with the user, then poll poll_url every 5 seconds with " @@ -247,7 +247,7 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) ], "user_message": ( "Operator token is expired or revoked. A new verification session has been " - "minted — visit verify_url to refresh." + "minted: visit verify_url to refresh." ), } ) @@ -257,14 +257,14 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) "action": "retry_with_backoff", "steps": [ "Verification is temporarily unavailable. Retry the request after 5-30 seconds with exponential backoff.", - "This is NOT a compliance denial — the user does not need to re-verify their " + "This is NOT a compliance denial: the user does not need to re-verify their " "identity. Send the same identity headers (X-Wallet-Address or X-Operator-Token) " "on retry.", "If the request continues to fail after 3+ retries (~60 seconds total), surface the " "error to the user with the merchant's support contact.", ], "user_message": ( - "Verification is temporarily unavailable. Please try again in a moment — this is a " + "Verification is temporarily unavailable. Please try again in a moment: this is a " "transient issue, not a problem with your account." ), } @@ -294,7 +294,7 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) # Codes stamped explicitly upstream are intentionally absent: ``missing_identity`` is # stamped by build_missing_identity_reason, and ``wallet_signer_mismatch`` / # ``wallet_auth_requires_wallet_signing`` are stamped in core.py via get_signer_verdict -# + build_signer_mismatch_body — they never reach this fallback through +# + build_signer_mismatch_body: they never reach this fallback through # denial_reason_to_body. _DEFAULT_AGENT_INSTRUCTIONS: dict[str, str] = { "api_error": _API_ERROR_INSTRUCTIONS, @@ -308,7 +308,7 @@ def _missing_identity_instructions(aip_trusted_issuers: list[str] | None = None) def build_missing_identity_reason(aip_trusted_issuers: list[str] | None = None) -> DenialReason: """Construct a missing_identity DenialReason with the cross-merchant memory hint attached. - Emitted when the adapter has no identity AND no create_session_on_missing config — this is the + Emitted when the adapter has no identity AND no create_session_on_missing config: this is the cold-start bootstrap path where the memory hint is most useful. The attached agent_instructions hint the agent to try stored identity (returning-customer fast path) before running the session/verify flow. @@ -330,7 +330,7 @@ def build_missing_identity_reason(aip_trusted_issuers: list[str] | None = None) "Identity verification is required to access this resource. Visit verify_url to complete KYC." ), "wallet_not_trusted": "The wallet does not meet the merchant compliance policy.", - "api_error": "AgentScore is unreachable. This is transient — retry in a few seconds.", + "api_error": "AgentScore is unreachable. This is transient: retry in a few seconds.", "payment_required": "Assess endpoint not enabled for this merchant. Contact support.", "wallet_signer_mismatch": ( "Payment signer does not match the wallet claimed via X-Wallet-Address. The signer and the " @@ -341,7 +341,7 @@ def build_missing_identity_reason(aip_trusted_issuers: list[str] | None = None) "Switch to X-Operator-Token, or use a wallet-signing rail (Tempo MPP, x402)." ), "token_expired": ( - "The operator token is expired or revoked. A fresh verification session has been minted — " + "The operator token is expired or revoked. A fresh verification session has been minted: " "visit verify_url to mint a new token." ), "invalid_credential": ( @@ -367,7 +367,7 @@ def build_verification_required_body( Goods merchants that surface an ``order_id`` (or similar) from ``CreateSessionOnMissing.on_before_session`` get it for free via - ``denial_reason_to_body``'s ``reason.extra`` passthrough — but can also + ``denial_reason_to_body``'s ``reason.extra`` passthrough: but can also pass ``extra=`` for fallbacks (e.g. when invoked outside the auto-mint path and order_id needs to come from the validated context). """ @@ -426,12 +426,12 @@ def denial_reason_to_body(reason: DenialReason) -> dict[str, Any]: if reason.linked_wallets: body["linked_wallets"] = reason.linked_wallets # Merchant-supplied fields from on_before_session hook. Guard against collision - # with reserved fields — the gate owns those and can't let a hook override them. + # with reserved fields: the gate owns those and can't let a hook override them. if reason.extra: for key, value in reason.extra.items(): if key in _RESERVED_FIELDS: _log.warning( - "on_before_session returned reserved field '%s' — ignoring to preserve gate authority", + "on_before_session returned reserved field '%s': ignoring to preserve gate authority", key, ) continue diff --git a/agentscore_commerce/identity/a2a.py b/agentscore_commerce/identity/a2a.py index 4b04d33..54f4013 100644 --- a/agentscore_commerce/identity/a2a.py +++ b/agentscore_commerce/identity/a2a.py @@ -1,11 +1,11 @@ -"""Google A2A (Agent-to-Agent) v1.0 Agent Card builder. +"""A2A (Agent-to-Agent) v1.0 Agent Card builder. Compose the JSON payload for an A2A v1.0 Agent Card matching the canonical ``AgentCard`` type from ``@a2a-js/sdk``. Returned object is the unsigned card -body — wrap with an ``A2AAgentCardSignature`` (RFC 7515 JWS) to sign vendor-side -before publishing at /.well-known/agent-card.json. +body; to sign it vendor-side, attach ``A2AAgentCardSignature`` entries (RFC 7515 JWS) +as ``signatures`` before publishing at /.well-known/agent-card.json. -Why publish: A2A is a Linux Foundation standard. Signed Agent Cards let any +Why publish: A2A is a Linux Foundation standard. 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[] @@ -33,11 +33,11 @@ A2A_DEFAULT_TRANSPORT = _DEFAULT_TRANSPORT UCP_A2A_EXTENSION_URI = "https://ucp.dev/2026-08-25/specification/reference" -"""Canonical UCP A2A extension URI — verifiers look for this exact URI in +"""Canonical UCP A2A extension URI: verifiers look for this exact URI in ``capabilities.extensions[]`` to detect UCP support on the agent card.""" AIP_A2A_EXTENSION_URI = "https://www.agentscore.com/.well-known/agent-identity" -"""Canonical URI for the AIP (Agentic Identity Protocol) A2A agent-card extension — points at the +"""Canonical URI for the AIP (Agentic Identity Protocol) A2A agent-card extension: points at the issuer-discovery well-known so a reader can resolve the protocol.""" @@ -55,7 +55,7 @@ class A2AAgentInterface: """ transport: str - """Open string — core values are ``JSONRPC``, ``GRPC``, ``HTTP+JSON``.""" + """Open string: core values are ``JSONRPC``, ``GRPC``, ``HTTP+JSON``.""" url: str def to_dict(self) -> dict[str, Any]: @@ -169,7 +169,7 @@ def aip_a2a_extension( """Build the AIP entry for an A2A agent card's ``capabilities.extensions[]``. Advertises that the agent accepts an Agent Identity Token (JWT) in an - ``Agent-Identity`` header plus an RFC 9421 proof-of-possession signature — so an + ``Agent-Identity`` header plus an RFC 9421 proof-of-possession signature: so an agent discovering via the agent-card learns it can present an AIT (matching what the merchant's mpp.json / llms.txt / skill.md already advertise). Optional ``required_trust_level`` / ``required_amr`` surface a gate's human-presence diff --git a/agentscore_commerce/identity/address.py b/agentscore_commerce/identity/address.py index 37173d6..1ce0a0c 100644 --- a/agentscore_commerce/identity/address.py +++ b/agentscore_commerce/identity/address.py @@ -1,8 +1,8 @@ """Network-aware address normalization. -EVM addresses (0x + 40 hex chars) are case-insensitive in the protocol — we lowercase them +EVM addresses (0x + 40 hex chars) are case-insensitive in the protocol: we lowercase them so DB lookups against `address_lower`-style columns work. Solana addresses are base58 and -case-sensitive — we MUST preserve the input verbatim, never lowercase. +case-sensitive: we MUST preserve the input verbatim, never lowercase. Must produce the same normalization the AgentScore API applies. Drift here silently breaks captured-wallet resolution and signer-match. diff --git a/agentscore_commerce/identity/aiohttp.py b/agentscore_commerce/identity/aiohttp.py index 1306218..1edd574 100644 --- a/agentscore_commerce/identity/aiohttp.py +++ b/agentscore_commerce/identity/aiohttp.py @@ -233,7 +233,7 @@ async def _agentscore_middleware( if recovered is not None: signer_payload = {"address": recovered.address, "network": recovered.network} - # Only acheck_identity is wrapped — the downstream handler call must NOT be in the + # Only acheck_identity is wrapped: the downstream handler call must NOT be in the # try, otherwise an exception in the user's route would be misclassified as an # AgentScore infra failure and (under fail_open) re-invoke their handler. try: @@ -245,7 +245,7 @@ async def _agentscore_middleware( except TokenDeniedError as err: return _deny_response(request, build_token_denied_reason(err)) except InvalidCredentialError: - # Permanent — no auto-session, agent should switch tokens or restart. + # Permanent: no auto-session, agent should switch tokens or restart. return _deny_response(request, build_invalid_credential_reason()) except QuotaExceededError: if client.fail_open: @@ -292,7 +292,7 @@ async def _agentscore_middleware( # with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient, # jurisdiction_restricted) keep the bare wallet_not_trusted denial. # `jurisdiction_restricted` is unfixable: the API only emits it after KYC is - # verified (the user's KYC'd country is in the blocked list — re-doing KYC + # verified (the user's KYC'd country is in the blocked list: re-doing KYC # won't change the country). if is_fixable_denial(result.reasons) and create_session_on_missing is not None: session_reason = await try_create_session_denial_reason( @@ -323,7 +323,7 @@ def get_signer_verdict(request: web.Request) -> SignerVerdict | None: credential, or for fail-open pass-throughs (no assess call). Reads the request-scoped verdict stashed by the gate (projected from THIS request's - assess response) — concurrency-safe against a sibling same-wallet request. + assess response): concurrency-safe against a sibling same-wallet request. """ state = request.get(GATE_STATE_KEY) if not isinstance(state, dict): @@ -379,7 +379,7 @@ def conditional_agentscore_gate_middleware(**kwargs: Any) -> Any: # --------------------------------------------------------------------------- -# AIP gate (Agentic Identity Protocol) — verifies a key-bound Agent Identity Token (AIT) +# AIP gate (Agentic Identity Protocol): verifies a key-bound Agent Identity Token (AIT) # from a trusted IdP instead of an opaque operator token. Cryptographic identity only; # merchants who want compliance enrichment feed the verified claims to ``/v1/assess``. # aiohttp's ``web.Request`` exposes method / url / headers, so this verifies straight off the diff --git a/agentscore_commerce/identity/core.py b/agentscore_commerce/identity/core.py index 6cc495d..540b2e2 100644 --- a/agentscore_commerce/identity/core.py +++ b/agentscore_commerce/identity/core.py @@ -120,7 +120,7 @@ def __init__( default_ua = f"agentscore-commerce/{_pkg_version('agentscore-commerce')}" self.user_agent = f"{user_agent} ({default_ua})" if user_agent else default_ua self._cache: TTLCache[AssessResult] = TTLCache(cache_seconds) - # Parallel cache of the raw /v1/assess response dict — populated alongside the + # Parallel cache of the raw /v1/assess response dict: populated alongside the # projected AssessResult cache (same signer-aware key) so a fresh response for a new # signer never overwrites another signer's raw blob. Same TTL semantics as _cache. # Signer verdicts (signer_match + signer_sanctions) from the most recent assess call that @@ -130,7 +130,7 @@ def __init__( # cache_seconds=0 the entry expires within the same tick, which would non-deterministically # drop the verdict and let a signer-mismatch fall through to settlement). The slot always # holds the LATEST signer's verdict, so a 2nd request with a different signer (a cache miss - # under the signer-aware key) overwrites it — get_signer_verdict can never return a verdict + # under the signer-aware key) overwrites it: get_signer_verdict can never return a verdict # computed for a stale signer. Mirrors the reference `lastSignerRaw`. self._last_signer_raw: dict[str, dict[str, Any]] = {} @@ -181,7 +181,7 @@ def _cache_key( if aip_token: identity_key = f"aip:{hashlib.sha256(aip_token.encode()).hexdigest()}" elif operator_token: - # operator_token is opaque ASCII — lowercasing is safe. + # operator_token is opaque ASCII: lowercasing is safe. identity_key = operator_token.lower() else: # Wallet addresses go through normalize_address so Solana base58 (case-sensitive) @@ -191,7 +191,7 @@ def _cache_key( # per-request signer_match + signer_sanctions verdicts (and the unconditional signer-OFAC # screen) are computed for THIS signer; without the signer in the key, a 2nd request that # claims the same identity but signs with a DIFFERENT wallet would hit the cache and return - # the prior signer's verdict — a sanctioned signer could ride a stale `clear` to settlement. + # the prior signer's verdict: a sanctioned signer could ride a stale `clear` to settlement. # Keying on the normalized signer makes a different signer a cache MISS that re-screens. # Requests with no signer keep the identity-only key (operator-token / discovery legs). # Every part is percent-encoded (delimiter-proof): the claimed X-Wallet-Address is @@ -215,7 +215,7 @@ def _build_body( ) -> dict[str, Any]: """Construct the assess request body. - Testable helper for the policy/chain wiring contract — pinned so a future SDK + Testable helper for the policy/chain wiring contract: pinned so a future SDK body-shape regression would fail the gate's own tests as well. """ body: dict[str, Any] = {} @@ -241,7 +241,7 @@ def _build_body( def _headers(self) -> dict[str, str]: """Construct the canonical assess request headers. - Testable helper for the X-API-Key + User-Agent contract — pinned independently + Testable helper for the X-API-Key + User-Agent contract: pinned independently so a regression on either header would fail the gate's own tests. """ return { @@ -276,7 +276,7 @@ def _parse_response(self, resp: Any) -> AssessResult: raise InvalidCredentialError() if code: _log.warning( - "[gate] /v1/assess returned 401 %s — no specific handler, surfacing as RuntimeError.", + "[gate] /v1/assess returned 401 %s: no specific handler, surfacing as RuntimeError.", code, ) msg = f"AgentScore API returned {status}" @@ -401,7 +401,7 @@ def check( # mock or proxy returning bare `429` falls through to generic. Reroute by # status_code so the gate's fail_open path still surfaces 'quota_exceeded'. if exc.status_code == 429: - _log.warning("[gate] /v1/assess returned 429 (untyped — defensive)") + _log.warning("[gate] /v1/assess returned 429 (untyped: defensive)") raise QuotaExceededError("quota_exceeded") from exc # Wraps any other 401 (schema drift), 5xx, network errors, body-parse failures. # Surface code so ops notice schema-drift cases instead of a silent 503. @@ -465,7 +465,7 @@ async def acheck( raise httpx.TimeoutException(str(exc)) from exc except AgentScoreError as exc: if exc.status_code == 429: - _log.warning("[gate] /v1/assess returned 429 (untyped — defensive)") + _log.warning("[gate] /v1/assess returned 429 (untyped: defensive)") raise QuotaExceededError("quota_exceeded") from exc _log.warning("[gate] /v1/assess call failed (%s): %s", exc.code, exc) # Message format pinned for downstream merchant log scrapers. @@ -532,7 +532,7 @@ def _stash_signer_raw( Keyed by the normalized claimed wallet address. ONLY when the wallet is the EFFECTIVE identity (no operator_token, no aip_token) AND a signer was supplied AND the response - actually carried signer verdicts — matching the gate's enforcement guard. With an + actually carried signer verdicts: matching the gate's enforcement guard. With an operator-token / AIT present, that identity wins and signer-match is deliberately NOT enforced, so we must not surface a verdict for that wallet. Mirrors the reference implementation. """ @@ -567,10 +567,10 @@ def project_signer_verdict(self, raw: dict[str, Any] | None, claimed_address: st """Project ``signer_match`` + ``signer_sanctions`` from a SPECIFIC raw assess response. Pure / request-scoped: takes the exact ``/v1/assess`` response dict THIS request got - (``AssessResult.raw``) and the claimed wallet — it reads NO shared state. Adapters call + (``AssessResult.raw``) and the claimed wallet: it reads NO shared state. Adapters call this with their per-request raw and stash the result on the per-request state, so two concurrent requests that claim the same wallet but sign with different wallets each see - their OWN verdict (the shared ``_last_signer_raw`` slot would race — see + their OWN verdict (the shared ``_last_signer_raw`` slot would race: see :meth:`get_signer_verdict`). Returns ``None`` when the response carried no signer verdicts (operator-token-only paths, discovery legs). """ @@ -596,13 +596,13 @@ def get_signer_verdict(self, claimed_address: str) -> SignerVerdict | None: """Synchronous read of the cached signer verdicts (signer_match + signer_sanctions). Both verdicts were composed by the gate's primary /v1/assess call on this - request — single round trip. Returns ``None`` when the gate didn't run with + request: single round trip. Returns ``None`` when the gate didn't run with a signer (operator-token-only paths, discovery legs). Reads the dedicated, non-expiring signer slot (keyed by normalized claimed address), which holds the LATEST signer's verdict for that wallet. NOTE: this slot lives on the SHARED core (the gate is module-scoped), so under concurrency two requests claiming the - same wallet with DIFFERENT signers race here — the verdict you read may have been + same wallet with DIFFERENT signers race here: the verdict you read may have been computed for the other request's signer. Adapters therefore do NOT use this method for per-request enforcement; they stash the request-scoped verdict (projected from THIS request's ``AssessResult.raw`` via :meth:`project_signer_verdict`) on the per-request @@ -611,7 +611,7 @@ def get_signer_verdict(self, claimed_address: str) -> SignerVerdict | None: compatibility. Wallet-OFAC SDN enforcement is unconditional whenever a signer is in the - request — SDN wallet-address hits are already enforced by the gate + request: SDN wallet-address hits are already enforced by the gate (decision -> deny before the handler runs); merchant code typically only needs this for the signer_match wallet-binding verdict. """ @@ -709,7 +709,7 @@ class QuotaExceededError(RuntimeError): Distinct from a generic 5xx so adapters with ``fail_open=True`` can surface ``infra_reason='quota_exceeded'`` to merchant logs/alerts. Compliance denials - are unaffected — those still deny regardless of fail_open. + are unaffected: those still deny regardless of fail_open. Subclasses ``RuntimeError`` so a broad ``except RuntimeError`` still catches the 429 case; specific code that wants to distinguish 429 from generic 5xx catches @@ -720,7 +720,7 @@ class QuotaExceededError(RuntimeError): class TokenDeniedError(Exception): """Raised when /v1/assess returns 401 token_expired. - Covers both revoked and TTL-expired credentials — the API does not distinguish; it doesn't + Covers both revoked and TTL-expired credentials: the API does not distinguish; it doesn't disclose which. Carries the full response body so the adapter can forward the auto-minted session fields (verify_url, session_id, poll_secret, poll_url, next_steps, agent_memory) to the agent instead of collapsing to wallet_not_trusted. @@ -750,7 +750,7 @@ def build_token_denied_reason(err: TokenDeniedError) -> DenialReason: ) -# Permanent — the operator_token doesn't exist (typo, never minted, fabricated). +# Permanent: the operator_token doesn't exist (typo, never minted, fabricated). # Distinct from TokenDeniedError: no auto-session is issued because the agent may # have other valid tokens to try first. Agents should switch tokens or drop the # header to bootstrap a fresh session. @@ -759,9 +759,9 @@ def build_token_denied_reason(err: TokenDeniedError) -> DenialReason: "action": "switch_token_or_restart_session", "steps": [ "The X-Operator-Token you sent does not match any credential. This is a permanent " - "state — retrying with the same token will keep failing.", + "state: retrying with the same token will keep failing.", "If you have other stored opc_... credentials, retry with one of them.", - "Otherwise drop X-Operator-Token and retry with no identity header — the merchant " + "Otherwise drop X-Operator-Token and retry with no identity header: the merchant " "will mint a fresh verification session in the 403 body (verify_url + poll_secret) " "so the user can re-verify and you can poll for a new operator_token.", ], @@ -777,7 +777,7 @@ class InvalidCredentialError(Exception): """Raised when /v1/assess returns 401 invalid_credential. The token doesn't exist at all (typo, never minted, fabricated). No auto-session - is issued — agents should switch to a different stored token or drop the header + is issued: agents should switch to a different stored token or drop the header to bootstrap a fresh session via the merchant's createSessionOnMissing path. """ @@ -789,7 +789,7 @@ def __init__(self) -> None: def build_invalid_credential_reason() -> DenialReason: """Project an InvalidCredentialError into a DenialReason. - No session fields — the API didn't mint one. Adapters render this as a 403 with + No session fields: the API didn't mint one. Adapters render this as a 403 with agent_instructions that point the agent at recovery (try a different token or restart the session flow). """ diff --git a/agentscore_commerce/identity/default_denied.py b/agentscore_commerce/identity/default_denied.py index 089704b..b322fb6 100644 --- a/agentscore_commerce/identity/default_denied.py +++ b/agentscore_commerce/identity/default_denied.py @@ -2,7 +2,7 @@ Replaces the denial-mapping switch a merchant would otherwise hand-write. -The shape is framework-neutral (``{status, body, headers?}``) — matches +The shape is framework-neutral (``{status, body, headers?}``): matches ``Checkout``'s ``on_denied`` signature directly. For per-framework gate middleware (``AgentScoreGate(...)``) the merchant adapts at the call site with the framework's ``JSONResponse(body, status_code=status, headers=headers)`` diff --git a/agentscore_commerce/identity/django.py b/agentscore_commerce/identity/django.py index 2273164..ef70871 100644 --- a/agentscore_commerce/identity/django.py +++ b/agentscore_commerce/identity/django.py @@ -181,7 +181,7 @@ def __call__(self, request: HttpRequest) -> Any: identity = self._extract_identity(request) # Stash state so capture_wallet() can read operator_token + client after the view runs. - setattr( # noqa: B010 — dynamic attribute attach on HttpRequest + setattr( # noqa: B010 # dynamic attribute attach on HttpRequest request, "_agentscore_gate", { @@ -213,7 +213,7 @@ def __call__(self, request: HttpRequest) -> Any: if recovered is not None: signer_payload = {"address": recovered.address, "network": recovered.network} - # Only check_identity is wrapped — get_response (which runs the downstream view) must + # Only check_identity is wrapped: get_response (which runs the downstream view) must # NOT be in the try, otherwise an exception in the user's view would be misclassified # as an AgentScore infra failure and (under fail_open) re-invoke their view. try: @@ -226,7 +226,7 @@ def __call__(self, request: HttpRequest) -> Any: reason = build_token_denied_reason(err) return self._on_denied(request, reason) except InvalidCredentialError: - # Permanent — no auto-session, agent should switch tokens or restart. + # Permanent: no auto-session, agent should switch tokens or restart. return self._on_denied(request, build_invalid_credential_reason()) except QuotaExceededError: if self._client.fail_open: @@ -255,7 +255,7 @@ def __call__(self, request: HttpRequest) -> Any: _handle_state["operator_handle"] = self._client.project_operator_handle(result.raw) if result.allow: - setattr(request, "agentscore", result.raw) # noqa: B010 — dynamic attribute attach on HttpRequest + setattr(request, "agentscore", result.raw) # noqa: B010 # dynamic attribute attach on HttpRequest state = getattr(request, "_agentscore_gate", None) if isinstance(state, dict): if result.quota is not None: @@ -273,7 +273,7 @@ def __call__(self, request: HttpRequest) -> Any: # with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient, # jurisdiction_restricted) keep the bare wallet_not_trusted denial. # `jurisdiction_restricted` is unfixable: the API only emits it after KYC is - # verified (the user's KYC'd country is in the blocked list — re-doing KYC + # verified (the user's KYC'd country is in the blocked list: re-doing KYC # won't change the country). if is_fixable_denial(result.reasons) and self._create_session_on_missing is not None: session_reason = try_create_session_denial_reason_sync( @@ -300,7 +300,7 @@ def get_signer_verdict(request: HttpRequest) -> SignerVerdict | None: credential, or for fail-open pass-throughs (no assess call). Reads the request-scoped verdict stashed by the gate (projected from THIS request's - assess response) — concurrency-safe against a sibling same-wallet request. + assess response): concurrency-safe against a sibling same-wallet request. """ state = getattr(request, "_agentscore_gate", None) if not isinstance(state, dict): @@ -342,7 +342,7 @@ class ConditionalAgentScoreMiddleware(AgentScoreMiddleware): Discovery legs flow through; settle legs trigger the full gate. - Settings shape is identical to :class:`AgentScoreMiddleware` — the + Settings shape is identical to :class:`AgentScoreMiddleware`: the ``AGENTSCORE_GATE`` dict's ``condition`` key is overwritten with the payment-header check. @@ -359,7 +359,7 @@ def __init__(self, get_response: Any) -> None: # --------------------------------------------------------------------------- -# AIP gate (Agentic Identity Protocol) — verifies a key-bound Agent Identity Token (AIT) +# AIP gate (Agentic Identity Protocol): verifies a key-bound Agent Identity Token (AIT) # from a trusted IdP instead of an opaque operator token. Cryptographic identity only; # merchants who want compliance enrichment feed the verified claims to ``/v1/assess``. # Django (WSGI) has no request object the async verifier accepts directly, so the middleware @@ -474,7 +474,7 @@ class ConditionalAipGateMiddleware(AipGateMiddleware): Requests without an ``Agent-Identity`` header flow through unauthenticated; requests that carry one must pass full verification. Settings shape is identical to - :class:`AipGateMiddleware` — the ``AGENTSCORE_AIP_GATE`` dict's ``condition`` key is + :class:`AipGateMiddleware`: the ``AGENTSCORE_AIP_GATE`` dict's ``condition`` key is overwritten with the ``Agent-Identity`` header check. """ diff --git a/agentscore_commerce/identity/fastapi.py b/agentscore_commerce/identity/fastapi.py index 7449de7..7cf145d 100644 --- a/agentscore_commerce/identity/fastapi.py +++ b/agentscore_commerce/identity/fastapi.py @@ -70,8 +70,8 @@ class _GateDenialError(Exception): """Carries a pre-rendered denial document up to the Starlette exception handler. - Unlike ``HTTPException(detail=body)`` — which nests the document under a ``detail`` - key — this preserves the FLAT wire contract the node adapters emit (consumers read + Unlike ``HTTPException(detail=body)``: which nests the document under a ``detail`` + key: this preserves the FLAT wire contract the node adapters emit (consumers read ``body["type"]`` / ``body["error"]`` directly). The handler installed by :func:`_install_gate_denial_handler` renders it via ``JSONResponse``. """ @@ -136,7 +136,7 @@ def get_gate_degraded_state(request: Request) -> dict[str, Any]: Returns ``{"degraded": False}`` for normal allows; ``{"degraded": True, "infra_reason": "quota_exceeded" | "api_error" | "network_timeout"}`` when the gate - was bypassed (compliance NOT enforced — log/alert). + was bypassed (compliance NOT enforced: log/alert). Only set when ``fail_open=True`` was configured AND the failure was an infra failure. Real compliance denials never trigger fail-open and so never set this flag. @@ -202,7 +202,7 @@ class AgentScoreGate: """FastAPI dependency that gates a route on AgentScore trust. Instantiate once at module scope, then attach to routes via ``Depends(gate)``. - Uses FastAPI's dependency-injection system — on a denial the dependency raises an + Uses FastAPI's dependency-injection system: on a denial the dependency raises an internal exception that an auto-registered Starlette handler renders as a FLAT denial document (``body["error"]["code"]``, not nested under ``detail``), matching the node adapters' cross-framework wire contract; the route body is skipped. @@ -323,7 +323,7 @@ async def __call__(self, request: Request) -> None: except TokenDeniedError as err: self._deny(request, build_token_denied_reason(err)) except InvalidCredentialError: - # Permanent — no auto-session, agent should switch tokens or restart. + # Permanent: no auto-session, agent should switch tokens or restart. self._deny(request, build_invalid_credential_reason()) except QuotaExceededError: if self._client.fail_open: @@ -368,7 +368,7 @@ async def __call__(self, request: Request) -> None: # agent polls until status=verified, gets a fresh opc_..., and retries with # X-Operator-Token. No "go to verify_url and tell us when done" gap. # Unfixable reasons (sanctions_flagged, age_insufficient, jurisdiction_restricted) - # keep the bare wallet_not_trusted denial — re-verification won't fix them. + # keep the bare wallet_not_trusted denial: re-verification won't fix them. # `jurisdiction_restricted` is unfixable because the API only emits it AFTER KYC # is verified (the user's KYC'd country is in the blocked list). if is_fixable_denial(result.reasons) and self._create_session_on_missing is not None: @@ -462,7 +462,7 @@ class ConditionalAgentScoreGate: ``Authorization: Payment``) flow through to the handler unauthenticated; settle legs trigger the full gate. - Use this for routes that should support anonymous discovery — the 402 + Use this for routes that should support anonymous discovery: the 402 emit path advertises all rails to any x402 wallet, and identity is verified at settle time on the retry leg. @@ -491,7 +491,7 @@ async def __call__(self, request: Request) -> None: # --------------------------------------------------------------------------- -# AIP gate (Agentic Identity Protocol) — verifies a key-bound Agent Identity Token (AIT) +# AIP gate (Agentic Identity Protocol): verifies a key-bound Agent Identity Token (AIT) # from a trusted IdP instead of an opaque operator token. Cryptographic identity only; # merchants who want compliance enrichment feed the verified claims to ``/v1/assess``. # Starlette's ``Request`` already satisfies ``RequestLike`` (method / url / headers), so the diff --git a/agentscore_commerce/identity/flask.py b/agentscore_commerce/identity/flask.py index 3da90ee..b895711 100644 --- a/agentscore_commerce/identity/flask.py +++ b/agentscore_commerce/identity/flask.py @@ -297,7 +297,7 @@ def _agentscore_check() -> Response | tuple[Response, int] | None: # with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient, # jurisdiction_restricted) keep the bare wallet_not_trusted denial. # `jurisdiction_restricted` is unfixable: the API only emits it after KYC is - # verified (the user's KYC'd country is in the blocked list — re-doing KYC + # verified (the user's KYC'd country is in the blocked list: re-doing KYC # won't change the country). if is_fixable_denial(result.reasons) and create_session_on_missing is not None: session_reason = try_create_session_denial_reason_sync( @@ -323,7 +323,7 @@ def _agentscore_check() -> Response | tuple[Response, int] | None: except TokenDeniedError as err: return _deny(build_token_denied_reason(err)) except InvalidCredentialError: - # Permanent — no auto-session, agent should switch tokens or restart. + # Permanent: no auto-session, agent should switch tokens or restart. return _deny(build_invalid_credential_reason()) except QuotaExceededError: if client.fail_open: @@ -352,7 +352,7 @@ def get_signer_verdict() -> SignerVerdict | None: assess call). See :class:`SignerVerdict` for the verdict shape. Reads the request-scoped verdict stashed by the gate (projected from THIS request's - assess response) — concurrency-safe against a sibling same-wallet request. + assess response): concurrency-safe against a sibling same-wallet request. """ from flask import g @@ -372,7 +372,7 @@ def capture_wallet( ) -> None: """Report a wallet that paid under the operator_token the Flask gate extracted on this request. - Reads gate state from Flask's ``g`` object — must be called inside a request context after + Reads gate state from Flask's ``g`` object: must be called inside a request context after the gate's before_request handler ran. Fire-and-forget: no-ops silently if the request was wallet-authenticated (no operator_token) or the API call fails. @@ -386,7 +386,7 @@ def purchase(): """ from flask import g - # Accessing `g` outside a request context raises RuntimeError — treat as no-op so background + # Accessing `g` outside a request context raises RuntimeError: treat as no-op so background # threads/workers that mistakenly import this helper don't crash user code. try: state = getattr(g, "_agentscore_gate", None) @@ -423,7 +423,7 @@ def conditional_agentscore_gate(app: Flask, **kwargs: Any) -> None: # --------------------------------------------------------------------------- -# AIP gate (Agentic Identity Protocol) — verifies a key-bound Agent Identity Token (AIT) +# AIP gate (Agentic Identity Protocol): verifies a key-bound Agent Identity Token (AIT) # from a trusted IdP instead of an opaque operator token. Cryptographic identity only; # merchants who want compliance enrichment feed the verified claims to ``/v1/assess``. # Flask is WSGI (no request object the async verifier accepts directly), so the gate builds diff --git a/agentscore_commerce/identity/middleware.py b/agentscore_commerce/identity/middleware.py index 61d0228..849654e 100644 --- a/agentscore_commerce/identity/middleware.py +++ b/agentscore_commerce/identity/middleware.py @@ -246,7 +246,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: if recovered is not None: signer_payload = {"address": recovered.address, "network": recovered.network} - # Only acheck_identity is wrapped — `await self.app(...)` (which runs the downstream + # Only acheck_identity is wrapped: `await self.app(...)` (which runs the downstream # ASGI app) must NOT be in the try, otherwise an exception in the user's app would # be misclassified as an AgentScore infra failure and (under fail_open) re-invoke it. try: @@ -265,7 +265,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: await response(scope, receive, send) return except InvalidCredentialError: - # Permanent — no auto-session, agent should switch tokens or restart. + # Permanent: no auto-session, agent should switch tokens or restart. reason = build_invalid_credential_reason() response = await self._on_denied(request, reason) await response(scope, receive, send) @@ -325,7 +325,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: # with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient, # jurisdiction_restricted) keep the bare wallet_not_trusted denial. # `jurisdiction_restricted` is unfixable: the API only emits it after KYC is - # verified (the user's KYC'd country is in the blocked list — re-doing KYC + # verified (the user's KYC'd country is in the blocked list: re-doing KYC # won't change the country). if is_fixable_denial(result.reasons) and self._create_session_on_missing is not None: session_reason = await try_create_session_denial_reason( @@ -352,19 +352,19 @@ def get_signer_verdict(request: Request) -> SignerVerdict | None: """Synchronous read of the cached signer verdicts for the current request. Both ``signer_match`` (wallet-binding) and ``signer_sanctions`` (OFAC SDN wallet check) - are composed by the gate's primary ``/v1/assess`` call on this request — single round trip. + are composed by the gate's primary ``/v1/assess`` call on this request: single round trip. This getter projects them off the gate's cache; no extra HTTP call. Returns ``None`` when the gate didn't run with a signer: operator-token-only paths, discovery legs that arrive without a payment credential, and fail-open pass-throughs. - Wallet-OFAC SDN enforcement is unconditional whenever a signer is in the request — + Wallet-OFAC SDN enforcement is unconditional whenever a signer is in the request: SDN wallet-address hits already flip the gate to ``decision=deny`` before the handler runs. Merchant code typically only reads ``signer_match`` for the wallet-binding verdict (e.g. via :func:`build_signer_mismatch_body`). Reads the request-scoped verdict stashed by the gate (projected from THIS request's - assess response) — concurrency-safe against a sibling same-wallet request. + assess response): concurrency-safe against a sibling same-wallet request. """ state = request.scope.get("state", {}).get(GATE_STATE_KEY) if not isinstance(state, dict): @@ -425,7 +425,7 @@ def __init__(self, app: Any, **kwargs: Any) -> None: # --------------------------------------------------------------------------- -# AIP gate (Agentic Identity Protocol) — verifies a key-bound Agent Identity Token (AIT) +# AIP gate (Agentic Identity Protocol): verifies a key-bound Agent Identity Token (AIT) # from a trusted IdP instead of an opaque operator token. Cryptographic identity only; # merchants who want compliance enrichment feed the verified claims to ``/v1/assess``. # Starlette's ``Request`` satisfies ``RequestLike``, so this ASGI middleware verifies diff --git a/agentscore_commerce/identity/policy.py b/agentscore_commerce/identity/policy.py index 096fd43..a64c39a 100644 --- a/agentscore_commerce/identity/policy.py +++ b/agentscore_commerce/identity/policy.py @@ -3,7 +3,7 @@ A *policy* is a small bag of fields describing what identity the merchant wants verified for a given resource: -- ``enforcement``: ``"hard"`` (the regulated-goods path — 403 on miss) or ``"soft"`` +- ``enforcement``: ``"hard"`` (the regulated-goods path: 403 on miss) or ``"soft"`` (gate denial is swallowed; the order completes with a degraded ``identity_status``). ``None`` = no gate at all. - ``require_kyc`` / ``require_sanctions_clear`` / ``min_age``: passed through @@ -14,13 +14,13 @@ This module ships three primitives: -1. :class:`PolicyBlock` — the typed shape. -2. :func:`build_gate_from_policy` — translate a block into an +1. :class:`PolicyBlock`: the typed shape. +2. :func:`build_gate_from_policy`: translate a block into an :class:`AgentScoreGate`. -3. :func:`run_gate_with_enforcement` — run the gate, swallow soft denials, +3. :func:`run_gate_with_enforcement`: run the gate, swallow soft denials, return a structured :class:`GateResult`. -All three are additive — vendors that don't need per-product policy can keep +All three are additive: vendors that don't need per-product policy can keep using ``AgentScoreGate(...)`` directly. Most merchants will implement shipping checks adjacent to the gate per-request. """ @@ -81,7 +81,7 @@ class GateResult: # OFAC SDN denial reasons. These are strict-liability: soft enforcement may downgrade # KYC / age / jurisdiction misses (the merchant accepts the order with a degraded -# identity_status), but it must NEVER swallow a sanctions deny — falsely settling for a +# identity_status), but it must NEVER swallow a sanctions deny: falsely settling for a # sanctioned wallet is an OFAC violation regardless of the merchant's soft posture. The API # emits `sanctions_flagged` in `decision_reasons` for BOTH the operator/wallet SDN hit and # the payment-signer OFAC SDN hit; `sanctions_check_unavailable` is the fail-closed @@ -126,7 +126,7 @@ def build_gate_from_policy( """Build a per-request :class:`AgentScoreGate` from a :class:`PolicyBlock`-shaped mapping. Returns ``None`` when ``policy`` is None, missing ``enforcement``, or has - ``enforcement=None`` — the caller should treat that as "no gate; anonymous OK". + ``enforcement=None``: the caller should treat that as "no gate; anonymous OK". Use a fresh gate per request rather than constructing once at module scope when policy varies per resource (e.g. per product). The gate is cheap to @@ -137,7 +137,7 @@ def build_gate_from_policy( return None if not policy.get("enforcement"): return None - # Lazy import — avoids circular import at package init time + # Lazy import: avoids circular import at package init time # (identity package init pulls policy → fastapi → payment.signer → identity). from agentscore_commerce.identity.fastapi import AgentScoreGate @@ -169,7 +169,7 @@ async def run_gate_with_enforcement( - ``enforcement="soft"`` + gate denies: swallow the denial; status="unverified". - gate accepts: status="verified". - **Sanctions are never swallowed.** Soft mode is a commercial knob — it lets a merchant + **Sanctions are never swallowed.** Soft mode is a commercial knob: it lets a merchant accept an order from an agent that didn't satisfy KYC / age / jurisdiction (stamping a degraded ``identity_status`` for ops). But an OFAC SDN sanctions deny is strict-liability: settling for a sanctioned wallet is a violation regardless of the merchant's posture. So a @@ -191,7 +191,7 @@ async def run_gate_with_enforcement( except _GateDenialError as exc: # Post-flatten the gate raises a FLAT _GateDenialError (not HTTPException). # Convert it to a GateResult so soft mode can swallow the denial and hard mode - # can propagate the flat body — same contract as the HTTPException path below. + # can propagate the flat body: same contract as the HTTPException path below. # A sanctions deny stays terminal in BOTH modes (see _is_sanctions_denial). if enforcement == "hard" or _is_sanctions_denial(exc.body): return GateResult(status="denied", denial_status=exc.status, denial_body=exc.body) @@ -217,7 +217,7 @@ def shipping_country_allowed(country: str, policy: Mapping[str, Any] | None) -> def shipping_state_allowed(state: str, country: str, policy: Mapping[str, Any] | None) -> bool: """US-state allowlist (e.g. wine). - Only enforced for US shipments — non-US is governed by + Only enforced for US shipments: non-US is governed by ``shipping_country_allowed`` independently. """ if policy is None: @@ -249,7 +249,7 @@ def validate_shipping_against_policy( policy means "ship anywhere" and the function is a no-op. The reason a location is excluded is **merchant-defined**: it might be regulatory (regulated goods + state allowlist), operational (no fulfillment partner), - or commercial (fragility, fraud-rate-by-region, etc.) — the helper + or commercial (fragility, fraud-rate-by-region, etc.): the helper doesn't assume. ``product_name`` is the user-facing item name surfaced in the error @@ -260,7 +260,7 @@ def validate_shipping_against_policy( ``country_message`` / ``state_message`` override the default messages verbatim (use these when the default phrasing isn't right for your - consumer agents — e.g. you want to surface the regulatory reason + consumer agents: e.g. you want to surface the regulatory reason explicitly, or you want the message in a different language). """ item = f"'{product_name}'" if product_name else "this item" diff --git a/agentscore_commerce/identity/sanic.py b/agentscore_commerce/identity/sanic.py index c90b651..656d80c 100644 --- a/agentscore_commerce/identity/sanic.py +++ b/agentscore_commerce/identity/sanic.py @@ -265,7 +265,7 @@ async def _agentscore_check(request: Request) -> HTTPResponse | None: # with X-Operator-Token. Unfixable reasons (sanctions_flagged, age_insufficient, # jurisdiction_restricted) keep the bare wallet_not_trusted denial. # `jurisdiction_restricted` is unfixable: the API only emits it after KYC is - # verified (the user's KYC'd country is in the blocked list — re-doing KYC + # verified (the user's KYC'd country is in the blocked list: re-doing KYC # won't change the country). if is_fixable_denial(result.reasons) and create_session_on_missing is not None: session_reason = await try_create_session_denial_reason( @@ -292,7 +292,7 @@ async def _agentscore_check(request: Request) -> HTTPResponse | None: except TokenDeniedError as err: return _deny_response(request, build_token_denied_reason(err)) except InvalidCredentialError: - # Permanent — no auto-session, agent should switch tokens or restart. + # Permanent: no auto-session, agent should switch tokens or restart. return _deny_response(request, build_invalid_credential_reason()) except QuotaExceededError: if client.fail_open: @@ -321,7 +321,7 @@ def get_signer_verdict(request: Request) -> SignerVerdict | None: credential, or for fail-open pass-throughs (no assess call). Reads the request-scoped verdict stashed by the gate (projected from THIS request's - assess response) — concurrency-safe against a sibling same-wallet request. + assess response): concurrency-safe against a sibling same-wallet request. """ state = getattr(request.ctx, GATE_STATE_ATTR, None) if not isinstance(state, dict): @@ -376,7 +376,7 @@ def conditional_agentscore_gate(app: Sanic, **kwargs: Any) -> None: # --------------------------------------------------------------------------- -# AIP gate (Agentic Identity Protocol) — verifies a key-bound Agent Identity Token (AIT) +# AIP gate (Agentic Identity Protocol): verifies a key-bound Agent Identity Token (AIT) # from a trusted IdP instead of an opaque operator token. Cryptographic identity only; # merchants who want compliance enrichment feed the verified claims to ``/v1/assess``. # Sanic's ``Request`` exposes method / url / headers, so this verifies straight off the diff --git a/agentscore_commerce/identity/sessions.py b/agentscore_commerce/identity/sessions.py index 28d5a63..5f484c1 100644 --- a/agentscore_commerce/identity/sessions.py +++ b/agentscore_commerce/identity/sessions.py @@ -38,7 +38,7 @@ class CreateSessionOnMissing: merged into ``DenialReason.extra`` so custom ``on_denied`` handlers can include merchant-specific fields (e.g. ``order_id``) in the 403 response. - Both hooks can be sync or ``async def``. Hook errors are logged and swallowed — a + Both hooks can be sync or ``async def``. Hook errors are logged and swallowed: a failing side effect should not block the 403 from reaching the agent. """ @@ -117,14 +117,14 @@ def _session_denial_reason( ) -> DenialReason | None: # Validate required fields before trusting the response. A misbehaving (or # mocked-wrong) API could 200 without session_id/poll_secret/verify_url, which - # would propagate None into the 403 body and leave the agent stuck — treat that + # would propagate None into the 403 body and leave the agent stuck: treat that # as a session-create failure and let the caller fall back to missing_identity. if not ( isinstance(data.get("session_id"), str) and isinstance(data.get("poll_secret"), str) and isinstance(data.get("verify_url"), str) ): - logger.warning("/v1/sessions returned 200 without required fields — treating as failure") + logger.warning("/v1/sessions returned 200 without required fields: treating as failure") return None # The API emits structured ``next_steps`` on /v1/sessions success. Stringify it into # the gate's ``agent_instructions`` contract so every denial body surfaces the same @@ -164,7 +164,7 @@ async def try_create_session_denial_reason( """Hit ``POST /v1/sessions`` and return a populated DenialReason, or None on failure. Async variant. Invokes ``cfg.get_session_options(ctx)`` and ``cfg.on_before_session(ctx, session)`` - if set — both may be sync or async. + if set: both may be sync or async. """ try: dynamic: Any = None @@ -203,7 +203,7 @@ def try_create_session_denial_reason_sync( ) -> DenialReason | None: """Synchronous variant of :func:`try_create_session_denial_reason` for Flask/Django. - Hook callables MUST be sync (not ``async def``) — sync code can't await. If an + Hook callables MUST be sync (not ``async def``): sync code can't await. If an async hook is passed in a sync adapter config, it's skipped with a warning. """ try: @@ -212,7 +212,7 @@ def try_create_session_denial_reason_sync( try: hook_dynamic = cfg.get_session_options(ctx) if inspect.iscoroutine(hook_dynamic): - logger.warning("get_session_options returned a coroutine in a sync adapter — skipping") + logger.warning("get_session_options returned a coroutine in a sync adapter: skipping") hook_dynamic.close() else: dynamic = hook_dynamic @@ -232,7 +232,7 @@ def try_create_session_denial_reason_sync( try: hook_result: Any = cfg.on_before_session(ctx, _session_metadata(data)) if inspect.iscoroutine(hook_result): - logger.warning("on_before_session returned a coroutine in a sync adapter — skipping") + logger.warning("on_before_session returned a coroutine in a sync adapter: skipping") hook_result.close() elif isinstance(hook_result, dict): extra = cast("dict[str, Any]", hook_result) diff --git a/agentscore_commerce/identity/signer.py b/agentscore_commerce/identity/signer.py index 3e7b5ac..9e806ac 100644 --- a/agentscore_commerce/identity/signer.py +++ b/agentscore_commerce/identity/signer.py @@ -26,7 +26,7 @@ def extract_x402_signer(x402_payment_header: str | None) -> str | None: """Decode an x402 ``payment-signature`` / ``x-payment`` header and return the signer. - Currently extracts EVM (EIP-3009) signers only — see module docstring for why + Currently extracts EVM (EIP-3009) signers only: see module docstring for why Solana extraction is left to callers. Returns ``None`` when the header is missing, malformed, or the rail isn't EVM x402. """ diff --git a/agentscore_commerce/identity/types.py b/agentscore_commerce/identity/types.py index 1d0c18a..7aca570 100644 --- a/agentscore_commerce/identity/types.py +++ b/agentscore_commerce/identity/types.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Any, Literal from agentscore import Network as Network -from agentscore.types import SignerSanctions as SignerSanctions # noqa: TC002 — runtime re-export for vendors +from agentscore.types import SignerSanctions as SignerSanctions # noqa: TC002 # runtime re-export for vendors if TYPE_CHECKING: # AIP types live in the agentscore-py SDK. Type-only import (mirrors how the SDK owns the @@ -22,12 +22,12 @@ # the payment signer; wallet-auth is rejected on rails with no wallet signer. "wallet_signer_mismatch", "wallet_auth_requires_wallet_signing", - # Credential is no longer valid (revoked or TTL-expired — the two cases share this + # Credential is no longer valid (revoked or TTL-expired: the two cases share this # code deliberately; the API doesn't disclose which). The 401 body carries an # auto-minted session so the agent recovers without an API key. "token_expired", # The operator_token doesn't exist at all (typo, never minted, fabricated). Distinct - # from token_expired — no auto-session is issued because the agent may have a + # from token_expired: no auto-session is issued because the agent may have a # different valid token to try first. Agents should switch tokens or drop the header # to bootstrap a fresh session. "invalid_credential", @@ -36,7 +36,7 @@ @dataclass class AgentIdentity: - """Identity of an agent — wallet address, operator token, or AIP Agent Identity Token.""" + """Identity of an agent: wallet address, operator token, or AIP Agent Identity Token.""" address: str | None = None operator_token: str | None = None @@ -66,7 +66,7 @@ class AgentMemoryHint: quickstart: str identity_check_endpoint: str # ``identity_paths`` may carry an optional ``agent_identity`` key (the AIP path) when the - # merchant opted into AIP — emitted by ``build_agent_memory_hint`` alongside wallet / operator_token. + # merchant opted into AIP: emitted by ``build_agent_memory_hint`` alongside wallet / operator_token. identity_paths: dict[str, str] bootstrap: str do_not_persist_in_memory: list[str] @@ -144,7 +144,7 @@ class SignerVerdict: ``/v1/assess`` call (single round trip). ``signer_match`` describes the wallet- binding; ``signer_sanctions`` describes the OFAC SDN wallet-address check. - Wallet-OFAC SDN enforcement is unconditional whenever a signer is in the request — + Wallet-OFAC SDN enforcement is unconditional whenever a signer is in the request: a ``signer_sanctions`` hit OR an unavailable lookup already flips ``decision -> deny`` inside the gate before the handler runs. Merchant code typically only needs to read ``signer_match`` here for the wallet-binding verdict. @@ -162,7 +162,7 @@ class SignerVerdict: def build_agent_memory_hint(aip_trusted_issuers: list[str] | None = None) -> AgentMemoryHint: """Build the cross-merchant memory hint emitted on bootstrap denials. - Memory pointers always point at the canonical production API — merchants must not be + Memory pointers always point at the canonical production API: merchants must not be able to redirect the cross-merchant memory surface, since that would be a phishing vector for the verify URL. @@ -188,7 +188,7 @@ def build_agent_memory_hint(aip_trusted_issuers: list[str] | None = None) -> Age "present it: send the JWT in an `Agent-Identity` header plus an RFC 9421 HTTP Message " "Signature (Signature-Input + Signature over @method @authority @path agent-identity, " 'tag="agent-identity") signed with the token-bound cnf key. The merchant verifies it and ' - "applies its policy — no separate AgentScore credential needed." + "applies its policy: no separate AgentScore credential needed." ) return AgentMemoryHint( save_for_future_agentscore_gates=True, @@ -196,7 +196,7 @@ def build_agent_memory_hint(aip_trusted_issuers: list[str] | None = None) -> Age "AgentScore is a cross-merchant identity substrate for agentic commerce. Any endpoint that " "returns 403 missing_identity / identity_verification_required or 402 challenges " "referencing AgentScore uses " - "the same identity system. One KYC works across all AgentScore merchants — no re-verification " + "the same identity system. One KYC works across all AgentScore merchants: no re-verification " "per site. Save this pattern (and the endpoints below) to persistent memory so future " "AgentScore encounters skip the cold-start session flow." ), @@ -206,7 +206,7 @@ def build_agent_memory_hint(aip_trusted_issuers: list[str] | None = None) -> Age bootstrap=( "If you have neither a linked wallet nor a valid operator_token, follow the session/verify " "flow in the per-request `agent_instructions` block. This happens at most once per agent " - "identity — after first verification, the operator_token and any subsequently used wallet " + "identity: after first verification, the operator_token and any subsequently used wallet " "are reusable everywhere." ), do_not_persist_in_memory=["operator_token", "poll_secret"], @@ -251,7 +251,7 @@ def apply_degraded(state: dict[str, Any] | None, infra_reason: FailOpenInfraReas Per-adapter helpers resolve the state container in the framework's request-scoped store (``request.state`` on FastAPI, ``g`` on Flask, attribute on Django, mapping on aiohttp, ``request.ctx`` on Sanic, ``scope["state"]`` on ASGI) and hand that dict here. Keeps the - contract — `degraded: True` + `infra_reason` — in one place across all 6 adapters. + contract (`degraded: True` + `infra_reason`) in one place across all 6 adapters. """ if isinstance(state, dict): state["degraded"] = True @@ -289,7 +289,7 @@ class AssessResult: resolved_operator: str | None = None verify_url: str | None = None policy_result: PolicyResult | None = None - # IdP provenance, present only when ``identity_method == "aip_token"`` — which issuer + # IdP provenance, present only when ``identity_method == "aip_token"``: which issuer # attested the identity and the trust level it asserted. Surfaced as the SDK's raw # ``aip`` block (issuer/subject/trust_level/agent_provider/pop_verified); mirrors the # SDK's ``AssessResponse.aip``. diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index d63b045..a052ca5 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -156,7 +156,7 @@ def from_jwk(cls, jwk: dict[str, Any]) -> UCPSigningKey: @dataclass class UCPServiceBinding: - """Transport binding entry — keyed under a service name (e.g., ``dev.ucp.shopping``).""" + """Transport binding entry: keyed under a service name (e.g., ``dev.ucp.shopping``).""" version: str spec: str @@ -203,7 +203,7 @@ def to_dict(self) -> dict[str, Any]: @dataclass class UCPCapabilityBinding: - """Capability binding entry — keyed under a capability name (e.g., ``dev.ucp.shopping.checkout``).""" + """Capability binding entry: keyed under a capability name (e.g., ``dev.ucp.shopping.checkout``).""" version: str spec: str @@ -240,7 +240,7 @@ def to_dict(self) -> dict[str, Any]: @dataclass class UCPPaymentHandlerBinding: - """Payment handler binding entry — keyed under a handler reverse-DNS name (e.g., ``com.google.pay``).""" + """Payment handler binding entry: keyed under a handler reverse-DNS name (e.g., ``com.google.pay``).""" id: str version: str @@ -276,7 +276,7 @@ def to_dict(self) -> dict[str, Any]: @dataclass class UCPProfileBody: - """UCP body — nested under the ``ucp`` key of the published profile.""" + """UCP body: nested under the ``ucp`` key of the published profile.""" version: str = _DEFAULT_VERSION services: dict[str, list[UCPServiceBinding]] = field(default_factory=dict) @@ -380,7 +380,7 @@ def build_ucp_profile( ``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 + data is ever placed on the public profile: per-operator identity attestation flows through the AP2 risk-signal endpoint, not here. Example:: @@ -429,7 +429,7 @@ async def ucp_profile(): } # Auto-inject `com.agentscore.identity` capability when the merchant declares a gate - # policy. Static merchant-policy declaration only — no per-operator data on the public + # 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 @@ -486,7 +486,7 @@ async def ucp_profile(): # CAIP-2 → UCP-namespace network-name mapping. UCP payment_handler bindings publish # network strings in the UCP namespace ("base-8453", "solana-mainnet-beta"); RailSpecs # carry the CAIP-2 form internally ("eip155:8453", "solana:5eykt4..."). Unknown values -# pass through verbatim — vendors who pin a non-standard rail can override the spec's +# pass through verbatim: vendors who pin a non-standard rail can override the spec's # network field directly. _CAIP2_TO_UCP_NETWORK = { networks.base.mainnet.caip2: "base-8453", diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index a6070d7..f2198f5 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -7,10 +7,10 @@ This module provides: -* :func:`generate_ucp_signing_key` — generate an Ed25519 (or ES256) keypair -* :func:`sign_ucp_profile` — sign a profile, returning a JWS-attached envelope -* :func:`verify_ucp_profile` — verify a signed profile against a JWKS -* :func:`build_jwks_response` — assemble a JWKS document for ``/.well-known/jwks.json`` +* :func:`generate_ucp_signing_key`: generate an Ed25519 (or ES256) keypair +* :func:`sign_ucp_profile`: sign a profile, returning a JWS-attached envelope +* :func:`verify_ucp_profile`: verify a signed profile against a JWKS +* :func:`build_jwks_response`: assemble a JWKS document for ``/.well-known/jwks.json`` Implementation rides on ``joserfc`` (optional extra). Install via ``pip install agentscore-commerce[ucp]``. Merchants who don't sign their profile @@ -116,9 +116,9 @@ def _load_joserfc() -> Any: class GeneratedUCPKey: """Output of :func:`generate_ucp_signing_key`. - * ``private_key`` is the joserfc Key object — pass to :func:`sign_ucp_profile`. + * ``private_key`` is the joserfc Key object: pass to :func:`sign_ucp_profile`. Never publish. - * ``public_jwk`` is the JWK dict — publish at ``/.well-known/jwks.json`` and + * ``public_jwk`` is the JWK dict: publish at ``/.well-known/jwks.json`` and inline in the UCP profile's ``keys[]``. """ @@ -129,7 +129,7 @@ class GeneratedUCPKey: def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDSA") -> GeneratedUCPKey: """Generate an Ed25519 (default) or ES256 keypair for signing UCP profiles. - The ``private_key`` is a joserfc ``Key`` — store it securely (env var, KMS, secret + The ``private_key`` is a joserfc ``Key``: store it securely (env var, KMS, secret manager) and pass to :func:`sign_ucp_profile`. The ``public_jwk`` is a dict you publish at ``/.well-known/jwks.json`` and inline @@ -140,8 +140,8 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS from agentscore_commerce.identity.ucp_jwks import generate_ucp_signing_key key = generate_ucp_signing_key(kid='merchant-2026-05') - # key.private_key — persist securely - # key.public_jwk — publish at /.well-known/jwks.json + # key.private_key: persist securely + # key.public_jwk : publish at /.well-known/jwks.json """ _load_joserfc() @@ -277,7 +277,7 @@ def sign_ucp_profile( ``kid``, and validate. The profile's ``keys[]`` MUST already include a JWK with the matching - ``kid`` — otherwise verifiers can't find the public key. + ``kid``: otherwise verifiers can't find the public key. Example:: @@ -391,7 +391,7 @@ def verify_ucp_profile( f"UCP `signature` must be a string; got {type(sig).__name__}.", ) - # Pre-deserialize header checks — joserfc's deserialize_compact accepts kid-less + # Pre-deserialize header checks: joserfc's deserialize_compact accepts kid-less # JWSs (it iterates the KeySet) so we enforce kid/typ/alg ourselves. header = _peek_jws_header(sig) if header.get("typ") != _PROFILE_TYP: @@ -502,7 +502,7 @@ def verify_ucp_profile( # Compare the bytes that were actually signed against the canonical body of the # profile we received. ``deserialize_compact`` validates the JWS against the bytes - # embedded in the JWS payload segment — but the profile body could have been + # embedded in the JWS payload segment: but the profile body could have been # swapped after signing while the JWS stayed unchanged. if not hmac.compare_digest(obj.payload, expected_payload): raise UCPVerificationError( @@ -629,7 +629,7 @@ def _build_env_signing_key( "x": raw["x"], "y": raw["y"], } - # Empty-string kid in env JWK falls through to the configured default — + # Empty-string kid in env JWK falls through to the configured default: # publishing `"kid": ""` would break every kid-pinning verifier. public_jwk["kid"] = jwk_dict.get("kid") or kid_default public_jwk["alg"] = detected_alg diff --git a/agentscore_commerce/middleware/asgi.py b/agentscore_commerce/middleware/asgi.py index 6bef59f..977cf8c 100644 --- a/agentscore_commerce/middleware/asgi.py +++ b/agentscore_commerce/middleware/asgi.py @@ -25,11 +25,11 @@ class RateLimitMiddleware: """ASGI rate-limit middleware (60 req / 60 s / IP by default). Constructor args (all keyword): - * ``window_seconds`` — bucket size in seconds (default 60). - * ``max_requests`` — max requests per bucket (default 60). - * ``key_resolver`` — ``(scope) -> str`` override. Default = first hop of ``x-forwarded-for``. - * ``redis_url`` — when set, lazy-imports ``redis.asyncio``; otherwise in-memory. - * ``key_prefix`` — Redis key prefix (default ``'rl:'``). + * ``window_seconds``: bucket size in seconds (default 60). + * ``max_requests``: max requests per bucket (default 60). + * ``key_resolver``: ``(scope) -> str`` override. Default = first hop of ``x-forwarded-for``. + * ``redis_url``: when set, lazy-imports ``redis.asyncio``; otherwise in-memory. + * ``key_prefix``: Redis key prefix (default ``'rl:'``). """ def __init__( diff --git a/agentscore_commerce/payment/__init__.py b/agentscore_commerce/payment/__init__.py index 0c427f0..60aee73 100644 --- a/agentscore_commerce/payment/__init__.py +++ b/agentscore_commerce/payment/__init__.py @@ -1,4 +1,4 @@ -"""Payment helpers — networks/usdc/rails registries, paymentauth.org directive builders, dispatch, headers.""" +"""Payment helpers: networks/usdc/rails registries, paymentauth.org directive builders, dispatch, headers.""" from agentscore_commerce.payment.amounts import format_usd_cents, usd_to_atomic from agentscore_commerce.payment.compose_rails import build_mppx_compose_rails diff --git a/agentscore_commerce/payment/amounts.py b/agentscore_commerce/payment/amounts.py index 240d021..c47b685 100644 --- a/agentscore_commerce/payment/amounts.py +++ b/agentscore_commerce/payment/amounts.py @@ -65,7 +65,7 @@ def format_usd_cents(cents: float, decimals: int = 2) -> str: agent-side string-comparison flakiness. ``decimals`` controls dollar-precision and defaults to ``2`` (canonical USD - cents). Raise it for sub-cent unit pricing — e.g. ``format_usd_cents(0.05, 4)`` + cents). Raise it for sub-cent unit pricing: e.g. ``format_usd_cents(0.05, 4)`` returns ``"0.0005"`` for a half-of-one-millicent amount. ``cents`` accepts a float so per-token / per-byte pricing models can compute ``price_cents = unit_price_cents * n`` without rounding before formatting. diff --git a/agentscore_commerce/payment/compose_rails.py b/agentscore_commerce/payment/compose_rails.py index ba12fb3..43ae4e7 100644 --- a/agentscore_commerce/payment/compose_rails.py +++ b/agentscore_commerce/payment/compose_rails.py @@ -64,7 +64,7 @@ def build_mppx_compose_rails( rail). Default ``True``. Stripe's documented USD minimum is $0.50 because the fixed - processing fee (~$0.30) exceeds revenue below that — sub-50-cent + processing fee (~$0.30) exceeds revenue below that: sub-50-cent charges that DO go through still cost the merchant money (a $0.11 PI nets -$0.19 after fees). Some Stripe accounts also reject PI creation under the floor with ``amount_too_small``. @@ -75,7 +75,7 @@ def build_mppx_compose_rails( Raises: ValueError: when Solana is requested but ``amount_usd`` can't convert - to atomic — merchants should catch and return a 402 to drop the rail + to atomic: merchants should catch and return a 402 to drop the rail rather than crash the request. """ rails: list[tuple[str, dict[str, Any]]] = [] diff --git a/agentscore_commerce/payment/default_rails.py b/agentscore_commerce/payment/default_rails.py index 84b2dcc..486e17b 100644 --- a/agentscore_commerce/payment/default_rails.py +++ b/agentscore_commerce/payment/default_rails.py @@ -7,7 +7,7 @@ Per-order recipient minting (Stripe-multichain) is wired via Checkout's ``mint_recipients`` hook, so the ``recipient=""`` sentinel here is the -expected shape — ``mint_recipients`` overrides it at request time. +expected shape: ``mint_recipients`` overrides it at request time. """ from __future__ import annotations @@ -33,7 +33,7 @@ def build_default_checkout_rails( Keys match the convention used across consumer codebases: ``tempo``, ``x402_base``, ``solana_mpp``, ``stripe``. Empty-string ``recipient`` is a - placeholder — ``Checkout.mint_recipients`` must populate real values at + placeholder: ``Checkout.mint_recipients`` must populate real values at request time. Each kwarg accepts a partial dict of rail-spec fields (matching the diff --git a/agentscore_commerce/payment/dispatch.py b/agentscore_commerce/payment/dispatch.py index 00904ef..d00a93b 100644 --- a/agentscore_commerce/payment/dispatch.py +++ b/agentscore_commerce/payment/dispatch.py @@ -1,8 +1,8 @@ """Payment dispatch helpers. -* :func:`detect_rail_from_headers` — detect which payment-protocol family +* :func:`detect_rail_from_headers`: detect which payment-protocol family (x402 vs MPP) the inbound request carries, based on header presence. -* :func:`dispatch_settlement_by_network` — route a settlement payload to +* :func:`dispatch_settlement_by_network`: route a settlement payload to evm vs svm handler based on the CAIP-2 network family in ``payload.accepted.network``. """ diff --git a/agentscore_commerce/payment/headers.py b/agentscore_commerce/payment/headers.py index 4e34f4b..ac365f4 100644 --- a/agentscore_commerce/payment/headers.py +++ b/agentscore_commerce/payment/headers.py @@ -5,7 +5,7 @@ Reduces ~10 lines of merchant boilerplate per 402 response. Layered on top of :func:`payment_directive` / :func:`www_authenticate_header` / -:func:`payment_required_header` — those primitives stay exposed for vendors who want +:func:`payment_required_header`: those primitives stay exposed for vendors who want full control. """ @@ -26,28 +26,28 @@ class PaymentHeadersRail: """One rail entry for :func:`build_payment_headers`.""" rail: str - """Symbolic rail name — ``tempo-mainnet``, ``x402-base-mainnet``, ``stripe``, etc.""" + """Symbolic rail name: ``tempo-mainnet``, ``x402-base-mainnet``, ``stripe``, etc.""" amount_usd: str | float """Amount in USD as a number or string.""" recipient: str | None = None - """Recipient address (on-chain) — required for crypto rails.""" + """Recipient address (on-chain): required for crypto rails.""" network_id: str | None = None - """Stripe profile_id / network_id — required for ``stripe`` rail.""" + """Stripe profile_id / network_id: required for ``stripe`` rail.""" chain_id: int | None = None - """EVM chain id override — usually inferred from rail.""" + """EVM chain id override: usually inferred from rail.""" currency: str | None = None - """Token contract / currency override — usually inferred from rail.""" + """Token contract / currency override: usually inferred from rail.""" decimals: int | None = None - """Decimal precision override — usually inferred from rail (USDC=6, etc.).""" + """Decimal precision override: usually inferred from rail (USDC=6, etc.).""" method: str | None = None - """MPP method override — usually inferred from rail.""" + """MPP method override: usually inferred from rail.""" intent: str | None = None """MPP intent. Default ``charge``.""" @@ -86,7 +86,7 @@ def build_payment_headers( ) -> PaymentHeadersResult: """Compose WWW-Authenticate + PAYMENT-REQUIRED headers from a single rails declaration. - Returns a dict with snake_case keys — callers map to actual HTTP header names:: + Returns a dict with snake_case keys: callers map to actual HTTP header names:: headers = build_payment_headers(...) response.headers["www-authenticate"] = headers["www_authenticate"] @@ -95,7 +95,7 @@ def build_payment_headers( ``order_id`` is used as the directive challenge id (per-rail it becomes ``"{order_id}-{rail}"``). ``realm`` is the host of the merchant URL (e.g. - ``agents.merchant.example``). ``x402`` is optional — pass an ``X402AcceptsBlock`` + ``agents.merchant.example``). ``x402`` is optional: pass an ``X402AcceptsBlock`` to include the standard PAYMENT-REQUIRED header so x402 clients (``x402[fastapi]``, ``agentscore-pay``) can parse the binary-friendly format. Omit to skip. diff --git a/agentscore_commerce/payment/idempotency.py b/agentscore_commerce/payment/idempotency.py index ae0a49c..672555f 100644 --- a/agentscore_commerce/payment/idempotency.py +++ b/agentscore_commerce/payment/idempotency.py @@ -6,7 +6,7 @@ Convention: 1. Prefer the upstream payment-rail's stable identifier (Stripe PaymentIntent id, x402 - tx hash) when one exists — those are already idempotent on their side. + tx hash) when one exists: those are already idempotent on their side. 2. Fall back to a synthesized ``pi-{order_id}-{amount_cents}`` key when no upstream id is available. 3. Server caps idempotency keys at 200 chars; this helper warns when that boundary is @@ -30,7 +30,7 @@ def build_idempotency_key( """Compose a stable idempotency key for AgentScore wallet capture and other retry-safe POSTs. Returns ``None`` when no inputs are present (caller should treat as "no idempotency - key — first attempt only", same shape as omitting the field entirely). + key: first attempt only", same shape as omitting the field entirely). Examples:: @@ -57,10 +57,10 @@ def _clamp_key(key: str) -> str: return key # Server truncates anyway; surfacing the warning here gives callers a chance to design # shorter inputs. We still return the original key (server-side truncation is the - # source of truth) — clamping client-side would change semantics for any caller already + # source of truth): clamping client-side would change semantics for any caller already # depending on the full string for their own dedup. _log.warning( - "[agentscore-commerce] idempotency key longer than %d chars — server will truncate, " + "[agentscore-commerce] idempotency key longer than %d chars: server will truncate, " "may cause silent collisions if multiple keys share the first %d chars.", _SERVER_IDEMPOTENCY_KEY_MAX, _SERVER_IDEMPOTENCY_KEY_MAX, diff --git a/agentscore_commerce/payment/mppx_server.py b/agentscore_commerce/payment/mppx_server.py index 93cba04..714e5dd 100644 --- a/agentscore_commerce/payment/mppx_server.py +++ b/agentscore_commerce/payment/mppx_server.py @@ -1,7 +1,7 @@ """One-call MPP server setup wrapping the official `pympp` Python package. Wires Tempo charge, Tempo session (channel-based for variable-cost / -streaming), and Stripe SPT methods from rail specs — replaces the boilerplate +streaming), and Stripe SPT methods from rail specs: replaces the boilerplate of constructing each method by hand. Usage:: @@ -26,7 +26,7 @@ Keys are rail names (``"tempo"``, ``"tempo_session"``, ``"stripe"``); values are the canonical ``*RailSpec`` instances every other helper also consumes. -`pympp` is an OPTIONAL peer dependency — install only if you accept MPP rails:: +`pympp` is an OPTIONAL peer dependency: install only if you accept MPP rails:: pip install 'pympp[server,tempo,stripe]>=0.6,<1' """ @@ -64,11 +64,11 @@ async def _tempo_method(spec: TempoRailSpec) -> Any: tempo_module = _import_optional("mpp.methods.tempo") tempo_factory = getattr(tempo_module, "tempo", None) if tempo_module else None if not callable(tempo_factory): - msg = "pympp[tempo] not installed — run `pip install 'pympp[tempo]'` for Tempo MPP rails." + msg = "pympp[tempo] not installed: run `pip install 'pympp[tempo]'` for Tempo MPP rails." raise ImportError(msg) charge_intent_cls = getattr(tempo_module, "ChargeIntent", None) if tempo_module else None if charge_intent_cls is None: - msg = "pympp[tempo] missing ChargeIntent — upgrade pympp to 0.6+." + msg = "pympp[tempo] missing ChargeIntent: upgrade pympp to 0.6+." raise ImportError(msg) default_currency = USDC.tempo.testnet.address if spec.testnet else USDC.tempo.mainnet.address chain_id = 42431 if spec.testnet else (spec.chain_id or 4217) @@ -109,7 +109,7 @@ async def create_mppx_server( ``rails`` keys are rail names (``"tempo"``, ``"tempo_session"``, ``"stripe"``); values are the canonical ``*RailSpec`` instances every other helper also consumes. Tempo session is reserved for future pympp ``SessionIntent`` - support — passing it today raises ``ImportError``. + support: passing it today raises ``ImportError``. pympp 0.6 takes a single ``method`` per ``Mpp`` instance. When ``rails`` is provided, the first resolvable rail in dict-insertion order wins; merchants @@ -118,7 +118,7 @@ async def create_mppx_server( """ pympp = _import_optional("mpp.server") if pympp is None or not hasattr(pympp, "Mpp"): - msg = "pympp not installed — run `pip install 'pympp[server,tempo,stripe]>=0.6,<1'` to use create_mppx_server." + msg = "pympp not installed: run `pip install 'pympp[server,tempo,stripe]>=0.6,<1'` to use create_mppx_server." raise ImportError(msg) resolved_method: Any = method @@ -131,7 +131,7 @@ async def create_mppx_server( break if isinstance(spec, TempoSessionRailSpec): msg = ( - "pympp[tempo] session support not available — pympp 0.6 has not " + "pympp[tempo] session support not available: pympp 0.6 has not " "shipped a SessionIntent factory yet. Upgrade pympp when it does " "or pass `method=` directly with a hand-built TempoMethod." ) @@ -144,7 +144,7 @@ async def create_mppx_server( if resolved_method is None: msg = ( - "create_mppx_server called with no method or rails — pass `method=` or a " + "create_mppx_server called with no method or rails: pass `method=` or a " "non-empty `rails={...}` map keyed by rail name (`tempo`, `tempo_session`, `stripe`)." ) raise ValueError(msg) diff --git a/agentscore_commerce/payment/network_kind.py b/agentscore_commerce/payment/network_kind.py index 68c8ecc..a69247b 100644 --- a/agentscore_commerce/payment/network_kind.py +++ b/agentscore_commerce/payment/network_kind.py @@ -29,6 +29,6 @@ def is_solana_network(value: Any) -> bool: """True when the network is a CAIP-2 Solana chain (``solana:``). Note: the bare string ``"solana"`` (no ``:``) is the mppx-internal label, - NOT a CAIP-2 spec — this helper treats it as ``False``. + NOT a CAIP-2 spec: this helper treats it as ``False``. """ return _read_network(value).startswith("solana:") diff --git a/agentscore_commerce/payment/payment_header.py b/agentscore_commerce/payment/payment_header.py index 62a0335..30f0ce9 100644 --- a/agentscore_commerce/payment/payment_header.py +++ b/agentscore_commerce/payment/payment_header.py @@ -1,6 +1,6 @@ """Detect whether a request is a settle leg (carries a payment credential). -The complement is a discovery leg — no credential, expects a 402. +The complement is a discovery leg: no credential, expects a 402. Used by the gate-conditional mount pattern: mount ``AgentScoreGate`` on a route only when payment is being attempted, so the discovery leg flows through @@ -8,9 +8,9 @@ Three credential channels are checked: -- ``Payment-Signature`` — MPP credentials (Tempo, Solana, Stripe SPT) -- ``X-Payment`` — x402 v1 EIP-3009 credentials -- ``Authorization: Payment `` — x402 v2 / paymentauth.org credentials +- ``Payment-Signature``: MPP credentials (Tempo, Solana, Stripe SPT) +- ``X-Payment``: x402 v1 EIP-3009 credentials +- ``Authorization: Payment ``: x402 v2 / paymentauth.org credentials """ from __future__ import annotations @@ -178,7 +178,7 @@ def malformed_payment_credential(request_or_headers: Any) -> MalformedPaymentCre This is deliberately a SHAPE check only. Signature verification, payTo binding, and challenge validation stay where they are (the x402 validator - and the MPP settle path) — those need per-request state the hooks produce. + and the MPP settle path): those need per-request state the hooks produce. A well-formed-but-invalid credential still reaches the real validators and fails there. diff --git a/agentscore_commerce/payment/settlement_override.py b/agentscore_commerce/payment/settlement_override.py index a231351..172dd8c 100644 --- a/agentscore_commerce/payment/settlement_override.py +++ b/agentscore_commerce/payment/settlement_override.py @@ -1,4 +1,4 @@ -"""x402 Settlement-Overrides header helpers — used with the `upto` scheme to specify the actual amount. +"""x402 Settlement-Overrides header helpers: used with the `upto` scheme to specify the actual amount. The header is JSON-encoded and lives on the merchant's response; the facilitator settles for that amount instead of the advertised maximum. Per the x402 docs, the amount field accepts: diff --git a/agentscore_commerce/payment/solana.py b/agentscore_commerce/payment/solana.py index 87e650d..b8d743b 100644 --- a/agentscore_commerce/payment/solana.py +++ b/agentscore_commerce/payment/solana.py @@ -7,9 +7,9 @@ ``load_solana_fee_payer(private_key=...)`` accepts a Solana keypair in any of the three forms agents commonly export it as: -* **base58** (Phantom export format) — 64-byte secret+public, or 32-byte +* **base58** (Phantom export format): 64-byte secret+public, or 32-byte secret-only -* **hex** — 128-char string (64 bytes hex: 32-byte secret + 32-byte public) +* **hex**: 128-char string (64 bytes hex: 32-byte secret + 32-byte public) Returns a ``KeyPairSigner`` from ``solders`` ready to pass to ``mppx``'s ``solana/charge`` rail. Returns ``None`` when ``private_key`` is empty / absent @@ -39,7 +39,7 @@ def load_solana_fee_payer(private_key: str | None) -> Any | None: try: from solders.keypair import Keypair # type: ignore[import-not-found] except ImportError as err: - msg = "solders not installed — run `pip install 'pympp[solana]>=0.6'` for load_solana_fee_payer." + msg = "solders not installed: run `pip install 'pympp[solana]>=0.6'` for load_solana_fee_payer." raise ImportError(msg) from err if re.fullmatch(r"[0-9a-fA-F]{128}", private_key): @@ -49,7 +49,7 @@ def load_solana_fee_payer(private_key: str | None) -> Any | None: try: import base58 # type: ignore[import-not-found] except ImportError as err: - msg = "base58 not installed — required for base58-encoded Solana fee-payer keys." + msg = "base58 not installed: required for base58-encoded Solana fee-payer keys." raise ImportError(msg) from err decoded = base58.b58decode(private_key) diff --git a/agentscore_commerce/payment/wwwauthenticate.py b/agentscore_commerce/payment/wwwauthenticate.py index 4a0118f..84e3204 100644 --- a/agentscore_commerce/payment/wwwauthenticate.py +++ b/agentscore_commerce/payment/wwwauthenticate.py @@ -19,7 +19,7 @@ def alias_amount_fields(accepts: list[Any]) -> list[Any]: Opt-in helper: the 402 emitters (``payment_required_header`` / ``build_402_body``) do NOT call this. Strict x402 v2 settlement matches the agent's echoed requirement against the server's rebuilt one by exact comparison, so an extra ``maxAmountRequired`` - the rebuild lacks silently fails settle — keep emitted ``accepts`` as + the rebuild lacks silently fails settle: keep emitted ``accepts`` as ``build_payment_requirements`` produced them. Call this only for a client hardcoded to read ``maxAmountRequired`` regardless of ``x402Version``. """ diff --git a/agentscore_commerce/payment/x402_server.py b/agentscore_commerce/payment/x402_server.py index 504d6e0..cecf1da 100644 --- a/agentscore_commerce/payment/x402_server.py +++ b/agentscore_commerce/payment/x402_server.py @@ -13,7 +13,7 @@ bazaar=True, ) -`x402` is an OPTIONAL peer dependency — install only the schemes you use:: +`x402` is an OPTIONAL peer dependency: install only the schemes you use:: pip install 'x402[evm,fastapi]>=2.9,<3' # for non-Coinbase facilitators pip install 'agentscore-commerce[x402,coinbase]' # for the Coinbase facilitator (adds cdp-sdk) @@ -75,7 +75,7 @@ def _build_coinbase_facilitator( api_key_secret = api_key_secret or os.environ.get("CDP_API_KEY_SECRET") if not api_key_id or not api_key_secret: msg = ( - "facilitator='coinbase' requires CDP_API_KEY_ID and CDP_API_KEY_SECRET — " + "facilitator='coinbase' requires CDP_API_KEY_ID and CDP_API_KEY_SECRET: " "set them as env vars or pass cdp_api_key_id / cdp_api_key_secret to " "create_x402_server." ) @@ -84,7 +84,7 @@ def _build_coinbase_facilitator( cdp_jwt_module = _import_optional("cdp.auth.utils.jwt") if cdp_jwt_module is None: msg = ( - "cdp-sdk not installed — run `pip install 'agentscore-commerce[coinbase]'` " + "cdp-sdk not installed: run `pip install 'agentscore-commerce[coinbase]'` " "(or `pip install cdp-sdk`) to use facilitator='coinbase'." ) raise ImportError(msg) @@ -93,7 +93,7 @@ def _build_coinbase_facilitator( facilitator_config_cls = getattr(http_module, "FacilitatorConfig", None) if http_module else None facilitator_client_cls = getattr(http_module, "HTTPFacilitatorClient", None) if http_module else None if facilitator_config_cls is None or facilitator_client_cls is None: - msg = "x402.http missing FacilitatorConfig / HTTPFacilitatorClient — upgrade x402>=2.9." + msg = "x402.http missing FacilitatorConfig / HTTPFacilitatorClient: upgrade x402>=2.9." raise ImportError(msg) facilitator_url = COINBASE_FACILITATOR_URL @@ -152,11 +152,11 @@ async def create_x402_server( # x402 2.9 layout: top-level `x402` package (with `x402` re-exports of # `x402ResourceServer`, `x402Facilitator`); schemes under # `x402.mechanisms.evm.{exact,upto}.server`. The 2.8-era v1+v2 dual - # register helper is obsolete — `register()` is v2 only and the resource + # register helper is obsolete: `register()` is v2 only and the resource # server handles v1 fallback internally via the facilitator. x402_top = _import_optional("x402") if x402_top is None or not hasattr(x402_top, "x402ResourceServer"): - msg = "x402 not installed — run `pip install 'x402[evm,fastapi]>=2.9,<3'` to use create_x402_server." + msg = "x402 not installed: run `pip install 'x402[evm,fastapi]>=2.9,<3'` to use create_x402_server." raise ImportError(msg) # Auto-select the Coinbase CDP facilitator when both env vars are present. @@ -179,7 +179,7 @@ async def create_x402_server( http_module = _import_optional("x402.http") facilitator_client_cls = getattr(http_module, "HTTPFacilitatorClient", None) if http_module else None if facilitator_client_cls is None: - msg = "x402.http missing HTTPFacilitatorClient — upgrade x402>=2.9." + msg = "x402.http missing HTTPFacilitatorClient: upgrade x402>=2.9." raise ImportError(msg) facilitator_instance = facilitator_client_cls() else: @@ -202,7 +202,7 @@ async def create_x402_server( evm_upto_module = _import_optional("x402.mechanisms.evm.upto.server") scheme_cls = getattr(evm_upto_module, "UptoEvmScheme", None) if evm_upto_module else None if scheme_cls is None: - msg = "x402[evm] not installed — run `pip install 'x402[evm]'` for x402 base upto rails." + msg = "x402[evm] not installed: run `pip install 'x402[evm]'` for x402 base upto rails." raise ImportError(msg) server.register(network, scheme_cls()) else: @@ -210,7 +210,7 @@ async def create_x402_server( evm_exact_module = _import_optional("x402.mechanisms.evm.exact.server") scheme_cls = getattr(evm_exact_module, "ExactEvmScheme", None) if evm_exact_module else None if scheme_cls is None: - msg = "x402[evm] not installed — run `pip install 'x402[evm]'` for x402 base rails." + msg = "x402[evm] not installed: run `pip install 'x402[evm]'` for x402 base rails." raise ImportError(msg) server.register(network, scheme_cls()) @@ -221,11 +221,11 @@ async def create_x402_server( bazaar_module = _import_optional("x402.extensions.bazaar") bazaar_ext = getattr(bazaar_module, "bazaar_resource_server_extension", None) if bazaar_module else None if bazaar_ext is None: - msg = "x402[extensions] not installed — run `pip install 'x402[extensions]'` for bazaar discovery." + msg = "x402[extensions] not installed: run `pip install 'x402[extensions]'` for bazaar discovery." raise ImportError(msg) register_extension = getattr(server, "register_extension", None) if not callable(register_extension): - msg = "x402 server does not expose register_extension — bazaar registration unavailable." + msg = "x402 server does not expose register_extension: bazaar registration unavailable." raise RuntimeError(msg) register_extension(bazaar_ext) @@ -259,11 +259,11 @@ def build_x402_accepts_for_402( 2. Remember to call ``model_dump(by_alias=True, mode="json")`` on each Pydantic requirement so the surrounding JSON response can serialize it 3. Hardcode ``extra`` (which differs by the actual on-chain contract: base mainnet - USDC has ``name="USD Coin"``, base sepolia USDC has ``name="USDC"`` — EIP-712 + USDC has ``name="USD Coin"``, base sepolia USDC has ``name="USDC"``: EIP-712 domain hashes differ, so getting this wrong silently breaks every signature verify at the facilitator) - Returns a list of plain dicts in the shape that x402 expects on the wire — drop + Returns a list of plain dicts in the shape that x402 expects on the wire: drop them straight into the ``accepts`` field of the 402 challenge body. Raises ``Exception`` if the underlying ``build_payment_requirements`` raises; @@ -273,7 +273,7 @@ def build_x402_accepts_for_402( config_cls_module = _import_optional("x402.schemas.config") config_cls = getattr(config_cls_module, "ResourceConfig", None) if config_cls_module else None if config_cls is None: - msg = "x402 not installed — run `pip install 'x402[evm,fastapi]>=2.9,<3'` to use build_x402_accepts_for_402." + msg = "x402 not installed: run `pip install 'x402[evm,fastapi]>=2.9,<3'` to use build_x402_accepts_for_402." raise ImportError(msg) config = config_cls( scheme=scheme, diff --git a/agentscore_commerce/payment/x402_settle.py b/agentscore_commerce/payment/x402_settle.py index e020389..28e43fc 100644 --- a/agentscore_commerce/payment/x402_settle.py +++ b/agentscore_commerce/payment/x402_settle.py @@ -176,7 +176,7 @@ def classify_orchestration_error(err: BaseException | str) -> ClassifiedX402Erro ``try/except`` around the full settle flow). Returns a :class:`ClassifiedX402Error` when the error message matches a known pattern; ``None`` otherwise. - Callers should rethrow on ``None`` — this helper never swallows unknown errors. + Callers should rethrow on ``None``: this helper never swallows unknown errors. The typical pattern:: try: @@ -248,7 +248,7 @@ def coerce_resource_config(config: Any) -> Any: ``payTo`` / ``maxTimeoutSeconds`` camelCase keys. x402's Python ``ResourceConfig`` is a Pydantic model with ``pay_to`` / ``max_timeout_seconds`` snake_case fields, and ``build_payment_requirements`` - does ``config.network`` attribute access — so a raw dict raises + does ``config.network`` attribute access: so a raw dict raises ``AttributeError("'dict' object has no attribute 'network'")``. Coerce here so callers can pass either shape. @@ -338,7 +338,7 @@ async def process_x402_settle( """Run the x402 verify→settle flow and return a tagged outcome. ``resource_config`` accepts either a ``dict`` (JS-style with ``payTo`` / - ``maxTimeoutSeconds`` camelCase keys) or an x402 ``ResourceConfig`` instance — + ``maxTimeoutSeconds`` camelCase keys) or an x402 ``ResourceConfig`` instance: dicts are coerced before the build step. Set ``extension`` to fold a Bazaar (or other) extension into the verify step; @@ -365,7 +365,7 @@ async def process_x402_settle( # Per-request extension enrichment runs only when a caller explicitly attaches one # (e.g. the Bazaar discovery extension). x402 2.9 takes the enriched dict as the # second argument to ``build_payment_requirements`` rather than as a verify-step - # input, but the fold happens at build time — so we replay the build with the + # input, but the fold happens at build time: so we replay the build with the # enriched extensions and use those requirements going forward. if extension is not None: resolved_transport_context = transport_context @@ -390,7 +390,7 @@ async def process_x402_settle( return ProcessX402SettleFailure(phase="facilitator_error", step="build_requirements", error=err) # x402 2.9's ``x402ResourceServer`` exposes ``verify_payment(payload, requirements)`` - # — not ``process_payment_request`` (a fictional method that earlier versions of this + # not ``process_payment_request`` (a fictional method that earlier versions of this # helper called and only ever worked against test stubs). try: verify_result = await server.verify_payment(coerced_payload, matched_requirement) diff --git a/agentscore_commerce/payment/x402_validation.py b/agentscore_commerce/payment/x402_validation.py index ae6aaca..f952e8d 100644 --- a/agentscore_commerce/payment/x402_validation.py +++ b/agentscore_commerce/payment/x402_validation.py @@ -10,7 +10,7 @@ extract the signed network + payTo, validate against the merchant's accepted network, validate the payTo address shape, and check that the payTo was minted by THIS merchant (cache hit). Each step has its own denial code and ``next_steps`` - shape — getting the message right by hand across 4 conditions is fiddly. + shape: getting the message right by hand across 4 conditions is fiddly. """ from __future__ import annotations @@ -34,7 +34,7 @@ def validate_x402_network_config(*, base_network: str) -> None: """Boot-time guard: raise if the base network isn't supported. Raises ``ValueError`` with a message that names the unsupported value AND lists the - valid options — agents tracking down a misconfigured deploy don't need to grep for + valid options: agents tracking down a misconfigured deploy don't need to grep for the supported list. """ if base_network not in X402_SUPPORTED_BASE_NETWORKS: @@ -49,7 +49,7 @@ def validate_x402_network_config(*, base_network: str) -> None: @dataclass class VerifyX402RequestSuccess: - """Successful verification — caller passes ``payload`` straight into ``process_x402_settle``.""" + """Successful verification: caller passes ``payload`` straight into ``process_x402_settle``.""" payload: dict[str, Any] signed_network: str @@ -59,7 +59,7 @@ class VerifyX402RequestSuccess: @dataclass class VerifyX402RequestFailure: - """Failed verification — caller returns ``body`` with HTTP ``status``.""" + """Failed verification: caller returns ``body`` with HTTP ``status``.""" body: dict[str, Any] status: Literal[400] = 400 @@ -80,7 +80,7 @@ def _header_lookup(headers: dict[str, str], *names: str) -> str | None: _REGENERATE_WARNING = ( "Use `agentscore-pay pay --chain base` (or `tempo request` for Tempo USDC) so the credential " - "is signed and submitted via the protocol handshake. Do NOT use `tempo wallet transfer` — " + "is signed and submitted via the protocol handshake. Do NOT use `tempo wallet transfer`: " "that sends USDC on-chain but does not complete the handshake." ) @@ -106,7 +106,7 @@ async def verify_x402_request( Returns ``VerifyX402RequestSuccess`` when valid; the caller passes ``payload`` straight into :func:`process_x402_settle`. Returns ``VerifyX402RequestFailure`` - when invalid — ``body`` includes ``next_steps`` with ``regenerate_payment_credential`` + when invalid: ``body`` includes ``next_steps`` with ``regenerate_payment_credential`` so agents can recover deterministically from the response alone. Reads the header from ``payment-signature`` first, falling back to ``x-payment`` diff --git a/agentscore_commerce/payment/zero_settle.py b/agentscore_commerce/payment/zero_settle.py index acd2ff1..b9fb86a 100644 --- a/agentscore_commerce/payment/zero_settle.py +++ b/agentscore_commerce/payment/zero_settle.py @@ -2,7 +2,7 @@ CDP rejects EIP-3009 ``transferWithAuthorization`` with ``value=0`` as ``invalid_payload``; pympp's tempo intents accept only ``hash`` and -``transaction`` payload types — the wallet-bound ``proof`` payload that the +``transaction`` payload types: the wallet-bound ``proof`` payload that the mppx client emits for $0 settles (and that the mppx server verifies) has no pympp counterpart yet. Both upstream verify+settle paths therefore fail when the authorized amount is zero, so merchants that drop the settle to $0 in a @@ -13,7 +13,7 @@ return ``ZeroSettleResult(signer_address, signer_network, tx_hash=None)``. Identity is still authenticated by the merchant's gate above; the redemption code is single-use; nothing on-chain to verify. The recovered signer block is -UNAUTHENTICATED (parse-only) on the MPP path — treat it as an attribution +UNAUTHENTICATED (parse-only) on the MPP path: treat it as an attribution hint, not a verified identity. Known divergence from the Node SDK: node-commerce delegates $0 Tempo settles diff --git a/agentscore_commerce/quote_cache.py b/agentscore_commerce/quote_cache.py index 7595512..6e33934 100644 --- a/agentscore_commerce/quote_cache.py +++ b/agentscore_commerce/quote_cache.py @@ -2,15 +2,15 @@ :func:`create_result_cache` is the neutral primitive: a keyed JSON-value cache with a stable content-hash key builder. Use it to cache any per-request result -a merchant computes on the probe leg and replays on the settle leg — e.g. the +a merchant computes on the probe leg and replays on the settle leg: e.g. the output of a paid upstream call made in a ``Checkout`` ``pre_validate`` hook, so a payment retry (or a junk payment header) never pays upstream twice. :func:`create_quote_cache` is the compute-first-flavored wrapper used by ``compute_first_checkout``: the cached value is a :class:`CachedQuote` (``body`` / ``price_cents`` / ``recipients``). Standard x402-fetch retry -semantics resign the buyer's ORIGINAL request body — there's no ``result_id`` -echo channel through the protocol — so both caches key by a stable +semantics resign the buyer's ORIGINAL request body: there's no ``result_id`` +echo channel through the protocol: so both caches key by a stable content-hash of the request body. Same body → same hash → same cache slot. Default in-memory ``dict``; optional ``redis_url`` lazy-imports diff --git a/agentscore_commerce/stripe_multichain/__init__.py b/agentscore_commerce/stripe_multichain/__init__.py index 35ef1c8..d5e9276 100644 --- a/agentscore_commerce/stripe_multichain/__init__.py +++ b/agentscore_commerce/stripe_multichain/__init__.py @@ -1,4 +1,4 @@ -"""Stripe multichain helpers — PaymentIntent with deposit_options + testnet simulator + Stripe SPT method for pympp.""" +"""Stripe multichain helpers: PaymentIntent with deposit_options + testnet simulator + Stripe SPT method for pympp.""" from agentscore_commerce.stripe_multichain.mppx_stripe import ( DEFAULT_PAYMENT_METHOD_TYPES, diff --git a/agentscore_commerce/stripe_multichain/mppx_stripe.py b/agentscore_commerce/stripe_multichain/mppx_stripe.py index 56c3003..07764c7 100644 --- a/agentscore_commerce/stripe_multichain/mppx_stripe.py +++ b/agentscore_commerce/stripe_multichain/mppx_stripe.py @@ -22,7 +22,7 @@ secret_key=os.environ["MPP_SECRET_KEY"], ) -``pympp`` is an OPTIONAL peer dependency — vendors who don't use Stripe SPT don't need +``pympp`` is an OPTIONAL peer dependency: vendors who don't use Stripe SPT don't need to install it. Throws ``ImportError`` if pympp (or its stripe support) is missing. """ @@ -44,20 +44,20 @@ async def create_mppx_stripe( Args: profile_id: Stripe profile_id / network_id advertised in your ``stripe/charge`` ``accepted_methods`` entry. - secret_key: Stripe secret key — pympp uses it to validate inbound SharedPaymentTokens. + secret_key: Stripe secret key: pympp uses it to validate inbound SharedPaymentTokens. payment_method_types: Payment method types this stripe rail accepts. Defaults to ``["card", "link"]``. """ try: stripe_module = importlib.import_module("mpp.methods.stripe") except ImportError as exc: - msg = "pympp[stripe] not installed — run `pip install 'pympp[stripe]'` to use create_mppx_stripe." + msg = "pympp[stripe] not installed: run `pip install 'pympp[stripe]'` to use create_mppx_stripe." raise ImportError(msg) from exc charge_factory = getattr(stripe_module, "charge", None) if not callable(charge_factory): msg = ( - "mpp.methods.stripe.charge not found — your pympp version may not ship " + "mpp.methods.stripe.charge not found: your pympp version may not ship " "Stripe SPT support. Upgrade with `pip install -U pympp`." ) raise ImportError(msg) diff --git a/agentscore_commerce/stripe_multichain/pay_to_address.py b/agentscore_commerce/stripe_multichain/pay_to_address.py index f0c5382..36e9036 100644 --- a/agentscore_commerce/stripe_multichain/pay_to_address.py +++ b/agentscore_commerce/stripe_multichain/pay_to_address.py @@ -7,7 +7,7 @@ advertises a stable per-order deposit address. - **Settle leg** (MPP credential attached): reuse the buyer's signed-against payTo from the credential (after verifying it's in the local cache OR matches - a configured ``static_recipients`` entry) — otherwise the verify leg would + a configured ``static_recipients`` entry): otherwise the verify leg would compare against a freshly-rotated address and reject the credential. Stripe SPT and card methods don't carry an on-chain recipient, so the settle @@ -62,7 +62,7 @@ async def create_pay_to_address_from_stripe_pi( On the settle leg, when ``authorization_header`` carries an MPP credential binding a ``tempo`` or ``solana`` recipient, returns THAT address (after verifying it's in ``pi_cache`` OR matches a configured ``static_recipients`` - entry — static addresses are always-accepted because the merchant owns + entry: static addresses are always-accepted because the merchant owns them). Otherwise mints a fresh :func:`create_multichain_payment_intent` for the rails NOT covered by ``static_recipients``, caches the merged address map, and registers static recipients with ``pi_cache.cache_address`` @@ -71,7 +71,7 @@ async def create_pay_to_address_from_stripe_pi( ``tempo``). ``static_recipients`` (optional, keyed by network) lets the merchant pin a - fixed receive wallet on chains where per-call rotation is expensive — Solana + fixed receive wallet on chains where per-call rotation is expensive: Solana in particular, since MPP spec §13.6 charges ~0.002 SOL of ATA rent per new recipient into accounts the merchant can't close. Example: ``static_recipients={"solana": "FR96wd96urH..."}``. The SDK skips Stripe @@ -136,7 +136,7 @@ async def mint_multichain_recipients( Returns the full per-rail deposit map. Prefer this when the merchant's ``mint_recipients`` hook needs every rail's address (typical multi-rail - merchant) — avoids the "returned string is ambiguous" trap when + merchant): avoids the "returned string is ambiguous" trap when ``static_recipients`` is configured (settle leg's bound recipient may be the solana static rather than the tempo per-PI address). """ diff --git a/agentscore_commerce/stripe_multichain/pi_cache.py b/agentscore_commerce/stripe_multichain/pi_cache.py index 6380a9e..8869b82 100644 --- a/agentscore_commerce/stripe_multichain/pi_cache.py +++ b/agentscore_commerce/stripe_multichain/pi_cache.py @@ -2,20 +2,20 @@ Stripe-multichain merchants need three lookups during a request lifecycle: -1. **Is this on-chain ``pay_to`` address one we minted?** — when an MPP credential +1. **Is this on-chain ``pay_to`` address one we minted?**: when an MPP credential arrives with a ``recipient``, verify it matches a recently-minted Stripe deposit address. Validates the credential's deposit address against the addresses the merchant has actually minted. -2. **Which PaymentIntent owns this deposit address?** — when settling, the +2. **Which PaymentIntent owns this deposit address?**: when settling, the ``simulate_crypto_deposit`` test_helpers call needs the PaymentIntent id for the deposit address that was paid to. -3. **Which sibling deposit addresses belong to the same PaymentIntent?** — when +3. **Which sibling deposit addresses belong to the same PaymentIntent?**: when enriching a 402 with x402 entries, the merchant needs the Base + Solana addresses Stripe minted alongside the original Tempo address (one PI carries up to three). -All three are TTL-bounded (default 300s — long enough for an agent to retry, short +All three are TTL-bounded (default 300s: long enough for an agent to retry, short enough to bound memory). Backed by Redis when ``redis_url`` is set, falls back to in-process dict otherwise. Single-instance servers can use the in-memory cache; multi-instance deployments need a shared cache (Redis) so a deposit lands on @@ -40,7 +40,7 @@ class _RedisLike(Protocol): - """Subset of redis.asyncio.Redis we use — typed structurally so ``redis`` stays an optional peer dep.""" + """Subset of redis.asyncio.Redis we use: typed structurally so ``redis`` stays an optional peer dep.""" async def set(self, key: str, value: str, *, ex: int) -> Any: ... async def get(self, key: str) -> str | None: ... @@ -78,10 +78,10 @@ def create_pi_cache( A background task evicts expired in-memory entries every 60 seconds; call ``stop()`` from server shutdown handlers to cancel it. - ``redis_url`` — connection URL (e.g. ``rediss://…cache.amazonaws.com:6379``); when + ``redis_url``: connection URL (e.g. ``rediss://…cache.amazonaws.com:6379``); when omitted, the cache falls back to in-process dicts with the same API. - ``ttl_seconds`` — entry TTL (default 300). - ``key_prefix`` — Redis key prefix (default ``'payto:'``). + ``ttl_seconds``: entry TTL (default 300). + ``key_prefix``: Redis key prefix (default ``'payto:'``). """ ttl = ttl_seconds @@ -96,7 +96,7 @@ async def _get_redis() -> _RedisLike | None: return None if redis_client is not None: return redis_client - # Dynamic import keeps `redis` as an optional peer dep — merchants without + # Dynamic import keeps `redis` as an optional peer dep: merchants without # Redis don't pay the install cost. from importlib import import_module diff --git a/agentscore_commerce/stripe_multichain/simulate_deposit.py b/agentscore_commerce/stripe_multichain/simulate_deposit.py index 74b557e..061668b 100644 --- a/agentscore_commerce/stripe_multichain/simulate_deposit.py +++ b/agentscore_commerce/stripe_multichain/simulate_deposit.py @@ -1,4 +1,4 @@ -"""Stripe test_helpers/simulate_crypto_deposit caller — testnet helper for end-to-end exercises.""" +"""Stripe test_helpers/simulate_crypto_deposit caller: testnet helper for end-to-end exercises.""" import logging from collections.abc import Callable @@ -15,7 +15,7 @@ } # Stripe's documented magic test_helpers transaction hash that resolves the -# PaymentIntent to ``succeeded`` within 15 seconds. Same value across all networks — +# PaymentIntent to ``succeeded`` within 15 seconds. Same value across all networks: # Stripe normalizes the format internally. Anything else (including network-shaped # placeholder bytes) is rejected with "not a valid testmode transaction hash". # @@ -70,14 +70,14 @@ async def simulate_deposit_if_test_mode( network: Literal["tempo", "base", "solana"], stripe_secret_key: str, buyer_wallet: str | None = None, - token_currency: str = "usdc", # noqa: S107 — literal default, not a secret + token_currency: str = "usdc", # noqa: S107 # literal default, not a secret stripe_version: str | None = None, ) -> None: """Higher-level wrapper around :func:`simulate_crypto_deposit` for the testnet/dev path. Bundles the three steps every Stripe-multichain merchant repeats: - 1. Gate on ``sk_test_`` key prefix — production keys reject the test_helpers endpoint + 1. Gate on ``sk_test_`` key prefix: production keys reject the test_helpers endpoint with 400; live deposits reach Stripe's real crypto-deposit watcher instead. 2. Resolve the PaymentIntent id from the deposit address (cache lookup). 3. Call ``simulate_crypto_deposit`` with Stripe's documented success magic hash. @@ -86,14 +86,14 @@ async def simulate_deposit_if_test_mode( ``[stripe] ✗ Failed to simulate deposit for PI : `` on failure. Errors are caught and logged (never raised) so a sim hiccup doesn't fail the order. - Use case is exclusively dev/testnet end-to-end — production servers (sk_live_) no-op. + Use case is exclusively dev/testnet end-to-end: production servers (sk_live_) no-op. """ if not stripe_secret_key.startswith("sk_test_"): return pi_id = get_payment_intent_id(deposit_address) if not pi_id: logger.warning( - "[stripe] Skipping deposit simulation — no PI cached for deposit address %s… (network=%s). " + "[stripe] Skipping deposit simulation: no PI cached for deposit address %s… (network=%s). " "The PI cache TTL may have expired between 402 emission and settlement.", deposit_address[:10], network, diff --git a/examples/README.md b/examples/README.md index 6942856..dbd3b55 100644 --- a/examples/README.md +++ b/examples/README.md @@ -7,8 +7,8 @@ Runnable, copy-pasteable example integrations covering the most common merchant | [`identity_only.py`](./identity_only.py) | Compliance gate without payment | Minimal: wraps any endpoint with KYC + age + jurisdiction checks. Vendor handles their own payment. | | [`api_provider.py`](./api_provider.py) | API provider (Exa-style) | Per-call billing on multiple rails: Tempo MPP + x402 (Base + Solana), all driven by `Checkout`. No identity gate. Demos `Checkout(discovery_probe=...)` for x402-crawler auto-routing, `build_merchant_index_json` + `standard_endpoint_descriptions(kind="api")` for `GET /` discovery, and `build_redemption_skill_md` with the trial-credit body shape on `GET /redemption.md`. | | [`multi_rail_merchant.py`](./multi_rail_merchant.py) | Full agent-commerce merchant | Identity gate + Tempo MPP + x402 (Base + Solana) + Stripe SPT via `Checkout`. Demos `pricing_result` (cents → typed PricingResult), `Receipt` + `ReceiptNextSteps` + `build_success_next_steps` in `on_settled`, per-order Stripe-multichain deposit minting via `mint_recipients`, and `simulate_deposit_if_test_mode`. | -| [`stripe_multichain_merchant.py`](./stripe_multichain_merchant.py) | Stripe-anchored multi-chain | Stripe PaymentIntent with deposit_options for tempo/base/solana; crypto deposits flow through Stripe. Read `result.deposit_addresses[network]` directly. Includes testnet `simulate_crypto_deposit` helper. For low-margin endpoints (sub-dollar APIs), use `create_pay_to_address_from_stripe_pi` / `mint_multichain_recipients` with `static_recipients={"solana": ""}` — see the `stripe_multichain` row in the main `README.md` for the full pattern and economics. | -| [`compute_first_merchant.py`](./compute_first_merchant.py) | Pay-per-result variable cost (LLM, transcode, per-token/byte) | The probe leg runs the work server-side, caches the result by body content-hash, and emits a 402 at the **exact** computed price; the retry pays that price and gets the cached result. Exact-mode rails only (x402-exact Base, plus Tempo/Solana/Stripe SPT via a `compose_mppx` callback) — deliberately scoped out of x402-upto (Permit2) and Settlement-Overrides. Uses `compute_first_checkout` + `create_quote_cache`; pairs with `rate_limit_fastapi` since the probe leg runs work pre-payment. | +| [`stripe_multichain_merchant.py`](./stripe_multichain_merchant.py) | Stripe-anchored multi-chain | Stripe PaymentIntent with deposit_options for tempo/base/solana; crypto deposits flow through Stripe. Read `result.deposit_addresses[network]` directly. Includes testnet `simulate_crypto_deposit` helper. For low-margin endpoints (sub-dollar APIs), use `create_pay_to_address_from_stripe_pi` / `mint_multichain_recipients` with `static_recipients={"solana": ""}`: see the `stripe_multichain` row in the main `README.md` for the full pattern and economics. | +| [`compute_first_merchant.py`](./compute_first_merchant.py) | Pay-per-result variable cost (LLM, transcode, per-token/byte) | The probe leg runs the work server-side, caches the result by body content-hash, and emits a 402 at the **exact** computed price; the retry pays that price and gets the cached result. Exact-mode rails only (x402-exact Base, plus Tempo/Solana/Stripe SPT via a `compose_mppx` callback): deliberately scoped out of x402-upto (Permit2) and Settlement-Overrides. Uses `compute_first_checkout` (its cache is `create_quote_cache`); pairs with `rate_limit_fastapi` since the probe leg runs work pre-payment. | | [`compliance_merchant.py`](./compliance_merchant.py) | Regulated-goods merchant (wine, cannabis, etc.) | Full compliance gate via `Checkout(gate=CheckoutGateConfig(...))` + custom `on_denied` composing commerce helpers: `verification_agent_instructions`, `is_fixable_denial`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`. Shows how vendors write only the business-specific denial branches and let commerce handle the rest. | | [`per_product_policy_merchant.py`](./per_product_policy_merchant.py) | Multi-product merchant with mixed compliance needs | One product carries a hard gate (wine: KYC + 21 + US-state allowlist), another has no gate at all (anonymous merch, ships anywhere), a third uses `enforcement="soft"` (request KYC as a fraud signal but accept anonymous sales, stamping `identity_status="unverified"` on the order). Uses `PolicyBlock`, the one-call `validate_shipping_against_policy`, and `Checkout(gate=CheckoutGateConfig(per_request_policy=...))`. | | [`signed_ucp_merchant.py`](./signed_ucp_merchant.py) | Signed UCP profile + JWKS endpoint | One-call mount via `checkout.mount_ucp_routes_fastapi(app, ...)` registers `/.well-known/ucp` + `/.well-known/jwks.json` + the OPTIONS preflights. AgentScore's `agentscore-profile+jws` is a vendor extension for trust-mode verifiers (regulated-commerce, AP2-aware) that opt into auditable profiles; UCP §6 itself does NOT mandate signing. Wires ephemeral-for-dev / env-JWK-for-prod and `bootstrap_ucp_signing_key` lifespan-hook usage. | diff --git a/examples/compliance_merchant.py b/examples/compliance_merchant.py index 09a03bb..eb2a758 100644 --- a/examples/compliance_merchant.py +++ b/examples/compliance_merchant.py @@ -28,7 +28,7 @@ pip install 'agentscore-commerce[fastapi]' Env vars: - AGENTSCORE_API_KEY — your AgentScore API key + AGENTSCORE_API_KEY: your AgentScore API key Run: uvicorn examples.compliance_merchant:app --port 3000 """ diff --git a/examples/compute_first_merchant.py b/examples/compute_first_merchant.py index 1f9fb1f..c54b459 100644 --- a/examples/compute_first_merchant.py +++ b/examples/compute_first_merchant.py @@ -11,7 +11,7 @@ * upto's facilitator support is still limited (Coinbase CDP testnet rejects upto-mode settles today; only mainnet claims support). -* Permit2 is Ethereum-only — no Solana, no Tempo non-EIP-3009, no Stripe. +* Permit2 is Ethereum-only: no Solana, no Tempo non-EIP-3009, no Stripe. * Compute-first works on every exact-mode rail in the ecosystem with no buyer setup and no facilitator extensions. @@ -21,11 +21,11 @@ This example wires the x402-exact rail on Base only. To add MPP rails (Tempo, Solana, Stripe SPT), pass a ``compose_mppx`` callback that builds -mppx intents at the exact cached price — see ``multi_rail_merchant.py`` +mppx intents at the exact cached price: see ``multi_rail_merchant.py`` for the fixed-price MPP compose pattern; the compute-first variant is structurally identical except the helper passes the cached price + recipients into your callback. Stripe SPT requires the computed price -to be at least $0.50 USD — below that Stripe's fixed ~$0.30 fee makes +to be at least $0.50 USD: below that Stripe's fixed ~$0.30 fee makes the charge unprofitable, so ``build_mppx_compose_rails`` auto-drops the stripe rail and sub-50-cent pay-per-result APIs ship Tempo + x402 + Solana only. @@ -83,7 +83,7 @@ async def _run_search(body: dict[str, Any], _ctx: ComputeFirstWorkContext) -> Wo search_handler = compute_first_checkout( name="search", url=f"{APP_URL}/search", - # $0.01 per result. Use 0.0001 for sub-cent / per-token pricing — the + # $0.01 per result. Use 0.0001 for sub-cent / per-token pricing: the # helper auto-derives decimal precision from the unit price. unit_price_cents=1, rails=ComputeFirstRails( diff --git a/examples/identity_only.py b/examples/identity_only.py index a63fef9..1932101 100644 --- a/examples/identity_only.py +++ b/examples/identity_only.py @@ -16,7 +16,7 @@ pip install agentscore-commerce[fastapi] Env vars: - AGENTSCORE_API_KEY — your AgentScore API key + AGENTSCORE_API_KEY: your AgentScore API key Run: uvicorn examples.identity_only:app --port 3000 """ @@ -59,7 +59,7 @@ @app.post("/restricted", dependencies=[Depends(gate)]) async def restricted(assess: dict[str, Any] = Depends(get_agentscore_data)) -> dict[str, Any]: - """Gated route — only reached when the agent passes the compliance policy. + """Gated route: only reached when the agent passes the compliance policy. `assess` is the raw `/v1/assess` response: ``{ decision, operator, kyc_verified, age_bracket, jurisdiction, ... }``. Run your own business @@ -87,4 +87,4 @@ async def capture_wallet_example(request: Request) -> dict[str, Any]: # ── Public routes (no gate) ──────────────────────────────────────────────── @app.get("/public-info") async def public_info() -> dict[str, str]: - return {"message": "open access — no identity required"} + return {"message": "open access: no identity required"} diff --git a/examples/multi_rail_merchant.py b/examples/multi_rail_merchant.py index cca5bcf..d48205b 100644 --- a/examples/multi_rail_merchant.py +++ b/examples/multi_rail_merchant.py @@ -113,7 +113,7 @@ async def _mint_recipients(ctx: Any) -> dict[str, str]: For low-margin endpoints (sub-dollar per call), pass ``static_recipients={"solana": os.environ["MERCHANT_SOLANA_RECIPIENT"]}`` to - skip Stripe minting on Solana — at $0.01/call MPP spec §13.6's ~$0.50 per-PI + skip Stripe minting on Solana: at $0.01/call MPP spec §13.6's ~$0.50 per-PI ATA rent dominates revenue. With a stable merchant-owned recipient + one-time external pre-funding of its USDC ATA, every settle pays only the per-tx fee. """ diff --git a/examples/per_product_policy_merchant.py b/examples/per_product_policy_merchant.py index 498b70b..68594bd 100644 --- a/examples/per_product_policy_merchant.py +++ b/examples/per_product_policy_merchant.py @@ -13,7 +13,7 @@ 1. `pre_validate` looks up the product row by slug and stashes the policy block onto `ctx.state` for downstream hooks. 2. `per_request_policy(ctx)` returns the merged policy dict (including - `enforcement: "hard"|"soft"|None`) — the SDK gate runs hard/soft based on + `enforcement: "hard"|"soft"|None`): the SDK gate runs hard/soft based on the field. 3. Soft denials are swallowed by the SDK and stamp `identity_status="unverified"` onto the order; hard denials propagate the @@ -23,7 +23,7 @@ pip install 'agentscore-commerce[fastapi]' Env vars: - AGENTSCORE_API_KEY — your AgentScore API key + AGENTSCORE_API_KEY: your AgentScore API key Run: uvicorn examples.per_product_policy_merchant:app --port 3000 """ diff --git a/examples/stripe_multichain_merchant.py b/examples/stripe_multichain_merchant.py index af671a2..e358b85 100644 --- a/examples/stripe_multichain_merchant.py +++ b/examples/stripe_multichain_merchant.py @@ -13,7 +13,7 @@ pip install 'agentscore-commerce[fastapi,stripe]' Env vars: - STRIPE_SECRET_KEY — sk_live_... or sk_test_... + STRIPE_SECRET_KEY: sk_live_... or sk_test_... Run: uvicorn examples.stripe_multichain_merchant:app --port 3000 """ @@ -56,7 +56,7 @@ async def checkout(body: dict) -> dict: ) # 2. Return per-network deposit addresses to the agent (or 402 with - # addresses embedded — see multi_rail_merchant.py for the full 402-builder + # addresses embedded: see multi_rail_merchant.py for the full 402-builder # pattern). amount_usd = body["amount_usd"] tempo = result.deposit_addresses.get("tempo") @@ -78,7 +78,7 @@ async def checkout(body: dict) -> dict: # ── Testnet helper: simulate a deposit landing on a PI ────────────────────── # Useful for end-to-end testing without real on-chain transfers. For the # typical "fire after PI mint if sk_test_" pattern, prefer -# `simulate_deposit_if_test_mode` which gates internally — see +# `simulate_deposit_if_test_mode` which gates internally: see # multi_rail_merchant.py. @app.post("/testnet/simulate-deposit") async def simulate_deposit(body: dict) -> dict: diff --git a/osv-scanner.toml b/osv-scanner.toml index 36fa56d..6df2bea 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -6,8 +6,8 @@ # via `--ignore-vuln` flags; keep both in sync. # # Conventions: -# - `id` — the OSV / PYSEC / GHSA identifier -# - `ignoreUntil` — review-by date (ISO 8601). Leave empty for permanent +# - `id`: the OSV / PYSEC / GHSA identifier +# - `ignoreUntil`: review-by date (ISO 8601). Leave empty for permanent # ignores (e.g. disputed-by-upstream). Otherwise pick a date that forces # us to re-evaluate when the upstream releases a fix. -# - `reason` — short justification +# - `reason`: short justification diff --git a/pyproject.toml b/pyproject.toml index 03b2730..3c6fe72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ include = ["agentscore_commerce"] # Live symbols vulture sees as unused. ASGI signatures, Protocol method params, # public re-exports, TYPE_CHECKING-only imports referenced via string casts. # Replaces a top-level vulture_whitelist.py file (which CodeQuality flagged -# as "statement has no effect" — accurate observation, but vulture's whitelist +# as "statement has no effect": accurate observation, but vulture's whitelist # semantics require bare identifiers, which static analyzers can't distinguish # from real no-op statements). ignore_names = [ diff --git a/scripts/regenerate_cross_lang_fixtures.py b/scripts/regenerate_cross_lang_fixtures.py index 19bdab5..7a43ae4 100644 --- a/scripts/regenerate_cross_lang_fixtures.py +++ b/scripts/regenerate_cross_lang_fixtures.py @@ -54,7 +54,7 @@ def _envelope(signed: dict[str, Any], public_jwk: dict[str, Any], alg: str, kid: } -# Spec-compliant binding helpers — each scenario uses these (or variants) so the +# Spec-compliant binding helpers: each scenario uses these (or variants) so the # fixtures cover the full set of canonical UCP fields per binding type. @@ -110,7 +110,7 @@ def _stripe_handler(config: dict[str, Any]) -> UCPPaymentHandlerBinding: def main() -> None: - # py-minimal — empty maps; just metadata + signing keys. + # py-minimal: empty maps; just metadata + signing keys. kid = "py-minimal-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( @@ -121,7 +121,7 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-minimal", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-es256-rails — multi-transport service + multi-rail + ES256 signing key. + # py-es256-rails: multi-transport service + multi-rail + ES256 signing key. kid = "py-es256-rails-ES256" key = generate_ucp_signing_key(kid=kid, alg="ES256") profile = build_ucp_profile( @@ -141,7 +141,7 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid, alg="ES256") _write("py-es256-rails", _envelope(signed, key.public_jwk, "ES256", kid)) - # py-extras-int — payment_handler config with int + string fields. + # py-extras-int: payment_handler config with int + string fields. kid = "py-extras-int-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( @@ -155,14 +155,14 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-extras-int", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-capability — hand-crafted vendor capability under com.agentscore.identity. + # py-capability: hand-crafted vendor capability under com.agentscore.identity. kid = "py-capability-EdDSA" key = generate_ucp_signing_key(kid=kid) custom_capability = UCPCapabilityBinding( version="2026-04-08", spec="https://www.agentscore.com/specification/identity", schema="https://www.agentscore.com/schemas/ucp/com-agentscore-identity-v1.json", - # `extras` flat on the binding — kyc_required is a vendor field on this binding. + # `extras` flat on the binding: kyc_required is a vendor field on this binding. extras={"kyc_required": True}, ) profile = build_ucp_profile( @@ -177,7 +177,7 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-capability", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-unicode — multi-byte UTF-8 in name / endpoint / config. + # py-unicode: multi-byte UTF-8 in name / endpoint / config. kid = "py-unicode-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( @@ -191,7 +191,7 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-unicode", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-multikey — JWKS with two keys, signed by the newer one. + # py-multikey: JWKS with two keys, signed by the newer one. old_key = generate_ucp_signing_key(kid="py-multikey-old") new_key = generate_ucp_signing_key(kid="py-multikey-new") profile = build_ucp_profile( @@ -217,7 +217,7 @@ def main() -> None: }, ) - # py-emoji-keys — extras at top-level (outside the `ucp` envelope) with non-ASCII + # py-emoji-keys: extras at top-level (outside the `ucp` envelope) with non-ASCII # object keys (BMP private use, CJK compatibility, supplementary plane). # Exercises codepoint-vs-UTF-16 sort. kid = "py-emoji-keys-EdDSA" @@ -239,7 +239,7 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-emoji-keys", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip via top-level extras. + # py-int-boundary: exercises Number.MAX_SAFE_INTEGER round-trip via top-level extras. kid = "py-int-boundary-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( @@ -257,7 +257,7 @@ 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-agentscore-gate-full — exercises build_ucp_profile with a fully-populated + # 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" @@ -276,7 +276,7 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-agentscore-gate-full", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-agentscore-gate-blocked — exercises blocked_jurisdictions + # 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) diff --git a/tests/test_address.py b/tests/test_address.py index b8d2b1b..8669a79 100644 --- a/tests/test_address.py +++ b/tests/test_address.py @@ -1,4 +1,4 @@ -"""Address normalization tests — must produce identical results to the node SDK +"""Address normalization tests: must produce identical results to the node SDK so EVM and Solana addresses are normalized identically across both SDK languages.""" from __future__ import annotations @@ -31,7 +31,7 @@ def test_accepts_real_solana_pubkeys(self): assert is_solana_address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") def test_rejects_evm_addresses(self): - # 0x... could match the base58 alphabet — explicit guard prevents routing + # 0x... could match the base58 alphabet: explicit guard prevents routing # an EVM address into the Solana code path. assert not is_solana_address("0x690BF056DA820EF2e74f8943B3Fe5ca4ADEe7a3e") @@ -67,5 +67,5 @@ def test_preserves_solana_case(self): def test_falls_through_to_lowercase_for_unrecognized(self): # Garbage still returns SOMETHING so callers don't need an is-valid guard - # before normalizing — DB writes are guarded separately. + # before normalizing: DB writes are guarded separately. assert normalize_address("NotAnAddress") == "notanaddress" diff --git a/tests/test_agent_memory_emitter.py b/tests/test_agent_memory_emitter.py index 18d4a17..a28a67f 100644 --- a/tests/test_agent_memory_emitter.py +++ b/tests/test_agent_memory_emitter.py @@ -14,13 +14,13 @@ def test_returns_canonical_hint_when_first_encounter_true(): def test_hint_strings_byte_match_node_commerce_for_wire_parity(): - """Cross-language wire parity — these exact strings appear in agent_memory bodies emitted + """Cross-language wire parity: these exact strings appear in agent_memory bodies emitted by both @agent-score/commerce (node-commerce/src/core.ts buildAgentMemoryHint) and this package. Agents that memorize the pattern from one merchant must recognize it byte-for-byte from another regardless of which SDK the merchant runs. """ hint = build_agent_memory_hint() - # Backticks-around-header markdown is intentional — node-commerce uses them for monospace + # Backticks-around-header markdown is intentional: node-commerce uses them for monospace # rendering in markdown-aware viewers (chat surfaces, dashboards). Python must match. assert hint.identity_paths["wallet"].endswith( "`X-Wallet-Address: 0x...`. Shortest path; no token lifecycle to manage." diff --git a/tests/test_aiohttp.py b/tests/test_aiohttp.py index 85dbfd8..2c06233 100644 --- a/tests/test_aiohttp.py +++ b/tests/test_aiohttp.py @@ -428,7 +428,7 @@ async def test_no_ops_when_wallet_authenticated(self): @pytest.mark.asyncio async def test_no_ops_when_gate_did_not_run(self): - # Handler wired without the gate middleware — capture_wallet must silently no-op. + # Handler wired without the gate middleware: capture_wallet must silently no-op. app = web.Application() app.router.add_post("/", _capture_handler) with patch("agentscore_commerce.identity.core.AgentScoreCore.acapture_wallet", new=AsyncMock()) as mock_cap: @@ -515,7 +515,7 @@ async def handler(_req): @respx.mock async def test_aiohttp_handler_exception_is_not_swallowed_by_gate(): """Regression: gate's try-block must NOT wrap downstream handler. If the user's - handler raises, the exception must propagate up — NOT be misclassified as an + handler raises, the exception must propagate up: NOT be misclassified as an AgentScore infra failure (which under fail_open would re-invoke the handler).""" respx.post("https://api.agentscore.com/v1/assess").mock( return_value=httpx.Response(200, json={"decision": "allow", "decision_reasons": []}), @@ -534,7 +534,7 @@ async def boom_handler(_req): async with TestClient(TestServer(app)) as client: resp = await client.get("/", headers={"x-wallet-address": "0xabc"}) - # aiohttp surfaces an unhandled exception as 500 — the important thing is the + # aiohttp surfaces an unhandled exception as 500: the important thing is the # handler ran exactly once (no fail-open retry), and the gate didn't claim # the exception was an AgentScore infra failure. assert resp.status == 500 diff --git a/tests/test_aip_adapters.py b/tests/test_aip_adapters.py index db20916..6622666 100644 --- a/tests/test_aip_adapters.py +++ b/tests/test_aip_adapters.py @@ -5,7 +5,7 @@ The Python adapters with an AIP gate are: the ASGI middleware (Starlette / FastAPI via ``add_middleware``), the FastAPI ``AipGate`` / ``ConditionalAipGate`` dependencies, and the aiohttp ``aip_gate_middleware``. Each is exercised end-to-end with a real signed AIT, and the parts-based -entry points (``verify_ait_parts`` / ``build_verify_context_from_parts``) are tested directly — the +entry points (``verify_ait_parts`` / ``build_verify_context_from_parts``) are tested directly: the same coverage shape as the node suite. """ @@ -238,7 +238,7 @@ def test_allows_a_valid_ait_and_denies_a_missing_one(self) -> None: denied = client.post("/checkout", headers={"host": AUTHORITY}) assert denied.status_code == 401 - # FLAT application/problem+json document — body["type"], not nested under "detail". + # FLAT application/problem+json document: body["type"], not nested under "detail". assert denied.headers["content-type"].startswith("application/problem+json") assert denied.json()["type"] == "urn:aip:error:agent_identity_required" diff --git a/tests/test_aip_checkout.py b/tests/test_aip_checkout.py index e2ad893..fcd3fda 100644 --- a/tests/test_aip_checkout.py +++ b/tests/test_aip_checkout.py @@ -12,11 +12,11 @@ The full happy-path verify (issuer JWKS + RFC 9421 PoP) is covered at the ``verify_ait_parts`` level in test_aip_verify / test_aip_gate. Here we assert the orchestrator contract: the invalid -cases (which fail before any JWKS fetch — no network needed), the offline (no-api_key) policy / +cases (which fail before any JWKS fetch: no network needed), the offline (no-api_key) policy / trust enforcement, issuer-conditional policy, and the assess-forwarding of token + signature. The gate runs only on the settle leg (a payment credential attached), so each request carries an -``x-payment`` header — otherwise ``handle`` treats it as anonymous discovery and emits a 402. +``x-payment`` header: otherwise ``handle`` treats it as anonymous discovery and emits a 402. """ from __future__ import annotations @@ -300,7 +300,7 @@ async def test_fails_closed_when_policy_bearing_gate_has_no_api_key(self) -> Non async def test_allows_valid_ait_on_identity_only_gate(self) -> None: # Identity-only gate (no policy fields) + verified AIT + no api_key → the gate returns # None (allow) and Checkout proceeds to x402 settle. The stub x-payload then fails - # verification (400 verify_failed) — but reaching settle at all proves the gate did NOT + # verification (400 verify_failed): but reaching settle at all proves the gate did NOT # block with aip_policy_requires_api_key. res = await _offline_gate().handle(_req(_signed_headers(_mint_ait(identity={"id_verified": True})))) assert res.body.get("error", {}).get("code") != "aip_policy_requires_api_key" @@ -430,7 +430,7 @@ async def test_empty_issuer_override_drops_policy_and_allows(self, _two_issuer_k assert res.settle_phase == "verify_failed" async def test_matches_issuer_override_after_canonicalization(self, _two_issuer_keys: dict[str, OKPKey]) -> None: - # Key has a trailing slash; verified iss is 'https://issuer.example' — must still match. + # Key has a trailing slash; verified iss is 'https://issuer.example': must still match. gate = self._gate(_two_issuer_keys[OURS], {"https://issuer.example/": AipIssuerPolicy()}) res = await gate.handle(self._signed_from(ISS, _idp, KID, {"email_verified": True})) assert res.body.get("error", {}).get("code") != "aip_policy_requires_api_key" @@ -499,7 +499,7 @@ async def deny(**_kwargs: Any) -> dict[str, Any]: assert "sanctions_flagged" in res.body["detail"] # Escalation hint derived from the gate's effective policy. assert res.body["required_claims"] == ["sanctions_clear", "age_over_21"] - # Rich AgentScore scheme preserved verbatim — still the agent's source of truth. + # Rich AgentScore scheme preserved verbatim: still the agent's source of truth. assert res.body["error"]["code"] == "wallet_not_trusted" assert res.body["reasons"] == ["sanctions_flagged"] assert "agent_instructions" in res.body @@ -641,7 +641,7 @@ def _discovery_req() -> CheckoutRequest: def _missing_identity_app(gate: Any) -> Any: - """Mount an AgentScoreGate on a minimal FastAPI app via Depends — the same chain + """Mount an AgentScoreGate on a minimal FastAPI app via Depends: the same chain Checkout's missing-identity path drives (build_gate_from_policy → AgentScoreGate).""" from fastapi import Depends, FastAPI @@ -660,7 +660,7 @@ class TestCheckoutAipEmittedBodyAdvertisesAip: """Regression: the EMITTED 402 + missing-identity bodies must advertise AIP. The memory-hint builder (``build_agent_memory_hint``) is unit-tested in isolation in - test_aip_agent_memory, but neither emit site was wired to pass ``aip_trusted_issuers`` — + test_aip_agent_memory, but neither emit site was wired to pass ``aip_trusted_issuers``: so the advertisement silently dropped on the wire. These exercise the real emit paths and assert ``agent_memory`` carries ``aip_trusted_issuers`` + the ``agent_identity`` path. Ports node-commerce ``tests/agent_memory_aip.test.ts`` to the orchestrator level. @@ -676,7 +676,7 @@ async def test_emitted_402_body_advertises_aip_when_gate_accepts_it(self) -> Non assert ISS in memory["aip_trusted_issuers"] assert "Agent-Identity" in memory["identity_paths"]["agent_identity"] assert "RFC 9421" in memory["identity_paths"]["agent_identity"] - # AIP is additive — the wallet + operator_token paths remain present. + # AIP is additive: the wallet + operator_token paths remain present. assert memory["identity_paths"]["wallet"] assert memory["identity_paths"]["operator_token"] @@ -739,8 +739,8 @@ class TestCheckoutPerRequestPolicyEnforcement: """Regression: Checkout's per-request-policy gate must FIRE, not be silently bypassed. ``_run_gate`` previously popped ``enforcement`` out of the merged policy before calling - ``build_gate_from_policy`` — which keys off ``enforcement`` to decide whether to build a - gate at all — so the gate came back ``None`` and a settle-leg request bypassed compliance + ``build_gate_from_policy``: which keys off ``enforcement`` to decide whether to build a + gate at all: so the gate came back ``None`` and a settle-leg request bypassed compliance and proceeded to settle. (Not AIP-specific; surfaced during the AIP parity audit.) A settle leg with no resolvable identity must now be DENIED with ``missing_identity``. """ @@ -754,7 +754,7 @@ async def test_per_request_policy_gate_is_built_not_bypassed(self) -> None: # Proof the gate now FIRES: once built, the framework-agnostic handle() demands the # native request object the per-request FastAPI gate needs. Under the bug (enforcement # popped -> build_gate_from_policy returns None) there was no gate, no such demand, and - # the settle leg proceeded — silently bypassing compliance. (The built gate's actual + # the settle leg proceeded: silently bypassing compliance. (The built gate's actual # missing-identity denial is covered by TestCheckoutAipEmittedBodyAdvertisesAip.) with pytest.raises(RuntimeError, match="requires CheckoutRequest"): await checkout.handle(_req({})) @@ -765,7 +765,7 @@ class TestCheckoutStaticGateEnforcement: FIRE the gate, not silently bypass ALL compliance. ``_run_gate`` builds ``merged_policy`` from the static gate fields (require_kyc / sanctions / - min_age / jurisdictions) but those fields NEVER include an ``enforcement`` key — that only + min_age / jurisdictions) but those fields NEVER include an ``enforcement`` key: that only ever comes from a ``per_request_policy`` hook. So ``enforcement`` resolved to ``None`` → ``build_gate_from_policy`` returned ``None`` (no enforcement => no gate) → ``run_gate_with_enforcement(None, None)`` short-circuited to status="anonymous" (ALLOW), diff --git a/tests/test_aip_gate.py b/tests/test_aip_gate.py index 451392f..62ba260 100644 --- a/tests/test_aip_gate.py +++ b/tests/test_aip_gate.py @@ -233,7 +233,7 @@ def test_canonical_body_fields_ride_along_verbatim(self) -> None: def test_merchant_extra_cannot_clobber_the_problem_json_envelope(self) -> None: # `body` carries merchant `extra` passthrough fields (on_before_session hook); a hook - # echoing `status`/`type`/`title`/`detail` must not override the canonical envelope — + # echoing `status`/`type`/`title`/`detail` must not override the canonical envelope: # the caller derives the HTTP status from `superset["status"]`. from agentscore_commerce.aip.gate import build_aip_policy_deny_body diff --git a/tests/test_aip_http_signature.py b/tests/test_aip_http_signature.py index 009c3aa..558f42f 100644 --- a/tests/test_aip_http_signature.py +++ b/tests/test_aip_http_signature.py @@ -1,4 +1,4 @@ -"""RFC 9421 HTTP Message Signature (AIP subset) — sign / verify + cross-language conformance. +"""RFC 9421 HTTP Message Signature (AIP subset): sign / verify + cross-language conformance. Ports node-commerce ``tests/aip_http_signature.test.ts``. Two things are pinned here: @@ -32,7 +32,7 @@ ) from agentscore_commerce.aip.http_signature import SignatureParams, _calculate_jwk_thumbprint -# Filter joserfc's EdDSA deprecation SecurityWarning (RFC 9864) for the whole module — AIP pins +# Filter joserfc's EdDSA deprecation SecurityWarning (RFC 9864) for the whole module: AIP pins # Ed25519 as its only signing curve, so the warning is expected and not actionable here. pytestmark = pytest.mark.filterwarnings("ignore::UserWarning") @@ -61,7 +61,7 @@ def _make_key() -> tuple[dict, dict, str]: def _round_trip(**overrides: object): # The verifier now REQUIRES `expires` (replay-window hardening), so default to a 60s window # (matching pay's signer) unless a test overrides it. `sign_message` itself omits `expires` by - # default — that's only the serialization-format default, exercised explicitly below. + # default: that's only the serialization-format default, exercised explicitly below. created = overrides.get("created") expires_default = (created + 60) if isinstance(created, int) else None args = { @@ -135,7 +135,7 @@ def test_emits_one_line_per_component_plus_signature_params_no_trailing_newline( assert not base.endswith("\n") def test_raises_when_a_covered_component_has_no_value(self) -> None: - with pytest.raises(Exception): # noqa: B017 — _MissingComponentError is private + with pytest.raises(Exception): # noqa: B017 # _MissingComponentError is private build_signature_base( SignatureParams(components=["@method", "x-missing"]), method=BASE_REQ["method"], @@ -263,7 +263,7 @@ def test_rejects_when_cnf_jwk_is_a_different_key_keyid_mismatch(self) -> None: # created+expires present so the sig reaches the keyid check, not the time-bound gates. expires=1715400060, ) - # present the wrong cnf (our original key) — keyid in the sig won't match its thumbprint + # present the wrong cnf (our original key): keyid in the sig won't match its thumbprint r = verify_message_signature( **BASE_REQ, # type: ignore[arg-type] signature_input=sm.signature_input, @@ -307,7 +307,7 @@ def test_rejects_a_signature_missing_created(self) -> None: assert (r.ok, r.reason) == (False, "created_missing") def test_rejects_a_signature_missing_expires(self) -> None: - # sign_message omits `expires` by default — exactly the spec-loose shape the hardening rejects. + # sign_message omits `expires` by default: exactly the spec-loose shape the hardening rejects. sm = sign_message( **BASE_REQ, # type: ignore[arg-type] private_jwk=PRIVATE_JWK, @@ -512,7 +512,7 @@ def test_python_verify_accepts_pays_real_signer_output_cross_repo(self) -> None: assert r.ok is True def test_python_verify_rejects_the_node_api_vector_missing_expires(self) -> None: - # The byte-pinned node/api signMessage vector carries `created` but OMITS `expires` — exactly + # The byte-pinned node/api signMessage vector carries `created` but OMITS `expires`: exactly # the spec-loose shape the replay-window hardening rejects. (Mirrors core/api's conformance # test, which now asserts `expires_missing` for this same vector.) The WITH-`expires` accept # path is covered by the pay vector above and the explicit accept test below. diff --git a/tests/test_aip_jwks.py b/tests/test_aip_jwks.py index 4a2581f..e853b05 100644 --- a/tests/test_aip_jwks.py +++ b/tests/test_aip_jwks.py @@ -111,7 +111,7 @@ def test_returns_none_for_non_urls(self) -> None: assert canonicalize_issuer("not a url") is None def test_returns_none_for_malformed_authorities_instead_of_raising(self) -> None: - # ``iss`` comes from the UNVERIFIED JWT payload — these used to raise ValueError + # ``iss`` comes from the UNVERIFIED JWT payload: these used to raise ValueError # (urlsplit / .port) and crash the verifier with a 500. assert canonicalize_issuer("https://host:abc") is None assert canonicalize_issuer("https://host:99999999") is None @@ -200,7 +200,7 @@ async def test_serves_a_second_lookup_from_cache_no_second_fetch(self) -> None: assert len(fetch.calls) == 1 # type: ignore[attr-defined] async def test_refetches_once_on_a_kid_miss_past_the_cooldown(self) -> None: - # A kid-miss forces one refetch (rotation may have published the key) — but only ONCE the + # A kid-miss forces one refetch (rotation may have published the key): but only ONCE the # per-issuer refetch cooldown has elapsed. WITHIN the cooldown a kid-miss is suppressed (the # DoS guard); past it, a single refetch is allowed and the rotated key resolves. from agentscore_commerce.aip import JWKS_REFETCH_COOLDOWN_SECONDS @@ -235,7 +235,7 @@ async def test_unknown_kid_flood_is_suppressed_by_the_refetch_cooldown(self) -> First lookup of an unknown kid warms the cache (one fetch) and stamps the per-issuer cooldown; subsequent lookups of the same unknown kid within the cooldown window - short-circuit to key_not_found WITHOUT another upstream fetch — bounding an attacker's + short-circuit to key_not_found WITHOUT another upstream fetch: bounding an attacker's unknown-kid flood to ~one fetch per issuer per cooldown, mirroring the API verifier. (The distinct-kid variant is covered by test_distinct_unknown_kid_flood_is_bounded_*.) """ @@ -343,7 +343,7 @@ async def fetch(url: str, headers: dict) -> _FakeResponse: miss = await c.get_key("https://issuer.example", "key-B") assert (miss.ok, miss.reason) == (False, "key_not_found") assert state["n"] == 1 - # Immediate retry within the cooldown is suppressed (no 2nd fetch) — still key_not_found. + # Immediate retry within the cooldown is suppressed (no 2nd fetch): still key_not_found. suppressed = await c.get_key("https://issuer.example", "key-B") assert (suppressed.ok, suppressed.reason) == (False, "key_not_found") assert state["n"] == 1 @@ -401,7 +401,7 @@ async def test_sequential_failures_within_the_cooldown_issue_exactly_one_fetch(s """Negative cache: a FAILED fetch stamps the refetch cooldown too. Without it, a failing issuer leaves no cache entry, so every sequential request refetched - upstream — the failure path bypassed the DoS cooldown that bounds the success path. + upstream: the failure path bypassed the DoS cooldown that bounds the success path. """ from agentscore_commerce.aip import JWKS_REFETCH_COOLDOWN_SECONDS @@ -455,7 +455,7 @@ def test_cold_cache_lookups_across_threads_each_running_asyncio_run_all_succeed( """Loop-aware single-flight: threaded WSGI (Flask/Django) runs ``asyncio.run`` per request. The old single-flight parked concurrent cold-cache callers on a Future created on ANOTHER - thread's loop — awaiting it raised RuntimeError → 500. Cross-loop callers must now fetch + thread's loop: awaiting it raised RuntimeError → 500. Cross-loop callers must now fetch independently instead of awaiting the foreign future; every caller succeeds. """ import asyncio diff --git a/tests/test_aip_request.py b/tests/test_aip_request.py index 7f78691..8b528ca 100644 --- a/tests/test_aip_request.py +++ b/tests/test_aip_request.py @@ -99,7 +99,7 @@ def test_false_for_an_empty_header_value(self) -> None: assert has_agent_identity_header(_make({"agent-identity": ""})) is False -# ── build_verify_context_from_parts — @path derivation matches the signer ── +# ── build_verify_context_from_parts: @path derivation matches the signer ── class TestBuildVerifyContextFromPartsPathDerivation: diff --git a/tests/test_aip_verify.py b/tests/test_aip_verify.py index 51b8169..7f4b5fb 100644 --- a/tests/test_aip_verify.py +++ b/tests/test_aip_verify.py @@ -249,7 +249,7 @@ async def test_rejects_an_ait_with_an_absurdly_long_lifetime(self) -> None: async def test_rejects_an_ait_whose_lifetime_exceeds_the_300s_edge_ceiling(self) -> None: # A 600s-lifetime AIT (under the old 3600 default, over the new 300) is now rejected at the - # edge, matching the authoritative API verifier. The PoP is fresh and exp is ahead of now — + # edge, matching the authoritative API verifier. The PoP is fresh and exp is ahead of now: # only the exp-iat span is the problem. (Lowered 3600 -> 300.) ctx = signed_ctx(mint_ait(iat=NOW - 10, exp=NOW + 590)) # 600s lifetime > 300s r = await verify_ait(ctx, jwks=jwks_for(IDP_PUBLIC_JWK), now=NOW) @@ -312,7 +312,7 @@ async def test_rejects_a_request_whose_path_was_tampered_after_signing(self) -> async def test_rejects_does_not_throw_on_an_ait_bound_to_a_p256_cnf_key(self) -> None: # The PoP verifier is Ed25519-only. A structurally-valid AIT whose cnf is a P-256 EC key - # must return a typed failure, NOT crash the gate. Sign with the normal Ed25519 agent key — + # must return a typed failure, NOT crash the gate. Sign with the normal Ed25519 agent key: # the verifier rejects on the cnf key type first. ec = ECKey.import_key(generate_private_key(SECP256R1())) ec_pub = ec.as_dict(private=False) diff --git a/tests/test_challenge.py b/tests/test_challenge.py index b9b7117..bbbec2d 100644 --- a/tests/test_challenge.py +++ b/tests/test_challenge.py @@ -226,7 +226,7 @@ def test_build_402_body_assembles_full_response(): def test_build_402_body_keeps_accepts_byte_identical(): - """accepts entries pass through unchanged — no v1 maxAmountRequired alias. + """accepts entries pass through unchanged: no v1 maxAmountRequired alias. @x402/core matches v2 by whole-object deepEqual of the echoed requirement, so an extra maxAmountRequired the server's rebuild lacks silently fails settle. diff --git a/tests/test_checkout.py b/tests/test_checkout.py index 43f7ef8..0572ad4 100644 --- a/tests/test_checkout.py +++ b/tests/test_checkout.py @@ -45,7 +45,7 @@ def _req(*, headers: dict[str, str] | None = None, body: dict[str, Any] | None = # ───────────────────────────────────────────────────────────────────────────── -# 402 emit — every rail combination +# 402 emit: every rail combination # ───────────────────────────────────────────────────────────────────────────── @@ -57,7 +57,7 @@ async def test_emit_402_x402_only_no_mppx_no_identity() -> None: url="https://api.example/call", compute_pricing=lambda _ctx: PricingResult(amount_usd=0.01), x402_server=None, - # x402_base_network omitted — emit-only, no settle handler + # x402_base_network omitted: emit-only, no settle handler ) result = await checkout.handle(_req()) assert result.status == 402 @@ -309,7 +309,7 @@ async def test_emit_402_custodial_only_stripe() -> None: class _StubX402Server: - """Minimal x402 server fake — exercises settle path without real x402 deps. + """Minimal x402 server fake: exercises settle path without real x402 deps. Mirrors x402 2.9's ``x402ResourceServer`` surface enough to pass ``process_x402_settle``: ``build_payment_requirements(config) -> [req]``, @@ -370,7 +370,7 @@ async def test_x402_settle_success_runs_on_settled_hook() -> None: """Goods seller: on_settled persists the order; success body merges reference_id.""" on_settled = AsyncMock(return_value={"order_status": "queued"}) checkout = Checkout( - # Recipient must equal the payload's signed payTo — the gate binds the agent-supplied payTo + # Recipient must equal the payload's signed payTo: the gate binds the agent-supplied payTo # to the configured recipient (payTo-binding fix), so they must match for the settle to run. rails={"x402_base": X402BaseRailSpec(recipient="0x000000000000000000000000000000000000dEaD")}, url="https://api.example/purchase", @@ -443,7 +443,7 @@ async def test_x402_settle_custom_is_cached_address_still_honored() -> None: url="https://api.example/purchase", compute_pricing=lambda _ctx: PricingResult(amount_usd=0.01), x402_server=_StubX402Server(settle_success=True), - # Merchant attests this minted payTo belongs to this order — accept it even though it's + # Merchant attests this minted payTo belongs to this order: accept it even though it's # not the static recipient. is_cached_address=lambda addr: addr.lower() == "0xfeedfacefeedfacefeedfacefeedfacefeedface", ) @@ -456,7 +456,7 @@ async def test_x402_settle_custom_is_cached_address_still_honored() -> None: @pytest.mark.asyncio async def test_x402_default_rails_empty_recipient_sentinel_binds_to_minted_pay_to() -> None: - """Regression: the documented per-order-mint default — ``build_default_checkout_rails`` leaves + """Regression: the documented per-order-mint default: ``build_default_checkout_rails`` leaves ``recipient=""`` (the sentinel) and the real payTo is minted per request via ``mint_recipients``. Previously the empty-string sentinel passed through ``_resolve_static_x402_recipient`` and the @@ -606,7 +606,7 @@ async def on_settled(ctx: CheckoutContext, _outcome: object) -> None: @pytest.mark.asyncio async def test_compose_mppx_payment_receipt_header_surfaces_on_response() -> None: """When ``compose_mppx`` populates ``payment_receipt_header``, Checkout echoes - it as a ``payment-receipt`` HTTP header on the success response — symmetric + it as a ``payment-receipt`` HTTP header on the success response: symmetric to the existing ``payment_response_header`` (x402) behavior.""" receipt_header = "eyJzdGF0dXMiOiJzdWNjZXNzIn0" compose_mppx = AsyncMock( @@ -685,7 +685,7 @@ def to_payment_receipt() -> str: @pytest.mark.asyncio async def test_compose_mppx_auto_extracts_receipt_header_from_raw_tuple() -> None: """``raw=(credential, receipt)`` (the pympp Mpp.charge return) is also a - recognized shape — the second element's ``to_payment_receipt()`` is lifted.""" + recognized shape: the second element's ``to_payment_receipt()`` is lifted.""" class _Receipt: @staticmethod @@ -940,7 +940,7 @@ async def mint() -> str: def test_init_requires_x402_base_railspec_when_x402_server_provided() -> None: - """x402_server demands an X402BaseRailSpec in rails['x402_base'] — the rail's + """x402_server demands an X402BaseRailSpec in rails['x402_base']: the rail's `network` field carries the CAIP-2, so there's no separate kwarg to forget.""" with pytest.raises(ValueError, match="X402BaseRailSpec"): Checkout( diff --git a/tests/test_checkout_compute_first.py b/tests/test_checkout_compute_first.py index 583de7c..450f83f 100644 --- a/tests/test_checkout_compute_first.py +++ b/tests/test_checkout_compute_first.py @@ -107,7 +107,7 @@ async def test_mpp_settle_with_no_compose_hook_returns_503() -> None: ) # First do probe to seed cache await handler.handle(_build_request()) - # Now settle on MPP — but no compose_mppx wired → 503 mpp_unavailable + # Now settle on MPP: but no compose_mppx wired → 503 mpp_unavailable status, body, _headers = await handler.handle(_build_request(headers={"authorization": "Payment "})) assert status == 503 assert body["error"]["code"] == "mpp_unavailable" @@ -134,7 +134,7 @@ async def _broken(_body: dict[str, Any], _ctx: ComputeFirstWorkContext) -> WorkO @pytest.mark.asyncio async def test_probe_leg_emits_402_with_pricing_and_retry_body() -> None: - """Exercise the _emit_402 path — work returns 1 result, probe caches + + """Exercise the _emit_402 path: work returns 1 result, probe caches + emits a 402 with accepted methods, pricing block, retry_body.""" handler = ComputeFirstCheckout( @@ -185,7 +185,7 @@ async def _record(body: dict[str, Any], _ctx: ComputeFirstWorkContext) -> WorkOu @pytest.mark.asyncio async def test_fractional_unit_price_auto_derives_decimals() -> None: - """Sub-cent pricing — auto-derive precision from unit_price_cents.""" + """Sub-cent pricing: auto-derive precision from unit_price_cents.""" handler = ComputeFirstCheckout( name="tokens", diff --git a/tests/test_checkout_compute_first_settle.py b/tests/test_checkout_compute_first_settle.py index 161b812..42769c9 100644 --- a/tests/test_checkout_compute_first_settle.py +++ b/tests/test_checkout_compute_first_settle.py @@ -150,7 +150,7 @@ async def test_invalid_x402_header_returns_400() -> None: @pytest.mark.asyncio async def test_x402_settle_rejects_agent_controlled_pay_to() -> None: """payTo-binding (funds-drain guard): a payload whose signed payTo points at an - AGENT-controlled wallet — not the configured x402_base recipient — is rejected before any + AGENT-controlled wallet (not the configured x402_base recipient) is rejected before any on-chain settle, so the agent can't re-route funds away from the merchant. """ server = _make_fake_x402_server() @@ -170,7 +170,7 @@ async def test_x402_settle_rejects_agent_controlled_pay_to() -> None: _req(headers={"x-payment": _x402_header(pay_to=attacker_pay_to)}, body=body) ) assert 400 <= status < 500 - # Rejected at verification — settle_payment must NOT have run for the swapped recipient. + # Rejected at verification: settle_payment must NOT have run for the swapped recipient. server.settle_payment.assert_not_called() assert response_body["error"]["code"] in ("payment_proof_invalid", "payment_required") @@ -182,7 +182,7 @@ async def test_x402_on_settled_hook_fires_and_errors_caught() -> None: async def _on_settled(ctx: ComputeFirstSettledContext) -> None: settled_calls.append(ctx) - raise RuntimeError("hook broken — should be caught") + raise RuntimeError("hook broken: should be caught") handler = ComputeFirstCheckout( name="x402_onsettled", diff --git a/tests/test_checkout_signer_match.py b/tests/test_checkout_signer_match.py index 8c59bd6..c42cd0f 100644 --- a/tests/test_checkout_signer_match.py +++ b/tests/test_checkout_signer_match.py @@ -23,13 +23,13 @@ ASSESS_URL = "https://api.agentscore.com/v1/assess" CLAIMED_WALLET = "0x1111111111111111111111111111111111111111" -# The payment signer recovered from the x402 payload — a DIFFERENT wallet than claimed. +# The payment signer recovered from the x402 payload: a DIFFERENT wallet than claimed. ACTUAL_SIGNER = "0x2222222222222222222222222222222222222222" LINKED_WALLET = "0x3333333333333333333333333333333333333333" class _StubX402Server: - """Settle path fake — only reached if the gate WRONGLY allowed the mismatch.""" + """Settle path fake: only reached if the gate WRONGLY allowed the mismatch.""" def build_payment_requirements(self, _config: Any) -> list[Any]: return [{"scheme": "exact", "network": "eip155:8453"}] @@ -150,14 +150,14 @@ async def test_signer_mismatch_on_wallet_signing_rail_denies_not_settles() -> No # actual_signer_operator is always emitted for wallet_signer_mismatch (string = signer resolves # to a DIFFERENT operator; the assess mock returns signer_operator="op_other"). assert body["actual_signer_operator"] == "op_other" - # PARITY (issue #5): Checkout emits the SAME body shape as node-commerce's Checkout.runGate — + # PARITY (issue #5): Checkout emits the SAME body shape as node-commerce's Checkout.runGate: # the recovery hint rides in `agent_instructions` (denial_reason_to_body), NOT in the standalone # build_signer_mismatch_body helper's `next_steps` container. So: agent_instructions present with # the canonical resign action, and NO next_steps key. assert "next_steps" not in body instructions = json.loads(body["agent_instructions"]) assert instructions["action"] == "resign_or_switch_to_operator_token" - # CRITICAL: the settle never ran — no on-chain capture for a mismatched signer. + # CRITICAL: the settle never ran: no on-chain capture for a mismatched signer. assert "transaction" not in body diff --git a/tests/test_checkout_wallet_ofac_default.py b/tests/test_checkout_wallet_ofac_default.py index d0d89ff..b943670 100644 --- a/tests/test_checkout_wallet_ofac_default.py +++ b/tests/test_checkout_wallet_ofac_default.py @@ -119,7 +119,7 @@ async def test_clean_signer_with_no_gate_allows_settle_to_proceed( checkout = _checkout(gate=None) request = _req(headers={"x-payment": _x402_payment_header(CLEAN_WALLET)}) result = await checkout.handle(request) - # No x402 server configured; the settle path will fail downstream — but the + # No x402 server configured; the settle path will fail downstream: but the # OFAC gate ITSELF must have allowed (not denied). status != 403. assert result.status != 403 or "wallet_not_trusted" not in str(result.body) @@ -253,7 +253,7 @@ async def _raise(*args: Any, **kwargs: Any) -> None: @pytest.mark.asyncio async def test_api_outage_fails_closed(monkeypatch: pytest.MonkeyPatch) -> None: - """When /v1/assess raises (network failure / 5xx), return 503 — strict + """When /v1/assess raises (network failure / 5xx), return 503: strict liability fail-closed.""" monkeypatch.setenv("AGENTSCORE_API_KEY", "ask_test_key") diff --git a/tests/test_classify_orchestration_error.py b/tests/test_classify_orchestration_error.py index b83aeb5..ef8b508 100644 --- a/tests/test_classify_orchestration_error.py +++ b/tests/test_classify_orchestration_error.py @@ -1,4 +1,4 @@ -"""Tests for ``classify_orchestration_error`` — string-match classification of +"""Tests for ``classify_orchestration_error``: string-match classification of arbitrary thrown errors during the 402 orchestration. Locked cross-language fixtures shared with the Node sibling at @@ -28,7 +28,7 @@ ("facilitator_lowercase", "Facilitator unreachable", "payment_provider_unavailable"), ("cdp_lowercase", "CDP JWT expired", "payment_provider_unavailable"), ("stripe_uppercase", "STRIPE timeout", "payment_provider_unavailable"), - # Unknown — caller rethrows + # Unknown: caller rethrows ("database_error", "duplicate key value violates unique constraint", None), ("network_error", "ECONNREFUSED", None), ("empty_string", "", None), diff --git a/tests/test_core.py b/tests/test_core.py index 4b75daa..a33f842 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -352,7 +352,7 @@ def test_check_raises_quota_exceeded_on_429(self): @respx.mock def test_check_raises_quota_exceeded_on_typed_429(self): - """SDK emits typed QuotaExceededError when body has error.code='quota_exceeded' — + """SDK emits typed QuotaExceededError when body has error.code='quota_exceeded': commerce wraps it so callers get the gate's QuotaExceededError sentinel. """ from agentscore_commerce.identity.core import QuotaExceededError @@ -711,7 +711,7 @@ def test_cache_key_folds_in_the_signer_when_present(self): def test_cache_key_is_delimiter_injection_proof(self): # A crafted (invalid) claimed X-Wallet-Address embedding the literal `|sig:` joiner must - # NOT collide with the structured key of a real (identity, signer) pair — that collision + # NOT collide with the structured key of a real (identity, signer) pair: that collision # would let the crafted identity poison the pair's cached signer verdicts. client = _make_client() crafted = client._cache_key(address="0xABC|sig:0xEVIL:base") @@ -743,7 +743,7 @@ def test_build_body_address_only_backwards_compat(self): class TestInvalidCredential: - """Coverage for the 401 invalid_credential branch — distinct from token_expired + """Coverage for the 401 invalid_credential branch: distinct from token_expired in that no auto-session is minted. The client surfaces it as InvalidCredentialError so adapters can render a permanent-failure 403 instead of a transient 503 retry.""" @@ -790,14 +790,14 @@ def test_build_invalid_credential_reason_carries_action_copy(self): assert reason.agent_instructions is not None instructions = json.loads(reason.agent_instructions) assert instructions["action"] == "switch_token_or_restart_session" - # No session fields — the API doesn't mint one for this case. + # No session fields: the API doesn't mint one for this case. assert reason.session_id is None assert reason.verify_url is None assert reason.poll_secret is None class TestAcheckTypedErrors: - """Async path mirror of TestCheckFailOpen — exercises SdkXxxError → commerce-error mapping + """Async path mirror of TestCheckFailOpen: exercises SdkXxxError → commerce-error mapping in :meth:`acheck`. Pinned independently of the sync path because adapters wire each path separately and a regression in one wouldn't show up via the other. """ diff --git a/tests/test_default_read_only_on_denied.py b/tests/test_default_read_only_on_denied.py index 658977c..c1f4d2a 100644 --- a/tests/test_default_read_only_on_denied.py +++ b/tests/test_default_read_only_on_denied.py @@ -1,4 +1,4 @@ -"""Tests for ``default_read_only_on_denied`` — read-only resource gate denial.""" +"""Tests for ``default_read_only_on_denied``: read-only resource gate denial.""" from agentscore_commerce.identity.default_denied import default_read_only_on_denied from agentscore_commerce.identity.types import DenialReason diff --git a/tests/test_denial.py b/tests/test_denial.py index d149295..9c88c33 100644 --- a/tests/test_denial.py +++ b/tests/test_denial.py @@ -46,11 +46,11 @@ def test_known_fixable_reasons_in_set(self): def test_jurisdiction_restricted_is_unfixable(self): # The API only emits jurisdiction_restricted AFTER KYC is verified, meaning the # user's KYC'd country is in the merchant's blocked list. Re-doing KYC won't - # change the country — same shape as sanctions_flagged / age_insufficient. + # change the country: same shape as sanctions_flagged / age_insufficient. assert "jurisdiction_restricted" not in FIXABLE_DENIAL_REASONS def test_empty_or_none_returns_false(self): - # Without a known reason we can't promise a fix — default to bare denial. + # Without a known reason we can't promise a fix: default to bare denial. assert not is_fixable_denial(None) assert not is_fixable_denial([]) diff --git a/tests/test_django.py b/tests/test_django.py index 0f260d9..6d13738 100644 --- a/tests/test_django.py +++ b/tests/test_django.py @@ -596,7 +596,7 @@ def test_constructor_chain_stored_and_forwarded(self) -> None: def test_handler_exception_is_not_swallowed_by_gate(self) -> None: """Regression: gate's try-block must NOT wrap the downstream view (`get_response`). - If the user's view raises, the exception must propagate up — NOT be misclassified as + If the user's view raises, the exception must propagate up: NOT be misclassified as an AgentScore infra failure (which under fail_open would re-invoke the view).""" invocations = {"count": 0} diff --git a/tests/test_extract_owner_scope.py b/tests/test_extract_owner_scope.py index 8439b9d..7c1aa0d 100644 --- a/tests/test_extract_owner_scope.py +++ b/tests/test_extract_owner_scope.py @@ -1,4 +1,4 @@ -"""Tests for ``extract_owner_scope`` — canonical owner identity from headers.""" +"""Tests for ``extract_owner_scope``: canonical owner identity from headers.""" from agentscore_commerce.identity.tokens import ( OwnerScope, @@ -8,7 +8,7 @@ # A real EIP-55 checksummed EVM address + its lowercase form. The stored ``orders.wallet_address`` # column persists the lowercased signer, so extract_owner_scope MUST lowercase the inbound -# X-Wallet-Address — otherwise a checksummed header misses its own order rows (404). +# X-Wallet-Address: otherwise a checksummed header misses its own order rows (404). _CHECKSUMMED = "0xeb2Ca790F72787c7e61bC6c861353a1e4ACDFCa5" _LOWERCASED = _CHECKSUMMED.lower() @@ -28,7 +28,7 @@ def test_checksummed_wallet_resolves_same_scope_as_lowercase() -> None: def test_preserves_solana_address_verbatim() -> None: - # Solana addresses are base58 and case-sensitive — normalization MUST NOT lowercase them. + # Solana addresses are base58 and case-sensitive: normalization MUST NOT lowercase them. sol = "DQyrAcCrDXQ7iiRTHtPhHkjFmh1mVGwXqUL9F4FUe9YN" scope = extract_owner_scope({"x-wallet-address": sol}) assert scope.wallet_address == sol diff --git a/tests/test_fastapi.py b/tests/test_fastapi.py index 32960b4..b6aa2a5 100644 --- a/tests/test_fastapi.py +++ b/tests/test_fastapi.py @@ -63,7 +63,7 @@ def test_denies_untrusted_wallet(self): resp = client.get("/", headers={"X-Wallet-Address": "0xabc"}) assert resp.status_code == 403 body = resp.json() - # FLAT denial document — top-level keys, never nested under "detail". + # FLAT denial document: top-level keys, never nested under "detail". assert body["error"]["code"] == "wallet_not_trusted" assert body["reasons"] == ["kyc_required"] @@ -111,7 +111,7 @@ def test_api_error_returns_403_api_error(self): def test_quota_exceeded_returns_503_when_fail_closed(self): """429 from /v1/assess gets dedicated handling; with fail_open=False (default) it surfaces as 503 api_error to the buyer with quota-specific contact_merchant - instructions (NOT retry_with_backoff — quota won't recover from retry).""" + instructions (NOT retry_with_backoff: quota won't recover from retry).""" import json as _json respx.post(ASSESS_URL).mock(return_value=httpx.Response(429)) @@ -385,7 +385,7 @@ def test_fixable_wallet_denial_bootstraps_session(self): @respx.mock def test_unfixable_wallet_denial_returns_bare_wallet_not_trusted(self): - # Sanctions / age / jurisdiction_restricted are unfixable — re-verification + # Sanctions / age / jurisdiction_restricted are unfixable: re-verification # won't change the outcome. Gate should emit bare wallet_not_trusted (no # session bootstrap) so the agent surfaces contact-support copy. _mock_assess("deny", reasons=["sanctions_flagged"]) @@ -447,7 +447,7 @@ def test_no_ops_when_wallet_authenticated(self): assert capture_route.call_count == 0 def test_no_ops_when_gate_did_not_run(self): - """Handler wired without the gate dependency — capture_wallet must silently no-op.""" + """Handler wired without the gate dependency: capture_wallet must silently no-op.""" app = FastAPI() @app.post("/purchase") @@ -519,7 +519,7 @@ async def index(request: Request): @respx.mock def test_returns_none_when_api_omits_quota_headers(self): - # Enterprise / unlimited tiers don't emit X-Quota-* headers — the gate state + # Enterprise / unlimited tiers don't emit X-Quota-* headers: the gate state # carries no quota and get_gate_quota_info returns None. from agentscore_commerce.identity.fastapi import get_gate_quota_info @@ -580,7 +580,7 @@ def index(): client = TestClient(app, raise_server_exceptions=False) resp = client.get("/", headers={"x-operator-token": "opc_exp"}) assert resp.status_code == 401 - # FLAT denial document — top-level keys, never nested under "detail". + # FLAT denial document: top-level keys, never nested under "detail". body = resp.json() assert body["error"]["code"] == "token_expired" assert json.loads(body["agent_instructions"]) == {"action": "deliver_verify_url_and_poll"} @@ -719,7 +719,7 @@ def _root(req: Request): client = TestClient(app) resp = client.get("/", headers={"X-Wallet-Address": "0xabc"}) assert resp.status_code == 200 - # No signer was extracted (no x402 header), so the verdict is None — but the + # No signer was extracted (no x402 header), so the verdict is None: but the # client.get_signer_verdict read path (lines 336-339) was exercised. assert captured["verdict"] is None diff --git a/tests/test_flask.py b/tests/test_flask.py index e304971..c149e0a 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -552,7 +552,7 @@ def test_no_ops_outside_request_context(self) -> None: from agentscore_commerce.identity.flask import capture_wallet app = Flask(__name__) # no gate registered - # App context but no request context — Flask's `g` is only meaningful inside a request. + # App context but no request context: Flask's `g` is only meaningful inside a request. with ( app.app_context(), patch("agentscore_commerce.identity.flask.AgentScoreCore.capture_wallet") as mock_capture, diff --git a/tests/test_gate_quota_info.py b/tests/test_gate_quota_info.py index f782645..2ae2da8 100644 --- a/tests/test_gate_quota_info.py +++ b/tests/test_gate_quota_info.py @@ -70,7 +70,7 @@ def test_django_get_gate_quota_info_returns_none_when_absent() -> None: request = MagicMock() request._agentscore_gate = None # type: ignore[assignment] - # Attribute may not exist at all — getattr default. + # Attribute may not exist at all: getattr default. delattr(request, "_agentscore_gate") assert get_gate_quota_info(request) is None diff --git a/tests/test_get_signer_verdict.py b/tests/test_get_signer_verdict.py index e75168a..9230367 100644 --- a/tests/test_get_signer_verdict.py +++ b/tests/test_get_signer_verdict.py @@ -1,4 +1,4 @@ -"""Per-adapter coverage for ``get_signer_verdict`` — returns ``None`` when no signer was +"""Per-adapter coverage for ``get_signer_verdict``: returns ``None`` when no signer was extracted (operator-token-only paths, no payment credential, missing gate state). The verdict is REQUEST-SCOPED: the gate stashes ``state["signer_verdict"]`` (projected from @@ -216,7 +216,7 @@ def test_sanic_get_signer_verdict_reads_request_scoped_verdict() -> None: # --------------------------------------------------------------------------- -# AgentScoreCore.get_signer_verdict — projection branches +# AgentScoreCore.get_signer_verdict: projection branches # --------------------------------------------------------------------------- diff --git a/tests/test_idempotency_helper.py b/tests/test_idempotency_helper.py index 7d3ef31..d85e778 100644 --- a/tests/test_idempotency_helper.py +++ b/tests/test_idempotency_helper.py @@ -49,6 +49,6 @@ def test_warns_when_key_exceeds_200_chars(caplog): key = "a" * 201 with caplog.at_level(logging.WARNING, logger="agentscore_commerce.payment.idempotency"): result = build_idempotency_key(payment_intent_id=key) - # Original key returned unchanged — server is the source of truth for truncation. + # Original key returned unchanged: server is the source of truth for truncation. assert result == key assert any("idempotency key longer than 200 chars" in rec.message for rec in caplog.records) diff --git a/tests/test_lifted_helpers.py b/tests/test_lifted_helpers.py index a7b9ab3..73c6a3e 100644 --- a/tests/test_lifted_helpers.py +++ b/tests/test_lifted_helpers.py @@ -706,7 +706,7 @@ async def settle_payment(self, _payload: object, _req: object) -> SettleResponse assert decoded["success"] is True assert decoded["transaction"] == "0xabc" assert decoded["network"] == "eip155:8453" - # by_alias=True: wire shape uses errorReason / errorMessage (camelCase) — not snake_case. + # by_alias=True: wire shape uses errorReason / errorMessage (camelCase): not snake_case. assert "errorReason" in decoded diff --git a/tests/test_load_ucp_signing_key_from_env.py b/tests/test_load_ucp_signing_key_from_env.py index b1dc97b..35bf884 100644 --- a/tests/test_load_ucp_signing_key_from_env.py +++ b/tests/test_load_ucp_signing_key_from_env.py @@ -1,4 +1,4 @@ -"""Tests for ``load_ucp_signing_key_from_env`` — env-driven UCP signing-key loader. +"""Tests for ``load_ucp_signing_key_from_env``: env-driven UCP signing-key loader. Locked behavior contract (shared with the Node sibling at ``node-commerce/tests/identity/load-ucp-signing-key-from-env.test.ts``): diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 6115765..9f5dade 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -398,7 +398,7 @@ def _snoop(request: Request) -> JSONResponse: def test_middleware_quota_exceeded_returns_503_when_fail_closed(): """429 from /v1/assess gets dedicated handling; with fail_open=False (default) it surfaces as 503 api_error to the buyer with quota-specific contact_merchant - instructions (NOT retry_with_backoff — quota won't recover from retry).""" + instructions (NOT retry_with_backoff: quota won't recover from retry).""" respx.post(ASSESS_URL).mock(return_value=httpx.Response(429)) app = _make_app() @@ -506,7 +506,7 @@ def test_middleware_passes_through_token_expired_with_auto_session(): @respx.mock def test_middleware_emits_invalid_credential_no_session(): - # `invalid_credential` is permanent — the API returns 401 with NO auto-session + # `invalid_credential` is permanent: the API returns 401 with NO auto-session # (distinct from token_expired). Middleware must classify it as a 403 with # action='switch_token_or_restart_session', NOT fall through to api_error 503 # which would tell the agent to retry forever on a permanent state. @@ -529,7 +529,7 @@ def test_middleware_emits_invalid_credential_no_session(): assert instructions["action"] == "switch_token_or_restart_session" msg = instructions["user_message"].lower() assert "switch tokens" in msg or "different stored token" in msg - # No session fields — the API didn't mint one for this case. + # No session fields: the API didn't mint one for this case. assert "session_id" not in body assert "verify_url" not in body assert "poll_secret" not in body @@ -607,7 +607,7 @@ def test_middleware_fail_open_on_402_lets_request_through(): @respx.mock def test_middleware_handler_exception_is_not_swallowed_by_gate(): """Regression: gate's try-block must NOT wrap the downstream ASGI app. If the user's - app raises, the exception must propagate up — NOT be misclassified as an AgentScore + app raises, the exception must propagate up: NOT be misclassified as an AgentScore infra failure (which under fail_open would re-invoke the app).""" _mock_assess(decision="allow") @@ -623,7 +623,7 @@ def boom_route(_request: Request) -> JSONResponse: client = TestClient(app, raise_server_exceptions=False) resp = client.get("/", headers={"x-wallet-address": "0xabc"}) - # Starlette surfaces unhandled exceptions as 500 — the important thing is the route + # Starlette surfaces unhandled exceptions as 500: the important thing is the route # ran exactly once (no fail-open retry) and the gate didn't claim the exception was # an AgentScore infra failure. assert resp.status_code == 500 @@ -796,7 +796,7 @@ def _handler(request): client = TestClient(app) resp = client.get("/", headers={"X-Wallet-Address": "0xsvread"}) assert resp.status_code == 200 - # No signer was extracted (no x402 header), so the verdict is None — the + # No signer was extracted (no x402 header), so the verdict is None: the # client.get_signer_verdict read path was still exercised. assert captured["verdict"] is None diff --git a/tests/test_network_kind.py b/tests/test_network_kind.py index 5d6c7c1..ef6c7eb 100644 --- a/tests/test_network_kind.py +++ b/tests/test_network_kind.py @@ -13,7 +13,7 @@ def test_is_evm_network_string() -> None: def test_is_solana_network_string() -> None: assert is_solana_network("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp") is True assert is_solana_network("eip155:8453") is False - # bare "solana" (no `:`) is mppx-internal, not CAIP-2 — should be False + # bare "solana" (no `:`) is mppx-internal, not CAIP-2: should be False assert is_solana_network("solana") is False diff --git a/tests/test_pay_to_address.py b/tests/test_pay_to_address.py index 159ea46..0ff37b7 100644 --- a/tests/test_pay_to_address.py +++ b/tests/test_pay_to_address.py @@ -153,7 +153,7 @@ async def test_reuses_credential_recipient_when_cached() -> None: pi_cache=cache, # type: ignore[arg-type] ) assert result == "0xCACHED" - # No mint happened — no addresses cached. + # No mint happened: no addresses cached. assert cache.cached_addresses == [] @@ -459,7 +459,7 @@ async def test_credential_missing_recipient_field_raises() -> None: cache = FakePiCache(has_address_result=True) with patch("mpp.Credential", FakeCredential), pytest.raises(CheckoutValidationError) as exc: - # FakeCredential.from_authorization splits on ':' — empty recipient after method. + # FakeCredential.from_authorization splits on ':': empty recipient after method. await create_pay_to_address_from_stripe_pi( authorization_header="Payment tempo:", amount_cents=100, diff --git a/tests/test_payment_directive.py b/tests/test_payment_directive.py index 354baa4..da3be4b 100644 --- a/tests/test_payment_directive.py +++ b/tests/test_payment_directive.py @@ -34,7 +34,7 @@ def test_build_payment_request_blob_overrides_take_precedence(): def test_build_payment_request_blob_includes_decimals_for_node_parity(): - """Wire-format parity with @agent-score/commerce — the decoded JSON must include `decimals` + """Wire-format parity with @agent-score/commerce: the decoded JSON must include `decimals` (mppx tempo schema requires it). If this assertion fails, node-commerce and python-commerce are emitting different request blobs for the same payment, which breaks cross-SDK interop. """ diff --git a/tests/test_payment_header.py b/tests/test_payment_header.py index 1e66fe9..478d9e8 100644 --- a/tests/test_payment_header.py +++ b/tests/test_payment_header.py @@ -46,7 +46,7 @@ def __init__(self, headers: dict[str, str]) -> None: def test_reads_headers_with_get_returning_list_or_tuple() -> None: - """Headers `.get` returns list values for repeated headers — first hop wins.""" + """Headers `.get` returns list values for repeated headers: first hop wins.""" class MultiHeaders: def get(self, name: str) -> list[str] | None: diff --git a/tests/test_payment_servers.py b/tests/test_payment_servers.py index 304a703..acdd5e7 100644 --- a/tests/test_payment_servers.py +++ b/tests/test_payment_servers.py @@ -3,7 +3,7 @@ The prior mock-based suite tracked x402 2.8 / pympp pre-release internal layout (``x402.servers``, ``HTTPFacilitatorClient``, ``Mppx``, ``charge(currency=...)``). x402 2.9 + pympp 0.6 shipped breaking refactors so the mocks no longer reflect -reality. We now run against the actually-installed peer deps — the tests skip +reality. We now run against the actually-installed peer deps: the tests skip when the deps aren't present so the suite still runs in minimal envs. """ @@ -68,7 +68,7 @@ async def test_create_x402_server_registers_base_sepolia_scheme() -> None: @pytest.mark.asyncio async def test_create_x402_server_coinbase_facilitator_wires_cdp_jwt(monkeypatch: pytest.MonkeyPatch) -> None: """``facilitator="coinbase"`` builds an HTTPFacilitatorClient pointed at the - CDP URL with a per-endpoint JWT auth provider — not a bare in-process facilitator. + CDP URL with a per-endpoint JWT auth provider: not a bare in-process facilitator. This is the regression that 1.3.2 fixes: 1.3.0 + 1.3.1 both passed an empty ``x402Facilitator()`` instance and silently failed downstream when @@ -149,7 +149,7 @@ def build_payment_requirements(self, config, _ext=None): assert len(accepts) == 1 assert isinstance(accepts[0], dict) assert accepts[0]["network"] == "eip155:8453" - # Camel-case keys (by_alias=True) — facilitator + clients expect this shape. + # Camel-case keys (by_alias=True): facilitator + clients expect this shape. assert accepts[0]["payTo"] == "0x000000000000000000000000000000000000dEaD" assert accepts[0]["maxTimeoutSeconds"] == 300 assert accepts[0]["extra"] == {"name": "USD Coin", "version": "2"} diff --git a/tests/test_payment_signer.py b/tests/test_payment_signer.py index 84ffba8..f8c03c3 100644 --- a/tests/test_payment_signer.py +++ b/tests/test_payment_signer.py @@ -83,12 +83,12 @@ def test_returns_none_when_payload_field_is_not_a_dict(self): "eyJjaGFsbGVuZ2UiOiB7InNvdXJjZSI6ICJkaWQ6cGtoOnNvbGFuYTo1ZXlrdDRVc0Z2OFA4TkpkVFJFcFkxdnpxS3FaS3ZkcFVrZkZw" "OjduUUVneHFFVzFiRHFhVDNrWldhOEtxVWs0V2ZoNFZiY3cifX0=" ) -_MPP_NO_SOURCE = "Payment eyJmb28iOiAiYmFyIn0=" # {"foo": "bar"} — no source field anywhere -_MPP_NON_DICT_JSON = "Payment WzEsIDIsIDNd" # [1, 2, 3] — JSON list, not an object +_MPP_NO_SOURCE = "Payment eyJmb28iOiAiYmFyIn0=" # {"foo": "bar"}: no source field anywhere +_MPP_NON_DICT_JSON = "Payment WzEsIDIsIDNd" # [1, 2, 3]: JSON list, not an object _MPP_NON_DID_SOURCE = "Payment eyJzb3VyY2UiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbSJ9" # {"source": "https://example.com"} -# {"source": "did:pkh:tezos:NetXdQprcVkpaWU:tz1abc..."} — valid did:pkh shape but unknown family +# {"source": "did:pkh:tezos:NetXdQprcVkpaWU:tz1abc..."}: valid did:pkh shape but unknown family _MPP_UNKNOWN_FAMILY = "Payment eyJzb3VyY2UiOiAiZGlkOnBraDp0ZXpvczpOZXRYZFFwcmNWa3BhV1U6dHoxYWJjZGVmZ2hpamtsbW5vcCJ9" -# {"source": "did:pkh:eip155:4217:not-an-evm-address"} — valid did:pkh but malformed address +# {"source": "did:pkh:eip155:4217:not-an-evm-address"}: valid did:pkh but malformed address _MPP_MALFORMED_ADDR = "Payment eyJzb3VyY2UiOiAiZGlkOnBraDplaXAxNTU6NDIxNzpub3QtYW4tZXZtLWFkZHJlc3MifQ==" _MPP_FIXTURES: list[tuple[str, str, PaymentSigner | None]] = [ @@ -164,7 +164,7 @@ def test_no_headers_returns_none(self) -> None: def test_does_not_require_mpp_parsing_module(self) -> None: """Regression: the helper must NOT import ``mpp._parsing`` (private upstream). - This is a smoke test — we don't try to mock the import absence, just confirm + This is a smoke test: we don't try to mock the import absence, just confirm the helper works without pympp's private parser being involved. The function body relies only on stdlib (base64 + json) for the MPP path. """ diff --git a/tests/test_policy.py b/tests/test_policy.py index 074ddef..3f64448 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -178,7 +178,7 @@ async def test_run_gate_hard_converts_gate_denial_error() -> None: async def test_run_gate_soft_swallows_gate_denial_error() -> None: # soft mode SWALLOWS a non-sanctions _GateDenialError (KYC/age/jurisdiction), stamping # status="unverified" so the order completes with a degraded identity_status. (Sanctions - # are the sole exception — see test_run_gate_soft_does_not_swallow_sanctions_*.) + # are the sole exception: see test_run_gate_soft_does_not_swallow_sanctions_*.) # run_gate_with_enforcement previously caught only HTTPException, so once the gate # started raising the flat _GateDenialError, soft mode let the denial propagate. from agentscore_commerce.identity.fastapi import _GateDenialError @@ -210,7 +210,7 @@ async def test_run_gate_soft_does_not_swallow_sanctions_gate_denial_error() -> N @pytest.mark.asyncio async def test_run_gate_soft_does_not_swallow_sanctions_unavailable() -> None: # The fail-closed unavailable-screen variant (`sanctions_check_unavailable`) is also a - # strict-liability deny — soft must not downgrade it to settled. + # strict-liability deny: soft must not downgrade it to settled. from agentscore_commerce.identity.fastapi import _GateDenialError body = {"error": {"code": "wallet_not_trusted"}, "reasons": ["sanctions_check_unavailable"]} @@ -252,7 +252,7 @@ def test_module_exports_public_surface() -> None: def test_validate_shipping_no_op_on_null_policy() -> None: - # No raise — ship anywhere when policy is None. + # No raise: ship anywhere when policy is None. validate_shipping_against_policy(country="AQ", state="", policy=None) diff --git a/tests/test_quote_cache_redis.py b/tests/test_quote_cache_redis.py index 26feebb..1a044da 100644 --- a/tests/test_quote_cache_redis.py +++ b/tests/test_quote_cache_redis.py @@ -74,7 +74,7 @@ def _raise_import(name: str, *args: Any, **kwargs: Any) -> Any: @pytest.mark.asyncio async def test_try_create_redis_returns_none_on_generic_exception(monkeypatch: pytest.MonkeyPatch) -> None: - """Generic Exception branch (lines 73-75) — e.g. malformed URL.""" + """Generic Exception branch (lines 73-75): e.g. malformed URL.""" import importlib real_import = importlib.import_module diff --git a/tests/test_rail_spec.py b/tests/test_rail_spec.py index 1b523d5..f56cf7b 100644 --- a/tests/test_rail_spec.py +++ b/tests/test_rail_spec.py @@ -53,7 +53,7 @@ def test_solana_mpp_rail_spec_defaults() -> None: def test_solana_mpp_rail_spec_with_fee_payer_signer() -> None: - """Fee-payer signer roundtrips through the spec — opaque object.""" + """Fee-payer signer roundtrips through the spec: opaque object.""" sentinel_signer = object() spec = SolanaMppRailSpec(recipient="GEQg2TM4VL315Bd4LLkGrhBjdNfoatKjCJYHBDPM3D74", signer=sentinel_signer) assert spec.signer is sentinel_signer @@ -124,7 +124,7 @@ async def factory() -> str: @pytest.mark.asyncio async def test_resolve_recipient_called_once_per_resolution() -> None: - """Each `resolve_recipient` call invokes the factory once — caching is caller-side.""" + """Each `resolve_recipient` call invokes the factory once: caching is caller-side.""" calls = 0 async def factory() -> str: diff --git a/tests/test_redis_internal.py b/tests/test_redis_internal.py index 50901b4..86e690e 100644 --- a/tests/test_redis_internal.py +++ b/tests/test_redis_internal.py @@ -38,6 +38,6 @@ async def test_memoized_redis_with_unreachable_url() -> None: get = memoized_redis(url="redis://127.0.0.1:1", label="test-unreachable") result = await get() # Either None (no redis installed / construction failed) or a client object - # that won't be queried — either way memoization is the key behavior here. + # that won't be queried: either way memoization is the key behavior here. again = await get() assert result is again diff --git a/tests/test_response.py b/tests/test_response.py index bbc526d..fe1b69f 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -1,6 +1,6 @@ """Tests for the shared denial-body marshaller. -Covers the fallback agent_instructions injection added in PR-fix-wallet-not-trusted — +Covers the fallback agent_instructions injection added in PR-fix-wallet-not-trusted: every denial code that doesn't already get instructions from the gate must come out of ``denial_reason_to_body`` with a machine-readable next-step block. """ @@ -67,7 +67,7 @@ def test_explicit_agent_instructions_takes_precedence_over_default() -> None: def test_api_error_emits_retry_with_backoff_instructions() -> None: # api_error denials get a structured agent_instructions block with retry-with-backoff # guidance so agents distinguish transient AgentScore-side issues from compliance denials. - # agent_instructions is the single retry channel — no separate next_steps block. + # agent_instructions is the single retry channel: no separate next_steps block. body = denial_reason_to_body(DenialReason(code="api_error")) assert "agent_instructions" in body instructions = json.loads(body["agent_instructions"]) @@ -138,5 +138,5 @@ def test_extra_passes_through_but_reserved_fields_are_dropped() -> None: ) ) assert body["order_id"] == "ord_2" - # `verify_url` is reserved — the hook value is ignored, not echoed. + # `verify_url` is reserved: the hook value is ignored, not echoed. assert body.get("verify_url") != "https://phish.example" diff --git a/tests/test_robots_tag.py b/tests/test_robots_tag.py index ad7cf20..a214740 100644 --- a/tests/test_robots_tag.py +++ b/tests/test_robots_tag.py @@ -50,7 +50,7 @@ def test_replace_true_skips_defaults() -> None: class _FakeApp: - """Minimal ASGI inner app — captures the headers that pass through send.""" + """Minimal ASGI inner app: captures the headers that pass through send.""" def __init__(self) -> None: self.captured_headers: list[tuple[bytes, bytes]] = [] diff --git a/tests/test_sanic.py b/tests/test_sanic.py index 065372a..89e6da5 100644 --- a/tests/test_sanic.py +++ b/tests/test_sanic.py @@ -403,7 +403,7 @@ def test_no_ops_when_wallet_authenticated(self): mock_capture.assert_not_awaited() def test_no_ops_when_gate_did_not_run(self): - # App without the gate middleware — capture_wallet must silently no-op. + # App without the gate middleware: capture_wallet must silently no-op. app = Sanic.get_app("sanic_no_gate", force_create=True) @app.post("/purchase") diff --git a/tests/test_seamless_helpers.py b/tests/test_seamless_helpers.py index 5de3f61..a14bb72 100644 --- a/tests/test_seamless_helpers.py +++ b/tests/test_seamless_helpers.py @@ -792,7 +792,7 @@ async def _run_gate(_ctx: Any) -> str: async def test_gate_per_request_policy_none_routes_to_wallet_ofac_floor_denies_sdn( monkeypatch: pytest.MonkeyPatch, ) -> None: - """`per_request_policy(ctx) → None` no longer skips the gate — it falls through + """`per_request_policy(ctx) → None` no longer skips the gate: it falls through to the always-on wallet OFAC SDN floor. With an api_key + a wallet-signed payment, the floor screens the signer and DENIES an OFAC-SDN signer.""" from agentscore_commerce.checkout import CheckoutGateConfig @@ -821,7 +821,7 @@ async def _policy(_ctx: Any) -> None: body={}, ), ) - # Floor fired and denied on the SDN signer — settle must NOT proceed. + # Floor fired and denied on the SDN signer: settle must NOT proceed. mock_aassess.assert_called_once() assert result.status == 403 assert result.settled is False @@ -868,7 +868,7 @@ async def test_gate_per_request_policy_none_floor_skips_without_signer( monkeypatch: pytest.MonkeyPatch, ) -> None: """`per_request_policy(ctx) → None` → wallet OFAC floor; with NO extractable - signer (Stripe SPT / card / no crypto payment) the floor is a no-op — no + signer (Stripe SPT / card / no crypto payment) the floor is a no-op: no forced wallet, no assess call, settle proceeds to 200.""" from agentscore_commerce.checkout import CheckoutGateConfig @@ -953,7 +953,7 @@ async def _mock_run_gate(_raw: Any, _gate_instance: Any, *, enforcement: Any = N async def _on_settled(ctx: Any, outcome: SettleOutcome) -> dict[str, Any]: if ctx.capture_wallet is not None: - # Don't actually fire — would call AgentScoreCore — but mark that the closure exists. + # Don't actually fire (it would call AgentScoreCore), but mark that the closure exists. capture_calls.append({"available": True, "tx": outcome.tx_hash}) return {"order_id": "o-1"} diff --git a/tests/test_signer_match.py b/tests/test_signer_match.py index 2999330..dcecd73 100644 --- a/tests/test_signer_match.py +++ b/tests/test_signer_match.py @@ -225,7 +225,7 @@ def fake_post(*_args: object, **kwargs: object) -> MagicMock: client.check(address=WALLET_A, signer={"address": WALLET_C, "network": "evm"}) second = client.get_signer_verdict(WALLET_A) - # The 2nd request was NOT served from cache — the API was hit a second time and re-screened. + # The 2nd request was NOT served from cache: the API was hit a second time and re-screened. assert len(calls) == 2 assert calls[1]["signer"] == {"address": WALLET_C, "network": "evm"} # The verdict slot reflects the SECOND signer's sanctions result, not a stale replay of the 1st. @@ -236,7 +236,7 @@ def fake_post(*_args: object, **kwargs: object) -> MagicMock: def test_same_signer_same_identity_is_a_cache_hit() -> None: """Control for the cache-key change: an identical (identity, signer) pair still hits the cache. - A repeat of the exact same wallet+signer inside the window must NOT re-hit the API — otherwise + A repeat of the exact same wallet+signer inside the window must NOT re-hit the API: otherwise the signer-aware key would have defeated caching entirely. """ client = AgentScoreCore(api_key=API_KEY) @@ -287,7 +287,7 @@ def test_get_signer_verdict_returns_none_when_address_not_cached() -> None: # --------------------------------------------------------------------------- -# 401 token_expired pass-through — covers both revoked and TTL-expired credentials +# 401 token_expired pass-through: covers both revoked and TTL-expired credentials # (API deliberately doesn't disclose which). The 401 body carries an auto-minted # session so agents recover without an API key. # --------------------------------------------------------------------------- @@ -400,7 +400,7 @@ def _homepage(_request: object) -> JSONResponse: # --------------------------------------------------------------------------- -# denial_reason_to_body — agent_memory + wallet-signer-match field marshalling +# denial_reason_to_body: agent_memory + wallet-signer-match field marshalling # --------------------------------------------------------------------------- @@ -521,7 +521,7 @@ def test_build_missing_identity_reason_hints_probe_strategy() -> None: def test_denial_reason_to_body_omits_agent_memory_on_non_bootstrap_denial() -> None: - """wallet_signer_mismatch is post-identity — body must NOT carry an agent_memory hint.""" + """wallet_signer_mismatch is post-identity: body must NOT carry an agent_memory hint.""" from agentscore_commerce.identity._response import denial_reason_to_body from agentscore_commerce.identity.types import DenialReason diff --git a/tests/test_signer_verdict_request_scoped.py b/tests/test_signer_verdict_request_scoped.py index db1305c..1155190 100644 --- a/tests/test_signer_verdict_request_scoped.py +++ b/tests/test_signer_verdict_request_scoped.py @@ -5,7 +5,7 @@ Before the fix, every adapter's ``get_signer_verdict`` read ``client.get_signer_verdict(addr)`` off the SHARED core, whose ``_last_signer_raw`` slot is keyed by claimed address only. Under concurrency the slot is last-writer-wins: request A (clean signer) could read request B's verdict -(sanctioned signer) — or, worse, request B (sanctioned) could read A's ``pass``/``clear`` and +(sanctioned signer): or, worse, request B (sanctioned) could read A's ``pass``/``clear`` and settle. The gate now stashes the verdict projected from THIS request's assess response on the per-request state, which can't be raced. """ @@ -52,7 +52,7 @@ async def test_concurrent_same_wallet_distinct_signers_get_own_verdict() -> None # Barrier: both requests must reach the assess call (and stash into the SHARED core slot) # before EITHER proceeds to read its verdict. This forces the worst-case interleaving where - # the shared _last_signer_raw[CLAIMED] slot is last-writer-wins — exactly the race. + # the shared _last_signer_raw[CLAIMED] slot is last-writer-wins: exactly the race. both_assessed = asyncio.Barrier(2) async def fake_acheck_identity(identity: Any, _chain: Any = None, signer: Any = None) -> AssessResult: @@ -102,11 +102,11 @@ def _payment_header(signer_addr: str) -> str: body_clean = resp_clean.json() body_sanctioned = resp_sanctioned.json() - # The CLEAN request must see its OWN clean verdict — never the sanctioned signer's. + # The CLEAN request must see its OWN clean verdict: never the sanctioned signer's. assert body_clean["kind"] == "pass" assert body_clean["signer_sanctions"] == {"kind": "clear"} - # The SANCTIONED request must see its OWN sanctioned verdict — never riding the clean one. + # The SANCTIONED request must see its OWN sanctioned verdict: never riding the clean one. assert body_sanctioned["kind"] == "wallet_signer_mismatch" assert body_sanctioned["actual_signer"] == SIGNER_SANCTIONED assert body_sanctioned["signer_sanctions"] == {"kind": "sdn_hit"} diff --git a/tests/test_solana.py b/tests/test_solana.py index ceccf82..3bfc053 100644 --- a/tests/test_solana.py +++ b/tests/test_solana.py @@ -20,13 +20,13 @@ def test_hex_format_attempts_construction() -> None: result = load_solana_fee_payer(hex_key) assert result is not None except ImportError: - # solders not installed in this env — branch was exercised + # solders not installed in this env: branch was exercised pass def test_base58_format_attempts_construction() -> None: """Non-hex string falls through to base58 path.""" - # solders/base58 missing OR decoded length unexpected — branch exercised either way + # solders/base58 missing OR decoded length unexpected: branch exercised either way with contextlib.suppress(ImportError, ValueError): # 32-byte secret encoded as base58 (Phantom secret-only format) load_solana_fee_payer("5Kd3NBUAdUnhyzenEwVLy9pBKxSwXvE9FMPyR4UKZvpu") @@ -34,7 +34,7 @@ def test_base58_format_attempts_construction() -> None: def test_base58_with_invalid_decoded_length_raises_value_error() -> None: """Base58 strings that decode to !=32 and !=64 bytes raise ValueError.""" - # 'aaa' decodes to 2 bytes — not 32 or 64 + # 'aaa' decodes to 2 bytes: not 32 or 64 with contextlib.suppress(ValueError, ImportError): load_solana_fee_payer("aaa") diff --git a/tests/test_tokens.py b/tests/test_tokens.py index 0bd66c9..3df2d8b 100644 --- a/tests/test_tokens.py +++ b/tests/test_tokens.py @@ -1,6 +1,6 @@ """Tests for ``agentscore_commerce.identity.tokens.hash_operator_token``. -The expected digests below are hardcoded — locked as the cross-language +The expected digests below are hardcoded: locked as the cross-language contract with the Node sibling at ``node-commerce/tests/identity/tokens.test.ts``. Both files reference the same fixture inputs and the same expected output bytes. A drift in either language (algorithm swap, encoding change, accidental truncation) @@ -25,7 +25,7 @@ ("opc_cross_lang_fixture", "96690dd2659bc1e33227e943d5f8a526c7c95a0ede5775a1573abab6578ca8ec"), ("opc_anything", "e6ba517ac96ee39190c4d703b2d968fec96e87827374e56095a2f443d870730d"), ("opc_42", "731985dd676ea0702b3e6f6cbb107eaf467319e2801e6f953f08cbcc7dd71684"), - # Non-ASCII fixture — UTF-8 encoding of "é" is 0xC3 0xA9; locks the encoding + # Non-ASCII fixture: UTF-8 encoding of "é" is 0xC3 0xA9; locks the encoding # contract so a future implementation that drops the explicit "utf-8" arg # still produces the same bytes. ("opc_é", "c1dba11d60cbfc1264d115e07a74a0355b6a66ded4ee3f930024a1733ba6942f"), diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 9cb3ca9..38d65ac 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -44,7 +44,7 @@ def test_emits_spec_envelope_with_ucp_body_and_outer_keys(): assert "keys" in d assert "signing_keys" not in d # removed in UCP 2026-08-25 assert d["ucp"]["version"] == "2026-08-25" - # No top-level `spec` field per UCP spec — spec lives per-binding. + # No top-level `spec` field per UCP spec: spec lives per-binding. assert "spec" not in d assert "version" not in d # version lives under `ucp` assert d["ucp"]["version"] @@ -82,7 +82,7 @@ def test_appends_agentscore_capability_when_gate_provided(): # Date-format version (UCP convention; matches every other binding's version field). assert cap["version"] == "2026-04-08" assert "com-agentscore-identity-v1.json" in cap["schema"] - # Multi-parent extends — matches Shopify's dev.shopify.catalog.storefront pattern + # 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"] # Config is the merchant's policy declaration, NOT per-operator data. Public diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 8bc9339..132af9c 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -560,7 +560,7 @@ def test_verify_wraps_unrecognized_critical_header(self) -> None: with pytest.raises(UCPVerificationError) as exc: verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) assert exc.value.code == "unrecognized_critical_header" - # Silence unused-import warnings — registry is referenced for the joserfc namespace. + # Silence unused-import warnings: registry is referenced for the joserfc namespace. _ = jws, JWSRegistry def test_verify_crit_with_missing_kid_emits_unrecognized_critical_header(self) -> None: diff --git a/tests/test_zero_settle.py b/tests/test_zero_settle.py index e8ed21d..d2284eb 100644 --- a/tests/test_zero_settle.py +++ b/tests/test_zero_settle.py @@ -103,7 +103,7 @@ ( "mpp_credential_without_source", "tempo", - "Payment eyJmb28iOiAiYmFyIn0=", # {"foo": "bar"} — no source field + "Payment eyJmb28iOiAiYmFyIn0=", # {"foo": "bar"}: no source field ZeroSettleResult(signer_address=None, signer_network=None), ), ]