exaai-agent 2.1.2__py3-none-any.whl → 2.2.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: exaai-agent
3
- Version: 2.1.2
3
+ Version: 2.2.1
4
4
  Summary: ExaAi - Advanced AI Security Agent for Comprehensive Penetration Testing
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -37,6 +37,7 @@ Requires-Dist: rich
37
37
  Requires-Dist: tenacity (>=9.0.0,<10.0.0)
38
38
  Requires-Dist: textual (>=4.0.0,<5.0.0)
39
39
  Requires-Dist: uvicorn
40
+ Requires-Dist: websockets (>=12.0,<13.0)
40
41
  Requires-Dist: xmltodict (>=0.13.0,<0.14.0)
41
42
  Description-Content-Type: text/markdown
42
43
 
@@ -151,8 +152,7 @@ ExaAiAgent is an elite AI-powered cybersecurity agent that acts like a real pene
151
152
  # Install ExaAiAgent
152
153
 
153
154
  # Method 1: Automated Script (Recommended)
154
- curl -sSL https://raw.githubusercontent.com/hleliofficiel/ExaAiAgent/main/install.sh | bash
155
-
155
+ pip install exaai-agent
156
156
  # Method 2: pipx
157
157
  pipx install exaai-agent
158
158
 
@@ -366,6 +366,33 @@ export PERPLEXITY_API_KEY="key" # For search capabilities
366
366
 
367
367
  ---
368
368
 
369
+ ## 🛠️ Troubleshooting
370
+
371
+ ### 🔧 Troubleshooting
372
+
373
+ #### Problem: "LLM Connection Failed" or Model Not Found
374
+ Modern models (like `gemini-3-pro-preview`) require the latest version of `litellm` to be recognized correctly.
375
+
376
+ **Solution: Update LiteLLM**
377
+ ```bash
378
+ pip install -U litellm
379
+ ```
380
+
381
+ **Linux/Debian Users (Externally Managed Environment):**
382
+ If you encounter permission errors or "externally-managed-environment", you may need to use a virtual environment (`venv`) or force a user install:
383
+
384
+ ```bash
385
+ # Option 1: Virtual Environment (Recommended for Servers)
386
+ python3 -m venv venv
387
+ source venv/bin/activate
388
+ pip install exaai-agent
389
+
390
+ # Option 2: Force User Install
391
+ pip install -U litellm --user --break-system-packages
392
+ ```
393
+
394
+ ---
395
+
369
396
  ## 🤝 Contributing
370
397
 
371
398
  We welcome contributions! Check out our [Contributing Guide](CONTRIBUTING.md).
@@ -8,10 +8,12 @@ exaaiagnt/agents/base_agent.py,sha256=WzVJwjBM_gUo-P92zLHKq7C6wclDsVfAAO-bavLx5C
8
8
  exaaiagnt/agents/scan_modes.py,sha256=q2sSuTRCh_hOdVfikhHhOrEWl1GRCG9Uh6S6rALhLv0,8580
9
9
  exaaiagnt/agents/shared_memory.py,sha256=4Ps_kcUHIRVhpumlaF5ZGXmpoeAFi9JrJgEBK33qB84,10774
10
10
  exaaiagnt/agents/state.py,sha256=Tzjdegq3L4IS26XPlK9GQDcwqf6X-KKvih9_4LcXPYw,5881
11
+ exaaiagnt/dashboard/server.py,sha256=jmx6UwBm-Lpso5izf-W0pclqrTDoyBeF89B9l11eemY,3332
12
+ exaaiagnt/dashboard/templates/index.html,sha256=d5_Uwl2bNTm8Ynb_jlOoXftlrZ7F1KptzZBdxyDK3F0,11751
11
13
  exaaiagnt/interface/__init__.py,sha256=ww23sFOQhICEIrIo0MtwWv2qHW5qUprvPj8QVjv3SM0,44
12
14
  exaaiagnt/interface/assets/tui_styles.tcss,sha256=iwhx72f5bIiSg72168ifyR_Q0TKfL3Bf0lU6cyJw1Bg,12114
13
- exaaiagnt/interface/cli.py,sha256=nRKMGJXV884CfT6znU7qWPumMhefUVEnHgDZzD-LoPs,9480
14
- exaaiagnt/interface/main.py,sha256=PO62c4P26qT9v2nLWkfQSC_SWjaCncaxdZUT42U9Lu0,19483
15
+ exaaiagnt/interface/cli.py,sha256=vTkeNELONfXwzQ_XgPhX3UcHVSbOBL_E6USnkRZtM74,9855
16
+ exaaiagnt/interface/main.py,sha256=MK8Y8u43P3Y3hvHpR84NPL49YOA0jRR64DzZocWZn7M,20107
15
17
  exaaiagnt/interface/tool_components/__init__.py,sha256=Dz5ci3VMzvhlPOwQ2x9Nd11cmFzx1OP7sdlpZPMTT4k,935
16
18
  exaaiagnt/interface/tool_components/agents_graph_renderer.py,sha256=eVFRqmJ-TxyxZ-hssLTweDAio4UvsZZgxo2dKky0N1U,4399
17
19
  exaaiagnt/interface/tool_components/base_renderer.py,sha256=P0zYeRnbkr2NYoE8KDQmj1TzrAGX6r7qLMb4Sw7AoTI,1905
@@ -28,12 +30,12 @@ exaaiagnt/interface/tool_components/terminal_renderer.py,sha256=-ORL2vBH5XImUZrI
28
30
  exaaiagnt/interface/tool_components/thinking_renderer.py,sha256=-MQLkKCgOJksrustULFf2jhAjJrP5bbfS2BQ6zgNKCc,927
29
31
  exaaiagnt/interface/tool_components/user_message_renderer.py,sha256=6gHJ1hG-pwcTsxLM7JuYZuaDu8cZ2MeOuUDF3LGy-4I,1432
30
32
  exaaiagnt/interface/tool_components/web_search_renderer.py,sha256=JnJa22ACIcRksfxxdenesUo8Th9cHSxo-fej9YcuYHs,911
31
- exaaiagnt/interface/tui.py,sha256=bMuIJvrauCStAJkjvRWzDueLDraKnrHWtC5ZjyuDxOc,49970
33
+ exaaiagnt/interface/tui.py,sha256=lPEqiW-_Un8dtP48311ksmqGs4LkGfxnz0r0nEQmfWA,49970
32
34
  exaaiagnt/interface/utils.py,sha256=xp6eDOC8C0c3cjt791S_jBDs1B-xp_ydIb74QnMLEt8,20219
33
35
  exaaiagnt/llm/__init__.py,sha256=hUVixjSSIUtwIP2I5D_9e6Kdxhhunnajgxx_2DEYNww,1095
34
36
  exaaiagnt/llm/config.py,sha256=HQ0skwQxtHwiDLDWBCU1Fp4UoQ8tbrTNQw9s7JGVaiY,3303
35
37
  exaaiagnt/llm/fallback.py,sha256=oPS0PGRxEHnyyBgS4yP9zdwSf4JFJh4dYZ3g8OFwWEE,11413
