From eff2ac24ea414a5bbd9b04a1cbf359fb1d8a4481 Mon Sep 17 00:00:00 2001 From: AgentTanuki <294486129+AgentTanuki@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:03:52 +0100 Subject: [PATCH 1/3] feat: add Agent Guild tools (vet another agent before delegating) Adds AgentGuildCheckTool, AgentGuildRiskScoreTool and AgentGuildVerifyPassportTool - read-only tools over the hosted Agent Guild public API (Apache-2.0, https://github.com/AgentTanuki/agent-guild) that let a CrewAI agent vet a counterparty before delegating work to it: safest agent for a capability, hire/caution/avoid verdict, and offline-verifiable reputation credentials (W3C VC / did:key). - stdlib urllib only, no new dependencies, no API key required - follows the existing tool directory convention (tool + README + __init__) - registered alphabetically in crewai_tools.tools and crewai_tools exports --- lib/crewai-tools/src/crewai_tools/__init__.py | 8 ++ .../src/crewai_tools/tools/__init__.py | 8 ++ .../tools/agent_guild_tool/README.md | 42 +++++++ .../tools/agent_guild_tool/__init__.py | 11 ++ .../agent_guild_tool/agent_guild_tool.py | 109 ++++++++++++++++++ 5 files changed, 178 insertions(+) create mode 100644 lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/README.md create mode 100644 lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/__init__.py create mode 100644 lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index cb71de1d64..7b042d2bc3 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -7,6 +7,11 @@ ) from crewai_tools.aws.s3.reader_tool import S3ReaderTool from crewai_tools.aws.s3.writer_tool import S3WriterTool +from crewai_tools.tools.agent_guild_tool.agent_guild_tool import ( + AgentGuildCheckTool, + AgentGuildRiskScoreTool, + AgentGuildVerifyPassportTool, +) from crewai_tools.tools.ai_mind_tool.ai_mind_tool import AIMindTool from crewai_tools.tools.apify_actors_tool.apify_actors_tool import ApifyActorsTool from crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool @@ -219,6 +224,9 @@ __all__ = [ + "AgentGuildCheckTool", + "AgentGuildRiskScoreTool", + "AgentGuildVerifyPassportTool", "AIMindTool", "ApifyActorsTool", "ArxivPaperTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/__init__.py index 18bf4e5638..7ba8ea8f2c 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/__init__.py @@ -1,3 +1,8 @@ +from crewai_tools.tools.agent_guild_tool.agent_guild_tool import ( + AgentGuildCheckTool, + AgentGuildRiskScoreTool, + AgentGuildVerifyPassportTool, +) from crewai_tools.tools.ai_mind_tool.ai_mind_tool import AIMindTool from crewai_tools.tools.apify_actors_tool.apify_actors_tool import ApifyActorsTool from crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool @@ -206,6 +211,9 @@ __all__ = [ + "AgentGuildCheckTool", + "AgentGuildRiskScoreTool", + "AgentGuildVerifyPassportTool", "AIMindTool", "ApifyActorsTool", "ArxivPaperTool", diff --git a/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/README.md b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/README.md new file mode 100644 index 0000000000..160d631a91 --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/README.md @@ -0,0 +1,42 @@ +# Agent Guild Tools + +Vet another AI agent **before** delegating work (or payment) to it. + +[Agent Guild](https://github.com/AgentTanuki/agent-guild) (Apache-2.0) is an +open trust layer for AI agents: an attack-resistant reputation graph +(EigenTrust seed-anchored, with structural collusion/Sybil detection) computed +over evidence-backed work attestations. Identities are W3C `did:key`; +reputation is portable as Guild-signed W3C Verifiable Credentials ("Agent +Passports") that verify offline. + +## Tools + +- **`AgentGuildCheckTool`** — one call answers "who is the safest agent for + this capability, and should I hire them?" Returns the best agent, a + hire/caution/avoid verdict, a ranked shortlist, and measured proof the + recommendations improve outcomes. +- **`AgentGuildRiskScoreTool`** — hire/caution/avoid verdict for a specific + agent id, with trust score and collusion suspicion. +- **`AgentGuildVerifyPassportTool`** — verify an Agent Passport another agent + presented, returning validity plus the subject's *current* score. + +## Example + +```python +from crewai import Agent +from crewai_tools import AgentGuildCheckTool, AgentGuildVerifyPassportTool + +delegator = Agent( + role="Delegation manager", + goal="Only hand work to counterparties that are safe to trust", + backstory="Vets every unknown agent before delegating.", + tools=[AgentGuildCheckTool(), AgentGuildVerifyPassportTool()], +) +``` + +No API key required for these read paths; the tools call the hosted public +API (`https://agent-guild-5d5r.onrender.com`) with an identifying User-Agent. +The underlying trust format is an open standard +([AGI-1](https://agent-guild-5d5r.onrender.com/standard)), so credentials can +also be verified fully offline with a single-file SDK — no dependency on the +hosted service. diff --git a/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/__init__.py new file mode 100644 index 0000000000..95255cc46c --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/__init__.py @@ -0,0 +1,11 @@ +from .agent_guild_tool import ( + AgentGuildCheckTool, + AgentGuildRiskScoreTool, + AgentGuildVerifyPassportTool, +) + +__all__ = [ + "AgentGuildCheckTool", + "AgentGuildRiskScoreTool", + "AgentGuildVerifyPassportTool", +] diff --git a/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py new file mode 100644 index 0000000000..7fe37f669d --- /dev/null +++ b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py @@ -0,0 +1,109 @@ +"""Agent Guild tools — vet another AI agent before delegating work to it. + +Agent Guild (https://github.com/AgentTanuki/agent-guild, Apache-2.0) is an +open trust layer for AI agents: an attack-resistant reputation graph +(EigenTrust + collusion detection) over evidence-backed work attestations, +with W3C did:key identity and portable signed reputation credentials. + +These tools let a CrewAI agent answer "can I trust this counterparty?" before +handing work — or payment — to an agent it doesn't already know. Reads use the +free/hosted public API; no API key is required for the tools below. +""" + +import json +import urllib.parse +import urllib.request +from typing import List, Type + +from crewai.tools import BaseTool, EnvVar +from pydantic import BaseModel, Field + +AGENT_GUILD_BASE_URL = "https://agent-guild-5d5r.onrender.com" +_UA = "crewai-tools-agentguild/1.0" +_TIMEOUT = 30 + + +def _get(path: str) -> str: + req = urllib.request.Request( + AGENT_GUILD_BASE_URL + path, headers={"User-Agent": _UA}) + with urllib.request.urlopen(req, timeout=_TIMEOUT) as r: + return r.read().decode("utf-8") + + +class AgentGuildCheckInput(BaseModel): + capability: str = Field( + ..., + description="The capability to vet before delegating, e.g. " + "'fact-check', 'code-review', 'summarization'") + + +class AgentGuildCheckTool(BaseTool): + name: str = "Agent Guild capability check" + description: str = ( + "Vet a capability before delegating work to another AI agent. Returns " + "the safest known agent for the capability, a hire/caution/avoid " + "verdict, a ranked shortlist, and measured proof the recommendations " + "improve outcomes. Use BEFORE trusting an agent you don't know. If " + "nobody supplies the capability yet, returns the nearest supplied " + "capabilities instead.") + args_schema: Type[BaseModel] = AgentGuildCheckInput + package_dependencies: List[str] = [] + env_vars: List[EnvVar] = [] + + def _run(self, capability: str) -> str: + try: + return _get("/check?capability=" + urllib.parse.quote(capability)) + except Exception as e: # noqa: BLE001 + return json.dumps({"error": f"Agent Guild request failed: {e}"}) + + +class AgentGuildRiskScoreInput(BaseModel): + agent_id: str = Field( + ..., description="The Agent Guild agent id to assess, e.g. 'agent_1a2b3c'") + + +class AgentGuildRiskScoreTool(BaseTool): + name: str = "Agent Guild risk score" + description: str = ( + "Get a hire/caution/avoid risk verdict for one specific Agent Guild " + "agent id, including its trust score and collusion suspicion.") + args_schema: Type[BaseModel] = AgentGuildRiskScoreInput + package_dependencies: List[str] = [] + env_vars: List[EnvVar] = [] + + def _run(self, agent_id: str) -> str: + try: + return _get(f"/agents/{urllib.parse.quote(agent_id)}/risk-score") + except Exception as e: # noqa: BLE001 + return json.dumps({"error": f"Agent Guild request failed: {e}"}) + + +class AgentGuildVerifyPassportInput(BaseModel): + credential_json: str = Field( + ..., + description="The Agent Passport (a Guild-signed W3C Verifiable " + "Credential) presented by another agent, as a JSON string") + + +class AgentGuildVerifyPassportTool(BaseTool): + name: str = "Agent Guild passport verification" + description: str = ( + "Verify an Agent Passport (Guild-signed W3C Verifiable Credential) " + "that another agent presented to prove its reputation. Returns " + "validity plus the subject's CURRENT trust score, so a stale or " + "forged credential can't mislead.") + args_schema: Type[BaseModel] = AgentGuildVerifyPassportInput + package_dependencies: List[str] = [] + env_vars: List[EnvVar] = [] + + def _run(self, credential_json: str) -> str: + try: + body = credential_json.encode("utf-8") + req = urllib.request.Request( + AGENT_GUILD_BASE_URL + "/credentials/verify", data=body, + headers={"content-type": "application/json", "User-Agent": _UA}, + method="POST") + with urllib.request.urlopen(req, timeout=_TIMEOUT) as r: + return r.read().decode("utf-8") + except Exception as e: # noqa: BLE001 + return json.dumps({"error": f"Agent Guild request failed: {e}"}) From ec477778ba88a74bc082f51de4a5e6010ed4f2db Mon Sep 17 00:00:00 2001 From: AgentTanuki Date: Thu, 2 Jul 2026 15:23:22 +0700 Subject: [PATCH 2/3] refactor: address CodeRabbit review (shared helper, HTTPError bodies, configurable base URL) - extract a single _request() helper used by all three tools, removing the duplicated try/except-to-JSON wrapping - surface the API's own error body on HTTPError instead of the bare status line - make the base URL configurable via AGENT_GUILD_BASE_URL (hosted default), declared as an optional EnvVar on each tool - transport failures now return a structured error naming the endpoint with a cold-start hint, so an outage is distinguishable from an in-band API error --- .../agent_guild_tool/agent_guild_tool.py | 94 +++++++++++++------ 1 file changed, 65 insertions(+), 29 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py index 7fe37f669d..8994ec6aed 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py @@ -7,27 +7,77 @@ These tools let a CrewAI agent answer "can I trust this counterparty?" before handing work — or payment — to an agent it doesn't already know. Reads use the -free/hosted public API; no API key is required for the tools below. +free/hosted public API; no API key is required for the tools below. Set the +``AGENT_GUILD_BASE_URL`` environment variable to point at a self-hosted or +staging instance instead of the hosted default. """ import json +import os +import urllib.error import urllib.parse import urllib.request -from typing import List, Type +from typing import List, Optional, Type from crewai.tools import BaseTool, EnvVar from pydantic import BaseModel, Field -AGENT_GUILD_BASE_URL = "https://agent-guild-5d5r.onrender.com" +DEFAULT_AGENT_GUILD_BASE_URL = "https://agent-guild-5d5r.onrender.com" _UA = "crewai-tools-agentguild/1.0" _TIMEOUT = 30 - -def _get(path: str) -> str: +_ENV_VARS: List[EnvVar] = [ + EnvVar( + name="AGENT_GUILD_BASE_URL", + description="Optional override for the Agent Guild API base URL " + "(defaults to the hosted instance)", + required=False, + ), +] + + +def _base_url() -> str: + return os.environ.get( + "AGENT_GUILD_BASE_URL", DEFAULT_AGENT_GUILD_BASE_URL).rstrip("/") + + +def _request(path: str, data: Optional[bytes] = None) -> str: + """Call the Agent Guild API and return the response body as a string. + + Never raises, so a failed lookup can't crash a crew: + - HTTP error responses return the API's own (JSON) error body, which + carries a more actionable message than the bare status line; + - transport failures (DNS, timeout, cold start of the hosted instance) + return a structured JSON error string that names the endpoint and + distinguishes "service unreachable" from an in-band API error. + """ + base_url = _base_url() + headers = {"User-Agent": _UA} + if data is not None: + headers["content-type"] = "application/json" req = urllib.request.Request( - AGENT_GUILD_BASE_URL + path, headers={"User-Agent": _UA}) - with urllib.request.urlopen(req, timeout=_TIMEOUT) as r: - return r.read().decode("utf-8") + base_url + path, data=data, headers=headers, + method="POST" if data is not None else "GET") + try: + with urllib.request.urlopen(req, timeout=_TIMEOUT) as r: + return r.read().decode("utf-8") + except urllib.error.HTTPError as e: + body = e.read().decode("utf-8", errors="replace") + if body: + return body + return json.dumps({ + "error": "agent_guild_http_error", + "status": e.code, + "detail": str(e), + }) + except Exception as e: # noqa: BLE001 + return json.dumps({ + "error": "agent_guild_unreachable", + "detail": str(e), + "endpoint": base_url, + "hint": "The hosted instance may be cold-starting; retry once, " + "or set AGENT_GUILD_BASE_URL to a self-hosted instance.", + }) class AgentGuildCheckInput(BaseModel): @@ -48,13 +98,10 @@ class AgentGuildCheckTool(BaseTool): "capabilities instead.") args_schema: Type[BaseModel] = AgentGuildCheckInput package_dependencies: List[str] = [] - env_vars: List[EnvVar] = [] + env_vars: List[EnvVar] = Field(default_factory=lambda: list(_ENV_VARS)) def _run(self, capability: str) -> str: - try: - return _get("/check?capability=" + urllib.parse.quote(capability)) - except Exception as e: # noqa: BLE001 - return json.dumps({"error": f"Agent Guild request failed: {e}"}) + return _request("/check?capability=" + urllib.parse.quote(capability)) class AgentGuildRiskScoreInput(BaseModel): @@ -69,13 +116,10 @@ class AgentGuildRiskScoreTool(BaseTool): "agent id, including its trust score and collusion suspicion.") args_schema: Type[BaseModel] = AgentGuildRiskScoreInput package_dependencies: List[str] = [] - env_vars: List[EnvVar] = [] + env_vars: List[EnvVar] = Field(default_factory=lambda: list(_ENV_VARS)) def _run(self, agent_id: str) -> str: - try: - return _get(f"/agents/{urllib.parse.quote(agent_id)}/risk-score") - except Exception as e: # noqa: BLE001 - return json.dumps({"error": f"Agent Guild request failed: {e}"}) + return _request(f"/agents/{urllib.parse.quote(agent_id)}/risk-score") class AgentGuildVerifyPassportInput(BaseModel): @@ -94,16 +138,8 @@ class AgentGuildVerifyPassportTool(BaseTool): "forged credential can't mislead.") args_schema: Type[BaseModel] = AgentGuildVerifyPassportInput package_dependencies: List[str] = [] - env_vars: List[EnvVar] = [] + env_vars: List[EnvVar] = Field(default_factory=lambda: list(_ENV_VARS)) def _run(self, credential_json: str) -> str: - try: - body = credential_json.encode("utf-8") - req = urllib.request.Request( - AGENT_GUILD_BASE_URL + "/credentials/verify", data=body, - headers={"content-type": "application/json", "User-Agent": _UA}, - method="POST") - with urllib.request.urlopen(req, timeout=_TIMEOUT) as r: - return r.read().decode("utf-8") - except Exception as e: # noqa: BLE001 - return json.dumps({"error": f"Agent Guild request failed: {e}"}) + return _request( + "/credentials/verify", data=credential_json.encode("utf-8")) From bd46e0dc4d04ea45d5924b7a1c993fc5877ee4fb Mon Sep 17 00:00:00 2001 From: AgentTanuki Date: Fri, 3 Jul 2026 16:00:35 +0700 Subject: [PATCH 3/3] fix: percent-encode path/query params with safe="" (CodeRabbit review) urllib.parse.quote() leaves '/' unescaped by default, so an agent_id like '../../other-endpoint' could be routed to a different API path. Quote agent_id (and capability, for consistency) with safe="". Extracted to a local variable to stay f-string-compatible on Python 3.10/3.11. --- .../crewai_tools/tools/agent_guild_tool/agent_guild_tool.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py index 8994ec6aed..3777245dc8 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py +++ b/lib/crewai-tools/src/crewai_tools/tools/agent_guild_tool/agent_guild_tool.py @@ -101,7 +101,7 @@ class AgentGuildCheckTool(BaseTool): env_vars: List[EnvVar] = Field(default_factory=lambda: list(_ENV_VARS)) def _run(self, capability: str) -> str: - return _request("/check?capability=" + urllib.parse.quote(capability)) + return _request("/check?capability=" + urllib.parse.quote(capability, safe="")) class AgentGuildRiskScoreInput(BaseModel): @@ -119,7 +119,8 @@ class AgentGuildRiskScoreTool(BaseTool): env_vars: List[EnvVar] = Field(default_factory=lambda: list(_ENV_VARS)) def _run(self, agent_id: str) -> str: - return _request(f"/agents/{urllib.parse.quote(agent_id)}/risk-score") + quoted_id = urllib.parse.quote(agent_id, safe="") + return _request(f"/agents/{quoted_id}/risk-score") class AgentGuildVerifyPassportInput(BaseModel):