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
strix/interface/main.py
CHANGED
|
@@ -6,10 +6,10 @@ Strix Agent Interface
|
|
|
6
6
|
import argparse
|
|
7
7
|
import asyncio
|
|
8
8
|
import logging
|
|
9
|
-
import os
|
|
10
9
|
import shutil
|
|
11
10
|
import sys
|
|
12
11
|
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
13
|
|
|
14
14
|
import litellm
|
|
15
15
|
from docker.errors import DockerException
|
|
@@ -17,9 +17,14 @@ from rich.console import Console
|
|
|
17
17
|
from rich.panel import Panel
|
|
18
18
|
from rich.text import Text
|
|
19
19
|
|
|
20
|
-
from strix.
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
from strix.config import Config, apply_saved_config, save_current_config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
apply_saved_config()
|
|
24
|
+
|
|
25
|
+
from strix.interface.cli import run_cli # noqa: E402
|
|
26
|
+
from strix.interface.tui import run_tui # noqa: E402
|
|
27
|
+
from strix.interface.utils import ( # noqa: E402
|
|
23
28
|
assign_workspace_subdirs,
|
|
24
29
|
build_final_stats_text,
|
|
25
30
|
check_docker_connection,
|
|
@@ -29,10 +34,12 @@ from strix.interface.utils import (
|
|
|
29
34
|
image_exists,
|
|
30
35
|
infer_target_type,
|
|
31
36
|
process_pull_line,
|
|
37
|
+
rewrite_localhost_targets,
|
|
32
38
|
validate_llm_response,
|
|
33
39
|
)
|
|
34
|
-
from strix.runtime.docker_runtime import
|
|
35
|
-
from strix.telemetry
|
|
40
|
+
from strix.runtime.docker_runtime import HOST_GATEWAY_HOSTNAME # noqa: E402
|
|
41
|
+
from strix.telemetry import posthog # noqa: E402
|
|
42
|
+
from strix.telemetry.tracer import get_global_tracer # noqa: E402
|
|
36
43
|
|
|
37
44
|
|
|
38
45
|
logging.getLogger().setLevel(logging.ERROR)
|
|
@@ -43,30 +50,30 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|
|
43
50
|
missing_required_vars = []
|
|
44
51
|
missing_optional_vars = []
|
|
45
52
|
|
|
46
|
-
if not
|
|
53
|
+
if not Config.get("strix_llm"):
|
|
47
54
|
missing_required_vars.append("STRIX_LLM")
|
|
48
55
|
|
|
49
56
|
has_base_url = any(
|
|
50
57
|
[
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
Config.get("llm_api_base"),
|
|
59
|
+
Config.get("openai_api_base"),
|
|
60
|
+
Config.get("litellm_base_url"),
|
|
61
|
+
Config.get("ollama_api_base"),
|
|
55
62
|
]
|
|
56
63
|
)
|
|
57
64
|
|
|
58
|
-
if not
|
|
59
|
-
|
|
60
|
-
missing_required_vars.append("LLM_API_KEY")
|
|
61
|
-
else:
|
|
62
|
-
missing_optional_vars.append("LLM_API_KEY")
|
|
65
|
+
if not Config.get("llm_api_key"):
|
|
66
|
+
missing_optional_vars.append("LLM_API_KEY")
|
|
63
67
|
|
|
64
68
|
if not has_base_url:
|
|
65
69
|
missing_optional_vars.append("LLM_API_BASE")
|
|
66
70
|
|
|
67
|
-
if not
|
|
71
|
+
if not Config.get("perplexity_api_key"):
|
|
68
72
|
missing_optional_vars.append("PERPLEXITY_API_KEY")
|
|
69
73
|
|
|
74
|
+
if not Config.get("strix_reasoning_effort"):
|
|
75
|
+
missing_optional_vars.append("STRIX_REASONING_EFFORT")
|
|
76
|
+
|
|
70
77
|
if missing_required_vars:
|
|
71
78
|
error_text = Text()
|
|
72
79
|
error_text.append("❌ ", style="bold red")
|
|
@@ -92,13 +99,6 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|
|
92
99
|
" - Model name to use with litellm (e.g., 'openai/gpt-5')\n",
|
|
93
100
|
style="white",
|
|
94
101
|
)
|
|
95
|
-
elif var == "LLM_API_KEY":
|
|
96
|
-
error_text.append("• ", style="white")
|
|
97
|
-
error_text.append("LLM_API_KEY", style="bold cyan")
|
|
98
|
-
error_text.append(
|
|
99
|
-
" - API key for the LLM provider (required for cloud providers)\n",
|
|
100
|
-
style="white",
|
|
101
|
-
)
|
|
102
102
|
|
|
103
103
|
if missing_optional_vars:
|
|
104
104
|
error_text.append("\nOptional environment variables:\n", style="white")
|
|
@@ -106,7 +106,11 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|
|
106
106
|
if var == "LLM_API_KEY":
|
|
107
107
|
error_text.append("• ", style="white")
|
|
108
108
|
error_text.append("LLM_API_KEY", style="bold cyan")
|
|
109
|
-
error_text.append(
|
|
109
|
+
error_text.append(
|
|
110
|
+
" - API key for the LLM provider "
|
|
111
|
+
"(not needed for local models, Vertex AI, AWS, etc.)\n",
|
|
112
|
+
style="white",
|
|
113
|
+
)
|
|
110
114
|
elif var == "LLM_API_BASE":
|
|
111
115
|
error_text.append("• ", style="white")
|
|
112
116
|
error_text.append("LLM_API_BASE", style="bold cyan")
|
|
@@ -121,18 +125,24 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|
|
121
125
|
" - API key for Perplexity AI web search (enables real-time research)\n",
|
|
122
126
|
style="white",
|
|
123
127
|
)
|
|
128
|
+
elif var == "STRIX_REASONING_EFFORT":
|
|
129
|
+
error_text.append("• ", style="white")
|
|
130
|
+
error_text.append("STRIX_REASONING_EFFORT", style="bold cyan")
|
|
131
|
+
error_text.append(
|
|
132
|
+
" - Reasoning effort level: none, minimal, low, medium, high, xhigh "
|
|
133
|
+
"(default: high)\n",
|
|
134
|
+
style="white",
|
|
135
|
+
)
|
|
124
136
|
|
|
125
137
|
error_text.append("\nExample setup:\n", style="white")
|
|
126
138
|
error_text.append("export STRIX_LLM='openai/gpt-5'\n", style="dim white")
|
|
127
139
|
|
|
128
|
-
if "LLM_API_KEY" in missing_required_vars:
|
|
129
|
-
error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white")
|
|
130
|
-
|
|
131
140
|
if missing_optional_vars:
|
|
132
141
|
for var in missing_optional_vars:
|
|
133
142
|
if var == "LLM_API_KEY":
|
|
134
143
|
error_text.append(
|
|
135
|
-
"export LLM_API_KEY='your-api-key-here'
|
|
144
|
+
"export LLM_API_KEY='your-api-key-here' "
|
|
145
|
+
"# not needed for local models, Vertex AI, AWS, etc.\n",
|
|
136
146
|
style="dim white",
|
|
137
147
|
)
|
|
138
148
|
elif var == "LLM_API_BASE":
|
|
@@ -145,6 +155,11 @@ def validate_environment() -> None: # noqa: PLR0912, PLR0915
|
|
|
145
155
|
error_text.append(
|
|
146
156
|
"export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white"
|
|
147
157
|
)
|
|
158
|
+
elif var == "STRIX_REASONING_EFFORT":
|
|
159
|
+
error_text.append(
|
|
160
|
+
"export STRIX_REASONING_EFFORT='high'\n",
|
|
161
|
+
style="dim white",
|
|
162
|
+
)
|
|
148
163
|
|
|
149
164
|
panel = Panel(
|
|
150
165
|
error_text,
|
|
@@ -187,33 +202,33 @@ async def warm_up_llm() -> None:
|
|
|
187
202
|
console = Console()
|
|
188
203
|
|
|
189
204
|
try:
|
|
190
|
-
model_name =
|
|
191
|
-
api_key =
|
|
192
|
-
|
|
193
|
-
if api_key:
|
|
194
|
-
litellm.api_key = api_key
|
|
195
|
-
|
|
205
|
+
model_name = Config.get("strix_llm")
|
|
206
|
+
api_key = Config.get("llm_api_key")
|
|
196
207
|
api_base = (
|
|
197
|
-
|
|
198
|
-
or
|
|
199
|
-
or
|
|
200
|
-
or
|
|
208
|
+
Config.get("llm_api_base")
|
|
209
|
+
or Config.get("openai_api_base")
|
|
210
|
+
or Config.get("litellm_base_url")
|
|
211
|
+
or Config.get("ollama_api_base")
|
|
201
212
|
)
|
|
202
|
-
if api_base:
|
|
203
|
-
litellm.api_base = api_base
|
|
204
213
|
|
|
205
214
|
test_messages = [
|
|
206
215
|
{"role": "system", "content": "You are a helpful assistant."},
|
|
207
216
|
{"role": "user", "content": "Reply with just 'OK'."},
|
|
208
217
|
]
|
|
209
218
|
|
|
210
|
-
llm_timeout = int(
|
|
219
|
+
llm_timeout = int(Config.get("llm_timeout") or "300")
|
|
211
220
|
|
|
212
|
-
|
|
213
|
-
model
|
|
214
|
-
messages
|
|
215
|
-
timeout
|
|
216
|
-
|
|
221
|
+
completion_kwargs: dict[str, Any] = {
|
|
222
|
+
"model": model_name,
|
|
223
|
+
"messages": test_messages,
|
|
224
|
+
"timeout": llm_timeout,
|
|
225
|
+
}
|
|
226
|
+
if api_key:
|
|
227
|
+
completion_kwargs["api_key"] = api_key
|
|
228
|
+
if api_base:
|
|
229
|
+
completion_kwargs["api_base"] = api_base
|
|
230
|
+
|
|
231
|
+
response = litellm.completion(**completion_kwargs)
|
|
217
232
|
|
|
218
233
|
validate_llm_response(response)
|
|
219
234
|
|
|
@@ -240,6 +255,15 @@ async def warm_up_llm() -> None:
|
|
|
240
255
|
sys.exit(1)
|
|
241
256
|
|
|
242
257
|
|
|
258
|
+
def get_version() -> str:
|
|
259
|
+
try:
|
|
260
|
+
from importlib.metadata import version
|
|
261
|
+
|
|
262
|
+
return version("strix-agent")
|
|
263
|
+
except Exception: # noqa: BLE001
|
|
264
|
+
return "unknown"
|
|
265
|
+
|
|
266
|
+
|
|
243
267
|
def parse_arguments() -> argparse.Namespace:
|
|
244
268
|
parser = argparse.ArgumentParser(
|
|
245
269
|
description="Strix Multi-Agent Cybersecurity Penetration Testing Tool",
|
|
@@ -270,11 +294,18 @@ Examples:
|
|
|
270
294
|
strix --target example.com --instruction "Focus on authentication vulnerabilities"
|
|
271
295
|
|
|
272
296
|
# Custom instructions (from file)
|
|
273
|
-
strix --target example.com --instruction ./instructions.txt
|
|
274
|
-
strix --target https://app.com --instruction /path/to/detailed_instructions.md
|
|
297
|
+
strix --target example.com --instruction-file ./instructions.txt
|
|
298
|
+
strix --target https://app.com --instruction-file /path/to/detailed_instructions.md
|
|
275
299
|
""",
|
|
276
300
|
)
|
|
277
301
|
|
|
302
|
+
parser.add_argument(
|
|
303
|
+
"-v",
|
|
304
|
+
"--version",
|
|
305
|
+
action="version",
|
|
306
|
+
version=f"strix {get_version()}",
|
|
307
|
+
)
|
|
308
|
+
|
|
278
309
|
parser.add_argument(
|
|
279
310
|
"-t",
|
|
280
311
|
"--target",
|
|
@@ -292,15 +323,15 @@ Examples:
|
|
|
292
323
|
"testing approaches (e.g., 'Perform thorough authentication testing'), "
|
|
293
324
|
"test credentials (e.g., 'Use the following credentials to access the app: "
|
|
294
325
|
"admin:password123'), "
|
|
295
|
-
"or areas of interest (e.g., 'Check login API endpoint for security issues').
|
|
296
|
-
"You can also provide a path to a file containing detailed instructions "
|
|
297
|
-
"(e.g., '--instruction ./instructions.txt').",
|
|
326
|
+
"or areas of interest (e.g., 'Check login API endpoint for security issues').",
|
|
298
327
|
)
|
|
299
328
|
|
|
300
329
|
parser.add_argument(
|
|
301
|
-
"--
|
|
330
|
+
"--instruction-file",
|
|
302
331
|
type=str,
|
|
303
|
-
help="
|
|
332
|
+
help="Path to a file containing detailed custom instructions for the penetration test. "
|
|
333
|
+
"Use this option when you have lengthy or complex instructions saved in a file "
|
|
334
|
+
"(e.g., '--instruction-file ./detailed_instructions.txt').",
|
|
304
335
|
)
|
|
305
336
|
|
|
306
337
|
parser.add_argument(
|
|
@@ -313,18 +344,37 @@ Examples:
|
|
|
313
344
|
),
|
|
314
345
|
)
|
|
315
346
|
|
|
347
|
+
parser.add_argument(
|
|
348
|
+
"-m",
|
|
349
|
+
"--scan-mode",
|
|
350
|
+
type=str,
|
|
351
|
+
choices=["quick", "standard", "deep"],
|
|
352
|
+
default="deep",
|
|
353
|
+
help=(
|
|
354
|
+
"Scan mode: "
|
|
355
|
+
"'quick' for fast CI/CD checks, "
|
|
356
|
+
"'standard' for routine testing, "
|
|
357
|
+
"'deep' for thorough security reviews (default). "
|
|
358
|
+
"Default: deep."
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
|
|
316
362
|
args = parser.parse_args()
|
|
317
363
|
|
|
318
|
-
if args.instruction:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
364
|
+
if args.instruction and args.instruction_file:
|
|
365
|
+
parser.error(
|
|
366
|
+
"Cannot specify both --instruction and --instruction-file. Use one or the other."
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if args.instruction_file:
|
|
370
|
+
instruction_path = Path(args.instruction_file)
|
|
371
|
+
try:
|
|
372
|
+
with instruction_path.open(encoding="utf-8") as f:
|
|
373
|
+
args.instruction = f.read().strip()
|
|
374
|
+
if not args.instruction:
|
|
375
|
+
parser.error(f"Instruction file '{instruction_path}' is empty")
|
|
376
|
+
except Exception as e: # noqa: BLE001
|
|
377
|
+
parser.error(f"Failed to read instruction file '{instruction_path}': {e}")
|
|
328
378
|
|
|
329
379
|
args.targets_info = []
|
|
330
380
|
for target in args.target:
|
|
@@ -343,6 +393,7 @@ Examples:
|
|
|
343
393
|
parser.error(f"Invalid target '{target}'")
|
|
344
394
|
|
|
345
395
|
assign_workspace_subdirs(args.targets_info)
|
|
396
|
+
rewrite_localhost_targets(args.targets_info, HOST_GATEWAY_HOSTNAME)
|
|
346
397
|
|
|
347
398
|
return args
|
|
348
399
|
|
|
@@ -410,17 +461,20 @@ def display_completion_message(args: argparse.Namespace, results_path: Path) ->
|
|
|
410
461
|
console.print("\n")
|
|
411
462
|
console.print(panel)
|
|
412
463
|
console.print()
|
|
464
|
+
console.print("[dim]🌐 Website:[/] [cyan]https://strix.ai[/]")
|
|
465
|
+
console.print("[dim]💬 Discord:[/] [cyan]https://discord.gg/YjKFvEZSdZ[/]")
|
|
466
|
+
console.print()
|
|
413
467
|
|
|
414
468
|
|
|
415
469
|
def pull_docker_image() -> None:
|
|
416
470
|
console = Console()
|
|
417
471
|
client = check_docker_connection()
|
|
418
472
|
|
|
419
|
-
if image_exists(client,
|
|
473
|
+
if image_exists(client, Config.get("strix_image")): # type: ignore[arg-type]
|
|
420
474
|
return
|
|
421
475
|
|
|
422
476
|
console.print()
|
|
423
|
-
console.print(f"[bold cyan]🐳 Pulling Docker image:[/] {
|
|
477
|
+
console.print(f"[bold cyan]🐳 Pulling Docker image:[/] {Config.get('strix_image')}")
|
|
424
478
|
console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]")
|
|
425
479
|
console.print()
|
|
426
480
|
|
|
@@ -429,7 +483,7 @@ def pull_docker_image() -> None:
|
|
|
429
483
|
layers_info: dict[str, str] = {}
|
|
430
484
|
last_update = ""
|
|
431
485
|
|
|
432
|
-
for line in client.api.pull(
|
|
486
|
+
for line in client.api.pull(Config.get("strix_image"), stream=True, decode=True):
|
|
433
487
|
last_update = process_pull_line(line, layers_info, status, last_update)
|
|
434
488
|
|
|
435
489
|
except DockerException as e:
|
|
@@ -438,7 +492,7 @@ def pull_docker_image() -> None:
|
|
|
438
492
|
error_text.append("❌ ", style="bold red")
|
|
439
493
|
error_text.append("FAILED TO PULL IMAGE", style="bold red")
|
|
440
494
|
error_text.append("\n\n", style="white")
|
|
441
|
-
error_text.append(f"Could not download: {
|
|
495
|
+
error_text.append(f"Could not download: {Config.get('strix_image')}\n", style="white")
|
|
442
496
|
error_text.append(str(e), style="dim red")
|
|
443
497
|
|
|
444
498
|
panel = Panel(
|
|
@@ -470,8 +524,9 @@ def main() -> None:
|
|
|
470
524
|
validate_environment()
|
|
471
525
|
asyncio.run(warm_up_llm())
|
|
472
526
|
|
|
473
|
-
|
|
474
|
-
|
|
527
|
+
save_current_config()
|
|
528
|
+
|
|
529
|
+
args.run_name = generate_run_name(args.targets_info)
|
|
475
530
|
|
|
476
531
|
for target_info in args.targets_info:
|
|
477
532
|
if target_info["type"] == "repository":
|
|
@@ -482,10 +537,32 @@ def main() -> None:
|
|
|
482
537
|
|
|
483
538
|
args.local_sources = collect_local_sources(args.targets_info)
|
|
484
539
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
540
|
+
is_whitebox = bool(args.local_sources)
|
|
541
|
+
|
|
542
|
+
posthog.start(
|
|
543
|
+
model=Config.get("strix_llm"),
|
|
544
|
+
scan_mode=args.scan_mode,
|
|
545
|
+
is_whitebox=is_whitebox,
|
|
546
|
+
interactive=not args.non_interactive,
|
|
547
|
+
has_instructions=bool(args.instruction),
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
exit_reason = "user_exit"
|
|
551
|
+
try:
|
|
552
|
+
if args.non_interactive:
|
|
553
|
+
asyncio.run(run_cli(args))
|
|
554
|
+
else:
|
|
555
|
+
asyncio.run(run_tui(args))
|
|
556
|
+
except KeyboardInterrupt:
|
|
557
|
+
exit_reason = "interrupted"
|
|
558
|
+
except Exception as e:
|
|
559
|
+
exit_reason = "error"
|
|
560
|
+
posthog.error("unhandled_exception", str(e))
|
|
561
|
+
raise
|
|
562
|
+
finally:
|
|
563
|
+
tracer = get_global_tracer()
|
|
564
|
+
if tracer:
|
|
565
|
+
posthog.end(tracer, exit_reason=exit_reason)
|
|
489
566
|
|
|
490
567
|
results_path = Path("strix_runs") / args.run_name
|
|
491
568
|
display_completion_message(args, results_path)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import html
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_FUNCTION_TAG_PREFIX = "<function="
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_safe_content(content: str) -> tuple[str, str]:
|
|
11
|
+
if not content:
|
|
12
|
+
return "", ""
|
|
13
|
+
|
|
14
|
+
last_lt = content.rfind("<")
|
|
15
|
+
if last_lt == -1:
|
|
16
|
+
return content, ""
|
|
17
|
+
|
|
18
|
+
suffix = content[last_lt:]
|
|
19
|
+
target = _FUNCTION_TAG_PREFIX # "<function="
|
|
20
|
+
|
|
21
|
+
if target.startswith(suffix):
|
|
22
|
+
return content[:last_lt], suffix
|
|
23
|
+
|
|
24
|
+
return content, ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class StreamSegment:
|
|
29
|
+
type: Literal["text", "tool"]
|
|
30
|
+
content: str
|
|
31
|
+
tool_name: str | None = None
|
|
32
|
+
args: dict[str, str] | None = None
|
|
33
|
+
is_complete: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_streaming_content(content: str) -> list[StreamSegment]:
|
|
37
|
+
if not content:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
segments: list[StreamSegment] = []
|
|
41
|
+
|
|
42
|
+
func_pattern = r"<function=([^>]+)>"
|
|
43
|
+
func_matches = list(re.finditer(func_pattern, content))
|
|
44
|
+
|
|
45
|
+
if not func_matches:
|
|
46
|
+
safe_content, _ = _get_safe_content(content)
|
|
47
|
+
text = safe_content.strip()
|
|
48
|
+
if text:
|
|
49
|
+
segments.append(StreamSegment(type="text", content=text))
|
|
50
|
+
return segments
|
|
51
|
+
|
|
52
|
+
first_func_start = func_matches[0].start()
|
|
53
|
+
if first_func_start > 0:
|
|
54
|
+
text_before = content[:first_func_start].strip()
|
|
55
|
+
if text_before:
|
|
56
|
+
segments.append(StreamSegment(type="text", content=text_before))
|
|
57
|
+
|
|
58
|
+
for i, match in enumerate(func_matches):
|
|
59
|
+
tool_name = match.group(1)
|
|
60
|
+
func_start = match.end()
|
|
61
|
+
|
|
62
|
+
func_end_match = re.search(r"</function>", content[func_start:])
|
|
63
|
+
|
|
64
|
+
if func_end_match:
|
|
65
|
+
func_body = content[func_start : func_start + func_end_match.start()]
|
|
66
|
+
is_complete = True
|
|
67
|
+
end_pos = func_start + func_end_match.end()
|
|
68
|
+
else:
|
|
69
|
+
if i + 1 < len(func_matches):
|
|
70
|
+
next_func_start = func_matches[i + 1].start()
|
|
71
|
+
func_body = content[func_start:next_func_start]
|
|
72
|
+
else:
|
|
73
|
+
func_body = content[func_start:]
|
|
74
|
+
is_complete = False
|
|
75
|
+
end_pos = len(content)
|
|
76
|
+
|
|
77
|
+
args = _parse_streaming_params(func_body)
|
|
78
|
+
|
|
79
|
+
segments.append(
|
|
80
|
+
StreamSegment(
|
|
81
|
+
type="tool",
|
|
82
|
+
content=func_body,
|
|
83
|
+
tool_name=tool_name,
|
|
84
|
+
args=args,
|
|
85
|
+
is_complete=is_complete,
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if is_complete and i + 1 < len(func_matches):
|
|
90
|
+
next_start = func_matches[i + 1].start()
|
|
91
|
+
text_between = content[end_pos:next_start].strip()
|
|
92
|
+
if text_between:
|
|
93
|
+
segments.append(StreamSegment(type="text", content=text_between))
|
|
94
|
+
|
|
95
|
+
return segments
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _parse_streaming_params(func_body: str) -> dict[str, str]:
|
|
99
|
+
args: dict[str, str] = {}
|
|
100
|
+
|
|
101
|
+
complete_pattern = r"<parameter=([^>]+)>(.*?)</parameter>"
|
|
102
|
+
complete_matches = list(re.finditer(complete_pattern, func_body, re.DOTALL))
|
|
103
|
+
complete_end_pos = 0
|
|
104
|
+
|
|
105
|
+
for match in complete_matches:
|
|
106
|
+
param_name = match.group(1)
|
|
107
|
+
param_value = html.unescape(match.group(2).strip())
|
|
108
|
+
args[param_name] = param_value
|
|
109
|
+
complete_end_pos = max(complete_end_pos, match.end())
|
|
110
|
+
|
|
111
|
+
remaining = func_body[complete_end_pos:]
|
|
112
|
+
incomplete_pattern = r"<parameter=([^>]+)>(.*)$"
|
|
113
|
+
incomplete_match = re.search(incomplete_pattern, remaining, re.DOTALL)
|
|
114
|
+
if incomplete_match:
|
|
115
|
+
param_name = incomplete_match.group(1)
|
|
116
|
+
param_value = html.unescape(incomplete_match.group(2).strip())
|
|
117
|
+
args[param_name] = param_value
|
|
118
|
+
|
|
119
|
+
return args
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from . import (
|
|
2
|
+
agent_message_renderer,
|
|
2
3
|
agents_graph_renderer,
|
|
3
4
|
browser_renderer,
|
|
4
5
|
file_edit_renderer,
|
|
@@ -10,6 +11,7 @@ from . import (
|
|
|
10
11
|
scan_info_renderer,
|
|
11
12
|
terminal_renderer,
|
|
12
13
|
thinking_renderer,
|
|
14
|
+
todo_renderer,
|
|
13
15
|
user_message_renderer,
|
|
14
16
|
web_search_renderer,
|
|
15
17
|
)
|
|
@@ -20,6 +22,7 @@ from .registry import ToolTUIRegistry, get_tool_renderer, register_tool_renderer
|
|
|
20
22
|
__all__ = [
|
|
21
23
|
"BaseToolRenderer",
|
|
22
24
|
"ToolTUIRegistry",
|
|
25
|
+
"agent_message_renderer",
|
|
23
26
|
"agents_graph_renderer",
|
|
24
27
|
"browser_renderer",
|
|
25
28
|
"file_edit_renderer",
|
|
@@ -34,6 +37,7 @@ __all__ = [
|
|
|
34
37
|
"scan_info_renderer",
|
|
35
38
|
"terminal_renderer",
|
|
36
39
|
"thinking_renderer",
|
|
40
|
+
"todo_renderer",
|
|
37
41
|
"user_message_renderer",
|
|
38
42
|
"web_search_renderer",
|
|
39
43
|
]
|