Skip to content

Commit 4fd883a

Browse files
vvillait88claude
andcommitted
feat: signer field + signer_sanctions verdict (TEC-295 Phase 2b)
Python-side mirror of agentscore/node-sdk#TBD and the api change in agentscore/core#TBD. Breaking field rename plus a new response type: * assess(resolve_signer=...) -> assess(signer=...) on both sync + async * request body field resolve_signer -> signer (snake_case wire) * ResolveSigner type -> Signer * New SignerSanctions discriminated union: SignerSanctionsClear | SignerSanctionsHit | SignerSanctionsUnavailable * New AssessResponse.signer_sanctions optional field * __init__.py exports updated; ResolveSigner removed from public API No back-compat alias. Callers passing `resolve_signer=...` get a TypeError at call time. The api silently ignores `resolve_signer` request fields if any straggler send them; this SDK won't. Version 2.1.2 -> 2.2.0. Minor bump rather than major because internal consumers (agentscore-commerce + pay) are the primary users and the TypedDict surface catches the rename at type-check time. CLAUDE.md + README + tests updated. 152/152 tests pass; ruff + ty + uv.lock clean. uv.lock refreshed via uv sync --upgrade. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 324f9ed commit 4fd883a

6 files changed

Lines changed: 63 additions & 18 deletions

File tree

