Skip to content

Commit aa8a810

Browse files
vvillait88claude
andauthored
feat: typed assess errors + X-Quota headers + signer_match types (python parity) (#21)
## Summary - Add typed `AssessError` taxonomy mirroring node-sdk so consumers can branch on denial codes without string parsing - Capture `X-Quota-Limit` / `X-Quota-Used` / `X-Quota-Reset` headers on `AssessResponse` - Add `ResolveSigner` request type + `SignerMatch` response type for server-side signer matching on `/v1/assess` (TEC-263 SDK side) - Add `telemetry_signer_match()` method (sync + async) for posting gate-side outcomes - Wrap all httpx errors (not just timeouts) for parity with node-sdk's error surface - Parity tests added for retry-quota / generic-4xx / token-expired-empty-body - ty dev bump 0.0.33 → 0.0.34 - Bump version 2.0.2 → 2.1.0 (additive minor) Closes TEC-274. ## Test plan - [x] Unit tests pass locally (`uv run pytest`) - [x] Coverage above 95% threshold (`--cov-fail-under=95`) - [x] Lefthook pre-push (ty + vulture) passes - [ ] CI green on this PR - [ ] `uv build` produces clean wheel 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 510a3ce commit aa8a810

9 files changed

Lines changed: 908 additions & 68 deletions

File tree

.claude/CLAUDE.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ Two identity paths: `X-Wallet-Address` (wallet-based) and `X-Operator-Token` (cr
99
## Methods (sync + async)
1010

1111
- `get_reputation` / `aget_reputation` — cached reputation lookup (free)
12-
- `assess` / `aassess` — identity gate with policy (paid). Accepts `operator_token` for non-wallet agents. Response includes `linked_wallets[]` and `resolved_operator`.
12+
- `assess` / `aassess` — identity gate with policy (paid). Accepts `operator_token` for non-wallet agents. Response includes `linked_wallets[]` and `resolved_operator`. Optional `resolve_signer: { address, network }` opts into server-side wallet-signer-match — the response then carries a `signer_match` block describing whether the supplied signer wallet resolves to the same operator as the claimed `address`.
1313
- `create_session` / `acreate_session` — create verification session. Returns `agent_memory` + `next_steps`.
1414
- `poll_session` / `apoll_session` — poll session status, returns credential when verified, plus `next_steps.action`.
1515
- `create_credential` / `acreate_credential` — create operator credential (24h TTL default). Response includes `agent_memory`.
1616
- `list_credentials` / `alist_credentials` — list active credentials
1717
- `revoke_credential` / `arevoke_credential` — revoke a credential
1818
- `associate_wallet` / `aassociate_wallet` — report a signer wallet seen paying under a credential. Accepts optional `idempotency_key` (payment intent id / tx hash) so retries don't inflate transaction_count.
19+
- `telemetry_signer_match` / `atelemetry_signer_match` — fire-and-forget POST to `/v1/telemetry/signer-match`; commerce gate uses this to report `pass` / `wallet_signer_mismatch` / `wallet_auth_requires_wallet_signing` verdicts.
20+
21+
## Errors + observability
22+
23+
Typed error subclasses of `AgentScoreError` so callers can `except` on the specific class without parsing `err.code`: `PaymentRequiredError` (402), `TokenExpiredError` (401 token_expired — exposes parsed `verify_url` / `session_id` / `poll_secret` / `poll_url` / `next_steps` / `agent_memory` instance attributes), `InvalidCredentialError` (401 invalid_credential), `QuotaExceededError` (429 quota_exceeded — don't retry), `RateLimitedError` (429 rate_limited — retry after Retry-After), `TimeoutError` (httpx.TimeoutException — note: subclasses `AgentScoreError`, not the builtin; import explicitly from `agentscore.errors` to disambiguate). All non-timeout `httpx.HTTPError` (ConnectError, ProtocolError, NetworkError, etc.) wrap to `AgentScoreError(code="network_error", status_code=0)` for parity with node-sdk.
24+
25+
`assess()` / `aassess()` responses include an optional `quota` field captured from `X-Quota-Limit` / `X-Quota-Used` / `X-Quota-Reset` response headers, so callers can monitor approach-to-cap proactively before hitting 429.
1926

2027
## Architecture
2128

README.md

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,18 +147,60 @@ except AgentScoreError as e:
147147
print(e.code, e.status_code, str(e))
148148
```
149149

150-
`AgentScoreError.details` carries the rest of the response body — `verify_url`, `linked_wallets`, `claimed_operator`, `actual_signer`, `expected_signer`, `reasons`, `agent_memory` — so callers can branch on granular denial codes without re-parsing:
150+
`AgentScoreError.details` carries the rest of the response body — `verify_url`, `linked_wallets`, `claimed_operator`, `actual_signer`, `expected_signer`, `reasons`, `agent_memory` — so callers can branch on granular denial codes without re-parsing.
151+
152+
### Typed error classes
153+
154+
For status-code-specific recovery, the SDK raises typed subclasses of `AgentScoreError`. All inherit from `AgentScoreError` so existing `except AgentScoreError` still catches them.
155+
156+
| Class | Triggered by | What it adds |
157+
|---|---|---|
158+
| `PaymentRequiredError` | HTTP 402 | The endpoint is not enabled for this account |
159+
| `TokenExpiredError` | HTTP 401 with `error.code = "token_expired"` | Parsed body fields on the instance: `verify_url`, `session_id`, `poll_secret`, `poll_url`, `next_steps`, `agent_memory` — recover without re-parsing `details` |
160+
| `InvalidCredentialError` | HTTP 401 with `error.code = "invalid_credential"` | Permanent — switch tokens or restart |
161+
| `QuotaExceededError` | HTTP 429 with `error.code = "quota_exceeded"` | Account-level cap reached; don't retry |
162+
| `RateLimitedError` | HTTP 429 with `error.code = "rate_limited"` | Per-second sliding-window cap; retry after `Retry-After` |
163+
| `TimeoutError` | `httpx.TimeoutException` (connect/read/write/pool timeout) | Distinct from generic network errors. Note: subclasses `AgentScoreError`, **not** the builtin `TimeoutError` — import explicitly from `agentscore.errors` to disambiguate. |
164+
165+
All non-timeout `httpx.HTTPError` (ConnectError, ProtocolError, NetworkError, etc.) are wrapped as `AgentScoreError(code="network_error", status_code=0)`.
151166

152167
```python
168+
from agentscore import (
169+
AgentScore, AgentScoreError, TokenExpiredError, QuotaExceededError,
170+
)
171+
from agentscore.errors import TimeoutError as AgentScoreTimeoutError
172+
153173
try:
154174
client.assess("0xabc...", policy={"require_kyc": True})
175+
except TokenExpiredError as e:
176+
print("Verify at:", e.verify_url, "poll with:", e.poll_secret)
177+
except QuotaExceededError as e:
178+
print("Account quota reached — surface to user; don't retry.")
179+
except AgentScoreTimeoutError:
180+
print("Network timeout — retry with backoff.")
155181
except AgentScoreError as e:
156-
if e.code == "wallet_signer_mismatch":
157-
print("Re-sign from one of:", e.details.get("linked_wallets"))
158-
elif e.code == "token_expired":
159-
print("Verify at:", e.details.get("verify_url"))
182+
print(e.code, e.message)
183+
```
184+
185+
## Quota observability
186+
187+
`assess()` (and `aassess()`) responses include an optional `quota` field captured from `X-Quota-Limit` / `X-Quota-Used` / `X-Quota-Reset` response headers. Use it to monitor approach-to-cap proactively (warn at 80%, alert at 95%) before a 429:
188+
189+
```python
190+
result = client.assess("0xabc...", policy={"require_kyc": True})
191+
quota = result.get("quota")
192+
if quota and quota["limit"] and quota["used"]:
193+
pct = (quota["used"] / quota["limit"]) * 100
194+
if pct > 80:
195+
print(f"AgentScore quota at {pct:.1f}% — resets {quota['reset']}")
160196
```
161197

198+
`quota` is absent when the API doesn't emit the headers (Enterprise / unlimited tiers).
199+
200+
## Telemetry
201+
202+
`telemetry_signer_match(payload)` and `atelemetry_signer_match(payload)` are fire-and-forget POSTs to `/v1/telemetry/signer-match` so AgentScore can track aggregate signer-binding behavior across merchants. Used internally by `agentscore-commerce`'s gate; available directly for custom integrations that perform their own wallet-signer-match checks.
203+
162204
## Documentation
163205

164206
- [API Reference](https://docs.agentscore.sh)

agentscore/__init__.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from importlib.metadata import version as _pkg_version
22

33
from agentscore.client import AgentScore
4-
from agentscore.errors import AgentScoreError
4+
from agentscore.errors import (
5+
AgentScoreError,
6+
InvalidCredentialError,
7+
PaymentRequiredError,
8+
QuotaExceededError,
9+
RateLimitedError,
10+
TimeoutError,
11+
TokenExpiredError,
12+
)
513
from agentscore.test_mode import AGENTSCORE_TEST_ADDRESSES, is_agentscore_test_address
614
from agentscore.types import (
715
AccountVerification,
@@ -21,12 +29,15 @@
2129
Network,
2230
NextStepsAction,
2331
OperatorVerification,
32+
QuotaInfo,
2433
Reputation,
2534
ReputationResponse,
2635
ReputationStatus,
36+
ResolveSigner,
2737
SessionCreateRequest,
2838
SessionCreateResponse,
2939
SessionPollResponse,
40+
SignerMatch,
3041
VerificationLevel,
3142
WalletAuthRequiresSigningBody,
3243
WalletSignerMismatchBody,
@@ -52,15 +63,24 @@
5263
"DenialCode",
5364
"EntityType",
5465
"Grade",
66+
"InvalidCredentialError",
5567
"Network",
5668
"NextStepsAction",
5769
"OperatorVerification",
70+
"PaymentRequiredError",
71+
"QuotaExceededError",
72+
"QuotaInfo",
73+
"RateLimitedError",
5874
"Reputation",
5975
"ReputationResponse",
6076
"ReputationStatus",
77+
"ResolveSigner",
6178
"SessionCreateRequest",
6279
"SessionCreateResponse",
6380
"SessionPollResponse",
81+
"SignerMatch",
82+
"TimeoutError",
83+
"TokenExpiredError",
6484
"VerificationLevel",
6585
"WalletAuthRequiresSigningBody",
6686
"WalletSignerMismatchBody",

0 commit comments

Comments
 (0)