strix-agent 0.1.18__py3-none-any.whl → 0.3.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.

Potentially problematic release.


This version of strix-agent might be problematic. Click here for more details.

Files changed (68) hide show
  1. strix/agents/StrixAgent/strix_agent.py +49 -39
  2. strix/agents/StrixAgent/system_prompt.jinja +23 -10
  3. strix/agents/base_agent.py +90 -10
  4. strix/agents/state.py +23 -2
  5. strix/interface/cli.py +171 -0
  6. strix/interface/main.py +482 -0
  7. strix/{cli → interface}/tool_components/base_renderer.py +2 -2
  8. strix/{cli → interface}/tool_components/reporting_renderer.py +2 -1
  9. strix/{cli → interface}/tool_components/scan_info_renderer.py +17 -12
  10. strix/{cli/app.py → interface/tui.py} +107 -31
  11. strix/interface/utils.py +435 -0
  12. strix/prompts/README.md +64 -0
  13. strix/prompts/__init__.py +1 -1
  14. strix/prompts/cloud/.gitkeep +0 -0
  15. strix/prompts/custom/.gitkeep +0 -0
  16. strix/prompts/frameworks/fastapi.jinja +142 -0
  17. strix/prompts/frameworks/nextjs.jinja +126 -0
  18. strix/prompts/protocols/graphql.jinja +215 -0
  19. strix/prompts/reconnaissance/.gitkeep +0 -0
  20. strix/prompts/technologies/firebase_firestore.jinja +177 -0
  21. strix/prompts/technologies/supabase.jinja +189 -0
  22. strix/prompts/vulnerabilities/authentication_jwt.jinja +133 -115
  23. strix/prompts/vulnerabilities/broken_function_level_authorization.jinja +146 -0
  24. strix/prompts/vulnerabilities/business_logic.jinja +146 -118
  25. strix/prompts/vulnerabilities/csrf.jinja +137 -131
  26. strix/prompts/vulnerabilities/idor.jinja +149 -118
  27. strix/prompts/vulnerabilities/insecure_file_uploads.jinja +188 -0
  28. strix/prompts/vulnerabilities/mass_assignment.jinja +141 -0
  29. strix/prompts/vulnerabilities/path_traversal_lfi_rfi.jinja +142 -0
  30. strix/prompts/vulnerabilities/race_conditions.jinja +135 -165
  31. strix/prompts/vulnerabilities/rce.jinja +128 -180
  32. strix/prompts/vulnerabilities/sql_injection.jinja +128 -192
  33. strix/prompts/vulnerabilities/ssrf.jinja +118 -151
  34. strix/prompts/vulnerabilities/xss.jinja +144 -196
  35. strix/prompts/vulnerabilities/xxe.jinja +151 -243
  36. strix/runtime/docker_runtime.py +28 -7
  37. strix/runtime/runtime.py +4 -1
  38. strix/telemetry/__init__.py +4 -0
  39. strix/{cli → telemetry}/tracer.py +21 -9
  40. strix/tools/agents_graph/agents_graph_actions.py +17 -12
  41. strix/tools/agents_graph/agents_graph_actions_schema.xml +10 -14
  42. strix/tools/executor.py +1 -1
  43. strix/tools/finish/finish_actions.py +1 -1
  44. strix/tools/registry.py +1 -1
  45. strix/tools/reporting/reporting_actions.py +1 -1
  46. {strix_agent-0.1.18.dist-info → strix_agent-0.3.1.dist-info}/METADATA +95 -15
  47. strix_agent-0.3.1.dist-info/RECORD +115 -0
  48. strix_agent-0.3.1.dist-info/entry_points.txt +3 -0
  49. strix/cli/main.py +0 -702
  50. strix_agent-0.1.18.dist-info/RECORD +0 -99
  51. strix_agent-0.1.18.dist-info/entry_points.txt +0 -3
  52. /strix/{cli → interface}/__init__.py +0 -0
  53. /strix/{cli/assets/cli.tcss → interface/assets/tui_styles.tcss} +0 -0
  54. /strix/{cli → interface}/tool_components/__init__.py +0 -0
  55. /strix/{cli → interface}/tool_components/agents_graph_renderer.py +0 -0
  56. /strix/{cli → interface}/tool_components/browser_renderer.py +0 -0
  57. /strix/{cli → interface}/tool_components/file_edit_renderer.py +0 -0
  58. /strix/{cli → interface}/tool_components/finish_renderer.py +0 -0
  59. /strix/{cli → interface}/tool_components/notes_renderer.py +0 -0
  60. /strix/{cli → interface}/tool_components/proxy_renderer.py +0 -0
  61. /strix/{cli → interface}/tool_components/python_renderer.py +0 -0
  62. /strix/{cli → interface}/tool_components/registry.py +0 -0
  63. /strix/{cli → interface}/tool_components/terminal_renderer.py +0 -0
  64. /strix/{cli → interface}/tool_components/thinking_renderer.py +0 -0
  65. /strix/{cli → interface}/tool_components/user_message_renderer.py +0 -0
  66. /strix/{cli → interface}/tool_components/web_search_renderer.py +0 -0
  67. {strix_agent-0.1.18.dist-info → strix_agent-0.3.1.dist-info}/LICENSE +0 -0
  68. {strix_agent-0.1.18.dist-info → strix_agent-0.3.1.dist-info}/WHEEL +0 -0