36
- exaaiagnt/llm/llm.py,sha256=D9VnU73ffz7GhotGqQ5RZDufQ8xPKz8aouU23oW0kPY,18664
38
+ exaaiagnt/llm/llm.py,sha256=pMMRaHCnpD29iJR3ZTotqiONJ5JNI_Bc4Aem1ThZJl8,19858
37
39
  exaaiagnt/llm/llm_traffic_controller.py,sha256=DIgJvjrT0MIOStb8g2wTAMoDXp9YLEBlFD5Bwt862K8,12304
38
40
  exaaiagnt/llm/memory_compressor.py,sha256=_At7e5QlDv2vrUDUJMEwm4CjNJ2uGYQsOBhiHiQvVr8,7054
39
41
  exaaiagnt/llm/output_processor.py,sha256=JC3TtzYj9DJhJRuKzz_VV3WIwAyYhUdZeY1N9c4SzVw,13568
@@ -92,12 +94,12 @@ exaaiagnt/prompts/vulnerabilities/websocket_security.jinja,sha256=8xHptbsCCI1W9P
92
94
  exaaiagnt/prompts/vulnerabilities/xss.jinja,sha256=GG1egKjGw9onilu1azEhU2Hfxu9XjQfBA2j4P4KQRzE,8675
93
95
  exaaiagnt/prompts/vulnerabilities/xxe.jinja,sha256=yyWl5i74YiFZIhQknMPWXLKL0C5gwL6Ra_YB4xddJsc,7841
94
96
  exaaiagnt/runtime/__init__.py,sha256=MnuwXAMjvj2kQKJCOyzW-qQSpiipGzmESVWz436lqkQ,760
95
- exaaiagnt/runtime/docker_runtime.py,sha256=lb4cipUbCkwPw3_BxKyICFoIW8Su2zFs_sFW-r59Nfk,16185
97
+ exaaiagnt/runtime/docker_runtime.py,sha256=anlBMLwlJ1RFprp8Zs29hN03NHQyOlKDMoY9jzZcZzI,16641
96
98
  exaaiagnt/runtime/runtime.py,sha256=PRYByipRG9OhtIphIruGBSe4k-glVnYAXH68mikqt78,730
97
99
  exaaiagnt/runtime/tool_manager.py,sha256=3PSUxTmGsFptNlKkZDnzXyyBA_AfbKvqhMPyRi6_fMU,14770
98
100
  exaaiagnt/runtime/tool_server.py,sha256=e23TJYL5w32gMqLS9UJ0xw3XZ4lM38ETBvHVHKk3APU,6835
99
101
  exaaiagnt/telemetry/__init__.py,sha256=8QLHMvrVNLlGKezWTf3mTSmTOLIvDS5xVciry1KVS1Y,130
100
- exaaiagnt/telemetry/tracer.py,sha256=DZuQ-xGdPWB2FKEn_rRPxP1RO2_acXQRkHzuhApAI88,12582
102
+ exaaiagnt/telemetry/tracer.py,sha256=W7jWmsy41LZG_rvRCFRxa6Pxt6tAx8i5O3rW2dM6QUA,13299
101
103
  exaaiagnt/tools/__init__.py,sha256=1Z4EECFThur9Bn_EkqVHJYx9Z3pM1cdh6qXc3rTcyPA,4000
102
104
  exaaiagnt/tools/agents_graph/__init__.py,sha256=FLJ2kGxXICY2pRKrC0sgIc3w3KhZo7VID7hbwYcgBfM,278
103
105
  exaaiagnt/tools/agents_graph/agents_graph_actions.py,sha256=ujBj3R3MpOmIVsN4T3nekVhreu60xA2EEg9rYXCdz2c,21103
@@ -108,7 +110,7 @@ exaaiagnt/tools/browser/browser_actions.py,sha256=6p3drOznDLPVnqSo3CnJjq4qXXSvCh
108
110
  exaaiagnt/tools/browser/browser_actions_schema.xml,sha256=zZQD8sZWsNnxxnlY3QYDrfKHFu0x6i1vtfSOlIGyiho,9573
109
111
  exaaiagnt/tools/browser/browser_instance.py,sha256=vgb-dJP20SLaBOhFExn-uwfjdISBrHJpG7ewfiKQkYU,18605
110
112
  exaaiagnt/tools/browser/tab_manager.py,sha256=SMkDFOgEr3ADK2rP5Ko2uK-A24-8p0VsbjoxEj5eU-Y,13011
111
- exaaiagnt/tools/executor.py,sha256=YWw8Tz3MT3EgK9InLSjamkBWSY7sNcFziBU7Z1BMpMk,10869
113
+ exaaiagnt/tools/executor.py,sha256=nglkwrW_Tt0ZAkn9CQUheoOv6Ac5ZaKoRmwCGWY0uIU,11465
112
114
  exaaiagnt/tools/file_edit/__init__.py,sha256=8f6VlEoGP627hGtcdLkr63vdrTmdb8uyPIqB0qVgZd8,141
113
115
  exaaiagnt/tools/file_edit/file_edit_actions.py,sha256=w-rEB9MphijtXJUyypttQ4DLU4y9rwHd7Zpl21LTTis,3955
114
116
  exaaiagnt/tools/file_edit/file_edit_actions_schema.xml,sha256=tt0_QgSjOtTEy8ordsXUcEiVTePiybgn4aisZJ_qnbc,5477
@@ -116,12 +118,14 @@ exaaiagnt/tools/finish/__init__.py,sha256=QIMaHYusly8YaFR3zjYsy_CFawwgtTZvOU7gsE
116
118
  exaaiagnt/tools/finish/finish_actions.py,sha256=ZLyOuYMjdTPHN9_6sQxyHwp3-BVSusk5dM2WHUn3WMI,5714
117
119
  exaaiagnt/tools/finish/finish_actions_schema.xml,sha256=CS6Vq3ByyNxv2spRWS4oJcmUzWQRB7jvUAOS8aiWd8o,2294
118
120
  exaaiagnt/tools/k8s_scanner/__init__.py,sha256=MIo_Hl4p7Zzea9KSR_wVnxnlyyrB9eS8g3DlZxwSsHk,557
119
- exaaiagnt/tools/k8s_scanner/k8s_actions.py,sha256=2iK-PchYw9X8XZvx5tmEQwIi6ckA153lolbNpN5sm3g,13365
121
+ exaaiagnt/tools/k8s_scanner/k8s_actions.py,sha256=1Ow7uWTzAgOgQLjpH0M00YIycZ6bMsjXU2FgCrE-1m4,13477
122
+ exaaiagnt/tools/k8s_scanner/k8s_actions_schema.xml,sha256=s-Qnq8FX6zjT6qIRHG81dq9zSToj6BbnntLGQ-vWzhM,1272
120
123
  exaaiagnt/tools/notes/__init__.py,sha256=DUpkZUWN21tb9AXCWfJLrKrgLz9YEBVU8KQy1J6cyxU,189
121
124
  exaaiagnt/tools/notes/notes_actions.py,sha256=8ewd1kCxZO_ujKP2oXuU03r9p8EZEYgoO7i_6GWw_VA,5775
122
125
  exaaiagnt/tools/notes/notes_actions_schema.xml,sha256=nzirWDyzbPRxaG3jdSjmSCjfY-ggDy1kH7oxAuj-osw,6264
