agentscore-py 2.1.2__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.claude/CLAUDE.md +1 -1
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/PKG-INFO +34 -2
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/README.md +33 -1
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/agentscore/__init__.py +4 -2
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/agentscore/client.py +13 -13
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/agentscore/types.py +59 -13
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/pyproject.toml +1 -1
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/tests/test_client.py +128 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/uv.lock +1 -1
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.github/dependabot.yml +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.github/workflows/ci.yml +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.github/workflows/publish.yml +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.github/workflows/security.yml +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/.gitignore +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/CODE_OF_CONDUCT.md +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/CONTRIBUTING.md +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/LICENSE +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/SECURITY.md +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/agentscore/errors.py +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/agentscore/py.typed +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/agentscore/test_mode.py +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/lefthook.yml +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/ruff.toml +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/tests/__init__.py +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/tests/conftest.py +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/tests/test_errors.py +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/tests/test_integration.py +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/tests/test_test_mode.py +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/tests/test_types.py +0 -0
- {agentscore_py-2.1.2 → agentscore_py-2.3.0}/vulture_whitelist.py +0 -0
|
@@ -9,7 +9,7 @@ Two identity paths: `X-Wallet-Address` (wallet-based) and `X-Operator-Token` (cr
|
|
|
9
9
|
## Methods (sync + async)
|
|
10
10
|
|
|
11
11
|
- `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`. Optional `
|
|
12
|
+
- `assess` / `aassess` — identity gate with policy (paid). Accepts `operator_token` for non-wallet agents. Response includes `linked_wallets[]` and `resolved_operator`. Optional `signer: { address, network }` opts into server-side wallet-signer-match AND OFAC SDN wallet-address screening — the response then carries both a `signer_match` block (wallet-binding verdict: `pass` / `wallet_signer_mismatch` / `wallet_auth_requires_wallet_signing`) and a `signer_sanctions` block (discriminated union: `{status: "clear"}` | `{sanctioned: True, ofac_label, sdn_uid, listed_at}` | `{status: "unavailable"}`). Under `policy.require_sanctions_clear`, a `sanctioned: True` OR `status: "unavailable"` verdict flips the response `decision` to `deny` with `decision_reasons` including `sanctions_flagged` or `sanctions_check_unavailable` respectively (fail-closed; OFAC strict-liability).
|
|
13
13
|
- `create_session` / `acreate_session` — create verification session. Returns `agent_memory` + `next_steps`.
|
|
14
14
|
- `poll_session` / `apoll_session` — poll session status, returns credential when verified, plus `next_steps.action`.
|
|
15
15
|
- `create_credential` / `acreate_credential` — create operator credential (24h TTL default). Response includes `agent_memory`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentscore-py
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Python client for the AgentScore APIs
|
|
5
5
|
Project-URL: Homepage, https://agentscore.sh
|
|
6
6
|
Project-URL: Repository, https://github.com/agentscore/python-sdk
|
|
@@ -96,6 +96,38 @@ client.create_session(operator_token="opc_...") # KYC refresh
|
|
|
96
96
|
|
|
97
97
|
`assess()` responses include `resolved_operator` and `linked_wallets` — all same-operator sibling wallets (claimed via SIWE or captured via prior `associate_wallet`). The list may mix EVM addresses (`0x...` lowercased) and Solana addresses (base58, case-preserved) for cross-chain operators; merchants doing wallet-signer-match checks should accept a payment signed by any address in the list, regardless of chain. The `address` parameter on `assess()` and `get_reputation()` accepts either format — network is auto-detected from the address shape.
|
|
98
98
|
|
|
99
|
+
### Server-side signer-match + sanctions screening
|
|
100
|
+
|
|
101
|
+
Pass `signer={"address", "network"}` on `assess()` / `aassess()` to opt into server-side wallet-signer-match and OFAC SDN wallet-address screening on the same call. The response carries two new verdicts:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
result = client.assess(
|
|
105
|
+
"0xclaimed...",
|
|
106
|
+
signer={"address": "0xsigner...", "network": "evm"},
|
|
107
|
+
policy={"require_sanctions_clear": True},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# signer_match: wallet-binding verdict
|
|
111
|
+
# kind: 'pass' | 'wallet_signer_mismatch' | 'wallet_auth_requires_wallet_signing'
|
|
112
|
+
# plus claimed_operator / signer_operator / expected_signer / actual_signer /
|
|
113
|
+
# linked_wallets / agent_instructions
|
|
114
|
+
match = result.get("signer_match")
|
|
115
|
+
if match and match.get("kind") == "wallet_signer_mismatch":
|
|
116
|
+
# signer wallet resolves to a different operator than the claimed address
|
|
117
|
+
...
|
|
118
|
+
|
|
119
|
+
# signer_sanctions: OFAC SDN wallet-address verdict (discriminated union)
|
|
120
|
+
# {"status": "clear"} | {"sanctioned": True, "ofac_label", "sdn_uid", "listed_at"}
|
|
121
|
+
# | {"status": "unavailable"}
|
|
122
|
+
sanctions = result.get("signer_sanctions")
|
|
123
|
+
if sanctions and sanctions.get("sanctioned"):
|
|
124
|
+
print("OFAC hit:", sanctions["ofac_label"], sanctions["sdn_uid"])
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Under `policy.require_sanctions_clear`, the API flips `decision` to `deny` when `signer_sanctions` is `sanctioned: True` OR `status: "unavailable"` — `decision_reasons` will include `sanctions_flagged` or `sanctions_check_unavailable` respectively (fail-closed; OFAC strict-liability). Without the policy flag, both verdicts are informational.
|
|
128
|
+
|
|
129
|
+
Pass `signer["address"] = None` for rails without a wallet signer (Stripe SPT, card-only). The API responds with `signer_match["kind"] == "wallet_auth_requires_wallet_signing"` and a parsed `agent_instructions` block telling the agent to switch to `X-Operator-Token` auth — spread the block directly into a 403 body.
|
|
130
|
+
|
|
99
131
|
### Credential Management
|
|
100
132
|
|
|
101
133
|
```python
|
|
@@ -218,7 +250,7 @@ if quota and quota["limit"] and quota["used"]:
|
|
|
218
250
|
print(f"AgentScore quota at {pct:.1f}% — resets {quota['reset']}")
|
|
219
251
|
```
|
|
220
252
|
|
|
221
|
-
`quota` is absent when the API doesn't emit the headers (Enterprise / unlimited tiers).
|
|
253
|
+
`quota` is absent when the API doesn't emit the headers (Enterprise / unlimited tiers). On a 429 response the SDK raises `QuotaExceededError` / `RateLimitedError` instead of returning a body, so `quota` is only readable on successful calls — drive proactive alerting off the success-path field.
|
|
222
254
|
|
|
223
255
|
## Telemetry
|
|
224
256
|
|
|
@@ -73,6 +73,38 @@ client.create_session(operator_token="opc_...") # KYC refresh
|
|
|
73
73
|
|
|
74
74
|
`assess()` responses include `resolved_operator` and `linked_wallets` — all same-operator sibling wallets (claimed via SIWE or captured via prior `associate_wallet`). The list may mix EVM addresses (`0x...` lowercased) and Solana addresses (base58, case-preserved) for cross-chain operators; merchants doing wallet-signer-match checks should accept a payment signed by any address in the list, regardless of chain. The `address` parameter on `assess()` and `get_reputation()` accepts either format — network is auto-detected from the address shape.
|
|
75
75
|
|
|
76
|
+
### Server-side signer-match + sanctions screening
|
|
77
|
+
|
|
78
|
+
Pass `signer={"address", "network"}` on `assess()` / `aassess()` to opt into server-side wallet-signer-match and OFAC SDN wallet-address screening on the same call. The response carries two new verdicts:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
result = client.assess(
|
|
82
|
+
"0xclaimed...",
|
|
83
|
+
signer={"address": "0xsigner...", "network": "evm"},
|
|
84
|
+
policy={"require_sanctions_clear": True},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# signer_match: wallet-binding verdict
|
|
88
|
+
# kind: 'pass' | 'wallet_signer_mismatch' | 'wallet_auth_requires_wallet_signing'
|
|
89
|
+
# plus claimed_operator / signer_operator / expected_signer / actual_signer /
|
|
90
|
+
# linked_wallets / agent_instructions
|
|
91
|
+
match = result.get("signer_match")
|
|
92
|
+
if match and match.get("kind") == "wallet_signer_mismatch":
|
|
93
|
+
# signer wallet resolves to a different operator than the claimed address
|
|
94
|
+
...
|
|
95
|
+
|
|
96
|
+
# signer_sanctions: OFAC SDN wallet-address verdict (discriminated union)
|
|
97
|
+
# {"status": "clear"} | {"sanctioned": True, "ofac_label", "sdn_uid", "listed_at"}
|
|
98
|
+
# | {"status": "unavailable"}
|
|
99
|
+
sanctions = result.get("signer_sanctions")
|
|
100
|
+
if sanctions and sanctions.get("sanctioned"):
|
|
101
|
+
print("OFAC hit:", sanctions["ofac_label"], sanctions["sdn_uid"])
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Under `policy.require_sanctions_clear`, the API flips `decision` to `deny` when `signer_sanctions` is `sanctioned: True` OR `status: "unavailable"` — `decision_reasons` will include `sanctions_flagged` or `sanctions_check_unavailable` respectively (fail-closed; OFAC strict-liability). Without the policy flag, both verdicts are informational.
|
|
105
|
+
|
|
106
|
+
Pass `signer["address"] = None` for rails without a wallet signer (Stripe SPT, card-only). The API responds with `signer_match["kind"] == "wallet_auth_requires_wallet_signing"` and a parsed `agent_instructions` block telling the agent to switch to `X-Operator-Token` auth — spread the block directly into a 403 body.
|
|
107
|
+
|
|
76
108
|
### Credential Management
|
|
77
109
|
|
|
78
110
|
```python
|
|
@@ -195,7 +227,7 @@ if quota and quota["limit"] and quota["used"]:
|
|
|
195
227
|
print(f"AgentScore quota at {pct:.1f}% — resets {quota['reset']}")
|
|
196
228
|
```
|
|
197
229
|
|
|
198
|
-
`quota` is absent when the API doesn't emit the headers (Enterprise / unlimited tiers).
|
|
230
|
+
`quota` is absent when the API doesn't emit the headers (Enterprise / unlimited tiers). On a 429 response the SDK raises `QuotaExceededError` / `RateLimitedError` instead of returning a body, so `quota` is only readable on successful calls — drive proactive alerting off the success-path field.
|
|
199
231
|
|
|
200
232
|
## Telemetry
|
|
201
233
|
|
|
@@ -33,11 +33,12 @@ from agentscore.types import (
|
|
|
33
33
|
Reputation,
|
|
34
34
|
ReputationResponse,
|
|
35
35
|
ReputationStatus,
|
|
36
|
-
ResolveSigner,
|
|
37
36
|
SessionCreateRequest,
|
|
38
37
|
SessionCreateResponse,
|
|
39
38
|
SessionPollResponse,
|
|
39
|
+
Signer,
|
|
40
40
|
SignerMatch,
|
|
41
|
+
SignerSanctions,
|
|
41
42
|
VerificationLevel,
|
|
42
43
|
WalletAuthRequiresSigningBody,
|
|
43
44
|
WalletSignerMismatchBody,
|
|
@@ -74,11 +75,12 @@ __all__ = [
|
|
|
74
75
|
"Reputation",
|
|
75
76
|
"ReputationResponse",
|
|
76
77
|
"ReputationStatus",
|
|
77
|
-
"ResolveSigner",
|
|
78
78
|
"SessionCreateRequest",
|
|
79
79
|
"SessionCreateResponse",
|
|
80
80
|
"SessionPollResponse",
|
|
81
|
+
"Signer",
|
|
81
82
|
"SignerMatch",
|
|
83
|
+
"SignerSanctions",
|
|
82
84
|
"TimeoutError",
|
|
83
85
|
"TokenExpiredError",
|
|
84
86
|
"VerificationLevel",
|
|
@@ -137,9 +137,9 @@ if TYPE_CHECKING:
|
|
|
137
137
|
DecisionPolicy,
|
|
138
138
|
Network,
|
|
139
139
|
ReputationResponse,
|
|
140
|
-
ResolveSigner,
|
|
141
140
|
SessionCreateResponse,
|
|
142
141
|
SessionPollResponse,
|
|
142
|
+
Signer,
|
|
143
143
|
)
|
|
144
144
|
|
|
145
145
|
|
|
@@ -255,13 +255,13 @@ class AgentScore:
|
|
|
255
255
|
refresh: bool | None = None,
|
|
256
256
|
policy: DecisionPolicy | None = None,
|
|
257
257
|
operator_token: str | None = None,
|
|
258
|
-
|
|
258
|
+
signer: Signer | None = None,
|
|
259
259
|
) -> AssessResponse:
|
|
260
260
|
"""Assess a wallet or operator (paid, writes score on-the-fly).
|
|
261
261
|
|
|
262
|
-
``
|
|
262
|
+
``signer`` opts into server-side wallet-signer-match: when supplied,
|
|
263
263
|
the API resolves the signer wallet against the claimed ``address`` and emits
|
|
264
|
-
a ``signer_match`` block on the response. See :class:`
|
|
264
|
+
a ``signer_match`` block on the response. See :class:`Signer`.
|
|
265
265
|
"""
|
|
266
266
|
body: dict[str, Any] = {}
|
|
267
267
|
if address:
|
|
@@ -274,8 +274,8 @@ class AgentScore:
|
|
|
274
274
|
body["refresh"] = refresh
|
|
275
275
|
if policy is not None:
|
|
276
276
|
body["policy"] = dict(policy)
|
|
277
|
-
if
|
|
278
|
-
body["
|
|
277
|
+
if signer is not None:
|
|
278
|
+
body["signer"] = dict(signer)
|
|
279
279
|
client = self._get_sync_client()
|
|
280
280
|
data, response = self._send_sync_with_response(lambda: client.post("/v1/assess", json=body))
|
|
281
281
|
quota = _extract_quota(response)
|
|
@@ -393,11 +393,11 @@ class AgentScore:
|
|
|
393
393
|
refresh: bool | None = None,
|
|
394
394
|
policy: DecisionPolicy | None = None,
|
|
395
395
|
operator_token: str | None = None,
|
|
396
|
-
|
|
396
|
+
signer: Signer | None = None,
|
|
397
397
|
) -> AssessResponse:
|
|
398
398
|
"""Assess a wallet or operator (paid, writes score on-the-fly).
|
|
399
399
|
|
|
400
|
-
``
|
|
400
|
+
``signer`` opts into server-side wallet-signer-match — async mirror of
|
|
401
401
|
:meth:`assess`.
|
|
402
402
|
"""
|
|
403
403
|
body: dict[str, Any] = {}
|
|
@@ -411,8 +411,8 @@ class AgentScore:
|
|
|
411
411
|
body["refresh"] = refresh
|
|
412
412
|
if policy is not None:
|
|
413
413
|
body["policy"] = dict(policy)
|
|
414
|
-
if
|
|
415
|
-
body["
|
|
414
|
+
if signer is not None:
|
|
415
|
+
body["signer"] = dict(signer)
|
|
416
416
|
client = self._get_async_client()
|
|
417
417
|
data, response = await self._send_async_with_response(lambda: client.post("/v1/assess", json=body))
|
|
418
418
|
quota = _extract_quota(response)
|
|
@@ -507,9 +507,9 @@ class AgentScore:
|
|
|
507
507
|
def telemetry_signer_match(self, payload: dict[str, Any]) -> None:
|
|
508
508
|
"""Fire-and-forget telemetry — report a wallet-signer-match verdict.
|
|
509
509
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
510
|
+
Tracks aggregate signer-binding behavior across merchants. Does not raise;
|
|
511
|
+
failures are logged at warning level so persistent telemetry outages are visible
|
|
512
|
+
in ops logs.
|
|
513
513
|
"""
|
|
514
514
|
try:
|
|
515
515
|
client = self._get_sync_client()
|
|
@@ -169,7 +169,7 @@ class DecisionPolicy(TypedDict, total=False):
|
|
|
169
169
|
allowed_jurisdictions: list[str]
|
|
170
170
|
|
|
171
171
|
|
|
172
|
-
class
|
|
172
|
+
class Signer(TypedDict):
|
|
173
173
|
"""Server-side wallet-signer-match request.
|
|
174
174
|
|
|
175
175
|
When passed to ``assess()`` / ``aassess()``, the API resolves this signer wallet
|
|
@@ -187,15 +187,15 @@ class ResolveSigner(TypedDict):
|
|
|
187
187
|
network: Literal["evm", "solana"]
|
|
188
188
|
|
|
189
189
|
|
|
190
|
-
class SignerMatch(TypedDict
|
|
190
|
+
class SignerMatch(TypedDict):
|
|
191
191
|
"""Server-side wallet-signer-match verdict.
|
|
192
192
|
|
|
193
193
|
Emitted on ``AssessResponse.signer_match`` when the request supplied
|
|
194
|
-
``
|
|
194
|
+
``signer``. Mirrors the verdict shape commerce SDK gates produce locally;
|
|
195
195
|
SDK consumers spread this into 403 bodies verbatim instead of re-deriving via 2
|
|
196
196
|
extra ``/v1/assess`` round trips.
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
``kind`` is always present; other fields depend on which kind was emitted.
|
|
199
199
|
"""
|
|
200
200
|
|
|
201
201
|
# ``pass`` — claimed wallet and signer wallet resolve to the same operator (or are
|
|
@@ -204,23 +204,65 @@ class SignerMatch(TypedDict, total=False):
|
|
|
204
204
|
# has no wallet signer); agent should switch to operator_token auth.
|
|
205
205
|
kind: Literal["pass", "wallet_signer_mismatch", "wallet_auth_requires_wallet_signing"]
|
|
206
206
|
# Operator the claimed wallet resolves to. ``None`` if unlinked.
|
|
207
|
-
claimed_operator: str | None
|
|
207
|
+
claimed_operator: NotRequired[str | None]
|
|
208
208
|
# Operator the signer wallet resolves to. ``None`` if unlinked.
|
|
209
|
-
signer_operator: str | None
|
|
209
|
+
signer_operator: NotRequired[str | None]
|
|
210
210
|
# Echoed only on ``wallet_auth_requires_wallet_signing`` — the claimed wallet from
|
|
211
211
|
# the request. Helps agents construct the recovery message.
|
|
212
|
-
claimed_wallet: str
|
|
212
|
+
claimed_wallet: NotRequired[str]
|
|
213
213
|
# Echoed on ``wallet_signer_mismatch`` — the claimed wallet, normalized.
|
|
214
|
-
expected_signer: str
|
|
214
|
+
expected_signer: NotRequired[str]
|
|
215
215
|
# Echoed on ``wallet_signer_mismatch`` — the signer wallet, normalized.
|
|
216
|
-
actual_signer: str
|
|
216
|
+
actual_signer: NotRequired[str]
|
|
217
217
|
# Same-operator linked wallets the agent could re-sign from to satisfy the claim.
|
|
218
218
|
# Mirrors the top-level ``linked_wallets`` deny-guard — omitted on ``deny`` verdicts.
|
|
219
|
-
linked_wallets: list[str]
|
|
219
|
+
linked_wallets: NotRequired[list[str]]
|
|
220
220
|
# JSON-encoded ``{action, steps, user_message}`` envelope for SDK denial bodies.
|
|
221
221
|
# Authoritative copy lives server-side; SDK consumers spread this into their 403
|
|
222
222
|
# body without re-parsing.
|
|
223
|
-
agent_instructions: str
|
|
223
|
+
agent_instructions: NotRequired[str]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class SignerSanctionsClear(TypedDict):
|
|
227
|
+
"""Server-side wallet-sanctions verdict — address NOT on the OFAC SDN list."""
|
|
228
|
+
|
|
229
|
+
status: Literal["clear"]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class SignerSanctionsHit(TypedDict):
|
|
233
|
+
"""Server-side wallet-sanctions verdict — address IS on the OFAC SDN list.
|
|
234
|
+
|
|
235
|
+
Under ``policy.require_sanctions_clear``, this verdict flips the response
|
|
236
|
+
``decision`` to ``deny`` with ``decision_reasons`` including ``sanctions_flagged``.
|
|
237
|
+
An :class:`SignerSanctionsUnavailable` verdict under the same policy yields
|
|
238
|
+
``decision_reasons`` including ``sanctions_check_unavailable`` (fail-closed).
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
sanctioned: Literal[True]
|
|
242
|
+
# Raw OFAC Digital Currency Address label the hit was published under (``ETH``,
|
|
243
|
+
# ``XBT``, ``USDT``, ``SOL``, ...). Investigation-history metadata; the gate's
|
|
244
|
+
# enforcement axis is the format-classified family, not this label.
|
|
245
|
+
ofac_label: str
|
|
246
|
+
# SDN entry's Identity ID. Same ``sdn_uid`` may surface multiple addresses (one
|
|
247
|
+
# entity, multiple wallets); join key for audit.
|
|
248
|
+
sdn_uid: str
|
|
249
|
+
# ISO date OFAC initially designated the entity. ``None`` if upstream omits.
|
|
250
|
+
listed_at: str | None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class SignerSanctionsUnavailable(TypedDict):
|
|
254
|
+
"""Server-side wallet-sanctions verdict — lookup itself failed.
|
|
255
|
+
|
|
256
|
+
Under ``policy.require_sanctions_clear``, the gate fail-closes — falsely allowing
|
|
257
|
+
a sanctioned settle is an OFAC strict-liability violation; falsely denying a clean
|
|
258
|
+
buyer is bad UX.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
status: Literal["unavailable"]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Discriminated union: branch on ``status`` (clear/unavailable) vs ``sanctioned`` (True).
|
|
265
|
+
SignerSanctions = SignerSanctionsClear | SignerSanctionsHit | SignerSanctionsUnavailable
|
|
224
266
|
|
|
225
267
|
|
|
226
268
|
class _AssessResponseRequired(TypedDict):
|
|
@@ -264,8 +306,11 @@ class AssessResponse(_AssessResponseRequired, total=False):
|
|
|
264
306
|
policy_result: PolicyResult | None
|
|
265
307
|
explanation: NotRequired[list[PolicyExplanation]]
|
|
266
308
|
# Server-side wallet-signer-match verdict, returned only when the request supplied
|
|
267
|
-
# ``
|
|
309
|
+
# ``signer``. Empty otherwise.
|
|
268
310
|
signer_match: NotRequired[SignerMatch]
|
|
311
|
+
# Server-side OFAC SDN wallet-address verdict, returned only when the request supplied
|
|
312
|
+
# ``signer``. Empty otherwise.
|
|
313
|
+
signer_sanctions: NotRequired[SignerSanctions]
|
|
269
314
|
# Quota state for this account, captured from response headers on the success path.
|
|
270
315
|
# Use to monitor approach-to-cap proactively (warn at 80%, alert at 95%) before 429.
|
|
271
316
|
quota: NotRequired[QuotaInfo]
|
|
@@ -396,7 +441,8 @@ namespace with a different key scheme."""
|
|
|
396
441
|
|
|
397
442
|
|
|
398
443
|
class AssociateWalletResponse(TypedDict):
|
|
399
|
-
|
|
444
|
+
# Always ``True`` on 2xx; failures surface via :class:`AgentScoreError` instead.
|
|
445
|
+
associated: Literal[True]
|
|
400
446
|
first_seen: bool
|
|
401
447
|
deduped: NotRequired[bool]
|
|
402
448
|
# Cross-merchant pattern hint. Emitted only on the first wallet capture (first_seen=True)
|
|
@@ -274,6 +274,134 @@ def test_assess_forwards_policy_in_body():
|
|
|
274
274
|
assert body["policy"] == policy
|
|
275
275
|
|
|
276
276
|
|
|
277
|
+
# ---------------------------------------------------------------------------
|
|
278
|
+
# assess: signer (server-side wallet-signer-match + sanctions screening)
|
|
279
|
+
# ---------------------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@respx.mock
|
|
283
|
+
def test_assess_forwards_signer_in_body():
|
|
284
|
+
"""assess(signer={...}) opts into server-side wallet-signer-match + signer-sanctions."""
|
|
285
|
+
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
|
|
286
|
+
client = AgentScore(api_key=API_KEY)
|
|
287
|
+
signer = {"address": "0xsigner000000000000000000000000000000abc1", "network": "evm"}
|
|
288
|
+
client.assess(ADDRESS, signer=signer)
|
|
289
|
+
body = json.loads(route.calls.last.request.content)
|
|
290
|
+
assert body["signer"] == signer
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@respx.mock
|
|
294
|
+
def test_assess_signer_omitted_when_none():
|
|
295
|
+
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
|
|
296
|
+
client = AgentScore(api_key=API_KEY)
|
|
297
|
+
client.assess(ADDRESS)
|
|
298
|
+
body = json.loads(route.calls.last.request.content)
|
|
299
|
+
assert "signer" not in body
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@respx.mock
|
|
303
|
+
def test_assess_returns_signer_match_and_signer_sanctions():
|
|
304
|
+
"""Response surface: signer_match (wallet-binding) + signer_sanctions (OFAC SDN) compose on the same call."""
|
|
305
|
+
payload = {
|
|
306
|
+
**ASSESS_PAYLOAD,
|
|
307
|
+
"signer_match": {
|
|
308
|
+
"kind": "wallet_signer_mismatch",
|
|
309
|
+
"claimed_operator": "op_claimed",
|
|
310
|
+
"signer_operator": "op_attacker",
|
|
311
|
+
"expected_signer": "0xclaimed",
|
|
312
|
+
"actual_signer": "0xattacker",
|
|
313
|
+
"linked_wallets": ["0xclaimed"],
|
|
314
|
+
"agent_instructions": '{"action":"resign_or_switch_to_operator_token","steps":[],"user_message":"x"}',
|
|
315
|
+
},
|
|
316
|
+
"signer_sanctions": {
|
|
317
|
+
"sanctioned": True,
|
|
318
|
+
"ofac_label": "ETH",
|
|
319
|
+
"sdn_uid": "19011",
|
|
320
|
+
"listed_at": "2019-09-13T07:00:00.000Z",
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=payload))
|
|
324
|
+
client = AgentScore(api_key=API_KEY)
|
|
325
|
+
result = client.assess(ADDRESS, signer={"address": "0xattacker", "network": "evm"})
|
|
326
|
+
assert result["signer_match"]["kind"] == "wallet_signer_mismatch"
|
|
327
|
+
assert result["signer_match"]["expected_signer"] == "0xclaimed"
|
|
328
|
+
assert result["signer_sanctions"]["sanctioned"] is True
|
|
329
|
+
assert result["signer_sanctions"]["ofac_label"] == "ETH"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@respx.mock
|
|
333
|
+
def test_assess_returns_signer_sanctions_clear():
|
|
334
|
+
payload = {**ASSESS_PAYLOAD, "signer_sanctions": {"status": "clear"}}
|
|
335
|
+
respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=payload))
|
|
336
|
+
client = AgentScore(api_key=API_KEY)
|
|
337
|
+
result = client.assess(ADDRESS, signer={"address": "0xsigner", "network": "evm"})
|
|
338
|
+
assert result["signer_sanctions"]["status"] == "clear"
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@respx.mock
|
|
342
|
+
def test_assess_returns_signer_sanctions_unavailable():
|
|
343
|
+
payload = {**ASSESS_PAYLOAD, "signer_sanctions": {"status": "unavailable"}}
|
|
344
|
+
respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=payload))
|
|
345
|
+
client = AgentScore(api_key=API_KEY)
|
|
346
|
+
result = client.assess(ADDRESS, signer={"address": "0xsigner", "network": "evm"})
|
|
347
|
+
assert result["signer_sanctions"]["status"] == "unavailable"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@pytest.mark.asyncio
|
|
351
|
+
@respx.mock
|
|
352
|
+
async def test_aassess_forwards_signer_in_body():
|
|
353
|
+
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
|
|
354
|
+
client = AgentScore(api_key=API_KEY)
|
|
355
|
+
signer = {"address": "0xsignerasync", "network": "solana"}
|
|
356
|
+
await client.aassess(ADDRESS, signer=signer)
|
|
357
|
+
body = json.loads(route.calls.last.request.content)
|
|
358
|
+
assert body["signer"] == signer
|
|
359
|
+
await client.aclose()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@respx.mock
|
|
363
|
+
def test_assess_signer_preserves_solana_case():
|
|
364
|
+
"""Solana addresses are case-sensitive; signer must thread through unmodified."""
|
|
365
|
+
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
|
|
366
|
+
client = AgentScore(api_key=API_KEY)
|
|
367
|
+
# Real-shape Solana base58 mixed case
|
|
368
|
+
sol = "DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"
|
|
369
|
+
client.assess(ADDRESS, signer={"address": sol, "network": "solana"})
|
|
370
|
+
body = json.loads(route.calls.last.request.content)
|
|
371
|
+
assert body["signer"]["address"] == sol # byte-equal, no lowercasing
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@respx.mock
|
|
375
|
+
def test_assess_signer_address_null_forwarded():
|
|
376
|
+
"""signer.address=None for rails with no wallet signer (Stripe SPT, card)."""
|
|
377
|
+
route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD))
|
|
378
|
+
client = AgentScore(api_key=API_KEY)
|
|
379
|
+
client.assess(ADDRESS, signer={"address": None, "network": "evm"})
|
|
380
|
+
body = json.loads(route.calls.last.request.content)
|
|
381
|
+
assert body["signer"] == {"address": None, "network": "evm"}
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@respx.mock
|
|
385
|
+
def test_assess_signer_raises_token_expired_with_signer():
|
|
386
|
+
"""signer + 401 token_expired: TokenExpiredError raised; signer was still sent."""
|
|
387
|
+
route = respx.post(f"{BASE_URL}/v1/assess").mock(
|
|
388
|
+
return_value=httpx.Response(
|
|
389
|
+
401,
|
|
390
|
+
json={
|
|
391
|
+
"error": {"code": "token_expired", "message": "expired"},
|
|
392
|
+
"verify_url": "https://example/verify",
|
|
393
|
+
},
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
from agentscore.errors import TokenExpiredError
|
|
397
|
+
|
|
398
|
+
client = AgentScore(api_key=API_KEY)
|
|
399
|
+
with pytest.raises(TokenExpiredError):
|
|
400
|
+
client.assess(operator_token="opc_expired", signer={"address": "0xs", "network": "evm"})
|
|
401
|
+
body = json.loads(route.calls.last.request.content)
|
|
402
|
+
assert body["signer"] == {"address": "0xs", "network": "evm"}
|
|
403
|
+
|
|
404
|
+
|
|
277
405
|
# ---------------------------------------------------------------------------
|
|
278
406
|
# Async: aget_reputation
|
|
279
407
|
# ---------------------------------------------------------------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|