strix-agent 0.4.0__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 (118) hide show
  1. strix/__init__.py +0 -0
  2. strix/agents/StrixAgent/__init__.py +4 -0
  3. strix/agents/StrixAgent/strix_agent.py +89 -0
  4. strix/agents/StrixAgent/system_prompt.jinja +404 -0
  5. strix/agents/__init__.py +10 -0
  6. strix/agents/base_agent.py +518 -0
  7. strix/agents/state.py +163 -0
  8. strix/interface/__init__.py +4 -0
  9. strix/interface/assets/tui_styles.tcss +694 -0
  10. strix/interface/cli.py +230 -0
  11. strix/interface/main.py +500 -0
  12. strix/interface/tool_components/__init__.py +39 -0
  13. strix/interface/tool_components/agents_graph_renderer.py +123 -0
  14. strix/interface/tool_components/base_renderer.py +62 -0
  15. strix/interface/tool_components/browser_renderer.py +120 -0
  16. strix/interface/tool_components/file_edit_renderer.py +99 -0
  17. strix/interface/tool_components/finish_renderer.py +31 -0
  18. strix/interface/tool_components/notes_renderer.py +108 -0
  19. strix/interface/tool_components/proxy_renderer.py +255 -0
  20. strix/interface/tool_components/python_renderer.py +34 -0
  21. strix/interface/tool_components/registry.py +72 -0
  22. strix/interface/tool_components/reporting_renderer.py +53 -0
  23. strix/interface/tool_components/scan_info_renderer.py +64 -0
  24. strix/interface/tool_components/terminal_renderer.py +131 -0
  25. strix/interface/tool_components/thinking_renderer.py +29 -0
  26. strix/interface/tool_components/user_message_renderer.py +43 -0
  27. strix/interface/tool_components/web_search_renderer.py +28 -0
  28. strix/interface/tui.py +1274 -0
  29. strix/interface/utils.py +559 -0
  30. strix/llm/__init__.py +15 -0
  31. strix/llm/config.py +20 -0
  32. strix/llm/llm.py +465 -0
  33. strix/llm/memory_compressor.py +212 -0
  34. strix/llm/request_queue.py +87 -0
  35. strix/llm/utils.py +87 -0
  36. strix/prompts/README.md +64 -0
  37. strix/prompts/__init__.py +109 -0
  38. strix/prompts/cloud/.gitkeep +0 -0
  39. strix/prompts/coordination/root_agent.jinja +41 -0
  40. strix/prompts/custom/.gitkeep +0 -0
  41. strix/prompts/frameworks/fastapi.jinja +142 -0
  42. strix/prompts/frameworks/nextjs.jinja +126 -0
  43. strix/prompts/protocols/graphql.jinja +215 -0
  44. strix/prompts/reconnaissance/.gitkeep +0 -0
  45. strix/prompts/technologies/firebase_firestore.jinja +177 -0
  46. strix/prompts/technologies/supabase.jinja +189 -0
  47. strix/prompts/vulnerabilities/authentication_jwt.jinja +147 -0
  48. strix/prompts/vulnerabilities/broken_function_level_authorization.jinja +146 -0
  49. strix/prompts/vulnerabilities/business_logic.jinja +171 -0
  50. strix/prompts/vulnerabilities/csrf.jinja +174 -0
  51. strix/prompts/vulnerabilities/idor.jinja +195 -0
  52. strix/prompts/vulnerabilities/information_disclosure.jinja +222 -0
  53. strix/prompts/vulnerabilities/insecure_file_uploads.jinja +188 -0
  54. strix/prompts/vulnerabilities/mass_assignment.jinja +141 -0
  55. strix/prompts/vulnerabilities/open_redirect.jinja +177 -0
  56. strix/prompts/vulnerabilities/path_traversal_lfi_rfi.jinja +142 -0
  57. strix/prompts/vulnerabilities/race_conditions.jinja +164 -0
  58. strix/prompts/vulnerabilities/rce.jinja +154 -0
  59. strix/prompts/vulnerabilities/sql_injection.jinja +151 -0
  60. strix/prompts/vulnerabilities/ssrf.jinja +135 -0
  61. strix/prompts/vulnerabilities/subdomain_takeover.jinja +155 -0
  62. strix/prompts/vulnerabilities/xss.jinja +169 -0
  63. strix/prompts/vulnerabilities/xxe.jinja +184 -0
  64. strix/runtime/__init__.py +19 -0
  65. strix/runtime/docker_runtime.py +399 -0
  66. strix/runtime/runtime.py +29 -0
  67. strix/runtime/tool_server.py +205 -0
  68. strix/telemetry/__init__.py +4 -0
  69. strix/telemetry/tracer.py +337 -0
  70. strix/tools/__init__.py +64 -0
  71. strix/tools/agents_graph/__init__.py +16 -0
  72. strix/tools/agents_graph/agents_graph_actions.py +621 -0
  73. strix/tools/agents_graph/agents_graph_actions_schema.xml +226 -0
  74. strix/tools/argument_parser.py +121 -0
  75. strix/tools/browser/__init__.py +4 -0
  76. strix/tools/browser/browser_actions.py +236 -0
  77. strix/tools/browser/browser_actions_schema.xml +183 -0
  78. strix/tools/browser/browser_instance.py +533 -0
  79. strix/tools/browser/tab_manager.py +342 -0
  80. strix/tools/executor.py +305 -0
  81. strix/tools/file_edit/__init__.py +4 -0
  82. strix/tools/file_edit/file_edit_actions.py +141 -0
  83. strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
  84. strix/tools/finish/__init__.py +4 -0
  85. strix/tools/finish/finish_actions.py +174 -0
  86. strix/tools/finish/finish_actions_schema.xml +45 -0
  87. strix/tools/notes/__init__.py +14 -0
  88. strix/tools/notes/notes_actions.py +191 -0
  89. strix/tools/notes/notes_actions_schema.xml +150 -0
  90. strix/tools/proxy/__init__.py +20 -0
  91. strix/tools/proxy/proxy_actions.py +101 -0
  92. strix/tools/proxy/proxy_actions_schema.xml +267 -0
  93. strix/tools/proxy/proxy_manager.py +785 -0
  94. strix/tools/python/__init__.py +4 -0
  95. strix/tools/python/python_actions.py +47 -0
  96. strix/tools/python/python_actions_schema.xml +131 -0
  97. strix/tools/python/python_instance.py +172 -0
  98. strix/tools/python/python_manager.py +131 -0
  99. strix/tools/registry.py +196 -0
  100. strix/tools/reporting/__init__.py +6 -0
  101. strix/tools/reporting/reporting_actions.py +63 -0
  102. strix/tools/reporting/reporting_actions_schema.xml +30 -0
  103. strix/tools/terminal/__init__.py +4 -0
  104. strix/tools/terminal/terminal_actions.py +35 -0
  105. strix/tools/terminal/terminal_actions_schema.xml +146 -0
  106. strix/tools/terminal/terminal_manager.py +151 -0
  107. strix/tools/terminal/terminal_session.py +447 -0
  108. strix/tools/thinking/__init__.py +4 -0
  109. strix/tools/thinking/thinking_actions.py +18 -0
  110. strix/tools/thinking/thinking_actions_schema.xml +52 -0
  111. strix/tools/web_search/__init__.py +4 -0
  112. strix/tools/web_search/web_search_actions.py +80 -0
  113. strix/tools/web_search/web_search_actions_schema.xml +83 -0
  114. strix_agent-0.4.0.dist-info/LICENSE +201 -0
  115. strix_agent-0.4.0.dist-info/METADATA +282 -0
  116. strix_agent-0.4.0.dist-info/RECORD +118 -0
  117. strix_agent-0.4.0.dist-info/WHEEL +4 -0
  118. strix_agent-0.4.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,337 @@
1
+ import logging
2
+ from datetime import UTC, datetime
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Any, Optional
5
+ from uuid import uuid4
6
+
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _global_tracer: Optional["Tracer"] = None
15
+
16
+
17
+ def get_global_tracer() -> Optional["Tracer"]:
18
+ return _global_tracer
19
+
20
+
21
+ def set_global_tracer(tracer: "Tracer") -> None:
22
+ global _global_tracer # noqa: PLW0603
23
+ _global_tracer = tracer
24
+
25
+
26
+ class Tracer:
27
+ def __init__(self, run_name: str | None = None):
28
+ self.run_name = run_name
29
+ self.run_id = run_name or f"run-{uuid4().hex[:8]}"
30
+ self.start_time = datetime.now(UTC).isoformat()
31
+ self.end_time: str | None = None
32
+
33
+ self.agents: dict[str, dict[str, Any]] = {}
34
+ self.tool_executions: dict[int, dict[str, Any]] = {}
35
+ self.chat_messages: list[dict[str, Any]] = []
36
+
37
+ self.vulnerability_reports: list[dict[str, Any]] = []
38
+ self.final_scan_result: str | None = None
39
+
40
+ self.scan_results: dict[str, Any] | None = None
41
+ self.scan_config: dict[str, Any] | None = None
42
+ self.run_metadata: dict[str, Any] = {
43
+ "run_id": self.run_id,
44
+ "run_name": self.run_name,
45
+ "start_time": self.start_time,
46
+ "end_time": None,
47
+ "targets": [],
48
+ "status": "running",
49
+ }
50
+ self._run_dir: Path | None = None
51
+ self._next_execution_id = 1
52
+ self._next_message_id = 1
53
+ self._saved_vuln_ids: set[str] = set()
54
+
55
+ self.vulnerability_found_callback: Callable[[str, str, str, str], None] | None = None
56
+
57
+ def set_run_name(self, run_name: str) -> None:
58
+ self.run_name = run_name
59
+ self.run_id = run_name
60
+
61
+ def get_run_dir(self) -> Path:
62
+ if self._run_dir is None:
63
+ runs_dir = Path.cwd() / "strix_runs"
64
+ runs_dir.mkdir(exist_ok=True)
65
+
66
+ run_dir_name = self.run_name if self.run_name else self.run_id
67
+ self._run_dir = runs_dir / run_dir_name
68
+ self._run_dir.mkdir(exist_ok=True)
69
+
70
+ return self._run_dir
71
+
72
+ def add_vulnerability_report(
73
+ self,
74
+ title: str,
75
+ content: str,
76
+ severity: str,
77
+ ) -> str:
78
+ report_id = f"vuln-{len(self.vulnerability_reports) + 1:04d}"
79
+
80
+ report = {
81
+ "id": report_id,
82
+ "title": title.strip(),
83
+ "content": content.strip(),
84
+ "severity": severity.lower().strip(),
85
+ "timestamp": datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC"),
86
+ }
87
+
88
+ self.vulnerability_reports.append(report)
89
+ logger.info(f"Added vulnerability report: {report_id} - {title}")
90
+
91
+ if self.vulnerability_found_callback:
92
+ self.vulnerability_found_callback(
93
+ report_id, title.strip(), content.strip(), severity.lower().strip()
94
+ )
95
+
96
+ self.save_run_data()
97
+ return report_id
98
+
99
+ def set_final_scan_result(
100
+ self,
101
+ content: str,
102
+ success: bool = True,
103
+ ) -> None:
104
+ self.final_scan_result = content.strip()
105
+
106
+ self.scan_results = {
107
+ "scan_completed": True,
108
+ "content": content,
109
+ "success": success,
110
+ }
111
+
112
+ logger.info(f"Set final scan result: success={success}")
113
+ self.save_run_data(mark_complete=True)
114
+
115
+ def log_agent_creation(
116
+ self, agent_id: str, name: str, task: str, parent_id: str | None = None
117
+ ) -> None:
118
+ agent_data: dict[str, Any] = {
119
+ "id": agent_id,
120
+ "name": name,
121
+ "task": task,
122
+ "status": "running",
123
+ "parent_id": parent_id,
124
+ "created_at": datetime.now(UTC).isoformat(),
125
+ "updated_at": datetime.now(UTC).isoformat(),
126
+ "tool_executions": [],
127
+ }
128
+
129
+ self.agents[agent_id] = agent_data
130
+
131
+ def log_chat_message(
132
+ self,
133
+ content: str,
134
+ role: str,
135
+ agent_id: str | None = None,
136
+ metadata: dict[str, Any] | None = None,
137
+ ) -> int:
138
+ message_id = self._next_message_id
139
+ self._next_message_id += 1
140
+
141
+ message_data = {
142
+ "message_id": message_id,
143
+ "content": content,
144
+ "role": role,
145
+ "agent_id": agent_id,
146
+ "timestamp": datetime.now(UTC).isoformat(),
147
+ "metadata": metadata or {},
148
+ }
149
+
150
+ self.chat_messages.append(message_data)
151
+ return message_id
152
+
153
+ def log_tool_execution_start(self, agent_id: str, tool_name: str, args: dict[str, Any]) -> int:
154
+ execution_id = self._next_execution_id
155
+ self._next_execution_id += 1
156
+
157
+ now = datetime.now(UTC).isoformat()
158
+ execution_data = {
159
+ "execution_id": execution_id,
160
+ "agent_id": agent_id,
161
+ "tool_name": tool_name,
162
+ "args": args,
163
+ "status": "running",
164
+ "result": None,
165
+ "timestamp": now,
166
+ "started_at": now,
167
+ "completed_at": None,
168
+ }
169
+
170
+ self.tool_executions[execution_id] = execution_data
171
+
172
+ if agent_id in self.agents:
173
+ self.agents[agent_id]["tool_executions"].append(execution_id)
174
+
175
+ return execution_id
176
+
177
+ def update_tool_execution(
178
+ self, execution_id: int, status: str, result: Any | None = None
179
+ ) -> None:
180
+ if execution_id in self.tool_executions:
181
+ self.tool_executions[execution_id]["status"] = status
182
+ self.tool_executions[execution_id]["result"] = result
183
+ self.tool_executions[execution_id]["completed_at"] = datetime.now(UTC).isoformat()
184
+
185
+ def update_agent_status(
186
+ self, agent_id: str, status: str, error_message: str | None = None
187
+ ) -> None:
188
+ if agent_id in self.agents:
189
+ self.agents[agent_id]["status"] = status
190
+ self.agents[agent_id]["updated_at"] = datetime.now(UTC).isoformat()
191
+ if error_message:
192
+ self.agents[agent_id]["error_message"] = error_message
193
+
194
+ def set_scan_config(self, config: dict[str, Any]) -> None:
195
+ self.scan_config = config
196
+ self.run_metadata.update(
197
+ {
198
+ "targets": config.get("targets", []),
199
+ "user_instructions": config.get("user_instructions", ""),
200
+ "max_iterations": config.get("max_iterations", 200),
201
+ }
202
+ )
203
+ self.get_run_dir()
204
+
205
+ def save_run_data(self, mark_complete: bool = False) -> None:
206
+ try:
207
+ run_dir = self.get_run_dir()
208
+ if mark_complete:
209
+ self.end_time = datetime.now(UTC).isoformat()
210
+
211
+ if self.final_scan_result:
212
+ penetration_test_report_file = run_dir / "penetration_test_report.md"
213
+ with penetration_test_report_file.open("w", encoding="utf-8") as f:
214
+ f.write("# Security Penetration Test Report\n\n")
215
+ f.write(
216
+ f"**Generated:** {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}\n\n"
217
+ )
218
+ f.write(f"{self.final_scan_result}\n")
219
+ logger.info(
220
+ f"Saved final penetration test report to: {penetration_test_report_file}"
221
+ )
222
+
223
+ if self.vulnerability_reports:
224
+ vuln_dir = run_dir / "vulnerabilities"
225
+ vuln_dir.mkdir(exist_ok=True)
226
+
227
+ new_reports = [
228
+ report
229
+ for report in self.vulnerability_reports
230
+ if report["id"] not in self._saved_vuln_ids
231
+ ]
232
+
233
+ for report in new_reports:
234
+ vuln_file = vuln_dir / f"{report['id']}.md"
235
+ with vuln_file.open("w", encoding="utf-8") as f:
236
+ f.write(f"# {report['title']}\n\n")
237
+ f.write(f"**ID:** {report['id']}\n")
238
+ f.write(f"**Severity:** {report['severity'].upper()}\n")
239
+ f.write(f"**Found:** {report['timestamp']}\n\n")
240
+ f.write("## Description\n\n")
241
+ f.write(f"{report['content']}\n")
242
+ self._saved_vuln_ids.add(report["id"])
243
+
244
+ if self.vulnerability_reports:
245
+ severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
246
+ sorted_reports = sorted(
247
+ self.vulnerability_reports,
248
+ key=lambda x: (severity_order.get(x["severity"], 5), x["timestamp"]),
249
+ )
250
+
251
+ vuln_csv_file = run_dir / "vulnerabilities.csv"
252
+ with vuln_csv_file.open("w", encoding="utf-8", newline="") as f:
253
+ import csv
254
+
255
+ fieldnames = ["id", "title", "severity", "timestamp", "file"]
256
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
257
+ writer.writeheader()
258
+
259
+ for report in sorted_reports:
260
+ writer.writerow(
261
+ {
262
+ "id": report["id"],
263
+ "title": report["title"],
264
+ "severity": report["severity"].upper(),
265
+ "timestamp": report["timestamp"],
266
+ "file": f"vulnerabilities/{report['id']}.md",
267
+ }
268
+ )
269
+
270
+ if new_reports:
271
+ logger.info(
272
+ f"Saved {len(new_reports)} new vulnerability report(s) to: {vuln_dir}"
273
+ )
274
+ logger.info(f"Updated vulnerability index: {vuln_csv_file}")
275
+
276
+ logger.info(f"📊 Essential scan data saved to: {run_dir}")
277
+
278
+ except (OSError, RuntimeError):
279
+ logger.exception("Failed to save scan data")
280
+
281
+ def _calculate_duration(self) -> float:
282
+ try:
283
+ start = datetime.fromisoformat(self.start_time.replace("Z", "+00:00"))
284
+ if self.end_time:
285
+ end = datetime.fromisoformat(self.end_time.replace("Z", "+00:00"))
286
+ return (end - start).total_seconds()
287
+ except (ValueError, TypeError):
288
+ pass
289
+ return 0.0
290
+
291
+ def get_agent_tools(self, agent_id: str) -> list[dict[str, Any]]:
292
+ return [
293
+ exec_data
294
+ for exec_data in self.tool_executions.values()
295
+ if exec_data.get("agent_id") == agent_id
296
+ ]
297
+
298
+ def get_real_tool_count(self) -> int:
299
+ return sum(
300
+ 1
301
+ for exec_data in self.tool_executions.values()
302
+ if exec_data.get("tool_name") not in ["scan_start_info", "subagent_start_info"]
303
+ )
304
+
305
+ def get_total_llm_stats(self) -> dict[str, Any]:
306
+ from strix.tools.agents_graph.agents_graph_actions import _agent_instances
307
+
308
+ total_stats = {
309
+ "input_tokens": 0,
310
+ "output_tokens": 0,
311
+ "cached_tokens": 0,
312
+ "cache_creation_tokens": 0,
313
+ "cost": 0.0,
314
+ "requests": 0,
315
+ "failed_requests": 0,
316
+ }
317
+
318
+ for agent_instance in _agent_instances.values():
319
+ if hasattr(agent_instance, "llm") and hasattr(agent_instance.llm, "_total_stats"):
320
+ agent_stats = agent_instance.llm._total_stats
321
+ total_stats["input_tokens"] += agent_stats.input_tokens
322
+ total_stats["output_tokens"] += agent_stats.output_tokens
323
+ total_stats["cached_tokens"] += agent_stats.cached_tokens
324
+ total_stats["cache_creation_tokens"] += agent_stats.cache_creation_tokens
325
+ total_stats["cost"] += agent_stats.cost
326
+ total_stats["requests"] += agent_stats.requests
327
+ total_stats["failed_requests"] += agent_stats.failed_requests
328
+
329
+ total_stats["cost"] = round(total_stats["cost"], 4)
330
+
331
+ return {
332
+ "total": total_stats,
333
+ "total_tokens": total_stats["input_tokens"] + total_stats["output_tokens"],
334
+ }
335
+
336
+ def cleanup(self) -> None:
337
+ self.save_run_data(mark_complete=True)
@@ -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
+ ]