Skip to content

Commit 79f7bea

Browse files
vvillait88claude
andauthored
Identity verification and compliance gating (v1.5.0) (#11)
## Summary - Identity expansion for v1.5.0 release - See agentscore/core#148 for the full changeset ## Test plan - [x] All tests pass locally 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 53087c9 commit 79f7bea

12 files changed

Lines changed: 779 additions & 63 deletions

File tree

.claude/CLAUDE.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
Python client for the AgentScore trust and reputation API.
44

5+
## Identity Model
6+
7+
## Methods (sync + async)
8+
9+
- `get_reputation` / `aget_reputation` — cached reputation lookup (free)
10+
- `assess` / `aassess` — identity gate with policy (paid). Accepts `operator_token` for non-wallet agents.
11+
- `create_session` / `acreate_session` — create verification session
12+
- `poll_session` / `apoll_session` — poll session status, returns credential when verified
13+
- `create_credential` / `acreate_credential` — create operator credential (24h TTL default)
14+
- `list_credentials` / `alist_credentials` — list active credentials
15+
- `revoke_credential` / `arevoke_credential` — revoke a credential
16+
517
## Architecture
618

719
Single-package Python library published to PyPI.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
ci:
1414
runs-on: blacksmith-2vcpu-ubuntu-2404
1515
steps:
16-
- uses: actions/checkout@v6
16+
- uses: useblacksmith/checkout@v1
1717
- uses: astral-sh/setup-uv@v7
1818
- run: uv sync --frozen --all-extras
1919
- run: uv run ruff check .

.github/workflows/security.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
runs-on: blacksmith-2vcpu-ubuntu-2404
2020
timeout-minutes: 5
2121
steps:
22-
- uses: actions/checkout@v6
22+
- uses: useblacksmith/checkout@v1
2323

2424
- name: Install osv-scanner
2525
run: |
@@ -34,7 +34,7 @@ jobs:
3434
runs-on: blacksmith-2vcpu-ubuntu-2404
3535
timeout-minutes: 5
3636
steps:
37-
- uses: actions/checkout@v6
37+
- uses: useblacksmith/checkout@v1
3838
- uses: astral-sh/setup-uv@v7
3939
- uses: actions/setup-python@v6
4040
with:

README.md

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ print(rep["score"]["value"], rep["score"]["grade"])
2525
# Filter to a specific chain
2626
base_rep = client.get_reputation("0x1234...", chain="base")
2727

28-
# On-the-fly assessment with policy (paid)
29-
result = client.assess("0x1234...", policy={"min_grade": "B", "min_score": 35})
30-
print(result["decision"], result["decision_reasons"])
31-
32-
# Compliance assessment with verification policy
28+
# Identity gate with policy (paid)
3329
gated = client.assess("0x1234...", policy={
3430
"require_kyc": True,
3531
"require_sanctions_clear": True,
@@ -45,12 +41,53 @@ rep = client.get_reputation("0x1234...")
4541
print(rep.get("verification_level")) # "none" | "wallet_claimed" | "kyc_verified"
4642
```
4743

44+
### Credential-Based Identity
45+
46+
Agents without wallets can use operator credentials for identity:
47+
48+
```python
49+
result = client.assess(operator_token="opc_...")
50+
print(result["decision"]) # "allow" | "deny"
51+
```
52+
53+
### Verification Sessions
54+
55+
Bootstrap identity for first-time agents:
56+
57+
```python
58+
session = client.create_session()
59+
print(session["verify_url"], session["poll_secret"])
60+
61+
status = client.poll_session(session["session_id"], session["poll_secret"])
62+
if status["status"] == "verified":
63+
print(status["operator_token"]) # "opc_..." — use for future requests
64+
```
65+
66+
### Credential Management
67+
68+
```python
69+
cred = client.create_credential(label="my-agent", ttl_days=7)
70+
print(cred["credential"]) # shown once
71+
72+
credentials = client.list_credentials()
73+
client.revoke_credential(cred["id"])
74+
```
75+
4876
### Async
4977

78+
All methods have async variants prefixed with `a`:
79+
5080
```python
5181
async with AgentScore(api_key="as_live_...") as client:
5282
rep = await client.aget_reputation("0x1234...")
53-
result = await client.aassess("0x1234...", policy={"min_grade": "B"})
83+
result = await client.aassess("0x1234...", policy={"require_kyc": True})
84+
85+
# Identity model methods
86+
session = await client.acreate_session()
87+
status = await client.apoll_session(session["session_id"], session["poll_secret"])
88+
cred = await client.acreate_credential(label="my-agent")
89+
await client.alist_credentials()
90+
await client.arevoke_credential(cred["id"])
5491
```
5592

5693
### Context Manager

agentscore/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,19 @@
44
from agentscore.errors import AgentScoreError
55
from agentscore.types import (
66
AssessResponse,
7+
CredentialCreateResponse,
8+
CredentialItem,
9+
CredentialListResponse,
710
DecisionPolicy,
811
EntityType,
912
Grade,
1013
OperatorVerification,
1114
Reputation,
1215
ReputationResponse,
1316
ReputationStatus,
17+
SessionCreateRequest,
18+
SessionCreateResponse,
19+
SessionPollResponse,
1420
VerificationLevel,
1521
)
1622

@@ -20,13 +26,19 @@
2026
"AgentScore",
2127
"AgentScoreError",
2228
"AssessResponse",
29+
"CredentialCreateResponse",
30+
"CredentialItem",
31+
"CredentialListResponse",
2332
"DecisionPolicy",
2433
"EntityType",
2534
"Grade",
2635
"OperatorVerification",
2736
"Reputation",
2837
"ReputationResponse",
2938
"ReputationStatus",
39+
"SessionCreateRequest",
40+
"SessionCreateResponse",
41+
"SessionPollResponse",
3042
"VerificationLevel",
3143
"__version__",
3244
]

agentscore/client.py

Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@
1010
if TYPE_CHECKING:
1111
from agentscore.types import (
1212
AssessResponse,
13+
CredentialCreateResponse,
14+
CredentialListResponse,
1315
DecisionPolicy,
1416
ReputationResponse,
17+
SessionCreateResponse,
18+
SessionPollResponse,
1519
)
1620

1721

@@ -58,6 +62,13 @@ def _get_async_client(self) -> httpx.AsyncClient:
5862
return self._async_client
5963

6064
def _handle_response(self, response: httpx.Response) -> dict:
65+
if response.status_code == 429:
66+
retry_after = response.headers.get("retry-after", "1")
67+
raise AgentScoreError(
68+
code="rate_limited",
69+
message=f"Rate limit exceeded. Retry after {retry_after}s",
70+
status_code=429,
71+
)
6172
if response.status_code >= 400:
6273
try:
6374
body = response.json()
@@ -95,13 +106,18 @@ def get_reputation(self, address: str, chain: str | None = None) -> ReputationRe
95106

96107
def assess(
97108
self,
98-
address: str,
109+
address: str | None = None,
99110
chain: str | None = None,
100111
refresh: bool = False,
101112
policy: DecisionPolicy | None = None,
113+
operator_token: str | None = None,
102114
) -> AssessResponse:
103-
"""Assess a wallet (paid, writes score on-the-fly)."""
104-
body: dict[str, Any] = {"address": address}
115+
"""Assess a wallet or operator (paid, writes score on-the-fly)."""
116+
body: dict[str, Any] = {}
117+
if address:
118+
body["address"] = address
119+
if operator_token:
120+
body["operator_token"] = operator_token
105121
if chain:
106122
body["chain"] = chain
107123
if refresh:
@@ -112,6 +128,57 @@ def assess(
112128
response = client.post("/v1/assess", json=body)
113129
return self._handle_response(response)
114130

131+
def create_session(
132+
self,
133+
context: str | None = None,
134+
product_name: str | None = None,
135+
) -> SessionCreateResponse:
136+
"""Create an assessment session for deferred scoring."""
137+
body: dict[str, Any] = {}
138+
if context is not None:
139+
body["context"] = context
140+
if product_name is not None:
141+
body["product_name"] = product_name
142+
client = self._get_sync_client()
143+
response = client.post("/v1/sessions", json=body)
144+
return self._handle_response(response)
145+
146+
def poll_session(self, session_id: str, poll_secret: str) -> SessionPollResponse:
147+
"""Poll a session for its current status and result."""
148+
client = self._get_sync_client()
149+
response = client.get(
150+
f"/v1/sessions/{session_id}",
151+
headers={"X-Poll-Secret": poll_secret},
152+
)
153+
return self._handle_response(response)
154+
155+
def create_credential(
156+
self,
157+
label: str | None = None,
158+
ttl_days: int | None = None,
159+
) -> CredentialCreateResponse:
160+
"""Create a new API credential."""
161+
body: dict[str, Any] = {}
162+
if label is not None:
163+
body["label"] = label
164+
if ttl_days is not None:
165+
body["ttl_days"] = ttl_days
166+
client = self._get_sync_client()
167+
response = client.post("/v1/credentials", json=body)
168+
return self._handle_response(response)
169+
170+
def list_credentials(self) -> CredentialListResponse:
171+
"""List all API credentials."""
172+
client = self._get_sync_client()
173+
response = client.get("/v1/credentials")
174+
return self._handle_response(response)
175+
176+
def revoke_credential(self, id: str) -> dict:
177+
"""Revoke an API credential by ID."""
178+
client = self._get_sync_client()
179+
response = client.delete(f"/v1/credentials/{id}")
180+
return self._handle_response(response)
181+
115182
# --- Async methods ---
116183

117184
async def aget_reputation(self, address: str, chain: str | None = None) -> ReputationResponse:
@@ -125,13 +192,18 @@ async def aget_reputation(self, address: str, chain: str | None = None) -> Reput
125192

126193
async def aassess(
127194
self,
128-
address: str,
195+
address: str | None = None,
129196
chain: str | None = None,
130197
refresh: bool = False,
131198
policy: DecisionPolicy | None = None,
199+
operator_token: str | None = None,
132200
) -> AssessResponse:
133-
"""Assess a wallet (paid, writes score on-the-fly)."""
134-
body: dict[str, Any] = {"address": address}
201+
"""Assess a wallet or operator (paid, writes score on-the-fly)."""
202+
body: dict[str, Any] = {}
203+
if address:
204+
body["address"] = address
205+
if operator_token:
206+
body["operator_token"] = operator_token
135207
if chain:
136208
body["chain"] = chain
137209
if refresh:
@@ -142,6 +214,57 @@ async def aassess(
142214
response = await client.post("/v1/assess", json=body)
143215
return self._handle_response(response)
144216

217+
async def acreate_session(
218+
self,
219+
context: str | None = None,
220+
product_name: str | None = None,
221+
) -> SessionCreateResponse:
222+
"""Create an assessment session for deferred scoring."""
223+
body: dict[str, Any] = {}
224+
if context is not None:
225+
body["context"] = context
226+
if product_name is not None:
227+
body["product_name"] = product_name
228+
client = self._get_async_client()
229+
response = await client.post("/v1/sessions", json=body)
230+
return self._handle_response(response)
231+
232+
async def apoll_session(self, session_id: str, poll_secret: str) -> SessionPollResponse:
233+
"""Poll a session for its current status and result."""
234+
client = self._get_async_client()
235+
response = await client.get(
236+
f"/v1/sessions/{session_id}",
237+
headers={"X-Poll-Secret": poll_secret},
238+
)
239+
return self._handle_response(response)
240+
241+
async def acreate_credential(
242+
self,
243+
label: str | None = None,
244+
ttl_days: int | None = None,
245+
) -> CredentialCreateResponse:
246+
"""Create a new API credential."""
247+
body: dict[str, Any] = {}
248+
if label is not None:
249+
body["label"] = label
250+
if ttl_days is not None:
251+
body["ttl_days"] = ttl_days
252+
client = self._get_async_client()
253+
response = await client.post("/v1/credentials", json=body)
254+
return self._handle_response(response)
255+
256+
async def alist_credentials(self) -> CredentialListResponse:
257+
"""List all API credentials."""
258+
client = self._get_async_client()
259+
response = await client.get("/v1/credentials")
260+
return self._handle_response(response)
261+
262+
async def arevoke_credential(self, id: str) -> dict:
263+
"""Revoke an API credential by ID."""
264+
client = self._get_async_client()
265+
response = await client.delete(f"/v1/credentials/{id}")
266+
return self._handle_response(response)
267+
145268
def close(self):
146269
if self._sync_client:
147270
self._sync_client.close()

0 commit comments

Comments
 (0)