agent0-sdk 1.4.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.
- agent0_sdk/__init__.py +57 -0
- agent0_sdk/core/agent.py +1187 -0
- agent0_sdk/core/contracts.py +547 -0
- agent0_sdk/core/endpoint_crawler.py +330 -0
- agent0_sdk/core/feedback_manager.py +1052 -0
- agent0_sdk/core/indexer.py +1837 -0
- agent0_sdk/core/ipfs_client.py +357 -0
- agent0_sdk/core/models.py +303 -0
- agent0_sdk/core/oasf_validator.py +98 -0
- agent0_sdk/core/sdk.py +1005 -0
- agent0_sdk/core/subgraph_client.py +853 -0
- agent0_sdk/core/transaction_handle.py +71 -0
- agent0_sdk/core/value_encoding.py +91 -0
- agent0_sdk/core/web3_client.py +399 -0
- agent0_sdk/taxonomies/all_domains.json +1565 -0
- agent0_sdk/taxonomies/all_skills.json +1030 -0
- agent0_sdk-1.4.0.dist-info/METADATA +403 -0
- agent0_sdk-1.4.0.dist-info/RECORD +21 -0
- agent0_sdk-1.4.0.dist-info/WHEEL +5 -0
- agent0_sdk-1.4.0.dist-info/licenses/LICENSE +22 -0
- agent0_sdk-1.4.0.dist-info/top_level.txt +1 -0
agent0_sdk/core/agent.py
ADDED
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent class for managing individual agents.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
from .models import (
|
|
14
|
+
AgentId, Address, URI, Timestamp, IdemKey,
|
|
15
|
+
EndpointType, TrustModel, Endpoint, RegistrationFile
|
|
16
|
+
)
|
|
17
|
+
from .web3_client import Web3Client
|
|
18
|
+
from .endpoint_crawler import EndpointCrawler
|
|
19
|
+
from .oasf_validator import validate_skill, validate_domain
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .sdk import SDK
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
from .transaction_handle import TransactionHandle
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Agent:
|
|
30
|
+
"""Represents an individual agent with its registration data."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, sdk: "SDK", registration_file: RegistrationFile):
|
|
33
|
+
"""Initialize agent with SDK and registration file."""
|
|
34
|
+
self.sdk = sdk
|
|
35
|
+
self.registration_file = registration_file
|
|
36
|
+
# Track which metadata has changed since last registration to avoid sending unchanged data
|
|
37
|
+
self._dirty_metadata = set()
|
|
38
|
+
self._last_registered_wallet = None
|
|
39
|
+
self._last_registered_ens = None
|
|
40
|
+
# Initialize endpoint crawler for fetching capabilities
|
|
41
|
+
self._endpoint_crawler = EndpointCrawler(timeout=5)
|
|
42
|
+
|
|
43
|
+
# Read-only properties for direct access
|
|
44
|
+
@property
|
|
45
|
+
def agentId(self) -> Optional[AgentId]:
|
|
46
|
+
"""Get agent ID (read-only)."""
|
|
47
|
+
return self.registration_file.agentId
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def agentURI(self) -> Optional[URI]:
|
|
51
|
+
"""Get agent URI (read-only)."""
|
|
52
|
+
return self.registration_file.agentURI
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def name(self) -> str:
|
|
56
|
+
"""Get agent name (read-only)."""
|
|
57
|
+
return self.registration_file.name
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def description(self) -> str:
|
|
61
|
+
"""Get agent description (read-only)."""
|
|
62
|
+
return self.registration_file.description
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def image(self) -> Optional[URI]:
|
|
66
|
+
"""Get agent image URI (read-only)."""
|
|
67
|
+
return self.registration_file.image
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def active(self) -> bool:
|
|
71
|
+
"""Get agent active status (read-only)."""
|
|
72
|
+
return self.registration_file.active
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def x402support(self) -> bool:
|
|
76
|
+
"""Get agent x402 support status (read-only)."""
|
|
77
|
+
return self.registration_file.x402support
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def walletAddress(self) -> Optional[Address]:
|
|
81
|
+
"""Get agent wallet address (read-only)."""
|
|
82
|
+
return self.registration_file.walletAddress
|
|
83
|
+
|
|
84
|
+
def getWallet(self) -> Optional[Address]:
|
|
85
|
+
"""Read the verified agent wallet from the Identity Registry (on-chain).
|
|
86
|
+
|
|
87
|
+
This calls the contract function `getAgentWallet(agentId)` and returns:
|
|
88
|
+
- the wallet address if set and non-zero
|
|
89
|
+
- None if unset/cleared (zero address)
|
|
90
|
+
"""
|
|
91
|
+
if not self.agentId:
|
|
92
|
+
raise ValueError("Agent must be registered before reading wallet from chain.")
|
|
93
|
+
|
|
94
|
+
agent_id_int = int(self.agentId.split(":")[-1]) if ":" in self.agentId else int(self.agentId)
|
|
95
|
+
wallet = self.sdk.web3_client.call_contract(self.sdk.identity_registry, "getAgentWallet", agent_id_int)
|
|
96
|
+
|
|
97
|
+
if not wallet or not isinstance(wallet, str):
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
if wallet.lower() == "0x0000000000000000000000000000000000000000":
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
return wallet
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def walletChainId(self) -> Optional[int]:
|
|
107
|
+
"""Get agent wallet chain ID (read-only)."""
|
|
108
|
+
return self.registration_file.walletChainId
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def endpoints(self) -> List[Endpoint]:
|
|
112
|
+
"""Get agent endpoints list (read-only - use setter methods to modify)."""
|
|
113
|
+
return self.registration_file.endpoints
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def trustModels(self) -> List[Union[TrustModel, str]]:
|
|
117
|
+
"""Get agent trust models list (read-only - use setter methods to modify)."""
|
|
118
|
+
return self.registration_file.trustModels
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def metadata(self) -> Dict[str, Any]:
|
|
122
|
+
"""Get agent metadata dict (read-only - use setter methods to modify)."""
|
|
123
|
+
return self.registration_file.metadata
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def updatedAt(self) -> Timestamp:
|
|
127
|
+
"""Get last update timestamp (read-only)."""
|
|
128
|
+
return self.registration_file.updatedAt
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def owners(self) -> List[Address]:
|
|
132
|
+
"""Get agent owners list (read-only)."""
|
|
133
|
+
return self.registration_file.owners
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def operators(self) -> List[Address]:
|
|
137
|
+
"""Get agent operators list (read-only)."""
|
|
138
|
+
return self.registration_file.operators
|
|
139
|
+
|
|
140
|
+
# Derived endpoint properties (convenience)
|
|
141
|
+
@property
|
|
142
|
+
def mcpEndpoint(self) -> Optional[str]:
|
|
143
|
+
"""Get MCP endpoint value (read-only)."""
|
|
144
|
+
for endpoint in self.registration_file.endpoints:
|
|
145
|
+
if endpoint.type == EndpointType.MCP:
|
|
146
|
+
return endpoint.value
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def a2aEndpoint(self) -> Optional[str]:
|
|
151
|
+
"""Get A2A endpoint value (read-only)."""
|
|
152
|
+
for endpoint in self.registration_file.endpoints:
|
|
153
|
+
if endpoint.type == EndpointType.A2A:
|
|
154
|
+
return endpoint.value
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def ensEndpoint(self) -> Optional[str]:
|
|
159
|
+
"""Get ENS endpoint value (read-only)."""
|
|
160
|
+
for endpoint in self.registration_file.endpoints:
|
|
161
|
+
if endpoint.type == EndpointType.ENS:
|
|
162
|
+
return endpoint.value
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def mcpTools(self) -> Optional[List[str]]:
|
|
167
|
+
"""Get MCP tools list (read-only)."""
|
|
168
|
+
for endpoint in self.registration_file.endpoints:
|
|
169
|
+
if endpoint.type == EndpointType.MCP:
|
|
170
|
+
return endpoint.meta.get('mcpTools')
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def mcpPrompts(self) -> Optional[List[str]]:
|
|
175
|
+
"""Get MCP prompts list (read-only)."""
|
|
176
|
+
for endpoint in self.registration_file.endpoints:
|
|
177
|
+
if endpoint.type == EndpointType.MCP:
|
|
178
|
+
return endpoint.meta.get('mcpPrompts')
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def mcpResources(self) -> Optional[List[str]]:
|
|
183
|
+
"""Get MCP resources list (read-only)."""
|
|
184
|
+
for endpoint in self.registration_file.endpoints:
|
|
185
|
+
if endpoint.type == EndpointType.MCP:
|
|
186
|
+
return endpoint.meta.get('mcpResources')
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def a2aSkills(self) -> Optional[List[str]]:
|
|
191
|
+
"""Get A2A skills list (read-only)."""
|
|
192
|
+
for endpoint in self.registration_file.endpoints:
|
|
193
|
+
if endpoint.type == EndpointType.A2A:
|
|
194
|
+
return endpoint.meta.get('a2aSkills')
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
def registrationFile(self) -> RegistrationFile:
|
|
198
|
+
"""Get the compiled registration file."""
|
|
199
|
+
return self.registration_file
|
|
200
|
+
|
|
201
|
+
def _collectMetadataForRegistration(self) -> List[Dict[str, Any]]:
|
|
202
|
+
"""Collect all metadata entries for registration.
|
|
203
|
+
|
|
204
|
+
Note: agentWallet is now a reserved metadata key and cannot be set via setMetadata().
|
|
205
|
+
It must be set separately using setWallet() with signature verification.
|
|
206
|
+
"""
|
|
207
|
+
metadata_entries = []
|
|
208
|
+
|
|
209
|
+
# Note: agentWallet is no longer set via metadata - it's now reserved and managed via setWallet()
|
|
210
|
+
|
|
211
|
+
# Add ENS name metadata
|
|
212
|
+
if self.ensEndpoint:
|
|
213
|
+
name_bytes = self.ensEndpoint.encode('utf-8')
|
|
214
|
+
metadata_entries.append({
|
|
215
|
+
"key": "agentName",
|
|
216
|
+
"value": name_bytes
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
# Add custom metadata
|
|
220
|
+
for key, value in self.metadata.items():
|
|
221
|
+
if isinstance(value, str):
|
|
222
|
+
value_bytes = value.encode('utf-8')
|
|
223
|
+
elif isinstance(value, (int, float)):
|
|
224
|
+
value_bytes = str(value).encode('utf-8')
|
|
225
|
+
else:
|
|
226
|
+
value_bytes = str(value).encode('utf-8')
|
|
227
|
+
|
|
228
|
+
metadata_entries.append({
|
|
229
|
+
"key": key,
|
|
230
|
+
"value": value_bytes
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
return metadata_entries
|
|
234
|
+
|
|
235
|
+
# Endpoint management
|
|
236
|
+
def setMCP(self, endpoint: str, version: str = "2025-06-18", auto_fetch: bool = True) -> 'Agent':
|
|
237
|
+
"""
|
|
238
|
+
Set MCP endpoint with version.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
endpoint: MCP endpoint URL
|
|
242
|
+
version: MCP version
|
|
243
|
+
auto_fetch: If True, automatically fetch capabilities from the endpoint (default: True)
|
|
244
|
+
"""
|
|
245
|
+
# Remove existing MCP endpoint if any
|
|
246
|
+
self.registration_file.endpoints = [
|
|
247
|
+
ep for ep in self.registration_file.endpoints
|
|
248
|
+
if ep.type != EndpointType.MCP
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
# Try to fetch capabilities from the endpoint (soft fail)
|
|
252
|
+
meta = {"version": version}
|
|
253
|
+
if auto_fetch:
|
|
254
|
+
try:
|
|
255
|
+
capabilities = self._endpoint_crawler.fetch_mcp_capabilities(endpoint)
|
|
256
|
+
if capabilities:
|
|
257
|
+
meta.update(capabilities)
|
|
258
|
+
logger.debug(
|
|
259
|
+
f"Fetched MCP capabilities: {len(capabilities.get('mcpTools', []))} tools, "
|
|
260
|
+
f"{len(capabilities.get('mcpPrompts', []))} prompts, "
|
|
261
|
+
f"{len(capabilities.get('mcpResources', []))} resources"
|
|
262
|
+
)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
# Soft fail - continue without capabilities
|
|
265
|
+
logger.debug(f"Could not fetch MCP capabilities (non-blocking): {e}")
|
|
266
|
+
|
|
267
|
+
# Add new MCP endpoint
|
|
268
|
+
mcp_endpoint = Endpoint(
|
|
269
|
+
type=EndpointType.MCP,
|
|
270
|
+
value=endpoint,
|
|
271
|
+
meta=meta
|
|
272
|
+
)
|
|
273
|
+
self.registration_file.endpoints.append(mcp_endpoint)
|
|
274
|
+
self.registration_file.updatedAt = int(time.time())
|
|
275
|
+
return self
|
|
276
|
+
|
|
277
|
+
def setA2A(self, agentcard: str, version: str = "0.30", auto_fetch: bool = True) -> 'Agent':
|
|
278
|
+
"""
|
|
279
|
+
Set A2A endpoint with version.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
agentcard: A2A endpoint URL
|
|
283
|
+
version: A2A version
|
|
284
|
+
auto_fetch: If True, automatically fetch skills from the endpoint (default: True)
|
|
285
|
+
"""
|
|
286
|
+
# Remove existing A2A endpoint if any
|
|
287
|
+
self.registration_file.endpoints = [
|
|
288
|
+
ep for ep in self.registration_file.endpoints
|
|
289
|
+
if ep.type != EndpointType.A2A
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
# Try to fetch capabilities from the endpoint (soft fail)
|
|
293
|
+
meta = {"version": version}
|
|
294
|
+
if auto_fetch:
|
|
295
|
+
try:
|
|
296
|
+
capabilities = self._endpoint_crawler.fetch_a2a_capabilities(agentcard)
|
|
297
|
+
if capabilities:
|
|
298
|
+
meta.update(capabilities)
|
|
299
|
+
skills_count = len(capabilities.get('a2aSkills', []))
|
|
300
|
+
logger.debug(f"Fetched A2A capabilities: {skills_count} skills")
|
|
301
|
+
except Exception as e:
|
|
302
|
+
# Soft fail - continue without capabilities
|
|
303
|
+
logger.debug(f"Could not fetch A2A capabilities (non-blocking): {e}")
|
|
304
|
+
|
|
305
|
+
# Add new A2A endpoint
|
|
306
|
+
a2a_endpoint = Endpoint(
|
|
307
|
+
type=EndpointType.A2A,
|
|
308
|
+
value=agentcard,
|
|
309
|
+
meta=meta
|
|
310
|
+
)
|
|
311
|
+
self.registration_file.endpoints.append(a2a_endpoint)
|
|
312
|
+
self.registration_file.updatedAt = int(time.time())
|
|
313
|
+
return self
|
|
314
|
+
|
|
315
|
+
def removeEndpoint(
|
|
316
|
+
self,
|
|
317
|
+
type: Optional[EndpointType] = None,
|
|
318
|
+
value: Optional[str] = None
|
|
319
|
+
) -> 'Agent':
|
|
320
|
+
"""Remove endpoint(s) with wildcard semantics."""
|
|
321
|
+
if type is None and value is None:
|
|
322
|
+
# Remove all endpoints
|
|
323
|
+
self.registration_file.endpoints.clear()
|
|
324
|
+
else:
|
|
325
|
+
# Remove matching endpoints
|
|
326
|
+
self.registration_file.endpoints = [
|
|
327
|
+
ep for ep in self.registration_file.endpoints
|
|
328
|
+
if not (
|
|
329
|
+
(type is None or ep.type == type) and
|
|
330
|
+
(value is None or ep.value == value)
|
|
331
|
+
)
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
self.registration_file.updatedAt = int(time.time())
|
|
335
|
+
return self
|
|
336
|
+
|
|
337
|
+
def removeEndpoints(self) -> 'Agent':
|
|
338
|
+
"""Remove all endpoints."""
|
|
339
|
+
return self.removeEndpoint()
|
|
340
|
+
|
|
341
|
+
# OASF endpoint management
|
|
342
|
+
def _get_or_create_oasf_endpoint(self) -> Endpoint:
|
|
343
|
+
"""Get existing OASF endpoint or create a new one with default values."""
|
|
344
|
+
# Find existing OASF endpoint
|
|
345
|
+
for ep in self.registration_file.endpoints:
|
|
346
|
+
if ep.type == EndpointType.OASF:
|
|
347
|
+
return ep
|
|
348
|
+
|
|
349
|
+
# Create new OASF endpoint with default values
|
|
350
|
+
oasf_endpoint = Endpoint(
|
|
351
|
+
type=EndpointType.OASF,
|
|
352
|
+
value="https://github.com/agntcy/oasf/",
|
|
353
|
+
# Version string follows ERC-8004 spec example ("0.8")
|
|
354
|
+
meta={"version": "0.8", "skills": [], "domains": []}
|
|
355
|
+
)
|
|
356
|
+
self.registration_file.endpoints.append(oasf_endpoint)
|
|
357
|
+
return oasf_endpoint
|
|
358
|
+
|
|
359
|
+
def addSkill(self, slug: str, validate_oasf: bool = False) -> 'Agent':
|
|
360
|
+
"""
|
|
361
|
+
Add a skill to the OASF endpoint.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
slug: The skill slug to add (e.g., "natural_language_processing/natural_language_generation/summarization")
|
|
365
|
+
validate_oasf: If True, validate the slug against the OASF taxonomy (default: False)
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
self for method chaining
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
ValueError: If validate_oasf=True and the slug is not valid
|
|
372
|
+
"""
|
|
373
|
+
if validate_oasf:
|
|
374
|
+
if not validate_skill(slug):
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"Invalid OASF skill slug: {slug}. "
|
|
377
|
+
"Use validate_oasf=False to skip validation."
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
oasf_endpoint = self._get_or_create_oasf_endpoint()
|
|
381
|
+
|
|
382
|
+
# Initialize skills array if missing
|
|
383
|
+
if "skills" not in oasf_endpoint.meta:
|
|
384
|
+
oasf_endpoint.meta["skills"] = []
|
|
385
|
+
|
|
386
|
+
# Add slug if not already present (avoid duplicates)
|
|
387
|
+
skills = oasf_endpoint.meta["skills"]
|
|
388
|
+
if slug not in skills:
|
|
389
|
+
skills.append(slug)
|
|
390
|
+
|
|
391
|
+
self.registration_file.updatedAt = int(time.time())
|
|
392
|
+
return self
|
|
393
|
+
|
|
394
|
+
def removeSkill(self, slug: str) -> 'Agent':
|
|
395
|
+
"""
|
|
396
|
+
Remove a skill from the OASF endpoint.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
slug: The skill slug to remove
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
self for method chaining
|
|
403
|
+
"""
|
|
404
|
+
# Find OASF endpoint
|
|
405
|
+
for ep in self.registration_file.endpoints:
|
|
406
|
+
if ep.type == EndpointType.OASF:
|
|
407
|
+
if "skills" in ep.meta and isinstance(ep.meta["skills"], list):
|
|
408
|
+
skills = ep.meta["skills"]
|
|
409
|
+
if slug in skills:
|
|
410
|
+
skills.remove(slug)
|
|
411
|
+
self.registration_file.updatedAt = int(time.time())
|
|
412
|
+
break
|
|
413
|
+
|
|
414
|
+
return self
|
|
415
|
+
|
|
416
|
+
def addDomain(self, slug: str, validate_oasf: bool = False) -> 'Agent':
|
|
417
|
+
"""
|
|
418
|
+
Add a domain to the OASF endpoint.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
slug: The domain slug to add (e.g., "finance_and_business/investment_services")
|
|
422
|
+
validate_oasf: If True, validate the slug against the OASF taxonomy (default: False)
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
self for method chaining
|
|
426
|
+
|
|
427
|
+
Raises:
|
|
428
|
+
ValueError: If validate_oasf=True and the slug is not valid
|
|
429
|
+
"""
|
|
430
|
+
if validate_oasf:
|
|
431
|
+
if not validate_domain(slug):
|
|
432
|
+
raise ValueError(
|
|
433
|
+
f"Invalid OASF domain slug: {slug}. "
|
|
434
|
+
"Use validate_oasf=False to skip validation."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
oasf_endpoint = self._get_or_create_oasf_endpoint()
|
|
438
|
+
|
|
439
|
+
# Initialize domains array if missing
|
|
440
|
+
if "domains" not in oasf_endpoint.meta:
|
|
441
|
+
oasf_endpoint.meta["domains"] = []
|
|
442
|
+
|
|
443
|
+
# Add slug if not already present (avoid duplicates)
|
|
444
|
+
domains = oasf_endpoint.meta["domains"]
|
|
445
|
+
if slug not in domains:
|
|
446
|
+
domains.append(slug)
|
|
447
|
+
|
|
448
|
+
self.registration_file.updatedAt = int(time.time())
|
|
449
|
+
return self
|
|
450
|
+
|
|
451
|
+
def removeDomain(self, slug: str) -> 'Agent':
|
|
452
|
+
"""
|
|
453
|
+
Remove a domain from the OASF endpoint.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
slug: The domain slug to remove
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
self for method chaining
|
|
460
|
+
"""
|
|
461
|
+
# Find OASF endpoint
|
|
462
|
+
for ep in self.registration_file.endpoints:
|
|
463
|
+
if ep.type == EndpointType.OASF:
|
|
464
|
+
if "domains" in ep.meta and isinstance(ep.meta["domains"], list):
|
|
465
|
+
domains = ep.meta["domains"]
|
|
466
|
+
if slug in domains:
|
|
467
|
+
domains.remove(slug)
|
|
468
|
+
self.registration_file.updatedAt = int(time.time())
|
|
469
|
+
break
|
|
470
|
+
|
|
471
|
+
return self
|
|
472
|
+
|
|
473
|
+
# Trust models
|
|
474
|
+
def setTrust(
|
|
475
|
+
self,
|
|
476
|
+
reputation: bool = False,
|
|
477
|
+
cryptoEconomic: bool = False,
|
|
478
|
+
teeAttestation: bool = False
|
|
479
|
+
) -> 'Agent':
|
|
480
|
+
"""Set trust models using keyword arguments."""
|
|
481
|
+
trust_models = []
|
|
482
|
+
if reputation:
|
|
483
|
+
trust_models.append(TrustModel.REPUTATION)
|
|
484
|
+
if cryptoEconomic:
|
|
485
|
+
trust_models.append(TrustModel.CRYPTO_ECONOMIC)
|
|
486
|
+
if teeAttestation:
|
|
487
|
+
trust_models.append(TrustModel.TEE_ATTESTATION)
|
|
488
|
+
|
|
489
|
+
self.registration_file.trustModels = trust_models
|
|
490
|
+
self.registration_file.updatedAt = int(time.time())
|
|
491
|
+
return self
|
|
492
|
+
|
|
493
|
+
def trustModels(self, models: List[Union[TrustModel, str]]) -> 'Agent':
|
|
494
|
+
"""Set trust models (replace set)."""
|
|
495
|
+
self.registration_file.trustModels = models
|
|
496
|
+
self.registration_file.updatedAt = int(time.time())
|
|
497
|
+
return self
|
|
498
|
+
|
|
499
|
+
# Basic info
|
|
500
|
+
def updateInfo(
|
|
501
|
+
self,
|
|
502
|
+
name: Optional[str] = None,
|
|
503
|
+
description: Optional[str] = None,
|
|
504
|
+
image: Optional[URI] = None
|
|
505
|
+
) -> 'Agent':
|
|
506
|
+
"""Update basic agent information."""
|
|
507
|
+
if name is not None:
|
|
508
|
+
self.registration_file.name = name
|
|
509
|
+
if description is not None:
|
|
510
|
+
self.registration_file.description = description
|
|
511
|
+
if image is not None:
|
|
512
|
+
self.registration_file.image = image
|
|
513
|
+
|
|
514
|
+
self.registration_file.updatedAt = int(time.time())
|
|
515
|
+
return self
|
|
516
|
+
|
|
517
|
+
def setWallet(
|
|
518
|
+
self,
|
|
519
|
+
new_wallet: Address,
|
|
520
|
+
chainId: Optional[int] = None,
|
|
521
|
+
*,
|
|
522
|
+
new_wallet_signer: Optional[Union[str, Any]] = None,
|
|
523
|
+
deadline: Optional[int] = None,
|
|
524
|
+
signature: Optional[bytes] = None,
|
|
525
|
+
) -> Optional[TransactionHandle["Agent"]]:
|
|
526
|
+
"""Set agent wallet address on-chain (verified agentWallet).
|
|
527
|
+
|
|
528
|
+
This method is **on-chain only**. The `agentWallet` is a verified attribute.
|
|
529
|
+
|
|
530
|
+
EOAs: provide `new_wallet_signer` (private key string or eth-account account) OR ensure the SDK
|
|
531
|
+
signer address matches `new_wallet` so the SDK can auto-sign.\n
|
|
532
|
+
Contract wallets (ERC-1271): provide `signature` bytes produced by the wallet’s signing mechanism.
|
|
533
|
+
The SDK will build the correct EIP-712 typed data internally, but cannot produce the wallet signature.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
new_wallet: New wallet address (must be controlled by the signer that produces the signature)
|
|
537
|
+
chainId: Optional local bookkeeping for registration file (walletChainId). Defaults to agent chain.
|
|
538
|
+
new_wallet_signer: EOA signer used to sign the EIP-712 message (private key string or eth-account account)
|
|
539
|
+
deadline: Signature deadline timestamp. Defaults to now+60s (must be <= now+5min per contract).
|
|
540
|
+
signature: Raw signature bytes (intended for ERC-1271 / external signing only)
|
|
541
|
+
"""
|
|
542
|
+
# This API is only meaningful for already-registered agents.
|
|
543
|
+
if not self.agentId:
|
|
544
|
+
raise ValueError(
|
|
545
|
+
"Cannot set agent wallet before the agent is registered on-chain. "
|
|
546
|
+
"Call agent.register(...) / agent.registerIPFS() first to obtain agentId."
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
addr = new_wallet
|
|
550
|
+
|
|
551
|
+
if not addr:
|
|
552
|
+
raise ValueError("Wallet address cannot be empty. Use a non-zero address.")
|
|
553
|
+
|
|
554
|
+
# Validate address format
|
|
555
|
+
if not addr.startswith("0x") or len(addr) != 42:
|
|
556
|
+
raise ValueError(f"Invalid Ethereum address format: {addr}. Must be 42 characters starting with '0x'")
|
|
557
|
+
|
|
558
|
+
# Validate hexadecimal characters
|
|
559
|
+
try:
|
|
560
|
+
int(addr[2:], 16)
|
|
561
|
+
except ValueError:
|
|
562
|
+
raise ValueError(f"Invalid hexadecimal characters in address: {addr}")
|
|
563
|
+
|
|
564
|
+
# Determine chain ID to use (local bookkeeping)
|
|
565
|
+
if chainId is None:
|
|
566
|
+
# Extract chain ID from agentId if available, otherwise use SDK's chain ID
|
|
567
|
+
if self.agentId and ":" in self.agentId:
|
|
568
|
+
try:
|
|
569
|
+
chainId = int(self.agentId.split(":")[0]) # First part is chainId
|
|
570
|
+
except (ValueError, IndexError):
|
|
571
|
+
chainId = self.sdk.chainId # Use SDK's chain ID as fallback
|
|
572
|
+
else:
|
|
573
|
+
chainId = self.sdk.chainId # Use SDK's chain ID as fallback
|
|
574
|
+
|
|
575
|
+
# Parse agent ID
|
|
576
|
+
agent_id_int = int(self.agentId.split(":")[-1]) if ":" in self.agentId else int(self.agentId)
|
|
577
|
+
|
|
578
|
+
# Check if wallet is already set to this address (skip if same)
|
|
579
|
+
try:
|
|
580
|
+
current_wallet = self.getWallet()
|
|
581
|
+
if current_wallet and current_wallet.lower() == addr.lower():
|
|
582
|
+
logger.debug(f"Agent wallet is already set to {addr}, skipping on-chain update")
|
|
583
|
+
# Still update local registration file
|
|
584
|
+
self.registration_file.walletAddress = addr
|
|
585
|
+
self.registration_file.walletChainId = chainId
|
|
586
|
+
self.registration_file.updatedAt = int(time.time())
|
|
587
|
+
return None
|
|
588
|
+
except Exception as e:
|
|
589
|
+
logger.debug(f"Could not check current agent wallet: {e}, proceeding with update")
|
|
590
|
+
|
|
591
|
+
# Set deadline (default to 60 seconds from now; contract max is now+5min)
|
|
592
|
+
if deadline is None:
|
|
593
|
+
deadline = int(time.time()) + 60
|
|
594
|
+
|
|
595
|
+
# Resolve typed data + signature
|
|
596
|
+
identity_registry_address = self.sdk.identity_registry.address
|
|
597
|
+
owner_address = self.sdk.web3_client.call_contract(self.sdk.identity_registry, "ownerOf", agent_id_int)
|
|
598
|
+
|
|
599
|
+
full_message = self.sdk.web3_client.build_agent_wallet_set_typed_data(
|
|
600
|
+
agent_id=agent_id_int,
|
|
601
|
+
new_wallet=addr,
|
|
602
|
+
owner=owner_address,
|
|
603
|
+
deadline=deadline,
|
|
604
|
+
verifying_contract=identity_registry_address,
|
|
605
|
+
chain_id=self.sdk.web3_client.chain_id,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
if signature is None:
|
|
609
|
+
# EOA signing paths
|
|
610
|
+
if new_wallet_signer is not None:
|
|
611
|
+
# Validate signer address matches addr (fail fast)
|
|
612
|
+
try:
|
|
613
|
+
from eth_account import Account as _Account
|
|
614
|
+
if isinstance(new_wallet_signer, str):
|
|
615
|
+
signer_addr = _Account.from_key(new_wallet_signer).address
|
|
616
|
+
else:
|
|
617
|
+
signer_addr = getattr(new_wallet_signer, "address", None)
|
|
618
|
+
except Exception:
|
|
619
|
+
signer_addr = getattr(new_wallet_signer, "address", None)
|
|
620
|
+
|
|
621
|
+
if not signer_addr or signer_addr.lower() != addr.lower():
|
|
622
|
+
raise ValueError(
|
|
623
|
+
f"new_wallet_signer address ({signer_addr}) does not match new_wallet ({addr})."
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
signature = self.sdk.web3_client.sign_typed_data(full_message, new_wallet_signer) # type: ignore[arg-type]
|
|
627
|
+
else:
|
|
628
|
+
# Auto-sign only if SDK signer == new wallet
|
|
629
|
+
current_address = self.sdk.web3_client.account.address if self.sdk.web3_client.account else None
|
|
630
|
+
if current_address and current_address.lower() == addr.lower():
|
|
631
|
+
signature = self.sdk.web3_client.sign_typed_data(full_message, self.sdk.web3_client.account)
|
|
632
|
+
else:
|
|
633
|
+
raise ValueError(
|
|
634
|
+
f"New wallet must sign. Provide new_wallet_signer (EOA) or signature (ERC-1271/external). "
|
|
635
|
+
f"SDK signer is {current_address}, new_wallet is {addr}."
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
# Optional: verify recover matches addr for EOA signatures
|
|
639
|
+
recovered = self.sdk.web3_client.w3.eth.account.recover_message(
|
|
640
|
+
__import__("eth_account.messages").messages.encode_typed_data(full_message=full_message),
|
|
641
|
+
signature=signature,
|
|
642
|
+
)
|
|
643
|
+
if recovered.lower() != addr.lower():
|
|
644
|
+
raise ValueError(f"Signature verification failed: recovered {recovered} but expected {addr}")
|
|
645
|
+
|
|
646
|
+
# Submit on-chain tx (tx sender is SDK signer: owner/operator)
|
|
647
|
+
try:
|
|
648
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
649
|
+
self.sdk.identity_registry,
|
|
650
|
+
"setAgentWallet",
|
|
651
|
+
agent_id_int,
|
|
652
|
+
addr,
|
|
653
|
+
deadline,
|
|
654
|
+
signature
|
|
655
|
+
)
|
|
656
|
+
except Exception as e:
|
|
657
|
+
raise ValueError(f"Failed to set agent wallet on-chain: {e}")
|
|
658
|
+
|
|
659
|
+
def _apply(_receipt: Dict[str, Any]) -> "Agent":
|
|
660
|
+
self.registration_file.walletAddress = addr
|
|
661
|
+
self.registration_file.walletChainId = chainId
|
|
662
|
+
self.registration_file.updatedAt = int(time.time())
|
|
663
|
+
self._last_registered_wallet = addr
|
|
664
|
+
return self
|
|
665
|
+
|
|
666
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply)
|
|
667
|
+
|
|
668
|
+
def unsetWallet(self) -> Optional[TransactionHandle["Agent"]]:
|
|
669
|
+
"""Unset agent wallet address on-chain (verified agentWallet).
|
|
670
|
+
|
|
671
|
+
This method is **on-chain only** and requires the agent to be registered.
|
|
672
|
+
It unsets the on-chain value and clears the local
|
|
673
|
+
`walletAddress` / `walletChainId` fields.
|
|
674
|
+
"""
|
|
675
|
+
if not self.agentId:
|
|
676
|
+
raise ValueError(
|
|
677
|
+
"Cannot unset agent wallet before the agent is registered on-chain. "
|
|
678
|
+
"Call agent.register(...) / agent.registerIPFS() first to obtain agentId."
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Parse agent ID (tokenId is always the last segment)
|
|
682
|
+
agent_id_int = int(self.agentId.split(":")[-1]) if ":" in self.agentId else int(self.agentId)
|
|
683
|
+
|
|
684
|
+
# Optional short-circuit if already unset (best-effort).
|
|
685
|
+
try:
|
|
686
|
+
current_wallet = self.getWallet()
|
|
687
|
+
if current_wallet is None:
|
|
688
|
+
self.registration_file.walletAddress = None
|
|
689
|
+
self.registration_file.walletChainId = None
|
|
690
|
+
self.registration_file.updatedAt = int(time.time())
|
|
691
|
+
return None
|
|
692
|
+
except Exception:
|
|
693
|
+
pass
|
|
694
|
+
|
|
695
|
+
try:
|
|
696
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
697
|
+
self.sdk.identity_registry,
|
|
698
|
+
"unsetAgentWallet",
|
|
699
|
+
agent_id_int
|
|
700
|
+
)
|
|
701
|
+
except Exception as e:
|
|
702
|
+
raise ValueError(f"Failed to unset agent wallet on-chain: {e}")
|
|
703
|
+
|
|
704
|
+
def _apply(_receipt: Dict[str, Any]) -> "Agent":
|
|
705
|
+
self.registration_file.walletAddress = None
|
|
706
|
+
self.registration_file.walletChainId = None
|
|
707
|
+
self.registration_file.updatedAt = int(time.time())
|
|
708
|
+
return self
|
|
709
|
+
|
|
710
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply)
|
|
711
|
+
|
|
712
|
+
def setENS(self, name: str, version: str = "1.0") -> 'Agent':
|
|
713
|
+
"""Set ENS name both on-chain and in registration file."""
|
|
714
|
+
# Remove existing ENS endpoints
|
|
715
|
+
self.registration_file.endpoints = [
|
|
716
|
+
ep for ep in self.registration_file.endpoints
|
|
717
|
+
if ep.type != EndpointType.ENS
|
|
718
|
+
]
|
|
719
|
+
|
|
720
|
+
# Check if ENS changed
|
|
721
|
+
if name != self._last_registered_ens:
|
|
722
|
+
self._dirty_metadata.add("agentName")
|
|
723
|
+
|
|
724
|
+
# Add new ENS endpoint
|
|
725
|
+
ens_endpoint = Endpoint(
|
|
726
|
+
type=EndpointType.ENS,
|
|
727
|
+
value=name,
|
|
728
|
+
meta={"version": version}
|
|
729
|
+
)
|
|
730
|
+
self.registration_file.endpoints.append(ens_endpoint)
|
|
731
|
+
self.registration_file.updatedAt = int(time.time())
|
|
732
|
+
|
|
733
|
+
return self
|
|
734
|
+
|
|
735
|
+
def setActive(self, active: bool) -> 'Agent':
|
|
736
|
+
"""Set agent active status."""
|
|
737
|
+
self.registration_file.active = active
|
|
738
|
+
self.registration_file.updatedAt = int(time.time())
|
|
739
|
+
return self
|
|
740
|
+
|
|
741
|
+
def setX402Support(self, x402Support: bool) -> 'Agent':
|
|
742
|
+
"""Set agent x402 payment support."""
|
|
743
|
+
self.registration_file.x402support = x402Support
|
|
744
|
+
self.registration_file.updatedAt = int(time.time())
|
|
745
|
+
return self
|
|
746
|
+
|
|
747
|
+
# Metadata management
|
|
748
|
+
def setMetadata(self, kv: Dict[str, Any]) -> 'Agent':
|
|
749
|
+
"""Set metadata (SDK-managed bag)."""
|
|
750
|
+
# Mark all provided keys as dirty
|
|
751
|
+
for key in kv.keys():
|
|
752
|
+
self._dirty_metadata.add(key)
|
|
753
|
+
|
|
754
|
+
self.registration_file.metadata.update(kv)
|
|
755
|
+
self.registration_file.updatedAt = int(time.time())
|
|
756
|
+
return self
|
|
757
|
+
|
|
758
|
+
def getMetadata(self) -> Dict[str, Any]:
|
|
759
|
+
"""Get metadata."""
|
|
760
|
+
return self.registration_file.metadata.copy()
|
|
761
|
+
|
|
762
|
+
def delMetadata(self, key: str) -> 'Agent':
|
|
763
|
+
"""Delete a metadata key."""
|
|
764
|
+
if key in self.registration_file.metadata:
|
|
765
|
+
del self.registration_file.metadata[key]
|
|
766
|
+
# Mark this key as dirty for tracking
|
|
767
|
+
self._dirty_metadata.discard(key) # Remove from dirty set since it's being deleted
|
|
768
|
+
self.registration_file.updatedAt = int(time.time())
|
|
769
|
+
return self
|
|
770
|
+
|
|
771
|
+
# Local inspection
|
|
772
|
+
def getRegistrationFile(self) -> RegistrationFile:
|
|
773
|
+
"""Get current in-memory file (not necessarily published yet)."""
|
|
774
|
+
return self.registration_file
|
|
775
|
+
|
|
776
|
+
# Registration (on-chain)
|
|
777
|
+
def registerIPFS(self) -> TransactionHandle[RegistrationFile]:
|
|
778
|
+
"""Register agent on-chain with IPFS flow (mint -> pin -> set URI) or update existing registration.
|
|
779
|
+
|
|
780
|
+
Submitted-by-default: returns a TransactionHandle immediately after the first tx is submitted.
|
|
781
|
+
"""
|
|
782
|
+
# Validate basic info
|
|
783
|
+
if not self.registration_file.name or not self.registration_file.description:
|
|
784
|
+
raise ValueError("Agent must have name and description before registration")
|
|
785
|
+
|
|
786
|
+
if self.registration_file.agentId:
|
|
787
|
+
# Agent already registered: upload -> submit setAgentURI; do metadata best-effort after confirmation.
|
|
788
|
+
ipfsCid = self.sdk.ipfs_client.addRegistrationFile(
|
|
789
|
+
self.registration_file,
|
|
790
|
+
chainId=self.sdk.chain_id(),
|
|
791
|
+
identityRegistryAddress=self.sdk.identity_registry.address,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
agentId_int = int(self.agentId.split(":")[-1])
|
|
795
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
796
|
+
self.sdk.identity_registry,
|
|
797
|
+
"setAgentURI",
|
|
798
|
+
agentId_int,
|
|
799
|
+
f"ipfs://{ipfsCid}",
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
def _apply(_receipt: Dict[str, Any]) -> RegistrationFile:
|
|
803
|
+
# Best-effort metadata updates (may involve additional txs)
|
|
804
|
+
if self._dirty_metadata:
|
|
805
|
+
metadata_entries = self._collectMetadataForRegistration()
|
|
806
|
+
for entry in metadata_entries:
|
|
807
|
+
if entry["key"] in self._dirty_metadata:
|
|
808
|
+
try:
|
|
809
|
+
h = self.sdk.web3_client.transact_contract(
|
|
810
|
+
self.sdk.identity_registry,
|
|
811
|
+
"setMetadata",
|
|
812
|
+
agentId_int,
|
|
813
|
+
entry["key"],
|
|
814
|
+
entry["value"],
|
|
815
|
+
)
|
|
816
|
+
self.sdk.web3_client.wait_for_transaction(h, timeout=30)
|
|
817
|
+
except Exception as e:
|
|
818
|
+
logger.warning(f"Metadata update failed or timed out for {entry['key']} (tx sent): {e}")
|
|
819
|
+
|
|
820
|
+
self.registration_file.agentURI = f"ipfs://{ipfsCid}"
|
|
821
|
+
self.registration_file.updatedAt = int(time.time())
|
|
822
|
+
self._last_registered_wallet = self.walletAddress
|
|
823
|
+
self._last_registered_ens = self.ensEndpoint
|
|
824
|
+
self._dirty_metadata.clear()
|
|
825
|
+
return self.registration_file
|
|
826
|
+
|
|
827
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply)
|
|
828
|
+
|
|
829
|
+
# First time registration: tx1=register(no URI) -> wait -> upload -> tx2=setAgentURI -> wait
|
|
830
|
+
metadata_entries = self._collectMetadataForRegistration()
|
|
831
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
832
|
+
self.sdk.identity_registry,
|
|
833
|
+
"register",
|
|
834
|
+
"",
|
|
835
|
+
metadata_entries,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
def _apply_first(receipt: Dict[str, Any]) -> RegistrationFile:
|
|
839
|
+
agentId_minted = self._extractAgentIdFromReceipt(receipt)
|
|
840
|
+
self.registration_file.agentId = f"{self.sdk.chain_id()}:{agentId_minted}"
|
|
841
|
+
self.registration_file.updatedAt = int(time.time())
|
|
842
|
+
|
|
843
|
+
ipfsCid = self.sdk.ipfs_client.addRegistrationFile(
|
|
844
|
+
self.registration_file,
|
|
845
|
+
chainId=self.sdk.chain_id(),
|
|
846
|
+
identityRegistryAddress=self.sdk.identity_registry.address,
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
txHash2 = self.sdk.web3_client.transact_contract(
|
|
850
|
+
self.sdk.identity_registry,
|
|
851
|
+
"setAgentURI",
|
|
852
|
+
agentId_minted,
|
|
853
|
+
f"ipfs://{ipfsCid}",
|
|
854
|
+
)
|
|
855
|
+
self.sdk.web3_client.wait_for_transaction(txHash2, timeout=30)
|
|
856
|
+
|
|
857
|
+
self.registration_file.agentURI = f"ipfs://{ipfsCid}"
|
|
858
|
+
self.registration_file.updatedAt = int(time.time())
|
|
859
|
+
self._last_registered_wallet = self.walletAddress
|
|
860
|
+
self._last_registered_ens = self.ensEndpoint
|
|
861
|
+
self._dirty_metadata.clear()
|
|
862
|
+
return self.registration_file
|
|
863
|
+
|
|
864
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply_first)
|
|
865
|
+
|
|
866
|
+
def register(self, agentUri: str) -> TransactionHandle[RegistrationFile]:
|
|
867
|
+
"""Register agent on-chain with direct URI (submitted-by-default)."""
|
|
868
|
+
# Validate basic info
|
|
869
|
+
if not self.registration_file.name or not self.registration_file.description:
|
|
870
|
+
raise ValueError("Agent must have name and description before registration")
|
|
871
|
+
|
|
872
|
+
if self.registration_file.agentId:
|
|
873
|
+
# Update URI on-chain for existing agent
|
|
874
|
+
updated = self.updateRegistration(agentURI=agentUri)
|
|
875
|
+
if isinstance(updated, TransactionHandle):
|
|
876
|
+
return updated
|
|
877
|
+
# Should not happen (agentURI was provided), but keep a safe fallback.
|
|
878
|
+
raise RuntimeError("Expected updateRegistration to return a TransactionHandle when agentURI is provided")
|
|
879
|
+
|
|
880
|
+
return self._registerWithUri(agentUri)
|
|
881
|
+
|
|
882
|
+
def _registerWithoutUri(self, idem: Optional[IdemKey] = None) -> TransactionHandle[RegistrationFile]:
|
|
883
|
+
"""Register without URI (IPFS flow step 1) with metadata."""
|
|
884
|
+
# Collect metadata for registration
|
|
885
|
+
metadata_entries = self._collectMetadataForRegistration()
|
|
886
|
+
|
|
887
|
+
# Mint agent with metadata
|
|
888
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
889
|
+
self.sdk.identity_registry,
|
|
890
|
+
"register",
|
|
891
|
+
"", # Empty agentURI for now
|
|
892
|
+
metadata_entries
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
def _apply(receipt: Dict[str, Any]) -> RegistrationFile:
|
|
896
|
+
agentId = self._extractAgentIdFromReceipt(receipt)
|
|
897
|
+
self.registration_file.agentId = f"{self.sdk.chain_id()}:{agentId}"
|
|
898
|
+
self.registration_file.updatedAt = int(time.time())
|
|
899
|
+
return self.registration_file
|
|
900
|
+
|
|
901
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply)
|
|
902
|
+
|
|
903
|
+
def _registerWithUri(self, agentURI: URI, idem: Optional[IdemKey] = None) -> TransactionHandle[RegistrationFile]:
|
|
904
|
+
"""Register with direct URI and metadata."""
|
|
905
|
+
# Update registration file
|
|
906
|
+
self.registration_file.agentURI = agentURI
|
|
907
|
+
self.registration_file.updatedAt = int(time.time())
|
|
908
|
+
|
|
909
|
+
# Collect metadata for registration
|
|
910
|
+
metadata_entries = self._collectMetadataForRegistration()
|
|
911
|
+
|
|
912
|
+
# Mint agent with URI and metadata
|
|
913
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
914
|
+
self.sdk.identity_registry,
|
|
915
|
+
"register",
|
|
916
|
+
agentURI,
|
|
917
|
+
metadata_entries
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
def _apply(receipt: Dict[str, Any]) -> RegistrationFile:
|
|
921
|
+
agentId = self._extractAgentIdFromReceipt(receipt)
|
|
922
|
+
self.registration_file.agentId = f"{self.sdk.chain_id()}:{agentId}"
|
|
923
|
+
self.registration_file.updatedAt = int(time.time())
|
|
924
|
+
return self.registration_file
|
|
925
|
+
|
|
926
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply)
|
|
927
|
+
|
|
928
|
+
def _extractAgentIdFromReceipt(self, receipt: Dict[str, Any]) -> int:
|
|
929
|
+
"""Extract agent ID from transaction receipt."""
|
|
930
|
+
# Look for Transfer event (ERC-721)
|
|
931
|
+
for i, log in enumerate(receipt.get('logs', [])):
|
|
932
|
+
try:
|
|
933
|
+
topics = log.get('topics', [])
|
|
934
|
+
if len(topics) >= 4:
|
|
935
|
+
topic0 = topics[0].hex()
|
|
936
|
+
# Check if this is a Transfer event (ERC-721) by looking at the topic
|
|
937
|
+
if topic0 == 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef':
|
|
938
|
+
# The fourth topic should contain the token ID
|
|
939
|
+
agentId_hex = topics[3].hex()
|
|
940
|
+
agentId = int(agentId_hex, 16)
|
|
941
|
+
return agentId
|
|
942
|
+
except Exception:
|
|
943
|
+
continue
|
|
944
|
+
|
|
945
|
+
# If no Transfer event found, try to get the token ID from the transaction
|
|
946
|
+
# This is a fallback for cases where the event might not be properly indexed
|
|
947
|
+
try:
|
|
948
|
+
# Get the transaction details
|
|
949
|
+
tx = self.sdk.web3_client.w3.eth.get_transaction(receipt['transactionHash'])
|
|
950
|
+
|
|
951
|
+
# Try to call the contract to get the latest token ID
|
|
952
|
+
# This assumes the contract has a method to get the total supply or latest ID
|
|
953
|
+
try:
|
|
954
|
+
total_supply = self.sdk.identity_registry.functions.totalSupply().call()
|
|
955
|
+
if total_supply > 0:
|
|
956
|
+
# Return the latest token ID (total supply - 1, since it's 0-indexed)
|
|
957
|
+
agentId = total_supply - 1
|
|
958
|
+
return agentId
|
|
959
|
+
except Exception:
|
|
960
|
+
pass
|
|
961
|
+
|
|
962
|
+
except Exception:
|
|
963
|
+
pass
|
|
964
|
+
|
|
965
|
+
raise ValueError("Could not extract agent ID from transaction receipt")
|
|
966
|
+
|
|
967
|
+
def updateRegistration(
|
|
968
|
+
self,
|
|
969
|
+
agentURI: Optional[URI] = None,
|
|
970
|
+
idem: Optional[IdemKey] = None,
|
|
971
|
+
) -> Union[RegistrationFile, TransactionHandle[RegistrationFile]]:
|
|
972
|
+
"""Update registration after edits."""
|
|
973
|
+
if not self.registration_file.agentId:
|
|
974
|
+
raise ValueError("Agent must be registered before updating")
|
|
975
|
+
|
|
976
|
+
# Update URI if provided
|
|
977
|
+
if agentURI is not None:
|
|
978
|
+
self.registration_file.agentURI = agentURI
|
|
979
|
+
|
|
980
|
+
# Update timestamp
|
|
981
|
+
self.registration_file.updatedAt = int(time.time())
|
|
982
|
+
|
|
983
|
+
# Update on-chain URI if needed
|
|
984
|
+
if agentURI is not None:
|
|
985
|
+
agentId_int = int(self.registration_file.agentId.split(":")[-1])
|
|
986
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
987
|
+
self.sdk.identity_registry,
|
|
988
|
+
"setAgentURI",
|
|
989
|
+
agentId_int,
|
|
990
|
+
agentURI,
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
def _apply(_receipt: Dict[str, Any]) -> RegistrationFile:
|
|
994
|
+
return self.registration_file
|
|
995
|
+
|
|
996
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply)
|
|
997
|
+
|
|
998
|
+
return self.registration_file
|
|
999
|
+
|
|
1000
|
+
def setAgentUri(self, uri: str) -> 'Agent':
|
|
1001
|
+
"""Set the agent URI in registration file (will be saved on-chain during next register call)."""
|
|
1002
|
+
if not self.registration_file.agentId:
|
|
1003
|
+
raise ValueError("Agent must be registered before setting URI")
|
|
1004
|
+
|
|
1005
|
+
# Update local registration file
|
|
1006
|
+
self.registration_file.agentURI = uri
|
|
1007
|
+
self.registration_file.updatedAt = int(time.time())
|
|
1008
|
+
|
|
1009
|
+
return self
|
|
1010
|
+
|
|
1011
|
+
# Ownership and lifecycle controls
|
|
1012
|
+
def transfer(
|
|
1013
|
+
self,
|
|
1014
|
+
to: Address,
|
|
1015
|
+
approve_operator: bool = False,
|
|
1016
|
+
idem: Optional[IdemKey] = None,
|
|
1017
|
+
) -> TransactionHandle[Dict[str, Any]]:
|
|
1018
|
+
"""Transfer agent ownership.
|
|
1019
|
+
|
|
1020
|
+
Note: When an agent is transferred, the agentWallet is automatically reset
|
|
1021
|
+
to the zero address on-chain. The new owner must call setWallet() to
|
|
1022
|
+
set a new wallet address with EIP-712 signature verification.
|
|
1023
|
+
"""
|
|
1024
|
+
if not self.registration_file.agentId:
|
|
1025
|
+
raise ValueError("Agent must be registered before transferring")
|
|
1026
|
+
|
|
1027
|
+
agentId = int(self.registration_file.agentId.split(":")[-1])
|
|
1028
|
+
|
|
1029
|
+
# Transfer ownership
|
|
1030
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
1031
|
+
self.sdk.identity_registry,
|
|
1032
|
+
"transferFrom",
|
|
1033
|
+
self.sdk.web3_client.account.address,
|
|
1034
|
+
to,
|
|
1035
|
+
agentId
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
def _apply(_receipt: Dict[str, Any]) -> Dict[str, Any]:
|
|
1039
|
+
# Note: agentWallet will be reset to zero address by the contract
|
|
1040
|
+
self.registration_file.walletAddress = None
|
|
1041
|
+
self._last_registered_wallet = None
|
|
1042
|
+
self.registration_file.updatedAt = int(time.time())
|
|
1043
|
+
return {
|
|
1044
|
+
"txHash": txHash,
|
|
1045
|
+
"agentId": self.registration_file.agentId,
|
|
1046
|
+
"from": self.sdk.web3_client.account.address,
|
|
1047
|
+
"to": to,
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply)
|
|
1051
|
+
|
|
1052
|
+
def addOperator(self, operator: Address, idem: Optional[IdemKey] = None) -> TransactionHandle[Dict[str, Any]]:
|
|
1053
|
+
"""Add operator (setApprovalForAll)."""
|
|
1054
|
+
if not self.registration_file.agentId:
|
|
1055
|
+
raise ValueError("Agent must be registered before adding operators")
|
|
1056
|
+
|
|
1057
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
1058
|
+
self.sdk.identity_registry,
|
|
1059
|
+
"setApprovalForAll",
|
|
1060
|
+
operator,
|
|
1061
|
+
True
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1064
|
+
return TransactionHandle(
|
|
1065
|
+
web3_client=self.sdk.web3_client,
|
|
1066
|
+
tx_hash=txHash,
|
|
1067
|
+
compute_result=lambda _receipt: {"txHash": txHash, "operator": operator},
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
def removeOperator(self, operator: Address, idem: Optional[IdemKey] = None) -> TransactionHandle[Dict[str, Any]]:
|
|
1071
|
+
"""Remove operator."""
|
|
1072
|
+
if not self.registration_file.agentId:
|
|
1073
|
+
raise ValueError("Agent must be registered before removing operators")
|
|
1074
|
+
|
|
1075
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
1076
|
+
self.sdk.identity_registry,
|
|
1077
|
+
"setApprovalForAll",
|
|
1078
|
+
operator,
|
|
1079
|
+
False
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
return TransactionHandle(
|
|
1083
|
+
web3_client=self.sdk.web3_client,
|
|
1084
|
+
tx_hash=txHash,
|
|
1085
|
+
compute_result=lambda _receipt: {"txHash": txHash, "operator": operator},
|
|
1086
|
+
)
|
|
1087
|
+
|
|
1088
|
+
def transfer(self, newOwnerAddress: str) -> TransactionHandle[Dict[str, Any]]:
|
|
1089
|
+
"""Transfer agent ownership to a new address.
|
|
1090
|
+
|
|
1091
|
+
Only the current owner can transfer the agent.
|
|
1092
|
+
|
|
1093
|
+
Note: When an agent is transferred, the agentWallet is automatically reset
|
|
1094
|
+
to the zero address on-chain. The new owner must call setWallet() to
|
|
1095
|
+
set a new wallet address with EIP-712 signature verification.
|
|
1096
|
+
|
|
1097
|
+
Args:
|
|
1098
|
+
newOwnerAddress: Ethereum address of the new owner
|
|
1099
|
+
|
|
1100
|
+
Returns:
|
|
1101
|
+
Transaction receipt
|
|
1102
|
+
|
|
1103
|
+
Raises:
|
|
1104
|
+
ValueError: If address is invalid or transfer not allowed
|
|
1105
|
+
"""
|
|
1106
|
+
if not self.registration_file.agentId:
|
|
1107
|
+
raise ValueError("Agent must be registered before transfer")
|
|
1108
|
+
|
|
1109
|
+
# Validate new owner address
|
|
1110
|
+
if not newOwnerAddress or newOwnerAddress == "0x0000000000000000000000000000000000000000":
|
|
1111
|
+
raise ValueError("New owner address cannot be zero address")
|
|
1112
|
+
|
|
1113
|
+
# Get current owner using SDK utility
|
|
1114
|
+
currentOwner = self.sdk.getAgentOwner(self.registration_file.agentId)
|
|
1115
|
+
|
|
1116
|
+
# Check if caller is the current owner
|
|
1117
|
+
callerAddress = self.sdk.web3_client.account.address
|
|
1118
|
+
if callerAddress.lower() != currentOwner.lower():
|
|
1119
|
+
raise ValueError(f"Only the current owner ({currentOwner}) can transfer the agent")
|
|
1120
|
+
|
|
1121
|
+
# Prevent self-transfer
|
|
1122
|
+
if newOwnerAddress.lower() == currentOwner.lower():
|
|
1123
|
+
raise ValueError("Cannot transfer to the same owner")
|
|
1124
|
+
|
|
1125
|
+
# Validate address format (basic checksum validation)
|
|
1126
|
+
try:
|
|
1127
|
+
# Convert to checksum format for validation
|
|
1128
|
+
checksum_address = self.sdk.web3_client.w3.to_checksum_address(newOwnerAddress)
|
|
1129
|
+
except Exception as e:
|
|
1130
|
+
raise ValueError(f"Invalid address format: {e}")
|
|
1131
|
+
|
|
1132
|
+
logger.debug(f"Transferring agent {self.registration_file.agentId} from {currentOwner} to {checksum_address}")
|
|
1133
|
+
|
|
1134
|
+
# Parse agentId to extract tokenId for contract call
|
|
1135
|
+
agent_id_str = str(self.registration_file.agentId)
|
|
1136
|
+
if ":" in agent_id_str:
|
|
1137
|
+
token_id = int(agent_id_str.split(":")[-1])
|
|
1138
|
+
else:
|
|
1139
|
+
token_id = int(agent_id_str)
|
|
1140
|
+
|
|
1141
|
+
# Call transferFrom on the IdentityRegistry contract
|
|
1142
|
+
txHash = self.sdk.web3_client.transact_contract(
|
|
1143
|
+
self.sdk.identity_registry,
|
|
1144
|
+
"transferFrom",
|
|
1145
|
+
currentOwner,
|
|
1146
|
+
checksum_address,
|
|
1147
|
+
token_id
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
def _apply(_receipt: Dict[str, Any]) -> Dict[str, Any]:
|
|
1151
|
+
logger.debug(f"Agent {self.registration_file.agentId} successfully transferred to {checksum_address}")
|
|
1152
|
+
self.registration_file.walletAddress = None
|
|
1153
|
+
self._last_registered_wallet = None
|
|
1154
|
+
self.registration_file.updatedAt = int(time.time())
|
|
1155
|
+
return {
|
|
1156
|
+
"txHash": txHash,
|
|
1157
|
+
"from": currentOwner,
|
|
1158
|
+
"to": checksum_address,
|
|
1159
|
+
"agentId": self.registration_file.agentId,
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return TransactionHandle(web3_client=self.sdk.web3_client, tx_hash=txHash, compute_result=_apply)
|
|
1163
|
+
|
|
1164
|
+
def activate(self, idem: Optional[IdemKey] = None) -> RegistrationFile:
|
|
1165
|
+
"""Activate agent (soft "undelete")."""
|
|
1166
|
+
self.registration_file.active = True
|
|
1167
|
+
self.registration_file.updatedAt = int(time.time())
|
|
1168
|
+
return self.registration_file
|
|
1169
|
+
|
|
1170
|
+
def deactivate(self, idem: Optional[IdemKey] = None) -> RegistrationFile:
|
|
1171
|
+
"""Deactivate agent (soft "delete")."""
|
|
1172
|
+
self.registration_file.active = False
|
|
1173
|
+
self.registration_file.updatedAt = int(time.time())
|
|
1174
|
+
return self.registration_file
|
|
1175
|
+
|
|
1176
|
+
# Utility methods
|
|
1177
|
+
def toJson(self) -> str:
|
|
1178
|
+
"""Convert registration file to JSON."""
|
|
1179
|
+
return json.dumps(self.registration_file.to_dict(
|
|
1180
|
+
chain_id=self.sdk.chain_id(),
|
|
1181
|
+
identity_registry_address=self.sdk.identity_registry.address if self.sdk.identity_registry else None
|
|
1182
|
+
), indent=2)
|
|
1183
|
+
|
|
1184
|
+
def saveToFile(self, filePath: str) -> None:
|
|
1185
|
+
"""Save registration file to local file."""
|
|
1186
|
+
with open(filePath, 'w') as f:
|
|
1187
|
+
f.write(self.to_json())
|