strix-agent 0.1.19__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 (41) hide show
  1. strix/agents/StrixAgent/strix_agent.py +49 -40
  2. strix/agents/StrixAgent/system_prompt.jinja +15 -0
  3. strix/agents/base_agent.py +71 -11
  4. strix/agents/state.py +5 -1
  5. strix/interface/cli.py +171 -0
  6. strix/interface/main.py +482 -0
  7. strix/{cli → interface}/tool_components/scan_info_renderer.py +17 -12
  8. strix/{cli/app.py → interface/tui.py} +15 -16
  9. strix/interface/utils.py +435 -0
  10. strix/runtime/docker_runtime.py +28 -7
  11. strix/runtime/runtime.py +4 -1
  12. strix/telemetry/__init__.py +4 -0
  13. strix/{cli → telemetry}/tracer.py +21 -9
  14. strix/tools/agents_graph/agents_graph_actions.py +13 -9
  15. strix/tools/executor.py +1 -1
  16. strix/tools/finish/finish_actions.py +1 -1
  17. strix/tools/reporting/reporting_actions.py +1 -1
  18. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/METADATA +45 -4
  19. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/RECORD +39 -36
  20. strix_agent-0.3.1.dist-info/entry_points.txt +3 -0
  21. strix/cli/main.py +0 -703
  22. strix_agent-0.1.19.dist-info/entry_points.txt +0 -3
  23. /strix/{cli → interface}/__init__.py +0 -0
  24. /strix/{cli/assets/cli.tcss → interface/assets/tui_styles.tcss} +0 -0
  25. /strix/{cli → interface}/tool_components/__init__.py +0 -0
  26. /strix/{cli → interface}/tool_components/agents_graph_renderer.py +0 -0
  27. /strix/{cli → interface}/tool_components/base_renderer.py +0 -0
  28. /strix/{cli → interface}/tool_components/browser_renderer.py +0 -0
  29. /strix/{cli → interface}/tool_components/file_edit_renderer.py +0 -0
  30. /strix/{cli → interface}/tool_components/finish_renderer.py +0 -0
  31. /strix/{cli → interface}/tool_components/notes_renderer.py +0 -0
  32. /strix/{cli → interface}/tool_components/proxy_renderer.py +0 -0
  33. /strix/{cli → interface}/tool_components/python_renderer.py +0 -0
  34. /strix/{cli → interface}/tool_components/registry.py +0 -0
  35. /strix/{cli → interface}/tool_components/reporting_renderer.py +0 -0
  36. /strix/{cli → interface}/tool_components/terminal_renderer.py +0 -0
  37. /strix/{cli → interface}/tool_components/thinking_renderer.py +0 -0
  38. /strix/{cli → interface}/tool_components/user_message_renderer.py +0 -0
  39. /strix/{cli → interface}/tool_components/web_search_renderer.py +0 -0
  40. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/LICENSE +0 -0
  41. {strix_agent-0.1.19.dist-info → strix_agent-0.3.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,435 @@
1
+ import re
2
+ import secrets
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ import tempfile
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from urllib.parse import urlparse
10
+
11
+ import docker
12
+ from docker.errors import DockerException, ImageNotFound
13
+ from rich.console import Console
14
+ from rich.panel import Panel
15
+ from rich.text import Text
16
+
17
+
18
+ # Token formatting utilities
19
+ def format_token_count(count: float) -> str:
20
+ count = int(count)
21
+ if count >= 1_000_000:
22
+ return f"{count / 1_000_000:.1f}M"
23
+ if count >= 1_000:
24
+ return f"{count / 1_000:.1f}K"
25
+ return str(count)
26
+
27
+
28
+ # Display utilities
29
+ def get_severity_color(severity: str) -> str:
30
+ severity_colors = {
31
+ "critical": "#dc2626",
32
+ "high": "#ea580c",
33
+ "medium": "#d97706",
34
+ "low": "#65a30d",
35
+ "info": "#0284c7",
36
+ }
37
+ return severity_colors.get(severity, "#6b7280")
38
+
39
+
40
+ def build_stats_text(tracer: Any) -> Text:
41
+ stats_text = Text()
42
+ if not tracer:
43
+ return stats_text
44
+
45
+ vuln_count = len(tracer.vulnerability_reports)
46
+ tool_count = tracer.get_real_tool_count()
47
+ agent_count = len(tracer.agents)
48
+
49
+ if vuln_count > 0:
50
+ severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
51
+ for report in tracer.vulnerability_reports:
52
+ severity = report.get("severity", "").lower()
53
+ if severity in severity_counts:
54
+ severity_counts[severity] += 1
55
+
56
+ stats_text.append("🔍 Vulnerabilities Found: ", style="bold red")
57
+
58
+ severity_parts = []
59
+ for severity in ["critical", "high", "medium", "low", "info"]:
60
+ count = severity_counts[severity]
61
+ if count > 0:
62
+ severity_color = get_severity_color(severity)
63
+ severity_text = Text()
64
+ severity_text.append(f"{severity.upper()}: ", style=severity_color)
65
+ severity_text.append(str(count), style=f"bold {severity_color}")
66
+ severity_parts.append(severity_text)
67
+
68
+ for i, part in enumerate(severity_parts):
69
+ stats_text.append(part)
70
+ if i < len(severity_parts) - 1:
71
+ stats_text.append(" | ", style="dim white")
72
+
73
+ stats_text.append(" (Total: ", style="dim white")
74
+ stats_text.append(str(vuln_count), style="bold yellow")
75
+ stats_text.append(")", style="dim white")
76
+ stats_text.append("\n")
77
+ else:
78
+ stats_text.append("🔍 Vulnerabilities Found: ", style="bold green")
79
+ stats_text.append("0", style="bold white")
80
+ stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green")
81
+ stats_text.append("\n")
82
+
83
+ stats_text.append("🤖 Agents Used: ", style="bold cyan")
84
+ stats_text.append(str(agent_count), style="bold white")
85
+ stats_text.append(" • ", style="dim white")
86
+ stats_text.append("🛠️ Tools Called: ", style="bold cyan")
87
+ stats_text.append(str(tool_count), style="bold white")
88
+
89
+ return stats_text
90
+
91
+
92
+ def build_llm_stats_text(tracer: Any) -> Text:
93
+ llm_stats_text = Text()
94
+ if not tracer:
95
+ return llm_stats_text
96
+
97
+ llm_stats = tracer.get_total_llm_stats()
98
+ total_stats = llm_stats["total"]
99
+
100
+ if total_stats["requests"] > 0:
101
+ llm_stats_text.append("📥 Input Tokens: ", style="bold cyan")
102
+ llm_stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white")
103
+
104
+ if total_stats["cached_tokens"] > 0:
105
+ llm_stats_text.append(" • ", style="dim white")
106
+ llm_stats_text.append("⚡ Cached: ", style="bold green")
107
+ llm_stats_text.append(
108
+ format_token_count(total_stats["cached_tokens"]), style="bold green"
109
+ )
110
+
111
+ llm_stats_text.append(" • ", style="dim white")
112
+ llm_stats_text.append("📤 Output Tokens: ", style="bold cyan")
113
+ llm_stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white")
114
+
115
+ if total_stats["cost"] > 0:
116
+ llm_stats_text.append(" • ", style="dim white")
117
+ llm_stats_text.append("💰 Total Cost: $", style="bold cyan")
118
+ llm_stats_text.append(f"{total_stats['cost']:.4f}", style="bold yellow")
119
+
120
+ return llm_stats_text
121
+
122
+
123
+ # Name generation utilities
124
+ def generate_run_name() -> str:
125
+ # fmt: off
126
+ adjectives = [
127
+ "stealthy", "sneaky", "crafty", "elite", "phantom", "shadow", "silent",
128
+ "rogue", "covert", "ninja", "ghost", "cyber", "digital", "binary",
129
+ "encrypted", "obfuscated", "masked", "cloaked", "invisible", "anonymous"
130
+ ]
131
+ nouns = [
132
+ "exploit", "payload", "backdoor", "rootkit", "keylogger", "botnet", "trojan",
133
+ "worm", "virus", "packet", "buffer", "shell", "daemon", "spider", "crawler",
134
+ "scanner", "sniffer", "honeypot", "firewall", "breach"
135
+ ]
136
+ # fmt: on
137
+ adj = secrets.choice(adjectives)
138
+ noun = secrets.choice(nouns)
139
+ number = secrets.randbelow(900) + 100
140
+ return f"{adj}-{noun}-{number}"
141
+
142
+
143
+ # Target processing utilities
144
+ def infer_target_type(target: str) -> tuple[str, dict[str, str]]:
145
+ if not target or not isinstance(target, str):
146
+ raise ValueError("Target must be a non-empty string")
147
+
148
+ target = target.strip()
149
+
150
+ lower_target = target.lower()
151
+ bare_repo_prefixes = (
152
+ "github.com/",
153
+ "www.github.com/",
154
+ "gitlab.com/",
155
+ "www.gitlab.com/",
156
+ "bitbucket.org/",
157
+ "www.bitbucket.org/",
158
+ )
159
+ if any(lower_target.startswith(p) for p in bare_repo_prefixes):
160
+ return "repository", {"target_repo": f"https://{target}"}
161
+
162
+ parsed = urlparse(target)
163
+ if parsed.scheme in ("http", "https"):
164
+ if any(
165
+ host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"]
166
+ ):
167
+ return "repository", {"target_repo": target}
168
+ return "web_application", {"target_url": target}
169
+
170
+ path = Path(target).expanduser()
171
+ try:
172
+ if path.exists():
173
+ if path.is_dir():
174
+ resolved = path.resolve()
175
+ return "local_code", {"target_path": str(resolved)}
176
+ raise ValueError(f"Path exists but is not a directory: {target}")
177
+ except (OSError, RuntimeError) as e:
178
+ raise ValueError(f"Invalid path: {target} - {e!s}") from e
179
+
180
+ if target.startswith("git@") or target.endswith(".git"):
181
+ return "repository", {"target_repo": target}
182
+
183
+ if "." in target and "/" not in target and not target.startswith("."):
184
+ parts = target.split(".")
185
+ if len(parts) >= 2 and all(p and p.strip() for p in parts):
186
+ return "web_application", {"target_url": f"https://{target}"}
187
+
188
+ raise ValueError(
189
+ f"Invalid target: {target}\n"
190
+ "Target must be one of:\n"
191
+ "- A valid URL (http:// or https://)\n"
192
+ "- A Git repository URL (https://github.com/... or git@github.com:...)\n"
193
+ "- A local directory path\n"
194
+ "- A domain name (e.g., example.com)"
195
+ )
196
+
197
+
198
+ def sanitize_name(name: str) -> str:
199
+ sanitized = re.sub(r"[^A-Za-z0-9._-]", "-", name.strip())
200
+ return sanitized or "target"
201
+
202
+
203
+ def derive_repo_base_name(repo_url: str) -> str:
204
+ if repo_url.endswith("/"):
205
+ repo_url = repo_url[:-1]
206
+
207
+ if ":" in repo_url and repo_url.startswith("git@"):
208
+ path_part = repo_url.split(":", 1)[1]
209
+ else:
210
+ path_part = urlparse(repo_url).path or repo_url
211
+
212
+ candidate = path_part.split("/")[-1]
213
+ if candidate.endswith(".git"):
214
+ candidate = candidate[:-4]
215
+
216
+ return sanitize_name(candidate or "repository")
217
+
218
+
219
+ def derive_local_base_name(path_str: str) -> str:
220
+ try:
221
+ base = Path(path_str).resolve().name
222
+ except (OSError, RuntimeError):
223
+ base = Path(path_str).name
224
+ return sanitize_name(base or "workspace")
225
+
226
+
227
+ def assign_workspace_subdirs(targets_info: list[dict[str, Any]]) -> None:
228
+ name_counts: dict[str, int] = {}
229
+
230
+ for target in targets_info:
231
+ target_type = target["type"]
232
+ details = target["details"]
233
+
234
+ base_name: str | None = None
235
+ if target_type == "repository":
236
+ base_name = derive_repo_base_name(details["target_repo"])
237
+ elif target_type == "local_code":
238
+ base_name = derive_local_base_name(details.get("target_path", "local"))
239
+
240
+ if base_name is None:
241
+ continue
242
+
243
+ count = name_counts.get(base_name, 0) + 1
244
+ name_counts[base_name] = count
245
+
246
+ workspace_subdir = base_name if count == 1 else f"{base_name}-{count}"
247
+
248
+ details["workspace_subdir"] = workspace_subdir
249
+
250
+
251
+ def collect_local_sources(targets_info: list[dict[str, Any]]) -> list[dict[str, str]]:
252
+ local_sources: list[dict[str, str]] = []
253
+
254
+ for target_info in targets_info:
255
+ details = target_info["details"]
256
+ workspace_subdir = details.get("workspace_subdir")
257
+
258
+ if target_info["type"] == "local_code" and "target_path" in details:
259
+ local_sources.append(
260
+ {
261
+ "source_path": details["target_path"],
262
+ "workspace_subdir": workspace_subdir,
263
+ }
264
+ )
265
+
266
+ elif target_info["type"] == "repository" and "cloned_repo_path" in details:
267
+ local_sources.append(
268
+ {
269
+ "source_path": details["cloned_repo_path"],
270
+ "workspace_subdir": workspace_subdir,
271
+ }
272
+ )
273
+
274
+ return local_sources
275
+
276
+
277
+ # Repository utilities
278
+ def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None) -> str:
279
+ console = Console()
280
+
281
+ git_executable = shutil.which("git")
282
+ if git_executable is None:
283
+ raise FileNotFoundError("Git executable not found in PATH")
284
+
285
+ temp_dir = Path(tempfile.gettempdir()) / "strix_repos" / run_name
286
+ temp_dir.mkdir(parents=True, exist_ok=True)
287
+
288
+ if dest_name:
289
+ repo_name = dest_name
290
+ else:
291
+ repo_name = Path(repo_url).stem if repo_url.endswith(".git") else Path(repo_url).name
292
+
293
+ clone_path = temp_dir / repo_name
294
+
295
+ if clone_path.exists():
296
+ shutil.rmtree(clone_path)
297
+
298
+ try:
299
+ with console.status(f"[bold cyan]Cloning repository {repo_url}...", spinner="dots"):
300
+ subprocess.run( # noqa: S603
301
+ [
302
+ git_executable,
303
+ "clone",
304
+ repo_url,
305
+ str(clone_path),
306
+ ],
307
+ capture_output=True,
308
+ text=True,
309
+ check=True,
310
+ )
311
+
312
+ return str(clone_path.absolute())
313
+
314
+ except subprocess.CalledProcessError as e:
315
+ error_text = Text()
316
+ error_text.append("❌ ", style="bold red")
317
+ error_text.append("REPOSITORY CLONE FAILED", style="bold red")
318
+ error_text.append("\n\n", style="white")
319
+ error_text.append(f"Could not clone repository: {repo_url}\n", style="white")
320
+ error_text.append(
321
+ f"Error: {e.stderr if hasattr(e, 'stderr') and e.stderr else str(e)}", style="dim red"
322
+ )
323
+
324
+ panel = Panel(
325
+ error_text,
326
+ title="[bold red]🛡️ STRIX CLONE ERROR",
327
+ title_align="center",
328
+ border_style="red",
329
+ padding=(1, 2),
330
+ )
331
+ console.print("\n")
332
+ console.print(panel)
333
+ console.print()
334
+ sys.exit(1)
335
+ except FileNotFoundError:
336
+ error_text = Text()
337
+ error_text.append("❌ ", style="bold red")
338
+ error_text.append("GIT NOT FOUND", style="bold red")
339
+ error_text.append("\n\n", style="white")
340
+ error_text.append("Git is not installed or not available in PATH.\n", style="white")
341
+ error_text.append("Please install Git to clone repositories.\n", style="white")
342
+
343
+ panel = Panel(
344
+ error_text,
345
+ title="[bold red]🛡️ STRIX CLONE ERROR",
346
+ title_align="center",
347
+ border_style="red",
348
+ padding=(1, 2),
349
+ )
350
+ console.print("\n")
351
+ console.print(panel)
352
+ console.print()
353
+ sys.exit(1)
354
+
355
+
356
+ # Docker utilities
357
+ def check_docker_connection() -> Any:
358
+ try:
359
+ return docker.from_env()
360
+ except DockerException:
361
+ console = Console()
362
+ error_text = Text()
363
+ error_text.append("❌ ", style="bold red")
364
+ error_text.append("DOCKER NOT AVAILABLE", style="bold red")
365
+ error_text.append("\n\n", style="white")
366
+ error_text.append("Cannot connect to Docker daemon.\n", style="white")
367
+ error_text.append("Please ensure Docker is installed and running.\n\n", style="white")
368
+ error_text.append("Try running: ", style="dim white")
369
+ error_text.append("sudo systemctl start docker", style="dim cyan")
370
+
371
+ panel = Panel(
372
+ error_text,
373
+ title="[bold red]🛡️ STRIX STARTUP ERROR",
374
+ title_align="center",
375
+ border_style="red",
376
+ padding=(1, 2),
377
+ )
378
+ console.print("\n", panel, "\n")
379
+ raise RuntimeError("Docker not available") from None
380
+
381
+
382
+ def image_exists(client: Any, image_name: str) -> bool:
383
+ try:
384
+ client.images.get(image_name)
385
+ except ImageNotFound:
386
+ return False
387
+ else:
388
+ return True
389
+
390
+
391
+ def update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None:
392
+ if "Pull complete" in layer_status or "Already exists" in layer_status:
393
+ layers_info[layer_id] = "✓"
394
+ elif "Downloading" in layer_status:
395
+ layers_info[layer_id] = "↓"
396
+ elif "Extracting" in layer_status:
397
+ layers_info[layer_id] = "📦"
398
+ elif "Waiting" in layer_status:
399
+ layers_info[layer_id] = "⏳"
400
+ else:
401
+ layers_info[layer_id] = "•"
402
+
403
+
404
+ def process_pull_line(
405
+ line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str
406
+ ) -> str:
407
+ if "id" in line and "status" in line:
408
+ layer_id = line["id"]
409
+ update_layer_status(layers_info, layer_id, line["status"])
410
+
411
+ completed = sum(1 for v in layers_info.values() if v == "✓")
412
+ total = len(layers_info)
413
+
414
+ if total > 0:
415
+ update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete"
416
+ if update_msg != last_update:
417
+ status.update(update_msg)
418
+ return update_msg
419
+
420
+ elif "status" in line and "id" not in line:
421
+ global_status = line["status"]
422
+ if "Pulling from" in global_status:
423
+ status.update("[bold cyan]Fetching image manifest...")
424
+ elif "Digest:" in global_status:
425
+ status.update("[bold cyan]Verifying image...")
426
+ elif "Status:" in global_status:
427
+ status.update("[bold cyan]Finalizing...")
428
+
429
+ return last_update
430
+
431
+
432
+ # LLM utilities
433
+ def validate_llm_response(response: Any) -> None:
434
+ if not response or not response.choices or not response.choices[0].message.content:
435
+ raise RuntimeError("Invalid response from LLM")
@@ -40,7 +40,7 @@ class DockerRuntime(AbstractRuntime):
40
40
 
41
41
  def _get_scan_id(self, agent_id: str) -> str:
42
42
  try:
43
- from strix.cli.tracer import get_global_tracer
43
+ from strix.telemetry.tracer import get_global_tracer
44
44
 
45
45
  tracer = get_global_tracer()
46
46
  if tracer and tracer.scan_config:
@@ -250,7 +250,9 @@ class DockerRuntime(AbstractRuntime):
250
250
 
251
251
  time.sleep(5)
252
252
 
253
- def _copy_local_directory_to_container(self, container: Container, local_path: str) -> None:
253
+ def _copy_local_directory_to_container(
254
+ self, container: Container, local_path: str, target_name: str | None = None
255
+ ) -> None:
254
256
  import tarfile
255
257
  from io import BytesIO
256
258
 
@@ -260,13 +262,20 @@ class DockerRuntime(AbstractRuntime):
260
262
  logger.warning(f"Local path does not exist or is not directory: {local_path_obj}")
261
263
  return
262
264
 
263
- logger.info(f"Copying local directory {local_path_obj} to container")
265
+ if target_name:
266
+ logger.info(
267
+ f"Copying local directory {local_path_obj} to container at "
268
+ f"/workspace/{target_name}"
269
+ )
270
+ else:
271
+ logger.info(f"Copying local directory {local_path_obj} to container")
264
272
 
