xache 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xache/__init__.py +142 -0
- xache/client.py +331 -0
- xache/crypto/__init__.py +17 -0
- xache/crypto/signing.py +244 -0
- xache/crypto/wallet.py +240 -0
- xache/errors.py +184 -0
- xache/payment/__init__.py +5 -0
- xache/payment/handler.py +244 -0
- xache/services/__init__.py +29 -0
- xache/services/budget.py +285 -0
- xache/services/collective.py +174 -0
- xache/services/extraction.py +173 -0
- xache/services/facilitator.py +296 -0
- xache/services/identity.py +415 -0
- xache/services/memory.py +401 -0
- xache/services/owner.py +293 -0
- xache/services/receipts.py +202 -0
- xache/services/reputation.py +274 -0
- xache/services/royalty.py +290 -0
- xache/services/sessions.py +268 -0
- xache/services/workspaces.py +447 -0
- xache/types.py +399 -0
- xache/utils/__init__.py +5 -0
- xache/utils/cache.py +214 -0
- xache/utils/http.py +209 -0
- xache/utils/retry.py +101 -0
- xache-5.0.0.dist-info/METADATA +337 -0
- xache-5.0.0.dist-info/RECORD +30 -0
- xache-5.0.0.dist-info/WHEEL +5 -0
- xache-5.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""Identity Service - Agent registration and ownership per LLD §2.2"""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
from ..types import (
|
|
6
|
+
RegisterIdentityResponse,
|
|
7
|
+
SubmitClaimRequest,
|
|
8
|
+
SubmitClaimResponse,
|
|
9
|
+
ProcessClaimRequest,
|
|
10
|
+
ProcessClaimResponse,
|
|
11
|
+
PendingClaim,
|
|
12
|
+
PendingClaimByOwner,
|
|
13
|
+
OnChainClaimRequest,
|
|
14
|
+
OnChainClaimResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class IdentityService:
|
|
19
|
+
"""Identity service for agent registration and ownership management"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, client):
|
|
22
|
+
self.client = client
|
|
23
|
+
|
|
24
|
+
async def register(
|
|
25
|
+
self,
|
|
26
|
+
wallet_address: str,
|
|
27
|
+
key_type: str,
|
|
28
|
+
chain: str,
|
|
29
|
+
owner_did: Optional[str] = None,
|
|
30
|
+
) -> RegisterIdentityResponse:
|
|
31
|
+
"""
|
|
32
|
+
Register a new agent identity per LLD §2.2
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
wallet_address: Wallet address
|
|
36
|
+
key_type: Key type ('evm' or 'solana')
|
|
37
|
+
chain: Chain ('base' or 'solana')
|
|
38
|
+
owner_did: Optional owner DID for SDK Auto-Registration (Option A)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
RegisterIdentityResponse
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```python
|
|
45
|
+
# Basic registration
|
|
46
|
+
identity = await client.identity.register(
|
|
47
|
+
wallet_address="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
|
|
48
|
+
key_type="evm",
|
|
49
|
+
chain="base",
|
|
50
|
+
)
|
|
51
|
+
print(f"DID: {identity.did}")
|
|
52
|
+
|
|
53
|
+
# Option A: SDK Auto-Registration with owner
|
|
54
|
+
identity_with_owner = await client.identity.register(
|
|
55
|
+
wallet_address="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
|
|
56
|
+
key_type="evm",
|
|
57
|
+
chain="base",
|
|
58
|
+
owner_did="did:owner:evm:0x123...", # Agent automatically linked
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
"""
|
|
62
|
+
# Validate request
|
|
63
|
+
self._validate_register_request(wallet_address, key_type, chain)
|
|
64
|
+
|
|
65
|
+
# Build request body
|
|
66
|
+
request_body: Dict[str, Any] = {
|
|
67
|
+
"walletAddress": wallet_address,
|
|
68
|
+
"keyType": key_type,
|
|
69
|
+
"chain": chain,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Add optional owner_did for Option A
|
|
73
|
+
if owner_did:
|
|
74
|
+
request_body["ownerDID"] = owner_did
|
|
75
|
+
|
|
76
|
+
# Make API request (no authentication required for registration)
|
|
77
|
+
response = await self.client.request(
|
|
78
|
+
"POST",
|
|
79
|
+
"/v1/identity/register",
|
|
80
|
+
request_body,
|
|
81
|
+
skip_auth=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if not response.success or not response.data:
|
|
85
|
+
raise Exception("Identity registration failed")
|
|
86
|
+
|
|
87
|
+
data = response.data
|
|
88
|
+
return RegisterIdentityResponse(
|
|
89
|
+
did=data["did"],
|
|
90
|
+
wallet_address=data["walletAddress"],
|
|
91
|
+
key_type=data["keyType"],
|
|
92
|
+
chain=data["chain"],
|
|
93
|
+
created_at=data["createdAt"],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
async def submit_claim_request(
|
|
97
|
+
self,
|
|
98
|
+
agent_did: str,
|
|
99
|
+
webhook_url: Optional[str] = None,
|
|
100
|
+
) -> SubmitClaimResponse:
|
|
101
|
+
"""
|
|
102
|
+
Submit ownership claim request (Option B: Async Claim Approval)
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
agent_did: Agent DID to claim
|
|
106
|
+
webhook_url: Optional webhook URL for claim notification
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
SubmitClaimResponse
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
```python
|
|
113
|
+
# Submit a claim request for an agent
|
|
114
|
+
claim = await client.identity.submit_claim_request(
|
|
115
|
+
agent_did="did:agent:evm:0x...",
|
|
116
|
+
webhook_url="https://my-app.com/webhooks/claim-notification",
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
print(f"Claim submitted: {claim.status}") # 'pending'
|
|
120
|
+
print(f"Claim ID: {claim.claim_id}")
|
|
121
|
+
```
|
|
122
|
+
"""
|
|
123
|
+
if not agent_did:
|
|
124
|
+
raise ValueError("agent_did is required")
|
|
125
|
+
|
|
126
|
+
request_body: Dict[str, Any] = {"agentDID": agent_did}
|
|
127
|
+
if webhook_url:
|
|
128
|
+
request_body["webhookUrl"] = webhook_url
|
|
129
|
+
|
|
130
|
+
response = await self.client.request(
|
|
131
|
+
"POST",
|
|
132
|
+
"/v1/ownership/claim-request",
|
|
133
|
+
request_body,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if not response.success or not response.data:
|
|
137
|
+
raise Exception("Failed to submit claim request")
|
|
138
|
+
|
|
139
|
+
data = response.data
|
|
140
|
+
return SubmitClaimResponse(
|
|
141
|
+
claim_id=data["claimId"],
|
|
142
|
+
status=data["status"],
|
|
143
|
+
message=data["message"],
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
async def process_claim_request(
|
|
147
|
+
self,
|
|
148
|
+
owner_did: str,
|
|
149
|
+
approved: bool,
|
|
150
|
+
owner_signature: Optional[str] = None,
|
|
151
|
+
agent_signature: Optional[str] = None,
|
|
152
|
+
message: Optional[str] = None,
|
|
153
|
+
timestamp: Optional[int] = None,
|
|
154
|
+
rejection_reason: Optional[str] = None,
|
|
155
|
+
) -> ProcessClaimResponse:
|
|
156
|
+
"""
|
|
157
|
+
Process ownership claim (approve/reject) (Option B: Async Claim Approval)
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
owner_did: Owner DID
|
|
161
|
+
approved: Whether to approve the claim
|
|
162
|
+
owner_signature: Owner signature (required if approved)
|
|
163
|
+
agent_signature: Agent signature (required if approved)
|
|
164
|
+
message: Optional message
|
|
165
|
+
timestamp: Optional timestamp
|
|
166
|
+
rejection_reason: Rejection reason (if not approved)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
ProcessClaimResponse
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
```python
|
|
173
|
+
# Agent approves a claim
|
|
174
|
+
result = await agent_client.identity.process_claim_request(
|
|
175
|
+
owner_did="did:owner:evm:0x...",
|
|
176
|
+
approved=True,
|
|
177
|
+
owner_signature="0x...",
|
|
178
|
+
agent_signature="0x...",
|
|
179
|
+
message="Ownership claim approval",
|
|
180
|
+
timestamp=int(time.time() * 1000),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
print(f"Claim status: {result.status}") # 'approved'
|
|
184
|
+
|
|
185
|
+
# Agent rejects a claim
|
|
186
|
+
rejected = await agent_client.identity.process_claim_request(
|
|
187
|
+
owner_did="did:owner:evm:0x...",
|
|
188
|
+
approved=False,
|
|
189
|
+
rejection_reason="Invalid claim",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
print(f"Claim status: {rejected.status}") # 'rejected'
|
|
193
|
+
```
|
|
194
|
+
"""
|
|
195
|
+
if not owner_did:
|
|
196
|
+
raise ValueError("owner_did is required")
|
|
197
|
+
|
|
198
|
+
if not isinstance(approved, bool):
|
|
199
|
+
raise ValueError("approved field is required")
|
|
200
|
+
|
|
201
|
+
if approved and (not owner_signature or not agent_signature):
|
|
202
|
+
raise ValueError("Signatures are required when approving a claim")
|
|
203
|
+
|
|
204
|
+
request_body: Dict[str, Any] = {
|
|
205
|
+
"ownerDID": owner_did,
|
|
206
|
+
"approved": approved,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if owner_signature:
|
|
210
|
+
request_body["ownerSignature"] = owner_signature
|
|
211
|
+
if agent_signature:
|
|
212
|
+
request_body["agentSignature"] = agent_signature
|
|
213
|
+
if message:
|
|
214
|
+
request_body["message"] = message
|
|
215
|
+
if timestamp:
|
|
216
|
+
request_body["timestamp"] = timestamp
|
|
217
|
+
if rejection_reason:
|
|
218
|
+
request_body["rejectionReason"] = rejection_reason
|
|
219
|
+
|
|
220
|
+
response = await self.client.request(
|
|
221
|
+
"POST",
|
|
222
|
+
"/v1/ownership/claim-process",
|
|
223
|
+
request_body,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if not response.success or not response.data:
|
|
227
|
+
raise Exception("Failed to process claim request")
|
|
228
|
+
|
|
229
|
+
data = response.data
|
|
230
|
+
return ProcessClaimResponse(
|
|
231
|
+
status=data["status"],
|
|
232
|
+
message=data["message"],
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
async def get_pending_claims_for_agent(self) -> Dict[str, Any]:
|
|
236
|
+
"""
|
|
237
|
+
Get pending claims for the authenticated agent (Option B: Async Claim Approval)
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
Dictionary with 'claims' list and 'count'
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
```python
|
|
244
|
+
# Agent checks pending claims
|
|
245
|
+
pending_claims = await agent_client.identity.get_pending_claims_for_agent()
|
|
246
|
+
|
|
247
|
+
print(f"You have {pending_claims['count']} pending claim(s)")
|
|
248
|
+
|
|
249
|
+
for claim in pending_claims['claims']:
|
|
250
|
+
print(f"Owner: {claim.owner_did}")
|
|
251
|
+
print(f"Requested at: {claim.requested_at}")
|
|
252
|
+
print(f"Webhook: {claim.webhook_url or 'None'}")
|
|
253
|
+
```
|
|
254
|
+
"""
|
|
255
|
+
agent_did = self.client.did
|
|
256
|
+
|
|
257
|
+
response = await self.client.request(
|
|
258
|
+
"GET",
|
|
259
|
+
f"/v1/ownership/pending-claims/{agent_did}",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if not response.success or not response.data:
|
|
263
|
+
raise Exception("Failed to get pending claims")
|
|
264
|
+
|
|
265
|
+
data = response.data
|
|
266
|
+
claims_list = [
|
|
267
|
+
PendingClaim(
|
|
268
|
+
claim_id=claim["claimId"],
|
|
269
|
+
owner_did=claim["ownerDID"],
|
|
270
|
+
owner_wallet=claim["ownerWallet"],
|
|
271
|
+
requested_at=claim["requestedAt"],
|
|
272
|
+
webhook_url=claim.get("webhookUrl"),
|
|
273
|
+
)
|
|
274
|
+
for claim in data.get("claims", [])
|
|
275
|
+
]
|
|
276
|
+
|
|
277
|
+
return {"claims": claims_list, "count": data.get("count", 0)}
|
|
278
|
+
|
|
279
|
+
async def get_pending_claims_by_owner(self) -> Dict[str, Any]:
|
|
280
|
+
"""
|
|
281
|
+
Get pending claims by the authenticated owner (Option B: Async Claim Approval)
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Dictionary with 'claims' list and 'count'
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
```python
|
|
288
|
+
# Owner checks their submitted claims
|
|
289
|
+
my_claims = await owner_client.identity.get_pending_claims_by_owner()
|
|
290
|
+
|
|
291
|
+
print(f"You have submitted {my_claims['count']} claim(s)")
|
|
292
|
+
|
|
293
|
+
for claim in my_claims['claims']:
|
|
294
|
+
print(f"Agent: {claim.agent_did}")
|
|
295
|
+
print(f"Status: {claim.status}")
|
|
296
|
+
print(f"Requested at: {claim.requested_at}")
|
|
297
|
+
```
|
|
298
|
+
"""
|
|
299
|
+
owner_did = self.client.did
|
|
300
|
+
|
|
301
|
+
response = await self.client.request(
|
|
302
|
+
"GET",
|
|
303
|
+
f"/v1/ownership/pending-claims/owner/{owner_did}",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if not response.success or not response.data:
|
|
307
|
+
raise Exception("Failed to get pending claims")
|
|
308
|
+
|
|
309
|
+
data = response.data
|
|
310
|
+
claims_list = [
|
|
311
|
+
PendingClaimByOwner(
|
|
312
|
+
agent_did=claim["agentDID"],
|
|
313
|
+
agent_wallet=claim["agentWallet"],
|
|
314
|
+
requested_at=claim["requestedAt"],
|
|
315
|
+
status=claim["status"],
|
|
316
|
+
)
|
|
317
|
+
for claim in data.get("claims", [])
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
return {"claims": claims_list, "count": data.get("count", 0)}
|
|
321
|
+
|
|
322
|
+
async def claim_on_chain(
|
|
323
|
+
self,
|
|
324
|
+
agent_did: str,
|
|
325
|
+
tx_hash: str,
|
|
326
|
+
chain: str,
|
|
327
|
+
) -> OnChainClaimResponse:
|
|
328
|
+
"""
|
|
329
|
+
Claim agent ownership via on-chain transaction (Option C: On-chain Claiming)
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
agent_did: Agent DID to claim
|
|
333
|
+
tx_hash: Transaction hash (Solana signature or EVM tx hash)
|
|
334
|
+
chain: Chain ('solana' or 'base')
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
OnChainClaimResponse
|
|
338
|
+
|
|
339
|
+
Example:
|
|
340
|
+
```python
|
|
341
|
+
# Claim ownership by providing a Solana transaction hash
|
|
342
|
+
result = await owner_client.identity.claim_on_chain(
|
|
343
|
+
agent_did="did:agent:sol:...",
|
|
344
|
+
tx_hash="5wHu7...", # Solana transaction signature
|
|
345
|
+
chain="solana",
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
print("Ownership claimed via on-chain transaction")
|
|
349
|
+
print(f"Status: {result.status}") # 'approved'
|
|
350
|
+
print(f"Transaction: {result.tx_hash}")
|
|
351
|
+
print(f"Method: {result.method}") # 'onchain-solana'
|
|
352
|
+
|
|
353
|
+
# Claim ownership by providing a Base (EVM) transaction hash
|
|
354
|
+
evm_result = await owner_client.identity.claim_on_chain(
|
|
355
|
+
agent_did="did:agent:evm:0x...",
|
|
356
|
+
tx_hash="0xabc123...", # EVM transaction hash
|
|
357
|
+
chain="base",
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
print("Ownership claimed via Base transaction")
|
|
361
|
+
print(f"Status: {evm_result.status}") # 'approved'
|
|
362
|
+
```
|
|
363
|
+
"""
|
|
364
|
+
if not agent_did:
|
|
365
|
+
raise ValueError("agent_did is required")
|
|
366
|
+
|
|
367
|
+
if not tx_hash:
|
|
368
|
+
raise ValueError("tx_hash is required")
|
|
369
|
+
|
|
370
|
+
if chain not in ["solana", "base"]:
|
|
371
|
+
raise ValueError('chain must be "solana" or "base"')
|
|
372
|
+
|
|
373
|
+
request_body = {
|
|
374
|
+
"agentDID": agent_did,
|
|
375
|
+
"txHash": tx_hash,
|
|
376
|
+
"chain": chain,
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
response = await self.client.request(
|
|
380
|
+
"POST",
|
|
381
|
+
"/v1/ownership/claim-onchain",
|
|
382
|
+
request_body,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if not response.success or not response.data:
|
|
386
|
+
raise Exception("Failed to claim ownership on-chain")
|
|
387
|
+
|
|
388
|
+
data = response.data
|
|
389
|
+
return OnChainClaimResponse(
|
|
390
|
+
status=data["status"],
|
|
391
|
+
tx_hash=data["txHash"],
|
|
392
|
+
method=data["method"],
|
|
393
|
+
message=data["message"],
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def _validate_register_request(
|
|
397
|
+
self, wallet_address: str, key_type: str, chain: str
|
|
398
|
+
):
|
|
399
|
+
"""Validate registration request"""
|
|
400
|
+
if not wallet_address:
|
|
401
|
+
raise ValueError("wallet_address is required")
|
|
402
|
+
|
|
403
|
+
if key_type not in ["evm", "solana"]:
|
|
404
|
+
raise ValueError('key_type must be "evm" or "solana"')
|
|
405
|
+
|
|
406
|
+
if chain not in ["base", "solana"]:
|
|
407
|
+
raise ValueError('chain must be "base" or "solana"')
|
|
408
|
+
|
|
409
|
+
# Validate wallet address format
|
|
410
|
+
if key_type == "evm":
|
|
411
|
+
if not re.match(r"^0x[a-fA-F0-9]{40}$", wallet_address):
|
|
412
|
+
raise ValueError("Invalid EVM wallet address format")
|
|
413
|
+
else:
|
|
414
|
+
if not re.match(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$", wallet_address):
|
|
415
|
+
raise ValueError("Invalid Solana wallet address format")
|