basedagents 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (23) hide show
  1. {basedagents-0.2.0 → basedagents-0.3.0}/PKG-INFO +1 -1
  2. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/__init__.py +6 -1
  3. basedagents-0.3.0/basedagents/integrations/__init__.py +8 -0
  4. basedagents-0.3.0/basedagents/integrations/autogen.py +224 -0
  5. basedagents-0.3.0/basedagents/integrations/crewai.py +251 -0
  6. basedagents-0.3.0/basedagents/middleware.py +316 -0
  7. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/PKG-INFO +1 -1
  8. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/SOURCES.txt +3 -0
  9. {basedagents-0.2.0 → basedagents-0.3.0}/pyproject.toml +1 -1
  10. basedagents-0.2.0/basedagents/integrations/__init__.py +0 -0
  11. {basedagents-0.2.0 → basedagents-0.3.0}/README.md +0 -0
  12. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/auth.py +0 -0
  13. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/cli.py +0 -0
  14. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/client.py +0 -0
  15. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/easy.py +0 -0
  16. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/integrations/langchain.py +0 -0
  17. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/keypair.py +0 -0
  18. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/pow.py +0 -0
  19. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/dependency_links.txt +0 -0
  20. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/entry_points.txt +0 -0
  21. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/requires.txt +0 -0
  22. {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/top_level.txt +0 -0
  23. {basedagents-0.2.0 → basedagents-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basedagents
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for basedagents.ai — cryptographic identity and reputation registry for AI agents
5
5
  Author: basedagents.ai
6
6
  License: MIT
@@ -33,10 +33,15 @@ from .keypair import AgentKeypair, generate as generate_keypair, from_private_ke
33
33
  from .client import RegistryClient, BasedAgentsError
34
34
  from .auth import build_headers as build_auth_headers
35
35
  from .easy import register_or_load
36
+ from .middleware import require_agent, verify_request, fetch_attestation, VerifiedAgent
36
37
 
37
- __version__ = "0.2.0"
38
+ __version__ = "0.3.0"
38
39
  __all__ = [
39
40
  "register_or_load",
41
+ "require_agent",
42
+ "verify_request",
43
+ "fetch_attestation",
44
+ "VerifiedAgent",
40
45
  "AgentKeypair",
41
46
  "RegistryClient",
42
47
  "BasedAgentsError",
@@ -0,0 +1,8 @@
1
+ """
2
+ Framework integrations for basedagents.
3
+
4
+ Supported:
5
+ - LangChain: register_langchain_agent
6
+ - CrewAI: register_crewai_agent
7
+ - AutoGen: register_autogen_agent
8
+ """
@@ -0,0 +1,224 @@
1
+ """
2
+ AutoGen integration for basedagents.
3
+
4
+ Introspects an AutoGen ConversableAgent, AssistantAgent, or GroupChat and
5
+ auto-populates capabilities and skills for registration.
6
+
7
+ Usage:
8
+ from autogen import AssistantAgent, GroupChat
9
+ from basedagents.integrations.autogen import register_autogen_agent
10
+
11
+ # Single agent
12
+ agent_id = register_autogen_agent(
13
+ assistant,
14
+ name="my-autogen-assistant",
15
+ description="Writes and executes Python code.",
16
+ )
17
+
18
+ # GroupChat (multi-agent)
19
+ agent_id = register_autogen_agent(
20
+ groupchat,
21
+ name="my-autogen-group",
22
+ description="Research + coding multi-agent system.",
23
+ )
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from typing import Any
28
+
29
+
30
+ # ── Known AutoGen agent class names → capabilities ────────────────────────────
31
+ _AGENT_CLASS_CAPABILITIES: dict[str, list[str]] = {
32
+ "AssistantAgent": ["reasoning", "code"],
33
+ "UserProxyAgent": ["code"], # executes code by default
34
+ "GPTAssistantAgent": ["reasoning", "code"],
35
+ "RetrieveAssistantAgent": ["reasoning", "knowledge"],
36
+ "RetrieveUserProxyAgent": ["knowledge"],
37
+ "MathUserProxyAgent": ["reasoning"],
38
+ "TeachableAgent": ["reasoning", "knowledge"],
39
+ "CompressibleAgent": ["reasoning"],
40
+ "TransformMessages": ["reasoning"],
41
+ "WebSurferAgent": ["web-search", "web-scraping"],
42
+ "MultimodalConversableAgent": ["vision", "reasoning"],
43
+ }
44
+
45
+ # ── Code execution detection ──────────────────────────────────────────────────
46
+ _CODE_EXEC_CLASSES = {"UserProxyAgent", "GPTAssistantAgent"}
47
+
48
+
49
+ def _collect_agents(agent_or_group: Any) -> list[Any]:
50
+ """Extract agents from GroupChat, GroupChatManager, or return single agent."""
51
+ # GroupChat has .agents
52
+ agents = getattr(agent_or_group, "agents", None)
53
+ if agents:
54
+ return list(agents)
55
+ # GroupChatManager has .groupchat.agents
56
+ gc = getattr(agent_or_group, "groupchat", None)
57
+ if gc:
58
+ return list(getattr(gc, "agents", []) or [])
59
+ # Single agent
60
+ return [agent_or_group]
61
+
62
+
63
+ def _is_multi_agent(agent_or_group: Any) -> bool:
64
+ return len(_collect_agents(agent_or_group)) > 1
65
+
66
+
67
+ def _agent_executes_code(agent: Any) -> bool:
68
+ """Check if an agent is configured to execute code."""
69
+ cls = type(agent).__name__
70
+ if cls in _CODE_EXEC_CLASSES:
71
+ return True
72
+ # human_input_mode="NEVER" + code_execution_config set = code executor
73
+ code_cfg = getattr(agent, "code_execution_config", None)
74
+ if code_cfg and code_cfg is not False:
75
+ return True
76
+ return False
77
+
78
+
79
+ def _detect_llm_skill(agent: Any) -> str | None:
80
+ """Try to detect LLM provider package from llm_config."""
81
+ llm_config = getattr(agent, "llm_config", None) or {}
82
+ if not isinstance(llm_config, dict):
83
+ return None
84
+ model = llm_config.get("model", "") or ""
85
+ config_list = llm_config.get("config_list", [{}])
86
+ if config_list:
87
+ model = model or config_list[0].get("model", "")
88
+ model = model.lower()
89
+ if "gpt" in model or "o1" in model or "o3" in model:
90
+ return "pyautogen"
91
+ if "claude" in model:
92
+ return "pyautogen"
93
+ if "gemini" in model:
94
+ return "pyautogen"
95
+ return "pyautogen" # always include base pyautogen
96
+
97
+
98
+ def extract_profile(
99
+ agent_or_group: Any,
100
+ extra_capabilities: list[str] | None = None,
101
+ extra_skills: list[dict[str, str]] | None = None,
102
+ ) -> dict[str, Any]:
103
+ """
104
+ Introspect an AutoGen agent or GroupChat and return a partial profile dict
105
+ with auto-detected capabilities and skills.
106
+ """
107
+ agents = _collect_agents(agent_or_group)
108
+ capabilities: set[str] = set(extra_capabilities or [])
109
+ skills_seen: set[str] = set()
110
+ skills: list[dict[str, str]] = list(extra_skills or [])
111
+
112
+ # Base skill
113
+ skills.append({"name": "pyautogen", "registry": "pypi"})
114
+ skills_seen.add("pyautogen")
115
+
116
+ if _is_multi_agent(agent_or_group):
117
+ capabilities.add("multi-agent")
118
+
119
+ for agent in agents:
120
+ cls_name = type(agent).__name__
121
+
122
+ for cap in _AGENT_CLASS_CAPABILITIES.get(cls_name, []):
123
+ capabilities.add(cap)
124
+
125
+ if _agent_executes_code(agent):
126
+ capabilities.add("code")
127
+
128
+ # Detect function/tool calling
129
+ fn_map = getattr(agent, "function_map", None) or {}
130
+ if fn_map:
131
+ capabilities.add("tool-use")
132
+
133
+ # Detect retrieval augmentation
134
+ retrieve_config = getattr(agent, "retrieve_config", None)
135
+ if retrieve_config:
136
+ capabilities.add("knowledge")
137
+
138
+ # Extra skill from LLM config
139
+ pkg = _detect_llm_skill(agent)
140
+ if pkg and pkg not in skills_seen:
141
+ skills.append({"name": pkg, "registry": "pypi"})
142
+ skills_seen.add(pkg)
143
+
144
+ # Fallback capability
145
+ if not capabilities:
146
+ capabilities.add("reasoning")
147
+
148
+ return {
149
+ "capabilities": sorted(capabilities),
150
+ "protocols": ["https"],
151
+ "skills": skills,
152
+ }
153
+
154
+
155
+ def register_autogen_agent(
156
+ agent_or_group: Any,
157
+ name: str,
158
+ description: str = "",
159
+ contact_endpoint: str | None = None,
160
+ organization: str | None = None,
161
+ version: str | None = None,
162
+ tags: list[str] | None = None,
163
+ extra_capabilities: list[str] | None = None,
164
+ extra_skills: list[dict[str, str]] | None = None,
165
+ keypair_path: str | None = None,
166
+ api_url: str | None = None,
167
+ verbose: bool = True,
168
+ ) -> str:
169
+ """
170
+ Register an AutoGen agent or GroupChat with basedagents.ai.
171
+
172
+ Introspects the agent to auto-detect capabilities and skills.
173
+ Idempotent — safe to call on every startup.
174
+
175
+ Args:
176
+ agent_or_group: AutoGen ConversableAgent, AssistantAgent, UserProxyAgent,
177
+ GroupChat, or GroupChatManager
178
+ name: Unique agent name (globally unique on registry)
179
+ description: What this agent/group does
180
+ contact_endpoint: URL where the agent can be reached for verification
181
+ organization: Optional org name
182
+ version: Optional version string
183
+ tags: Optional extra tags
184
+ extra_capabilities: Additional capabilities beyond auto-detected ones
185
+ extra_skills: Additional skills beyond auto-detected ones
186
+ keypair_path: Override keypair file location
187
+ api_url: Override API URL (defaults to BASEDAGENTS_API env or prod)
188
+ verbose: Print progress (default True)
189
+
190
+ Returns:
191
+ agent_id string
192
+
193
+ Example:
194
+ import autogen
195
+ from basedagents.integrations.autogen import register_autogen_agent
196
+
197
+ assistant = autogen.AssistantAgent("assistant", llm_config={"model": "gpt-4o"})
198
+ user_proxy = autogen.UserProxyAgent("user_proxy", code_execution_config={"work_dir": "."})
199
+
200
+ agent_id = register_autogen_agent(
201
+ assistant,
202
+ name="my-autogen-assistant",
203
+ description="Writes and debugs Python code via GPT-4o.",
204
+ )
205
+ """
206
+ from ..easy import register_or_load
207
+
208
+ profile = extract_profile(agent_or_group, extra_capabilities, extra_skills)
209
+ merged_tags = list(set(["autogen"] + (tags or [])))
210
+
211
+ return register_or_load(
212
+ name=name,
213
+ description=description,
214
+ capabilities=profile["capabilities"],
215
+ protocols=profile["protocols"],
216
+ skills=profile["skills"],
217
+ contact_endpoint=contact_endpoint,
218
+ organization=organization,
219
+ version=version,
220
+ tags=merged_tags,
221
+ keypair_path=keypair_path,
222
+ api_url=api_url,
223
+ verbose=verbose,
224
+ )
@@ -0,0 +1,251 @@
1
+ """
2
+ CrewAI integration for basedagents.
3
+
4
+ Introspects a CrewAI Crew or Agent and auto-populates capabilities and skills
5
+ for registration.
6
+
7
+ Usage:
8
+ from crewai import Crew, Agent
9
+ from basedagents.integrations.crewai import register_crewai_agent
10
+
11
+ # Register from a Crew (uses all agents' tools)
12
+ agent_id = register_crewai_agent(
13
+ crew,
14
+ name="my-research-crew",
15
+ description="Multi-agent crew for research and analysis.",
16
+ )
17
+
18
+ # Or register a single CrewAI Agent
19
+ agent_id = register_crewai_agent(
20
+ agent,
21
+ name="my-researcher",
22
+ description="Searches and summarises the web.",
23
+ )
24
+ """
25
+ from __future__ import annotations
26
+
27
+ from typing import Any
28
+
29
+
30
+ # ── Tool name → capabilities mapping ─────────────────────────────────────────
31
+ _TOOL_TO_CAPABILITIES: dict[str, list[str]] = {
32
+ # Search
33
+ "SerperDevTool": ["web-search"],
34
+ "TavilySearchTool": ["web-search"],
35
+ "EXASearchTool": ["web-search"],
36
+ "BraveSearchTool": ["web-search"],
37
+ "DuckDuckGoSearchTool": ["web-search"],
38
+ "GoogleSearchTool": ["web-search"],
39
+ "ScrapeWebsiteTool": ["web-scraping"],
40
+ "SeleniumScrapingTool": ["web-scraping"],
41
+ "ScrapeElementFromWebsiteTool": ["web-scraping"],
42
+ "WebsiteSearchTool": ["web-search", "web-scraping"],
43
+ # Code
44
+ "CodeDocsSearchTool": ["code", "knowledge"],
45
+ "CodeInterpreterTool": ["code"],
46
+ "GithubSearchTool": ["code", "web-search"],
47
+ # Files
48
+ "FileReadTool": ["file-access"],
49
+ "FileWriterTool": ["file-access"],
50
+ "DirectoryReadTool": ["file-access"],
51
+ "DirectorySearchTool": ["file-access"],
52
+ "PDFSearchTool": ["file-access", "knowledge"],
53
+ "DOCXSearchTool": ["file-access", "knowledge"],
54
+ "CSVSearchTool": ["file-access", "data-analysis"],
55
+ "JSONSearchTool": ["file-access"],
56
+ "TXTSearchTool": ["file-access"],
57
+ "XMLSearchTool": ["file-access"],
58
+ "SpreadsheetSearchTool": ["file-access", "data-analysis"],
59
+ # Data
60
+ "PGSearchTool": ["data-analysis", "sql"],
61
+ "MySQLSearchTool": ["data-analysis", "sql"],
62
+ "NL2SQLTool": ["data-analysis", "sql"],
63
+ # Knowledge / RAG
64
+ "RagTool": ["knowledge"],
65
+ "YoutubeVideoSearchTool": ["knowledge"],
66
+ "YoutubeChannelSearchTool": ["knowledge"],
67
+ "MDXSearchTool": ["knowledge"],
68
+ # Comms
69
+ "BrowserbaseTool": ["web-scraping"],
70
+ "MultiOnTool": ["web-scraping"],
71
+ # Vision
72
+ "VisionTool": ["vision"],
73
+ }
74
+
75
+ # ── Tool name → PyPI package ──────────────────────────────────────────────────
76
+ _TOOL_TO_PYPI: dict[str, str] = {
77
+ "SerperDevTool": "crewai-tools",
78
+ "TavilySearchTool": "crewai-tools",
79
+ "EXASearchTool": "crewai-tools",
80
+ "BraveSearchTool": "crewai-tools",
81
+ "DuckDuckGoSearchTool": "crewai-tools",
82
+ "ScrapeWebsiteTool": "crewai-tools",
83
+ "SeleniumScrapingTool": "crewai-tools",
84
+ "WebsiteSearchTool": "crewai-tools",
85
+ "CodeDocsSearchTool": "crewai-tools",
86
+ "CodeInterpreterTool": "crewai-tools",
87
+ "GithubSearchTool": "crewai-tools",
88
+ "FileReadTool": "crewai-tools",
89
+ "FileWriterTool": "crewai-tools",
90
+ "DirectoryReadTool": "crewai-tools",
91
+ "PDFSearchTool": "crewai-tools",
92
+ "DOCXSearchTool": "crewai-tools",
93
+ "CSVSearchTool": "crewai-tools",
94
+ "PGSearchTool": "crewai-tools",
95
+ "NL2SQLTool": "crewai-tools",
96
+ "RagTool": "crewai-tools",
97
+ "YoutubeVideoSearchTool": "crewai-tools",
98
+ "VisionTool": "crewai-tools",
99
+ }
100
+
101
+
102
+ def _collect_tools(crew_or_agent: Any) -> list[Any]:
103
+ """Extract all tools from a Crew or single Agent."""
104
+ tools: list[Any] = []
105
+
106
+ # Single Agent: has .tools
107
+ if hasattr(crew_or_agent, "tools") and not hasattr(crew_or_agent, "agents"):
108
+ return list(getattr(crew_or_agent, "tools", []) or [])
109
+
110
+ # Crew: has .agents, each with .tools
111
+ agents = getattr(crew_or_agent, "agents", []) or []
112
+ for agent in agents:
113
+ tools.extend(list(getattr(agent, "tools", []) or []))
114
+
115
+ return tools
116
+
117
+
118
+ def _is_multi_agent(crew_or_agent: Any) -> bool:
119
+ agents = getattr(crew_or_agent, "agents", None)
120
+ return bool(agents and len(agents) > 1)
121
+
122
+
123
+ def extract_profile(
124
+ crew_or_agent: Any,
125
+ extra_capabilities: list[str] | None = None,
126
+ extra_skills: list[dict[str, str]] | None = None,
127
+ ) -> dict[str, Any]:
128
+ """
129
+ Introspect a CrewAI Crew or Agent and return a partial profile dict
130
+ with auto-detected capabilities and skills.
131
+ """
132
+ tools = _collect_tools(crew_or_agent)
133
+ capabilities: set[str] = set(extra_capabilities or [])
134
+ skills_seen: set[str] = set()
135
+ skills: list[dict[str, str]] = list(extra_skills or [])
136
+
137
+ # Always add crewai base skill
138
+ skills.append({"name": "crewai", "registry": "pypi"})
139
+ skills_seen.add("crewai")
140
+
141
+ # Multi-agent crews get the orchestration capability
142
+ if _is_multi_agent(crew_or_agent):
143
+ capabilities.add("multi-agent")
144
+
145
+ for tool in tools:
146
+ cls_name = type(tool).__name__
147
+ tool_name = getattr(tool, "name", cls_name)
148
+
149
+ for key in (cls_name, tool_name):
150
+ for cap in _TOOL_TO_CAPABILITIES.get(key, []):
151
+ capabilities.add(cap)
152
+
153
+ pkg = _TOOL_TO_PYPI.get(cls_name) or _TOOL_TO_PYPI.get(tool_name)
154
+ if pkg and pkg not in skills_seen:
155
+ skills.append({"name": pkg, "registry": "pypi"})
156
+ skills_seen.add(pkg)
157
+
158
+ # Detect LLM provider
159
+ llm = getattr(crew_or_agent, "llm", None)
160
+ if llm is None:
161
+ # Try first agent's LLM
162
+ agents = getattr(crew_or_agent, "agents", []) or []
163
+ if agents:
164
+ llm = getattr(agents[0], "llm", None)
165
+ if llm:
166
+ cls_name = type(llm).__name__
167
+ _LLM_TO_PYPI = {
168
+ "ChatOpenAI": "langchain-openai",
169
+ "ChatAnthropic": "langchain-anthropic",
170
+ "ChatGoogleGenerativeAI": "langchain-google-genai",
171
+ "ChatGroq": "langchain-groq",
172
+ }
173
+ pkg = _LLM_TO_PYPI.get(cls_name)
174
+ if pkg and pkg not in skills_seen:
175
+ skills.append({"name": pkg, "registry": "pypi"})
176
+ skills_seen.add(pkg)
177
+
178
+ return {
179
+ "capabilities": sorted(capabilities) if capabilities else ["reasoning"],
180
+ "protocols": ["https"],
181
+ "skills": skills,
182
+ }
183
+
184
+
185
+ def register_crewai_agent(
186
+ crew_or_agent: Any,
187
+ name: str,
188
+ description: str = "",
189
+ contact_endpoint: str | None = None,
190
+ organization: str | None = None,
191
+ version: str | None = None,
192
+ tags: list[str] | None = None,
193
+ extra_capabilities: list[str] | None = None,
194
+ extra_skills: list[dict[str, str]] | None = None,
195
+ keypair_path: str | None = None,
196
+ api_url: str | None = None,
197
+ verbose: bool = True,
198
+ ) -> str:
199
+ """
200
+ Register a CrewAI Crew or Agent with basedagents.ai.
201
+
202
+ Introspects tools to auto-detect capabilities and skills.
203
+ Idempotent — safe to call on every startup.
204
+
205
+ Args:
206
+ crew_or_agent: CrewAI Crew or Agent instance
207
+ name: Unique agent name (globally unique on registry)
208
+ description: What this crew/agent does
209
+ contact_endpoint: URL where the agent can be reached for verification
210
+ organization: Optional org name
211
+ version: Optional version string
212
+ tags: Optional extra tags
213
+ extra_capabilities: Additional capabilities beyond auto-detected ones
214
+ extra_skills: Additional skills beyond auto-detected ones
215
+ keypair_path: Override keypair file location
216
+ api_url: Override API URL (defaults to BASEDAGENTS_API env or prod)
217
+ verbose: Print progress (default True)
218
+
219
+ Returns:
220
+ agent_id string
221
+
222
+ Example:
223
+ from crewai import Crew
224
+ from basedagents.integrations.crewai import register_crewai_agent
225
+
226
+ crew = Crew(agents=[researcher, writer], tasks=[...])
227
+ agent_id = register_crewai_agent(
228
+ crew,
229
+ name="my-research-crew",
230
+ description="Research and write blog posts.",
231
+ )
232
+ """
233
+ from ..easy import register_or_load
234
+
235
+ profile = extract_profile(crew_or_agent, extra_capabilities, extra_skills)
236
+ merged_tags = list(set(["crewai"] + (tags or [])))
237
+
238
+ return register_or_load(
239
+ name=name,
240
+ description=description,
241
+ capabilities=profile["capabilities"],
242
+ protocols=profile["protocols"],
243
+ skills=profile["skills"],
244
+ contact_endpoint=contact_endpoint,
245
+ organization=organization,
246
+ version=version,
247
+ tags=merged_tags,
248
+ keypair_path=keypair_path,
249
+ api_url=api_url,
250
+ verbose=verbose,
251
+ )
@@ -0,0 +1,316 @@
1
+ """
2
+ basedagents auth middleware.
3
+
4
+ Drop-in reputation-gated authentication for FastAPI and WSGI (Flask/Starlette) apps.
5
+ Agents prove identity via AgentSig header; the middleware verifies the attestation
6
+ offline using the basedagents registry public key.
7
+
8
+ Usage (FastAPI):
9
+ from basedagents.middleware import require_agent, VerifiedAgent
10
+
11
+ @app.post("/execute")
12
+ async def execute(request: Request, agent: VerifiedAgent = Depends(require_agent(
13
+ min_reputation=0.5,
14
+ capabilities=["code"],
15
+ ))):
16
+ print(f"Request from {agent.name} (rep={agent.reputation})")
17
+ ...
18
+
19
+ Usage (manual):
20
+ from basedagents.middleware import verify_request
21
+
22
+ agent = await verify_request(request_headers, method, path, body)
23
+ if agent is None:
24
+ raise HTTPException(status_code=403)
25
+ """
26
+ from __future__ import annotations
27
+
28
+ import base64
29
+ import hashlib
30
+ import json
31
+ import os
32
+ import time
33
+ from dataclasses import dataclass
34
+ from typing import Any, Callable
35
+
36
+ import httpx
37
+
38
+ from .keypair import _base58_decode as b58decode_key
39
+
40
+ # ── Registry public key (Ed25519, hex) ──────────────────────────────────────
41
+ # Published at https://api.basedagents.ai/v1/attestation/public-key
42
+ # Hardcoded here for offline verification — no API call needed at auth time.
43
+ REGISTRY_PUBLIC_KEY_HEX = "9827a77ffa3bbddff01444277707271838098f3e8f2d29a200054cc0bca308d0"
44
+
45
+ _DEFAULT_BASE = "https://api.basedagents.ai"
46
+ API_BASE = os.environ.get("BASEDAGENTS_API", _DEFAULT_BASE)
47
+
48
+ # Attestation TTL tolerance (seconds beyond stated expiry)
49
+ CLOCK_SKEW_TOLERANCE = 30
50
+
51
+
52
+ @dataclass
53
+ class VerifiedAgent:
54
+ """A fully verified, reputation-checked agent identity."""
55
+ agent_id: str
56
+ name: str
57
+ public_key_b58: str
58
+ capabilities: list[str]
59
+ protocols: list[str]
60
+ reputation: float
61
+ reputation_tier: str
62
+ verification_count: int
63
+ issued_at: int
64
+ expires_at: int
65
+
66
+
67
+ class AttestationError(Exception):
68
+ """Raised when attestation verification fails."""
69
+ pass
70
+
71
+
72
+ # ── In-process attestation cache ────────────────────────────────────────────
73
+ _cache: dict[str, tuple[dict[str, Any], float]] = {} # agent_id → (attestation, fetched_at)
74
+
75
+
76
+ def _cache_get(agent_id: str) -> dict[str, Any] | None:
77
+ entry = _cache.get(agent_id)
78
+ if entry is None:
79
+ return None
80
+ attestation, _ = entry
81
+ # Use until expires_at minus skew tolerance
82
+ if time.time() > attestation["expires_at"] - CLOCK_SKEW_TOLERANCE:
83
+ del _cache[agent_id]
84
+ return None
85
+ return attestation
86
+
87
+
88
+ def _cache_set(agent_id: str, attestation: dict[str, Any]) -> None:
89
+ _cache[agent_id] = (attestation, time.time())
90
+
91
+
92
+ # ── Attestation fetch & verify ───────────────────────────────────────────────
93
+
94
+ def _verify_attestation_signature(attestation: dict[str, Any]) -> bool:
95
+ """
96
+ Verify the registry's Ed25519 signature on the attestation document.
97
+ Uses the hardcoded REGISTRY_PUBLIC_KEY_HEX for offline verification.
98
+ """
99
+ try:
100
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
101
+ from cryptography.exceptions import InvalidSignature
102
+
103
+ sig_b64 = attestation.get("signature", "")
104
+ sig_bytes = base64.b64decode(sig_b64)
105
+
106
+ # Reconstruct the payload that was signed (all fields except signature, sorted keys)
107
+ payload = {k: v for k, v in attestation.items() if k not in ("signature", "_verify")}
108
+ sorted_keys = sorted(payload.keys())
109
+ canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
110
+ # Match server: JSON.stringify(payload, sortedKeys) — compact, no spaces
111
+ msg_bytes = canonical.encode("utf-8")
112
+
113
+ pub_key_bytes = bytes.fromhex(REGISTRY_PUBLIC_KEY_HEX)
114
+ pub_key = Ed25519PublicKey.from_public_bytes(pub_key_bytes)
115
+ pub_key.verify(sig_bytes, msg_bytes)
116
+ return True
117
+ except (InvalidSignature, Exception):
118
+ return False
119
+
120
+
121
+ def fetch_attestation(agent_id: str, base_url: str = API_BASE) -> dict[str, Any]:
122
+ """
123
+ Fetch a fresh attestation from the registry and verify its signature.
124
+ Raises AttestationError on failure.
125
+ """
126
+ cached = _cache_get(agent_id)
127
+ if cached is not None:
128
+ return cached
129
+
130
+ url = f"{base_url}/v1/agents/{agent_id}/attestation"
131
+ with httpx.Client(timeout=10.0) as client:
132
+ res = client.get(url)
133
+
134
+ if res.status_code == 404:
135
+ raise AttestationError(f"Agent {agent_id} not found in registry")
136
+ if res.status_code == 403:
137
+ raise AttestationError(f"Agent {agent_id} is suspended or revoked")
138
+ if not res.is_success:
139
+ raise AttestationError(f"Registry returned {res.status_code} for {agent_id}")
140
+
141
+ attestation = res.json()
142
+
143
+ # Verify registry signature
144
+ if not _verify_attestation_signature(attestation):
145
+ raise AttestationError("Attestation signature verification failed — possible tampering")
146
+
147
+ # Check expiry
148
+ now = int(time.time())
149
+ if now > attestation["expires_at"] + CLOCK_SKEW_TOLERANCE:
150
+ raise AttestationError("Attestation has expired")
151
+
152
+ _cache_set(agent_id, attestation)
153
+ return attestation
154
+
155
+
156
+ def verify_request(
157
+ headers: dict[str, str],
158
+ method: str,
159
+ path: str,
160
+ body: str | bytes = "",
161
+ base_url: str = API_BASE,
162
+ ) -> VerifiedAgent | None:
163
+ """
164
+ Verify an inbound agent request:
165
+ 1. Parse X-Agent-ID and Authorization: AgentSig headers
166
+ 2. Verify the request signature (agent signed this request)
167
+ 3. Fetch + verify the registry attestation (registry signed the agent's identity)
168
+ 4. Confirm the signing key matches the attested public key
169
+ Returns VerifiedAgent or None if verification fails.
170
+ """
171
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
172
+ from cryptography.exceptions import InvalidSignature
173
+
174
+ # Parse headers (case-insensitive)
175
+ headers_lower = {k.lower(): v for k, v in headers.items()}
176
+
177
+ agent_id = headers_lower.get("x-agent-id", "").strip()
178
+ auth_header = headers_lower.get("authorization", "")
179
+ timestamp_str = headers_lower.get("x-timestamp", "")
180
+
181
+ if not agent_id or not auth_header or not timestamp_str:
182
+ return None
183
+
184
+ if not auth_header.startswith("AgentSig "):
185
+ return None
186
+
187
+ try:
188
+ sig_part = auth_header[len("AgentSig "):]
189
+ pubkey_b58, sig_b64 = sig_part.rsplit(":", 1)
190
+ timestamp = int(timestamp_str)
191
+ except (ValueError, IndexError):
192
+ return None
193
+
194
+ # Check clock skew (±60 seconds)
195
+ now = int(time.time())
196
+ if abs(now - timestamp) > 60:
197
+ return None
198
+
199
+ # Verify request signature
200
+ # Signed message: "<METHOD>:<path>:<timestamp>:<sha256_hex_of_body>"
201
+ body_bytes = body.encode() if isinstance(body, str) else body
202
+ body_hash = hashlib.sha256(body_bytes).hexdigest()
203
+ message = f"{method.upper()}:{path}:{timestamp}:{body_hash}"
204
+
205
+ try:
206
+ pub_key_bytes = b58decode_key(pubkey_b58)
207
+ sig_bytes = base64.b64decode(sig_b64)
208
+ pub_key = Ed25519PublicKey.from_public_bytes(pub_key_bytes)
209
+ pub_key.verify(sig_bytes, message.encode("utf-8"))
210
+ except (InvalidSignature, Exception):
211
+ return None
212
+
213
+ # Fetch attestation and verify registry signature
214
+ try:
215
+ attestation = fetch_attestation(agent_id, base_url)
216
+ except AttestationError:
217
+ return None
218
+
219
+ # Confirm agent_id matches
220
+ if attestation["agent_id"] != agent_id:
221
+ return None
222
+
223
+ # Confirm signing key matches attested public key
224
+ if attestation["public_key_b58"] != pubkey_b58:
225
+ return None
226
+
227
+ return VerifiedAgent(
228
+ agent_id=attestation["agent_id"],
229
+ name=attestation["agent_name"],
230
+ public_key_b58=attestation["public_key_b58"],
231
+ capabilities=attestation["capabilities"],
232
+ protocols=attestation["protocols"],
233
+ reputation=attestation["reputation"],
234
+ reputation_tier=attestation["reputation_tier"],
235
+ verification_count=attestation["verification_count"],
236
+ issued_at=attestation["issued_at"],
237
+ expires_at=attestation["expires_at"],
238
+ )
239
+
240
+
241
+ # ── FastAPI dependency ────────────────────────────────────────────────────────
242
+
243
+ def require_agent(
244
+ min_reputation: float = 0.0,
245
+ capabilities: list[str] | None = None,
246
+ base_url: str = API_BASE,
247
+ ) -> Callable:
248
+ """
249
+ FastAPI dependency factory for reputation-gated agent authentication.
250
+
251
+ Args:
252
+ min_reputation: Minimum reputation score (0–1). Default 0 (any registered agent).
253
+ capabilities: Required capabilities. Agent must have ALL of them verified.
254
+ base_url: Override API base URL (useful for testing with staging).
255
+
256
+ Returns:
257
+ FastAPI Depends-compatible callable that returns VerifiedAgent.
258
+
259
+ Raises:
260
+ HTTPException(401) if agent identity cannot be verified.
261
+ HTTPException(403) if agent doesn't meet reputation/capability requirements.
262
+
263
+ Example:
264
+ @app.post("/run")
265
+ async def run(request: Request, agent: VerifiedAgent = Depends(require_agent(
266
+ min_reputation=0.5,
267
+ capabilities=["code"],
268
+ ))):
269
+ ...
270
+ """
271
+ async def _dependency(request: Any) -> VerifiedAgent:
272
+ # Import here to avoid hard dep on fastapi
273
+ try:
274
+ from fastapi import HTTPException
275
+ from fastapi import Request as FastAPIRequest
276
+ except ImportError:
277
+ raise ImportError("fastapi is required for require_agent(). pip install fastapi")
278
+
279
+ # Read body
280
+ body = b""
281
+ try:
282
+ body = await request.body()
283
+ except Exception:
284
+ pass
285
+
286
+ headers = dict(request.headers)
287
+ path = request.url.path
288
+ method = request.method
289
+
290
+ agent = verify_request(headers, method, path, body, base_url)
291
+
292
+ if agent is None:
293
+ raise HTTPException(
294
+ status_code=401,
295
+ detail="Agent identity verification failed. Include X-Agent-ID and Authorization: AgentSig headers.",
296
+ )
297
+
298
+ if agent.reputation < min_reputation:
299
+ raise HTTPException(
300
+ status_code=403,
301
+ detail=f"Agent reputation {agent.reputation:.3f} below required {min_reputation}. "
302
+ f"Current tier: {agent.reputation_tier}.",
303
+ )
304
+
305
+ if capabilities:
306
+ missing = [c for c in capabilities if c not in agent.capabilities]
307
+ if missing:
308
+ raise HTTPException(
309
+ status_code=403,
310
+ detail=f"Agent missing required capabilities: {missing}. "
311
+ f"Attested capabilities: {agent.capabilities}.",
312
+ )
313
+
314
+ return agent
315
+
316
+ return _dependency
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: basedagents
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for basedagents.ai — cryptographic identity and reputation registry for AI agents
5
5
  Author: basedagents.ai
6
6
  License: MIT
@@ -6,6 +6,7 @@ basedagents/cli.py
6
6
  basedagents/client.py
7
7
  basedagents/easy.py
8
8
  basedagents/keypair.py
9
+ basedagents/middleware.py
9
10
  basedagents/pow.py
10
11
  basedagents.egg-info/PKG-INFO
11
12
  basedagents.egg-info/SOURCES.txt
@@ -14,4 +15,6 @@ basedagents.egg-info/entry_points.txt
14
15
  basedagents.egg-info/requires.txt
15
16
  basedagents.egg-info/top_level.txt
16
17
  basedagents/integrations/__init__.py
18
+ basedagents/integrations/autogen.py
19
+ basedagents/integrations/crewai.py
17
20
  basedagents/integrations/langchain.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "basedagents"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Python SDK for basedagents.ai — cryptographic identity and reputation registry for AI agents"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
File without changes
File without changes
File without changes