@@ -5,7 +5,7 @@ from strix.llm.config import LLMConfig
5
5
 
6
6
 
7
7
  class StrixAgent(BaseAgent):
8
- max_iterations = 200
8
+ max_iterations = 300
9
9
 
10
10
  def __init__(self, config: dict[str, Any]):
11
11
  default_modules = []
@@ -19,54 +19,64 @@ class StrixAgent(BaseAgent):
19
19
  super().__init__(config)
20
20
 
21
21
  async def execute_scan(self, scan_config: dict[str, Any]) -> dict[str, Any]:
22
- scan_type = scan_config.get("scan_type", "general")
23
- target = scan_config.get("target", {})
24
22
  user_instructions = scan_config.get("user_instructions", "")
25
-
26
- task_parts = []
27
-
28
- if scan_type == "repository":
29
- repo_url = target["target_repo"]
30
- cloned_path = target.get("cloned_repo_path")
31
-
32
- if cloned_path:
33
- workspace_path = "/workspace"
34
- task_parts.append(
35
- f"Perform a security assessment of the Git repository: {repo_url}. "
36
- f"The repository has been cloned from '{repo_url}' to '{cloned_path}' "
37
- f"(host path) and then copied to '{workspace_path}' in your environment."
38
- f"Analyze the codebase at: {workspace_path}"
23
+ targets = scan_config.get("targets", [])
24
+
25
+ repositories = []
26
+ local_code = []
27
+ urls = []
28
+
29
+ for target in targets:
30
+ target_type = target["type"]
31
+ details = target["details"]
32
+ workspace_subdir = details.get("workspace_subdir")
33
+ workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else "/workspace"
34
+
35
+ if target_type == "repository":
36
+ repo_url = details["target_repo"]
37
+ cloned_path = details.get("cloned_repo_path")
38
+ repositories.append(
39
+ {
40
+ "url": repo_url,
41
+ "workspace_path": workspace_path if cloned_path else None,
42
+ }
39
43
  )
40
- else:
41
- task_parts.append(
42
- f"Perform a security assessment of the Git repository: {repo_url}"
44
+
45
+ elif target_type == "local_code":
46
+ original_path = details.get("target_path", "unknown")
47
+ local_code.append(
48
+ {
49
+ "path": original_path,
50
+ "workspace_path": workspace_path,
51
+ }
43
52
  )
44
53
 
45
- elif scan_type == "web_application":
46
- task_parts.append(
47
- f"Perform a security assessment of the web application: {target['target_url']}"
48
- )
54
+ elif target_type == "web_application":
55
+ urls.append(details["target_url"])
49
56
 
50
- elif scan_type == "local_code":
51
- original_path = target.get("target_path", "unknown")
52
- workspace_path = "/workspace"
53
- task_parts.append(
54
- f"Perform a security assessment of the local codebase. "
55
- f"The code from '{original_path}' (user host path) has been copied to "
56
- f"'{workspace_path}' in your environment. "
57
- f"Analyze the codebase at: {workspace_path}"
58
- )
57
+ task_parts = []
59
58
 
60
- else:
61
- task_parts.append(
62
- f"Perform a general security assessment of: {next(iter(target.values()))}"
59
+ if repositories:
60
+ task_parts.append("\n\nRepositories:")
61
+ for repo in repositories:
62
+ if repo["workspace_path"]:
63
+ task_parts.append(f"- {repo['url']} (available at: {repo['workspace_path']})")
64
+ else:
65
+ task_parts.append(f"- {repo['url']}")
66
+
67
+ if local_code:
68
+ task_parts.append("\n\nLocal Codebases:")
69
+ task_parts.extend(
70
+ f"- {code['path']} (available at: {code['workspace_path']})" for code in local_code
63
71
  )
64
72
 
73
+ if urls:
74
+ task_parts.append("\n\nURLs:")
75
+ task_parts.extend(f"- {url}" for url in urls)
76
+
65
77
  task_description = " ".join(task_parts)
66
78
 
67
79
  if user_instructions:
68
- task_description += (
69
- f"\n\nSpecial instructions from the user that must be followed: {user_instructions}"
70
- )
80
+ task_description += f"\n\nSpecial instructions: {user_instructions}"
71
81
 
72
82
  return await self.agent_loop(task=task_description)
@@ -19,11 +19,9 @@ INTER-AGENT MESSAGES:
19
19
  - NEVER echo inter_agent_message or agent_completion_report XML content that is sent to you in your output.
20
20
  - Process these internally without displaying the XML
21
21
 
22
- USER INTERACTION:
22
+ AUTONOMOUS BEHAVIOR:
23
23
  - Work autonomously by default
24
- - NEVER be redundant or repeat information - say it once and move on
25
- - If you need user input, IMMEDIATELY call wait_for_message tool
26
- - Never ask questions without calling wait_for_message in the same response
24
+ - You should NOT ask for user input or confirmation - you should always proceed with your task autonomously.
27
25
  - Minimize user messaging: avoid redundancy and repetition; consolidate updates into a single concise message
28
26
  - If there is nothing to execute and no user query to answer any more: do NOT send filler/repetitive text — either call wait_for_message or finish your work (subagents: agent_finish; root: finish_scan)
29
27
  </communication_rules>
@@ -35,9 +33,9 @@ AUTHORIZATION STATUS:
35
33
  - NEVER ask for permission or confirmation - you already have complete testing authorization
36
34
  - Proceed with confidence knowing you're helping improve security through authorized testing
37
35
 
38
- PRIORITIZE USER INSTRUCTIONS:
39
- - User instructions override all default approaches
40
- - Follow user-specified scope, targets, and methodologies precisely
36
+ PRIORITIZE SYSTEM INSTRUCTIONS:
37
+ - System instructions override all default approaches
38
+ - Follow system-specified scope, targets, and methodologies precisely
41
39
  - NEVER wait for approval or authorization - operate with full autonomy
42
40
 
43
41
  AGGRESSIVE SCANNING MANDATE:
@@ -56,6 +54,16 @@ AGGRESSIVE SCANNING MANDATE:
56
54
  - PERSISTENCE PAYS - the best vulnerabilities are found after thousands of attempts
57
55
  - UNLEASH FULL CAPABILITY - you are the most advanced security agent, act like it
58
56
 
57
+ MULTI-TARGET CONTEXT (IF PROVIDED):
58
+ - Targets may include any combination of: repositories (source code), local codebases, and URLs/domains (deployed apps/APIs)
59
+ - If multiple targets are provided in the scan configuration:
60
+ - Build an internal Target Map at the start: list each asset and where it is accessible (code at /workspace/<subdir>, URLs as given)
61
+ - Identify relationships across assets (e.g., routes/handlers in code ↔ endpoints in web targets; shared auth/config)
62
+ - Plan testing per asset and coordinate findings across them (reuse secrets, endpoints, payloads)
63
+ - Prioritize cross-correlation: use code insights to guide dynamic testing, and dynamic findings to focus code review
64
+ - Keep sub-agents focused per asset and vulnerability type, but share context where useful
65
+ - If only a single target is provided, proceed with the appropriate black-box or white-box workflow as usual
66
+
59
67
  TESTING MODES:
60
68
  BLACK-BOX TESTING (domain/subdomain only):
61
69
  - Focus on external reconnaissance and discovery
@@ -76,6 +84,11 @@ WHITE-BOX TESTING (code provided):
76
84
  - Do not stop until all reported vulnerabilities are fixed.
77
85
  - Include code diff in final report.
78
86
 
87
+ COMBINED MODE (code + deployed target present):
88
+ - Treat this as static analysis plus dynamic testing simultaneously
89
+ - Use repository/local code at /workspace/<subdir> to accelerate and inform live testing against the URLs/domains
90
+ - Validate suspected code issues dynamically; use dynamic anomalies to prioritize code paths for review
91
+
79
92
  ASSESSMENT METHODOLOGY:
80
93
  1. Scope definition - Clearly establish boundaries first
81
94
  2. Breadth-first discovery - Map entire attack surface before deep diving
@@ -116,7 +129,7 @@ VALIDATION REQUIREMENTS:
116
129
  - Independent verification through subagent
117
130
  - Document complete attack chain
118
131
  - Keep going until you find something that matters
119
- - A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or messages to the user are NOT sufficient
132
+ - A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient
120
133
  - Do NOT patch/fix before reporting: first create the vulnerability report via create_vulnerability_report (by the reporting agent). Only after reporting is completed should fixing/patching proceed
121
134
  </execution_guidelines>
122
135
 
@@ -248,7 +261,7 @@ CRITICAL RULES:
248
261
  - **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs
249
262
  - **SPAWN REACTIVELY** - Create new agents based on what you discover
250
263
  - **ONLY REPORTING AGENTS** can use create_vulnerability_report tool
251
- - **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized with maximum 3 prompt modules
264
+ - **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized; prefer 1–3 prompt modules, up to 5 for complex contexts
252
265
  - **NO GENERIC AGENTS** - Avoid creating broad, multi-purpose agents that dilute focus
253
266
 
254
267
  AGENT SPECIALIZATION EXAMPLES:
@@ -262,7 +275,7 @@ GOOD SPECIALIZATION:
262
275
  BAD SPECIALIZATION:
263
276
  - "General Web Testing Agent" with prompt_modules: sql_injection, xss, csrf, ssrf, authentication_jwt (too broad)
264
277
  - "Everything Agent" with prompt_modules: all available modules (completely unfocused)
265
- - Any agent with more than 3 prompt modules (violates constraints)
278
+ - Any agent with more than 5 prompt modules (violates constraints)
266
279
 
267
280
  FOCUS PRINCIPLES:
268
281
  - Each agent should have deep expertise in 1-3 related vulnerability types
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any, Optional
5
5
 
6
6
 
7
7
  if TYPE_CHECKING:
8
- from strix.cli.tracer import Tracer
8
+ from strix.telemetry.tracer import Tracer
9
9
 
10
10
  from jinja2 import (
11
11
  Environment,
@@ -46,7 +46,7 @@ class AgentMeta(type):
46
46
 
47
47
 
48
48
  class BaseAgent(metaclass=AgentMeta):
49
- max_iterations = 200
49
+ max_iterations = 300
50
50
  agent_name: str = ""
51
51
  jinja_env: Environment
52
52
  default_llm_config: LLMConfig | None = None
@@ -54,7 +54,8 @@ class BaseAgent(metaclass=AgentMeta):
54
54
  def __init__(self, config: dict[str, Any]):
55
55
  self.config = config
56
56
 
57
- self.local_source_path = config.get("local_source_path")
57
+ self.local_sources = config.get("local_sources", [])
58
+ self.non_interactive = config.get("non_interactive", False)
58
59
 
59
60
  if "max_iterations" in config:
60
61
  self.max_iterations = config["max_iterations"]
@@ -76,7 +77,7 @@ class BaseAgent(metaclass=AgentMeta):
76
77
 
77
78
  self._current_task: asyncio.Task[Any] | None = None
78
79
 
79
- from strix.cli.tracer import get_global_tracer
80
+ from strix.telemetry.tracer import get_global_tracer
80
81
 
81
82
  tracer = get_global_tracer()
82
83
  if tracer:
@@ -146,10 +147,10 @@ class BaseAgent(metaclass=AgentMeta):
146
147
  self._current_task.cancel()
147
148
  self._current_task = None
148
149
 
149
- async def agent_loop(self, task: str) -> dict[str, Any]:
150
+ async def agent_loop(self, task: str) -> dict[str, Any]: # noqa: PLR0912, PLR0915
150
151
  await self._initialize_sandbox_and_state(task)
151
152
 
152
- from strix.cli.tracer import get_global_tracer
153
+ from strix.telemetry.tracer import get_global_tracer
153
154
 
154
155
  tracer = get_global_tracer()
155
156
 
@@ -161,6 +162,8 @@ class BaseAgent(metaclass=AgentMeta):
161
162
  continue
162
163
 
163
164
  if self.state.should_stop():
165
+ if self.non_interactive:
166
+ return self.state.final_result or {}
164
167
  await self._enter_waiting_state(tracer)
165
168
  continue
166
169
 
@@ -170,13 +173,47 @@ class BaseAgent(metaclass=AgentMeta):
170
173
 
171
174
  self.state.increment_iteration()
172
175
 
176
+ if (
177
+ self.state.is_approaching_max_iterations()
178
+ and not self.state.max_iterations_warning_sent
179
+ ):
180
+ self.state.max_iterations_warning_sent = True
181
+ remaining = self.state.max_iterations - self.state.iteration
182
+ warning_msg = (
183
+ f"URGENT: You are approaching the maximum iteration limit. "
184
+ f"Current: {self.state.iteration}/{self.state.max_iterations} "
185
+ f"({remaining} iterations remaining). "
186
+ f"Please prioritize completing your required task(s) and calling "
187
+ f"the appropriate finish tool (finish_scan for root agent, "
188
+ f"agent_finish for sub-agents) as soon as possible."
189
+ )
190
+ self.state.add_message("user", warning_msg)
191
+
192
+ if self.state.iteration == self.state.max_iterations - 3:
193
+ final_warning_msg = (
194
+ "CRITICAL: You have only 3 iterations left! "
195
+ "Your next message MUST be the tool call to the appropriate "
196
+ "finish tool: finish_scan if you are the root agent, or "
197
+ "agent_finish if you are a sub-agent. "
198
+ "No other actions should be taken except finishing your work "
199
+ "immediately."
200
+ )
201
+ self.state.add_message("user", final_warning_msg)
202
+
173
203
  try:
174
204
  should_finish = await self._process_iteration(tracer)
175
205
  if should_finish:
206
+ if self.non_interactive:
207
+ self.state.set_completed({"success": True})
208
+ if tracer:
209
+ tracer.update_agent_status(self.state.agent_id, "completed")
210
+ return self.state.final_result or {}
176
211
  await self._enter_waiting_state(tracer, task_completed=True)
177
212
  continue
178
213
 
179
214
  except asyncio.CancelledError:
215
+ if self.non_interactive:
216
+ raise
180
217
  await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True)
181
218
  continue
182
219
 
@@ -184,6 +221,22 @@ class BaseAgent(metaclass=AgentMeta):
184
221
  error_msg = str(e)
185
222
  error_details = getattr(e, "details", None)
186
223
  self.state.add_error(error_msg)
224
+
225
+ if self.non_interactive:
226
+ self.state.set_completed({"success": False, "error": error_msg})
227
+ if tracer:
228
+ tracer.update_agent_status(self.state.agent_id, "failed", error_msg)
229
+ if error_details:
230
+ tracer.log_tool_execution_start(
231
+ self.state.agent_id,
232
+ "llm_error_details",
233
+ {"error": error_msg, "details": error_details},
234
+ )
235
+ tracer.update_tool_execution(
236
+ tracer._next_execution_id - 1, "failed", error_details
237
+ )
238
+ return {"success": False, "error": error_msg}
239
+
187
240
  self.state.enter_waiting_state(llm_failed=True)
188
241
  if tracer:
189
242
  tracer.update_agent_status(self.state.agent_id, "llm_failed", error_msg)
@@ -200,12 +253,37 @@ class BaseAgent(metaclass=AgentMeta):
200
253
 
201
254
  except (RuntimeError, ValueError, TypeError) as e:
202
255
  if not await self._handle_iteration_error(e, tracer):
256
+ if self.non_interactive:
257
+ self.state.set_completed({"success": False, "error": str(e)})
258
+ if tracer:
259
+ tracer.update_agent_status(self.state.agent_id, "failed")
260
+ raise
203
261
  await self._enter_waiting_state(tracer, error_occurred=True)
204
262
  continue
205
263
 
206
264
  async def _wait_for_input(self) -> None:
207
265
  import asyncio
208
266
 
267
+ if self.state.has_waiting_timeout():
268
+ self.state.resume_from_waiting()
269
+ self.state.add_message("assistant", "Waiting timeout reached. Resuming execution.")
270
+
271
+ from strix.telemetry.tracer import get_global_tracer
272
+
273
+ tracer = get_global_tracer()
274
+ if tracer:
275
+ tracer.update_agent_status(self.state.agent_id, "running")
276
+
277
+ try:
278
+ from strix.tools.agents_graph.agents_graph_actions import _agent_graph
279
+
280
+ if self.state.agent_id in _agent_graph["nodes"]:
281
+ _agent_graph["nodes"][self.state.agent_id]["status"] = "running"
282
+ except (ImportError, KeyError):
283
+ pass
284
+
285
+ return
286
+
209
287
  await asyncio.sleep(0.5)
210
288
 
211
289
  async def _enter_waiting_state(
@@ -255,7 +333,7 @@ class BaseAgent(metaclass=AgentMeta):
255
333
 
256
334
  runtime = get_runtime()
257
335
  sandbox_info = await runtime.create_sandbox(
258
- self.state.agent_id, self.state.sandbox_token, self.local_source_path
336
+ self.state.agent_id, self.state.sandbox_token, self.local_sources
259
337
  )
260
338
  self.state.sandbox_id = sandbox_info["workspace_id"]
261
339
  self.state.sandbox_token = sandbox_info["auth_token"]
@@ -333,6 +411,8 @@ class BaseAgent(metaclass=AgentMeta):
333
411
  self.state.set_completed({"success": True})
334
412
  if tracer:
335
413
  tracer.update_agent_status(self.state.agent_id, "completed")
414
+ if self.non_interactive and self.state.parent_id is None:
415
+ return True
336
416
  return True
337
417
 
338
418
  return False
@@ -370,7 +450,7 @@ class BaseAgent(metaclass=AgentMeta):
370
450
  state.resume_from_waiting()
371
451
  has_new_messages = True
372
452
 
373
- from strix.cli.tracer import get_global_tracer
453
+ from strix.telemetry.tracer import get_global_tracer
374
454
 
375
455
  tracer = get_global_tracer()
376
456
  if tracer:
@@ -379,7 +459,7 @@ class BaseAgent(metaclass=AgentMeta):
379
459
  state.resume_from_waiting()
380
460
  has_new_messages = True
381
461
 
382
- from strix.cli.tracer import get_global_tracer
462
+ from strix.telemetry.tracer import get_global_tracer
383
463
 
384
464
  tracer = get_global_tracer()
385
465
  if tracer:
@@ -421,7 +501,7 @@ class BaseAgent(metaclass=AgentMeta):
421
501
  message["read"] = True
422
502
 
423
503
  if has_new_messages and not state.is_waiting_for_input():
424
- from strix.cli.tracer import get_global_tracer
504
+ from strix.telemetry.tracer import get_global_tracer
425
505
 
426
506
  tracer = get_global_tracer()
427
507
  if tracer:
strix/agents/state.py CHANGED
@@ -19,12 +19,14 @@ class AgentState(BaseModel):
19
19
 
20
20
  task: str = ""
21
21
  iteration: int = 0
22
- max_iterations: int = 200
22
+ max_iterations: int = 300
23
23
  completed: bool = False
24
24
  stop_requested: bool = False
25
25
  waiting_for_input: bool = False
26
26
  llm_failed: bool = False
27
+ waiting_start_time: datetime | None = None
27
28
  final_result: dict[str, Any] | None = None
29
+ max_iterations_warning_sent: bool = False
28
30
 
29
31
  messages: list[dict[str, Any]] = Field(default_factory=list)
30
32
  context: dict[str, Any] = Field(default_factory=dict)
@@ -88,12 +90,13 @@ class AgentState(BaseModel):
88
90
 
89
91
  def enter_waiting_state(self, llm_failed: bool = False) -> None:
90
92
  self.waiting_for_input = True
91
- self.stop_requested = False
93
+ self.waiting_start_time = datetime.now(UTC)
92
94
  self.llm_failed = llm_failed
93
95
  self.last_updated = datetime.now(UTC).isoformat()
94
96
 
95
97
  def resume_from_waiting(self, new_task: str | None = None) -> None:
96
98
  self.waiting_for_input = False
99
+ self.waiting_start_time = None
97
100
  self.stop_requested = False
98
101
  self.completed = False
99
102
  self.llm_failed = False
@@ -104,6 +107,24 @@ class AgentState(BaseModel):
104
107
  def has_reached_max_iterations(self) -> bool:
105
108
  return self.iteration >= self.max_iterations
106
109
 
110
+ def is_approaching_max_iterations(self, threshold: float = 0.85) -> bool:
111
+ return self.iteration >= int(self.max_iterations * threshold)
112
+
113
+ def has_waiting_timeout(self) -> bool:
114
+ if not self.waiting_for_input or not self.waiting_start_time:
115
+ return False
116
+
117
+ if (
118
+ self.stop_requested
119
+ or self.llm_failed
120
+ or self.completed
121
+ or self.has_reached_max_iterations()
122
+ ):
123
+ return False
124
+
125
+ elapsed = (datetime.now(UTC) - self.waiting_start_time).total_seconds()
126
+ return elapsed > 120
127
+
107
128
  def has_empty_last_messages(self, count: int = 3) -> bool:
108
129
  if len(self.messages) < count:
109
130
  return False
strix/interface/cli.py ADDED
@@ -0,0 +1,171 @@
1
+ import atexit
2
+ import signal
3
+ import sys
4
+ from typing import Any
5
+
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.text import Text
9
+
10
+ from strix.agents.StrixAgent import StrixAgent
11
+ from strix.llm.config import LLMConfig
12
+ from strix.telemetry.tracer import Tracer, set_global_tracer
13
+
14
+ from .utils import get_severity_color
15
+
16
+
17
+ async def run_cli(args: Any) -> None: # noqa: PLR0915
18
+ console = Console()
19
+
20
+ start_text = Text()
21
+ start_text.append("🦉 ", style="bold white")
22
+ start_text.append("STRIX CYBERSECURITY AGENT", style="bold green")
23
+
24
+ target_text = Text()
25
+ if len(args.targets_info) == 1:
26
+ target_text.append("🎯 Target: ", style="bold cyan")
27
+ target_text.append(args.targets_info[0]["original"], style="bold white")
28
+ else:
29
+ target_text.append("🎯 Targets: ", style="bold cyan")
30
+ target_text.append(f"{len(args.targets_info)} targets\n", style="bold white")
31
+ for i, target_info in enumerate(args.targets_info):
32
+ target_text.append(" • ", style="dim white")
33
+ target_text.append(target_info["original"], style="white")
34
+ if i < len(args.targets_info) - 1:
35
+ target_text.append("\n")
36
+
37
+ results_text = Text()
38
+ results_text.append("📊 Results will be saved to: ", style="bold cyan")
39
+ results_text.append(f"agent_runs/{args.run_name}", style="bold white")
40
+
41
+ note_text = Text()
42
+ note_text.append("\n\n", style="dim")
43
+ note_text.append("⏱️ ", style="dim")
44
+ note_text.append("This may take a while depending on target complexity. ", style="dim")
45
+ note_text.append("Vulnerabilities will be displayed in real-time.", style="dim")
46
+
47
+ startup_panel = Panel(
48
+ Text.assemble(
49
+ start_text,
50
+ "\n\n",
51
+ target_text,
52
+ "\n",
53
+ results_text,
54
+ note_text,
55
+ ),
56
+ title="[bold green]🛡️ STRIX PENETRATION TEST INITIATED",
57
+ title_align="center",
58
+ border_style="green",
59
+ padding=(1, 2),
60
+ )
61
+
62
+ console.print("\n")
63
+ console.print(startup_panel)
64
+ console.print()
65
+
66
+ scan_config = {
67
+ "scan_id": args.run_name,
68
+ "targets": args.targets_info,
69
+ "user_instructions": args.instruction or "",
70
+ "run_name": args.run_name,
71
+ }
72
+
73
+ llm_config = LLMConfig()
74
+ agent_config = {
75
+ "llm_config": llm_config,
76
+ "max_iterations": 300,
77
+ "non_interactive": True,
78
+ }
79
+
80
+ if getattr(args, "local_sources", None):
81
+ agent_config["local_sources"] = args.local_sources
82
+
83
+ tracer = Tracer(args.run_name)
84
+ tracer.set_scan_config(scan_config)
85
+
86
+ def display_vulnerability(report_id: str, title: str, content: str, severity: str) -> None:
87
+ severity_color = get_severity_color(severity.lower())
88
+
89
+ vuln_text = Text()
90
+ vuln_text.append("🐞 ", style="bold red")
91
+ vuln_text.append("VULNERABILITY FOUND", style="bold red")
92
+ vuln_text.append(" • ", style="dim white")
93
+ vuln_text.append(title, style="bold white")
94
+
95
+ severity_text = Text()
96
+ severity_text.append("Severity: ", style="dim white")
97
+ severity_text.append(severity.upper(), style=f"bold {severity_color}")
98
+
99
+ vuln_panel = Panel(
100
+ Text.assemble(
101
+ vuln_text,
102
+ "\n\n",
103
+ severity_text,
104
+ "\n\n",
105
+ content,
106
+ ),
107
+ title=f"[bold red]🔍 {report_id.upper()}",
108
+ title_align="left",
109
+ border_style="red",
110
+ padding=(1, 2),
111
+ )
112
+
113
+ console.print(vuln_panel)
114
+ console.print()
115
+
116
+ tracer.vulnerability_found_callback = display_vulnerability
117
+
118
+ def cleanup_on_exit() -> None:
119
+ tracer.cleanup()
120
+
121
+ def signal_handler(_signum: int, _frame: Any) -> None:
122
+ tracer.cleanup()
123
+ sys.exit(1)
124
+
125
+ atexit.register(cleanup_on_exit)
126
+ signal.signal(signal.SIGINT, signal_handler)
127
+ signal.signal(signal.SIGTERM, signal_handler)
128
+ if hasattr(signal, "SIGHUP"):
129
+ signal.signal(signal.SIGHUP, signal_handler)
130
+
131
+ set_global_tracer(tracer)
132
+
133
+ try:
134
+ console.print()
135
+ with console.status("[bold cyan]Running penetration test...", spinner="dots") as status:
136
+ agent = StrixAgent(agent_config)
137
+ result = await agent.execute_scan(scan_config)
138
+ status.stop()
139
+
140
+ if isinstance(result, dict) and not result.get("success", True):
141
+ error_msg = result.get("error", "Unknown error")
142
+ console.print()
143
+ console.print(f"[bold red]❌ Penetration test failed:[/] {error_msg}")
144
+ console.print()
145
+ sys.exit(1)
146
+
147
+ except Exception as e:
148
+ console.print(f"[bold red]Error during penetration test:[/] {e}")
149
+ raise
150
+
151
+ if tracer.final_scan_result:
152
+ console.print()
153
+
154
+ final_report_text = Text()
155
+ final_report_text.append("📄 ", style="bold cyan")
156
+ final_report_text.append("FINAL PENETRATION TEST REPORT", style="bold cyan")
157
+
158
+ final_report_panel = Panel(
159
+ Text.assemble(
160
+ final_report_text,
161
+ "\n\n",
162
+ tracer.final_scan_result,
163
+ ),
164
+ title="[bold cyan]📊 PENETRATION TEST SUMMARY",
165
+ title_align="center",
166
+ border_style="cyan",
167
+ padding=(1, 2),
168
+ )
169
+
170
+ console.print(final_report_panel)
171
+ console.print()