strix-agent 0.4.0__py3-none-any.whl → 0.6.2__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.
- strix/agents/StrixAgent/strix_agent.py +3 -3
- strix/agents/StrixAgent/system_prompt.jinja +30 -26
- strix/agents/base_agent.py +159 -75
- strix/agents/state.py +5 -2
- strix/config/__init__.py +12 -0
- strix/config/config.py +172 -0
- strix/interface/assets/tui_styles.tcss +195 -230
- strix/interface/cli.py +16 -41
- strix/interface/main.py +151 -74
- strix/interface/streaming_parser.py +119 -0
- strix/interface/tool_components/__init__.py +4 -0
- strix/interface/tool_components/agent_message_renderer.py +190 -0
- strix/interface/tool_components/agents_graph_renderer.py +54 -38
- strix/interface/tool_components/base_renderer.py +68 -36
- strix/interface/tool_components/browser_renderer.py +106 -91
- strix/interface/tool_components/file_edit_renderer.py +117 -36
- strix/interface/tool_components/finish_renderer.py +43 -10
- strix/interface/tool_components/notes_renderer.py +63 -38
- strix/interface/tool_components/proxy_renderer.py +133 -92
- strix/interface/tool_components/python_renderer.py +121 -8
- strix/interface/tool_components/registry.py +19 -12
- strix/interface/tool_components/reporting_renderer.py +196 -28
- strix/interface/tool_components/scan_info_renderer.py +22 -19
- strix/interface/tool_components/terminal_renderer.py +270 -90
- strix/interface/tool_components/thinking_renderer.py +8 -6
- strix/interface/tool_components/todo_renderer.py +225 -0
- strix/interface/tool_components/user_message_renderer.py +26 -19
- strix/interface/tool_components/web_search_renderer.py +7 -6
- strix/interface/tui.py +907 -262
- strix/interface/utils.py +236 -4
- strix/llm/__init__.py +6 -2
- strix/llm/config.py +8 -5
- strix/llm/dedupe.py +217 -0
- strix/llm/llm.py +209 -356
- strix/llm/memory_compressor.py +6 -5
- strix/llm/utils.py +17 -8
- strix/runtime/__init__.py +12 -3
- strix/runtime/docker_runtime.py +121 -202
- strix/runtime/tool_server.py +55 -95
- strix/skills/README.md +64 -0
- strix/skills/__init__.py +110 -0
- strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
- strix/skills/scan_modes/deep.jinja +145 -0
- strix/skills/scan_modes/quick.jinja +63 -0
- strix/skills/scan_modes/standard.jinja +91 -0
- strix/telemetry/README.md +38 -0
- strix/telemetry/__init__.py +7 -1
- strix/telemetry/posthog.py +137 -0
- strix/telemetry/tracer.py +194 -54
- strix/tools/__init__.py +11 -4
- strix/tools/agents_graph/agents_graph_actions.py +20 -21
- strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
- strix/tools/browser/browser_actions.py +10 -6
- strix/tools/browser/browser_actions_schema.xml +6 -1
- strix/tools/browser/browser_instance.py +96 -48
- strix/tools/browser/tab_manager.py +121 -102
- strix/tools/context.py +12 -0
- strix/tools/executor.py +63 -4
- strix/tools/file_edit/file_edit_actions.py +6 -3
- strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
- strix/tools/finish/finish_actions.py +80 -105
- strix/tools/finish/finish_actions_schema.xml +121 -14
- strix/tools/notes/notes_actions.py +6 -33
- strix/tools/notes/notes_actions_schema.xml +50 -46
- strix/tools/proxy/proxy_actions.py +14 -2
- strix/tools/proxy/proxy_actions_schema.xml +0 -1
- strix/tools/proxy/proxy_manager.py +28 -16
- strix/tools/python/python_actions.py +2 -2
- strix/tools/python/python_actions_schema.xml +9 -1
- strix/tools/python/python_instance.py +39 -37
- strix/tools/python/python_manager.py +43 -31
- strix/tools/registry.py +73 -12
- strix/tools/reporting/reporting_actions.py +218 -31
- strix/tools/reporting/reporting_actions_schema.xml +256 -8
- strix/tools/terminal/terminal_actions.py +2 -2
- strix/tools/terminal/terminal_actions_schema.xml +6 -0
- strix/tools/terminal/terminal_manager.py +41 -30
- strix/tools/thinking/thinking_actions_schema.xml +27 -25
- strix/tools/todo/__init__.py +18 -0
- strix/tools/todo/todo_actions.py +568 -0
- strix/tools/todo/todo_actions_schema.xml +225 -0
- strix/utils/__init__.py +0 -0
- strix/utils/resource_paths.py +13 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
- strix_agent-0.6.2.dist-info/RECORD +134 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
- strix/llm/request_queue.py +0 -87
- strix/prompts/README.md +0 -64
- strix/prompts/__init__.py +0 -109
- strix_agent-0.4.0.dist-info/RECORD +0 -118
- /strix/{prompts → skills}/cloud/.gitkeep +0 -0
- /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
- /strix/{prompts → skills}/custom/.gitkeep +0 -0
- /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
- /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
- /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
- /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
- /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
- /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
- {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,43 +1,56 @@
|
|
|
1
1
|
import atexit
|
|
2
2
|
import contextlib
|
|
3
|
-
import signal
|
|
4
|
-
import sys
|
|
5
3
|
import threading
|
|
6
4
|
from typing import Any
|
|
7
5
|
|
|
6
|
+
from strix.tools.context import get_current_agent_id
|
|
7
|
+
|
|
8
8
|
from .browser_instance import BrowserInstance
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class BrowserTabManager:
|
|
12
12
|
def __init__(self) -> None:
|
|
13
|
-
self.
|
|
13
|
+
self._browsers_by_agent: dict[str, BrowserInstance] = {}
|
|
14
14
|
self._lock = threading.Lock()
|
|
15
15
|
|
|
16
16
|
self._register_cleanup_handlers()
|
|
17
17
|
|
|
18
|
+
def _get_agent_browser(self) -> BrowserInstance | None:
|
|
19
|
+
agent_id = get_current_agent_id()
|
|
20
|
+
with self._lock:
|
|
21
|
+
return self._browsers_by_agent.get(agent_id)
|
|
22
|
+
|
|
23
|
+
def _set_agent_browser(self, browser: BrowserInstance | None) -> None:
|
|
24
|
+
agent_id = get_current_agent_id()
|
|
25
|
+
with self._lock:
|
|
26
|
+
if browser is None:
|
|
27
|
+
self._browsers_by_agent.pop(agent_id, None)
|
|
28
|
+
else:
|
|
29
|
+
self._browsers_by_agent[agent_id] = browser
|
|
30
|
+
|
|
18
31
|
def launch_browser(self, url: str | None = None) -> dict[str, Any]:
|
|
19
32
|
with self._lock:
|
|
20
|
-
|
|
33
|
+
agent_id = get_current_agent_id()
|
|
34
|
+
if agent_id in self._browsers_by_agent:
|
|
21
35
|
raise ValueError("Browser is already launched")
|
|
22
36
|
|
|
23
37
|
try:
|
|
24
|
-
|
|
25
|
-
result =
|
|
38
|
+
browser = BrowserInstance()
|
|
39
|
+
result = browser.launch(url)
|
|
40
|
+
self._browsers_by_agent[agent_id] = browser
|
|
26
41
|
result["message"] = "Browser launched successfully"
|
|
27
42
|
except (OSError, ValueError, RuntimeError) as e:
|
|
28
|
-
if self.browser_instance:
|
|
29
|
-
self.browser_instance = None
|
|
30
43
|
raise RuntimeError(f"Failed to launch browser: {e}") from e
|
|
31
44
|
else:
|
|
32
45
|
return result
|
|
33
46
|
|
|
34
47
|
def goto_url(self, url: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
browser = self._get_agent_browser()
|
|
49
|
+
if browser is None:
|
|
50
|
+
raise ValueError("Browser not launched")
|
|
38
51
|
|
|
39
52
|
try:
|
|
40
|
-
result =
|
|
53
|
+
result = browser.goto(url, tab_id)
|
|
41
54
|
result["message"] = f"Navigated to {url}"
|
|
42
55
|
except (OSError, ValueError, RuntimeError) as e:
|
|
43
56
|
raise RuntimeError(f"Failed to navigate to URL: {e}") from e
|
|
@@ -45,12 +58,12 @@ class BrowserTabManager:
|
|
|
45
58
|
return result
|
|
46
59
|
|
|
47
60
|
def click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
61
|
+
browser = self._get_agent_browser()
|
|
62
|
+
if browser is None:
|
|
63
|
+
raise ValueError("Browser not launched")
|
|
51
64
|
|
|
52
65
|
try:
|
|
53
|
-
result =
|
|
66
|
+
result = browser.click(coordinate, tab_id)
|
|
54
67
|
result["message"] = f"Clicked at {coordinate}"
|
|
55
68
|
except (OSError, ValueError, RuntimeError) as e:
|
|
56
69
|
raise RuntimeError(f"Failed to click: {e}") from e
|
|
@@ -58,12 +71,12 @@ class BrowserTabManager:
|
|
|
58
71
|
return result
|
|
59
72
|
|
|
60
73
|
def type_text(self, text: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
browser = self._get_agent_browser()
|
|
75
|
+
if browser is None:
|
|
76
|
+
raise ValueError("Browser not launched")
|
|
64
77
|
|
|
65
78
|
try:
|
|
66
|
-
result =
|
|
79
|
+
result = browser.type_text(text, tab_id)
|
|
67
80
|
result["message"] = f"Typed text: {text[:50]}{'...' if len(text) > 50 else ''}"
|
|
68
81
|
except (OSError, ValueError, RuntimeError) as e:
|
|
69
82
|
raise RuntimeError(f"Failed to type text: {e}") from e
|
|
@@ -71,12 +84,12 @@ class BrowserTabManager:
|
|
|
71
84
|
return result
|
|
72
85
|
|
|
73
86
|
def scroll(self, direction: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
browser = self._get_agent_browser()
|
|
88
|
+
if browser is None:
|
|
89
|
+
raise ValueError("Browser not launched")
|
|
77
90
|
|
|
78
91
|
try:
|
|
79
|
-
result =
|
|
92
|
+
result = browser.scroll(direction, tab_id)
|
|
80
93
|
result["message"] = f"Scrolled {direction}"
|
|
81
94
|
except (OSError, ValueError, RuntimeError) as e:
|
|
82
95
|
raise RuntimeError(f"Failed to scroll: {e}") from e
|
|
@@ -84,12 +97,12 @@ class BrowserTabManager:
|
|
|
84
97
|
return result
|
|
85
98
|
|
|
86
99
|
def back(self, tab_id: str | None = None) -> dict[str, Any]:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
browser = self._get_agent_browser()
|
|
101
|
+
if browser is None:
|
|
102
|
+
raise ValueError("Browser not launched")
|
|
90
103
|
|
|
91
104
|
try:
|
|
92
|
-
result =
|
|
105
|
+
result = browser.back(tab_id)
|
|
93
106
|
result["message"] = "Navigated back"
|
|
94
107
|
except (OSError, ValueError, RuntimeError) as e:
|
|
95
108
|
raise RuntimeError(f"Failed to go back: {e}") from e
|
|
@@ -97,12 +110,12 @@ class BrowserTabManager:
|
|
|
97
110
|
return result
|
|
98
111
|
|
|
99
112
|
def forward(self, tab_id: str | None = None) -> dict[str, Any]:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
113
|
+
browser = self._get_agent_browser()
|
|
114
|
+
if browser is None:
|
|
115
|
+
raise ValueError("Browser not launched")
|
|
103
116
|
|
|
104
117
|
try:
|
|
105
|
-
result =
|
|
118
|
+
result = browser.forward(tab_id)
|
|
106
119
|
result["message"] = "Navigated forward"
|
|
107
120
|
except (OSError, ValueError, RuntimeError) as e:
|
|
108
121
|
raise RuntimeError(f"Failed to go forward: {e}") from e
|
|
@@ -110,12 +123,12 @@ class BrowserTabManager:
|
|
|
110
123
|
return result
|
|
111
124
|
|
|
112
125
|
def new_tab(self, url: str | None = None) -> dict[str, Any]:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
browser = self._get_agent_browser()
|
|
127
|
+
if browser is None:
|
|
128
|
+
raise ValueError("Browser not launched")
|
|
116
129
|
|
|
117
130
|
try:
|
|
118
|
-
result =
|
|
131
|
+
result = browser.new_tab(url)
|
|
119
132
|
result["message"] = f"Created new tab {result.get('tab_id', '')}"
|
|
120
133
|
except (OSError, ValueError, RuntimeError) as e:
|
|
121
134
|
raise RuntimeError(f"Failed to create new tab: {e}") from e
|
|
@@ -123,12 +136,12 @@ class BrowserTabManager:
|
|
|
123
136
|
return result
|
|
124
137
|
|
|
125
138
|
def switch_tab(self, tab_id: str) -> dict[str, Any]:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
139
|
+
browser = self._get_agent_browser()
|
|
140
|
+
if browser is None:
|
|
141
|
+
raise ValueError("Browser not launched")
|
|
129
142
|
|
|
130
143
|
try:
|
|
131
|
-
result =
|
|
144
|
+
result = browser.switch_tab(tab_id)
|
|
132
145
|
result["message"] = f"Switched to tab {tab_id}"
|
|
133
146
|
except (OSError, ValueError, RuntimeError) as e:
|
|
134
147
|
raise RuntimeError(f"Failed to switch tab: {e}") from e
|
|
@@ -136,12 +149,12 @@ class BrowserTabManager:
|
|
|
136
149
|
return result
|
|
137
150
|
|
|
138
151
|
def close_tab(self, tab_id: str) -> dict[str, Any]:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
browser = self._get_agent_browser()
|
|
153
|
+
if browser is None:
|
|
154
|
+
raise ValueError("Browser not launched")
|
|
142
155
|
|
|
143
156
|
try:
|
|
144
|
-
result =
|
|
157
|
+
result = browser.close_tab(tab_id)
|
|
145
158
|
result["message"] = f"Closed tab {tab_id}"
|
|
146
159
|
except (OSError, ValueError, RuntimeError) as e:
|
|
147
160
|
raise RuntimeError(f"Failed to close tab: {e}") from e
|
|
@@ -149,12 +162,12 @@ class BrowserTabManager:
|
|
|
149
162
|
return result
|
|
150
163
|
|
|
151
164
|
def wait_browser(self, duration: float, tab_id: str | None = None) -> dict[str, Any]:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
165
|
+
browser = self._get_agent_browser()
|
|
166
|
+
if browser is None:
|
|
167
|
+
raise ValueError("Browser not launched")
|
|
155
168
|
|
|
156
169
|
try:
|
|
157
|
-
result =
|
|
170
|
+
result = browser.wait(duration, tab_id)
|
|
158
171
|
result["message"] = f"Waited {duration}s"
|
|
159
172
|
except (OSError, ValueError, RuntimeError) as e:
|
|
160
173
|
raise RuntimeError(f"Failed to wait: {e}") from e
|
|
@@ -162,12 +175,12 @@ class BrowserTabManager:
|
|
|
162
175
|
return result
|
|
163
176
|
|
|
164
177
|
def execute_js(self, js_code: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
browser = self._get_agent_browser()
|
|
179
|
+
if browser is None:
|
|
180
|
+
raise ValueError("Browser not launched")
|
|
168
181
|
|
|
169
182
|
try:
|
|
170
|
-
result =
|
|
183
|
+
result = browser.execute_js(js_code, tab_id)
|
|
171
184
|
result["message"] = "JavaScript executed successfully"
|
|
172
185
|
except (OSError, ValueError, RuntimeError) as e:
|
|
173
186
|
raise RuntimeError(f"Failed to execute JavaScript: {e}") from e
|
|
@@ -175,12 +188,12 @@ class BrowserTabManager:
|
|
|
175
188
|
return result
|
|
176
189
|
|
|
177
190
|
def double_click(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
191
|
+
browser = self._get_agent_browser()
|
|
192
|
+
if browser is None:
|
|
193
|
+
raise ValueError("Browser not launched")
|
|
181
194
|
|
|
182
195
|
try:
|
|
183
|
-
result =
|
|
196
|
+
result = browser.double_click(coordinate, tab_id)
|
|
184
197
|
result["message"] = f"Double clicked at {coordinate}"
|
|
185
198
|
except (OSError, ValueError, RuntimeError) as e:
|
|
186
199
|
raise RuntimeError(f"Failed to double click: {e}") from e
|
|
@@ -188,12 +201,12 @@ class BrowserTabManager:
|
|
|
188
201
|
return result
|
|
189
202
|
|
|
190
203
|
def hover(self, coordinate: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
204
|
+
browser = self._get_agent_browser()
|
|
205
|
+
if browser is None:
|
|
206
|
+
raise ValueError("Browser not launched")
|
|
194
207
|
|
|
195
208
|
try:
|
|
196
|
-
result =
|
|
209
|
+
result = browser.hover(coordinate, tab_id)
|
|
197
210
|
result["message"] = f"Hovered at {coordinate}"
|
|
198
211
|
except (OSError, ValueError, RuntimeError) as e:
|
|
199
212
|
raise RuntimeError(f"Failed to hover: {e}") from e
|
|
@@ -201,12 +214,12 @@ class BrowserTabManager:
|
|
|
201
214
|
return result
|
|
202
215
|
|
|
203
216
|
def press_key(self, key: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
217
|
+
browser = self._get_agent_browser()
|
|
218
|
+
if browser is None:
|
|
219
|
+
raise ValueError("Browser not launched")
|
|
207
220
|
|
|
208
221
|
try:
|
|
209
|
-
result =
|
|
222
|
+
result = browser.press_key(key, tab_id)
|
|
210
223
|
result["message"] = f"Pressed key {key}"
|
|
211
224
|
except (OSError, ValueError, RuntimeError) as e:
|
|
212
225
|
raise RuntimeError(f"Failed to press key: {e}") from e
|
|
@@ -214,12 +227,12 @@ class BrowserTabManager:
|
|
|
214
227
|
return result
|
|
215
228
|
|
|
216
229
|
def save_pdf(self, file_path: str, tab_id: str | None = None) -> dict[str, Any]:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
230
|
+
browser = self._get_agent_browser()
|
|
231
|
+
if browser is None:
|
|
232
|
+
raise ValueError("Browser not launched")
|
|
220
233
|
|
|
221
234
|
try:
|
|
222
|
-
result =
|
|
235
|
+
result = browser.save_pdf(file_path, tab_id)
|
|
223
236
|
result["message"] = f"Page saved as PDF: {file_path}"
|
|
224
237
|
except (OSError, ValueError, RuntimeError) as e:
|
|
225
238
|
raise RuntimeError(f"Failed to save PDF: {e}") from e
|
|
@@ -227,12 +240,12 @@ class BrowserTabManager:
|
|
|
227
240
|
return result
|
|
228
241
|
|
|
229
242
|
def get_console_logs(self, tab_id: str | None = None, clear: bool = False) -> dict[str, Any]:
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
browser = self._get_agent_browser()
|
|
244
|
+
if browser is None:
|
|
245
|
+
raise ValueError("Browser not launched")
|
|
233
246
|
|
|
234
247
|
try:
|
|
235
|
-
result =
|
|
248
|
+
result = browser.get_console_logs(tab_id, clear)
|
|
236
249
|
action_text = "cleared and retrieved" if clear else "retrieved"
|
|
237
250
|
|
|
238
251
|
logs = result.get("console_logs", [])
|
|
@@ -249,12 +262,12 @@ class BrowserTabManager:
|
|
|
249
262
|
return result
|
|
250
263
|
|
|
251
264
|
def view_source(self, tab_id: str | None = None) -> dict[str, Any]:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
265
|
+
browser = self._get_agent_browser()
|
|
266
|
+
if browser is None:
|
|
267
|
+
raise ValueError("Browser not launched")
|
|
255
268
|
|
|
256
269
|
try:
|
|
257
|
-
result =
|
|
270
|
+
result = browser.view_source(tab_id)
|
|
258
271
|
result["message"] = "Page source retrieved"
|
|
259
272
|
except (OSError, ValueError, RuntimeError) as e:
|
|
260
273
|
raise RuntimeError(f"Failed to get page source: {e}") from e
|
|
@@ -262,18 +275,18 @@ class BrowserTabManager:
|
|
|
262
275
|
return result
|
|
263
276
|
|
|
264
277
|
def list_tabs(self) -> dict[str, Any]:
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
278
|
+
browser = self._get_agent_browser()
|
|
279
|
+
if browser is None:
|
|
280
|
+
return {"tabs": {}, "total_count": 0, "current_tab": None}
|
|
268
281
|
|
|
269
282
|
try:
|
|
270
283
|
tab_info = {}
|
|
271
|
-
for tid, tab_page in
|
|
284
|
+
for tid, tab_page in browser.pages.items():
|
|
272
285
|
try:
|
|
273
286
|
tab_info[tid] = {
|
|
274
287
|
"url": tab_page.url,
|
|
275
288
|
"title": "Unknown" if tab_page.is_closed() else "Active",
|
|
276
|
-
"is_current": tid ==
|
|
289
|
+
"is_current": tid == browser.current_page_id,
|
|
277
290
|
}
|
|
278
291
|
except (AttributeError, RuntimeError):
|
|
279
292
|
tab_info[tid] = {
|
|
@@ -285,19 +298,20 @@ class BrowserTabManager:
|
|
|
285
298
|
return {
|
|
286
299
|
"tabs": tab_info,
|
|
287
300
|
"total_count": len(tab_info),
|
|
288
|
-
"current_tab":
|
|
301
|
+
"current_tab": browser.current_page_id,
|
|
289
302
|
}
|
|
290
303
|
except (OSError, ValueError, RuntimeError) as e:
|
|
291
304
|
raise RuntimeError(f"Failed to list tabs: {e}") from e
|
|
292
305
|
|
|
293
306
|
def close_browser(self) -> dict[str, Any]:
|
|
307
|
+
agent_id = get_current_agent_id()
|
|
294
308
|
with self._lock:
|
|
295
|
-
|
|
309
|
+
browser = self._browsers_by_agent.pop(agent_id, None)
|
|
310
|
+
if browser is None:
|
|
296
311
|
raise ValueError("Browser not launched")
|
|
297
312
|
|
|
298
313
|
try:
|
|
299
|
-
|
|
300
|
-
self.browser_instance = None
|
|
314
|
+
browser.close()
|
|
301
315
|
except (OSError, ValueError, RuntimeError) as e:
|
|
302
316
|
raise RuntimeError(f"Failed to close browser: {e}") from e
|
|
303
317
|
else:
|
|
@@ -307,33 +321,38 @@ class BrowserTabManager:
|
|
|
307
321
|
"is_running": False,
|
|
308
322
|
}
|
|
309
323
|
|
|
324
|
+
def cleanup_agent(self, agent_id: str) -> None:
|
|
325
|
+
with self._lock:
|
|
326
|
+
browser = self._browsers_by_agent.pop(agent_id, None)
|
|
327
|
+
|
|
328
|
+
if browser:
|
|
329
|
+
with contextlib.suppress(Exception):
|
|
330
|
+
browser.close()
|
|
331
|
+
|
|
310
332
|
def cleanup_dead_browser(self) -> None:
|
|
311
333
|
with self._lock:
|
|
312
|
-
|
|
334
|
+
dead_agents = []
|
|
335
|
+
for agent_id, browser in self._browsers_by_agent.items():
|
|
336
|
+
if not browser.is_alive():
|
|
337
|
+
dead_agents.append(agent_id)
|
|
338
|
+
|
|
339
|
+
for agent_id in dead_agents:
|
|
340
|
+
browser = self._browsers_by_agent.pop(agent_id)
|
|
313
341
|
with contextlib.suppress(Exception):
|
|
314
|
-
|
|
315
|
-
self.browser_instance = None
|
|
342
|
+
browser.close()
|
|
316
343
|
|
|
317
344
|
def close_all(self) -> None:
|
|
318
345
|
with self._lock:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
346
|
+
browsers = list(self._browsers_by_agent.values())
|
|
347
|
+
self._browsers_by_agent.clear()
|
|
348
|
+
|
|
349
|
+
for browser in browsers:
|
|
350
|
+
with contextlib.suppress(Exception):
|
|
351
|
+
browser.close()
|
|
323
352
|
|
|
324
353
|
def _register_cleanup_handlers(self) -> None:
|
|
325
354
|
atexit.register(self.close_all)
|
|
326
355
|
|
|
327
|
-
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
328
|
-
signal.signal(signal.SIGINT, self._signal_handler)
|
|
329
|
-
|
|
330
|
-
if hasattr(signal, "SIGHUP"):
|
|
331
|
-
signal.signal(signal.SIGHUP, self._signal_handler)
|
|
332
|
-
|
|
333
|
-
def _signal_handler(self, _signum: int, _frame: Any) -> None:
|
|
334
|
-
self.close_all()
|
|
335
|
-
sys.exit(0)
|
|
336
|
-
|
|
337
356
|
|
|
338
357
|
_browser_tab_manager = BrowserTabManager()
|
|
339
358
|
|
strix/tools/context.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
current_agent_id: ContextVar[str] = ContextVar("current_agent_id", default="default")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_current_agent_id() -> str:
|
|
8
|
+
return current_agent_id.get()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def set_current_agent_id(agent_id: str) -> None:
|
|
12
|
+
current_agent_id.set(agent_id)
|
strix/tools/executor.py
CHANGED
|
@@ -4,6 +4,9 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
7
|
+
from strix.config import Config
|
|
8
|
+
from strix.telemetry import posthog
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
if os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "false":
|
|
9
12
|
from strix.runtime import get_runtime
|
|
@@ -12,11 +15,17 @@ from .argument_parser import convert_arguments
|
|
|
12
15
|
from .registry import (
|
|
13
16
|
get_tool_by_name,
|
|
14
17
|
get_tool_names,
|
|
18
|
+
get_tool_param_schema,
|
|
15
19
|
needs_agent_state,
|
|
16
20
|
should_execute_in_sandbox,
|
|
17
21
|
)
|
|
18
22
|
|
|
19
23
|
|
|
24
|
+
_SERVER_TIMEOUT = float(Config.get("strix_sandbox_execution_timeout") or "120")
|
|
25
|
+
SANDBOX_EXECUTION_TIMEOUT = _SERVER_TIMEOUT + 30
|
|
26
|
+
SANDBOX_CONNECT_TIMEOUT = float(Config.get("strix_sandbox_connect_timeout") or "10")
|
|
27
|
+
|
|
28
|
+
|
|
20
29
|
async def execute_tool(tool_name: str, agent_state: Any | None = None, **kwargs: Any) -> Any:
|
|
21
30
|
execute_in_sandbox = should_execute_in_sandbox(tool_name)
|
|
22
31
|
sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
|
|
@@ -62,22 +71,31 @@ async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: A
|
|
|
62
71
|
"Content-Type": "application/json",
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
timeout = httpx.Timeout(
|
|
75
|
+
timeout=SANDBOX_EXECUTION_TIMEOUT,
|
|
76
|
+
connect=SANDBOX_CONNECT_TIMEOUT,
|
|
77
|
+
)
|
|
78
|
+
|
|
65
79
|
async with httpx.AsyncClient(trust_env=False) as client:
|
|
66
80
|
try:
|
|
67
81
|
response = await client.post(
|
|
68
|
-
request_url, json=request_data, headers=headers, timeout=
|
|
82
|
+
request_url, json=request_data, headers=headers, timeout=timeout
|
|
69
83
|
)
|
|
70
84
|
response.raise_for_status()
|
|
71
85
|
response_data = response.json()
|
|
72
86
|
if response_data.get("error"):
|
|
87
|
+
posthog.error("tool_execution_error", f"{tool_name}: {response_data['error']}")
|
|
73
88
|
raise RuntimeError(f"Sandbox execution error: {response_data['error']}")
|
|
74
89
|
return response_data.get("result")
|
|
75
90
|
except httpx.HTTPStatusError as e:
|
|
91
|
+
posthog.error("tool_http_error", f"{tool_name}: HTTP {e.response.status_code}")
|
|
76
92
|
if e.response.status_code == 401:
|
|
77
93
|
raise RuntimeError("Authentication failed: Invalid or missing sandbox token") from e
|
|
78
94
|
raise RuntimeError(f"HTTP error calling tool server: {e.response.status_code}") from e
|
|
79
95
|
except httpx.RequestError as e:
|
|
80
|
-
|
|
96
|
+
error_type = type(e).__name__
|
|
97
|
+
posthog.error("tool_request_error", f"{tool_name}: {error_type}")
|
|
98
|
+
raise RuntimeError(f"Request error calling tool server: {error_type}") from e
|
|
81
99
|
|
|
82
100
|
|
|
83
101
|
async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwargs: Any) -> Any:
|
|
@@ -99,14 +117,51 @@ async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwarg
|
|
|
99
117
|
|
|
100
118
|
def validate_tool_availability(tool_name: str | None) -> tuple[bool, str]:
|
|
101
119
|
if tool_name is None:
|
|
102
|
-
|
|
120
|
+
available = ", ".join(sorted(get_tool_names()))
|
|
121
|
+
return False, f"Tool name is missing. Available tools: {available}"
|
|
103
122
|
|
|
104
123
|
if tool_name not in get_tool_names():
|
|
105
|
-
|
|
124
|
+
available = ", ".join(sorted(get_tool_names()))
|
|
125
|
+
return False, f"Tool '{tool_name}' is not available. Available tools: {available}"
|
|
106
126
|
|
|
107
127
|
return True, ""
|
|
108
128
|
|
|
109
129
|
|
|
130
|
+
def _validate_tool_arguments(tool_name: str, kwargs: dict[str, Any]) -> str | None:
|
|
131
|
+
param_schema = get_tool_param_schema(tool_name)
|
|
132
|
+
if not param_schema or not param_schema.get("has_params"):
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
allowed_params: set[str] = param_schema.get("params", set())
|
|
136
|
+
required_params: set[str] = param_schema.get("required", set())
|
|
137
|
+
optional_params = allowed_params - required_params
|
|
138
|
+
|
|
139
|
+
schema_hint = _format_schema_hint(tool_name, required_params, optional_params)
|
|
140
|
+
|
|
141
|
+
unknown_params = set(kwargs.keys()) - allowed_params
|
|
142
|
+
if unknown_params:
|
|
143
|
+
unknown_list = ", ".join(sorted(unknown_params))
|
|
144
|
+
return f"Tool '{tool_name}' received unknown parameter(s): {unknown_list}\n{schema_hint}"
|
|
145
|
+
|
|
146
|
+
missing_required = [
|
|
147
|
+
param for param in required_params if param not in kwargs or kwargs.get(param) in (None, "")
|
|
148
|
+
]
|
|
149
|
+
if missing_required:
|
|
150
|
+
missing_list = ", ".join(sorted(missing_required))
|
|
151
|
+
return f"Tool '{tool_name}' missing required parameter(s): {missing_list}\n{schema_hint}"
|
|
152
|
+
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _format_schema_hint(tool_name: str, required: set[str], optional: set[str]) -> str:
|
|
157
|
+
parts = [f"Valid parameters for '{tool_name}':"]
|
|
158
|
+
if required:
|
|
159
|
+
parts.append(f" Required: {', '.join(sorted(required))}")
|
|
160
|
+
if optional:
|
|
161
|
+
parts.append(f" Optional: {', '.join(sorted(optional))}")
|
|
162
|
+
return "\n".join(parts)
|
|
163
|
+
|
|
164
|
+
|
|
110
165
|
async def execute_tool_with_validation(
|
|
111
166
|
tool_name: str | None, agent_state: Any | None = None, **kwargs: Any
|
|
112
167
|
) -> Any:
|
|
@@ -116,6 +171,10 @@ async def execute_tool_with_validation(
|
|
|
116
171
|
|
|
117
172
|
assert tool_name is not None
|
|
118
173
|
|
|
174
|
+
arg_error = _validate_tool_arguments(tool_name, kwargs)
|
|
175
|
+
if arg_error:
|
|
176
|
+
return f"Error: {arg_error}"
|
|
177
|
+
|
|
119
178
|
try:
|
|
120
179
|
result = await execute_tool(tool_name, agent_state, **kwargs)
|
|
121
180
|
except Exception as e: # noqa: BLE001
|
|
@@ -3,9 +3,6 @@ import re
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any, cast
|
|
5
5
|
|
|
6
|
-
from openhands_aci import file_editor
|
|
7
|
-
from openhands_aci.utils.shell import run_shell_cmd
|
|
8
|
-
|
|
9
6
|
from strix.tools.registry import register_tool
|
|
10
7
|
|
|
11
8
|
|
|
@@ -33,6 +30,8 @@ def str_replace_editor(
|
|
|
33
30
|
new_str: str | None = None,
|
|
34
31
|
insert_line: int | None = None,
|
|
35
32
|
) -> dict[str, Any]:
|
|
33
|
+
from openhands_aci import file_editor
|
|
34
|
+
|
|
36
35
|
try:
|
|
37
36
|
path_obj = Path(path)
|
|
38
37
|
if not path_obj.is_absolute():
|
|
@@ -64,6 +63,8 @@ def list_files(
|
|
|
64
63
|
path: str,
|
|
65
64
|
recursive: bool = False,
|
|
66
65
|
) -> dict[str, Any]:
|
|
66
|
+
from openhands_aci.utils.shell import run_shell_cmd
|
|
67
|
+
|
|
67
68
|
try:
|
|
68
69
|
path_obj = Path(path)
|
|
69
70
|
if not path_obj.is_absolute():
|
|
@@ -116,6 +117,8 @@ def search_files(
|
|
|
116
117
|
regex: str,
|
|
117
118
|
file_pattern: str = "*",
|
|
118
119
|
) -> dict[str, Any]:
|
|
120
|
+
from openhands_aci.utils.shell import run_shell_cmd
|
|
121
|
+
|
|
119
122
|
try:
|
|
120
123
|
path_obj = Path(path)
|
|
121
124
|
if not path_obj.is_absolute():
|