123
126
  exaaiagnt/tools/prompt_injection/__init__.py,sha256=YjzfqLYKdOuEeV8WgE_b9swnP2uhYtOlPbeRplKOTzc,645
124
- exaaiagnt/tools/prompt_injection/prompt_injection_actions.py,sha256=xvg_MrRAjx9E1vumIJYK8utf34JjTupF3S4BVDV2cVM,24942
127
+ exaaiagnt/tools/prompt_injection/prompt_injection_actions.py,sha256=yNJv7ESHBMvzFiNZGI8Jc8m0GwG73Z5yIngYEmZTOic,27903
128
+ exaaiagnt/tools/prompt_injection/prompt_injection_actions_schema.xml,sha256=wZHYpaS35tOEuzY8j1-4JkkLgDWvFhJwm7MzlRGpfyg,1381
125
129
  exaaiagnt/tools/proxy/__init__.py,sha256=K3BFmT6QWw9heB7l7DnmI-Yj-DOI3BAow6g0GLWsA0c,329
126
130
  exaaiagnt/tools/proxy/proxy_actions.py,sha256=HyfI6iUVkRSnzLR_xNRKWhr4roDEAatGyYTq4wij2Js,2549
127
131
  exaaiagnt/tools/proxy/proxy_actions_schema.xml,sha256=d87SUKD2J16KQCCFpI5w2y-Ax1NgwOuGO3crr1bFPDw,11567
@@ -129,11 +133,11 @@ exaaiagnt/tools/proxy/proxy_manager.py,sha256=rmy8c_7jcYl9WeeA3O2lcZBnN7hESpTYxo
129
133
  exaaiagnt/tools/python/__init__.py,sha256=l5mFWKbtis61S8lOQY-gFn-vIlc13lu1K-pqqRywOCk,72
130
134
  exaaiagnt/tools/python/python_actions.py,sha256=ggLFFLW7cQamY1fvgUxzz9beOy_deY1zdouK_K8iF50,1436
131
135
  exaaiagnt/tools/python/python_actions_schema.xml,sha256=XcIUjz5uEoI7J3VFcby0O-0ovm9r9cIoADfpqDGJXlU,7122
132
- exaaiagnt/tools/python/python_instance.py,sha256=kYjyPIQG5QmYoKG4xMcb2wPyMcUO3R3A9vXINpXx6dQ,6030
136
+ exaaiagnt/tools/python/python_instance.py,sha256=pXF8MFVjxNeIUt6h_f2Oe5gjFEYJ-NM5Lqc6A3PUi40,6246
133
137
  exaaiagnt/tools/python/python_manager.py,sha256=C_k8C8uQ6ESpAgzhbf3km9jeFyI52SGd2m36mD1ErhM,4235
134
138
  exaaiagnt/tools/registry.py,sha256=iBRwtiWLQr3fo1vSAOehWbIlr6cqnNPLB1dluXtsf8s,6029
135
139
  exaaiagnt/tools/reporting/__init__.py,sha256=_cYxb3OP0vZtCwO_ExLBjhAn1ECaG-SH1Z4wfGDyT1Y,110
136
- exaaiagnt/tools/reporting/reporting_actions.py,sha256=aVEwfG5GgJ68bFJOicO_YD2yp5wCimxlnZzpXX3TJcQ,2200
140
+ exaaiagnt/tools/reporting/reporting_actions.py,sha256=CYAXz5ee_eN-zFxBdhJ-QsmtCECulnf9m44y2SrX4kk,3107
137
141
  exaaiagnt/tools/reporting/reporting_actions_schema.xml,sha256=y_g0iuyBuCh79fvA0ri8fOPlXY7uUd-P-mdzXLUyIJg,1629
138
142
  exaaiagnt/tools/response_analyzer.py,sha256=0B8LZ3pvBImH40t8QqCtLdxwc28Chq-2C8I23zi-XJM,10647
139
143
  exaaiagnt/tools/smart_fuzzer.py,sha256=cN9TgJcLz3MzDuBIP8EVtEdoJL67LcrFBc2zo9j53cE,12367
@@ -149,10 +153,10 @@ exaaiagnt/tools/tool_prompts.py,sha256=eQL7B8H8mo6d6mvtN_X9rmSfjwgz9Cuzfg7C7WH6T
149
153
  exaaiagnt/tools/vuln_validator.py,sha256=jViG2-3pVBwI3VXe5JsgCFsDBNk9nmT_s2vEe7cZjmA,14025
150
154
  exaaiagnt/tools/waf_bypass.py,sha256=71oPWnDHjn2EHi6I1SluZCKfqfXkA5j61oIkL5kNoSw,12047
151
155
  exaaiagnt/tools/web_search/__init__.py,sha256=m5PCHXqeNVraLRLNIbh54Z2N4Y_75d-ftqwyq3dbCd0,70
152
- exaaiagnt/tools/web_search/web_search_actions.py,sha256=jmlN2uIq8lRbhRnyaMQkC-44jhpfkLQZ_byYNlNNlOY,3111
156
+ exaaiagnt/tools/web_search/web_search_actions.py,sha256=k9zjzdqTVsNdh0pFtntIfwO77_ThQIedXSaUhJxPs2U,3295
153
157
  exaaiagnt/tools/web_search/web_search_actions_schema.xml,sha256=Ihc3Gv4LaPI_MzBbwZOt3y4pwg9xmtl8KfPNvFihEP4,4805
