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.
- strix/__init__.py +0 -0
- strix/agents/StrixAgent/__init__.py +4 -0
- strix/agents/StrixAgent/strix_agent.py +60 -0
- strix/agents/StrixAgent/system_prompt.jinja +504 -0
- strix/agents/__init__.py +10 -0
- strix/agents/base_agent.py +394 -0
- strix/agents/state.py +139 -0
- strix/cli/__init__.py +4 -0
- strix/cli/app.py +1124 -0
- strix/cli/assets/cli.tcss +680 -0
- strix/cli/main.py +542 -0
- strix/cli/tool_components/__init__.py +39 -0
- strix/cli/tool_components/agents_graph_renderer.py +129 -0
- strix/cli/tool_components/base_renderer.py +61 -0
- strix/cli/tool_components/browser_renderer.py +107 -0
- strix/cli/tool_components/file_edit_renderer.py +95 -0
- strix/cli/tool_components/finish_renderer.py +32 -0
- strix/cli/tool_components/notes_renderer.py +108 -0
- strix/cli/tool_components/proxy_renderer.py +255 -0
- strix/cli/tool_components/python_renderer.py +34 -0
- strix/cli/tool_components/registry.py +72 -0
- strix/cli/tool_components/reporting_renderer.py +53 -0
- strix/cli/tool_components/scan_info_renderer.py +58 -0
- strix/cli/tool_components/terminal_renderer.py +99 -0
- strix/cli/tool_components/thinking_renderer.py +29 -0
- strix/cli/tool_components/user_message_renderer.py +43 -0
- strix/cli/tool_components/web_search_renderer.py +28 -0
- strix/cli/tracer.py +308 -0
- strix/llm/__init__.py +14 -0
- strix/llm/config.py +19 -0
- strix/llm/llm.py +310 -0
- strix/llm/memory_compressor.py +206 -0
- strix/llm/request_queue.py +63 -0
- strix/llm/utils.py +84 -0
- strix/prompts/__init__.py +113 -0
- strix/prompts/coordination/root_agent.jinja +41 -0
- strix/prompts/vulnerabilities/authentication_jwt.jinja +129 -0
- strix/prompts/vulnerabilities/business_logic.jinja +143 -0
- strix/prompts/vulnerabilities/csrf.jinja +168 -0
- strix/prompts/vulnerabilities/idor.jinja +164 -0
- strix/prompts/vulnerabilities/race_conditions.jinja +194 -0
- strix/prompts/vulnerabilities/rce.jinja +222 -0
- strix/prompts/vulnerabilities/sql_injection.jinja +216 -0
- strix/prompts/vulnerabilities/ssrf.jinja +168 -0
- strix/prompts/vulnerabilities/xss.jinja +221 -0
- strix/prompts/vulnerabilities/xxe.jinja +276 -0
- strix/runtime/__init__.py +19 -0
- strix/runtime/docker_runtime.py +298 -0
- strix/runtime/runtime.py +25 -0
- strix/runtime/tool_server.py +97 -0
- strix/tools/__init__.py +64 -0
- strix/tools/agents_graph/__init__.py +16 -0
- strix/tools/agents_graph/agents_graph_actions.py +610 -0
- strix/tools/agents_graph/agents_graph_actions_schema.xml +223 -0
- strix/tools/argument_parser.py +120 -0
- strix/tools/browser/__init__.py +4 -0
- strix/tools/browser/browser_actions.py +236 -0
- strix/tools/browser/browser_actions_schema.xml +183 -0
- strix/tools/browser/browser_instance.py +533 -0
- strix/tools/browser/tab_manager.py +342 -0
- strix/tools/executor.py +302 -0
- strix/tools/file_edit/__init__.py +4 -0
- strix/tools/file_edit/file_edit_actions.py +141 -0
- strix/tools/file_edit/file_edit_actions_schema.xml +128 -0
- strix/tools/finish/__init__.py +4 -0
- strix/tools/finish/finish_actions.py +167 -0
- strix/tools/finish/finish_actions_schema.xml +45 -0
- strix/tools/notes/__init__.py +14 -0
- strix/tools/notes/notes_actions.py +191 -0
- strix/tools/notes/notes_actions_schema.xml +150 -0
- strix/tools/proxy/__init__.py +20 -0
- strix/tools/proxy/proxy_actions.py +101 -0
- strix/tools/proxy/proxy_actions_schema.xml +267 -0
- strix/tools/proxy/proxy_manager.py +785 -0
- strix/tools/python/__init__.py +4 -0
- strix/tools/python/python_actions.py +47 -0
- strix/tools/python/python_actions_schema.xml +131 -0
- strix/tools/python/python_instance.py +172 -0
- strix/tools/python/python_manager.py +131 -0
- strix/tools/registry.py +196 -0
- strix/tools/reporting/__init__.py +6 -0
- strix/tools/reporting/reporting_actions.py +63 -0
- strix/tools/reporting/reporting_actions_schema.xml +30 -0
- strix/tools/terminal/__init__.py +4 -0
- strix/tools/terminal/terminal_actions.py +53 -0
- strix/tools/terminal/terminal_actions_schema.xml +114 -0
- strix/tools/terminal/terminal_instance.py +231 -0
- strix/tools/terminal/terminal_manager.py +191 -0
- strix/tools/thinking/__init__.py +4 -0
- strix/tools/thinking/thinking_actions.py +18 -0
- strix/tools/thinking/thinking_actions_schema.xml +52 -0
- strix/tools/web_search/__init__.py +4 -0
- strix/tools/web_search/web_search_actions.py +80 -0
- strix/tools/web_search/web_search_actions_schema.xml +83 -0
- strix_agent-0.1.1.dist-info/LICENSE +201 -0
- strix_agent-0.1.1.dist-info/METADATA +200 -0
- strix_agent-0.1.1.dist-info/RECORD +99 -0
- strix_agent-0.1.1.dist-info/WHEEL +4 -0
- strix_agent-0.1.1.dist-info/entry_points.txt +3 -0
@@ -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
|
strix/tools/registry.py
ADDED
@@ -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()
|