.claude/CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ 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`. 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`.
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 — 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`.

agentscore/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@
3333
Reputation,
3434
ReputationResponse,
3535
ReputationStatus,
36-
ResolveSigner,
3736
SessionCreateRequest,
3837
SessionCreateResponse,
3938
SessionPollResponse,
39+
Signer,
4040
SignerMatch,
41+
SignerSanctions,
4142
VerificationLevel,
4243
WalletAuthRequiresSigningBody,
4344
WalletSignerMismatchBody,
@@ -74,11 +75,12 @@
7475
"Reputation",
7576
"ReputationResponse",
7677
"ReputationStatus",
77-
"ResolveSigner",
7878
"SessionCreateRequest",
7979
"SessionCreateResponse",
8080
"SessionPollResponse",
81+
"Signer",
8182
"SignerMatch",
83+
"SignerSanctions",
8284
"TimeoutError",
8385
"TokenExpiredError",
8486
"VerificationLevel",

agentscore/client.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,9 @@ def _build_error_from_response(response: httpx.Response) -> AgentScoreError:
137137
DecisionPolicy,
138138
Network,
139139
ReputationResponse,
140-
ResolveSigner,
141140
SessionCreateResponse,
142141
SessionPollResponse,
142+
Signer,
143143
)
144144

145145

@@ -255,13 +255,13 @@ def assess(
255255
refresh: bool | None = None,
256256
policy: DecisionPolicy | None = None,
257257
operator_token: str | None = None,
258-
resolve_signer: ResolveSigner | None = None,
258+
signer: Signer | None = None,
259259
) -> AssessResponse:
260260
"""Assess a wallet or operator (paid, writes score on-the-fly).
261261
262-
``resolve_signer`` opts into server-side wallet-signer-match: when supplied,
262+
``signer`` opts into server-side wallet-signer-match: when supplied,
263263
the API resolves the signer wallet against the claimed ``address`` and emits
264-
a ``signer_match`` block on the response. See :class:`ResolveSigner`.
264+
a ``signer_match`` block on the response. See :class:`Signer`.
265265
"""
266266
body: dict[str, Any] = {}
267267
if address:
@@ -274,8 +274,8 @@ def assess(
274274
body["refresh"] = refresh
275275
if policy is not None:
276276
body["policy"] = dict(policy)
277-
if resolve_signer is not None:
278-
body["resolve_signer"] = dict(resolve_signer)
277+
if signer is not None:
278+
body["signer"] = dict(signer)
279279
client = self._get_sync_client()
280280
data, response = self._send_sync_with_response(lambda: client.post("/v1/assess", json=body))
281281
quota = _extract_quota(response)
@@ -393,11 +393,11 @@ async def aassess(
393393
refresh: bool | None = None,
394394
policy: DecisionPolicy | None = None,
395395
operator_token: str | None = None,
396-
resolve_signer: ResolveSigner | None = None,
396+
signer: Signer | None = None,
397397
) -> AssessResponse:
398398
"""Assess a wallet or operator (paid, writes score on-the-fly).
399399
400-
``resolve_signer`` opts into server-side wallet-signer-match — async mirror of
400+
``signer`` opts into server-side wallet-signer-match — async mirror of
401401
:meth:`assess`.
402402
"""
403403
body: dict[str, Any] = {}
@@ -411,8 +411,8 @@ async def aassess(
411411
body["refresh"] = refresh
412412
if policy is not None:
413413
body["policy"] = dict(policy)
414-
if resolve_signer is not None:
415-
body["resolve_signer"] = dict(resolve_signer)
414+
if signer is not None:
415+
body["signer"] = dict(signer)
416416
client = self._get_async_client()
417417
data, response = await self._send_async_with_response(lambda: client.post("/v1/assess", json=body))
418418
quota = _extract_quota(response)

agentscore/types.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ class DecisionPolicy(TypedDict, total=False):
169169
allowed_jurisdictions: list[str]
170170

171171

172-
class ResolveSigner(TypedDict):
172+
class Signer(TypedDict):
173173
"""Server-side wallet-signer-match request.
174174
175175
When passed to ``assess()`` / ``aassess()``, the API resolves this signer wallet
@@ -191,7 +191,7 @@ class SignerMatch(TypedDict, total=False):
191191
"""Server-side wallet-signer-match verdict.
192192
193193
Emitted on ``AssessResponse.signer_match`` when the request supplied
194-
``resolve_signer``. Mirrors the verdict shape commerce SDK gates produce locally;
194+
``signer``. Mirrors the verdict shape commerce SDK gates produce locally;
195195
SDK consumers spread this into 403 bodies verbatim instead of re-deriving via 2
196196
extra ``/v1/assess`` round trips.
197197
@@ -223,6 +223,46 @@ class SignerMatch(TypedDict, total=False):
223223
agent_instructions: str
224224

225225

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+
"""
238+
239+
sanctioned: Literal[True]
240+
# Raw OFAC Digital Currency Address label the hit was published under (``ETH``,
241+
# ``XBT``, ``USDT``, ``SOL``, ...). Investigation-history metadata; the gate's
242+
# enforcement axis is the format-classified family, not this label.
243+
ofac_label: str
244+
# SDN entry's Identity ID. Same ``sdn_uid`` may surface multiple addresses (one
245+
# entity, multiple wallets); join key for audit.
246+
sdn_uid: str
247+
# ISO date OFAC initially designated the entity. ``None`` if upstream omits.
248+
listed_at: str | None
249+
250+
251+
class SignerSanctionsUnavailable(TypedDict):
252+
"""Server-side wallet-sanctions verdict — lookup itself failed.
253+
254+
Under ``policy.require_sanctions_clear``, the gate fail-closes — falsely allowing
255+
a sanctioned settle is an OFAC strict-liability violation; falsely denying a clean
256+
buyer is bad UX.
257+
"""
258+
259+
status: Literal["unavailable"]
260+
261+
262+
# Discriminated union: branch on ``status`` (clear/unavailable) vs ``sanctioned`` (True).
263+
SignerSanctions = SignerSanctionsClear | SignerSanctionsHit | SignerSanctionsUnavailable
264+
265+
226266
class _AssessResponseRequired(TypedDict):
227267
decision: str | None
228268
decision_reasons: list[str]
@@ -264,8 +304,11 @@ class AssessResponse(_AssessResponseRequired, total=False):
264304
policy_result: PolicyResult | None
265305
explanation: NotRequired[list[PolicyExplanation]]
266306
# Server-side wallet-signer-match verdict, returned only when the request supplied
267-
# ``resolve_signer``. Empty otherwise.
307+
# ``signer``. Empty otherwise.
268308
signer_match: NotRequired[SignerMatch]
309+
# Server-side OFAC SDN wallet-address verdict, returned only when the request supplied
310+
# ``signer``. Empty otherwise.
311+
signer_sanctions: NotRequired[SignerSanctions]
269312
# Quota state for this account, captured from response headers on the success path.
270313
# Use to monitor approach-to-cap proactively (warn at 80%, alert at 95%) before 429.
271314
quota: NotRequired[QuotaInfo]

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "agentscore-py"
7-
version = "2.1.2"
7+
version = "2.2.0"
88
description = "Python client for the AgentScore APIs"
99
readme = "README.md"
1010
license = "MIT"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)