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.
@@ -0,0 +1,401 @@
1
+ """Memory Service - Store, retrieve, delete encrypted memories per LLD §2.4"""
2
+
3
+ import json
4
+ import hashlib
5
+ from typing import Dict, Any, Optional, List
6
+
7
+ import nacl.secret
8
+ import nacl.utils
9
+ import nacl.hash
10
+
11
+ from ..types import (
12
+ StoreMemoryRequest,
13
+ StoreMemoryResponse,
14
+ RetrieveMemoryResponse,
15
+ BatchStoreMemoryRequest,
16
+ BatchStoreMemoryResponse,
17
+ BatchStoreMemoryResult,
18
+ BatchRetrieveMemoryRequest,
19
+ BatchRetrieveMemoryResponse,
20
+ BatchRetrieveMemoryResult,
21
+ )
22
+
23
+
24
+ class MemoryService:
25
+ """Memory service for encrypted data storage"""
26
+
27
+ def __init__(self, client):
28
+ self.client = client
29
+ self._encryption_key: Optional[bytes] = None
30
+
31
+ async def store(
32
+ self,
33
+ data: Dict[str, Any],
34
+ storage_tier: str,
35
+ metadata: Optional[Dict[str, Any]] = None,
36
+ ) -> StoreMemoryResponse:
37
+ """
38
+ Store encrypted memory per LLD §2.4
39
+ Cost: $0.01 (automatic 402 payment)
40
+
41
+ Example:
42
+ ```python
43
+ memory = await client.memory.store(
44
+ data={"key": "value", "nested": {"data": 123}},
45
+ storage_tier="hot",
46
+ metadata={"tags": ["important"]},
47
+ )
48
+ print(f"Memory ID: {memory.memory_id}")
49
+ ```
50
+ """
51
+ # Validate request
52
+ self._validate_store_request(data, storage_tier)
53
+
54
+ # Get encryption key
55
+ key = await self._get_encryption_key()
56
+
57
+ # Encrypt data client-side using PyNaCl
58
+ encrypted_data = self._encrypt_data(data, key)
59
+
60
+ # Make API request with automatic 402 payment
61
+ response = await self.client.request_with_payment(
62
+ "POST",
63
+ "/v1/memory/store",
64
+ {
65
+ "encryptedData": encrypted_data,
66
+ "storageTier": storage_tier,
67
+ "metadata": metadata,
68
+ },
69
+ )
70
+
71
+ if not response.success or not response.data:
72
+ raise Exception("Memory store failed")
73
+
74
+ resp_data = response.data
75
+ return StoreMemoryResponse(
76
+ memory_id=resp_data["memoryId"],
77
+ storage_tier=resp_data["storageTier"],
78
+ size=resp_data["size"],
79
+ receipt_id=resp_data["receiptId"],
80
+ )
81
+
82
+ async def retrieve(self, memory_id: str) -> RetrieveMemoryResponse:
83
+ """
84
+ Retrieve encrypted memory per LLD §2.4
85
+ Cost: $0.005 (automatic 402 payment)
86
+ """
87
+ if not memory_id:
88
+ raise ValueError("memory_id is required")
89
+
90
+ # Make API request with automatic 402 payment
91
+ response = await self.client.request_with_payment(
92
+ "POST",
93
+ "/v1/memory/retrieve",
94
+ {"memoryId": memory_id},
95
+ )
96
+
97
+ if not response.success or not response.data:
98
+ raise Exception("Memory retrieve failed")
99
+
100
+ resp_data = response.data
101
+
102
+ # Get encryption key
103
+ key = await self._get_encryption_key()
104
+
105
+ # Decrypt data client-side
106
+ decrypted_data = self._decrypt_data(resp_data["encryptedData"], key)
107
+
108
+ return RetrieveMemoryResponse(
109
+ memory_id=resp_data["memoryId"],
110
+ data=decrypted_data,
111
+ storage_tier=resp_data["storageTier"],
112
+ metadata=resp_data.get("metadata"),
113
+ receipt_id=resp_data["receiptId"],
114
+ )
115
+
116
+ async def delete(self, memory_id: str) -> Dict[str, Any]:
117
+ """Delete memory per LLD §2.4 (free)"""
118
+ if not memory_id:
119
+ raise ValueError("memory_id is required")
120
+
121
+ response = await self.client.request(
122
+ "DELETE",
123
+ f"/v1/memory/{memory_id}",
124
+ )
125
+
126
+ if not response.success or not response.data:
127
+ raise Exception("Memory delete failed")
128
+
129
+ return response.data
130
+
131
+ async def store_batch(
132
+ self,
133
+ items: List[Dict[str, Any]],
134
+ ) -> BatchStoreMemoryResponse:
135
+ """
136
+ Batch store encrypted memories per PRD FR-010, LLD §2.3
137
+ Max 100 items per batch
138
+ Cost: Single 402 payment for entire batch
139
+
140
+ Example:
141
+ ```python
142
+ result = await client.memory.store_batch([
143
+ {"data": {"key": "value1"}, "storage_tier": "hot"},
144
+ {"data": {"key": "value2"}, "storage_tier": "warm"},
145
+ {"data": {"key": "value3"}, "storage_tier": "cold"},
146
+ ])
147
+
148
+ print(f"Success: {result.success_count}, Failed: {result.failure_count}")
149
+ for r in result.results:
150
+ if r.memory_id:
151
+ print(f"Stored: {r.memory_id}")
152
+ else:
153
+ print(f"Failed: {r.error}")
154
+ ```
155
+ """
156
+ # Validate batch request
157
+ if not isinstance(items, list):
158
+ raise ValueError("items must be a list")
159
+
160
+ if len(items) == 0:
161
+ raise ValueError("items list cannot be empty")
162
+
163
+ if len(items) > 100:
164
+ raise ValueError("batch size exceeds maximum of 100 items")
165
+
166
+ # Validate each item
167
+ for idx, item in enumerate(items):
168
+ try:
169
+ self._validate_store_request(item.get("data"), item.get("storage_tier"))
170
+ except Exception as e:
171
+ raise ValueError(f"Invalid item at index {idx}: {str(e)}")
172
+
173
+ # Get encryption key
174
+ key = await self._get_encryption_key()
175
+
176
+ # Encrypt all items client-side
177
+ encrypted_items = []
178
+ for item in items:
179
+ encrypted_data = self._encrypt_data(item["data"], key)
180
+ encrypted_items.append({
181
+ "encryptedData": encrypted_data,
182
+ "storageTier": item["storage_tier"],
183
+ "metadata": item.get("metadata"),
184
+ })
185
+
186
+ # Make API request with automatic 402 payment
187
+ response = await self.client.request_with_payment(
188
+ "POST",
189
+ "/v1/memory/store/batch",
190
+ {"items": encrypted_items},
191
+ )
192
+
193
+ if not response.success or not response.data:
194
+ raise Exception("Batch memory store failed")
195
+
196
+ resp_data = response.data
197
+
198
+ # Convert results to dataclass instances
199
+ results = [
200
+ BatchStoreMemoryResult(
201
+ index=r["index"],
202
+ memory_id=r.get("memoryId"),
203
+ receipt_id=r.get("receiptId"),
204
+ error=r.get("error"),
205
+ )
206
+ for r in resp_data["results"]
207
+ ]
208
+
209
+ return BatchStoreMemoryResponse(
210
+ results=results,
211
+ success_count=resp_data["successCount"],
212
+ failure_count=resp_data["failureCount"],
213
+ batch_receipt_id=resp_data["batchReceiptId"],
214
+ )
215
+
216
+ async def retrieve_batch(
217
+ self,
218
+ memory_ids: List[str],
219
+ ) -> BatchRetrieveMemoryResponse:
220
+ """
221
+ Batch retrieve encrypted memories per PRD FR-011, LLD §2.3
222
+ Max 100 items per batch
223
+ Cost: Single 402 payment for entire batch
224
+
225
+ Example:
226
+ ```python
227
+ result = await client.memory.retrieve_batch([
228
+ "mem_abc123",
229
+ "mem_def456",
230
+ "mem_ghi789",
231
+ ])
232
+
233
+ print(f"Success: {result.success_count}, Failed: {result.failure_count}")
234
+ for r in result.results:
235
+ if r.data:
236
+ print(f"Retrieved: {r.memory_id}, {r.data}")
237
+ else:
238
+ print(f"Failed: {r.error}")
239
+ ```
240
+ """
241
+ # Validate batch request
242
+ if not isinstance(memory_ids, list):
243
+ raise ValueError("memory_ids must be a list")
244
+
245
+ if len(memory_ids) == 0:
246
+ raise ValueError("memory_ids list cannot be empty")
247
+
248
+ if len(memory_ids) > 100:
249
+ raise ValueError("batch size exceeds maximum of 100 items")
250
+
251
+ # Validate each memoryId
252
+ for idx, memory_id in enumerate(memory_ids):
253
+ if not memory_id or not isinstance(memory_id, str):
254
+ raise ValueError(f"Invalid memory_id at index {idx}")
255
+
256
+ # Make API request with automatic 402 payment
257
+ response = await self.client.request_with_payment(
258
+ "POST",
259
+ "/v1/memory/retrieve/batch",
260
+ {"memoryIds": memory_ids},
261
+ )
262
+
263
+ if not response.success or not response.data:
264
+ raise Exception("Batch memory retrieve failed")
265
+
266
+ resp_data = response.data
267
+
268
+ # Get encryption key
269
+ key = await self._get_encryption_key()
270
+
271
+ # Decrypt all successfully retrieved items
272
+ results = []
273
+ for result in resp_data["results"]:
274
+ # If retrieval failed, return error as-is
275
+ if result.get("error") or not result.get("encryptedData"):
276
+ results.append(
277
+ BatchRetrieveMemoryResult(
278
+ index=result["index"],
279
+ memory_id=result.get("memoryId"),
280
+ error=result.get("error") or "No data returned",
281
+ )
282
+ )
283
+ continue
284
+
285
+ # Decrypt the data
286
+ try:
287
+ decrypted_data = self._decrypt_data(result["encryptedData"], key)
288
+ results.append(
289
+ BatchRetrieveMemoryResult(
290
+ index=result["index"],
291
+ memory_id=result.get("memoryId"),
292
+ data=decrypted_data,
293
+ storage_tier=result.get("storageTier"),
294
+ metadata=result.get("metadata"),
295
+ receipt_id=result.get("receiptId"),
296
+ )
297
+ )
298
+ except Exception as e:
299
+ results.append(
300
+ BatchRetrieveMemoryResult(
301
+ index=result["index"],
302
+ memory_id=result.get("memoryId"),
303
+ error=f"Decryption failed: {str(e)}",
304
+ )
305
+ )
306
+
307
+ return BatchRetrieveMemoryResponse(
308
+ results=results,
309
+ success_count=resp_data["successCount"],
310
+ failure_count=resp_data["failureCount"],
311
+ batch_receipt_id=resp_data["batchReceiptId"],
312
+ )
313
+
314
+ def _validate_store_request(self, data: Dict[str, Any], storage_tier: str):
315
+ """Validate store request"""
316
+ if not isinstance(data, dict):
317
+ raise ValueError("data must be a dictionary")
318
+
319
+ if storage_tier not in ["hot", "warm", "cold"]:
320
+ raise ValueError('storage_tier must be "hot", "warm", or "cold"')
321
+
322
+ # Validate data size
323
+ json_str = json.dumps(data)
324
+ if len(json_str) > 400:
325
+ raise ValueError("data too large (max ~400 characters)")
326
+
327
+ async def _get_encryption_key(self) -> bytes:
328
+ """Get or derive encryption key"""
329
+ if self._encryption_key is None:
330
+ self._encryption_key = self._derive_encryption_key()
331
+ return self._encryption_key
332
+
333
+ def _derive_encryption_key(self) -> bytes:
334
+ """
335
+ Derive encryption key from client configuration using PyNaCl
336
+ Uses BLAKE2b hash function for deterministic key derivation
337
+ """
338
+ key_material = self.client.private_key + self.client.did
339
+ key_material_bytes = key_material.encode('utf-8')
340
+
341
+ # Use BLAKE2b for deterministic key derivation (32 bytes for SecretBox)
342
+ key = nacl.hash.blake2b(
343
+ key_material_bytes,
344
+ digest_size=nacl.secret.SecretBox.KEY_SIZE,
345
+ encoder=nacl.encoding.RawEncoder
346
+ )
347
+
348
+ return key
349
+
350
+ def _encrypt_data(self, data: Dict[str, Any], key: bytes) -> str:
351
+ """
352
+ Encrypt data using PyNaCl SecretBox (XSalsa20-Poly1305)
353
+ """
354
+ import base64
355
+
356
+ # Convert data to JSON bytes
357
+ json_str = json.dumps(data)
358
+ json_bytes = json_str.encode('utf-8')
359
+
360
+ # Create SecretBox with key
361
+ box = nacl.secret.SecretBox(key)
362
+
363
+ # Encrypt (nonce is automatically generated and prepended)
364
+ encrypted = box.encrypt(json_bytes)
365
+
366
+ # Return base64 encoded ciphertext (includes nonce)
367
+ return base64.b64encode(encrypted).decode('utf-8')
368
+
369
+ def _decrypt_data(self, encrypted_data: str, key: bytes) -> Dict[str, Any]:
370
+ """
371
+ Decrypt data using PyNaCl SecretBox
372
+ """
373
+ import base64
374
+
375
+ # Decode from base64
376
+ encrypted_bytes = base64.b64decode(encrypted_data)
377
+
378
+ # Create SecretBox with key
379
+ box = nacl.secret.SecretBox(key)
380
+
381
+ # Decrypt (nonce is automatically extracted from ciphertext)
382
+ decrypted_bytes = box.decrypt(encrypted_bytes)
383
+
384
+ # Convert to JSON
385
+ json_str = decrypted_bytes.decode('utf-8')
386
+ return json.loads(json_str)
387
+
388
+ def set_encryption_key(self, key: bytes):
389
+ """
390
+ Set custom encryption key
391
+ Useful for testing or using external key management
392
+ """
393
+ if len(key) != nacl.secret.SecretBox.KEY_SIZE:
394
+ raise ValueError(f"Key must be {nacl.secret.SecretBox.KEY_SIZE} bytes")
395
+ self._encryption_key = key
396
+
397
+ async def get_current_encryption_key(self) -> bytes:
398
+ """
399
+ Get current encryption key (for backup purposes)
400
+ """
401
+ return await self._get_encryption_key()
@@ -0,0 +1,293 @@
1
+ """
2
+ Owner Service - Owner identity registration and management
3
+ """
4
+
5
+ from typing import List, Optional, Dict
6
+ from dataclasses import dataclass
7
+ from ..types import DID
8
+
9
+
10
+ @dataclass
11
+ class Owner:
12
+ """Owner profile information"""
13
+ owner_did: DID
14
+ wallet_address: str
15
+ wallet_chain: str # 'evm' | 'solana'
16
+ entity_type: str # 'human' | 'organization'
17
+ display_name: Optional[str]
18
+ organization_name: Optional[str]
19
+ email: Optional[str]
20
+ email_verified: bool
21
+ wallet_verified: bool
22
+ created_at: str
23
+ updated_at: str
24
+
25
+
26
+ @dataclass
27
+ class OwnedAgent:
28
+ """Owned agent information"""
29
+ agent_did: DID
30
+ wallet_address: str
31
+ chain: str
32
+ claim_type: str # 'individual' | 'workspace'
33
+ workspace_id: Optional[str]
34
+ workspace_name: Optional[str]
35
+ role: Optional[str] # 'admin' | 'member'
36
+ added_at: str
37
+
38
+
39
+ class OwnerService:
40
+ """
41
+ Owner service for identity registration and management
42
+
43
+ Register and manage owner identities for enterprise fleet management.
44
+ """
45
+
46
+ def __init__(self, client):
47
+ self.client = client
48
+
49
+ async def register(
50
+ self,
51
+ wallet_address: str,
52
+ wallet_chain: str,
53
+ entity_type: str,
54
+ display_name: Optional[str] = None,
55
+ organization_name: Optional[str] = None,
56
+ email: Optional[str] = None,
57
+ supabase_user_id: Optional[str] = None,
58
+ ) -> Owner:
59
+ """
60
+ Register a new owner (human or organization)
61
+
62
+ Args:
63
+ wallet_address: Wallet address
64
+ wallet_chain: Wallet chain ('evm' or 'solana')
65
+ entity_type: Entity type ('human' or 'organization')
66
+ display_name: Optional display name
67
+ organization_name: Optional organization name
68
+ email: Optional email address
69
+ supabase_user_id: Optional Supabase user ID
70
+
71
+ Returns:
72
+ Registered owner
73
+
74
+ Example:
75
+ ```python
76
+ owner = await client.owner.register(
77
+ wallet_address="0x1234...",
78
+ wallet_chain="evm",
79
+ entity_type="human",
80
+ display_name="John Doe",
81
+ email="john@example.com"
82
+ )
83
+ print(f"Registered: {owner.owner_did}")
84
+ ```
85
+ """
86
+ body = {
87
+ "walletAddress": wallet_address,
88
+ "walletChain": wallet_chain,
89
+ "entityType": entity_type,
90
+ }
91
+
92
+ if display_name:
93
+ body["displayName"] = display_name
94
+ if organization_name:
95
+ body["organizationName"] = organization_name
96
+ if email:
97
+ body["email"] = email
98
+ if supabase_user_id:
99
+ body["supabaseUserId"] = supabase_user_id
100
+
101
+ response = await self.client.request(
102
+ "POST", "/v1/owners/register", body, skip_auth=True
103
+ )
104
+
105
+ if not response.success or not response.data:
106
+ raise Exception(
107
+ response.error.get("message", "Failed to register owner")
108
+ if response.error
109
+ else "Failed to register owner"
110
+ )
111
+
112
+ return self._parse_owner(response.data.get("owner", response.data))
113
+
114
+ async def verify_wallet(
115
+ self,
116
+ owner_did: DID,
117
+ message: str,
118
+ signature: str,
119
+ timestamp: int,
120
+ ) -> bool:
121
+ """
122
+ Verify wallet ownership with cryptographic signature
123
+
124
+ Args:
125
+ owner_did: Owner DID
126
+ message: Message that was signed
127
+ signature: Signature
128
+ timestamp: Timestamp of signature
129
+
130
+ Returns:
131
+ True if verified
132
+
133
+ Example:
134
+ ```python
135
+ verified = await client.owner.verify_wallet(
136
+ owner_did="did:owner:evm:0x1234...",
137
+ message="Verify wallet ownership",
138
+ signature="0x...",
139
+ timestamp=1699900000
140
+ )
141
+ print(f"Verified: {verified}")
142
+ ```
143
+ """
144
+ body = {
145
+ "ownerDID": owner_did,
146
+ "message": message,
147
+ "signature": signature,
148
+ "timestamp": timestamp,
149
+ }
150
+
151
+ response = await self.client.request(
152
+ "POST", "/v1/owners/verify-wallet", body, skip_auth=True
153
+ )
154
+
155
+ if not response.success or response.data is None:
156
+ raise Exception(
157
+ response.error.get("message", "Failed to verify wallet")
158
+ if response.error
159
+ else "Failed to verify wallet"
160
+ )
161
+
162
+ return response.data.get("verified", False)
163
+
164
+ async def get_profile(self) -> Optional[Owner]:
165
+ """
166
+ Get the authenticated owner's profile
167
+
168
+ Returns:
169
+ Owner profile or None if not found
170
+
171
+ Example:
172
+ ```python
173
+ profile = await client.owner.get_profile()
174
+ if profile:
175
+ print(f"Display name: {profile.display_name}")
176
+ ```
177
+ """
178
+ response = await self.client.request("GET", "/v1/owners/me")
179
+
180
+ if not response.success:
181
+ raise Exception(
182
+ response.error.get("message", "Failed to get profile")
183
+ if response.error
184
+ else "Failed to get profile"
185
+ )
186
+
187
+ if not response.data or not response.data.get("owner"):
188
+ return None
189
+
190
+ return self._parse_owner(response.data["owner"])
191
+
192
+ async def update_profile(
193
+ self,
194
+ display_name: Optional[str] = None,
195
+ organization_name: Optional[str] = None,
196
+ email: Optional[str] = None,
197
+ ) -> Owner:
198
+ """
199
+ Update the authenticated owner's profile
200
+
201
+ Args:
202
+ display_name: Optional new display name
203
+ organization_name: Optional new organization name
204
+ email: Optional new email
205
+
206
+ Returns:
207
+ Updated owner profile
208
+
209
+ Example:
210
+ ```python
211
+ updated = await client.owner.update_profile(
212
+ display_name="Jane Doe",
213
+ organization_name="Acme Corp"
214
+ )
215
+ print(f"Updated: {updated.display_name}")
216
+ ```
217
+ """
218
+ body = {}
219
+ if display_name is not None:
220
+ body["displayName"] = display_name
221
+ if organization_name is not None:
222
+ body["organizationName"] = organization_name
223
+ if email is not None:
224
+ body["email"] = email
225
+
226
+ response = await self.client.request("PUT", "/v1/owners/me", body)
227
+
228
+ if not response.success or not response.data:
229
+ raise Exception(
230
+ response.error.get("message", "Failed to update profile")
231
+ if response.error
232
+ else "Failed to update profile"
233
+ )
234
+
235
+ return self._parse_owner(response.data.get("owner", response.data))
236
+
237
+ async def get_owned_agents(self) -> Dict:
238
+ """
239
+ Get all agents owned by the authenticated owner
240
+
241
+ Returns:
242
+ Dictionary with agents and count
243
+
244
+ Example:
245
+ ```python
246
+ result = await client.owner.get_owned_agents()
247
+ print(f"Total owned agents: {result['count']}")
248
+
249
+ for agent in result['agents']:
250
+ print(f"{agent.agent_did} - {agent.claim_type}")
251
+ ```
252
+ """
253
+ response = await self.client.request("GET", "/v1/owners/me/agents")
254
+
255
+ if not response.success or not response.data:
256
+ raise Exception(
257
+ response.error.get("message", "Failed to get owned agents")
258
+ if response.error
259
+ else "Failed to get owned agents"
260
+ )
261
+
262
+ return {
263
+ "agents": [
264
+ OwnedAgent(
265
+ agent_did=a["agentDID"],
266
+ wallet_address=a["walletAddress"],
267
+ chain=a["chain"],
268
+ claim_type=a["claimType"],
269
+ workspace_id=a.get("workspaceId"),
270
+ workspace_name=a.get("workspaceName"),
271
+ role=a.get("role"),
272
+ added_at=a["addedAt"],
273
+ )
274
+ for a in response.data.get("agents", [])
275
+ ],
276
+ "count": response.data.get("count", 0),
277
+ }
278
+
279
+ def _parse_owner(self, data: dict) -> Owner:
280
+ """Parse owner data into Owner object"""
281
+ return Owner(
282
+ owner_did=data["ownerDID"],
283
+ wallet_address=data["walletAddress"],
284
+ wallet_chain=data["walletChain"],
285
+ entity_type=data["entityType"],
286
+ display_name=data.get("displayName"),
287
+ organization_name=data.get("organizationName"),
288
+ email=data.get("email"),
289
+ email_verified=data.get("emailVerified", False),
290
+ wallet_verified=data.get("walletVerified", False),
291
+ created_at=data["createdAt"],
292
+ updated_at=data["updatedAt"],
293
+ )