agent0-sdk 0.3rc1__py3-none-any.whl → 0.5__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 CHANGED
@@ -30,7 +30,7 @@ except ImportError:
30
30
  Agent = None
31
31
  _sdk_available = False
32
32
 
33
- __version__ = "0.3rc1"
33
+ __version__ = "0.5"
34
34
  __all__ = [
35
35
  "SDK",
36
36
  "Agent",
agent0_sdk/core/agent.py CHANGED
@@ -16,6 +16,7 @@ from .models import (
16
16
  )
17
17
  from .web3_client import Web3Client
18
18
  from .endpoint_crawler import EndpointCrawler
19
+ from .oasf_validator import validate_skill, validate_domain
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from .sdk import SDK
@@ -175,16 +176,14 @@ class Agent:
175
176
  return self.registration_file
176
177
 
177
178
  def _collectMetadataForRegistration(self) -> List[Dict[str, Any]]:
178
- """Collect all metadata entries for registration."""
179
+ """Collect all metadata entries for registration.
180
+
181
+ Note: agentWallet is now a reserved metadata key and cannot be set via setMetadata().
182
+ It must be set separately using setAgentWallet() with EIP-712 signature verification.
183
+ """
179
184
  metadata_entries = []
180
185
 
181
- # Add wallet address metadata
182
- if self.walletAddress:
183
- addr_bytes = bytes.fromhex(self.walletAddress[2:]) # Remove '0x' prefix
184
- metadata_entries.append({
185
- "key": "agentWallet",
186
- "value": addr_bytes
187
- })
186
+ # Note: agentWallet is no longer set via metadata - it's now reserved and managed via setAgentWallet()
188
187
 
189
188
  # Add ENS name metadata
190
189
  if self.ensEndpoint:
@@ -316,6 +315,137 @@ class Agent:
316
315
  """Remove all endpoints."""
317
316
  return self.removeEndpoint()
318
317
 
318
+ # OASF endpoint management
319
+ def _get_or_create_oasf_endpoint(self) -> Endpoint:
320
+ """Get existing OASF endpoint or create a new one with default values."""
321
+ # Find existing OASF endpoint
322
+ for ep in self.registration_file.endpoints:
323
+ if ep.type == EndpointType.OASF:
324
+ return ep
325
+
326
+ # Create new OASF endpoint with default values
327
+ oasf_endpoint = Endpoint(
328
+ type=EndpointType.OASF,
329
+ value="https://github.com/agntcy/oasf/",
330
+ meta={"version": "v0.8.0", "skills": [], "domains": []}
331
+ )
332
+ self.registration_file.endpoints.append(oasf_endpoint)
333
+ return oasf_endpoint
334
+
335
+ def addSkill(self, slug: str, validate_oasf: bool = False) -> 'Agent':
336
+ """
337
+ Add a skill to the OASF endpoint.
338
+
339
+ Args:
340
+ slug: The skill slug to add (e.g., "natural_language_processing/natural_language_generation/summarization")
341
+ validate_oasf: If True, validate the slug against the OASF taxonomy (default: False)
342
+
343
+ Returns:
344
+ self for method chaining
345
+
346
+ Raises:
347
+ ValueError: If validate_oasf=True and the slug is not valid
348
+ """
349
+ if validate_oasf:
350
+ if not validate_skill(slug):
351
+ raise ValueError(
352
+ f"Invalid OASF skill slug: {slug}. "
353
+ "Use validate_oasf=False to skip validation."
354
+ )
355
+
356
+ oasf_endpoint = self._get_or_create_oasf_endpoint()
357
+
358
+ # Initialize skills array if missing
359
+ if "skills" not in oasf_endpoint.meta:
360
+ oasf_endpoint.meta["skills"] = []
361
+
362
+ # Add slug if not already present (avoid duplicates)
363
+ skills = oasf_endpoint.meta["skills"]
364
+ if slug not in skills:
365
+ skills.append(slug)
366
+
367
+ self.registration_file.updatedAt = int(time.time())
368
+ return self
369
+
370
+ def removeSkill(self, slug: str) -> 'Agent':
371
+ """
372
+ Remove a skill from the OASF endpoint.
373
+
374
+ Args:
375
+ slug: The skill slug to remove
376
+
377
+ Returns:
378
+ self for method chaining
379
+ """
380
+ # Find OASF endpoint
381
+ for ep in self.registration_file.endpoints:
382
+ if ep.type == EndpointType.OASF:
383
+ if "skills" in ep.meta and isinstance(ep.meta["skills"], list):
384
+ skills = ep.meta["skills"]
385
+ if slug in skills:
386
+ skills.remove(slug)
387
+ self.registration_file.updatedAt = int(time.time())
388
+ break
389
+
390
+ return self
391
+
392
+ def addDomain(self, slug: str, validate_oasf: bool = False) -> 'Agent':
393
+ """
394
+ Add a domain to the OASF endpoint.
395
+
396
+ Args:
397
+ slug: The domain slug to add (e.g., "finance_and_business/investment_services")
398
+ validate_oasf: If True, validate the slug against the OASF taxonomy (default: False)
399
+
400
+ Returns:
401
+ self for method chaining
402
+
403
+ Raises:
404
+ ValueError: If validate_oasf=True and the slug is not valid
405
+ """
406
+ if validate_oasf:
407
+ if not validate_domain(slug):
408
+ raise ValueError(
409
+ f"Invalid OASF domain slug: {slug}. "
410
+ "Use validate_oasf=False to skip validation."
411
+ )
412
+
413
+ oasf_endpoint = self._get_or_create_oasf_endpoint()
414
+
415
+ # Initialize domains array if missing
416
+ if "domains" not in oasf_endpoint.meta:
417
+ oasf_endpoint.meta["domains"] = []
418
+
419
+ # Add slug if not already present (avoid duplicates)
420
+ domains = oasf_endpoint.meta["domains"]
421
+ if slug not in domains:
422
+ domains.append(slug)
423
+
424
+ self.registration_file.updatedAt = int(time.time())
425
+ return self
426
+
427
+ def removeDomain(self, slug: str) -> 'Agent':
428
+ """
429
+ Remove a domain from the OASF endpoint.
430
+
431
+ Args:
432
+ slug: The domain slug to remove
433
+
434
+ Returns:
435
+ self for method chaining
436
+ """
437
+ # Find OASF endpoint
438
+ for ep in self.registration_file.endpoints:
439
+ if ep.type == EndpointType.OASF:
440
+ if "domains" in ep.meta and isinstance(ep.meta["domains"], list):
441
+ domains = ep.meta["domains"]
442
+ if slug in domains:
443
+ domains.remove(slug)
444
+ self.registration_file.updatedAt = int(time.time())
445
+ break
446
+
447
+ return self
448
+
319
449
  # Trust models
320
450
  def setTrust(
321
451
  self,
@@ -360,20 +490,55 @@ class Agent:
360
490
  self.registration_file.updatedAt = int(time.time())
361
491
  return self
362
492
 
363
- def setAgentWallet(self, addr: Optional[Address], chainId: Optional[int] = None) -> 'Agent':
364
- """Set agent wallet address in registration file (will be saved on-chain during next register call)."""
365
- # Validate address format if provided
366
- if addr:
367
- if not addr.startswith("0x") or len(addr) != 42:
368
- raise ValueError(f"Invalid Ethereum address format: {addr}. Must be 42 characters starting with '0x'")
369
-
370
- # Validate hexadecimal characters
371
- try:
372
- int(addr[2:], 16)
373
- except ValueError:
374
- raise ValueError(f"Invalid hexadecimal characters in address: {addr}")
493
+ def setAgentWallet(
494
+ self,
495
+ new_wallet: Address,
496
+ chainId: Optional[int] = None,
497
+ *,
498
+ new_wallet_signer: Optional[Union[str, Any]] = None,
499
+ deadline: Optional[int] = None,
500
+ signature: Optional[bytes] = None,
501
+ ) -> 'Agent':
502
+ """Set agent wallet address on-chain (ERC-8004 agentWallet).
503
+
504
+ This method is **on-chain only**. The `agentWallet` is a verified attribute and must be set via
505
+ the IdentityRegistry `setAgentWallet` function.
506
+
507
+ EOAs: provide `new_wallet_signer` (private key string or eth-account account) OR ensure the SDK
508
+ signer address matches `new_wallet` so the SDK can auto-sign.\n
509
+ Contract wallets (ERC-1271): provide `signature` bytes produced by the wallet’s signing mechanism.
510
+ The SDK will build the correct EIP-712 typed data internally, but cannot produce the wallet signature.
511
+
512
+ Args:
513
+ new_wallet: New wallet address (must be controlled by the signer that produces the signature)
514
+ chainId: Optional local bookkeeping for registration file (walletChainId). Defaults to agent chain.
515
+ new_wallet_signer: EOA signer used to sign the EIP-712 message (private key string or eth-account account)
516
+ deadline: Signature deadline timestamp. Defaults to now+60s (must be <= now+5min per contract).
517
+ signature: Raw signature bytes (intended for ERC-1271 / external signing only)
518
+ """
519
+ # Breaking/clean: this API is only meaningful for already-registered agents.
520
+ if not self.agentId:
521
+ raise ValueError(
522
+ "Cannot set agent wallet before the agent is registered on-chain. "
523
+ "Call agent.register(...) / agent.registerIPFS() first to obtain agentId."
524
+ )
525
+
526
+ addr = new_wallet
527
+
528
+ if not addr:
529
+ raise ValueError("Wallet address cannot be empty. Use a non-zero address.")
530
+
531
+ # Validate address format
532
+ if not addr.startswith("0x") or len(addr) != 42:
533
+ raise ValueError(f"Invalid Ethereum address format: {addr}. Must be 42 characters starting with '0x'")
375
534
 
376
- # Determine chain ID to use
535
+ # Validate hexadecimal characters
536
+ try:
537
+ int(addr[2:], 16)
538
+ except ValueError:
539
+ raise ValueError(f"Invalid hexadecimal characters in address: {addr}")
540
+
541
+ # Determine chain ID to use (local bookkeeping)
377
542
  if chainId is None:
378
543
  # Extract chain ID from agentId if available, otherwise use SDK's chain ID
379
544
  if self.agentId and ":" in self.agentId:
@@ -384,14 +549,104 @@ class Agent:
384
549
  else:
385
550
  chainId = self.sdk.chainId # Use SDK's chain ID as fallback
386
551
 
387
- # Check if wallet changed
388
- if addr != self._last_registered_wallet:
389
- self._dirty_metadata.add("agentWallet")
552
+ # Parse agent ID
553
+ agent_id_int = int(self.agentId.split(":")[-1]) if ":" in self.agentId else int(self.agentId)
554
+
555
+ # Check if wallet is already set to this address (skip if same)
556
+ try:
557
+ current_wallet = self.sdk.web3_client.call_contract(
558
+ self.sdk.identity_registry,
559
+ "getAgentWallet",
560
+ agent_id_int
561
+ )
562
+ if current_wallet and current_wallet.lower() == addr.lower():
563
+ logger.debug(f"Agent wallet is already set to {addr}, skipping on-chain update")
564
+ # Still update local registration file
565
+ self.registration_file.walletAddress = addr
566
+ self.registration_file.walletChainId = chainId
567
+ self.registration_file.updatedAt = int(time.time())
568
+ return self
569
+ except Exception as e:
570
+ logger.debug(f"Could not check current agent wallet: {e}, proceeding with update")
571
+
572
+ # Set deadline (default to 60 seconds from now; contract max is now+5min)
573
+ if deadline is None:
574
+ deadline = int(time.time()) + 60
575
+
576
+ # Resolve typed data + signature
577
+ identity_registry_address = self.sdk.identity_registry.address
578
+ owner_address = self.sdk.web3_client.call_contract(self.sdk.identity_registry, "ownerOf", agent_id_int)
579
+
580
+ full_message = self.sdk.web3_client.build_agent_wallet_set_typed_data(
581
+ agent_id=agent_id_int,
582
+ new_wallet=addr,
583
+ owner=owner_address,
584
+ deadline=deadline,
585
+ verifying_contract=identity_registry_address,
586
+ chain_id=self.sdk.web3_client.chain_id,
587
+ )
588
+
589
+ if signature is None:
590
+ # EOA signing paths
591
+ if new_wallet_signer is not None:
592
+ # Validate signer address matches addr (fail fast)
593
+ try:
594
+ from eth_account import Account as _Account
595
+ if isinstance(new_wallet_signer, str):
596
+ signer_addr = _Account.from_key(new_wallet_signer).address
597
+ else:
598
+ signer_addr = getattr(new_wallet_signer, "address", None)
599
+ except Exception:
600
+ signer_addr = getattr(new_wallet_signer, "address", None)
601
+
602
+ if not signer_addr or signer_addr.lower() != addr.lower():
603
+ raise ValueError(
604
+ f"new_wallet_signer address ({signer_addr}) does not match new_wallet ({addr})."
605
+ )
606
+
607
+ signature = self.sdk.web3_client.sign_typed_data(full_message, new_wallet_signer) # type: ignore[arg-type]
608
+ else:
609
+ # Auto-sign only if SDK signer == new wallet
610
+ current_address = self.sdk.web3_client.account.address if self.sdk.web3_client.account else None
611
+ if current_address and current_address.lower() == addr.lower():
612
+ signature = self.sdk.web3_client.sign_typed_data(full_message, self.sdk.web3_client.account)
613
+ else:
614
+ raise ValueError(
615
+ f"New wallet must sign. Provide new_wallet_signer (EOA) or signature (ERC-1271/external). "
616
+ f"SDK signer is {current_address}, new_wallet is {addr}."
617
+ )
618
+
619
+ # Optional: verify recover matches addr for EOA signatures
620
+ recovered = self.sdk.web3_client.w3.eth.account.recover_message(
621
+ __import__("eth_account.messages").messages.encode_typed_data(full_message=full_message),
622
+ signature=signature,
623
+ )
624
+ if recovered.lower() != addr.lower():
625
+ raise ValueError(f"Signature verification failed: recovered {recovered} but expected {addr}")
626
+
627
+ # Call setAgentWallet on the contract
628
+ try:
629
+ txHash = self.sdk.web3_client.transact_contract(
630
+ self.sdk.identity_registry,
631
+ "setAgentWallet",
632
+ agent_id_int,
633
+ addr,
634
+ deadline,
635
+ signature
636
+ )
637
+
638
+ # Wait for transaction
639
+ receipt = self.sdk.web3_client.wait_for_transaction(txHash)
640
+ logger.debug(f"Agent wallet set on-chain: {txHash}")
641
+
642
+ except Exception as e:
643
+ raise ValueError(f"Failed to set agent wallet on-chain: {e}")
390
644
 
391
645
  # Update local registration file
392
646
  self.registration_file.walletAddress = addr
393
647
  self.registration_file.walletChainId = chainId
394
648
  self.registration_file.updatedAt = int(time.time())
649
+ self._last_registered_wallet = addr
395
650
 
396
651
  return self
397
652
 
@@ -504,7 +759,7 @@ class Agent:
504
759
  agentId = int(self.agentId.split(":")[-1])
505
760
  txHash = self.sdk.web3_client.transact_contract(
506
761
  self.sdk.identity_registry,
507
- "setAgentUri",
762
+ "setAgentURI",
508
763
  agentId,
509
764
  f"ipfs://{ipfsCid}"
510
765
  )
@@ -541,7 +796,7 @@ class Agent:
541
796
  agentId = int(self.agentId.split(":")[-1])
542
797
  txHash = self.sdk.web3_client.transact_contract(
543
798
  self.sdk.identity_registry,
544
- "setAgentUri",
799
+ "setAgentURI",
545
800
  agentId,
546
801
  f"ipfs://{ipfsCid}"
547
802
  )
@@ -583,7 +838,7 @@ class Agent:
583
838
  txHash = self.sdk.web3_client.transact_contract(
584
839
  self.sdk.identity_registry,
585
840
  "register",
586
- "", # Empty tokenUri for now
841
+ "", # Empty agentURI for now
587
842
  metadata_entries
588
843
  )
589
844
 
@@ -688,7 +943,7 @@ class Agent:
688
943
  agentId = int(self.registration_file.agentId.split(":")[-1])
689
944
  txHash = self.sdk.web3_client.transact_contract(
690
945
  self.sdk.identity_registry,
691
- "setAgentUri",
946
+ "setAgentURI",
692
947
  agentId,
693
948
  agentURI
694
949
  )
@@ -714,7 +969,12 @@ class Agent:
714
969
  approve_operator: bool = False,
715
970
  idem: Optional[IdemKey] = None,
716
971
  ) -> Dict[str, Any]:
717
- """Transfer agent ownership."""
972
+ """Transfer agent ownership.
973
+
974
+ Note: When an agent is transferred, the agentWallet is automatically reset
975
+ to the zero address on-chain. The new owner must call setAgentWallet() to
976
+ set a new wallet address with EIP-712 signature verification.
977
+ """
718
978
  if not self.registration_file.agentId:
719
979
  raise ValueError("Agent must be registered before transferring")
720
980
 
@@ -731,6 +991,11 @@ class Agent:
731
991
 
732
992
  receipt = self.sdk.web3_client.wait_for_transaction(txHash)
733
993
 
994
+ # Note: agentWallet will be reset to zero address by the contract
995
+ # Update local state to reflect this
996
+ self.registration_file.walletAddress = None
997
+ self._last_registered_wallet = None
998
+
734
999
  return {
735
1000
  "txHash": txHash,
736
1001
  "agentId": self.registration_file.agentId,
@@ -775,6 +1040,10 @@ class Agent:
775
1040
 
776
1041
  Only the current owner can transfer the agent.
777
1042
 
1043
+ Note: When an agent is transferred, the agentWallet is automatically reset
1044
+ to the zero address on-chain. The new owner must call setAgentWallet() to
1045
+ set a new wallet address with EIP-712 signature verification.
1046
+
778
1047
  Args:
779
1048
  newOwnerAddress: Ethereum address of the new owner
780
1049
 
@@ -832,6 +1101,11 @@ class Agent:
832
1101
 
833
1102
  logger.debug(f"Agent {self.registration_file.agentId} successfully transferred to {checksum_address}")
834
1103
 
1104
+ # Note: agentWallet will be reset to zero address by the contract
1105
+ # Update local state to reflect this
1106
+ self.registration_file.walletAddress = None
1107
+ self._last_registered_wallet = None
1108
+
835
1109
  return {"txHash": txHash, "from": currentOwner, "to": checksum_address, "agentId": self.registration_file.agentId}
836
1110
 
837
1111
  def activate(self, idem: Optional[IdemKey] = None) -> RegistrationFile: