strix-agent 0.1.8__py3-none-any.whl → 0.1.10__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/agents/StrixAgent/strix_agent.py +18 -6
- strix/agents/StrixAgent/system_prompt.jinja +29 -203
- strix/agents/base_agent.py +3 -0
- strix/cli/app.py +3 -1
- strix/cli/main.py +95 -8
- strix/cli/tool_components/terminal_renderer.py +92 -60
- strix/llm/config.py +1 -1
- strix/llm/llm.py +66 -2
- strix/llm/memory_compressor.py +1 -1
- strix/prompts/__init__.py +9 -13
- strix/prompts/vulnerabilities/authentication_jwt.jinja +7 -7
- strix/prompts/vulnerabilities/csrf.jinja +1 -1
- strix/prompts/vulnerabilities/idor.jinja +3 -3
- strix/prompts/vulnerabilities/rce.jinja +1 -1
- strix/prompts/vulnerabilities/sql_injection.jinja +3 -3
- strix/prompts/vulnerabilities/xss.jinja +3 -3
- strix/prompts/vulnerabilities/xxe.jinja +1 -1
- strix/runtime/docker_runtime.py +204 -160
- strix/runtime/runtime.py +3 -2
- strix/runtime/tool_server.py +136 -28
- strix/tools/agents_graph/agents_graph_actions.py +4 -10
- strix/tools/agents_graph/agents_graph_actions_schema.xml +18 -12
- strix/tools/argument_parser.py +2 -1
- strix/tools/executor.py +3 -0
- strix/tools/terminal/__init__.py +2 -2
- strix/tools/terminal/terminal_actions.py +22 -40
- strix/tools/terminal/terminal_actions_schema.xml +113 -84
- strix/tools/terminal/terminal_manager.py +83 -123
- strix/tools/terminal/terminal_session.py +447 -0
- {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/METADATA +6 -4
- {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/RECORD +34 -34
- strix/tools/terminal/terminal_instance.py +0 -231
- {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/LICENSE +0 -0
- {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/WHEEL +0 -0
- {strix_agent-0.1.8.dist-info → strix_agent-0.1.10.dist-info}/entry_points.txt +0 -0
@@ -1,113 +1,142 @@
|
|
1
1
|
<tools>
|
2
|
-
<tool name="
|
3
|
-
<description>
|
4
|
-
is PERSISTENT and remains active until explicitly closed, allowing for multi-step
|
5
|
-
workflows and long-running processes.</description>
|
2
|
+
<tool name="terminal_execute">
|
3
|
+
<description>Execute a bash command in a persistent terminal session. The terminal maintains state (environment variables, current directory, running processes) between commands.</description>
|
6
4
|
<parameters>
|
7
|
-
<parameter name="
|
8
|
-
<description>The
|
5
|
+
<parameter name="command" type="string" required="true">
|
6
|
+
<description>The bash command to execute. Can be empty to check output of running commands (will wait for timeout period to collect output).
|
7
|
+
|
8
|
+
Supported special keys and sequences (based on official tmux key names):
|
9
|
+
- Control sequences: C-c, C-d, C-z, C-a, C-e, C-k, C-l, C-u, C-w, etc. (also ^c, ^d, etc.)
|
10
|
+
- Navigation keys: Up, Down, Left, Right, Home, End
|
11
|
+
- Page keys: PageUp, PageDown, PgUp, PgDn, PPage, NPage
|
12
|
+
- Function keys: F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12
|
13
|
+
- Special keys: Enter, Escape, Space, Tab, BTab, BSpace, DC, IC
|
14
|
+
- Note: Use official tmux names (BSpace not Backspace, DC not Delete, IC not Insert, Escape not Esc)
|
15
|
+
- Meta/Alt sequences: M-key (e.g., M-f, M-b) - tmux official modifier
|
16
|
+
- Shift sequences: S-key (e.g., S-F6, S-Tab, S-Left)
|
17
|
+
- Combined modifiers: C-S-key, C-M-key, S-M-key, etc.
|
18
|
+
|
19
|
+
Special keys work automatically - no need to set is_input=true for keys like C-c, C-d, etc.
|
20
|
+
These are useful for interacting with vim, emacs, REPLs, and other interactive applications.</description>
|
9
21
|
</parameter>
|
10
|
-
<parameter name="
|
11
|
-
<description>
|
22
|
+
<parameter name="is_input" type="boolean" required="false">
|
23
|
+
<description>If true, the command is sent as input to a currently running process. If false (default), the command is executed as a new bash command.
|
24
|
+
Note: Special keys (C-c, C-d, etc.) automatically work when a process is running - you don't need to set is_input=true for them.
|
25
|
+
Use is_input=true for regular text input to running processes.</description>
|
12
26
|
</parameter>
|
13
|
-
<parameter name="
|
14
|
-
<description>
|
27
|
+
<parameter name="timeout" type="number" required="false">
|
28
|
+
<description>Optional timeout in seconds for command execution. If not provided, uses default timeout behavior. Set to higher values for long-running commands like installations or tests. Default is 10 seconds.</description>
|
15
29
|
</parameter>
|
16
30
|
<parameter name="terminal_id" type="string" required="false">
|
17
|
-
<description>Identifier for the terminal
|
31
|
+
<description>Identifier for the terminal session. Defaults to "default". Use different IDs to manage multiple concurrent terminal sessions.</description>
|
32
|
+
</parameter>
|
33
|
+
<parameter name="no_enter" type="boolean" required="false">
|
34
|
+
<description>If true, don't automatically add Enter/newline after the command. Useful for:
|
35
|
+
- Interactive prompts where you want to send keys without submitting
|
36
|
+
- Navigation keys in full-screen applications
|
37
|
+
|
38
|
+
Examples:
|
39
|
+
- terminal_execute("gg", is_input=true, no_enter=true) # Vim: go to top
|
40
|
+
- terminal_execute("5j", is_input=true, no_enter=true) # Vim: move down 5 lines
|
41
|
+
- terminal_execute("i", is_input=true, no_enter=true) # Vim: insert mode</description>
|
18
42
|
</parameter>
|
19
43
|
</parameters>
|
20
44
|
<returns type="Dict[str, Any]">
|
21
|
-
<description>Response containing:
|
45
|
+
<description>Response containing:
|
46
|
+
- content: Command output
|
47
|
+
- exit_code: Exit code of the command (only for completed commands)
|
48
|
+
- command: The executed command
|
49
|
+
- terminal_id: The terminal session ID
|
50
|
+
- status: Command status ('completed' or 'running')
|
51
|
+
- working_dir: Current working directory after command execution</description>
|
22
52
|
</returns>
|
23
53
|
<notes>
|
24
54
|
Important usage rules:
|
25
|
-
1.
|
26
|
-
|
27
|
-
|
28
|
-
2.
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
-
|
34
|
-
-
|
35
|
-
-
|
36
|
-
|
37
|
-
|
38
|
-
-
|
39
|
-
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
- Use "literal:" prefix to bypass spacing: [":", "literal:wq"] becomes ":wq"
|
59
|
-
- Special keys (Enter, Space, etc.) and literal strings never trigger spacing
|
60
|
-
11. WHEN TO USE LITERAL PREFIX:
|
61
|
-
- Vim commands: [":", "literal:wq", "Enter"] instead of [":", "w", "q", "Enter"]
|
62
|
-
- Any sequence where exact character positioning matters
|
63
|
-
- When you need multiple characters sent as a single unit
|
64
|
-
12. Do NOT use terminal actions for file editing or writing. Use the replace_in_file,
|
65
|
-
write_to_file, or read_file tools instead.
|
55
|
+
1. PERSISTENT SESSION: The terminal maintains state between commands. Environment variables,
|
56
|
+
current directory, and running processes persist across multiple tool calls.
|
57
|
+
|
58
|
+
2. COMMAND EXECUTION: Execute one command at a time. For multiple commands, chain them with
|
59
|
+
&& or ; operators, or make separate tool calls.
|
60
|
+
|
61
|
+
3. LONG-RUNNING COMMANDS:
|
62
|
+
- Commands never get killed automatically - they keep running in background
|
63
|
+
- Set timeout to control how long to wait for output before returning
|
64
|
+
- Use empty command "" to check progress (waits for timeout period to collect output)
|
65
|
+
- Use C-c, C-d, C-z to interrupt processes (works automatically, no is_input needed)
|
66
|
+
|
67
|
+
4. TIMEOUT HANDLING:
|
68
|
+
- Timeout controls how long to wait before returning current output
|
69
|
+
- Commands are NEVER killed on timeout - they keep running
|
70
|
+
- After timeout, you can run new commands or check progress with empty command
|
71
|
+
- All commands return status "completed" - you have full control
|
72
|
+
|
73
|
+
5. MULTIPLE TERMINALS: Use different terminal_id values to run multiple concurrent sessions.
|
74
|
+
|
75
|
+
6. INTERACTIVE PROCESSES:
|
76
|
+
- Special keys (C-c, C-d, etc.) work automatically when a process is running
|
77
|
+
- Use is_input=true for regular text input to running processes like:
|
78
|
+
* Interactive shells, REPLs, or prompts
|
79
|
+
* Long-running applications waiting for input
|
80
|
+
* Background processes that need interaction
|
81
|
+
- Use no_enter=true for stuff like Vim navigation, password typing, or multi-step commands
|
82
|
+
|
83
|
+
7. WORKING DIRECTORY: The terminal tracks and returns the current working directory.
|
84
|
+
Use absolute paths or cd commands to change directories as needed.
|
85
|
+
|
86
|
+
8. OUTPUT HANDLING: Large outputs are automatically truncated. The tool provides
|
87
|
+
the most relevant parts of the output for analysis.
|
66
88
|
</notes>
|
67
89
|
<examples>
|
68
|
-
#
|
69
|
-
<function=
|
70
|
-
<parameter=
|
71
|
-
|
90
|
+
# Execute a simple command
|
91
|
+
<function=terminal_execute>
|
92
|
+
<parameter=command>ls -la</parameter>
|
93
|
+
</function>
|
94
|
+
|
95
|
+
# Run a command with custom timeout
|
96
|
+
<function=terminal_execute>
|
97
|
+
<parameter=command>npm install</parameter>
|
98
|
+
<parameter=timeout>120</parameter>
|
99
|
+
</function>
|
100
|
+
|
101
|
+
# Check progress of running command (waits for timeout to collect output)
|
102
|
+
<function=terminal_execute>
|
103
|
+
<parameter=command></parameter>
|
104
|
+
<parameter=timeout>5</parameter>
|
72
105
|
</function>
|
73
106
|
|
74
|
-
#
|
75
|
-
<function=
|
76
|
-
<parameter=
|
77
|
-
<parameter=terminal_id>python_terminal</parameter>
|
78
|
-
<parameter=inputs>["python3", "Enter"]</parameter>
|
107
|
+
# Start a background service
|
108
|
+
<function=terminal_execute>
|
109
|
+
<parameter=command>python app.py > server.log 2>&1 &</parameter>
|
79
110
|
</function>
|
80
111
|
|
81
|
-
#
|
82
|
-
<function=
|
83
|
-
<parameter=
|
84
|
-
<parameter=
|
85
|
-
"Enter"]</parameter>
|
112
|
+
# Interact with a running process
|
113
|
+
<function=terminal_execute>
|
114
|
+
<parameter=command>y</parameter>
|
115
|
+
<parameter=is_input>true</parameter>
|
86
116
|
</function>
|
87
117
|
|
88
|
-
#
|
89
|
-
<function=
|
90
|
-
<parameter=
|
91
|
-
<parameter=time>2.0</parameter>
|
118
|
+
# Interrupt a running process (special keys work automatically)
|
119
|
+
<function=terminal_execute>
|
120
|
+
<parameter=command>C-c</parameter>
|
92
121
|
</function>
|
93
122
|
|
94
|
-
# Send
|
95
|
-
<function=
|
96
|
-
<parameter=
|
97
|
-
<parameter=
|
98
|
-
"Enter", "n", "Enter", "n", "Enter"]</parameter>
|
123
|
+
# Send Escape key (use official tmux name)
|
124
|
+
<function=terminal_execute>
|
125
|
+
<parameter=command>Escape</parameter>
|
126
|
+
<parameter=is_input>true</parameter>
|
99
127
|
</function>
|
100
128
|
|
101
|
-
#
|
102
|
-
<function=
|
103
|
-
<parameter=
|
104
|
-
<parameter=
|
129
|
+
# Use a different terminal session
|
130
|
+
<function=terminal_execute>
|
131
|
+
<parameter=command>python3</parameter>
|
132
|
+
<parameter=terminal_id>python_session</parameter>
|
105
133
|
</function>
|
106
134
|
|
107
|
-
#
|
108
|
-
<function=
|
109
|
-
<parameter=
|
110
|
-
<parameter=
|
135
|
+
# Send input to Python REPL in specific session
|
136
|
+
<function=terminal_execute>
|
137
|
+
<parameter=command>print("Hello World")</parameter>
|
138
|
+
<parameter=is_input>true</parameter>
|
139
|
+
<parameter=terminal_id>python_session</parameter>
|
111
140
|
</function>
|
112
141
|
</examples>
|
113
142
|
</tool>
|
@@ -5,173 +5,133 @@ import sys
|
|
5
5
|
import threading
|
6
6
|
from typing import Any
|
7
7
|
|
8
|
-
from .
|
8
|
+
from .terminal_session import TerminalSession
|
9
9
|
|
10
10
|
|
11
11
|
class TerminalManager:
|
12
12
|
def __init__(self) -> None:
|
13
|
-
self.
|
13
|
+
self.sessions: dict[str, TerminalSession] = {}
|
14
14
|
self._lock = threading.Lock()
|
15
15
|
self.default_terminal_id = "default"
|
16
|
+
self.default_timeout = 30.0
|
16
17
|
|
17
18
|
self._register_cleanup_handlers()
|
18
19
|
|
19
|
-
def
|
20
|
-
self,
|
20
|
+
def execute_command(
|
21
|
+
self,
|
22
|
+
command: str,
|
23
|
+
is_input: bool = False,
|
24
|
+
timeout: float | None = None,
|
25
|
+
terminal_id: str | None = None,
|
26
|
+
no_enter: bool = False,
|
21
27
|
) -> dict[str, Any]:
|
22
28
|
if terminal_id is None:
|
23
29
|
terminal_id = self.default_terminal_id
|
24
30
|
|
25
|
-
|
26
|
-
if terminal_id in self.terminals:
|
27
|
-
raise ValueError(f"Terminal '{terminal_id}' already exists")
|
28
|
-
|
29
|
-
initial_command = None
|
30
|
-
if inputs:
|
31
|
-
command_parts: list[str] = []
|
32
|
-
for input_item in inputs:
|
33
|
-
if input_item == "Enter":
|
34
|
-
initial_command = " ".join(command_parts) + "\n"
|
35
|
-
break
|
36
|
-
if input_item.startswith("literal:"):
|
37
|
-
command_parts.append(input_item[8:])
|
38
|
-
elif input_item not in [
|
39
|
-
"Space",
|
40
|
-
"Tab",
|
41
|
-
"Backspace",
|
42
|
-
]:
|
43
|
-
command_parts.append(input_item)
|
44
|
-
|
45
|
-
try:
|
46
|
-
terminal = TerminalInstance(terminal_id, initial_command)
|
47
|
-
self.terminals[terminal_id] = terminal
|
48
|
-
|
49
|
-
if inputs and not initial_command:
|
50
|
-
terminal.send_input(inputs)
|
51
|
-
result = terminal.wait(2.0)
|
52
|
-
else:
|
53
|
-
result = terminal.wait(1.0)
|
54
|
-
|
55
|
-
result["message"] = f"Terminal '{terminal_id}' created successfully"
|
56
|
-
|
57
|
-
except (OSError, ValueError, RuntimeError) as e:
|
58
|
-
raise RuntimeError(f"Failed to create terminal '{terminal_id}': {e}") from e
|
59
|
-
else:
|
60
|
-
return result
|
61
|
-
|
62
|
-
def send_input(
|
63
|
-
self, terminal_id: str | None = None, inputs: list[str] | None = None
|
64
|
-
) -> dict[str, Any]:
|
65
|
-
if terminal_id is None:
|
66
|
-
terminal_id = self.default_terminal_id
|
67
|
-
|
68
|
-
if not inputs:
|
69
|
-
raise ValueError("No inputs provided")
|
70
|
-
|
71
|
-
with self._lock:
|
72
|
-
if terminal_id not in self.terminals:
|
73
|
-
raise ValueError(f"Terminal '{terminal_id}' not found")
|
74
|
-
|
75
|
-
terminal = self.terminals[terminal_id]
|
31
|
+
session = self._get_or_create_session(terminal_id)
|
76
32
|
|
77
33
|
try:
|
78
|
-
|
79
|
-
result = terminal.wait(2.0)
|
80
|
-
result["message"] = f"Input sent to terminal '{terminal_id}'"
|
81
|
-
except (OSError, ValueError, RuntimeError) as e:
|
82
|
-
raise RuntimeError(f"Failed to send input to terminal '{terminal_id}': {e}") from e
|
83
|
-
else:
|
84
|
-
return result
|
34
|
+
result = session.execute(command, is_input, timeout or self.default_timeout, no_enter)
|
85
35
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
raise ValueError(f"Terminal '{terminal_id}' not found")
|
36
|
+
return {
|
37
|
+
"content": result["content"],
|
38
|
+
"command": command,
|
39
|
+
"terminal_id": terminal_id,
|
40
|
+
"status": result["status"],
|
41
|
+
"exit_code": result.get("exit_code"),
|
42
|
+
"working_dir": result.get("working_dir"),
|
43
|
+
}
|
95
44
|
|
96
|
-
|
45
|
+
except RuntimeError as e:
|
46
|
+
return {
|
47
|
+
"error": str(e),
|
48
|
+
"command": command,
|
49
|
+
"terminal_id": terminal_id,
|
50
|
+
"content": "",
|
51
|
+
"status": "error",
|
52
|
+
"exit_code": None,
|
53
|
+
"working_dir": None,
|
54
|
+
}
|
55
|
+
except OSError as e:
|
56
|
+
return {
|
57
|
+
"error": f"System error: {e}",
|
58
|
+
"command": command,
|
59
|
+
"terminal_id": terminal_id,
|
60
|
+
"content": "",
|
61
|
+
"status": "error",
|
62
|
+
"exit_code": None,
|
63
|
+
"working_dir": None,
|
64
|
+
}
|
97
65
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
else:
|
104
|
-
return result
|
66
|
+
def _get_or_create_session(self, terminal_id: str) -> TerminalSession:
|
67
|
+
with self._lock:
|
68
|
+
if terminal_id not in self.sessions:
|
69
|
+
self.sessions[terminal_id] = TerminalSession(terminal_id)
|
70
|
+
return self.sessions[terminal_id]
|
105
71
|
|
106
|
-
def
|
72
|
+
def close_session(self, terminal_id: str | None = None) -> dict[str, Any]:
|
107
73
|
if terminal_id is None:
|
108
74
|
terminal_id = self.default_terminal_id
|
109
75
|
|
110
76
|
with self._lock:
|
111
|
-
if terminal_id not in self.
|
112
|
-
|
77
|
+
if terminal_id not in self.sessions:
|
78
|
+
return {
|
79
|
+
"terminal_id": terminal_id,
|
80
|
+
"message": f"Terminal '{terminal_id}' not found",
|
81
|
+
"status": "not_found",
|
82
|
+
}
|
113
83
|
|
114
|
-
|
84
|
+
session = self.sessions.pop(terminal_id)
|
115
85
|
|
116
86
|
try:
|
117
|
-
|
118
|
-
except (
|
119
|
-
|
87
|
+
session.close()
|
88
|
+
except (RuntimeError, OSError) as e:
|
89
|
+
return {
|
90
|
+
"terminal_id": terminal_id,
|
91
|
+
"error": f"Failed to close terminal '{terminal_id}': {e}",
|
92
|
+
"status": "error",
|
93
|
+
}
|
120
94
|
else:
|
121
95
|
return {
|
122
96
|
"terminal_id": terminal_id,
|
123
97
|
"message": f"Terminal '{terminal_id}' closed successfully",
|
124
|
-
"
|
125
|
-
"is_running": False,
|
98
|
+
"status": "closed",
|
126
99
|
}
|
127
100
|
|
128
|
-
def
|
129
|
-
if terminal_id is None:
|
130
|
-
terminal_id = self.default_terminal_id
|
131
|
-
|
132
|
-
with self._lock:
|
133
|
-
if terminal_id not in self.terminals:
|
134
|
-
raise ValueError(f"Terminal '{terminal_id}' not found")
|
135
|
-
|
136
|
-
terminal = self.terminals[terminal_id]
|
137
|
-
|
138
|
-
return terminal.get_snapshot()
|
139
|
-
|
140
|
-
def list_terminals(self) -> dict[str, Any]:
|
101
|
+
def list_sessions(self) -> dict[str, Any]:
|
141
102
|
with self._lock:
|
142
|
-
|
143
|
-
for tid,
|
144
|
-
|
145
|
-
"is_running":
|
146
|
-
"
|
147
|
-
"process_id": terminal.process.pid if terminal.process else None,
|
103
|
+
session_info: dict[str, dict[str, Any]] = {}
|
104
|
+
for tid, session in self.sessions.items():
|
105
|
+
session_info[tid] = {
|
106
|
+
"is_running": session.is_running(),
|
107
|
+
"working_dir": session.get_working_dir(),
|
148
108
|
}
|
149
109
|
|
150
|
-
return {"
|
110
|
+
return {"sessions": session_info, "total_count": len(session_info)}
|
151
111
|
|
152
|
-
def
|
112
|
+
def cleanup_dead_sessions(self) -> None:
|
153
113
|
with self._lock:
|
154
|
-
|
155
|
-
for tid,
|
156
|
-
if not
|
157
|
-
|
114
|
+
dead_sessions: list[str] = []
|
115
|
+
for tid, session in self.sessions.items():
|
116
|
+
if not session.is_running():
|
117
|
+
dead_sessions.append(tid)
|
158
118
|
|
159
|
-
for tid in
|
160
|
-
|
119
|
+
for tid in dead_sessions:
|
120
|
+
session = self.sessions.pop(tid)
|
161
121
|
with contextlib.suppress(Exception):
|
162
|
-
|
122
|
+
session.close()
|
163
123
|
|
164
|
-
def
|
124
|
+
def close_all_sessions(self) -> None:
|
165
125
|
with self._lock:
|
166
|
-
|
167
|
-
self.
|
126
|
+
sessions_to_close = list(self.sessions.values())
|
127
|
+
self.sessions.clear()
|
168
128
|
|
169
|
-
for
|
129
|
+
for session in sessions_to_close:
|
170
130
|
with contextlib.suppress(Exception):
|
171
|
-
|
131
|
+
session.close()
|
172
132
|
|
173
133
|
def _register_cleanup_handlers(self) -> None:
|
174
|
-
atexit.register(self.
|
134
|
+
atexit.register(self.close_all_sessions)
|
175
135
|
|
176
136
|
signal.signal(signal.SIGTERM, self._signal_handler)
|
177
137
|
signal.signal(signal.SIGINT, self._signal_handler)
|
@@ -180,7 +140,7 @@ class TerminalManager:
|
|
180
140
|
signal.signal(signal.SIGHUP, self._signal_handler)
|
181
141
|
|
182
142
|
def _signal_handler(self, _signum: int, _frame: Any) -> None:
|
183
|
-
self.
|
143
|
+
self.close_all_sessions()
|
184
144
|
sys.exit(0)
|
185
145
|
|
186
146
|
|