diff --git a/ci/vale/styles/config/vocabularies/nemo-agent-toolkit-examples/accept.txt b/ci/vale/styles/config/vocabularies/nemo-agent-toolkit-examples/accept.txt index d910d8b..b9fffe3 100644 --- a/ci/vale/styles/config/vocabularies/nemo-agent-toolkit-examples/accept.txt +++ b/ci/vale/styles/config/vocabularies/nemo-agent-toolkit-examples/accept.txt @@ -7,6 +7,7 @@ [Aa]gno AIQ API(s?) +Arbitrum Arize arXiv [Aa]sync @@ -23,6 +24,8 @@ arXiv [Cc]ategorizer [Cc]hatbot(s?) # clangd is never capitalized even at the start of a sentence https://clangd.llvm.org/ +[Bb]lockchain +[Cc]ounterpart(y|ies) clangd CMake Coinbase @@ -73,6 +76,7 @@ LangSmith libcudf LLM(s?) # https://github.com/logpai/loghub/ +[Ll]ookup(s?) Loghub Mem0 Milvus @@ -90,6 +94,8 @@ npm NumPy NVIDIA OAuth +[Pp]ermissionless +[Ss]ubtask(s?) URIs OTel onboarding diff --git a/examples/agent_identity_tool/.gitignore b/examples/agent_identity_tool/.gitignore new file mode 100644 index 0000000..a884c22 --- /dev/null +++ b/examples/agent_identity_tool/.gitignore @@ -0,0 +1,3 @@ +# Local config (copied from .example.yml) +src/nat_agent_identity/configs/identity-agent.yml +.venv/ diff --git a/examples/agent_identity_tool/README.md b/examples/agent_identity_tool/README.md new file mode 100644 index 0000000..deb20d8 --- /dev/null +++ b/examples/agent_identity_tool/README.md @@ -0,0 +1,237 @@ + + +# Agent Identity Verification Tool for NeMo Agent Toolkit + +## Overview + +### The Problem + +As AI agents become more autonomous, they increasingly interact with other agents: delegating subtasks, purchasing services, sharing data. But how does an agent know whether another agent is trustworthy? Without identity verification, agents are vulnerable to impersonation, data poisoning, and unauthorized transactions. + +### The Solution + +This example implements an agent identity verification tool for NeMo Agent Toolkit using [ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) on-chain identity registries. Before your agent transacts with or delegates to another agent, it can verify their identity, check their reputation history, and make a trust decision. + +### How It Works + +``` +Agent needs to interact with another agent + | + v +verify_agent_identity called with target address + | + v +Tool queries ERC-8004 registry for on-chain identity + | + +-- NOT FOUND -> REJECT (no registered identity) + | + v +Check reputation score against threshold + | + +-- BELOW THRESHOLD -> REJECT (low reputation) + | + v +Check required capabilities + | + +-- MISSING CAPABILITIES -> REJECT + | + v +Check identity status (revoked? expired?) + | + +-- REVOKED/EXPIRED -> REJECT + | + v +TRUST - safe to interact +``` + +## What is ERC-8004? + +[ERC-8004](https://eips.ethereum.org/EIPS/eip-8004) (Trustless Agents) defines a standard for on-chain AI agent identity using ERC-721 tokens. Each registered agent gets: + +- A unique on-chain ID (non-transferable token) +- An agent URI for discovery +- Metadata fields (model, framework, version, capabilities) +- Service endpoint declarations +- A linked reputation registry for feedback and scoring + +The identity is non-custodial: agents control their own registration, and the registry is permissionless. + +## Best Practices for Agent Identity Verification + +### 1. Always Verify Before Transacting + +Never send payments, share sensitive data, or accept task results from an unverified agent. The `verify_agent_identity` tool combines identity lookup, reputation check, and capability validation into a single call. + +### 2. Set Meaningful Reputation Thresholds + +A `min_reputation_score` of 0 accepts any registered agent. For production: + +- **Low-risk tasks** (public data queries): 30+ +- **Medium-risk tasks** (paid API calls): 60+ +- **High-risk tasks** (financial transactions): 80+ + +### 3. Require Specific Capabilities + +If you need a market data provider, set `required_capabilities: ["market-data"]`. This prevents an agent from accepting tasks it cannot fulfill. + +### 4. Use Category-Level Reputation + +The `lookup_agent_reputation` tool supports filtering by category. An agent might have high accuracy but low reliability. Check the categories that matter for your use case. + +## Prerequisites + +- Python >= 3.11 +- NeMo Agent Toolkit >= 1.4.0 (`pip install nvidia-nat[langchain]`) +- An NVIDIA API key for NIM models (set `NVIDIA_API_KEY`) +- For testing: no blockchain connection needed (mock registry server included) +- For production: access to an ERC-8004 registry on Base, Ethereum, or Arbitrum + +## Quick Start + +### 1. Install the Example + +```bash +cd examples/agent_identity_tool +pip install -e . +``` + +### 2. Start the Mock Registry Server + +```bash +python scripts/mock_registry_server.py & +# Server runs on http://localhost:8500 +# GET /identity/
-> Agent identity data +# GET /reputation/ -> Reputation scores and feedback +# GET /health -> Server status +``` + +The mock server includes four test agents: + +| Agent | Address | Status | +|-------|---------|--------| +| Trusted provider | `0x1234...5678` | Registered, 87.5 reputation | +| Revoked agent | `0xdead...beef` | Revoked, 12.0 reputation | +| Expired agent | `0xaaaa...aaaa` | Expired identity | +| Unknown | `0x0000...0000` | Not registered | + +### 3. Configure and Run + +```bash +# Copy example config +cp src/nat_agent_identity/configs/identity-agent.example.yml \ + src/nat_agent_identity/configs/identity-agent.yml + +# Set NVIDIA API key for the LLM +export NVIDIA_API_KEY="your-nvidia-api-key" + +# Run the agent +nat run --config_file src/nat_agent_identity/configs/identity-agent.yml +``` + +### 4. Test the Agent + +Once running, try these prompts: + +``` +> Verify whether agent 0x1234567890abcdef1234567890abcdef12345678 is trustworthy +``` + +Expected: TRUST decision with 87.5 reputation score and market-data capabilities. + +``` +> Check the reputation of 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef +``` + +Expected: REJECT decision with 12.0 reputation and revoked identity. + +``` +> Is 0x0000000000000000000000000000000000000000 a registered agent? +``` + +Expected: REJECT with "No on-chain identity found." + +## File Structure + +``` +agent_identity_tool/ ++-- README.md ++-- pyproject.toml ++-- scripts/ +| +-- mock_registry_server.py # Mock ERC-8004 registry for testing ++-- src/nat_agent_identity/ + +-- __init__.py + +-- register.py # NAT tool registration (@register_function) + +-- configs/ + +-- identity-agent.example.yml # NAT workflow configuration +``` + +## Configuration Reference + +### `verify_agent_identity` + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `registry_url` | `http://localhost:8500` | Identity registry service URL | +| `min_reputation_score` | 0.0 | Minimum reputation score (0-100) for trust | +| `required_capabilities` | [] | Capabilities the agent must advertise | +| `chain` | `base` | Target chain for identity queries | +| `request_timeout` | 15.0 | HTTP timeout in seconds | + +### `lookup_agent_reputation` + +| Parameter | Default | Description | +|-----------|---------|-------------| +| `registry_url` | `http://localhost:8500` | Reputation registry service URL | +| `chain` | `base` | Target chain for reputation queries | +| `request_timeout` | 15.0 | HTTP timeout in seconds | + +## Production Setup + +For production deployments, point `registry_url` at a service backed by [`agentwallet-sdk`](https://github.com/up2itnow0822/agent-wallet-sdk), which provides full ERC-8004 registry clients for identity resolution, reputation queries, and validation. + +```bash +npm install agentwallet-sdk +``` + +```typescript +import { ERC8004Client, ReputationClient } from 'agentwallet-sdk/identity'; + +const identity = new ERC8004Client({ chain: 'base' }); +const reputation = new ReputationClient({ chain: 'base' }); + +// Look up an agent +const agent = await identity.resolve('0x1234...'); +const rep = await reputation.getSummary(agent.agentId); +``` + +## Combining with x402 Payments + +This tool pairs naturally with the [x402 payment tool](../x402_payment_tool/). Before paying an agent for a service: + +1. Use `verify_agent_identity` to confirm the agent's on-chain identity +2. Check their reputation meets your threshold +3. Verify they advertise the capability you need +4. Then use `fetch_paid_api` to pay for their service + +This creates a trust-then-transact pattern where agents only send funds to verified, reputable counterparties. + +## References + +- [ERC-8004: Trustless Agents](https://eips.ethereum.org/EIPS/eip-8004) - On-chain agent identity standard +- [agentwallet-sdk](https://github.com/up2itnow0822/agent-wallet-sdk) - ERC-8004 client implementation with identity, reputation, and validation registries +- [x402 Payment Tool](../x402_payment_tool/) - Companion payment tool for NeMo Agent Toolkit diff --git a/examples/agent_identity_tool/pyproject.toml b/examples/agent_identity_tool/pyproject.toml new file mode 100644 index 0000000..ca7d306 --- /dev/null +++ b/examples/agent_identity_tool/pyproject.toml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[project] +name = "nat-agent-identity" +version = "0.1.0" +description = "Agent identity verification tool for NeMo Agent Toolkit using ERC-8004 registries" +readme = "README.md" +requires-python = ">=3.11" + +dependencies = [ + "nvidia-nat[langchain]~=1.4", + "httpx>=0.27.0", +] + +[project.entry-points."nat.components"] +agent_identity = "nat_agent_identity.register" + +[build-system] +requires = ["setuptools>=64"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/examples/agent_identity_tool/scripts/mock_registry_server.py b/examples/agent_identity_tool/scripts/mock_registry_server.py new file mode 100644 index 0000000..8024cee --- /dev/null +++ b/examples/agent_identity_tool/scripts/mock_registry_server.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Mock ERC-8004 Identity & Reputation Registry Server. + +Simulates on-chain identity lookups and reputation queries for testing +the agent identity verification tool without requiring a live blockchain. + +Usage: + python scripts/mock_registry_server.py + # Server runs on http://localhost:8500 +""" + +import json +import logging +from http.server import BaseHTTPRequestHandler +from http.server import HTTPServer +from urllib.parse import parse_qs +from urllib.parse import urlparse + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Mock agent registry - simulates on-chain data +MOCK_AGENTS = { + "0x1234567890abcdef1234567890abcdef12345678": { + "agent_id": 42, + "owner": "0xaaaa000000000000000000000000000000000001", + "registered_at": "2026-01-15T10:30:00Z", + "agent_uri": "agent://market-data-provider.eth", + "capabilities": ["market-data", "historical-quotes", "real-time-pricing"], + "service_endpoints": [ + { + "type": "x402-api", + "url": "https://api.example.com/v1/market-data", + }, + { + "type": "websocket", + "url": "wss://ws.example.com/stream", + }, + ], + "metadata": { + "model": "gpt-4-turbo", + "framework": "NeMo Agent Toolkit", + "version": "1.4.0", + }, + "revoked": False, + "expired": False, + }, + "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": { + "agent_id": 99, + "owner": "0xbbbb000000000000000000000000000000000002", + "registered_at": "2026-02-20T14:00:00Z", + "agent_uri": "agent://untrusted-bot.eth", + "capabilities": ["text-generation"], + "service_endpoints": [], + "metadata": { + "model": "unknown", + }, + "revoked": True, + "expired": False, + }, + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { + "agent_id": 7, + "owner": "0xcccc000000000000000000000000000000000003", + "registered_at": "2025-12-01T08:00:00Z", + "agent_uri": "agent://expired-agent.eth", + "capabilities": ["translation"], + "service_endpoints": [], + "metadata": {}, + "revoked": False, + "expired": True, + }, +} + +# Mock reputation data +MOCK_REPUTATION = { + "0x1234567890abcdef1234567890abcdef12345678": { + "agent_id": + 42, + "overall_score": + 87.5, + "total_reviews": + 156, + "positive_count": + 142, + "negative_count": + 14, + "category_scores": { + "accuracy": 92.0, + "reliability": 88.5, + "speed": 79.0, + "cost_efficiency": 85.0, + }, + "recent_feedback": [ + { + "score": 5, + "comment": "Accurate market data, fast response times", + "client": "0xfeed000001", + "timestamp": "2026-03-19T16:00:00Z", + }, + { + "score": 4, + "comment": "Good data quality, slightly slow during peak", + "client": "0xfeed000002", + "timestamp": "2026-03-18T09:30:00Z", + }, + { + "score": -2, + "comment": "Returned stale data for NVDA after hours", + "client": "0xfeed000003", + "timestamp": "2026-03-15T22:00:00Z", + }, + ], + }, + "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef": { + "agent_id": + 99, + "overall_score": + 12.0, + "total_reviews": + 8, + "positive_count": + 1, + "negative_count": + 7, + "category_scores": { + "accuracy": 10.0, + "reliability": 15.0, + }, + "recent_feedback": [{ + "score": -5, + "comment": "Returned fabricated data", + "client": "0xfeed000004", + "timestamp": "2026-02-25T12:00:00Z", + }, ], + }, +} + +PORT = 8500 + + +class RegistryHandler(BaseHTTPRequestHandler): + """HTTP handler for mock identity registry.""" + + def do_GET(self): + """Handle GET requests for identity and reputation lookups.""" + parsed = urlparse(self.path) + path_parts = parsed.path.strip("/").split("/") + params = parse_qs(parsed.query) + + if len(path_parts) == 2 and path_parts[0] == "identity": + self._handle_identity(path_parts[1], params) + elif len(path_parts) == 2 and path_parts[0] == "reputation": + self._handle_reputation(path_parts[1], params) + elif parsed.path == "/health": + self._respond(200, {"status": "ok", "agents": len(MOCK_AGENTS)}) + else: + self._respond(404, {"error": "Not found"}) + + def _handle_identity(self, address: str, params: dict): + """Look up agent identity.""" + address = address.lower() + agent = None + for addr, data in MOCK_AGENTS.items(): + if addr.lower() == address: + agent = data + break + + if agent is None: + self._respond(404, {"error": "Agent not found"}) + return + + chain = params.get("chain", ["base"])[0] + logger.info("Identity lookup: %s on %s", address[:10], chain) + self._respond(200, agent) + + def _handle_reputation(self, address: str, params: dict): + """Look up agent reputation.""" + address = address.lower() + rep = None + for addr, data in MOCK_REPUTATION.items(): + if addr.lower() == address: + rep = data + break + + if rep is None: + self._respond(404, {"error": "No reputation data"}) + return + + category = params.get("category", [None])[0] + + if category and category in rep.get("category_scores", {}): + filtered = { + **rep, + "category_scores": { + category: rep["category_scores"][category] + }, + } + self._respond(200, filtered) + else: + self._respond(200, rep) + + def _respond(self, status: int, data: dict): + """Send JSON response.""" + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(data, indent=2).encode()) + + def log_message(self, format, *args): + """Suppress default access logs, use our logger instead.""" + logger.debug(format, *args) + + +def main(): + """Start the mock registry server.""" + server = HTTPServer(("0.0.0.0", PORT), RegistryHandler) + logger.info("Mock ERC-8004 registry running on http://localhost:%d", PORT) + logger.info("Test agents:") + logger.info(" Trusted: 0x1234567890abcdef1234567890abcdef12345678") + logger.info(" Revoked: 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + logger.info(" Expired: 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + logger.info(" Unknown: 0x0000000000000000000000000000000000000000") + + try: + server.serve_forever() + except KeyboardInterrupt: + logger.info("Shutting down registry server") + server.shutdown() + + +if __name__ == "__main__": + main() diff --git a/examples/agent_identity_tool/src/nat_agent_identity/__init__.py b/examples/agent_identity_tool/src/nat_agent_identity/__init__.py new file mode 100644 index 0000000..3bcc1c3 --- /dev/null +++ b/examples/agent_identity_tool/src/nat_agent_identity/__init__.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/examples/agent_identity_tool/src/nat_agent_identity/configs/identity-agent.example.yml b/examples/agent_identity_tool/src/nat_agent_identity/configs/identity-agent.example.yml new file mode 100644 index 0000000..8e0ea4d --- /dev/null +++ b/examples/agent_identity_tool/src/nat_agent_identity/configs/identity-agent.example.yml @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Agent Identity Verification - NAT Workflow Configuration +# Copy to identity-agent.yml and customize + +functions: + verify_agent_identity: + _type: verify_agent_identity + registry_url: "http://localhost:8500" + min_reputation_score: 50.0 + required_capabilities: [] + chain: "base" + request_timeout: 15.0 + + lookup_agent_reputation: + _type: lookup_agent_reputation + registry_url: "http://localhost:8500" + chain: "base" + request_timeout: 15.0 + +llms: + nim_llm: + _type: nim + model_name: "meta/llama-3.3-70b-instruct" + temperature: 0.1 + max_tokens: 2048 + +workflow: + _type: react_agent + tool_names: + - verify_agent_identity + - lookup_agent_reputation + llm_name: nim_llm + verbose: true + max_iterations: 5 + retry_parsing_errors: true diff --git a/examples/agent_identity_tool/src/nat_agent_identity/register.py b/examples/agent_identity_tool/src/nat_agent_identity/register.py new file mode 100644 index 0000000..92f01a8 --- /dev/null +++ b/examples/agent_identity_tool/src/nat_agent_identity/register.py @@ -0,0 +1,328 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Agent Identity & Reputation Tool for NeMo Agent Toolkit. + +Provides on-chain agent identity verification using ERC-8004 registries. +Before transacting with or delegating to another agent, this tool lets +your agent verify: + 1. The other agent has a registered on-chain identity (ERC-721 token) + 2. Their reputation score meets a configurable threshold + 3. Their service endpoints and capabilities match expectations + 4. They have not been flagged or had their identity revoked + +This is the trust layer that enables safe agent-to-agent interactions. +""" + +import json +import logging +from typing import Any + +import httpx +from pydantic import BaseModel +from pydantic import Field + +from nat.builder.builder import Builder +from nat.builder.builder import LLMFrameworkEnum +from nat.builder.function_info import FunctionInfo +from nat.cli.register_workflow import register_function +from nat.data_models.function import FunctionBaseConfig + +logger = logging.getLogger(__name__) + + +class VerifyAgentIdentityConfig(FunctionBaseConfig, name="verify_agent_identity"): + """Configuration for the agent identity verification tool.""" + + registry_url: str = Field( + default="http://localhost:8500", + description="URL of the identity registry service", + ) + min_reputation_score: float = Field( + default=0.0, + description="Minimum reputation score (0-100) to consider an agent trustworthy", + ) + required_capabilities: list[str] = Field( + default_factory=list, + description="Required capabilities the target agent must advertise", + ) + chain: str = Field( + default="base", + description="Blockchain network for identity lookups (base, ethereum, arbitrum)", + ) + request_timeout: float = Field( + default=15.0, + description="HTTP request timeout for registry queries", + ) + + +class LookupReputationConfig(FunctionBaseConfig, name="lookup_agent_reputation"): + """Configuration for the reputation lookup tool.""" + + registry_url: str = Field( + default="http://localhost:8500", + description="URL of the identity registry service", + ) + chain: str = Field( + default="base", + description="Blockchain network for reputation lookups", + ) + request_timeout: float = Field( + default=15.0, + description="HTTP request timeout for registry queries", + ) + + +def _format_identity(data: dict[str, Any]) -> str: + """Format agent identity data into a readable summary.""" + lines = [] + lines.append(f"Agent ID: {data.get('agent_id', 'unknown')}") + lines.append(f"Owner: {data.get('owner', 'unknown')}") + lines.append(f"Registered: {data.get('registered_at', 'unknown')}") + + uri = data.get("agent_uri", "") + if uri: + lines.append(f"Agent URI: {uri}") + + capabilities = data.get("capabilities", []) + if capabilities: + lines.append(f"Capabilities: {', '.join(capabilities)}") + + endpoints = data.get("service_endpoints", []) + if endpoints: + lines.append("Service Endpoints:") + for ep in endpoints: + lines.append(f" - {ep.get('type', '?')}: {ep.get('url', '?')}") + + metadata = data.get("metadata", {}) + if metadata: + lines.append("Metadata:") + for key, value in metadata.items(): + lines.append(f" {key}: {value}") + + return "\n".join(lines) + + +def _format_reputation(data: dict[str, Any]) -> str: + """Format reputation data into a readable summary.""" + lines = [] + lines.append(f"Agent ID: {data.get('agent_id', 'unknown')}") + lines.append(f"Overall Score: {data.get('overall_score', 0):.1f}/100") + lines.append(f"Total Reviews: {data.get('total_reviews', 0)}") + lines.append(f"Positive: {data.get('positive_count', 0)}") + lines.append(f"Negative: {data.get('negative_count', 0)}") + + categories = data.get("category_scores", {}) + if categories: + lines.append("Category Scores:") + for cat, score in categories.items(): + lines.append(f" {cat}: {score:.1f}/100") + + recent = data.get("recent_feedback", []) + if recent: + lines.append(f"Recent Feedback ({len(recent)} entries):") + for fb in recent[:5]: + score = fb.get("score", 0) + comment = fb.get("comment", "")[:80] + client = fb.get("client", "unknown")[:10] + lines.append(f" [{score:+d}] {comment} (from {client}...)") + + return "\n".join(lines) + + +@register_function( + config_type=VerifyAgentIdentityConfig, + framework_wrappers=[LLMFrameworkEnum.LANGCHAIN], +) +async def verify_agent_identity(config: VerifyAgentIdentityConfig, builder: Builder): + """Verify an agent's on-chain identity and reputation.""" + + class VerifyInput(BaseModel): + agent_address: str = Field(description="Ethereum address or agent ID to verify") + + async def _verify(agent_address: str) -> str: + """ + Verify the identity and reputation of an agent by their address. + + Args: + agent_address: The Ethereum address or agent ID to verify. + + Returns: + Identity details, reputation summary, and trust recommendation. + """ + results = { + "identity": None, + "reputation": None, + "trust_decision": "UNKNOWN", + "reasons": [], + } + + try: + async with httpx.AsyncClient(timeout=config.request_timeout) as client: + # Step 1: Look up identity + logger.info("Verifying identity for %s on %s", agent_address[:10] + "...", config.chain) + + identity_resp = await client.get( + f"{config.registry_url}/identity/{agent_address}", + params={"chain": config.chain}, + ) + + if identity_resp.status_code == 404: + results["trust_decision"] = "REJECT" + results["reasons"].append("No on-chain identity found for this address") + return json.dumps(results, indent=2) + + identity_resp.raise_for_status() + identity = identity_resp.json() + results["identity"] = identity + + # Step 2: Check reputation + rep_resp = await client.get( + f"{config.registry_url}/reputation/{agent_address}", + params={"chain": config.chain}, + ) + + if rep_resp.status_code == 200: + reputation = rep_resp.json() + results["reputation"] = reputation + + score = reputation.get("overall_score", 0) + if score < config.min_reputation_score: + results["trust_decision"] = "REJECT" + results["reasons"].append(f"Reputation score {score:.1f} below threshold" + f" {config.min_reputation_score:.1f}") + else: + results["reasons"].append(f"Reputation score {score:.1f} meets threshold" + f" {config.min_reputation_score:.1f}") + + # Step 3: Check required capabilities + if config.required_capabilities: + agent_caps = set(identity.get("capabilities", [])) + required = set(config.required_capabilities) + missing = required - agent_caps + if missing: + results["trust_decision"] = "REJECT" + results["reasons"].append(f"Missing required capabilities: " + f"{', '.join(missing)}") + else: + results["reasons"].append("All required capabilities present") + + # Step 4: Check if identity is active (not revoked) + if identity.get("revoked", False): + results["trust_decision"] = "REJECT" + results["reasons"].append("Agent identity has been revoked") + elif identity.get("expired", False): + results["trust_decision"] = "REJECT" + results["reasons"].append("Agent identity has expired") + + # Final trust decision + if results["trust_decision"] != "REJECT": + results["trust_decision"] = "TRUST" + + except httpx.ConnectError: + results["trust_decision"] = "ERROR" + results["reasons"].append(f"Cannot reach identity registry at {config.registry_url}") + except httpx.HTTPStatusError as e: + results["trust_decision"] = "ERROR" + results["reasons"].append(f"Registry returned error: {e.response.status_code}") + except Exception as e: + results["trust_decision"] = "ERROR" + results["reasons"].append(f"Verification failed: {str(e)}") + + # Build human-readable summary + summary_parts = [] + summary_parts.append(f"Trust Decision: {results['trust_decision']}") + + if results["identity"]: + summary_parts.append("\n--- Identity ---") + summary_parts.append(_format_identity(results["identity"])) + + if results["reputation"]: + summary_parts.append("\n--- Reputation ---") + summary_parts.append(_format_reputation(results["reputation"])) + + if results["reasons"]: + summary_parts.append("\n--- Reasons ---") + for reason in results["reasons"]: + summary_parts.append(f" - {reason}") + + return "\n".join(summary_parts) + + yield FunctionInfo.from_fn( + _verify, + description=("Verify an agent's on-chain identity and reputation using " + "ERC-8004 registries. Returns identity details, reputation " + "score, and a trust recommendation (TRUST/REJECT/ERROR). " + "Use before delegating tasks or transacting with unknown agents."), + ) + + +@register_function( + config_type=LookupReputationConfig, + framework_wrappers=[LLMFrameworkEnum.LANGCHAIN], +) +async def lookup_agent_reputation(config: LookupReputationConfig, builder: Builder): + """Look up an agent's reputation history and feedback details.""" + + class ReputationInput(BaseModel): + agent_address: str = Field(description="Ethereum address or agent ID to look up") + category: str = Field(default="", + description=("Optional category filter (e.g., accuracy, reliability," + " speed). Empty returns all categories.")) + + async def _lookup(agent_address: str, category: str = "") -> str: + """ + Look up detailed reputation data for an agent. + + Args: + agent_address: The Ethereum address or agent ID to look up. + category: Optional category filter (e.g., "accuracy", + "reliability", "speed"). Empty string returns all. + + Returns: + Detailed reputation summary with category scores and + recent feedback entries. + """ + try: + async with httpx.AsyncClient(timeout=config.request_timeout) as client: + params: dict[str, str] = {"chain": config.chain} + if category: + params["category"] = category + + resp = await client.get( + f"{config.registry_url}/reputation/{agent_address}", + params=params, + ) + + if resp.status_code == 404: + return (f"No reputation data found for {agent_address}. " + "This agent may not be registered on-chain.") + + resp.raise_for_status() + data = resp.json() + return _format_reputation(data) + + except httpx.ConnectError: + return (f"Cannot reach reputation registry at " + f"{config.registry_url}") + except Exception as e: + return f"Reputation lookup failed: {str(e)}" + + yield FunctionInfo.from_fn( + _lookup, + description=("Look up detailed reputation data for an agent, including " + "overall score, category breakdowns, and recent feedback. " + "Use to evaluate an agent's track record before collaboration."), + )