strix-agent 0.1.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.
Files changed (99) hide show
  1. strix/__init__.py +0 -0
  2. strix/agents/StrixAgent/__init__.py +4 -0
  3. strix/agents/StrixAgent/strix_agent.py +60 -0
  4. strix/agents/StrixAgent/system_prompt.jinja +504 -0
  5. strix/agents/__init__.py +10 -0
  6. strix/agents/base_agent.py +394 -0
  7. strix/agents/state.py +139 -0
  8. strix/cli/__init__.py +4 -0
  9. strix/cli/app.py +1124 -0
  10. strix/cli/assets/cli.tcss +680 -0
  11. strix/cli/main.py +542 -0
  12. strix/cli/tool_components/__init__.py +39 -0
  13. strix/cli/tool_components/agents_graph_renderer.py +129 -0
  14. strix/cli/tool_components/base_renderer.py +61 -0
  15. strix/cli/tool_components/browser_renderer.py +107 -0
  16. strix/cli/tool_components/file_edit_renderer.py +95 -0
  17. strix/cli/tool_components/finish_renderer.py +32 -0
  18. strix/cli/tool_components/notes_renderer.py +108 -0
  19. strix/cli/tool_components/proxy_renderer.py +255 -0
  20. strix/cli/tool_components/python_renderer.py +34 -0
  21. strix/cli/tool_components/registry.py +72 -0
  22. strix/cli/tool_components/reporting_renderer.py +53 -0
  23. strix/cli/tool_components/scan_info_renderer.py +58 -0
  24. strix/cli/tool_components/terminal_renderer.py +99 -0
  25. strix/cli/tool_components/thinking_renderer.py +29 -0
  26. strix/cli/tool_components/user_message_renderer.py +43 -0
  27. strix/cli/tool_components/web_search_renderer.py +28 -0
  28. strix/cli/tracer.py +308 -0
  29. strix/llm/__init__.py +14 -0
  30. strix/llm/config.py +19 -0
  31. strix/llm/llm.py +310 -0
  32. strix/llm/memory_compressor.py +206 -0
  33. strix/llm/request_queue.py +63 -0
  34. strix/llm/utils.py +84 -0
  35. strix/prompts/__init__.py +113 -0
  36. strix/prompts/coordination/root_agent.jinja +41 -0
  37. strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
  38. strix/prompts/vulnerabilities/business_logic.jinja +143 -0
  39. strix/prompts/vulnerabilities/csrf.jinja +168 -0
  40. strix/prompts/vulnerabilities/idor.jinja +164 -0
  41. strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
  42. strix/prompts/vulnerabilities/rce.jinja +222 -0
  43. strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
  44. strix/prompts/vulnerabilities/ssrf.jinja +168 -0
  45. strix/prompts/vulnerabilities/xss.jinja +221 -0
  46. strix/prompts/vulnerabilities/xxe.jinja +276 -0
  47. strix/runtime/__init__.py +19 -0
  48. strix/runtime/docker_runtime.py +298 -0
  49. strix/runtime/runtime.py +25 -0
  50. strix/runtime/tool_server.py +97 -0
  51. strix/tools/__init__.py +64 -0
  52. strix/tools/agents_graph/__init__.py +16 -0
  53. strix/tools/agents_graph/agents_graph_actions.py +610 -0
  54. strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
  55. strix/tools/argument_parser.py +120 -0
  56. strix/tools/browser/__init__.py +4 -0
  57. strix/tools/browser/browser_actions.py +236 -0
  58. strix/tools/browser/browser_actions_schema.xml +183 -0
  59. strix/tools/browser/browser_instance.py +533 -0
  60. strix/tools/browser/tab_manager.py +342 -0
  61. strix/tools/executor.py +302 -0
  62. strix/tools/file_edit/__init__.py +4 -0
  63. strix/tools/file_edit/file_edit_actions.py +141 -0
  64. strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
  65. strix/tools/finish/__init__.py +4 -0
  66. strix/tools/finish/finish_actions.py +167 -0
  67. strix/tools/finish/finish_actions_schema.xml +45 -0
  68. strix/tools/notes/__init__.py +14 -0
  69. strix/tools/notes/notes_actions.py +191 -0
  70. strix/tools/notes/notes_actions_schema.xml +150 -0
  71. strix/tools/proxy/__init__.py +20 -0
  72. strix/tools/proxy/proxy_actions.py +101 -0
  73. strix/tools/proxy/proxy_actions_schema.xml +267 -0
  74. strix/tools/proxy/proxy_manager.py +785 -0
  75. strix/tools/python/__init__.py +4 -0
  76. strix/tools/python/python_actions.py +47 -0
  77. strix/tools/python/python_actions_schema.xml +131 -0
  78. strix/tools/python/python_instance.py +172 -0
  79. strix/tools/python/python_manager.py +131 -0
  80. strix/tools/registry.py +196 -0
  81. strix/tools/reporting/__init__.py +6 -0
  82. strix/tools/reporting/reporting_actions.py +63 -0
  83. strix/tools/reporting/reporting_actions_schema.xml +30 -0
  84. strix/tools/terminal/__init__.py +4 -0
  85. strix/tools/terminal/terminal_actions.py +53 -0
  86. strix/tools/terminal/terminal_actions_schema.xml +114 -0
  87. strix/tools/terminal/terminal_instance.py +231 -0
  88. strix/tools/terminal/terminal_manager.py +191 -0
  89. strix/tools/thinking/__init__.py +4 -0
  90. strix/tools/thinking/thinking_actions.py +18 -0
  91. strix/tools/thinking/thinking_actions_schema.xml +52 -0
  92. strix/tools/web_search/__init__.py +4 -0
  93. strix/tools/web_search/web_search_actions.py +80 -0
  94. strix/tools/web_search/web_search_actions_schema.xml +83 -0
  95. strix_agent-0.1.1.dist-info/LICENSE +201 -0
  96. strix_agent-0.1.1.dist-info/METADATA +200 -0
  97. strix_agent-0.1.1.dist-info/RECORD +99 -0
  98. strix_agent-0.1.1.dist-info/WHEEL +4 -0
  99. strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,4 @@
1
+ from .python_actions import python_action
2
+
3
+
4
+ __all__ = ["python_action"]
@@ -0,0 +1,47 @@
1
+ from typing import Any, Literal
2
+
3
+ from strix.tools.registry import register_tool
4
+
5
+ from .python_manager import get_python_session_manager
6
+
7
+
8
+ PythonAction = Literal["new_session", "execute", "close", "list_sessions"]
9
+
10
+
11
+ @register_tool
12
+ def python_action(
13
+ action: PythonAction,
14
+ code: str | None = None,
15
+ timeout: int = 30,
16
+ session_id: str | None = None,
17
+ ) -> dict[str, Any]:
18
+ def _validate_code(action_name: str, code: str | None) -> None:
19
+ if not code:
20
+ raise ValueError(f"code parameter is required for {action_name} action")
21
+
22
+ def _validate_action(action_name: str) -> None:
23
+ raise ValueError(f"Unknown action: {action_name}")
24
+
25
+ manager = get_python_session_manager()
26
+
27
+ try:
28
+ match action:
29
+ case "new_session":
30
+ return manager.create_session(session_id, code, timeout)
31
+
32
+ case "execute":
33
+ _validate_code(action, code)
34
+ assert code is not None
35
+ return manager.execute_code(session_id, code, timeout)
36
+
37
+ case "close":
38
+ return manager.close_session(session_id)
39
+
40
+ case "list_sessions":
41
+ return manager.list_sessions()
42
+
43
+ case _:
44
+ _validate_action(action) # type: ignore[unreachable]
45
+
46
+ except (ValueError, RuntimeError) as e:
47
+ return {"stderr": str(e), "session_id": session_id, "stdout": "", "is_running": False}
@@ -0,0 +1,131 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <tools>
3
+ <tool name="python_action">
4
+ <description>Perform Python actions using persistent interpreter sessions for cybersecurity tasks.</description>
5
+ <details>Common Use Cases:
6
+ - Security script development and testing (payload generation, exploit scripts)
7
+ - Data analysis of security logs, network traffic, or vulnerability scans
8
+ - Cryptographic operations and security tool automation
9
+ - Interactive penetration testing workflows and proof-of-concept development
10
+ - Processing security data formats (JSON, XML, CSV from security tools)
11
+ - HTTP proxy interaction for web security testing (all proxy functions are pre-imported)
12
+
13
+ Each session instance is PERSISTENT and maintains its own global and local namespaces
14
+ until explicitly closed, allowing for multi-step security workflows and stateful computations.
15
+
16
+ PROXY FUNCTIONS PRE-IMPORTED:
17
+ All proxy action functions are automatically imported into every Python session, enabling
18
+ seamless HTTP traffic analysis and web security testing
19
+
20
+ This is particularly useful for:
21
+ - Analyzing captured HTTP traffic during web application testing
22
+ - Automating request manipulation and replay attacks
23
+ - Building custom security testing workflows combining proxy data with Python analysis
24
+ - Correlating multiple requests for advanced attack scenarios</details>
25
+ <parameters>
26
+ <parameter name="action" type="string" required="true">
27
+ <description>The Python action to perform: - new_session: Create a new Python interpreter session. This MUST be the first action for each session. - execute: Execute Python code in the specified session. - close: Close the specified session instance. - list_sessions: List all active Python sessions.</description>
28
+ </parameter>
29
+ <parameter name="code" type="string" required="false">
30
+ <description>Required for 'new_session' (as initial code) and 'execute' actions. The Python code to execute.</description>
31
+ </parameter>
32
+ <parameter name="timeout" type="integer" required="false">
33
+ <description>Maximum execution time in seconds for code execution. Applies to both 'new_session' (when initial code is provided) and 'execute' actions. Default is 30 seconds.</description>
34
+ </parameter>
35
+ <parameter name="session_id" type="string" required="false">
36
+ <description>Unique identifier for the Python session. If not provided, uses the default session ID.</description>
37
+ </parameter>
38
+ </parameters>
39
+ <returns type="Dict[str, Any]">
40
+ <description>Response containing: - session_id: the ID of the session that was operated on - stdout: captured standard output from code execution (for execute action) - stderr: any error message if execution failed - result: string representation of the last expression result - execution_time: time taken to execute the code - message: status message about the action performed - Various session info depending on the action</description>
41
+ </returns>
42
+ <notes>
43
+ Important usage rules:
44
+ 1. PERSISTENCE: Session instances remain active and maintain their state (variables,
45
+ imports, function definitions) until explicitly closed with the 'close' action.
46
+ This allows for multi-step workflows across multiple tool calls.
47
+ 2. MULTIPLE SESSIONS: You can run multiple Python sessions concurrently by using
48
+ different session_id values. Each session operates independently with its own
49
+ namespace.
50
+ 3. Session interaction MUST begin with 'new_session' action for each session instance.
51
+ 4. Only one action can be performed per call.
52
+ 5. CODE EXECUTION:
53
+ - Both expressions and statements are supported
54
+ - Expressions automatically return their result
55
+ - Print statements and stdout are captured
56
+ - Variables persist between executions in the same session
57
+ - Imports, function definitions, etc. persist in the session
58
+ - IPython magic commands are fully supported (%pip, %time, %whos, %%writefile, etc.)
59
+ - Line magics (%) and cell magics (%%) work as expected
60
+ 6. CLOSE: Terminates the session completely and frees memory
61
+ 7. The Python sessions can operate concurrently with other tools. You may invoke
62
+ terminal, browser, or other tools while maintaining active Python sessions.
63
+ 8. Each session has its own isolated namespace - variables in one session don't
64
+ affect others.
65
+ </notes>
66
+ <examples>
67
+ # Create new session for security analysis (default session)
68
+ <function=python_action>
69
+ <parameter=action>new_session</parameter>
70
+ <parameter=code>import hashlib
71
+ import base64
72
+ import json
73
+ print("Security analysis session started")</parameter>
74
+ </function>
75
+
76
+ # Analyze security data in the default session
77
+ <function=python_action>
78
+ <parameter=action>execute</parameter>
79
+ <parameter=code>vulnerability_data = {"cve": "CVE-2024-1234", "severity": "high"}
80
+ encoded_payload = base64.b64encode(json.dumps(vulnerability_data).encode())
81
+ print(f"Encoded: {encoded_payload.decode()}")</parameter>
82
+ </function>
83
+
84
+ # Long running security scan with custom timeout
85
+ <function=python_action>
86
+ <parameter=action>execute</parameter>
87
+ <parameter=code>import time
88
+ # Simulate long-running vulnerability scan
89
+ time.sleep(45)
90
+ print('Security scan completed!')</parameter>
91
+ <parameter=timeout>50</parameter>
92
+ </function>
93
+
94
+ # Use IPython magic commands for package management and profiling
95
+ <function=python_action>
96
+ <parameter=action>execute</parameter>
97
+ <parameter=code>%pip install requests
98
+ %time response = requests.get('https://httpbin.org/json')
99
+ %whos</parameter>
100
+
101
+ # Analyze requests for potential vulnerabilities
102
+ <function=python_action>
103
+ <parameter=action>execute</parameter>
104
+ <parameter=code># Filter for POST requests that might contain sensitive data
105
+ post_requests = list_requests(
106
+ httpql_filter="req.method.eq:POST",
107
+ page_size=20
108
+ )
109
+
110
+ # Analyze each POST request for potential issues
111
+ for req in post_requests.get('requests', []):
112
+ request_id = req['id']
113
+ # View the request details
114
+ request_details = view_request(request_id, part="request")
115
+
116
+ # Check for potential SQL injection points
117
+ body = request_details.get('body', '')
118
+ if any(keyword in body.lower() for keyword in ['select', 'union', 'insert', 'update']):
119
+ print(f"Potential SQL injection in request {request_id}")
120
+
121
+ # Repeat the request with a test payload
122
+ test_payload = repeat_request(request_id, {
123
+ 'body': body + "' OR '1'='1"
124
+ })
125
+ print(f"Test response status: {test_payload.get('status_code')}")
126
+
127
+ print("Security analysis complete!")</parameter>
128
+ </function>
129
+ </examples>
130
+ </tool>
131
+ </tools>
@@ -0,0 +1,172 @@
1
+ import io
2
+ import signal
3
+ import sys
4
+ import threading
5
+ from typing import Any
6
+
7
+ from IPython.core.interactiveshell import InteractiveShell
8
+
9
+
10
+ MAX_STDOUT_LENGTH = 10_000
11
+ MAX_STDERR_LENGTH = 5_000
12
+
13
+
14
+ class PythonInstance:
15
+ def __init__(self, session_id: str) -> None:
16
+ self.session_id = session_id
17
+ self.is_running = True
18
+ self._execution_lock = threading.Lock()
19
+
20
+ import os
21
+
22
+ os.chdir("/workspace")
23
+
24
+ self.shell = InteractiveShell()
25
+ self.shell.init_completer()
26
+ self.shell.init_history()
27
+ self.shell.init_logger()
28
+
29
+ self._setup_proxy_functions()
30
+
31
+ def _setup_proxy_functions(self) -> None:
32
+ try:
33
+ from strix.tools.proxy import proxy_actions
34
+
35
+ proxy_functions = [
36
+ "list_requests",
37
+ "list_sitemap",
38
+ "repeat_request",
39
+ "scope_rules",
40
+ "send_request",
41
+ "view_request",
42
+ "view_sitemap_entry",
43
+ ]
44
+
45
+ proxy_dict = {name: getattr(proxy_actions, name) for name in proxy_functions}
46
+ self.shell.user_ns.update(proxy_dict)
47
+ except ImportError:
48
+ pass
49
+
50
+ def _validate_session(self) -> dict[str, Any] | None:
51
+ if not self.is_running:
52
+ return {
53
+ "session_id": self.session_id,
54
+ "stdout": "",
55
+ "stderr": "Session is not running",
56
+ "result": None,
57
+ }
58
+ return None
59
+
60
+ def _setup_execution_environment(self, timeout: int) -> tuple[Any, io.StringIO, io.StringIO]:
61
+ stdout_capture = io.StringIO()
62
+ stderr_capture = io.StringIO()
63
+
64
+ def timeout_handler(signum: int, frame: Any) -> None:
65
+ raise TimeoutError(f"Code execution timed out after {timeout} seconds")
66
+
67
+ old_handler = signal.signal(signal.SIGALRM, timeout_handler)
68
+ signal.alarm(timeout)
69
+
70
+ sys.stdout = stdout_capture
71
+ sys.stderr = stderr_capture
72
+
73
+ return old_handler, stdout_capture, stderr_capture
74
+
75
+ def _cleanup_execution_environment(
76
+ self, old_handler: Any, old_stdout: Any, old_stderr: Any
77
+ ) -> None:
78
+ signal.signal(signal.SIGALRM, old_handler)
79
+ sys.stdout = old_stdout
80
+ sys.stderr = old_stderr
81
+
82
+ def _truncate_output(self, content: str, max_length: int, suffix: str) -> str:
83
+ if len(content) > max_length:
84
+ return content[:max_length] + suffix
85
+ return content
86
+
87
+ def _format_execution_result(
88
+ self, execution_result: Any, stdout_content: str, stderr_content: str
89
+ ) -> dict[str, Any]:
90
+ stdout = self._truncate_output(
91
+ stdout_content, MAX_STDOUT_LENGTH, "... [stdout truncated at 10k chars]"
92
+ )
93
+
94
+ if execution_result.result is not None:
95
+ if stdout and not stdout.endswith("\n"):
96
+ stdout += "\n"
97
+ result_repr = repr(execution_result.result)
98
+ result_repr = self._truncate_output(
99
+ result_repr, MAX_STDOUT_LENGTH, "... [result truncated at 10k chars]"
100
+ )
101
+ stdout += result_repr
102
+
103
+ stdout = self._truncate_output(
104
+ stdout, MAX_STDOUT_LENGTH, "... [output truncated at 10k chars]"
105
+ )
106
+
107
+ stderr_content = stderr_content if stderr_content else ""
108
+ stderr_content = self._truncate_output(
109
+ stderr_content, MAX_STDERR_LENGTH, "... [stderr truncated at 5k chars]"
110
+ )
111
+
112
+ if (
113
+ execution_result.error_before_exec or execution_result.error_in_exec
114
+ ) and not stderr_content:
115
+ stderr_content = "Execution error occurred"
116
+
117
+ return {
118
+ "session_id": self.session_id,
119
+ "stdout": stdout,
120
+ "stderr": stderr_content,
121
+ "result": repr(execution_result.result)
122
+ if execution_result.result is not None
123
+ else None,
124
+ }
125
+
126
+ def _handle_execution_error(self, error: BaseException) -> dict[str, Any]:
127
+ error_msg = str(error)
128
+ error_msg = self._truncate_output(
129
+ error_msg, MAX_STDERR_LENGTH, "... [error truncated at 5k chars]"
130
+ )
131
+
132
+ return {
133
+ "session_id": self.session_id,
134
+ "stdout": "",
135
+ "stderr": error_msg,
136
+ "result": None,
137
+ }
138
+
139
+ def execute_code(self, code: str, timeout: int = 30) -> dict[str, Any]:
140
+ session_error = self._validate_session()
141
+ if session_error:
142
+ return session_error
143
+
144
+ with self._execution_lock:
145
+ old_stdout, old_stderr = sys.stdout, sys.stderr
146
+
147
+ try:
148
+ old_handler, stdout_capture, stderr_capture = self._setup_execution_environment(
149
+ timeout
150
+ )
151
+
152
+ try:
153
+ execution_result = self.shell.run_cell(code, silent=False, store_history=True)
154
+ signal.alarm(0)
155
+
156
+ return self._format_execution_result(
157
+ execution_result, stdout_capture.getvalue(), stderr_capture.getvalue()
158
+ )
159
+
160
+ except (TimeoutError, KeyboardInterrupt, SystemExit) as e:
161
+ signal.alarm(0)
162
+ return self._handle_execution_error(e)
163
+
164
+ finally:
165
+ self._cleanup_execution_environment(old_handler, old_stdout, old_stderr)
166
+
167
+ def close(self) -> None:
168
+ self.is_running = False
169
+ self.shell.reset(new_session=False)
170
+
171
+ def is_alive(self) -> bool:
172
+ return self.is_running
@@ -0,0 +1,131 @@
1
+ import atexit
2
+ import contextlib
3
+ import signal
4
+ import sys
5
+ import threading
6
+ from typing import Any
7
+
8
+ from .python_instance import PythonInstance
9
+
10
+
11
+ class PythonSessionManager:
12
+ def __init__(self) -> None:
13
+ self.sessions: dict[str, PythonInstance] = {}
14
+ self._lock = threading.Lock()
15
+ self.default_session_id = "default"
16
+
17
+ self._register_cleanup_handlers()
18
+
19
+ def create_session(
20
+ self, session_id: str | None = None, initial_code: str | None = None, timeout: int = 30
21
+ ) -> dict[str, Any]:
22
+ if session_id is None:
23
+ session_id = self.default_session_id
24
+
25
+ with self._lock:
26
+ if session_id in self.sessions:
27
+ raise ValueError(f"Python session '{session_id}' already exists")
28
+
29
+ session = PythonInstance(session_id)
30
+ self.sessions[session_id] = session
31
+
32
+ if initial_code:
33
+ result = session.execute_code(initial_code, timeout)
34
+ result["message"] = (
35
+ f"Python session '{session_id}' created successfully with initial code"
36
+ )
37
+ else:
38
+ result = {
39
+ "session_id": session_id,
40
+ "message": f"Python session '{session_id}' created successfully",
41
+ }
42
+
43
+ return result
44
+
45
+ def execute_code(
46
+ self, session_id: str | None = None, code: str | None = None, timeout: int = 30
47
+ ) -> dict[str, Any]:
48
+ if session_id is None:
49
+ session_id = self.default_session_id
50
+
51
+ if not code:
52
+ raise ValueError("No code provided for execution")
53
+
54
+ with self._lock:
55
+ if session_id not in self.sessions:
56
+ raise ValueError(f"Python session '{session_id}' not found")
57
+
58
+ session = self.sessions[session_id]
59
+
60
+ result = session.execute_code(code, timeout)
61
+ result["message"] = f"Code executed in session '{session_id}'"
62
+ return result
63
+
64
+ def close_session(self, session_id: str | None = None) -> dict[str, Any]:
65
+ if session_id is None:
66
+ session_id = self.default_session_id
67
+
68
+ with self._lock:
69
+ if session_id not in self.sessions:
70
+ raise ValueError(f"Python session '{session_id}' not found")
71
+
72
+ session = self.sessions.pop(session_id)
73
+
74
+ session.close()
75
+ return {
76
+ "session_id": session_id,
77
+ "message": f"Python session '{session_id}' closed successfully",
78
+ "is_running": False,
79
+ }
80
+
81
+ def list_sessions(self) -> dict[str, Any]:
82
+ with self._lock:
83
+ session_info = {}
84
+ for sid, session in self.sessions.items():
85
+ session_info[sid] = {
86
+ "is_running": session.is_running,
87
+ "is_alive": session.is_alive(),
88
+ }
89
+
90
+ return {"sessions": session_info, "total_count": len(session_info)}
91
+
92
+ def cleanup_dead_sessions(self) -> None:
93
+ with self._lock:
94
+ dead_sessions = []
95
+ for sid, session in self.sessions.items():
96
+ if not session.is_alive():
97
+ dead_sessions.append(sid)
98
+
99
+ for sid in dead_sessions:
100
+ session = self.sessions.pop(sid)
101
+ with contextlib.suppress(Exception):
102
+ session.close()
103
+
104
+ def close_all_sessions(self) -> None:
105
+ with self._lock:
106
+ sessions_to_close = list(self.sessions.values())
107
+ self.sessions.clear()
108
+
109
+ for session in sessions_to_close:
110
+ with contextlib.suppress(Exception):
111
+ session.close()
112
+
113
+ def _register_cleanup_handlers(self) -> None:
114
+ atexit.register(self.close_all_sessions)
115
+
116
+ signal.signal(signal.SIGTERM, self._signal_handler)
117
+ signal.signal(signal.SIGINT, self._signal_handler)
118
+
119
+ if hasattr(signal, "SIGHUP"):
120
+ signal.signal(signal.SIGHUP, self._signal_handler)
121
+
122
+ def _signal_handler(self, _signum: int, _frame: Any) -> None:
123
+ self.close_all_sessions()
124
+ sys.exit(0)
125
+
126
+
127
+ _python_session_manager = PythonSessionManager()
128
+
129
+
130
+ def get_python_session_manager() -> PythonSessionManager:
131
+ return _python_session_manager
@@ -0,0 +1,196 @@
1
+ import inspect
2
+ import logging
3
+ import os
4
+ from collections.abc import Callable
5
+ from functools import wraps
6
+ from inspect import signature
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ tools: list[dict[str, Any]] = []
12
+ _tools_by_name: dict[str, Callable[..., Any]] = {}
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class ImplementedInClientSideOnlyError(Exception):
17
+ def __init__(
18
+ self,
19
+ message: str = "This tool is implemented in the client side only",
20
+ ) -> None:
21
+ self.message = message
22
+ super().__init__(self.message)
23
+
24
+
25
+ def _process_dynamic_content(content: str) -> str:
26
+ if "{{DYNAMIC_MODULES_DESCRIPTION}}" in content:
27
+ try:
28
+ from strix.prompts import generate_modules_description
29
+
30
+ modules_description = generate_modules_description()
31
+ content = content.replace("{{DYNAMIC_MODULES_DESCRIPTION}}", modules_description)
32
+ except ImportError:
33
+ logger.warning("Could not import prompts utilities for dynamic schema generation")
34
+ content = content.replace(
35
+ "{{DYNAMIC_MODULES_DESCRIPTION}}",
36
+ "List of prompt modules to load for this agent (max 3). Module discovery failed.",
37
+ )
38
+
39
+ return content
40
+
41
+
42
+ def _load_xml_schema(path: Path) -> Any:
43
+ if not path.exists():
44
+ return None
45
+ try:
46
+ content = path.read_text()
47
+
48
+ content = _process_dynamic_content(content)
49
+
50
+ start_tag = '<tool name="'
51
+ end_tag = "</tool>"
52
+ tools_dict = {}
53
+
54
+ pos = 0
55
+ while True:
56
+ start_pos = content.find(start_tag, pos)
57
+ if start_pos == -1:
58
+ break
59
+
60
+ name_start = start_pos + len(start_tag)
61
+ name_end = content.find('"', name_start)
62
+ if name_end == -1:
63
+ break
64
+ tool_name = content[name_start:name_end]
65
+
66
+ end_pos = content.find(end_tag, name_end)
67
+ if end_pos == -1:
68
+ break
69
+ end_pos += len(end_tag)
70
+
71
+ tool_element = content[start_pos:end_pos]
72
+ tools_dict[tool_name] = tool_element
73
+
74
+ pos = end_pos
75
+
76
+ if pos >= len(content):
77
+ break
78
+ except (IndexError, ValueError, UnicodeError) as e:
79
+ logger.warning(f"Error loading schema file {path}: {e}")
80
+ return None
81
+ else:
82
+ return tools_dict
83
+
84
+
85
+ def _get_module_name(func: Callable[..., Any]) -> str:
86
+ module = inspect.getmodule(func)
87
+ if not module:
88
+ return "unknown"
89
+
90
+ module_name = module.__name__
91
+ if ".tools." in module_name:
92
+ parts = module_name.split(".tools.")[-1].split(".")
93
+ if len(parts) >= 1:
94
+ return parts[0]
95
+ return "unknown"
96
+
97
+
98
+ def register_tool(
99
+ func: Callable[..., Any] | None = None, *, sandbox_execution: bool = True
100
+ ) -> Callable[..., Any]:
101
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
102
+ func_dict = {
103
+ "name": f.__name__,
104
+ "function": f,
105
+ "module": _get_module_name(f),
106
+ "sandbox_execution": sandbox_execution,
107
+ }
108
+
109
+ sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true"
110
+ if not sandbox_mode:
111
+ try:
112
+ module_path = Path(inspect.getfile(f))
113
+ schema_file_name = f"{module_path.stem}_schema.xml"
114
+ schema_path = module_path.parent / schema_file_name
115
+
116
+ xml_tools = _load_xml_schema(schema_path)
117
+
118
+ if xml_tools is not None and f.__name__ in xml_tools:
119
+ func_dict["xml_schema"] = xml_tools[f.__name__]
120
+ else:
121
+ func_dict["xml_schema"] = (
122
+ f'<tool name="{f.__name__}">'
123
+ "<description>Schema not found for tool.</description>"
124
+ "</tool>"
125
+ )
126
+ except (TypeError, FileNotFoundError) as e:
127
+ logger.warning(f"Error loading schema for {f.__name__}: {e}")
128
+ func_dict["xml_schema"] = (
129
+ f'<tool name="{f.__name__}">'
130
+ "<description>Error loading schema.</description>"
131
+ "</tool>"
132
+ )
133
+
134
+ tools.append(func_dict)
135
+ _tools_by_name[str(func_dict["name"])] = f
136
+
137
+ @wraps(f)
138
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
139
+ return f(*args, **kwargs)
140
+
141
+ return wrapper
142
+
143
+ if func is None:
144
+ return decorator
145
+ return decorator(func)
146
+
147
+
148
+ def get_tool_by_name(name: str) -> Callable[..., Any] | None:
149
+ return _tools_by_name.get(name)
150
+
151
+
152
+ def get_tool_names() -> list[str]:
153
+ return list(_tools_by_name.keys())
154
+
155
+
156
+ def needs_agent_state(tool_name: str) -> bool:
157
+ tool_func = get_tool_by_name(tool_name)
158
+ if not tool_func:
159
+ return False
160
+ sig = signature(tool_func)
161
+ return "agent_state" in sig.parameters
162
+
163
+
164
+ def should_execute_in_sandbox(tool_name: str) -> bool:
165
+ for tool in tools:
166
+ if tool.get("name") == tool_name:
167
+ return bool(tool.get("sandbox_execution", True))
168
+ return True
169
+
170
+
171
+ def get_tools_prompt() -> str:
172
+ tools_by_module: dict[str, list[dict[str, Any]]] = {}
173
+ for tool in tools:
174
+ module = tool.get("module", "unknown")
175
+ if module not in tools_by_module:
176
+ tools_by_module[module] = []
177
+ tools_by_module[module].append(tool)
178
+
179
+ xml_sections = []
180
+ for module, module_tools in sorted(tools_by_module.items()):
181
+ tag_name = f"{module}_tools"
182
+ section_parts = [f"<{tag_name}>"]
183
+ for tool in module_tools:
184
+ tool_xml = tool.get("xml_schema", "")
185
+ if tool_xml:
186
+ indented_tool = "\n".join(f" {line}" for line in tool_xml.split("\n"))
187
+ section_parts.append(indented_tool)
188
+ section_parts.append(f"</{tag_name}>")
189
+ xml_sections.append("\n".join(section_parts))
190
+
191
+ return "\n\n".join(xml_sections)
192
+
193
+
194
+ def clear_registry() -> None:
195
+ tools.clear()
196
+ _tools_by_name.clear()
@@ -0,0 +1,6 @@
1
+ from .reporting_actions import create_vulnerability_report
2
+
3
+
4
+ __all__ = [
5
+ "create_vulnerability_report",
6
+ ]