Skip to content

feat: collapsed signer_match + per-adapter quota/fail-open helpers (python parity)#4

Merged
vvillait88 merged 9 commits intomainfrom
monetization-branding-rollout
May 2, 2026
Merged

feat: collapsed signer_match + per-adapter quota/fail-open helpers (python parity)#4
vvillait88 merged 9 commits intomainfrom
monetization-branding-rollout

Conversation

@vvillait88
Copy link
Copy Markdown
Contributor

Summary

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 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(app, ...); 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": 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 evaluate
  • Dedicated 429 path with quota-specific recovery instructions

Brand + disclosure scrubs:

  • "AgentScore Commerce" brand applied to examples README
  • Disclosure cleanup on quota / 429 paths

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

Closes TEC-275, TEC-265.

Test plan

  • Unit tests pass locally (uv run pytest — 642 passing, 3 skipped)
  • Coverage above tier-A threshold (95% — actual 95.20%)
  • Lefthook pre-commit (ruff) + pre-push (ty + vulture) passes
  • CI green on this PR

🤖 Generated with Claude Code

vvillait88 and others added 8 commits May 1, 2026 03:58
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.",
Comment thread vulture_whitelist.py
OperatorVerification # noqa: F821

# TYPE_CHECKING imports referenced inside string-literal cast() calls
DecisionPolicy # noqa: F821
Comment thread vulture_whitelist.py

# 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.
@vvillait88 vvillait88 merged commit df32adf into main May 2, 2026
7 checks passed
@vvillait88 vvillait88 deleted the monetization-branding-rollout branch May 2, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant