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.
Files changed (117) hide show
  1. strix/agents/StrixAgent/strix_agent.py +3 -3
  2. strix/agents/StrixAgent/system_prompt.jinja +30 -26
  3. strix/agents/base_agent.py +159 -75
  4. strix/agents/state.py +5 -2
  5. strix/config/__init__.py +12 -0
  6. strix/config/config.py +172 -0
  7. strix/interface/assets/tui_styles.tcss +195 -230
  8. strix/interface/cli.py +16 -41
  9. strix/interface/main.py +151 -74
  10. strix/interface/streaming_parser.py +119 -0
  11. strix/interface/tool_components/__init__.py +4 -0
  12. strix/interface/tool_components/agent_message_renderer.py +190 -0
  13. strix/interface/tool_components/agents_graph_renderer.py +54 -38
  14. strix/interface/tool_components/base_renderer.py +68 -36
  15. strix/interface/tool_components/browser_renderer.py +106 -91
  16. strix/interface/tool_components/file_edit_renderer.py +117 -36
  17. strix/interface/tool_components/finish_renderer.py +43 -10
  18. strix/interface/tool_components/notes_renderer.py +63 -38
  19. strix/interface/tool_components/proxy_renderer.py +133 -92
  20. strix/interface/tool_components/python_renderer.py +121 -8
  21. strix/interface/tool_components/registry.py +19 -12
  22. strix/interface/tool_components/reporting_renderer.py +196 -28
  23. strix/interface/tool_components/scan_info_renderer.py +22 -19
  24. strix/interface/tool_components/terminal_renderer.py +270 -90
  25. strix/interface/tool_components/thinking_renderer.py +8 -6
  26. strix/interface/tool_components/todo_renderer.py +225 -0
  27. strix/interface/tool_components/user_message_renderer.py +26 -19
  28. strix/interface/tool_components/web_search_renderer.py +7 -6
  29. strix/interface/tui.py +907 -262
  30. strix/interface/utils.py +236 -4
  31. strix/llm/__init__.py +6 -2
  32. strix/llm/config.py +8 -5
  33. strix/llm/dedupe.py +217 -0
  34. strix/llm/llm.py +209 -356
  35. strix/llm/memory_compressor.py +6 -5
  36. strix/llm/utils.py +17 -8
  37. strix/runtime/__init__.py +12 -3
  38. strix/runtime/docker_runtime.py +121 -202
  39. strix/runtime/tool_server.py +55 -95
  40. strix/skills/README.md +64 -0
  41. strix/skills/__init__.py +110 -0
  42. strix/{prompts → skills}/frameworks/nextjs.jinja +26 -0
  43. strix/skills/scan_modes/deep.jinja +145 -0
  44. strix/skills/scan_modes/quick.jinja +63 -0
  45. strix/skills/scan_modes/standard.jinja +91 -0
  46. strix/telemetry/README.md +38 -0
  47. strix/telemetry/__init__.py +7 -1
  48. strix/telemetry/posthog.py +137 -0
  49. strix/telemetry/tracer.py +194 -54
  50. strix/tools/__init__.py +11 -4
  51. strix/tools/agents_graph/agents_graph_actions.py +20 -21
  52. strix/tools/agents_graph/agents_graph_actions_schema.xml +8 -8
  53. strix/tools/browser/browser_actions.py +10 -6
  54. strix/tools/browser/browser_actions_schema.xml +6 -1
  55. strix/tools/browser/browser_instance.py +96 -48
  56. strix/tools/browser/tab_manager.py +121 -102
  57. strix/tools/context.py +12 -0
  58. strix/tools/executor.py +63 -4
  59. strix/tools/file_edit/file_edit_actions.py +6 -3
  60. strix/tools/file_edit/file_edit_actions_schema.xml +45 -3
  61. strix/tools/finish/finish_actions.py +80 -105
  62. strix/tools/finish/finish_actions_schema.xml +121 -14
  63. strix/tools/notes/notes_actions.py +6 -33
  64. strix/tools/notes/notes_actions_schema.xml +50 -46
  65. strix/tools/proxy/proxy_actions.py +14 -2
  66. strix/tools/proxy/proxy_actions_schema.xml +0 -1
  67. strix/tools/proxy/proxy_manager.py +28 -16
  68. strix/tools/python/python_actions.py +2 -2
  69. strix/tools/python/python_actions_schema.xml +9 -1
  70. strix/tools/python/python_instance.py +39 -37
  71. strix/tools/python/python_manager.py +43 -31
  72. strix/tools/registry.py +73 -12
  73. strix/tools/reporting/reporting_actions.py +218 -31
  74. strix/tools/reporting/reporting_actions_schema.xml +256 -8
  75. strix/tools/terminal/terminal_actions.py +2 -2
  76. strix/tools/terminal/terminal_actions_schema.xml +6 -0
  77. strix/tools/terminal/terminal_manager.py +41 -30
  78. strix/tools/thinking/thinking_actions_schema.xml +27 -25
  79. strix/tools/todo/__init__.py +18 -0
  80. strix/tools/todo/todo_actions.py +568 -0
  81. strix/tools/todo/todo_actions_schema.xml +225 -0
  82. strix/utils/__init__.py +0 -0
  83. strix/utils/resource_paths.py +13 -0
  84. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/METADATA +90 -65
  85. strix_agent-0.6.2.dist-info/RECORD +134 -0
  86. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/WHEEL +1 -1
  87. strix/llm/request_queue.py +0 -87
  88. strix/prompts/README.md +0 -64
  89. strix/prompts/__init__.py +0 -109
  90. strix_agent-0.4.0.dist-info/RECORD +0 -118
  91. /strix/{prompts → skills}/cloud/.gitkeep +0 -0
  92. /strix/{prompts → skills}/coordination/root_agent.jinja +0 -0
  93. /strix/{prompts → skills}/custom/.gitkeep +0 -0
  94. /strix/{prompts → skills}/frameworks/fastapi.jinja +0 -0
  95. /strix/{prompts → skills}/protocols/graphql.jinja +0 -0
  96. /strix/{prompts → skills}/reconnaissance/.gitkeep +0 -0
  97. /strix/{prompts → skills}/technologies/firebase_firestore.jinja +0 -0
  98. /strix/{prompts → skills}/technologies/supabase.jinja +0 -0
  99. /strix/{prompts → skills}/vulnerabilities/authentication_jwt.jinja +0 -0
  100. /strix/{prompts → skills}/vulnerabilities/broken_function_level_authorization.jinja +0 -0
  101. /strix/{prompts → skills}/vulnerabilities/business_logic.jinja +0 -0
  102. /strix/{prompts → skills}/vulnerabilities/csrf.jinja +0 -0
  103. /strix/{prompts → skills}/vulnerabilities/idor.jinja +0 -0
  104. /strix/{prompts → skills}/vulnerabilities/information_disclosure.jinja +0 -0
  105. /strix/{prompts → skills}/vulnerabilities/insecure_file_uploads.jinja +0 -0
  106. /strix/{prompts → skills}/vulnerabilities/mass_assignment.jinja +0 -0
  107. /strix/{prompts → skills}/vulnerabilities/open_redirect.jinja +0 -0
  108. /strix/{prompts → skills}/vulnerabilities/path_traversal_lfi_rfi.jinja +0 -0
  109. /strix/{prompts → skills}/vulnerabilities/race_conditions.jinja +0 -0
  110. /strix/{prompts → skills}/vulnerabilities/rce.jinja +0 -0
  111. /strix/{prompts → skills}/vulnerabilities/sql_injection.jinja +0 -0
  112. /strix/{prompts → skills}/vulnerabilities/ssrf.jinja +0 -0
  113. /strix/{prompts → skills}/vulnerabilities/subdomain_takeover.jinja +0 -0
  114. /strix/{prompts → skills}/vulnerabilities/xss.jinja +0 -0
  115. /strix/{prompts → skills}/vulnerabilities/xxe.jinja +0 -0
  116. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info}/entry_points.txt +0 -0
  117. {strix_agent-0.4.0.dist-info → strix_agent-0.6.2.dist-info/licenses}/LICENSE +0 -0