265
273
  tar_buffer = BytesIO()
266
274
  with tarfile.open(fileobj=tar_buffer, mode="w") as tar:
267
275
  for item in local_path_obj.rglob("*"):
268
276
  if item.is_file():
269
- arcname = item.relative_to(local_path_obj)
277
+ rel_path = item.relative_to(local_path_obj)
278
+ arcname = Path(target_name) / rel_path if target_name else rel_path
270
279
  tar.add(item, arcname=arcname)
271
280
 
272
281
  tar_buffer.seek(0)
@@ -283,14 +292,26 @@ class DockerRuntime(AbstractRuntime):
283
292
  logger.exception("Failed to copy local directory to container")
284
293
 
285
294
  async def create_sandbox(
286
- self, agent_id: str, existing_token: str | None = None, local_source_path: str | None = None
295
+ self,
296
+ agent_id: str,
297
+ existing_token: str | None = None,
298
+ local_sources: list[dict[str, str]] | None = None,
287
299
  ) -> SandboxInfo:
288
300
  scan_id = self._get_scan_id(agent_id)
289
301
  container = self._get_or_create_scan_container(scan_id)
290
302
 
291
303
  source_copied_key = f"_source_copied_{scan_id}"
292
- if local_source_path and not hasattr(self, source_copied_key):
293
- self._copy_local_directory_to_container(container, local_source_path)
304
+ if local_sources and not hasattr(self, source_copied_key):
305
+ for index, source in enumerate(local_sources, start=1):
306
+ source_path = source.get("source_path")
307
+ if not source_path:
308
+ continue
309
+
310
+ target_name = source.get("workspace_subdir")
311
+ if not target_name:
312
+ target_name = Path(source_path).name or f"target_{index}"
313
+
314
+ self._copy_local_directory_to_container(container, source_path, target_name)
294
315
  setattr(self, source_copied_key, True)