154
- exaai_agent-2.1.2.dist-info/METADATA,sha256=OjaCfqExnqZH9WLe1FU-hfJk7sJndMruISsjGZnlMvQ,12762
155
- exaai_agent-2.1.2.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
156
- exaai_agent-2.1.2.dist-info/entry_points.txt,sha256=iLSTRDSjN8Zyc2Wo6WXmr4MtyEWkdqtZ1j_Hx73MiUs,137
157
- exaai_agent-2.1.2.dist-info/licenses/LICENSE,sha256=RV6IGl0sWdfbbtJmjPr1w_qwGyGt2jv02PXsAzN_kNs,11460
158
- exaai_agent-2.1.2.dist-info/RECORD,,
158
+ exaai_agent-2.2.1.dist-info/METADATA,sha256=_N3L88kQUscOnVK0NuHGAjH0MZXidMNpNiSm9_nWJJ0,13462
159
+ exaai_agent-2.2.1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
160
+ exaai_agent-2.2.1.dist-info/entry_points.txt,sha256=iLSTRDSjN8Zyc2Wo6WXmr4MtyEWkdqtZ1j_Hx73MiUs,137
161
+ exaai_agent-2.2.1.dist-info/licenses/LICENSE,sha256=RV6IGl0sWdfbbtJmjPr1w_qwGyGt2jv02PXsAzN_kNs,11460
162
+ exaai_agent-2.2.1.dist-info/RECORD,,
@@ -0,0 +1,99 @@
1
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
2
+ from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import HTMLResponse
4
+ import uvicorn
5
+ import json
6
+ import asyncio
7
+ import logging
8
+ from typing import List, Dict, Any
9
+ import os
10
+
11
+ from exaaiagnt.telemetry.tracer import get_global_tracer
12
+
13
+ app = FastAPI(title="ExaAi Live Dashboard")
14
+
15
+ # Serve static files
16
+ static_dir = os.path.join(os.path.dirname(__file__), "static")
17
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
18
+
19
+ class ConnectionManager:
20
+ def __init__(self):
21
+ self.active_connections: List[WebSocket] = []
22
+
23
+ async def connect(self, websocket: WebSocket):
24
+ await websocket.accept()
25
+ self.active_connections.append(websocket)
26
+
27
+ def disconnect(self, websocket: WebSocket):
28
+ self.active_connections.remove(websocket)
29
+
30
+ async def broadcast(self, message: str):
31
+ for connection in self.active_connections:
32
+ try:
33
+ await connection.send_text(message)
34
+ except Exception:
35
+ pass
36
+
37
+ manager = ConnectionManager()
38
+
39
+ @app.get("/")
40
+ async def get_dashboard():
41
+ html_path = os.path.join(os.path.dirname(__file__), "templates", "index.html")
42
+ with open(html_path, "r") as f:
43
+ return HTMLResponse(content=f.read())
44
+
45
+ @app.get("/api/stats")
46
+ async def get_stats():
47
+ tracer = get_global_tracer()
48
+ if not tracer:
49
+ return {"status": "Waiting for agent..."}
50
+
51
+ return {
52
+ "agents_count": len(tracer.agents),
53
+ "vulnerabilities": len(tracer.vulnerability_reports),
54
+ "tool_calls": len(tracer.tool_executions),
55
+ "start_time": tracer.start_time,
56
+ "run_name": tracer.run_name
57
+ }
58
+
59
+ @app.get("/api/vulnerabilities")
60
+ async def get_vulns():
61
+ tracer = get_global_tracer()
62
+ if not tracer:
63
+ return []
64
+ return tracer.vulnerability_reports
65
+
66
+ @app.websocket("/ws")
67
+ async def websocket_endpoint(websocket: WebSocket):
68
+ await manager.connect(websocket)
69
+ try:
70
+ while True:
71
+ # Send updates every second
72
+ tracer = get_global_tracer()
73
+ if tracer:
74
+ data = {
75
+ "agents": tracer.agents,
76
+ "stats": {
77
+ "active": sum(1 for a in tracer.agents.values() if a.get("status") == "running"),
78
+ "completed": sum(1 for a in tracer.agents.values() if a.get("status") == "completed"),
79
+ "failed": sum(1 for a in tracer.agents.values() if a.get("status") == "failed"),
80
+ },
81
+ "recent_logs": tracer.chat_messages[-10:] if tracer.chat_messages else []
82
+ }
83
+ await websocket.send_json(data)
84
+ await asyncio.sleep(1)
85
+ except WebSocketDisconnect:
86
+ manager.disconnect(websocket)
87
+ except Exception as e:
88
+ logging.error(f"WebSocket error: {e}")
89
+ manager.disconnect(websocket)
90
+
91
+ def start_dashboard(host="0.0.0.0", port=8000):
92
+ """Start the dashboard server in a background thread or process."""
93
+ config = uvicorn.Config(app, host=host, port=port, log_level="error")
94
+ server = uvicorn.Server(config)
95
+ # We'll run this in a thread from the main agent
96
+ import threading
97
+ t = threading.Thread(target=server.run, daemon=True)
98
+ t.start()
99
+ return t
@@ -0,0 +1,232 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ExaAi Mission Control</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <style>
10
+ body { background-color: #0f172a; color: #e2e8f0; font-family: 'Courier New', Courier, monospace; }
11
+ .neon-text { text-shadow: 0 0 5px #3b82f6, 0 0 10px #3b82f6; }
12
+ .neon-border { box-shadow: 0 0 5px #3b82f6; border-color: #3b82f6; }
13
+ .agent-card { transition: all 0.3s ease; }
14
+ .agent-card:hover { transform: translateY(-2px); box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
15
+ .vuln-critical { border-left: 4px solid #ef4444; background: rgba(239, 68, 68, 0.1); }
16
+ .vuln-high { border-left: 4px solid #f97316; background: rgba(249, 115, 22, 0.1); }
17
+ .vuln-medium { border-left: 4px solid #eab308; background: rgba(234, 179, 8, 0.1); }
18
+ ::-webkit-scrollbar { width: 8px; }
19
+ ::-webkit-scrollbar-track { background: #1e293b; }
20
+ ::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
21
+ ::-webkit-scrollbar-thumb:hover { background: #64748b; }
22
+ </style>
23
+ </head>
24
+ <body class="h-screen flex flex-col overflow-hidden">
25
+ <!-- Header -->
26
+ <header class="bg-slate-900 border-b border-slate-700 p-4 flex justify-between items-center shadow-lg z-10">
27
+ <div class="flex items-center gap-3">
28
+ <div class="w-3 h-3 rounded-full bg-green-500 animate-pulse"></div>
29
+ <h1 class="text-2xl font-bold tracking-wider text-blue-400 neon-text">EXAAI <span class="text-white">MISSION CONTROL</span></h1>
30
+ </div>
31
+ <div class="flex gap-6 text-sm">
32
+ <div class="flex flex-col items-end">
33
+ <span class="text-slate-400">STATUS</span>
34
+ <span class="text-green-400 font-bold">ONLINE</span>
35
+ </div>
36
+ <div class="flex flex-col items-end">
37
+ <span class="text-slate-400">AGENTS</span>
38
+ <span id="total-agents" class="text-blue-400 font-bold">0</span>
39
+ </div>
40
+ <div class="flex flex-col items-end">
41
+ <span class="text-slate-400">VULNERABILITIES</span>
42
+ <span id="total-vulns" class="text-red-500 font-bold neon-text">0</span>
43
+ </div>
44
+ </div>
45
+ </header>
46
+
47
+ <!-- Main Content -->
48
+ <main class="flex-1 flex overflow-hidden">
49
+ <!-- Sidebar: Agent Tree -->
50
+ <aside class="w-1/4 bg-slate-900 border-r border-slate-700 flex flex-col">
51
+ <div class="p-3 border-b border-slate-700 bg-slate-800">
52
+ <h2 class="font-bold text-slate-300 flex items-center gap-2">
53
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
54
+ Active Operations
55
+ </h2>
56
+ </div>
57
+ <div id="agent-list" class="flex-1 overflow-y-auto p-3 space-y-2">
58
+ <!-- Agents injected here -->
59
+ </div>
60
+ </aside>
61
+
62
+ <!-- Center: Live Feed & Stats -->
63
+ <section class="flex-1 flex flex-col bg-slate-900/50 relative">
64
+ <!-- Matrix Rain Background Effect (Optional/Subtle) -->
65
+ <div class="absolute inset-0 pointer-events-none opacity-5 bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0IiBoZWlnaHQ9IjQiPgo8cmVjdCB3aWR0aD0iNCIgaGVpZ2h0PSI0IiBmaWxsPSIjMWUyOTNiIi8+CjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjEiIGZpbGw9IiMzMzQxNTUiLz4KPC9zdmc+')]"></div>
66
+
67
+ <div class="h-1/2 border-b border-slate-700 flex">
68
+ <!-- Live Terminal Log -->
69
+ <div class="w-2/3 border-r border-slate-700 flex flex-col">
70
+ <div class="p-2 bg-slate-800 border-b border-slate-700 flex justify-between">
71
+ <span class="text-xs font-mono text-slate-400">>_ SYSTEM LOGS</span>
72
+ <span class="text-xs text-green-500">● LIVE</span>
73
+ </div>
74
+ <div id="console-logs" class="flex-1 overflow-y-auto p-4 font-mono text-xs text-slate-300 space-y-1 bg-black/40">
75
+ <!-- Logs injected here -->
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Quick Stats Chart -->
80
+ <div class="w-1/3 p-4 bg-slate-800/30 flex flex-col items-center justify-center">
81
+ <canvas id="statusChart"></canvas>
82
+ </div>
83
+ </div>
84
+
85
+ <!-- Vulnerabilities Panel -->
86
+ <div class="h-1/2 flex flex-col bg-slate-900">
87
+ <div class="p-3 border-b border-slate-700 bg-slate-800 flex justify-between items-center">
88
+ <h2 class="font-bold text-red-400 flex items-center gap-2">
89
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
90
+ Detected Vulnerabilities
91
+ </h2>
92
+ <span class="text-xs text-slate-500">Auto-refreshing...</span>
93
+ </div>
94
+ <div id="vuln-list" class="flex-1 overflow-y-auto p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
95
+ <!-- Vulns injected here -->
96
+ </div>
97
+ </div>
98
+ </section>
99
+ </main>
100
+
101
+ <script>
102
+ const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
103
+ const socket = new WebSocket(`${wsProtocol}//${window.location.host}/ws`);
104
+
105
+ // Chart setup
106
+ const ctx = document.getElementById('statusChart').getContext('2d');
107
+ const statusChart = new Chart(ctx, {
108
+ type: 'doughnut',
109
+ data: {
110
+ labels: ['Active', 'Completed', 'Failed'],
111
+ datasets: [{
112
+ data: [0, 0, 0],
113
+ backgroundColor: ['#3b82f6', '#22c55e', '#ef4444'],
114
+ borderWidth: 0
115
+ }]
116
+ },
117
+ options: {
118
+ responsive: true,
119
+ plugins: {
120
+ legend: { position: 'bottom', labels: { color: '#94a3b8' } }
121
+ },
122
+ cutout: '70%'
123
+ }
124
+ });
125
+
126
+ socket.onmessage = function(event) {
127
+ const data = JSON.parse(event.data);
128
+ updateDashboard(data);
129
+ };
130
+
131
+ function updateDashboard(data) {
132
+ // Update Headers
133
+ document.getElementById('total-agents').textContent = Object.keys(data.agents).length;
134
+
135
+ // Update Agent List
136
+ const agentList = document.getElementById('agent-list');
137
+ agentList.innerHTML = '';
138
+
139
+ Object.values(data.agents).forEach(agent => {
140
+ const div = document.createElement('div');
141
+ div.className = `agent-card p-3 rounded bg-slate-800 border border-slate-700 ${agent.status === 'running' ? 'border-l-4 border-l-blue-500' : ''}`;
142
+ div.innerHTML = `
143
+ <div class="flex justify-between items-start mb-1">
144
+ <span class="font-bold text-xs text-slate-200 truncate" title="${agent.name}">${agent.name}</span>
145
+ <span class="text-[10px] px-1.5 py-0.5 rounded ${getStatusColor(agent.status)}">${agent.status}</span>
146
+ </div>
147
+ <div class="text-[10px] text-slate-500 truncate">${agent.id.substring(0, 8)}...</div>
148
+ <div class="text-[11px] text-slate-400 mt-1 line-clamp-2">${agent.task || 'No task description'}</div>
149
+ `;
150
+ agentList.appendChild(div);
151
+ });
152
+
153
+ // Update Logs
154
+ const logContainer = document.getElementById('console-logs');
155
+ logContainer.innerHTML = '';
156
+ data.recent_logs.forEach(log => {
157
+ const div = document.createElement('div');
158
+ const time = new Date(log.timestamp * 1000).toLocaleTimeString();
159
+ const color = log.role === 'assistant' ? 'text-blue-400' : 'text-slate-400';
160
+ div.innerHTML = `<span class="text-slate-600">[${time}]</span> <span class="${color}">${log.role.toUpperCase()}:</span> <span class="text-slate-300">${escapeHtml(log.content.substring(0, 150))}...</span>`;
161
+ logContainer.appendChild(div);
162
+ });
163
+
164
+ // Update Chart
165
+ statusChart.data.datasets[0].data = [
166
+ data.stats.active,
167
+ data.stats.completed,
168
+ data.stats.failed
169
+ ];
170
+ statusChart.update();
171
+
172
+ // Update Vulns (Fetching separately or from WS if included)
173
+ fetchVulns();
174
+ }
175
+
176
+ async function fetchVulns() {
177
+ try {
178
+ const response = await fetch('/api/vulnerabilities');
179
+ const vulns = await response.json();
180
+ document.getElementById('total-vulns').textContent = vulns.length;
181
+
182
+ const container = document.getElementById('vuln-list');
183
+ container.innerHTML = '';
184
+
185
+ if (vulns.length === 0) {
186
+ container.innerHTML = '<div class="col-span-full text-center text-slate-500 py-10">No vulnerabilities detected... yet.</div>';
187
+ return;
188
+ }
189
+
190
+ vulns.forEach(v => {
191
+ const div = document.createElement('div');
192
+ const severityClass = `vuln-${v.severity.toLowerCase()}`;
193
+ div.className = `p-4 rounded bg-slate-800 border border-slate-700 ${severityClass}`;
194
+ div.innerHTML = `
195
+ <div class="flex justify-between items-center mb-2">
196
+ <h3 class="font-bold text-sm text-white">${v.title}</h3>
197
+ <span class="text-[10px] font-bold uppercase tracking-wider px-2 py-1 bg-black/30 rounded text-white">${v.severity}</span>
198
+ </div>
199
+ <p class="text-xs text-slate-400 line-clamp-3 mb-2">${v.content}</p>
200
+ <div class="text-[10px] text-slate-500">ID: ${v.report_id}</div>
201
+ `;
202
+ container.appendChild(div);
203
+ });
204
+ } catch (e) {
205
+ console.error("Failed to fetch vulns", e);
206
+ }
207
+ }
208
+
209
+ function getStatusColor(status) {
210
+ switch(status) {
211
+ case 'running': return 'bg-blue-900 text-blue-300';
212
+ case 'completed': return 'bg-green-900 text-green-300';
213
+ case 'failed': return 'bg-red-900 text-red-300';
214
+ default: return 'bg-slate-700 text-slate-300';
215
+ }
216
+ }
217
+
218
+ function escapeHtml(text) {
219
+ if (!text) return "";
220
+ return text
221
+ .replace(/&/g, "&amp;")
222
+ .replace(/</g, "&lt;")
223
+ .replace(/>/g, "&gt;")
224
+ .replace(/"/g, "&quot;")
225
+ .replace(/'/g, "&#039;");
226
+ }
227
+
228
+ // Initial load
229
+ fetchVulns();
230
+ </script>
231
+ </body>
232
+ </html>
@@ -30,7 +30,18 @@ BANNER = r"""
30
30
  """
31
31
 
32
32
 
33
+ from exaaiagnt.dashboard.server import start_dashboard
34
+
33
35
  async def run_cli(args: Any) -> None: # noqa: PLR0915
36
+ # Start the live dashboard
37
+ try:
38
+ start_dashboard()
39
+ console_temp = Console()
40
+ console_temp.print("[bold green]🚀 Live Dashboard available at: http://localhost:8000[/]")
41
+ except Exception as e:
42
+ import logging
43
+ logging.error(f"Failed to start dashboard: {e}")
44
+
34
45
  # Detect if running in a real terminal or headless (pipe/background)
35
46
  is_tty = sys.stdout.isatty()
36
47
  console = Console(force_terminal=is_tty, no_color=not is_tty)
@@ -224,7 +224,17 @@ async def warm_up_llm() -> None:
224
224
  error_text.append("\n\n", style="white")
225
225
  error_text.append("Could not establish connection to the language model.\n", style="white")
226
226
  error_text.append("Please check your configuration and try again.\n", style="white")
227
- error_text.append(f"\nError: {e}", style="dim white")
227
+
228
+ # Enhanced error diagnosis
229
+ error_str = str(e)
230
+ if "AuthenticationError" in error_str:
231
+ error_text.append("🔍 Hint: Your API key seems invalid or expired.\n", style="bold yellow")
232
+ elif "NotFoundError" in error_str:
233
+ error_text.append(f"🔍 Hint: The model '{model_name}' was not found. Check if the model name is correct.\n", style="bold yellow")
234
+ elif "ConnectionError" in error_str:
235
+ error_text.append("🔍 Hint: Network connection failed. Check your internet or proxy settings.\n", style="bold yellow")
236
+
237
+ error_text.append(f"\nError Details: {e}", style="dim white")
228
238
 
229
239
  panel = Panel(
230
240
  error_text,
@@ -242,7 +252,7 @@ async def warm_up_llm() -> None:
242
252
 
243
253
  def get_version() -> str:
244
254
  """Get the current ExaAi version."""
245
- return "2.1.2"
255
+ return "2.2.1"
246
256
 
247
257
 
248
258
  def parse_arguments() -> argparse.Namespace:
@@ -45,7 +45,7 @@ def get_package_version() -> str:
45
45
  return pkg_version("exaai-agent")
46
46
  except PackageNotFoundError:
47
47
  # Fallback version if package not installed
48
- return "2.1.2"
48
+ return "2.2.1"
49
49
 
50
50
 
51
51
  class ChatTextArea(TextArea): # type: ignore[misc]
@@ -80,7 +80,7 @@ class SplashScreen(Static): # type: ignore[misc]
80
80
  NEON_ORANGE = "#ff8800"
81
81
  SOFT_WHITE = "#e0e0e0"
82
82
 
83
- # Enhanced ASCII Logo - ExaAi v2.1.2
83
+ # Enhanced ASCII Logo - ExaAi v2.2.1
84
84
  BANNER = r"""
85
85
  ███████╗██╗ ██╗ █████╗ █████╗ ██╗
86
86
  ██╔════╝╚██╗██╔╝██╔══██╗ ██╔══██╗██║
exaaiagnt/llm/llm.py CHANGED
@@ -424,19 +424,54 @@ class LLM:
424
424
  else:
425
425
  completion_args["reasoning_effort"] = "high"
426
426
 
427
- # Use Adaptive Traffic Controller for intelligent rate limiting
427
+ # Use Adaptive Traffic Controller for intelligent rate limiting with retries
428
428
  controller = get_traffic_controller()
429
429
  agent_id = self.agent_id or "unknown_agent"
430
430
 
431
431
  async def do_request():
432
- from litellm import completion
433
- return completion(**completion_args, stream=False)
434
-
435
- response = await controller.queue_request(
436
- do_request,
437
- agent_id=agent_id,
438
- priority=RequestPriority.NORMAL
432
+ try:
433
+ from litellm import completion
434
+ return await completion(**completion_args, stream=False)
435
+ except litellm.RateLimitError:
436
+ # Let tenacity (in controller) handle retry
437
+ raise
438
+ except Exception as e:
439
+ # Log other transient errors for potential retry
440
+ logger.warning(f"Transient LLM error: {e}")
441
+ raise
442
+
443
+ # Wrap in retry logic
444
+ from tenacity import (
445
+ retry,
446
+ stop_after_attempt,
447
+ wait_exponential,
448
+ retry_if_exception_type,
449
+ )
450
+ import litellm
451
+
452
+ @retry(
453
+ retry=retry_if_exception_type((
454
+ litellm.RateLimitError,
455
+ litellm.ServiceUnavailableError,
456
+ litellm.APIConnectionError,
457
+ litellm.Timeout
458
+ )),
459
+ wait=wait_exponential(multiplier=1, min=4, max=60),
460
+ stop=stop_after_attempt(5),
461
+ reraise=True
439
462
  )
463
+ async def execute_with_retries():
464
+ return await controller.queue_request(
465
+ do_request,
466
+ agent_id=agent_id,
467
+ priority=RequestPriority.NORMAL
468
+ )
469
+
470
+ try:
471
+ response = await execute_with_retries()
472
+ except Exception:
473
+ self._total_stats.failed_requests += 1
474
+ raise
440
475
 
441
476
  self._total_stats.requests += 1
442
477
  self._last_request_stats = RequestStats(requests=1)
@@ -104,6 +104,14 @@ class DockerRuntime(AbstractRuntime):
104
104
  self._tool_server_port = tool_server_port
105
105
  self._tool_server_token = tool_server_token
106
106
 
107
+ # Mount kubeconfig if available
108
+ volumes = {}
109
+ kube_config = os.path.expanduser("~/.kube")
110
+ if os.path.exists(kube_config):
111
+ volumes[kube_config] = {'bind': '/root/.kube', 'mode': 'ro'}
112
+ # Also mount for pentester user
113
+ volumes[kube_config] = {'bind': '/home/pentester/.kube', 'mode': 'ro'}
114
+
107
115
  container = self.client.containers.run(
108
116
  EXAAI_IMAGE,
109
117
  command="sleep infinity",
@@ -114,6 +122,7 @@ class DockerRuntime(AbstractRuntime):
114
122
  f"{caido_port}/tcp": caido_port,
115
123
  f"{tool_server_port}/tcp": tool_server_port,
116
124
  },
125
+ volumes=volumes,
117
126
  cap_add=["NET_ADMIN", "NET_RAW"],
118
127
  labels={"exaai-scan-id": scan_id},
119
128
  environment={
@@ -137,19 +137,35 @@ class Tracer:
137
137
  ) -> int:
138
138
  message_id = self._next_message_id
139
139
  self._next_message_id += 1
140
+
141
+ # Ensure imports if missing
142
+ import time
140
143
 
141
144
  message_data = {
142
145
  "message_id": message_id,
143
146
  "content": content,
144
147
  "role": role,
145
148
  "agent_id": agent_id,
146
- "timestamp": datetime.now(UTC).isoformat(),
149
+ "timestamp": time.time(), # Use epoch time for easier frontend handling
147
150
  "metadata": metadata or {},
148
151
  }
149
152
 
150
153
  self.chat_messages.append(message_data)
154
+ # Removed auto-save on every message to improve performance
151
155
  return message_id
152
156
 
157
+ def get_dashboard_data(self) -> dict[str, Any]:
158
+ """Return data formatted for the live dashboard."""
159
+ return {
160
+ "agents": self.agents,
161
+ "vulnerabilities": self.vulnerability_reports,
162
+ "stats": {
163
+ "active_agents": sum(1 for a in self.agents.values() if a.get("status") == "running"),
164
+ "total_requests": sum(1 for msg in self.chat_messages if msg["role"] == "assistant"),
165
+ "total_vulns": len(self.vulnerability_reports)
166
+ }
167
+ }
168
+
153
169
  def log_tool_execution_start(self, agent_id: str, tool_name: str, args: dict[str, Any]) -> int:
154
170
  execution_id = self._next_execution_id
155
171
  self._next_execution_id += 1
@@ -90,11 +90,23 @@ async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwarg
90
90
  if needs_agent_state(tool_name):
91
91
  if agent_state is None:
92
92
  raise ValueError(f"Tool '{tool_name}' requires agent_state but none was provided.")
93
- result = tool_func(agent_state=agent_state, **converted_kwargs)
93
+ # Check if the tool function is a coroutine (async)
94
+ if inspect.iscoroutinefunction(tool_func):
95
+ result = await tool_func(agent_state=agent_state, **converted_kwargs)
96
+ else:
97
+ # Run synchronous blocking tools in a separate thread
98
+ import asyncio
99
+ result = await asyncio.to_thread(tool_func, agent_state=agent_state, **converted_kwargs)
94
100
  else:
95
- result = tool_func(**converted_kwargs)
101
+ # Check if the tool function is a coroutine (async)
102
+ if inspect.iscoroutinefunction(tool_func):
103
+ result = await tool_func(**converted_kwargs)
104
+ else:
105
+ # Run synchronous blocking tools in a separate thread
106
+ import asyncio
107
+ result = await asyncio.to_thread(tool_func, **converted_kwargs)
96
108
 
97
- return await result if inspect.isawaitable(result) else result
109
+ return result
98
110
 
99
111
 
100
112
  def validate_tool_availability(tool_name: str | None) -> tuple[bool, str]:
@@ -10,7 +10,7 @@ Comprehensive security testing for Kubernetes clusters including:
10
10
  - Container vulnerability scanning integration
11
11
 
12
12
  Author: ALhilali
13
- Version: 1.0.0
13
+ Version: 2.2.1
14
14
  """
15
15
 
16
16
  import logging
@@ -55,6 +55,9 @@ class SecurityFinding:
55
55
  status: CheckStatus = CheckStatus.FAIL
56
56
 
57
57
 
58
+ from exaaiagnt.tools.registry import register_tool
59
+
60
+ @register_tool
58
61
  class K8sScanner:
59
62
  """
60
63
  Kubernetes Security Scanner.
@@ -292,6 +295,7 @@ class K8sScanner:
292
295
 
293
296
  # === Convenience Functions ===
294
297
 
298
+ @register_tool
295
299
  def scan_cluster(context: Optional[str] = None) -> Dict[str, Any]:
296
300
  """Run a full cluster scan."""
297
301
  scanner = K8sScanner(context=context)
@@ -299,6 +303,7 @@ def scan_cluster(context: Optional[str] = None) -> Dict[str, Any]:
299
303
  return scanner.export_report()
300
304
 
301
305
 
306
+ @register_tool
302
307
  def check_rbac(namespace: str = "default") -> List[Dict[str, Any]]:
303
308
  """Run RBAC checks only."""
304
309
  scanner = K8sScanner()
@@ -306,6 +311,7 @@ def check_rbac(namespace: str = "default") -> List[Dict[str, Any]]:
306
311
  return scanner.export_report()["findings"]
307
312
 
308
313
 
314
+ @register_tool
309
315
  def check_pod_security(namespace: str = "default") -> List[Dict[str, Any]]:
310
316
  """Run Pod Security checks only."""
311
317
  scanner = K8sScanner()
@@ -0,0 +1,36 @@
1
+ <tool name="scan_cluster">
2
+ <description>
3
+ Run a full security scan on the connected Kubernetes cluster.
4
+ Checks for RBAC misconfigurations, Pod Security Standards violations,
5
+ missing NetworkPolicies, and insecure secret management.
6
+ </description>
7
+ <parameters>
8
+ <parameter name="context" type="string" optional="true">
9
+ The kubeconfig context to use. Defaults to current context.
10
+ </parameter>
11
+ </parameters>
12
+ </tool>
13
+
14
+ <tool name="check_rbac">
15
+ <description>
16
+ Check for dangerous RBAC permissions in a specific namespace.
17
+ Detects wildcard permissions, cluster-admin abuse, and risky role bindings.
18
+ </description>
19
+ <parameters>
20
+ <parameter name="namespace" type="string" optional="true">
21
+ Target namespace. Defaults to "default".
22
+ </parameter>
23
+ </parameters>
24
+ </tool>
25
+
26
+ <tool name="check_pod_security">
27
+ <description>
28
+ Audit pods against Pod Security Standards (PSS).
29
+ Finds privileged containers, host namespace usage, and capability issues.
30
+ </description>
31
+ <parameters>
32
+ <parameter name="namespace" type="string" optional="true">
33
+ Target namespace. Defaults to "default".
34
+ </parameter>
35
+ </parameters>
36
+ </tool>
@@ -504,6 +504,80 @@ What is the date and time?""",
504
504
 
505
505
  # === CONVENIENCE FUNCTIONS ===
506
506
 
507
+ from exaaiagnt.tools.registry import register_tool
508
+
509
+ @register_tool
510
+ def scan_llm_endpoint(
511
+ target_url: str,
512
+ method: str = "POST",
513
+ headers: Optional[Dict[str, str]] = None,
514
+ body_template: str = '{"prompt": "{{prompt}}"}',
515
+ injection_type: Optional[str] = None,
516
+ verbose: bool = False,
517
+ ) -> Dict[str, Any]:
518
+ """
519
+ Scan a remote LLM API endpoint for prompt injection vulnerabilities.
520
+
521
+ Args:
522
+ target_url: URL of the LLM API endpoint
523
+ method: HTTP method (POST, GET)
524
+ headers: JSON dict of headers (e.g. {"Authorization": "Bearer..."})
525
+ body_template: JSON string with {{prompt}} placeholder
526
+ injection_type: Optional filter (direct, jailbreak, etc.)
527
+ verbose: Enable verbose logging
528
+
529
+ Returns:
530
+ Scan summary dictionary
531
+ """
532
+ import requests
533
+
534
+ def target_function(prompt: str) -> str:
535
+ try:
536
+ # Replace placeholder with payload
537
+ # Handle JSON escaping for the payload
538
+ import json
539
+
540
+ # Simple template replacement might break JSON structure if payload has quotes
541
+ # So we parse, inject, then dump
542
+ if method.upper() == "POST" and "{" in body_template:
543
+ try:
544
+ # Try to be smart about JSON templates
545
+ # This is a simplified approach; robust implementation would use a proper template engine
546
+ # or assume body_template is a JSON string where we replace a specific key value
547
+
548
+ # Hacky string replacement for now, assuming user provided a valid template
549
+ # Better: require body_template to be a format string or rely on simple substitution
550
+ final_body = body_template.replace("{{prompt}}", prompt.replace('"', '\\"').replace('\n', '\\n'))
551
+
552
+ # Verify it's valid JSON, if not, fallback or error
553
+ # json.loads(final_body)
554
+
555
+ resp = requests.post(target_url, data=final_body, headers=headers, timeout=30)
556
+ return resp.text
557
+ except Exception:
558
+ # Fallback for non-JSON or complex templates
559
+ return f"Error building request body"
560
+
561
+ # GET method
562
+ if method.upper() == "GET":
563
+ resp = requests.get(target_url, params={"prompt": prompt}, headers=headers, timeout=30)
564
+ return resp.text
565
+
566
+ return ""
567
+ except Exception as e:
568
+ return f"Error calling endpoint: {str(e)}"
569
+
570
+ # Convert string injection_type to Enum if provided
571
+ itype = None
572
+ if injection_type:
573
+ try:
574
+ itype = InjectionType(injection_type.lower())
575
+ except ValueError:
576
+ pass
577
+
578
+ return scan_for_prompt_injection(target_function, verbose=verbose)
579
+
580
+ @register_tool
507
581
  def scan_for_prompt_injection(
508
582
  target_function: Callable[[str], str],
509
583
  verbose: bool = False,
@@ -0,0 +1,28 @@
1
+ <tool name="scan_llm_endpoint">
2
+ <description>
3
+ Scan a remote LLM API endpoint for prompt injection vulnerabilities.
4
+ Tests for direct injection, jailbreaks, data exfiltration, and system prompt leakage.
5
+ </description>
6
+ <parameters>
7
+ <parameter name="target_url" type="string" required="true">
8
+ Full URL of the target LLM API endpoint (e.g. https://api.example.com/v1/chat)
9
+ </parameter>
10
+ <parameter name="method" type="string" optional="true">
11
+ HTTP method to use (POST or GET). Defaults to POST.
12
+ </parameter>
13
+ <parameter name="headers" type="dictionary" optional="true">
14
+ HTTP headers to include (e.g. Authorization, Content-Type).
15
+ </parameter>
16
+ <parameter name="body_template" type="string" optional="true">
17
+ Request body template for POST requests. Must include {{prompt}} placeholder.
18
+ Default: {"prompt": "{{prompt}}"}
19
+ </parameter>
20
+ <parameter name="injection_type" type="string" optional="true">
21
+ Specific injection type to test: direct, jailbreak, extraction, exfiltration.
22
+ Leave empty to run all tests.
23
+ </parameter>
24
+ <parameter name="verbose" type="boolean" optional="true">
25
+ Enable detailed logging of attempts. Defaults to false.
26
+ </parameter>
27
+ </parameters>
28
+ </tool>
@@ -24,6 +24,11 @@ class PythonInstance:
24
24
  workspace = os.getenv("EXAAI_WORKSPACE", "/workspace")
25
25
  if os.path.isdir(workspace):
26
26
  os.chdir(workspace)
27
+
28
+ # Add current directory and workspace to sys.path to ensure local imports work
29
+ sys.path.insert(0, os.getcwd())
30
+ if workspace not in sys.path:
31
+ sys.path.insert(0, workspace)
27
32
 
28
33
  self.shell = InteractiveShell()
29
34
  self.shell.init_completer()
@@ -1,4 +1,7 @@
1
1
  from typing import Any
2
+ import json
3
+ import os
4
+ from datetime import datetime
2
5
 
3
6
  from exaaiagnt.tools.registry import register_tool
4
7
 
@@ -26,6 +29,29 @@ def create_vulnerability_report(
26
29
  if validation_error:
27
30
  return {"success": False, "message": validation_error}
28
31
 
32
+ # Auto-save report to disk
33
+ report_data = {
34
+ "title": title,
35
+ "content": content,
36
+ "severity": severity,
37
+ "timestamp": datetime.now().isoformat(),
38
+ "agent": os.getenv("EXAAI_AGENT_NAME", "unknown")
39
+ }
40
+
41
+ # Save to local reports directory
42
+ try:
43
+ reports_dir = os.path.join(os.getcwd(), "reports")
44
+ os.makedirs(reports_dir, exist_ok=True)
45
+ filename = f"vuln_report_{int(datetime.now().timestamp())}_{severity}.json"
46
+ filepath = os.path.join(reports_dir, filename)
47
+
48
+ with open(filepath, "w") as f:
49
+ json.dump(report_data, f, indent=2)
50
+
51
+ except Exception as e:
52
+ import logging
53
+ logging.warning(f"Failed to save local report: {e}")
54
+
29
55
  try:
30
56
  from exaaiagnt.telemetry.tracer import get_global_tracer
31
57
 
@@ -42,22 +68,25 @@ def create_vulnerability_report(
42
68
  "message": f"Vulnerability report '{title}' created successfully",
43
69
  "report_id": report_id,
44
70
  "severity": severity.lower(),
71
+ "local_path": filepath
45
72
  }
46
73
  import logging
47
74
 
48
- logging.warning("Global tracer not available - vulnerability report not stored")
75
+ logging.warning("Global tracer not available - vulnerability report not stored in tracer")
49
76
 
50
77
  return { # noqa: TRY300
51
78
  "success": True,
52
- "message": f"Vulnerability report '{title}' created successfully (not persisted)",
53
- "warning": "Report could not be persisted - tracer unavailable",
79
+ "message": f"Vulnerability report '{title}' created successfully (saved locally)",
80
+ "warning": "Report not persisted in tracer",
81
+ "local_path": filepath
54
82
  }
55
83
 
56
84
  except ImportError:
57
85
  return {
58
86
  "success": True,
59
- "message": f"Vulnerability report '{title}' created successfully (not persisted)",
60
- "warning": "Report could not be persisted - tracer module unavailable",
87
+ "message": f"Vulnerability report '{title}' created successfully (saved locally)",
88
+ "warning": "Report not persisted - tracer module unavailable",
89
+ "local_path": filepath
61
90
  }
62
91
  except (ValueError, TypeError) as e:
63
92
  return {"success": False, "message": f"Failed to create vulnerability report: {e!s}"}
@@ -32,7 +32,7 @@ security implications and details."""
32
32
 
33
33
 
34
34
  @register_tool(sandbox_execution=False)
35
- def web_search(query: str) -> dict[str, Any]:
35
+ def web_search(query: str, model: str = "sonar") -> dict[str, Any]:
36
36
  try:
37
37
  api_key = os.getenv("PERPLEXITY_API_KEY")
38
38
  if not api_key:
@@ -45,8 +45,10 @@ def web_search(query: str) -> dict[str, Any]:
45
45
  url = "https://api.perplexity.ai/chat/completions"
46
46
  headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
47
47
 
48
+ # Use sonar by default (cheaper/faster) or sonar-reasoning if requested
49
+ # 'sonar' is the new name for the standard model, 'sonar-reasoning' for deep research
48
50
  payload = {
49
- "model": "sonar-reasoning",
51
+ "model": model,
50
52
  "messages": [
51
53
  {"role": "system", "content": SYSTEM_PROMPT},
52
54
  {"role": "user", "content": query},