bfa-irc-a-sdk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: bfa-irc-a-sdk
3
+ Version: 0.1.0
4
+ Summary: A generic Backend for Agents (BFA) SDK with FAISS semantic routing.
5
+ Author: Sandro Garcia
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Operating System :: OS Independent
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: fastapi
12
+ Requires-Dist: uvicorn
13
+ Requires-Dist: httpx
14
+ Requires-Dist: python-dotenv
15
+ Requires-Dist: a2a-sdk
16
+ Requires-Dist: numpy
17
+ Requires-Dist: faiss-cpu
18
+ Requires-Dist: pydantic
19
+ Requires-Dist: fastmcp
20
+ Requires-Dist: pyyaml
21
+ Requires-Dist: mangum
22
+ Requires-Dist: openai
23
+ Requires-Dist: pyjwt
24
+ Requires-Dist: cryptography
25
+ Provides-Extra: local
26
+ Requires-Dist: sentence-transformers; extra == "local"
27
+
28
+ # Backend for Agents SDK (BFA) & IRC-A Protocol
29
+
30
+ A generic and opinionated framework and SDK to implement the **BFA (Backend for Agents)** pattern and the **IRC-A (Internet Relay Chat for Agents)** protocol, featuring native support for **FAISS-based Semantic Routing (vector search)**, asymmetric zero-trust security boundaries, and standard abstractions for A2A Agents and MCP Servers.
31
+
32
+ Read the official protocol specification:
33
+ 👉 **[IRC-A Protocol Whitepaper (v1.0.0)](IRC-A_Whitepaper.md)** - *Decentralized Agent Networks, Semantic Capability Routing, and Secure-by-Design Software Architecture.*
34
+
35
+ ---
36
+
37
+ ## Multilingual Documentation
38
+ * [Português (Portuguese)](README.pt.md)
39
+ * [Español (Spanish)](README.es.md)
40
+
41
+ ---
42
+
43
+ ## BFA / IRC-A Protocol Architecture
44
+
45
+ The BFA Gateway acts as a semantic middleware and registry broker layer between consumers (e.g., messaging UIs, chat systems) and specialized agents/tools.
46
+
47
+ ```mermaid
48
+ graph TD
49
+ Consumer["Consumer UI / Whatsapp / WebApp"] -->|1. Resolve Query| BFA[BFA Gateway]
50
+
51
+ subgraph BFA_Gateway ["BFA Gateway (Backend for Agents)"]
52
+ Router[Semantic Router] -->|2. Search Embeddings| FAISS[FAISS Vector Store]
53
+ Registry[Registry] -->|Load metadata| Router
54
+ end
55
+
56
+ BFA -->|3. Route & Invoke| Agent1["Cuentas Agent (A2A)"]
57
+ BFA -->|3. Route & Invoke| Agent2["Tarjetas Agent (A2A)"]
58
+ BFA -->|4. Execute Tool| MCP1["MDBank MCP (FastMCP)"]
59
+ ```
60
+
61
+ ---
62
+
63
+ ## Key Features
64
+
65
+ 1. **FAISS-Based Semantic Routing:** Instead of matching exact keywords (like BM25), the BFA Gateway indexes the descriptions, tags, and examples of agents and tools in a local FAISS vector index. This resolves queries to matching functions even when synonyms are used (e.g., matching *"plastic"* to *"credit card"*).
66
+ 2. **`BFAAgent` Abstraction:** Simplifies building A2A agents using the `a2a-sdk` and Starlette. Forces standard metadata declarations (`tags`, `examples`, `description`) required for semantic indexing.
67
+ 3. **`BFAMCP` Abstraction:** Wraps and extends `FastMCP` servers. Automatically exposes a standardized `/tools` endpoint returning input schemas, descriptions, and custom tags/examples for discovery.
68
+ 4. **Secure-by-Design IRC-A Security (Roadmap):** Employs asymmetric challenge-response registration handshakes, logical channel masking (via container-level `IRCA_CHANNELS` env variables) to segregate vector search spaces, and Ephemeral DET (Delegated Execution Tokens) to enable direct decentralized P2P invocation without gateway bottlenecks.
69
+ 5. **Serverless (AWS Lambda) Ready:** Includes a built-in **Mangum** adapter in the Gateway. Combined with the cloud-based `OpenAIEmbedder`, the BFA Gateway runs serverless on demand with zero cold-starts.
70
+
71
+ ---
72
+
73
+ ## Configuring Embedding Providers & Chunking
74
+
75
+ The BFA Gateway uses semantic embeddings to index agent/tool metadata in FAISS. You can choose between local models, cloud APIs, or offline mock routing via environment variables:
76
+
77
+ | Mode / Provider | Environment Variables | Dependencies | Description |
78
+ |---|---|---|---|
79
+ | **Local Real (Default)** | None | `bfa-sdk[local]` | Uses `sentence-transformers` locally. Recommended for Python <= 3.12 environments. |
80
+ | **OpenAI (Cloud)** | `BFA_USE_OPENAI_EMBEDDINGS=true`, `OPENAI_API_KEY="..."` | `openai` | Queries OpenAI's `text-embedding-3-small` endpoint. Perfect for serverless/Lambda environments. |
81
+ | **Offline Mock (Feature Hashing)** | `BFA_USE_MOCK_EMBEDDINGS=true` | None | Uses a stable MD5 feature hashing trick to route queries based on keywords. Zero dependencies, fast, and local. |
82
+
83
+ > [!NOTE]
84
+ > **Why is there no Chunking in the Gateway?**
85
+ > The BFA Gateway is a semantic router of services, not a document retrieval engine (RAG). It indexes short microservice metadata cards (names, descriptions, tags, examples) which fit completely within embedding token limits.
86
+ > If you need to perform **Document Chunking** (RAG over PDFs/manuals), it should be implemented **inside the respective A2A Agent's internal database/logic**, keeping the Gateway lightweight and decoupled from document storage.
87
+
88
+ ---
89
+
90
+ ## Installation
91
+
92
+ You can install the BFA SDK directly from GitHub using `pip`:
93
+
94
+ ```bash
95
+ # Install the development version from the main branch
96
+ pip install git+https://github.com/SandroG1977/bfa-sdk.git
97
+
98
+ # Or install a specific version with the secure IRC-A protocol support
99
+ pip install git+https://github.com/SandroG1977/bfa-sdk.git@feature/docstrings-init
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Running the Demo
105
+
106
+ ### 2. Run the MDBank Demo
107
+ The demo launches three mock servers in the background:
108
+ 1. A mock MDBank MCP server (`examples/mock_mdbank_mcp.py`) on port `8001`.
109
+ 2. A mock Cuentas A2A Agent (`examples/mock_cuentas_agent.py`) on port `8002`.
110
+ 3. A mock Tarjetas A2A Agent (`examples/mock_tarjetas_agent.py`) on port `8003`.
111
+ 4. The BFA Gateway on port `8000`, running dynamic discovery and performing test queries.
112
+
113
+ To run:
114
+ ```bash
115
+ python examples/run_demo.py
116
+ ```
117
+
118
+ ### 3. Run the UI Dashboard (IRC-A Central Hub)
119
+ We have included a React-based UI Dashboard under `examples/frontend` to visually monitor the active agents/tools registry, register new microservices dynamically (plug-and-play), and chat with the routed banking agents:
120
+
121
+ ```bash
122
+ # Navigate to the frontend folder
123
+ cd examples/frontend
124
+
125
+ # Install dependencies
126
+ npm install
127
+
128
+ # Start the development server
129
+ npm start
130
+ ```
131
+ Open `http://localhost:3000` to interact with your local agent hub in real-time.
132
+
133
+
134
+ ---
135
+
136
+ ## Credits & Acknowledgements
137
+
138
+ This SDK is a community-driven implementation and expansion of the **BFA (Backend for Agents)** architectural pattern originally designed and documented by **Michael Douglas Barbosa Araujo** (Staff AI Architect).
139
+
140
+ You can read his original article introducing the pattern here:
141
+ 👉 [O padrão Back-end para Agentes (BFA) - Medium](https://medium.com/@mdbaraujo/o-padr%C3%A3o-back-end-para-agentes-bfa-a53c1c6d87fb)
142
+
143
+ The goal of this project is to provide a standardized, packaged SDK extending his original concept with semantic vector routing (FAISS) and unified base adapters. All credit for the underlying protocol and architectural vision belongs to him.
144
+
145
+
@@ -0,0 +1,12 @@
1
+ bfa_sdk/__init__.py,sha256=0qwUPj_WSXuq0ZruXWUuP9ZWzS8WAW_e_27K3sno_YU,418
2
+ bfa_sdk/config.py,sha256=WaF5ElPo7hWOqsP4gq5pT32FsSylicZWP4b8g3WuPKw,2032
3
+ bfa_sdk/core/__init__.py,sha256=p_Q2qovu0ZrKSimv_SSVNy_5FK3RYxcd2eutui23pTg,27
4
+ bfa_sdk/core/agent.py,sha256=Ipy27GzhR_m6BTJhcxys-_fMi8Atd9C-xG9jnuNJAE4,14519
5
+ bfa_sdk/core/gateway.py,sha256=FIl5KTKfGNSwMAnvhAIkpgL3B3C0qXiXLwJKXlMeoPg,19724
6
+ bfa_sdk/core/mcp.py,sha256=bkt7vgKrMN0_vmkGtDkSKfB6yOzE4OGUyRBUDMz0D2c,15257
7
+ bfa_sdk/router/embedder.py,sha256=Qu6OQCrsPKbFtO6XgUuTmFxpGxQw27LRpDOuiTSbbQM,3708
8
+ bfa_sdk/router/search.py,sha256=oqsb43no4Dn3hCyjcVs-V07nqmaeYQHQWLWA22xbp4I,4652
9
+ bfa_irc_a_sdk-0.1.0.dist-info/METADATA,sha256=Ue5-36QiJVunjNkY6U9OEM0sytNB5KKhlvC4xE7ZEnQ,6909
10
+ bfa_irc_a_sdk-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
11
+ bfa_irc_a_sdk-0.1.0.dist-info/top_level.txt,sha256=yp9ist1-yHCot3fdt1MtKbJW74E3ICOVqhYuboxTxFY,8
12
+ bfa_irc_a_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ bfa_sdk
bfa_sdk/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ # Backend for Agents SDK (BFA)
2
+ # Version 0.1.0
3
+ """
4
+ BFA SDK: A lightweight framework for Backend for Agents (BFA) architecture with FAISS semantic routing.
5
+ """
6
+
7
+
8
+ from bfa_sdk.core.agent import BFAAgent
9
+ from bfa_sdk.core.mcp import BFAMCP
10
+ from bfa_sdk.core.gateway import create_gateway_app
11
+ from bfa_sdk.router.search import BFASemanticRouter
12
+
13
+ __all__ = ["BFAAgent", "BFAMCP", "create_gateway_app", "BFASemanticRouter"]
bfa_sdk/config.py ADDED
@@ -0,0 +1,45 @@
1
+ import os
2
+ import yaml
3
+ from typing import List, Dict, Any
4
+
5
+ class BFAConfig:
6
+ """
7
+ Configuration helper parsing endpoints and models from environment variables
8
+ or a YAML configuration file.
9
+ """
10
+ def __init__(self, config_path: str = None):
11
+ self.agent_endpoints: List[str] = []
12
+ self.mcp_endpoints: List[str] = []
13
+ self.embedding_model: str = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
14
+ self.use_mock_embeddings: bool = False
15
+ self.use_openai_embeddings: bool = False
16
+ self.openai_api_key: str = ""
17
+
18
+ # Load from config file if provided
19
+ if config_path and os.path.exists(config_path):
20
+ self.load_from_file(config_path)
21
+ else:
22
+ self.load_from_env()
23
+
24
+ def load_from_file(self, path: str):
25
+ with open(path, "r") as f:
26
+ data = yaml.safe_load(f) or {}
27
+ self.agent_endpoints = data.get("agent_endpoints", [])
28
+ self.mcp_endpoints = data.get("mcp_endpoints", [])
29
+ self.embedding_model = data.get("embedding_model", self.embedding_model)
30
+ self.use_mock_embeddings = data.get("use_mock_embeddings", False)
31
+ self.use_openai_embeddings = data.get("use_openai_embeddings", False)
32
+ self.openai_api_key = data.get("openai_api_key", "")
33
+
34
+ def load_from_env(self):
35
+ # Parse comma-separated strings
36
+ agents_env = os.getenv("BFA_AGENT_ENDPOINTS", "")
37
+ self.agent_endpoints = [x.strip() for x in agents_env.split(",") if x.strip()]
38
+
39
+ mcps_env = os.getenv("BFA_MCP_ENDPOINTS", "")
40
+ self.mcp_endpoints = [x.strip() for x in mcps_env.split(",") if x.strip()]
41
+
42
+ self.embedding_model = os.getenv("BFA_EMBEDDING_MODEL", self.embedding_model)
43
+ self.use_mock_embeddings = os.getenv("BFA_USE_MOCK_EMBEDDINGS", "false").lower() == "true"
44
+ self.use_openai_embeddings = os.getenv("BFA_USE_OPENAI_EMBEDDINGS", "false").lower() == "true"
45
+ self.openai_api_key = os.getenv("OPENAI_API_KEY", "")
@@ -0,0 +1 @@
1
+ # Core modules for BFA SDK
bfa_sdk/core/agent.py ADDED
@@ -0,0 +1,353 @@
1
+ import abc
2
+ import os
3
+ import httpx
4
+ import jwt
5
+ import asyncio
6
+ from contextlib import asynccontextmanager
7
+ from typing import List, Dict, Any, Optional
8
+ from starlette.applications import Starlette
9
+ from starlette.middleware.base import BaseHTTPMiddleware
10
+ from starlette.responses import JSONResponse
11
+ from a2a.server.request_handlers import DefaultRequestHandler
12
+ from a2a.server.tasks import InMemoryTaskStore
13
+ from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
14
+ from a2a.types import AgentCapabilities, AgentCard, AgentSkill, AgentInterface
15
+ from a2a.server.agent_execution import AgentExecutor
16
+ from a2a.server.agent_execution.context import RequestContext
17
+ from a2a.server.events.event_queue import EventQueue
18
+ from a2a.helpers import new_text_message
19
+ from a2a.types import Role
20
+ from cryptography.hazmat.primitives.asymmetric import rsa, padding
21
+ from cryptography.hazmat.primitives import hashes, serialization
22
+
23
+ class BFAAgentExecutor(AgentExecutor):
24
+ """
25
+ Internal executor that maps standard A2A execution to BFAAgent's run method.
26
+ """
27
+ def __init__(self, agent_instance):
28
+ super().__init__()
29
+ self.agent_instance = agent_instance
30
+
31
+ async def execute(self, context: RequestContext, event_queue: EventQueue):
32
+ try:
33
+ user_input = context.get_user_input()
34
+ # Run the concrete agent logic
35
+ response_text = await self.agent_instance.run(user_input, context=context)
36
+ # Enqueue response as a text message
37
+ await event_queue.enqueue_event(
38
+ new_text_message(str(response_text), role=Role.ROLE_AGENT)
39
+ )
40
+ except Exception as e:
41
+ await event_queue.enqueue_event(
42
+ new_text_message(f"Error in Agent execution: {str(e)}", role=Role.ROLE_AGENT)
43
+ )
44
+
45
+ async def cancel(self, context: RequestContext, event_queue: EventQueue):
46
+ pass
47
+
48
+
49
+ class IRCAHeaderTracingMiddleware(BaseHTTPMiddleware):
50
+ """
51
+ Starlette middleware for loop detection and propagation of transaction correlation context.
52
+ """
53
+ def __init__(self, app, agent_instance):
54
+ super().__init__(app)
55
+ self.agent_instance = agent_instance
56
+
57
+ async def dispatch(self, request, call_next):
58
+ trace_id = request.headers.get("x-trace-id")
59
+ visited_raw = request.headers.get("x-visited-nodes", "")
60
+
61
+ if trace_id:
62
+ visited_nodes = [x.strip() for x in visited_raw.split(",") if x.strip()]
63
+
64
+ # Execution loop mitigation check
65
+ if self.agent_instance.agent_id in visited_nodes:
66
+ return JSONResponse(
67
+ {
68
+ "jsonrpc": "2.0",
69
+ "error": {
70
+ "code": -32000,
71
+ "message": f"IRC-A Circular Loop Detected: Node '{self.agent_instance.agent_id}' visited twice."
72
+ },
73
+ "id": None
74
+ },
75
+ status_code=409
76
+ )
77
+
78
+ # Store values on request state
79
+ request.state.trace_id = trace_id
80
+ request.state.visited_nodes = visited_nodes + [self.agent_instance.agent_id]
81
+
82
+ return await call_next(request)
83
+
84
+
85
+ class BFAAgent(abc.ABC):
86
+ """
87
+ Abstract Base Class for BFA Agents.
88
+ Inheriting from this class automatically configures an A2A server.
89
+ Developers only need to implement the async run() method.
90
+ """
91
+ def __init__(
92
+ self,
93
+ agent_id: str,
94
+ name: str,
95
+ description: str,
96
+ tags: List[str],
97
+ examples: List[str],
98
+ url: str,
99
+ version: str = "1.0.0",
100
+ private_key: Any = None,
101
+ gateway_public_key: Any = None,
102
+ gateway_url: str = None
103
+ ):
104
+ self.agent_id = agent_id
105
+ self.name = name
106
+ self.description = description
107
+ self.tags = tags
108
+ self.examples = examples
109
+ self.url = url
110
+ self.version = version
111
+ self.gateway_url = gateway_url or os.getenv("BFA_GATEWAY_URL")
112
+
113
+ # Load/Generate keys
114
+ if private_key is None:
115
+ self._private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
116
+ elif isinstance(private_key, str):
117
+ self._private_key = serialization.load_pem_private_key(private_key.encode("utf-8"), password=None)
118
+ else:
119
+ self._private_key = private_key
120
+ self._public_key = self._private_key.public_key()
121
+
122
+ # Load gateway public key
123
+ if isinstance(gateway_public_key, str):
124
+ self.gateway_public_key = serialization.load_pem_public_key(gateway_public_key.encode("utf-8"))
125
+ else:
126
+ self.gateway_public_key = gateway_public_key
127
+
128
+ # Resolve channels
129
+ raw_channels = os.getenv("IRCA_CHANNELS", "#public")
130
+ self.channels = [ch.strip() for ch in raw_channels.split(",")]
131
+
132
+ self.session_token = None
133
+ self.token_expiry = 0
134
+
135
+ # Try to download gateway public key if missing
136
+ if not self.gateway_public_key and self.gateway_url:
137
+ try:
138
+ res = httpx.get(f"{self.gateway_url.rstrip('/')}/public_key", timeout=5)
139
+ if res.status_code == 200:
140
+ pem_str = res.json().get("public_key")
141
+ self.gateway_public_key = serialization.load_pem_public_key(pem_str.encode("utf-8"))
142
+ except Exception as e:
143
+ print(f"BFAAgent Warning: Could not download gateway public key: {e}")
144
+
145
+ # Create default A2A skill representing this agent
146
+ self.skill = AgentSkill(
147
+ id=self.agent_id,
148
+ name=self.name,
149
+ description=self.description,
150
+ tags=self.tags,
151
+ examples=self.examples
152
+ )
153
+
154
+ # Create AgentCard representation for BFA Discovery
155
+ self.agent_card = AgentCard(
156
+ name=self.name,
157
+ description=self.description,
158
+ default_input_modes=["text"],
159
+ default_output_modes=["text"],
160
+ skills=[self.skill],
161
+ version=self.version,
162
+ capabilities=AgentCapabilities(streaming=True),
163
+ supported_interfaces=[
164
+ AgentInterface(
165
+ protocol_binding="JSONRPC",
166
+ url=self.url,
167
+ )
168
+ ]
169
+ )
170
+
171
+ self.task_store = InMemoryTaskStore()
172
+ self.executor = BFAAgentExecutor(self)
173
+ self.http_handler = DefaultRequestHandler(
174
+ agent_executor=self.executor,
175
+ task_store=self.task_store,
176
+ agent_card=self.agent_card
177
+ )
178
+
179
+ # Setup standard routes
180
+ routes = []
181
+ routes.extend(create_agent_card_routes(self.agent_card))
182
+ routes.extend(create_jsonrpc_routes(request_handler=self.http_handler, rpc_url="/"))
183
+ self.app = Starlette(routes=routes)
184
+ self.app.add_middleware(IRCAHeaderTracingMiddleware, agent_instance=self)
185
+
186
+ # Register shutdown hook to disconnect from Gateway cleanly
187
+ async def shutdown_event():
188
+ if self.gateway_url:
189
+ try:
190
+ async with httpx.AsyncClient() as client:
191
+ await client.post(
192
+ f"{self.gateway_url.rstrip('/')}/register/disconnect",
193
+ json={"node_id": self.agent_id},
194
+ timeout=3
195
+ )
196
+ except Exception as e:
197
+ print(f"BFAAgent Warning: Failed to disconnect from gateway on shutdown: {e}")
198
+
199
+ # Register lifespan-based teardown handler dynamically
200
+ old_lifespan = self.app.router.lifespan_context
201
+ @asynccontextmanager
202
+ async def wrapped_lifespan(app_inst):
203
+ async with old_lifespan(app_inst) as yielded_val:
204
+ yield yielded_val
205
+ await shutdown_event()
206
+ self.app.router.lifespan_context = wrapped_lifespan
207
+
208
+ # Run auto-registration
209
+ if self.gateway_url:
210
+ self._auto_register_to_gateway()
211
+
212
+ @property
213
+ def public_key_pem(self) -> str:
214
+ pem = self._public_key.public_bytes(
215
+ encoding=serialization.Encoding.PEM,
216
+ format=serialization.PublicFormat.SubjectPublicKeyInfo
217
+ )
218
+ return pem.decode("utf-8")
219
+
220
+ def _auto_register_to_gateway(self) -> bool:
221
+ """Spawns register_with_gateway as a non-blocking background task."""
222
+ if not self.gateway_url:
223
+ return False
224
+ try:
225
+ loop = asyncio.get_running_loop()
226
+ loop.create_task(self.register_with_gateway(self.gateway_url))
227
+ return True
228
+ except RuntimeError:
229
+ import threading
230
+ threading.Thread(
231
+ target=lambda: asyncio.run(self.register_with_gateway(self.gateway_url)),
232
+ daemon=True
233
+ ).start()
234
+ return True
235
+
236
+ async def register_with_gateway(self, gateway_url: str) -> bool:
237
+ """
238
+ Dynamically register this agent with BFA Gateway using cryptographic challenge-response,
239
+ falling back to simple registration if unsupported.
240
+ """
241
+ # Delay slightly to allow the local Uvicorn/Starlette port to bind and listen
242
+ await asyncio.sleep(1.0)
243
+
244
+ self.gateway_url = gateway_url
245
+ init_url = f"{gateway_url.rstrip('/')}/register/init"
246
+ verify_url = f"{gateway_url.rstrip('/')}/register/verify"
247
+ fallback_url = f"{gateway_url.rstrip('/')}/register/agent"
248
+
249
+ try:
250
+ # 1. Initialize challenge
251
+ async with httpx.AsyncClient() as client:
252
+ # Try to download gateway public key if still missing
253
+ if not self.gateway_public_key:
254
+ try:
255
+ res_pub = await client.get(f"{gateway_url.rstrip('/')}/public_key", timeout=5)
256
+ if res_pub.status_code == 200:
257
+ pem_str = res_pub.json().get("public_key")
258
+ from cryptography.hazmat.primitives.serialization import load_pem_public_key
259
+ self.gateway_public_key = load_pem_public_key(pem_str.encode("utf-8"))
260
+ except Exception as e:
261
+ print(f"BFAAgent Warning: Could not download gateway public key during registration: {e}")
262
+
263
+ res = await client.post(init_url, json={"node_id": self.agent_id, "channels": self.channels}, timeout=5)
264
+ if res.status_code in (404, 405, 501):
265
+ raise NotImplementedError("Gateway does not support cryptographic challenge-response")
266
+ if res.status_code != 200:
267
+ raise NotImplementedError()
268
+ challenge = res.json()["challenge_bytes"]
269
+
270
+ # 2. Sign Challenge
271
+ signature = self._private_key.sign(
272
+ challenge.encode("utf-8"),
273
+ padding.PKCS1v15(),
274
+ hashes.SHA256()
275
+ )
276
+
277
+ # 3. Verify Challenge and obtain Session JWT
278
+ res_verify = await client.post(verify_url, json={
279
+ "node_id": self.agent_id,
280
+ "signature": signature.hex(),
281
+ "public_key": self.public_key_pem
282
+ }, timeout=5)
283
+
284
+ if res_verify.status_code == 200:
285
+ data = res_verify.json()
286
+ self.session_token = data["session_token"]
287
+ self.token_expiry = data["expiry"]
288
+
289
+ # Also register URL/channels in gateway index
290
+ await client.post(
291
+ fallback_url,
292
+ params={"url": self.url, "channels": ",".join(self.channels)},
293
+ timeout=5
294
+ )
295
+ print(f"BFAAgent: Successfully registered '{self.agent_id}' via cryptographic handshake.")
296
+ return True
297
+ else:
298
+ raise NotImplementedError()
299
+ except (NotImplementedError, KeyError, Exception):
300
+ # Fall back to simple, unauthenticated registration for compatibility
301
+ try:
302
+ async with httpx.AsyncClient() as client:
303
+ res_simple = await client.post(
304
+ fallback_url,
305
+ params={"url": self.url, "channels": ",".join(self.channels)},
306
+ timeout=5
307
+ )
308
+ if res_simple.status_code == 200:
309
+ print(f"BFAAgent: Successfully registered '{self.agent_id}' via simple registration fallback.")
310
+ return True
311
+ else:
312
+ print(f"BFAAgent Error: Registration fallback failed with status {res_simple.status_code}: {res_simple.text}")
313
+ return False
314
+ except Exception as ex:
315
+ print(f"BFAAgent Error: Failed to connect to Gateway fallback at {fallback_url}: {ex}")
316
+ return False
317
+
318
+ def verify_incoming_det(self, delegated_token: str, expected_function: str, runtime_args: dict) -> bool:
319
+ """
320
+ Offline Decentralized Verification performed locally by the agent node.
321
+ Validates the BFA-Gateway signature and enforces parameter lock-down.
322
+ """
323
+ if not self.gateway_public_key:
324
+ return False
325
+ try:
326
+ decoded_det = jwt.decode(
327
+ delegated_token,
328
+ self.gateway_public_key,
329
+ algorithms=["RS256"],
330
+ audience=self.agent_id
331
+ )
332
+
333
+ # Scope validation
334
+ if decoded_det.get("permitted_action") != expected_function:
335
+ return False
336
+
337
+ # Parameter lockdown verification
338
+ for key, value in decoded_det.get("restricted_params", {}).items():
339
+ if runtime_args.get(key) != value:
340
+ return False
341
+
342
+ return True
343
+ except Exception:
344
+ return False
345
+
346
+ @abc.abstractmethod
347
+ async def run(self, user_message: str, context: RequestContext) -> str:
348
+ """
349
+ Process the user message and return the agent's response.
350
+ Must be implemented by subclasses.
351
+ """
352
+ pass
353
+