Skip to content

Commit fad1a1b

Browse files
committed
feat: collapsed signer_match + per-adapter quota/fail-open helpers
Consume @agent-score/sdk's new typed-error and signer-match surface in agentscore-py 2.1.0. Brings the python merchant SDK to parity with node-commerce's TEC-275 + TEC-265 work. Identity / signer matching: - verify_wallet_signer_match / averify_wallet_signer_match collapse the prior 3-call gate fan-out into a single /v1/assess call carrying resolve_signer; the API resolves both wallets server-side and emits a signer_match verdict in the same response - Per-(claimed, signer) cache on commerce so repeat lookups skip the API - Fallback to the legacy 2-resolve path when the API response omits signer_match (canary rollout safety) Fail-open + quota helpers across the 6 framework adapters (fastapi, flask, django, aiohttp, sanic, middleware/ASGI): - fail_open=True flag on AgentScoreGate / agentscore_gate(); 429 / 5xx / network-timeout pass through to the handler with degraded=True + infra_reason on gate state. Compliance denials still deny. - get_gate_degraded_state(request) reads {degraded, infra_reason?} from framework-specific state container (g, scope, ctx, request.state, etc.) - get_gate_quota_info(request) returns the assess quota envelope captured during evaluate. Read-path-only contract. Tests: - tests/test_gate_quota_info.py — cross-adapter quota helper parity - tests/test_signer_match.py — collapsed surface coverage - 6 adapter test files updated for fail-open + quota plumbing Deps: - agentscore-py 2.0.2 → 2.1.0 (signer_match types, typed errors) - uv sync --upgrade pulled the rest of the pinned-floor packages Version: 1.0.3 → 1.1.0 (additive minor, parity with node-commerce).
1 parent 0af28a3 commit fad1a1b

22 files changed

Lines changed: 1535 additions & 218 deletions

‎CLAUDE.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Every helper is extracted from a real consumer, not speculated.
1717

1818
## Architecture
1919

20-
Single Python package, hatchling-built, published to PyPI as `agentscore-commerce`. Per-framework identity adapters expose the same surface — `AgentScoreGate` (or `agentscore_gate(app, ...)` for Flask/Sanic), `capture_wallet`, `verify_wallet_signer_match`, `get_assess_data`, `get_gate_degraded_state` — with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim).
20+
Single Python package, hatchling-built, published to PyPI as `agentscore-commerce`. Per-framework identity adapters expose the same surface — `AgentScoreGate` (or `agentscore_gate(app, ...)` for Flask/Sanic), `capture_wallet`, `verify_wallet_signer_match`, `get_assess_data`, `get_gate_degraded_state`, `get_gate_quota_info` — with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim).
2121

2222
| Directory | Contents |
2323
|---|---|
@@ -56,6 +56,8 @@ Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-
5656

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

59+
Wallet-signer-match: `verify_wallet_signer_match` / `averify_wallet_signer_match` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response — collapses the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signer_match_by_signer` sub-dict and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety).
60+
5961
### Fail-open (opt-in)
6062

6163
`fail_open=True` on `AgentScoreGate(...)` (or `agentscore_gate(app, ...)`) flips infra-failure handling: 429 / 5xx / network-timeout pass through to the handler with the gate state stamped `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"`. `get_gate_degraded_state(request)` (Flask: `get_gate_degraded_state()` — reads from `g`) returns `{"degraded": bool, "infra_reason"?: str}` for merchant logging/alerting. Default stays `fail_open=False` — regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag. The gate's `try` wraps only the AgentScore call — never the downstream user handler.

‎agentscore_commerce/identity/aiohttp.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from agentscore_commerce.identity.types import (
3333
AgentIdentity,
3434
DenialReason,
35+
GateQuotaInfo,
3536
Network,
3637
VerifyWalletSignerMatchOptions,
3738
VerifyWalletSignerResult,
@@ -72,6 +73,7 @@ def _mark_degraded_aiohttp(request: web.Request, infra_reason: str) -> None:
7273
"extract_payment_signer_address",
7374
"get_assess_data",
7475
"get_gate_degraded_state",
76+
"get_gate_quota_info",
7577
"is_fixable_denial",
7678
"read_x402_payment_header",
7779
"verification_agent_instructions",
@@ -100,6 +102,19 @@ def get_gate_degraded_state(request: web.Request) -> dict[str, Any]:
100102
return {"degraded": False}
101103

102104

105+
def get_gate_quota_info(request: web.Request) -> GateQuotaInfo | None:
106+
"""Read AgentScore assess quota observability for this request.
107+
108+
Captured from ``X-Quota-*`` response headers on this request's gate evaluate.
109+
"""
110+
state = request.get(GATE_STATE_KEY)
111+
if isinstance(state, dict):
112+
quota = state.get("quota")
113+
if isinstance(quota, GateQuotaInfo):
114+
return quota
115+
return None
116+
117+
103118
def _default_extract_identity(request: web.Request) -> AgentIdentity | None:
104119
token = request.headers.get(DEFAULT_TOKEN_HEADER)
105120
addr = request.headers.get(DEFAULT_ADDRESS_HEADER)
@@ -241,6 +256,10 @@ async def _agentscore_middleware(
241256

242257
if result.allow:
243258
request["agentscore"] = result.raw
259+
if result.quota is not None:
260+
state = request.get(GATE_STATE_KEY)
261+
if isinstance(state, dict):
262+
state["quota"] = result.quota
244263
return await handler(request)
245264

246265
# Fixable compliance denials (kyc_required, kyc_pending, kyc_failed) get the

0 commit comments

Comments
 (0)