295
316
 
296
317
  container_id = container.id
strix/runtime/runtime.py CHANGED
@@ -13,7 +13,10 @@ class SandboxInfo(TypedDict):
13
13
  class AbstractRuntime(ABC):
14
14
  @abstractmethod
15
15
  async def create_sandbox(
16
- self, agent_id: str, existing_token: str | None = None, local_source_path: str | None = None
16
+ self,
17
+ agent_id: str,
18
+ existing_token: str | None = None,
19
+ local_sources: list[dict[str, str]] | None = None,
17
20
  ) -> SandboxInfo:
18
21
  raise NotImplementedError
19
22
 
@@ -0,0 +1,4 @@
1
+ from .tracer import Tracer, get_global_tracer, set_global_tracer
2
+
3
+
4
+ __all__ = ["Tracer", "get_global_tracer", "set_global_tracer"]
@@ -1,10 +1,14 @@
1
1
  import logging
2
2
  from datetime import UTC, datetime
3
3
  from pathlib import Path
4
- from typing import Any, Optional
4
+ from typing import TYPE_CHECKING, Any, Optional
5
5
  from uuid import uuid4
6
6
 
7
7
 
8
+ if TYPE_CHECKING:
9
+ from collections.abc import Callable
10
+
11
+
8
12
  logger = logging.getLogger(__name__)
