From d284bdcbfd1a81d3651a138abc0ea4afb81acb7a Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Wed, 29 Apr 2026 19:49:36 -0700 Subject: [PATCH 1/5] chore: replace fake reason code in test_client with real API code \`sanctions_check_pending\` isn't a real API code. The real codes are kyc_required, kyc_pending, kyc_failed, sanctions_flagged, age_insufficient, jurisdiction_restricted. Test passes either way (the SDK passes reasons through verbatim), but the fake string propagates misinformation. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_client.py b/tests/test_client.py index a00319c..888648c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -515,7 +515,7 @@ def test_error_response_no_error_key_fallback(): ASSESS_WITH_COMPLIANCE = { **ASSESS_PAYLOAD, "decision": "deny", - "decision_reasons": ["kyc_required", "sanctions_check_pending"], + "decision_reasons": ["kyc_required", "sanctions_flagged"], "operator_verification": { "level": "none", "operator_type": None, @@ -633,7 +633,7 @@ def test_full_compliance_deny_flow(): compliance_response = { **REPUTATION_PAYLOAD, "decision": "deny", - "decision_reasons": ["kyc_required", "sanctions_check_pending"], + "decision_reasons": ["kyc_required", "sanctions_flagged"], "on_the_fly": False, "operator_verification": { "level": "none", @@ -654,7 +654,7 @@ def test_full_compliance_deny_flow(): ) assert result["decision"] == "deny" assert "kyc_required" in result["decision_reasons"] - assert "sanctions_check_pending" in result["decision_reasons"] + assert "sanctions_flagged" in result["decision_reasons"] assert result["verify_url"] == "https://agentscore.sh/verify/xyz789" assert result["operator_verification"]["level"] == "none" From de9784e5ab69e459a6fecc20aa836c122dd7dcfa Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Wed, 29 Apr 2026 20:02:24 -0700 Subject: [PATCH 2/5] fix: add invalid_credential to DenialCode union MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The commerce gate emits `invalid_credential` on 401 (separate from token_expired — permanent state, no auto-session). Tests already exercise this code path but the DenialCode literal was missing it; type checkers would flag any branch on this value as unreachable. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore/types.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/agentscore/types.py b/agentscore/types.py index 02ce3d7..e6de9cb 100644 --- a/agentscore/types.py +++ b/agentscore/types.py @@ -338,23 +338,32 @@ class AssociateWalletResponse(TypedDict): DenialCode = Literal[ - "operator_verification_required", - "compliance_denied", - "compliance_error", - "wallet_not_trusted", + # Gate-emitted codes from commerce middleware (canonical 9-element union) "missing_identity", "identity_verification_required", - "payment_required", - "api_error", - "kyc_required", - # Wallet-signer binding — claimed X-Wallet-Address must resolve to the same operator - # as the payment signer; wallet-auth is rejected on rails with no wallet signer. - "wallet_signer_mismatch", - "wallet_auth_requires_wallet_signing", # Credential is no longer valid (revoked or past its TTL — the two cases share this # code deliberately so the API doesn't leak which one). The 401 body carries an # auto-minted session so agents recover without holding an API key. "token_expired", + # Credential doesn't exist at all (typo, fabricated, never minted). Permanent state; + # no auto-session is issued because the agent may have other valid tokens to try. + "invalid_credential", + # Wallet-signer binding — claimed X-Wallet-Address must resolve to the same operator + # as the payment signer; wallet-auth is rejected on rails with no wallet signer. + "wallet_signer_mismatch", + "wallet_auth_requires_wallet_signing", + "wallet_not_trusted", + "api_error", + "payment_required", + # Merchant-emitted convenience codes (e.g. martin-estate's on_denied wraps gate + # denials into wine-specific business codes). Not emitted by the AgentScore API + # itself but appear in 4xx bodies the SDK may surface back to callers. + "operator_verification_required", + "compliance_denied", + "compliance_error", + # Decision-reason code surfaced in error.code by some merchants — kept for back-compat + # with merchants that flatten policy reasons into the error envelope. + "kyc_required", ] """Denial codes returned by the gate in 403/402 error bodies. Lets agents pick the right remediation without natural-language parsing.""" From b142ac03031a480aef663c0d61674e0b46788da0 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Wed, 29 Apr 2026 20:37:20 -0700 Subject: [PATCH 3/5] chore: bump to v2.0.1 Patch release for additive DenialCode union member (\`invalid_credential\`) and the \`opc_test_*\` test fixture cleanup. No breaking changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4853c67..a65dfed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-py" -version = "2.0.0" +version = "2.0.1" description = "Python client for the AgentScore trust and reputation API" readme = "README.md" license = "MIT" diff --git a/uv.lock b/uv.lock index 8926070..31bc290 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.11" [[package]] name = "agentscore-py" -version = "2.0.0" +version = "2.0.1" source = { editable = "." } dependencies = [ { name = "httpx" }, From 37df524732098f8f7799ab81eee97410c05bddcc Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Wed, 29 Apr 2026 20:39:58 -0700 Subject: [PATCH 4/5] =?UTF-8?q?chore:=20rename=20description=20from=20"tru?= =?UTF-8?q?st=20and=20reputation=20API"=20=E2=86=92=20"APIs"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK now wraps multiple AgentScore APIs (assess + sessions + credentials + captureWallet, etc.), not just the legacy trust+reputation endpoint. Update the package description, README tagline, CLAUDE.md, and class docstring. Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/CLAUDE.md | 2 +- README.md | 2 +- agentscore/client.py | 2 +- pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8de0479..d035936 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -1,6 +1,6 @@ # agentscore-py -Python client for the AgentScore trust and reputation API. +Python client for the AgentScore APIs. ## Identity Model diff --git a/README.md b/README.md index bd9202e..cd4f51d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/agentscore-py.svg)](https://pypi.org/project/agentscore-py/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -Python client for the [AgentScore](https://agentscore.sh) trust and reputation API. +Python client for the [AgentScore](https://agentscore.sh) APIs. ## Install diff --git a/agentscore/client.py b/agentscore/client.py index c8d7876..722e1ba 100644 --- a/agentscore/client.py +++ b/agentscore/client.py @@ -42,7 +42,7 @@ def _retry_after_seconds(response: httpx.Response) -> float: class AgentScore: - """Client for the AgentScore trust and reputation API.""" + """Client for the AgentScore APIs.""" def __init__( self, diff --git a/pyproject.toml b/pyproject.toml index a65dfed..e3636bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-py" version = "2.0.1" -description = "Python client for the AgentScore trust and reputation API" +description = "Python client for the AgentScore APIs" readme = "README.md" license = "MIT" requires-python = ">=3.11" From 1a6bc0fd6ac9766da88bcb35b912235cc1052620 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Wed, 29 Apr 2026 20:51:05 -0700 Subject: [PATCH 5/5] fix(assess): include refresh in body when explicitly set (parity with node-sdk) Python's assess() / aassess() previously dropped \`refresh: False\` entirely (only sent when truthy). Node SDK includes whatever the consumer explicitly passed. This is a wire-format divergence: agents passing \`refresh=False\` expecting the field to be present (e.g., to signal "no refresh needed") got different payloads from the two SDKs. Change the signature to \`refresh: bool | None = None\` and include in body when not-None. Default (no kwarg) omits the field as before; explicit \`refresh=False\` now sends false. Matches node-sdk semantics exactly. Tests: - old: refresh=False expected to omit field - new: refresh=False sends false; default omits field Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore/client.py | 12 ++++++------ tests/test_client.py | 14 +++++++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/agentscore/client.py b/agentscore/client.py index 722e1ba..0738043 100644 --- a/agentscore/client.py +++ b/agentscore/client.py @@ -153,7 +153,7 @@ def assess( self, address: str | None = None, chain: str | None = None, - refresh: bool = False, + refresh: bool | None = None, policy: DecisionPolicy | None = None, operator_token: str | None = None, ) -> AssessResponse: @@ -165,8 +165,8 @@ def assess( body["operator_token"] = operator_token if chain: body["chain"] = chain - if refresh: - body["refresh"] = True + if refresh is not None: + body["refresh"] = refresh if policy is not None: body["policy"] = dict(policy) client = self._get_sync_client() @@ -279,7 +279,7 @@ async def aassess( self, address: str | None = None, chain: str | None = None, - refresh: bool = False, + refresh: bool | None = None, policy: DecisionPolicy | None = None, operator_token: str | None = None, ) -> AssessResponse: @@ -291,8 +291,8 @@ async def aassess( body["operator_token"] = operator_token if chain: body["chain"] = chain - if refresh: - body["refresh"] = True + if refresh is not None: + body["refresh"] = refresh if policy is not None: body["policy"] = dict(policy) client = self._get_async_client() diff --git a/tests/test_client.py b/tests/test_client.py index 888648c..5262217 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -407,11 +407,23 @@ def test_user_agent_header_prepends_custom_user_agent(): @respx.mock -def test_assess_refresh_false_not_included_in_body(): +def test_assess_refresh_explicit_false_sent_in_body(): + # Matches node-sdk semantics: when consumer explicitly passes refresh=False, + # the SDK includes `refresh: false` in the body. Default (no value passed) + # omits the field entirely. route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD)) client = AgentScore(api_key=API_KEY) client.assess(ADDRESS, refresh=False) body = json.loads(route.calls.last.request.content) + assert body.get("refresh") is False + + +@respx.mock +def test_assess_refresh_omitted_when_default(): + route = respx.post(f"{BASE_URL}/v1/assess").mock(return_value=httpx.Response(200, json=ASSESS_PAYLOAD)) + client = AgentScore(api_key=API_KEY) + client.assess(ADDRESS) + body = json.loads(route.calls.last.request.content) assert "refresh" not in body