Skip to content

Commit 925c8d2

Browse files
vvillait88claude
andcommitted
Add verification types, compliance policy fields, X-API-Key auth (TEC-177, TEC-179, TEC-182)
- Add VerificationLevel literal type and OperatorVerification TypedDict - Add verification_level to ReputationResponse - Add operator_verification, verify_url, resolved_operator to AssessResponse - Add compliance policy fields to DecisionPolicy - Switch auth header from Authorization: Bearer to X-API-Key - Add python-dotenv + conftest.py for integration test env loading - Update tests for new types and auth header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ac89838 commit 925c8d2

8 files changed

Lines changed: 237 additions & 13 deletions

File tree

agentscore/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
DecisionPolicy,
88
EntityType,
99
Grade,
10+
OperatorVerification,
1011
Reputation,
1112
ReputationResponse,
1213
ReputationStatus,
14+
VerificationLevel,
1315
)
1416

1517
__version__ = _pkg_version("agentscore-py")
@@ -21,8 +23,10 @@
2123
"DecisionPolicy",
2224
"EntityType",
2325
"Grade",
26+
"OperatorVerification",
2427
"Reputation",
2528
"ReputationResponse",
2629
"ReputationStatus",
30+
"VerificationLevel",
2731
"__version__",
2832
]

agentscore/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def __init__(
3535
def _headers(self) -> dict:
3636
return {
3737
"Accept": "application/json",
38-
"Authorization": f"Bearer {self.api_key}",
38+
"X-API-Key": self.api_key,
3939
"User-Agent": f"agentscore-py/{_pkg_version('agentscore-py')}",
4040
}
4141

agentscore/types.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
Grade = Literal["A", "B", "C", "D", "F"]
66
EntityType = Literal["agent", "service", "hybrid", "wallet", "bot", "unknown"]
77
ReputationStatus = Literal["scored", "stale", "known_unscored"]
8+
VerificationLevel = Literal["none", "wallet_claimed", "kyc_verified"]
89

910

1011
class Subject(TypedDict):
@@ -132,12 +133,28 @@ class ReputationResponse(_ReputationResponseRequired, total=False):
132133
reputation: Reputation
133134
operator_score: OperatorScore
134135
agents: list[AgentSummary]
136+
verification_level: VerificationLevel
137+
138+
139+
class _OperatorVerificationRequired(TypedDict):
140+
level: VerificationLevel
141+
142+
143+
class OperatorVerification(_OperatorVerificationRequired, total=False):
144+
operator_type: str | None
145+
claimed_at: str | None
146+
verified_at: str | None
135147

136148

137149
class DecisionPolicy(TypedDict, total=False):
138150
min_grade: Grade
139151
min_score: int
140152
require_verified_payment_activity: bool
153+
require_kyc: bool
154+
require_sanctions_clear: bool
155+
min_age: int
156+
blocked_jurisdictions: list[str]
157+
require_entity_type: str
141158

142159

143160
class _AssessResponseRequired(TypedDict):
@@ -156,3 +173,6 @@ class AssessResponse(_AssessResponseRequired, total=False):
156173
operator_score: OperatorScore | None
157174
reputation: Reputation | None
158175
agents: list[AgentSummary]
176+
operator_verification: OperatorVerification
177+
resolved_operator: str
178+
verify_url: str

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ dev = [
4646
"pytest-cov>=6.0",
4747
"respx>=0.22.0",
4848
"pytest-asyncio>=1.2.0",
49+
"python-dotenv>=1.2.1",
4950
]

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from dotenv import load_dotenv
2+
3+
load_dotenv()

tests/test_client.py

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def test_assess_raises_on_402():
220220

221221

222222
# ---------------------------------------------------------------------------
223-
# Authorization header
223+
# API key header
224224
# ---------------------------------------------------------------------------
225225

226226
REPUTATION_PAYLOAD_SIMPLE = {
@@ -239,7 +239,7 @@ def test_auth_header_is_sent():
239239
)
240240
client = AgentScore(api_key="my-secret-key")
241241
client.get_reputation(ADDRESS)
242-
assert route.calls.last.request.headers["authorization"] == "Bearer my-secret-key"
242+
assert route.calls.last.request.headers["x-api-key"] == "my-secret-key"
243243

244244

245245
# ---------------------------------------------------------------------------
@@ -485,3 +485,182 @@ def test_error_response_no_error_key_fallback():
485485
client.get_reputation(ADDRESS)
486486
assert exc_info.value.status_code == 422
487487
assert exc_info.value.code == "unknown_error"
488+
489+
490+
# ---------------------------------------------------------------------------
491+
# Verification / Compliance fields
492+
# ---------------------------------------------------------------------------
493+
494+
495+
REPUTATION_WITH_VERIFICATION = {
496+
**REPUTATION_PAYLOAD,
497+
"verification_level": "kyc_verified",
498+
}
499+
500+
ASSESS_WITH_COMPLIANCE = {
501+
**ASSESS_PAYLOAD,
502+
"decision": "deny",
503+
"decision_reasons": ["kyc_required", "sanctions_check_pending"],
504+
"operator_verification": {
505+
"level": "none",
506+
"operator_type": None,
507+
"claimed_at": None,
508+
"verified_at": None,
509+
},
510+
"verify_url": "https://agentscore.sh/verify/abc123",
511+
"resolved_operator": "0xoperator456",
512+
}
513+
514+
515+
@respx.mock
516+
def test_get_reputation_returns_verification_level():
517+
respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(
518+
return_value=httpx.Response(200, json=REPUTATION_WITH_VERIFICATION)
519+
)
520+
client = AgentScore(api_key=API_KEY)
521+
result = client.get_reputation(ADDRESS)
522+
assert result["verification_level"] == "kyc_verified"
523+
524+
525+
@respx.mock
526+
def test_get_reputation_omits_verification_level_when_absent():
527+
respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(
528+
return_value=httpx.Response(200, json=REPUTATION_PAYLOAD)
529+
)
530+
client = AgentScore(api_key=API_KEY)
531+
result = client.get_reputation(ADDRESS)
532+
assert "verification_level" not in result
533+
534+
535+
@respx.mock
536+
def test_assess_returns_operator_verification():
537+
respx.post(f"{BASE_URL}/v1/assess").mock(
538+
return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)
539+
)
540+
client = AgentScore(api_key=API_KEY)
541+
result = client.assess(ADDRESS)
542+
assert result["operator_verification"]["level"] == "none"
543+
assert result["operator_verification"]["operator_type"] is None
544+
545+
546+
@respx.mock
547+
def test_assess_returns_verify_url():
548+
respx.post(f"{BASE_URL}/v1/assess").mock(
549+
return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)
550+
)
551+
client = AgentScore(api_key=API_KEY)
552+
result = client.assess(ADDRESS)
553+
assert result["verify_url"] == "https://agentscore.sh/verify/abc123"
554+
555+
556+
@respx.mock
557+
def test_assess_returns_resolved_operator():
558+
respx.post(f"{BASE_URL}/v1/assess").mock(
559+
return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)
560+
)
561+
client = AgentScore(api_key=API_KEY)
562+
result = client.assess(ADDRESS)
563+
assert result["resolved_operator"] == "0xoperator456"
564+
565+
566+
@respx.mock
567+
def test_assess_omits_verification_fields_when_absent():
568+
respx.post(f"{BASE_URL}/v1/assess").mock(
569+
return_value=httpx.Response(200, json=ASSESS_PAYLOAD)
570+
)
571+
client = AgentScore(api_key=API_KEY)
572+
result = client.assess(ADDRESS)
573+
assert "operator_verification" not in result
574+
assert "verify_url" not in result
575+
assert "resolved_operator" not in result
576+
577+
578+
@respx.mock
579+
def test_assess_sends_compliance_policy_fields():
580+
route = respx.post(f"{BASE_URL}/v1/assess").mock(
581+
return_value=httpx.Response(200, json=ASSESS_PAYLOAD)
582+
)
583+
client = AgentScore(api_key=API_KEY)
584+
policy = {
585+
"require_kyc": True,
586+
"require_sanctions_clear": True,
587+
"min_age": 90,
588+
"blocked_jurisdictions": ["KP", "IR"],
589+
"require_entity_type": "agent",
590+
}
591+
client.assess(ADDRESS, policy=policy)
592+
body = json.loads(route.calls.last.request.content)
593+
assert body["policy"]["require_kyc"] is True
594+
assert body["policy"]["require_sanctions_clear"] is True
595+
assert body["policy"]["min_age"] == 90
596+
assert body["policy"]["blocked_jurisdictions"] == ["KP", "IR"]
597+
assert body["policy"]["require_entity_type"] == "agent"
598+
599+
600+
@pytest.mark.asyncio
601+
@respx.mock
602+
async def test_aget_reputation_returns_verification_level():
603+
respx.get(f"{BASE_URL}/v1/reputation/{ADDRESS}").mock(
604+
return_value=httpx.Response(200, json=REPUTATION_WITH_VERIFICATION)
605+
)
606+
client = AgentScore(api_key=API_KEY)
607+
result = await client.aget_reputation(ADDRESS)
608+
assert result["verification_level"] == "kyc_verified"
609+
await client.aclose()
610+
611+
612+
@pytest.mark.asyncio
613+
@respx.mock
614+
async def test_aassess_returns_compliance_fields():
615+
respx.post(f"{BASE_URL}/v1/assess").mock(
616+
return_value=httpx.Response(200, json=ASSESS_WITH_COMPLIANCE)
617+
)
618+
client = AgentScore(api_key=API_KEY)
619+
result = await client.aassess(ADDRESS)
620+
assert result["operator_verification"]["level"] == "none"
621+
assert result["verify_url"] == "https://agentscore.sh/verify/abc123"
622+
assert result["resolved_operator"] == "0xoperator456"
623+
await client.aclose()
624+
625+
626+
# ---------------------------------------------------------------------------
627+
# Integration-style: compliance deny flow
628+
# ---------------------------------------------------------------------------
629+
630+
631+
@respx.mock
632+
def test_full_compliance_deny_flow():
633+
"""Full assess flow with compliance policy returning deny + verify_url."""
634+
compliance_response = {
635+
**REPUTATION_PAYLOAD,
636+
"decision": "deny",
637+
"decision_reasons": ["kyc_required", "sanctions_check_pending"],
638+
"on_the_fly": False,
639+
"operator_verification": {
640+
"level": "none",
641+
"operator_type": None,
642+
"claimed_at": None,
643+
"verified_at": None,
644+
},
645+
"verify_url": "https://agentscore.sh/verify/xyz789",
646+
}
647+
route = respx.post(f"{BASE_URL}/v1/assess").mock(
648+
return_value=httpx.Response(200, json=compliance_response)
649+
)
650+
client = AgentScore(api_key=API_KEY)
651+
result = client.assess(
652+
ADDRESS,
653+
policy={
654+
"require_kyc": True,
655+
"require_sanctions_clear": True,
656+
},
657+
)
658+
assert result["decision"] == "deny"
659+
assert "kyc_required" in result["decision_reasons"]
660+
assert "sanctions_check_pending" in result["decision_reasons"]
661+
assert result["verify_url"] == "https://agentscore.sh/verify/xyz789"
662+
assert result["operator_verification"]["level"] == "none"
663+
664+
body = json.loads(route.calls.last.request.content)
665+
assert body["policy"]["require_kyc"] is True
666+
assert body["policy"]["require_sanctions_clear"] is True

tests/test_integration.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,6 @@ def test_get_reputation_operator_score():
104104
assert isinstance(op["chains_active"], list)
105105

106106

107-
def test_get_reputation_reputation_field():
108-
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
109-
rep = client.get_reputation(TEST_ADDRESS)
110-
r = rep.get("reputation")
111-
if not r:
112-
pytest.skip("no reputation on test address")
113-
assert isinstance(r["feedback_count"], int)
114-
assert isinstance(r["client_count"], int)
115-
116-
117107
def test_assess_then_get_reputation():
118108
client = AgentScore(api_key=API_KEY, base_url=BASE_URL)
119109
assessed = client.assess(TEST_ADDRESS)

uv.lock

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)