@@ -190,36 +190,35 @@ def create_agent(
190
190
  task: str,
191
191
  name: str,
192
192
  inherit_context: bool = True,
193
- prompt_modules: str | None = None,
193
+ skills: str | None = None,
194
194
  ) -> dict[str, Any]:
195
195
  try:
196
196
  parent_id = agent_state.agent_id
197
197
 
198
- module_list = []
199
- if prompt_modules:
200
- module_list = [m.strip() for m in prompt_modules.split(",") if m.strip()]
198
+ skill_list = []
199
+ if skills:
200
+ skill_list = [s.strip() for s in skills.split(",") if s.strip()]
201
201
 
202
- if len(module_list) > 5:
202
+ if len(skill_list) > 5:
203
203
  return {
204
204
  "success": False,
205
205
  "error": (
206
- "Cannot specify more than 5 prompt modules for an agent "
207
- "(use comma-separated format)"
206
+ "Cannot specify more than 5 skills for an agent (use comma-separated format)"
208
207
  ),
209
208
  "agent_id": None,
210
209
  }
211
210
 
212
- if module_list:
213
- from strix.prompts import get_all_module_names, validate_module_names
211
+ if skill_list:
212
+ from strix.skills import get_all_skill_names, validate_skill_names
214
213
 
215
- validation = validate_module_names(module_list)
214
+ validation = validate_skill_names(skill_list)
216
215
  if validation["invalid"]:
217
- available_modules = list(get_all_module_names())
216
+ available_skills = list(get_all_skill_names())
218
217
  return {
219
218
  "success": False,
220
219
  "error": (
221
- f"Invalid prompt modules: {validation['invalid']}. "
222
- f"Available modules: {', '.join(available_modules)}"
220
+ f"Invalid skills: {validation['invalid']}. "
221
+ f"Available skills: {', '.join(available_skills)}"
223
222
  ),
224
223
  "agent_id": None,
225
224
  }
@@ -233,14 +232,14 @@ def create_agent(
233
232
  parent_agent = _agent_instances.get(parent_id)
234
233
 
235
234
  timeout = None
236
- if (
237
- parent_agent
238
- and hasattr(parent_agent, "llm_config")
239
- and hasattr(parent_agent.llm_config, "timeout")
240
- ):
241
- timeout = parent_agent.llm_config.timeout
242
-
243
- llm_config = LLMConfig(prompt_modules=module_list, timeout=timeout)
235
+ scan_mode = "deep"
236
+ if parent_agent and hasattr(parent_agent, "llm_config"):
237
+ if hasattr(parent_agent.llm_config, "timeout"):
238
+ timeout = parent_agent.llm_config.timeout
239
+ if hasattr(parent_agent.llm_config, "scan_mode"):
240
+ scan_mode = parent_agent.llm_config.scan_mode
241
+
242
+ llm_config = LLMConfig(skills=skill_list, timeout=timeout, scan_mode=scan_mode)
244
243
 
245
244
  agent_config = {
246
245
  "llm_config": llm_config,
@@ -79,8 +79,8 @@ Only create a new agent if no existing agent is handling the specific task.</des
79
79
  <parameter name="inherit_context" type="boolean" required="false">
80
80
  <description>Whether the new agent should inherit parent's conversation history and context</description>
81
81
  </parameter>
82
- <parameter name="prompt_modules" type="string" required="false">
83
- <description>Comma-separated list of prompt modules to use for the agent (MAXIMUM 5 modules allowed). Most agents should have at least one module in order to be useful. Agents should be highly specialized - use 1-3 related modules; up to 5 for complex contexts. {{DYNAMIC_MODULES_DESCRIPTION}}</description>
82
+ <parameter name="skills" type="string" required="false">
83
+ <description>Comma-separated list of skills to use for the agent (MAXIMUM 5 skills allowed). Most agents should have at least one skill in order to be useful. Agents should be highly specialized - use 1-3 related skills; up to 5 for complex contexts. {{DYNAMIC_SKILLS_DESCRIPTION}}</description>
84
84
  </parameter>
85
85
  </parameters>
86
86
  <returns type="Dict[str, Any]">
@@ -92,30 +92,30 @@ Only create a new agent if no existing agent is handling the specific task.</des
92
92
  <parameter=task>Validate and exploit the suspected SQL injection vulnerability found in
93
93
  the login form. Confirm exploitability and document proof of concept.</parameter>
94
94
  <parameter=name>SQLi Validator</parameter>
95
- <parameter=prompt_modules>sql_injection</parameter>
95
+ <parameter=skills>sql_injection</parameter>
96
96
  </function>
97
97
 
98
98
  <function=create_agent>
99
99
  <parameter=task>Test authentication mechanisms, JWT implementation, and session management
100
100
  for security vulnerabilities and bypass techniques.</parameter>
101
101
  <parameter=name>Auth Specialist</parameter>
102
- <parameter=prompt_modules>authentication_jwt, business_logic</parameter>
102
+ <parameter=skills>authentication_jwt, business_logic</parameter>
103
103
  </function>
104
104
 
105
- # Example of single-module specialization (most focused)
105
+ # Example of single-skill specialization (most focused)
106
106
  <function=create_agent>
107
107
  <parameter=task>Perform comprehensive XSS testing including reflected, stored, and DOM-based
108
108
  variants across all identified input points.</parameter>
109
109
  <parameter=name>XSS Specialist</parameter>
110
- <parameter=prompt_modules>xss</parameter>
110
+ <parameter=skills>xss</parameter>
111
111
  </function>
112
112
 
113
- # Example of up to 5 related modules (borderline acceptable)
113
+ # Example of up to 5 related skills (borderline acceptable)
114
114
  <function=create_agent>
115
115
  <parameter=task>Test for server-side vulnerabilities including SSRF, XXE, and potential
116
116
  RCE vectors in file upload and XML processing endpoints.</parameter>
117
117
  <parameter=name>Server-Side Attack Specialist</parameter>
118
- <parameter=prompt_modules>ssrf, xxe, rce</parameter>
118
+ <parameter=skills>ssrf, xxe, rce</parameter>
119
119
  </function>
120
120
  </examples>
121
121
  </tool>
@@ -1,8 +1,10 @@
1
- from typing import Any, Literal, NoReturn
1
+ from typing import TYPE_CHECKING, Any, Literal, NoReturn
2
2
 
3
3
  from strix.tools.registry import register_tool
4
4
 
5
- from .tab_manager import BrowserTabManager, get_browser_tab_manager
5
+
6
+ if TYPE_CHECKING:
7
+ from .tab_manager import BrowserTabManager
6
8
 
7
9
 
8
10
  BrowserAction = Literal[
@@ -71,7 +73,7 @@ def _validate_file_path(action_name: str, file_path: str | None) -> None:
71
73
 
72
74
 
73
75
  def _handle_navigation_actions(
74
- manager: BrowserTabManager,
76
+ manager: "BrowserTabManager",
75
77
  action: str,
76
78
  url: str | None = None,
77
79
  tab_id: str | None = None,
@@ -90,7 +92,7 @@ def _handle_navigation_actions(
90
92
 
91
93
 
92
94
  def _handle_interaction_actions(
93
- manager: BrowserTabManager,
95
+ manager: "BrowserTabManager",
94
96
  action: str,
95
97
  coordinate: str | None = None,
96
98
  text: str | None = None,
@@ -128,7 +130,7 @@ def _raise_unknown_action(action: str) -> NoReturn:
128
130
 
129
131
 
130
132
  def _handle_tab_actions(
131
- manager: BrowserTabManager,
133
+ manager: "BrowserTabManager",
132
134
  action: str,
133
135
  url: str | None = None,
134
136
  tab_id: str | None = None,
@@ -149,7 +151,7 @@ def _handle_tab_actions(
149
151
 
150
152
 
151
153
  def _handle_utility_actions(
152
- manager: BrowserTabManager,
154
+ manager: "BrowserTabManager",
153
155
  action: str,
154
156
  duration: float | None = None,
155
157
  js_code: str | None = None,
@@ -191,6 +193,8 @@ def browser_action(
191
193
  file_path: str | None = None,
192
194
  clear: bool = False,
193
195
  ) -> dict[str, Any]:
196
+ from .tab_manager import get_browser_tab_manager
197
+
194
198
  manager = get_browser_tab_manager()
195
199
 
196
200
  try:
@@ -1,4 +1,3 @@
1
- <?xml version="1.0" ?>
2
1
  <tools>
3
2
  <tool name="browser_action">
4
3
  <description>Perform browser actions using a Playwright-controlled browser with multiple tabs.
@@ -92,6 +91,12 @@
92
91
  code normally. It can be single line or multi-line.
93
92
  13. For form filling, click on the field first, then use 'type' to enter text.
94
93
  14. The browser runs in headless mode using Chrome engine for security and performance.
94
+ 15. RESOURCE MANAGEMENT:
95
+ - ALWAYS close tabs you no longer need using 'close_tab' action.
96
+ - ALWAYS close the browser with 'close' action when you have completely finished
97
+ all browser-related tasks. Do not leave the browser running if you're done with it.
98
+ - If you opened multiple tabs, close them as soon as you've extracted the needed
99
+ information from each one.
95
100
  </notes>
96
101
  <examples>
97
102
  # Launch browser at URL (creates tab_1)
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
2
  import base64
3
+ import contextlib
3
4
  import logging
4
5
  import threading
5
6
  from pathlib import Path
@@ -17,13 +18,82 @@ MAX_CONSOLE_LOGS_COUNT = 200
17
18
  MAX_JS_RESULT_LENGTH = 5_000
18
19
 
19
20
 
21
+ class _BrowserState:
22
+ """Singleton state for the shared browser instance."""
23
+
24
+ lock = threading.Lock()
25
+ event_loop: asyncio.AbstractEventLoop | None = None
26
+ event_loop_thread: threading.Thread | None = None
27
+ playwright: Playwright | None = None
28
+ browser: Browser | None = None
29
+
30
+
31
+ _state = _BrowserState()
32
+
33
+
34
+ def _ensure_event_loop() -> None:
35
+ if _state.event_loop is not None:
36
+ return
37
+
38
+ def run_loop() -> None:
39
+ _state.event_loop = asyncio.new_event_loop()
40
+ asyncio.set_event_loop(_state.event_loop)
41
+ _state.event_loop.run_forever()
42
+
43
+ _state.event_loop_thread = threading.Thread(target=run_loop, daemon=True)
44
+ _state.event_loop_thread.start()
45
+
46
+ while _state.event_loop is None:
47
+ threading.Event().wait(0.01)
48
+
49
+
50
+ async def _create_browser() -> Browser:
51
+ if _state.browser is not None and _state.browser.is_connected():
52
+ return _state.browser
53
+
54
+ if _state.browser is not None:
55
+ with contextlib.suppress(Exception):
56
+ await _state.browser.close()
57
+ _state.browser = None
58
+ if _state.playwright is not None:
59
+ with contextlib.suppress(Exception):
60
+ await _state.playwright.stop()
61
+ _state.playwright = None
62
+
63
+ _state.playwright = await async_playwright().start()
64
+ _state.browser = await _state.playwright.chromium.launch(
65
+ headless=True,
66
+ args=[
67
+ "--no-sandbox",
68
+ "--disable-dev-shm-usage",
69
+ "--disable-gpu",
70
+ "--disable-web-security",
71
+ ],
72
+ )
73
+ return _state.browser
74
+
75
+
76
+ def _get_browser() -> tuple[asyncio.AbstractEventLoop, Browser]:
77
+ with _state.lock:
78
+ _ensure_event_loop()
79
+ assert _state.event_loop is not None
80
+
81
+ if _state.browser is None or not _state.browser.is_connected():
82
+ future = asyncio.run_coroutine_threadsafe(_create_browser(), _state.event_loop)
83
+ future.result(timeout=30)
84
+
85
+ assert _state.browser is not None
86
+ return _state.event_loop, _state.browser
87
+
88
+
20
89
  class BrowserInstance:
21
90
  def __init__(self) -> None:
22
91
  self.is_running = True
23
92
  self._execution_lock = threading.Lock()
24
93
 
25
- self.playwright: Playwright | None = None
26
- self.browser: Browser | None = None
94
+ self._loop: asyncio.AbstractEventLoop | None = None
95
+ self._browser: Browser | None = None
96
+
27
97
  self.context: BrowserContext | None = None
28
98
  self.pages: dict[str, Page] = {}
29
99
  self.current_page_id: str | None = None
@@ -31,23 +101,6 @@ class BrowserInstance:
31
101
 
32
102
  self.console_logs: dict[str, list[dict[str, Any]]] = {}
33
103
 
34
- self._loop: asyncio.AbstractEventLoop | None = None
35
- self._loop_thread: threading.Thread | None = None
36
-
37
- self._start_event_loop()
38
-
39
- def _start_event_loop(self) -> None:
40
- def run_loop() -> None:
41
- self._loop = asyncio.new_event_loop()
42
- asyncio.set_event_loop(self._loop)
43
- self._loop.run_forever()
44
-
45
- self._loop_thread = threading.Thread(target=run_loop, daemon=True)
46
- self._loop_thread.start()
47
-
48
- while self._loop is None:
49
- threading.Event().wait(0.01)
50
-
51
104
  def _run_async(self, coro: Any) -> dict[str, Any]:
52
105
  if not self._loop or not self.is_running:
53
106
  raise RuntimeError("Browser instance is not running")
@@ -77,21 +130,10 @@ class BrowserInstance:
77
130
 
78
131
  page.on("console", handle_console)
79
132
 
80
- async def _launch_browser(self, url: str | None = None) -> dict[str, Any]:
81
- self.playwright = await async_playwright().start()
82
-
83
- self.browser = await self.playwright.chromium.launch(
84
- headless=True,
85
- args=[
86
- "--no-sandbox",
87
- "--disable-dev-shm-usage",
88
- "--disable-gpu",
89
- "--disable-web-security",
90
- "--disable-features=VizDisplayCompositor",
91
- ],
92
- )
133
+ async def _create_context(self, url: str | None = None) -> dict[str, Any]:
134
+ assert self._browser is not None
93
135
 
94
- self.context = await self.browser.new_context(
136
+ self.context = await self._browser.new_context(
95
137
  viewport={"width": 1280, "height": 720},
96
138
  user_agent=(
97
139
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
@@ -148,10 +190,11 @@ class BrowserInstance:
148
190
 
149
191
  def launch(self, url: str | None = None) -> dict[str, Any]:
150
192
  with self._execution_lock:
151
- if self.browser is not None:
193
+ if self.context is not None:
152
194
  raise ValueError("Browser is already launched")
153
195
 
154
- return self._run_async(self._launch_browser(url))
196
+ self._loop, self._browser = _get_browser()
197
+ return self._run_async(self._create_context(url))
155
198
 
156
199
  def goto(self, url: str, tab_id: str | None = None) -> dict[str, Any]:
157
200
  with self._execution_lock:
@@ -512,22 +555,27 @@ class BrowserInstance:
512
555
  def close(self) -> None:
513
556
  with self._execution_lock:
514
557
  self.is_running = False
515
- if self._loop:
516
- asyncio.run_coroutine_threadsafe(self._close_browser(), self._loop)
517
-
518
- self._loop.call_soon_threadsafe(self._loop.stop)
558
+ if self._loop and self.context:
559
+ future = asyncio.run_coroutine_threadsafe(self._close_context(), self._loop)
560
+ with contextlib.suppress(Exception):
561
+ future.result(timeout=5)
519
562
 
520
- if self._loop_thread:
521
- self._loop_thread.join(timeout=5)
563
+ self.pages.clear()
564
+ self.console_logs.clear()
565
+ self.current_page_id = None
566
+ self.context = None
522
567
 
523
- async def _close_browser(self) -> None:
568
+ async def _close_context(self) -> None:
524
569
  try:
525
- if self.browser:
526
- await self.browser.close()
527
- if self.playwright:
528
- await self.playwright.stop()
570
+ if self.context:
571
+ await self.context.close()
529
572
  except (OSError, RuntimeError) as e:
530
- logger.warning(f"Error closing browser: {e}")
573
+ logger.warning(f"Error closing context: {e}")
531
574
 
532
575
  def is_alive(self) -> bool:
533
- return self.is_running and self.browser is not None and self.browser.is_connected()
576
+ return (
577
+ self.is_running
578
+ and self.context is not None
579
+ and self._browser is not None
580
+ and self._browser.is_connected()
581
+ )