9
13
 
10
14
  _global_tracer: Optional["Tracer"] = None
@@ -40,14 +44,15 @@ class Tracer:
40
44
  "run_name": self.run_name,
41
45
  "start_time": self.start_time,
42
46
  "end_time": None,
43
- "target": None,
44
- "scan_type": None,
47
+ "targets": [],
45
48
  "status": "running",
46
49
  }
47
50
  self._run_dir: Path | None = None
48
51
  self._next_execution_id = 1
49
52
  self._next_message_id = 1
50
53
 
54
+ self.vulnerability_found_callback: Callable[[str, str, str, str], None] | None = None
55
+
51
56
  def set_run_name(self, run_name: str) -> None:
52
57
  self.run_name = run_name
53
58
  self.run_id = run_name
@@ -81,6 +86,12 @@ class Tracer:
81
86
 
82
87
  self.vulnerability_reports.append(report)
83
88
  logger.info(f"Added vulnerability report: {report_id} - {title}")
89
+
90
+ if self.vulnerability_found_callback:
91
+ self.vulnerability_found_callback(
92
+ report_id, title.strip(), content.strip(), severity.lower().strip()
93
+ )
94
+
84
95
  return report_id
85
96
 
86
97
  def set_final_scan_result(
@@ -181,8 +192,7 @@ class Tracer:
181
192
  self.scan_config = config
182
193
  self.run_metadata.update(
183
194
  {
184
- "target": config.get("target", {}),
185
- "scan_type": config.get("scan_type", "general"),
195
+ "targets": config.get("targets", []),
186
196
  "user_instructions": config.get("user_instructions", ""),
187
197
  "max_iterations": config.get("max_iterations", 200),
188
198
  }
@@ -194,14 +204,16 @@ class Tracer:
194
204
  self.end_time = datetime.now(UTC).isoformat()
195
205
 
196
206
  if self.final_scan_result:
197
- scan_report_file = run_dir / "scan_report.md"
198
- with scan_report_file.open("w", encoding="utf-8") as f:
199
- f.write("# Security Scan Report\n\n")
207
+ penetration_test_report_file = run_dir / "penetration_test_report.md"
208
+ with penetration_test_report_file.open("w", encoding="utf-8") as f:
209
+ f.write("# Security Penetration Test Report\n\n")
200
210
  f.write(
201
211
  f"**Generated:** {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}\n\n"
202
212
  )
203
213
  f.write(f"{self.final_scan_result}\n")
204
- logger.info(f"Saved final scan report to: {scan_report_file}")
214
+ logger.info(
215
+ f"Saved final penetration test report to: {penetration_test_report_file}"
216
+ )
205
217
 
206
218
  if self.vulnerability_reports:
207
219
  vuln_dir = run_dir / "vulnerabilities"
@@ -228,15 +228,19 @@ def create_agent(
228
228
  from strix.agents.state import AgentState
229
229
  from strix.llm.config import LLMConfig
230
230
 
231
- state = AgentState(task=task, agent_name=name, parent_id=parent_id, max_iterations=200)
231
+ state = AgentState(task=task, agent_name=name, parent_id=parent_id, max_iterations=300)
232
232
 
233
233
  llm_config = LLMConfig(prompt_modules=module_list)
234
- agent = StrixAgent(
235
- {
236
- "llm_config": llm_config,
237
- "state": state,
238
- }
239
- )
234
+
235
+ parent_agent = _agent_instances.get(parent_id)
236
+ agent_config = {
237
+ "llm_config": llm_config,
238
+ "state": state,
239
+ }
240
+ if parent_agent and hasattr(parent_agent, "non_interactive"):
241
+ agent_config["non_interactive"] = parent_agent.non_interactive
242
+
243
+ agent = StrixAgent(agent_config)
240
244
 
241
245
  inherited_messages = []
242
246
  if inherit_context:
@@ -487,7 +491,7 @@ def stop_agent(agent_id: str) -> dict[str, Any]:
487
491
  agent_node["status"] = "stopping"
488
492
 
489
493
  try:
490
- from strix.cli.tracer import get_global_tracer
494
+ from strix.telemetry.tracer import get_global_tracer
491
495
 
492
496
  tracer = get_global_tracer()
493
497
  if tracer:
@@ -578,7 +582,7 @@ def wait_for_message(
578
582
  _agent_graph["nodes"][agent_id]["waiting_reason"] = reason
579
583
 
580
584
  try:
581
- from strix.cli.tracer import get_global_tracer
585
+ from strix.telemetry.tracer import get_global_tracer
582
586
 
583
587
  tracer = get_global_tracer()
584
588
  if tracer:
strix/tools/executor.py CHANGED
@@ -240,7 +240,7 @@ async def _execute_single_tool(
240
240
 
241
241
  def _get_tracer_and_agent_id(agent_state: Any | None) -> tuple[Any | None, str]:
242
242
  try:
243
- from strix.cli.tracer import get_global_tracer
243
+ from strix.telemetry.tracer import get_global_tracer
244
244
 
245
245
  tracer = get_global_tracer()
246
246
  agent_id = agent_state.agent_id if agent_state else "unknown_agent"
@@ -107,7 +107,7 @@ def _check_active_agents(agent_state: Any = None) -> dict[str, Any] | None:
107
107
 
108
108
  def _finalize_with_tracer(content: str, success: bool) -> dict[str, Any]:
109
109
  try:
110
- from strix.cli.tracer import get_global_tracer
110
+ from strix.telemetry.tracer import get_global_tracer
111
111
 
112
112
  tracer = get_global_tracer()
113
113
  if tracer:
@@ -27,7 +27,7 @@ def create_vulnerability_report(
27
27
  return {"success": False, "message": validation_error}
28
28
 
29
29
  try:
30
- from strix.cli.tracer import get_global_tracer
30
+ from strix.telemetry.tracer import get_global_tracer
31
31
 
32
32
  tracer = get_global_tracer()
33
33
  if tracer: