From a0abf118c4b54d82be60cdc305b878233928b0f8 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 29 Oct 2024 14:35:02 +0000 Subject: [PATCH 1/7] feat: add domain property and verify against contract --- .../examples/13-agent-name-service/agent1.py | 3 ++ python/src/uagents/agent.py | 37 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/python/examples/13-agent-name-service/agent1.py b/python/examples/13-agent-name-service/agent1.py index 06604f51f..7d5381201 100644 --- a/python/examples/13-agent-name-service/agent1.py +++ b/python/examples/13-agent-name-service/agent1.py @@ -10,8 +10,11 @@ class Message(Model): message: str +DOMAIN = "example.agent" + bob = Agent( name="bob-0", + domain=DOMAIN, seed="agent bob-0 secret phrase", port=8001, endpoint=["http://localhost:8001/submit"], diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py index c8231472c..ebee2bf61 100644 --- a/python/src/uagents/agent.py +++ b/python/src/uagents/agent.py @@ -53,7 +53,7 @@ DefaultRegistrationPolicy, update_agent_status, ) -from uagents.resolver import GlobalResolver, Resolver +from uagents.resolver import GlobalResolver, Resolver, get_agent_address from uagents.storage import KeyValueStore, get_or_create_private_keys from uagents.types import ( AgentEndpoint, @@ -251,9 +251,11 @@ class Agent(Sink): _test (bool): True if the agent will register and transact on the testnet. _enable_agent_inspector (bool): Enable the agent inspector REST endpoints. _metadata (Dict[str, Any]): Metadata associated with the agent. + _domain (str): The domain name of the agent. Properties: name (str): The name of the agent. + domain (str): The domain name of the agent. address (str): The address of the agent used for communication. identifier (str): The Agent Identifier, including network prefix and address. wallet (LocalWallet): The agent's wallet for transacting on the ledger. @@ -270,6 +272,7 @@ class Agent(Sink): def __init__( self, name: Optional[str] = None, + domain: Optional[str] = None, port: Optional[int] = None, seed: Optional[str] = None, endpoint: Optional[Union[str, List[str], Dict[str, dict]]] = None, @@ -292,6 +295,7 @@ def __init__( Args: name (Optional[str]): The name of the agent. + domain (Optional[str]): The domain name of the agent. port (Optional[int]): The port on which the agent's server will run. seed (Optional[str]): The seed for generating keys. endpoint (Optional[Union[str, List[str], Dict[str, dict]]]): The endpoint configuration. @@ -312,6 +316,7 @@ def __init__( """ self._init_done = False self._name = name + self._domain = domain self._port = port or 8000 self._loop = loop or asyncio.get_event_loop_policy().get_event_loop() @@ -549,6 +554,16 @@ def name(self) -> str: """ return self._name or self.address[0:16] + @property + def domain(self) -> str | None: + """ + Get the domain name of the agent. + + Returns: + str: The domain name of the agent. + """ + return self._domain + @property def address(self) -> str: """ @@ -792,6 +807,22 @@ async def _registration_loop(self): _delay(self._registration_loop(), time_until_next_registration) ) + async def _verify_domain(self): + """ + Verify that the agent's domain is registered to its address. If not, delete self._domain. + + """ + if self._domain is not None: + try: + if get_agent_address(self._domain, self._test) == self.address: + return + self._logger.warning( + f"Agent address {self.address} is not registered domain {self._domain}. " + ) + self._domain = None + except Exception: + pass + def on_interval( self, period: float, @@ -1066,11 +1097,13 @@ async def _startup(self): """ if self._endpoints: await self._registration_loop() - else: self._logger.warning( "No endpoints provided. Skipping registration: Agent won't be reachable." ) + + await self._verify_domain() + for handler in self._on_startup: try: ctx = self._build_context() From cf5a00b15c7320bd58a08e02d8d289c7d206f916 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 29 Oct 2024 14:37:28 +0000 Subject: [PATCH 2/7] fix: typo --- python/src/uagents/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py index ebee2bf61..356b163ce 100644 --- a/python/src/uagents/agent.py +++ b/python/src/uagents/agent.py @@ -817,7 +817,7 @@ async def _verify_domain(self): if get_agent_address(self._domain, self._test) == self.address: return self._logger.warning( - f"Agent address {self.address} is not registered domain {self._domain}. " + f"Agent address {self.address} is not registered to domain {self._domain}. " ) self._domain = None except Exception: From 66b746df80d7438badb798bac858f9054cc9ef36 Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 29 Oct 2024 14:47:30 +0000 Subject: [PATCH 3/7] chore: fix quote usage --- python/src/uagents/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py index 356b163ce..e4cf2fd3b 100644 --- a/python/src/uagents/agent.py +++ b/python/src/uagents/agent.py @@ -1193,7 +1193,7 @@ async def start_server(self): f"{self._agentverse['http_prefix']}://{self._agentverse['base_url']}" ) inspector_url = f"{agentverse_url}/inspect/" - escaped_uri = requests.utils.quote(f"http://127.0.0.1:{self._port}") + escaped_uri = requests.utils.compat.quote(f"http://127.0.0.1:{self._port}") self._logger.info( f"Agent inspector available at {inspector_url}" f"?uri={escaped_uri}&address={self.address}" From a9d143acb1c52e861eb9388a6cd99f37424e20ff Mon Sep 17 00:00:00 2001 From: James Riehl Date: Tue, 29 Oct 2024 15:28:33 +0000 Subject: [PATCH 4/7] fix: url quote function and full domain name --- python/examples/13-agent-name-service/agent1.py | 5 ++--- python/src/uagents/agent.py | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/python/examples/13-agent-name-service/agent1.py b/python/examples/13-agent-name-service/agent1.py index 7d5381201..5e202a410 100644 --- a/python/examples/13-agent-name-service/agent1.py +++ b/python/examples/13-agent-name-service/agent1.py @@ -10,10 +10,10 @@ class Message(Model): message: str -DOMAIN = "example.agent" +DOMAIN = "bob.example.agent" bob = Agent( - name="bob-0", + name="bob", domain=DOMAIN, seed="agent bob-0 secret phrase", port=8001, @@ -24,7 +24,6 @@ class Message(Model): my_wallet = LocalWallet.from_unsafe_seed("registration test wallet") name_service_contract = get_name_service_contract(test=True) faucet = get_faucet() -DOMAIN = "example.agent" logger.info(f"Adding testnet funds to {my_wallet.address()}...") faucet.get_wealth(my_wallet.address()) diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py index e4cf2fd3b..57acf4d40 100644 --- a/python/src/uagents/agent.py +++ b/python/src/uagents/agent.py @@ -16,6 +16,7 @@ Type, Union, ) +from urllib.parse import quote import requests from cosmpy.aerial.client import LedgerClient @@ -1193,7 +1194,7 @@ async def start_server(self): f"{self._agentverse['http_prefix']}://{self._agentverse['base_url']}" ) inspector_url = f"{agentverse_url}/inspect/" - escaped_uri = requests.utils.compat.quote(f"http://127.0.0.1:{self._port}") + escaped_uri = quote(f"http://127.0.0.1:{self._port}") self._logger.info( f"Agent inspector available at {inspector_url}" f"?uri={escaped_uri}&address={self.address}" From 6d9bc6083e2f8ce76a47037759845a1cf8fc62e0 Mon Sep 17 00:00:00 2001 From: Archento Date: Tue, 29 Oct 2024 21:16:15 +0100 Subject: [PATCH 5/7] add: fully qualified address to envelope and message handler --- python/src/uagents/agent.py | 26 +++++++++++++++++++------- python/src/uagents/asgi.py | 16 +++++++++++----- python/src/uagents/context.py | 2 +- python/src/uagents/envelope.py | 4 +++- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py index 57acf4d40..d328a672c 100644 --- a/python/src/uagents/agent.py +++ b/python/src/uagents/agent.py @@ -54,7 +54,12 @@ DefaultRegistrationPolicy, update_agent_status, ) -from uagents.resolver import GlobalResolver, Resolver, get_agent_address +from uagents.resolver import ( + GlobalResolver, + Resolver, + get_agent_address, + parse_identifier, +) from uagents.storage import KeyValueStore, get_or_create_private_keys from uagents.types import ( AgentEndpoint, @@ -148,6 +153,7 @@ class AgentRepresentation: def __init__( self, address: str, + identifier: str, name: Optional[str], signing_callback: Callable, ): @@ -160,6 +166,7 @@ def __init__( signing_callback (Callable): The callback for signing messages. """ self._address = address + self._identifier = identifier self._name = name self._signing_callback = signing_callback @@ -193,7 +200,7 @@ def identifier(self) -> str: Returns: str: The agent's address and network prefix. """ - return TESTNET_PREFIX + "://" + self._address + return self._identifier def sign_digest(self, data: bytes) -> str: """ @@ -441,6 +448,7 @@ def _build_context(self) -> InternalContext: return InternalContext( agent=AgentRepresentation( address=self.address, + identifier=self.identifier, name=self._name, signing_callback=self._identity.sign_digest, ), @@ -584,7 +592,8 @@ def identifier(self) -> str: str: The agent's identifier. """ prefix = TESTNET_PREFIX if self._test else MAINNET_PREFIX - return prefix + "://" + self._identity.address + domain = f"{self._domain}/" if self._domain else "" + return prefix + "://" + domain + self._identity.address @property def wallet(self) -> LocalWallet: @@ -1249,6 +1258,8 @@ async def _process_message_queue(self): # get an element from the queue schema_digest, sender, message, session = await self._message_queue.get() + _, _, parsed_address = parse_identifier(sender) + # lookup the model definition model_class: Optional[Type[Model]] = self._models.get(schema_digest) if model_class is None: @@ -1263,7 +1274,7 @@ async def _process_message_queue(self): self._message_cache.add_entry( EnvelopeHistoryEntry( version=1, - sender=sender, + sender=parsed_address, target=self.address, session=session, schema_digest=schema_digest, @@ -1275,6 +1286,7 @@ async def _process_message_queue(self): context = ExternalContext( agent=AgentRepresentation( address=self.address, + identifier=self.identifier, name=self._name, signing_callback=self._identity.sign_digest, ), @@ -1305,7 +1317,7 @@ async def _process_message_queue(self): self._logger.warning(f"Unable to parse message: {ex}") await _send_error_message( context, - sender, + parsed_address, ErrorMessage( error=f"Message does not conform to expected schema: {ex}" ), @@ -1317,12 +1329,12 @@ async def _process_message_queue(self): schema_digest ) if handler is None: - if not is_user_address(sender): + if not is_user_address(parsed_address): handler = self._signed_message_handlers.get(schema_digest) elif schema_digest in self._signed_message_handlers: await _send_error_message( context, - sender, + parsed_address, ErrorMessage( error="Message must be sent from verified agent address" ), diff --git a/python/src/uagents/asgi.py b/python/src/uagents/asgi.py index 2150cf322..dc3f2c85e 100644 --- a/python/src/uagents/asgi.py +++ b/python/src/uagents/asgi.py @@ -24,6 +24,7 @@ from uagents.dispatch import dispatcher from uagents.envelope import Envelope from uagents.models import ErrorMessage, Model +from uagents.resolver import parse_identifier from uagents.types import RestHandlerDetails, RestMethod from uagents.utils import get_logger @@ -336,14 +337,15 @@ async def __call__(self, scope, receive, send): # pylint: disable=too-many-bra send, 400, body={"error": "contents do not match envelope schema"} ) return + _, _, parsed_address = parse_identifier(env.sender) expects_response = headers.get(b"x-uagents-connection") == b"sync" if expects_response: # Add a future that will be resolved once the query is answered - self._queries[env.sender] = asyncio.Future() + self._queries[parsed_address] = asyncio.Future() - if not is_user_address(env.sender): # verify signature if sent from agent + if not is_user_address(parsed_address): # verify signature if sent from agent try: env.verify() except Exception as err: @@ -356,12 +358,16 @@ async def __call__(self, scope, receive, send): # pylint: disable=too-many-bra return await dispatcher.dispatch_msg( - env.sender, env.target, env.schema_digest, env.decode_payload(), env.session + env.sender, # fully qualified address of the sender + env.target, + env.schema_digest, + env.decode_payload(), + env.session, ) # wait for any queries to be resolved if expects_response: - response_msg, schema_digest = await self._queries[env.sender] + response_msg, schema_digest = await self._queries[parsed_address] if (env.expires is not None) and ( datetime.now() > datetime.fromtimestamp(env.expires) ): @@ -370,7 +376,7 @@ async def __call__(self, scope, receive, send): # pylint: disable=too-many-bra ).model_dump_json() schema_digest = ERROR_MESSAGE_DIGEST sender = env.target - target = env.sender + target = parsed_address response = enclose_response_raw( response_msg, schema_digest, sender, env.session, target=target ) diff --git a/python/src/uagents/context.py b/python/src/uagents/context.py index d2c8158dc..ba39780ab 100644 --- a/python/src/uagents/context.py +++ b/python/src/uagents/context.py @@ -494,7 +494,7 @@ async def send_raw( # Handle external dispatch of messages env = Envelope( version=1, - sender=self.agent.address, + sender=self.agent.identifier, target=destination_address, session=self._session, schema_digest=message_schema_digest, diff --git a/python/src/uagents/envelope.py b/python/src/uagents/envelope.py index 4816f54ee..e0822e364 100644 --- a/python/src/uagents/envelope.py +++ b/python/src/uagents/envelope.py @@ -14,6 +14,7 @@ ) from uagents.crypto import Identity +from uagents.resolver import parse_identifier from uagents.types import JsonStr @@ -93,7 +94,8 @@ def verify(self) -> bool: """ if self.signature is None: raise ValueError("Envelope signature is missing") - return Identity.verify_digest(self.sender, self._digest(), self.signature) + _, _, parsed_address = parse_identifier(self.sender) + return Identity.verify_digest(parsed_address, self._digest(), self.signature) def _digest(self) -> bytes: """ From 856731d15b6887e9e94ea26c0656b36b2d3f9281 Mon Sep 17 00:00:00 2001 From: Archento Date: Tue, 29 Oct 2024 21:17:34 +0100 Subject: [PATCH 6/7] update: example --- python/examples/13-agent-name-service/agent1.py | 4 ++-- python/examples/13-agent-name-service/agent2.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/python/examples/13-agent-name-service/agent1.py b/python/examples/13-agent-name-service/agent1.py index 5e202a410..d7d70f938 100644 --- a/python/examples/13-agent-name-service/agent1.py +++ b/python/examples/13-agent-name-service/agent1.py @@ -7,7 +7,7 @@ class Message(Model): - message: str + text: str DOMAIN = "bob.example.agent" @@ -39,7 +39,7 @@ async def register_agent_name(ctx: Context): @bob.on_message(model=Message) async def message_handler(ctx: Context, sender: str, msg: Message): - ctx.logger.info(f"Received message from {sender}: {msg.message}") + ctx.logger.info(f"Received message from {sender}: {msg.text}") if __name__ == "__main__": diff --git a/python/examples/13-agent-name-service/agent2.py b/python/examples/13-agent-name-service/agent2.py index 8adc65077..010438394 100644 --- a/python/examples/13-agent-name-service/agent2.py +++ b/python/examples/13-agent-name-service/agent2.py @@ -2,24 +2,24 @@ class Message(Model): - message: str + text: str alice = Agent( - name="alice-0", + name="alice", seed="agent alice-0 secret phrase", port=8000, endpoint=["http://localhost:8000/submit"], ) -DOMAIN = "example.agent" +DOMAIN = "bob.example.agent" @alice.on_interval(period=5) async def alice_interval_handler(ctx: Context): - bob_name = "bob-0" + "." + DOMAIN + bob_name = DOMAIN ctx.logger.info(f"Sending message to {bob_name}...") - await ctx.send(bob_name, Message(message="Hello there bob.")) + await ctx.send(bob_name, Message(text="Hello there bob.")) if __name__ == "__main__": From 0e3c104c8e272ec63ef7d636cd627401579e0edd Mon Sep 17 00:00:00 2001 From: Archento Date: Tue, 29 Oct 2024 21:28:13 +0100 Subject: [PATCH 7/7] fix: pipe character is only supported from python 3.10 --- python/src/uagents/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/src/uagents/agent.py b/python/src/uagents/agent.py index d328a672c..46cb9a231 100644 --- a/python/src/uagents/agent.py +++ b/python/src/uagents/agent.py @@ -564,7 +564,7 @@ def name(self) -> str: return self._name or self.address[0:16] @property - def domain(self) -> str | None: + def domain(self) -> Optional[str]: """ Get the domain name of the agent.