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..0738043 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, @@ -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/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.""" diff --git a/pyproject.toml b/pyproject.toml index 4853c67..e3636bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,8 +4,8 @@ build-backend = "hatchling.build" [project] name = "agentscore-py" -version = "2.0.0" -description = "Python client for the AgentScore trust and reputation API" +version = "2.0.1" +description = "Python client for the AgentScore APIs" readme = "README.md" license = "MIT" requires-python = ">=3.11" diff --git a/tests/test_client.py b/tests/test_client.py index a00319c..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 @@ -515,7 +527,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 +645,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 +666,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" 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" },