strix-agent 0.1.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.
- strix/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +60 -0
- strix/agents/StrixAgent/system_prompt.jinja +504 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +394 -0
- strix/agents/state.py +139 -0
- strix/cli/__init__.py +4 -0
- strix/cli/app.py +1124 -0
- strix/cli/assets/cli.tcss +680 -0
- strix/cli/main.py +542 -0
- strix/cli/tool_components/__init__.py +39 -0
- strix/cli/tool_components/agents_graph_renderer.py +129 -0
- strix/cli/tool_components/base_renderer.py +61 -0
- strix/cli/tool_components/browser_renderer.py +107 -0
- strix/cli/tool_components/file_edit_renderer.py +95 -0
- strix/cli/tool_components/finish_renderer.py +32 -0
- strix/cli/tool_components/notes_renderer.py +108 -0
- strix/cli/tool_components/proxy_renderer.py +255 -0
- strix/cli/tool_components/python_renderer.py +34 -0
- strix/cli/tool_components/registry.py +72 -0
- strix/cli/tool_components/reporting_renderer.py +53 -0
- strix/cli/tool_components/scan_info_renderer.py +58 -0
- strix/cli/tool_components/terminal_renderer.py +99 -0
- strix/cli/tool_components/thinking_renderer.py +29 -0
- strix/cli/tool_components/user_message_renderer.py +43 -0
- strix/cli/tool_components/web_search_renderer.py +28 -0
- strix/cli/tracer.py +308 -0
- strix/llm/__init__.py +14 -0
- strix/llm/config.py +19 -0
- strix/llm/llm.py +310 -0
- strix/llm/memory_compressor.py +206 -0
- strix/llm/request_queue.py +63 -0
- strix/llm/utils.py +84 -0
- strix/prompts/__init__.py +113 -0
- strix/prompts/coordination/root_agent.jinja +41 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
- strix/prompts/vulnerabilities/business_logic.jinja +143 -0
- strix/prompts/vulnerabilities/csrf.jinja +168 -0
- strix/prompts/vulnerabilities/idor.jinja +164 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
- strix/prompts/vulnerabilities/rce.jinja +222 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
- strix/prompts/vulnerabilities/ssrf.jinja +168 -0
- strix/prompts/vulnerabilities/xss.jinja +221 -0
- strix/prompts/vulnerabilities/xxe.jinja +276 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +298 -0
- strix/runtime/runtime.py +25 -0
- strix/runtime/tool_server.py +97 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +610 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
- strix/tools/argument_parser.py +120 -0
- strix/tools/browser/__init__.py +4 -0
- strix/tools/browser/browser_actions.py +236 -0
- strix/tools/browser/browser_actions_schema.xml +183 -0
- strix/tools/browser/browser_instance.py +533 -0
- strix/tools/browser/tab_manager.py +342 -0
- strix/tools/executor.py +302 -0
- strix/tools/file_edit/__init__.py +4 -0
- strix/tools/file_edit/file_edit_actions.py +141 -0
- strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
- strix/tools/finish/__init__.py +4 -0
- strix/tools/finish/finish_actions.py +167 -0
- strix/tools/finish/finish_actions_schema.xml +45 -0
- strix/tools/notes/__init__.py +14 -0
- strix/tools/notes/notes_actions.py +191 -0
- strix/tools/notes/notes_actions_schema.xml +150 -0
- strix/tools/proxy/__init__.py +20 -0
- strix/tools/proxy/proxy_actions.py +101 -0
- strix/tools/proxy/proxy_actions_schema.xml +267 -0
- strix/tools/proxy/proxy_manager.py +785 -0
- strix/tools/python/__init__.py +4 -0
- strix/tools/python/python_actions.py +47 -0
- strix/tools/python/python_actions_schema.xml +131 -0
- strix/tools/python/python_instance.py +172 -0
- strix/tools/python/python_manager.py +131 -0
- strix/tools/registry.py +196 -0
- strix/tools/reporting/__init__.py +6 -0
- strix/tools/reporting/reporting_actions.py +63 -0
- strix/tools/reporting/reporting_actions_schema.xml +30 -0
- strix/tools/terminal/__init__.py +4 -0
- strix/tools/terminal/terminal_actions.py +53 -0
- strix/tools/terminal/terminal_actions_schema.xml +114 -0
- strix/tools/terminal/terminal_instance.py +231 -0
- strix/tools/terminal/terminal_manager.py +191 -0
- strix/tools/thinking/__init__.py +4 -0
- strix/tools/thinking/thinking_actions.py +18 -0
- strix/tools/thinking/thinking_actions_schema.xml +52 -0
- strix/tools/web_search/__init__.py +4 -0
- strix/tools/web_search/web_search_actions.py +80 -0
- strix/tools/web_search/web_search_actions_schema.xml +83 -0
- strix_agent-0.1.1.dist-info/LICENSE +201 -0
- strix_agent-0.1.1.dist-info/METADATA +200 -0
- strix_agent-0.1.1.dist-info/RECORD +99 -0
- strix_agent-0.1.1.dist-info/WHEEL +4 -0
- strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,298 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import secrets
|
4
|
+
import socket
|
5
|
+
import time
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import cast
|
8
|
+
|
9
|
+
import docker
|
10
|
+
from docker.errors import DockerException, NotFound
|
11
|
+
from docker.models.containers import Container
|
12
|
+
|
13
|
+
from .runtime import AbstractRuntime, SandboxInfo
|
14
|
+
|
15
|
+
|
16
|
+
STRIX_AGENT_LABEL = "StrixAgent_ID"
|
17
|
+
STRIX_SCAN_LABEL = "StrixScan_ID"
|
18
|
+
STRIX_IMAGE = os.getenv("STRIX_IMAGE", "ghcr.io/usestrix/strix-sandbox:0.1.1")
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
_initialized_volumes: set[str] = set()
|
22
|
+
|
23
|
+
|
24
|
+
class DockerRuntime(AbstractRuntime):
|
25
|
+
def __init__(self) -> None:
|
26
|
+
try:
|
27
|
+
self.client = docker.from_env()
|
28
|
+
except DockerException as e:
|
29
|
+
logger.exception("Failed to connect to Docker daemon")
|
30
|
+
raise RuntimeError("Docker is not available or not configured correctly.") from e
|
31
|
+
|
32
|
+
def _get_docker_socket_path(self) -> str | None:
|
33
|
+
standard_socket = "/var/run/docker.sock"
|
34
|
+
if Path(standard_socket).exists():
|
35
|
+
return standard_socket
|
36
|
+
|
37
|
+
if os.name == "nt":
|
38
|
+
windows_socket = "//./pipe/docker_engine"
|
39
|
+
if Path(windows_socket).exists():
|
40
|
+
return windows_socket
|
41
|
+
|
42
|
+
docker_host = os.environ.get("DOCKER_HOST", "")
|
43
|
+
if docker_host.startswith("unix://"):
|
44
|
+
socket_path = docker_host.replace("unix://", "")
|
45
|
+
if Path(socket_path).exists():
|
46
|
+
return socket_path
|
47
|
+
|
48
|
+
return None
|
49
|
+
|
50
|
+
def _generate_sandbox_token(self) -> str:
|
51
|
+
return secrets.token_urlsafe(32)
|
52
|
+
|
53
|
+
def _get_scan_id(self, agent_id: str) -> str:
|
54
|
+
try:
|
55
|
+
from strix.cli.tracer import get_global_tracer
|
56
|
+
|
57
|
+
tracer = get_global_tracer()
|
58
|
+
if tracer and tracer.scan_config:
|
59
|
+
return str(tracer.scan_config.get("scan_id", "default-scan"))
|
60
|
+
except ImportError:
|
61
|
+
logger.debug("Failed to import tracer, using fallback scan ID")
|
62
|
+
except AttributeError:
|
63
|
+
logger.debug("Tracer missing scan_config, using fallback scan ID")
|
64
|
+
|
65
|
+
return f"scan-{agent_id.split('-')[0]}"
|
66
|
+
|
67
|
+
def _find_available_port(self) -> int:
|
68
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
69
|
+
s.bind(("", 0))
|
70
|
+
return cast("int", s.getsockname()[1])
|
71
|
+
|
72
|
+
def _get_workspace_volume_name(self, scan_id: str) -> str:
|
73
|
+
return f"strix-workspace-{scan_id}"
|
74
|
+
|
75
|
+
def _get_sandbox_by_agent_id(self, agent_id: str) -> Container | None:
|
76
|
+
try:
|
77
|
+
containers = self.client.containers.list(
|
78
|
+
filters={"label": f"{STRIX_AGENT_LABEL}={agent_id}"}
|
79
|
+
)
|
80
|
+
if not containers:
|
81
|
+
return None
|
82
|
+
if len(containers) > 1:
|
83
|
+
logger.warning(
|
84
|
+
"Multiple sandboxes found for agent ID %s, using the first one.", agent_id
|
85
|
+
)
|
86
|
+
return cast("Container", containers[0])
|
87
|
+
except DockerException as e:
|
88
|
+
logger.warning("Failed to get sandbox by agent ID %s: %s", agent_id, e)
|
89
|
+
return None
|
90
|
+
|
91
|
+
def _ensure_workspace_volume(self, volume_name: str) -> None:
|
92
|
+
try:
|
93
|
+
self.client.volumes.get(volume_name)
|
94
|
+
logger.info(f"Using existing workspace volume: {volume_name}")
|
95
|
+
except NotFound:
|
96
|
+
self.client.volumes.create(name=volume_name, driver="local")
|
97
|
+
logger.info(f"Created new workspace volume: {volume_name}")
|
98
|
+
|
99
|
+
def _copy_local_directory_to_container(self, container: Container, local_path: str) -> None:
|
100
|
+
import tarfile
|
101
|
+
from io import BytesIO
|
102
|
+
|
103
|
+
try:
|
104
|
+
local_path_obj = Path(local_path).resolve()
|
105
|
+
if not local_path_obj.exists() or not local_path_obj.is_dir():
|
106
|
+
logger.warning(f"Local path does not exist or is not a directory: {local_path_obj}")
|
107
|
+
return
|
108
|
+
|
109
|
+
logger.info(f"Copying local directory {local_path_obj} to container {container.id}")
|
110
|
+
|
111
|
+
tar_buffer = BytesIO()
|
112
|
+
with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
|
113
|
+
for item in local_path_obj.rglob("*"):
|
114
|
+
if item.is_file():
|
115
|
+
arcname = item.relative_to(local_path_obj)
|
116
|
+
tar.add(item, arcname=arcname)
|
117
|
+
|
118
|
+
tar_buffer.seek(0)
|
119
|
+
|
120
|
+
container.put_archive("/shared_workspace", tar_buffer.getvalue())
|
121
|
+
|
122
|
+
container.exec_run(
|
123
|
+
"chown -R pentester:pentester /shared_workspace && chmod -R 755 /shared_workspace",
|
124
|
+
user="root",
|
125
|
+
)
|
126
|
+
|
127
|
+
logger.info(
|
128
|
+
f"Successfully copied {local_path_obj} to /shared_workspace in container "
|
129
|
+
f"{container.id}"
|
130
|
+
)
|
131
|
+
|
132
|
+
except (OSError, DockerException):
|
133
|
+
logger.exception("Failed to copy local directory to container")
|
134
|
+
|
135
|
+
async def create_sandbox(
|
136
|
+
self, agent_id: str, existing_token: str | None = None, local_source_path: str | None = None
|
137
|
+
) -> SandboxInfo:
|
138
|
+
sandbox = self._get_sandbox_by_agent_id(agent_id)
|
139
|
+
auth_token = existing_token or self._generate_sandbox_token()
|
140
|
+
|
141
|
+
scan_id = self._get_scan_id(agent_id)
|
142
|
+
volume_name = self._get_workspace_volume_name(scan_id)
|
143
|
+
|
144
|
+
self._ensure_workspace_volume(volume_name)
|
145
|
+
|
146
|
+
if not sandbox:
|
147
|
+
logger.info("Creating new Docker sandbox for agent %s", agent_id)
|
148
|
+
try:
|
149
|
+
tool_server_port = self._find_available_port()
|
150
|
+
caido_port = self._find_available_port()
|
151
|
+
|
152
|
+
volumes_config = {volume_name: {"bind": "/shared_workspace", "mode": "rw"}}
|
153
|
+
|
154
|
+
docker_socket_path = self._get_docker_socket_path()
|
155
|
+
if docker_socket_path and Path(docker_socket_path).exists():
|
156
|
+
volumes_config[docker_socket_path] = {
|
157
|
+
"bind": "/var/run/docker.sock",
|
158
|
+
"mode": "rw",
|
159
|
+
}
|
160
|
+
logger.info(f"Mounting Docker socket from {docker_socket_path}")
|
161
|
+
else:
|
162
|
+
logger.warning("Docker socket not found or not accessible")
|
163
|
+
|
164
|
+
sandbox = self.client.containers.run(
|
165
|
+
STRIX_IMAGE,
|
166
|
+
command="sleep infinity",
|
167
|
+
detach=True,
|
168
|
+
network_mode="host",
|
169
|
+
cap_add=["NET_ADMIN", "NET_RAW"],
|
170
|
+
labels={
|
171
|
+
STRIX_AGENT_LABEL: agent_id,
|
172
|
+
STRIX_SCAN_LABEL: scan_id,
|
173
|
+
},
|
174
|
+
environment={
|
175
|
+
"PYTHONUNBUFFERED": "1",
|
176
|
+
"STRIX_AGENT_ID": agent_id,
|
177
|
+
"STRIX_SANDBOX_TOKEN": auth_token,
|
178
|
+
"STRIX_TOOL_SERVER_PORT": str(tool_server_port),
|
179
|
+
"CAIDO_PORT": str(caido_port),
|
180
|
+
**(
|
181
|
+
{"DOCKER_HOST": os.environ["DOCKER_HOST"]}
|
182
|
+
if "DOCKER_HOST" in os.environ
|
183
|
+
else {}
|
184
|
+
),
|
185
|
+
},
|
186
|
+
volumes=volumes_config,
|
187
|
+
tty=True,
|
188
|
+
)
|
189
|
+
logger.info(
|
190
|
+
"Created new sandbox %s for agent %s with shared workspace %s",
|
191
|
+
sandbox.id,
|
192
|
+
agent_id,
|
193
|
+
volume_name,
|
194
|
+
)
|
195
|
+
except DockerException as e:
|
196
|
+
raise RuntimeError(f"Failed to create Docker sandbox: {e}") from e
|
197
|
+
|
198
|
+
assert sandbox is not None
|
199
|
+
if sandbox.status != "running":
|
200
|
+
sandbox.start()
|
201
|
+
time.sleep(15)
|
202
|
+
|
203
|
+
if local_source_path and volume_name not in _initialized_volumes:
|
204
|
+
self._copy_local_directory_to_container(sandbox, local_source_path)
|
205
|
+
_initialized_volumes.add(volume_name)
|
206
|
+
|
207
|
+
sandbox_id = sandbox.id
|
208
|
+
if sandbox_id is None:
|
209
|
+
raise RuntimeError("Docker container ID is unexpectedly None")
|
210
|
+
|
211
|
+
tool_server_port_str = sandbox.attrs["Config"]["Env"][
|
212
|
+
next(
|
213
|
+
(
|
214
|
+
i
|
215
|
+
for i, s in enumerate(sandbox.attrs["Config"]["Env"])
|
216
|
+
if s.startswith("STRIX_TOOL_SERVER_PORT=")
|
217
|
+
),
|
218
|
+
-1,
|
219
|
+
)
|
220
|
+
].split("=")[1]
|
221
|
+
tool_server_port = int(tool_server_port_str)
|
222
|
+
|
223
|
+
api_url = await self.get_sandbox_url(sandbox_id, tool_server_port)
|
224
|
+
|
225
|
+
return {
|
226
|
+
"workspace_id": sandbox_id,
|
227
|
+
"api_url": api_url,
|
228
|
+
"auth_token": auth_token,
|
229
|
+
"tool_server_port": tool_server_port,
|
230
|
+
}
|
231
|
+
|
232
|
+
async def get_sandbox_url(self, sandbox_id: str, port: int) -> str:
|
233
|
+
try:
|
234
|
+
container = self.client.containers.get(sandbox_id)
|
235
|
+
container.reload()
|
236
|
+
|
237
|
+
host = "localhost"
|
238
|
+
if "DOCKER_HOST" in os.environ:
|
239
|
+
docker_host = os.environ["DOCKER_HOST"]
|
240
|
+
if "://" in docker_host:
|
241
|
+
host = docker_host.split("://")[1].split(":")[0]
|
242
|
+
|
243
|
+
except NotFound:
|
244
|
+
raise ValueError(f"Sandbox {sandbox_id} not found.") from None
|
245
|
+
except DockerException as e:
|
246
|
+
raise RuntimeError(f"Failed to get sandbox URL for {sandbox_id}: {e}") from e
|
247
|
+
else:
|
248
|
+
return f"http://{host}:{port}"
|
249
|
+
|
250
|
+
async def destroy_sandbox(self, sandbox_id: str) -> None:
|
251
|
+
logger.info("Destroying Docker sandbox %s", sandbox_id)
|
252
|
+
try:
|
253
|
+
container = self.client.containers.get(sandbox_id)
|
254
|
+
|
255
|
+
scan_id = None
|
256
|
+
if container.labels and STRIX_SCAN_LABEL in container.labels:
|
257
|
+
scan_id = container.labels[STRIX_SCAN_LABEL]
|
258
|
+
|
259
|
+
container.stop()
|
260
|
+
container.remove()
|
261
|
+
logger.info("Successfully destroyed sandbox %s", sandbox_id)
|
262
|
+
|
263
|
+
if scan_id:
|
264
|
+
await self._cleanup_scan_workspace_if_empty(scan_id)
|
265
|
+
|
266
|
+
except NotFound:
|
267
|
+
logger.warning("Sandbox %s not found for destruction.", sandbox_id)
|
268
|
+
except DockerException as e:
|
269
|
+
logger.warning("Failed to destroy sandbox %s: %s", sandbox_id, e)
|
270
|
+
|
271
|
+
async def _cleanup_scan_workspace_if_empty(self, scan_id: str) -> None:
|
272
|
+
try:
|
273
|
+
volume_name = self._get_workspace_volume_name(scan_id)
|
274
|
+
|
275
|
+
containers = self.client.containers.list(
|
276
|
+
all=True, filters={"label": f"{STRIX_SCAN_LABEL}={scan_id}"}
|
277
|
+
)
|
278
|
+
|
279
|
+
if not containers:
|
280
|
+
try:
|
281
|
+
volume = self.client.volumes.get(volume_name)
|
282
|
+
volume.remove()
|
283
|
+
logger.info(
|
284
|
+
f"Cleaned up workspace volume {volume_name} for completed scan {scan_id}"
|
285
|
+
)
|
286
|
+
|
287
|
+
_initialized_volumes.discard(volume_name)
|
288
|
+
|
289
|
+
except NotFound:
|
290
|
+
logger.debug(f"Volume {volume_name} already removed")
|
291
|
+
except DockerException as e:
|
292
|
+
logger.warning(f"Failed to remove volume {volume_name}: {e}")
|
293
|
+
|
294
|
+
except DockerException as e:
|
295
|
+
logger.warning("Error during workspace cleanup for scan %s: %s", scan_id, e)
|
296
|
+
|
297
|
+
async def cleanup_scan_workspace(self, scan_id: str) -> None:
|
298
|
+
await self._cleanup_scan_workspace_if_empty(scan_id)
|
strix/runtime/runtime.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from typing import TypedDict
|
3
|
+
|
4
|
+
|
5
|
+
class SandboxInfo(TypedDict):
|
6
|
+
workspace_id: str
|
7
|
+
api_url: str
|
8
|
+
auth_token: str | None
|
9
|
+
tool_server_port: int
|
10
|
+
|
11
|
+
|
12
|
+
class AbstractRuntime(ABC):
|
13
|
+
@abstractmethod
|
14
|
+
async def create_sandbox(
|
15
|
+
self, agent_id: str, existing_token: str | None = None, local_source_path: str | None = None
|
16
|
+
) -> SandboxInfo:
|
17
|
+
raise NotImplementedError
|
18
|
+
|
19
|
+
@abstractmethod
|
20
|
+
async def get_sandbox_url(self, sandbox_id: str, port: int) -> str:
|
21
|
+
raise NotImplementedError
|
22
|
+
|
23
|
+
@abstractmethod
|
24
|
+
async def destroy_sandbox(self, sandbox_id: str) -> None:
|
25
|
+
raise NotImplementedError
|
@@ -0,0 +1,97 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
from typing import Any
|
4
|
+
|
5
|
+
from fastapi import Depends, FastAPI, HTTPException, status
|
6
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
7
|
+
from pydantic import BaseModel, ValidationError
|
8
|
+
|
9
|
+
|
10
|
+
SANDBOX_MODE = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
|
11
|
+
if not SANDBOX_MODE:
|
12
|
+
raise RuntimeError("Tool server should only run in sandbox mode (STRIX_SANDBOX_MODE=true)")
|
13
|
+
|
14
|
+
EXPECTED_TOKEN = os.getenv("STRIX_SANDBOX_TOKEN")
|
15
|
+
if not EXPECTED_TOKEN:
|
16
|
+
raise RuntimeError("STRIX_SANDBOX_TOKEN environment variable is required in sandbox mode")
|
17
|
+
|
18
|
+
app = FastAPI()
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
security = HTTPBearer()
|
21
|
+
|
22
|
+
security_dependency = Depends(security)
|
23
|
+
|
24
|
+
|
25
|
+
def verify_token(credentials: HTTPAuthorizationCredentials) -> str:
|
26
|
+
if not credentials or credentials.scheme != "Bearer":
|
27
|
+
logger.warning("Authentication failed: Invalid or missing Bearer token scheme")
|
28
|
+
raise HTTPException(
|
29
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
30
|
+
detail="Invalid authentication scheme. Bearer token required.",
|
31
|
+
headers={"WWW-Authenticate": "Bearer"},
|
32
|
+
)
|
33
|
+
|
34
|
+
if credentials.credentials != EXPECTED_TOKEN:
|
35
|
+
logger.warning("Authentication failed: Invalid token provided from remote host")
|
36
|
+
raise HTTPException(
|
37
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
38
|
+
detail="Invalid authentication token",
|
39
|
+
headers={"WWW-Authenticate": "Bearer"},
|
40
|
+
)
|
41
|
+
|
42
|
+
logger.debug("Authentication successful for tool execution request")
|
43
|
+
return credentials.credentials
|
44
|
+
|
45
|
+
|
46
|
+
class ToolExecutionRequest(BaseModel):
|
47
|
+
tool_name: str
|
48
|
+
kwargs: dict[str, Any]
|
49
|
+
|
50
|
+
|
51
|
+
class ToolExecutionResponse(BaseModel):
|
52
|
+
result: Any | None = None
|
53
|
+
error: str | None = None
|
54
|
+
|
55
|
+
|
56
|
+
@app.post("/execute", response_model=ToolExecutionResponse)
|
57
|
+
async def execute_tool(
|
58
|
+
request: ToolExecutionRequest, credentials: HTTPAuthorizationCredentials = security_dependency
|
59
|
+
) -> ToolExecutionResponse:
|
60
|
+
verify_token(credentials)
|
61
|
+
|
62
|
+
from strix.tools.argument_parser import ArgumentConversionError, convert_arguments
|
63
|
+
from strix.tools.registry import get_tool_by_name
|
64
|
+
|
65
|
+
try:
|
66
|
+
tool_func = get_tool_by_name(request.tool_name)
|
67
|
+
if not tool_func:
|
68
|
+
return ToolExecutionResponse(error=f"Tool '{request.tool_name}' not found")
|
69
|
+
|
70
|
+
converted_kwargs = convert_arguments(tool_func, request.kwargs)
|
71
|
+
|
72
|
+
result = tool_func(**converted_kwargs)
|
73
|
+
|
74
|
+
return ToolExecutionResponse(result=result)
|
75
|
+
|
76
|
+
except (ArgumentConversionError, ValidationError) as e:
|
77
|
+
logger.warning("Invalid tool arguments: %s", e)
|
78
|
+
return ToolExecutionResponse(error=f"Invalid arguments: {e}")
|
79
|
+
except TypeError as e:
|
80
|
+
logger.warning("Tool execution type error: %s", e)
|
81
|
+
return ToolExecutionResponse(error=f"Tool execution error: {e}")
|
82
|
+
except ValueError as e:
|
83
|
+
logger.warning("Tool execution value error: %s", e)
|
84
|
+
return ToolExecutionResponse(error=f"Tool execution error: {e}")
|
85
|
+
except Exception:
|
86
|
+
logger.exception("Unexpected error during tool execution")
|
87
|
+
return ToolExecutionResponse(error="Internal server error")
|
88
|
+
|
89
|
+
|
90
|
+
@app.get("/health")
|
91
|
+
async def health_check() -> dict[str, str]:
|
92
|
+
return {
|
93
|
+
"status": "healthy",
|
94
|
+
"sandbox_mode": str(SANDBOX_MODE),
|
95
|
+
"environment": "sandbox" if SANDBOX_MODE else "main",
|
96
|
+
"auth_configured": "true" if EXPECTED_TOKEN else "false",
|
97
|
+
}
|
strix/tools/__init__.py
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from .executor import (
|
4
|
+
execute_tool,
|
5
|
+
execute_tool_invocation,
|
6
|
+
execute_tool_with_validation,
|
7
|
+
extract_screenshot_from_result,
|
8
|
+
process_tool_invocations,
|
9
|
+
remove_screenshot_from_result,
|
10
|
+
validate_tool_availability,
|
11
|
+
)
|
12
|
+
from .registry import (
|
13
|
+
ImplementedInClientSideOnlyError,
|
14
|
+
get_tool_by_name,
|
15
|
+
get_tool_names,
|
16
|
+
get_tools_prompt,
|
17
|
+
needs_agent_state,
|
18
|
+
register_tool,
|
19
|
+
tools,
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
SANDBOX_MODE = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
|
24
|
+
|
25
|
+
HAS_PERPLEXITY_API = bool(os.getenv("PERPLEXITY_API_KEY"))
|
26
|
+
|
27
|
+
if not SANDBOX_MODE:
|
28
|
+
from .agents_graph import * # noqa: F403
|
29
|
+
from .browser import * # noqa: F403
|
30
|
+
from .file_edit import * # noqa: F403
|
31
|
+
from .finish import * # noqa: F403
|
32
|
+
from .notes import * # noqa: F403
|
33
|
+
from .proxy import * # noqa: F403
|
34
|
+
from .python import * # noqa: F403
|
35
|
+
from .reporting import * # noqa: F403
|
36
|
+
from .terminal import * # noqa: F403
|
37
|
+
from .thinking import * # noqa: F403
|
38
|
+
|
39
|
+
if HAS_PERPLEXITY_API:
|
40
|
+
from .web_search import * # noqa: F403
|
41
|
+
else:
|
42
|
+
from .browser import * # noqa: F403
|
43
|
+
from .file_edit import * # noqa: F403
|
44
|
+
from .notes import * # noqa: F403
|
45
|
+
from .proxy import * # noqa: F403
|
46
|
+
from .python import * # noqa: F403
|
47
|
+
from .terminal import * # noqa: F403
|
48
|
+
|
49
|
+
__all__ = [
|
50
|
+
"ImplementedInClientSideOnlyError",
|
51
|
+
"execute_tool",
|
52
|
+
"execute_tool_invocation",
|
53
|
+
"execute_tool_with_validation",
|
54
|
+
"extract_screenshot_from_result",
|
55
|
+
"get_tool_by_name",
|
56
|
+
"get_tool_names",
|
57
|
+
"get_tools_prompt",
|
58
|
+
"needs_agent_state",
|
59
|
+
"process_tool_invocations",
|
60
|
+
"register_tool",
|
61
|
+
"remove_screenshot_from_result",
|
62
|
+
"tools",
|
63
|
+
"validate_tool_availability",
|
64
|
+
]
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from .agents_graph_actions import (
|
2
|
+
agent_finish,
|
3
|
+
create_agent,
|
4
|
+
send_message_to_agent,
|
5
|
+
view_agent_graph,
|
6
|
+
wait_for_message,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"agent_finish",
|
12
|
+
"create_agent",
|
13
|
+
"send_message_to_agent",
|
14
|
+
"view_agent_graph",
|
15
|
+
"wait_for_message",
|
16
|
+
]
|