Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# agentscore-py

Python client for the AgentScore trust and reputation API.
Python client for the AgentScore APIs.

## Identity Model

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 7 additions & 7 deletions agentscore/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand Down
31 changes: 20 additions & 11 deletions agentscore/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
20 changes: 16 additions & 4 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading