feat: collapsed signer_match + per-adapter quota/fail-open helpers (python parity)#4
Merged
vvillait88 merged 9 commits intomainfrom May 2, 2026
Merged
Conversation
Per `core/internal_docs/branding.md` — first-mention bare "commerce SDK" / "Commerce" replaced with "AgentScore Commerce" in the examples README intro. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…open paths Closes TEC-265 (Python side, full parity with node-commerce). Opt-in (fail_open=False default) is preserved. When fail_open=True and AgentScore-side infra fails (429/5xx/network timeout): - Gate state on the request carries `degraded=True` + `infra_reason= 'quota_exceeded' | 'api_error' | 'network_timeout'` so merchants can log/alert without parsing console output. Compliance denials (sanctions, age, jurisdiction, etc.) are unaffected — those still deny regardless of fail_open. ### Client (`identity/client.py`) - New `QuotaExceededError` exception, raised by `_parse_response` on HTTP 429 so adapters can distinguish quota cap from generic 5xx - Existing `RuntimeError` paths unchanged ### Types (`identity/types.py`) - `AssessResult` extended with `degraded: bool = False` and `infra_reason: FailOpenInfraReason | None = None` - New `FailOpenInfraReason` type alias ### Adapters (6, full parity) - fastapi.py, flask.py, django.py, aiohttp.py, sanic.py, middleware.py - Each catches `QuotaExceededError`, `httpx.TimeoutException`, and generic `Exception` separately. fail_open=True paths set `degraded` + `infra_reason` on the framework-appropriate gate state via per-adapter `_mark_degraded_*` helper. fail_open=False (default) paths still surface 503 api_error denial. ### Agent-facing copy (`identity/_response.py`) - New `_API_ERROR_INSTRUCTIONS` retry-with-backoff block surfaced via `_DEFAULT_AGENT_INSTRUCTIONS` map for the `api_error` code. ### Tests (~22 added across 6 adapters + client + response) All 580 tests pass; coverage 95.08% (above 95% threshold); ty + ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pers + parity fixes End-to-end review surfaced 3 critical bugs in the prior failOpen commit; this fixes all three plus polish items. ### Critical fixes 1. **Add `get_gate_degraded_state(request)` to all 6 Python adapters** (fastapi, flask, django, aiohttp, sanic, middleware). Mirrors Node's `getGateDegradedState` for vendors who want to log/alert on degraded mode without importing private constants. 2. **Drop dead `AssessResult.degraded`/`infra_reason` fields** in `agentscore_commerce/identity/types.py`. They were never populated — the GateClient raises exceptions on infra failure rather than returning a degraded AssessResult. Misleading API surface; replaced with the adapter-level helpers that actually carry the flag. 3. **`QuotaExceededError` now subclasses `RuntimeError`** so adapters/merchants that previously caught `RuntimeError` for 429 still catch this. Preserves back-compat for anyone wrapping `GateClient.check`. New code that wants to distinguish quota from generic 5xx catches `QuotaExceededError` first. ### Tests (~10 added) - `get_gate_degraded_state` default + degraded paths for all 6 adapters (fastapi, flask, django, aiohttp, sanic, middleware) 590 tests pass; coverage 95.12%; ty + ruff clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ns + disclosure cleanup - CRITICAL: django/aiohttp/middleware adapters were wrapping the downstream user handler inside the gate's try-block; an exception in the user's view/handler/app would be misclassified as an AgentScore infra failure and (under fail_open) re-invoke their handler. Refactored all three so try wraps only acheck_identity. Added regression tests verifying the user's handler runs exactly once and exceptions propagate. - 429 fail-closed denials now carry contact_merchant agent_instructions (do-not-retry guidance) instead of generic retry_with_backoff that would loop forever - 429 fail-open path marks degraded=True + infra_reason='quota_exceeded' on the per-request gate state - Consolidated 6 per-adapter _mark_degraded_* helpers via shared apply_degraded() in types.py - Public get_gate_degraded_state helper exported from every adapter - README adds Fail-open behavior section + Flask signature note - Strip metering/pricing language from agent_instructions, source comments, docstrings, and HTTP response bodies (public-package strict surface) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…face description Also flag the try-block invariant: gate's try wraps only acheck_identity, never the downstream user handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).
Both are imported under TYPE_CHECKING and used inside cast("...") string
literals, which vulture can't trace. Mirrors the existing pattern in
vulture_whitelist.py for other false-positive type imports.
Comment on lines
+231
to
+233
| "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.", |
Comment on lines
+234
to
+235
| "If the request continues to fail after 3+ retries (~60 seconds total), surface the " | ||
| "error to the user with the merchant's support contact.", |
Comment on lines
+248
to
+249
| "AgentScore identity verification is unavailable for this merchant. This is a " | ||
| "merchant-side issue and is NOT recoverable via retry.", |
| OperatorVerification # noqa: F821 | ||
|
|
||
| # TYPE_CHECKING imports referenced inside string-literal cast() calls | ||
| DecisionPolicy # noqa: F821 |
|
|
||
| # TYPE_CHECKING imports referenced inside string-literal cast() calls | ||
| DecisionPolicy # noqa: F821 | ||
| ResolveSigner # noqa: F821 |
| ) | ||
|
|
||
| if TYPE_CHECKING: | ||
| from agentscore.types import DecisionPolicy, ResolveSigner |
CI workspace doesn't have ../python-sdk on the runner, so `uv sync --frozen --all-extras` fails with "Distribution not found". The local-dev convenience overlay belongs in a non-committed configuration; with this removed, both local and CI resolve agentscore-py from PyPI as a normal dep. Re-resolved uv.lock against the simplified source set + upgraded all extras (uv sync --upgrade --all-extras) — no changes to extras since they were already at latest after the prior commit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Identity / signer matching:
verify_wallet_signer_match/averify_wallet_signer_matchcollapse the prior 3-call gate fan-out into a single/v1/assesscall carryingresolve_signer; the API resolves both wallets server-side and emits asigner_matchverdict in the same response(claimed, signer)cache so repeat lookups skip the APIsigner_match(canary rollout safety)Fail-open + quota helpers across the 6 framework adapters (fastapi, flask, django, aiohttp, sanic, middleware/ASGI):
fail_open=Trueflag onAgentScoreGate(...)/agentscore_gate(app, ...); 429 / 5xx / network-timeout pass through to the handler withdegraded=True+infra_reasonon gate state. Compliance denials still deny.get_gate_degraded_state(request)reads{"degraded": bool, "infra_reason"?: str}from framework state container (g, scope, ctx, request.state, etc.)get_gate_quota_info(request)returns the assess quota envelope captured during evaluateBrand + disclosure scrubs:
Tests:
tests/test_gate_quota_info.py(NEW) — cross-adapter quota helper parity (12 tests across 5 adapters + middleware)tests/test_signer_match.py— collapsed surface coverageDeps:
agentscore-py2.0.2 → 2.1.0 (signer_match types, typed errors)uv sync --upgradepulled the rest of the pinned-floor packagesVersion: 1.0.3 → 1.1.0 (additive minor; parity with node-commerce).
Closes TEC-275, TEC-265.
Test plan
uv run pytest— 642 passing, 3 skipped)🤖 Generated with Claude Code