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,231 +0,0 @@
|
|
1
|
-
import contextlib
|
2
|
-
import os
|
3
|
-
import pty
|
4
|
-
import select
|
5
|
-
import signal
|
6
|
-
import subprocess
|
7
|
-
import threading
|
8
|
-
import time
|
9
|
-
from typing import Any
|
10
|
-
|
11
|
-
import pyte
|
12
|
-
|
13
|
-
|
14
|
-
MAX_TERMINAL_SNAPSHOT_LENGTH = 10_000
|
15
|
-
|
16
|
-
|
17
|
-
class TerminalInstance:
|
18
|
-
def __init__(self, terminal_id: str, initial_command: str | None = None) -> None:
|
19
|
-
self.terminal_id = terminal_id
|
20
|
-
self.process: subprocess.Popen[bytes] | None = None
|
21
|
-
self.master_fd: int | None = None
|
22
|
-
self.is_running = False
|
23
|
-
self._output_lock = threading.Lock()
|
24
|
-
self._reader_thread: threading.Thread | None = None
|
25
|
-
|
26
|
-
self.screen = pyte.HistoryScreen(80, 24, history=1000)
|
27
|
-
self.stream = pyte.ByteStream()
|
28
|
-
self.stream.attach(self.screen)
|
29
|
-
|
30
|
-
self._start_terminal(initial_command)
|
31
|
-
|
32
|
-
def _start_terminal(self, initial_command: str | None = None) -> None:
|
33
|
-
try:
|
34
|
-
self.master_fd, slave_fd = pty.openpty()
|
35
|
-
|
36
|
-
shell = "/bin/bash"
|
37
|
-
|
38
|
-
self.process = subprocess.Popen( # noqa: S603
|
39
|
-
[shell, "-i"],
|
40
|
-
stdin=slave_fd,
|
41
|
-
stdout=slave_fd,
|
42
|
-
stderr=slave_fd,
|
43
|
-
cwd="/workspace",
|
44
|
-
preexec_fn=os.setsid, # noqa: PLW1509 - Required for PTY functionality
|
45
|
-
)
|
46
|
-
|
47
|
-
os.close(slave_fd)
|
48
|
-
|
49
|
-
self.is_running = True
|
50
|
-
|
51
|
-
self._reader_thread = threading.Thread(target=self._read_output, daemon=True)
|
52
|
-
self._reader_thread.start()
|
53
|
-
|
54
|
-
time.sleep(0.5)
|
55
|
-
|
56
|
-
if initial_command:
|
57
|
-
self._write_to_terminal(initial_command)
|
58
|
-
|
59
|
-
except (OSError, ValueError) as e:
|
60
|
-
raise RuntimeError(f"Failed to start terminal: {e}") from e
|
61
|
-
|
62
|
-
def _read_output(self) -> None:
|
63
|
-
while self.is_running and self.master_fd:
|
64
|
-
try:
|
65
|
-
ready, _, _ = select.select([self.master_fd], [], [], 0.1)
|
66
|
-
if ready:
|
67
|
-
data = os.read(self.master_fd, 4096)
|
68
|
-
if data:
|
69
|
-
with self._output_lock, contextlib.suppress(TypeError):
|
70
|
-
self.stream.feed(data)
|
71
|
-
else:
|
72
|
-
break
|
73
|
-
except (OSError, ValueError):
|
74
|
-
break
|
75
|
-
|
76
|
-
def _write_to_terminal(self, data: str) -> None:
|
77
|
-
if self.master_fd and self.is_running:
|
78
|
-
try:
|
79
|
-
os.write(self.master_fd, data.encode("utf-8"))
|
80
|
-
except (OSError, ValueError) as e:
|
81
|
-
raise RuntimeError("Terminal is no longer available") from e
|
82
|
-
|
83
|
-
def send_input(self, inputs: list[str]) -> None:
|
84
|
-
if not self.is_running:
|
85
|
-
raise RuntimeError("Terminal is not running")
|
86
|
-
|
87
|
-
for i, input_item in enumerate(inputs):
|
88
|
-
if input_item.startswith("literal:"):
|
89
|
-
literal_text = input_item[8:]
|
90
|
-
self._write_to_terminal(literal_text)
|
91
|
-
else:
|
92
|
-
key_sequence = self._get_key_sequence(input_item)
|
93
|
-
if key_sequence:
|
94
|
-
self._write_to_terminal(key_sequence)
|
95
|
-
else:
|
96
|
-
self._write_to_terminal(input_item)
|
97
|
-
|
98
|
-
time.sleep(0.05)
|
99
|
-
|
100
|
-
if (
|
101
|
-
i < len(inputs) - 1
|
102
|
-
and not input_item.startswith("literal:")
|
103
|
-
and not self._is_special_key(input_item)
|
104
|
-
and not inputs[i + 1].startswith("literal:")
|
105
|
-
and not self._is_special_key(inputs[i + 1])
|
106
|
-
):
|
107
|
-
self._write_to_terminal(" ")
|
108
|
-
|
109
|
-
def get_snapshot(self) -> dict[str, Any]:
|
110
|
-
with self._output_lock:
|
111
|
-
history_lines = [
|
112
|
-
"".join(char.data for char in line_dict.values())
|
113
|
-
for line_dict in self.screen.history.top
|
114
|
-
]
|
115
|
-
|
116
|
-
current_lines = self.screen.display
|
117
|
-
|
118
|
-
all_lines = history_lines + current_lines
|
119
|
-
rendered_output = "\n".join(all_lines)
|
120
|
-
|
121
|
-
if len(rendered_output) > MAX_TERMINAL_SNAPSHOT_LENGTH:
|
122
|
-
rendered_output = rendered_output[-MAX_TERMINAL_SNAPSHOT_LENGTH:]
|
123
|
-
truncated = True
|
124
|
-
else:
|
125
|
-
truncated = False
|
126
|
-
|
127
|
-
return {
|
128
|
-
"terminal_id": self.terminal_id,
|
129
|
-
"snapshot": rendered_output,
|
130
|
-
"is_running": self.is_running,
|
131
|
-
"process_id": self.process.pid if self.process else None,
|
132
|
-
"truncated": truncated,
|
133
|
-
}
|
134
|
-
|
135
|
-
def wait(self, duration: float) -> dict[str, Any]:
|
136
|
-
time.sleep(duration)
|
137
|
-
return self.get_snapshot()
|
138
|
-
|
139
|
-
def close(self) -> None:
|
140
|
-
self.is_running = False
|
141
|
-
|
142
|
-
if self.process:
|
143
|
-
with contextlib.suppress(OSError, ProcessLookupError):
|
144
|
-
os.killpg(os.getpgid(self.process.pid), signal.SIGTERM)
|
145
|
-
|
146
|
-
try:
|
147
|
-
self.process.wait(timeout=2)
|
148
|
-
except subprocess.TimeoutExpired:
|
149
|
-
os.killpg(os.getpgid(self.process.pid), signal.SIGKILL)
|
150
|
-
self.process.wait()
|
151
|
-
|
152
|
-
if self.master_fd:
|
153
|
-
with contextlib.suppress(OSError):
|
154
|
-
os.close(self.master_fd)
|
155
|
-
self.master_fd = None
|
156
|
-
|
157
|
-
if self._reader_thread and self._reader_thread.is_alive():
|
158
|
-
self._reader_thread.join(timeout=1)
|
159
|
-
|
160
|
-
def _is_special_key(self, key: str) -> bool:
|
161
|
-
special_keys = {
|
162
|
-
"Enter",
|
163
|
-
"Space",
|
164
|
-
"Backspace",
|
165
|
-
"Tab",
|
166
|
-
"Escape",
|
167
|
-
"Up",
|
168
|
-
"Down",
|
169
|
-
"Left",
|
170
|
-
"Right",
|
171
|
-
"Home",
|
172
|
-
"End",
|
173
|
-
"PageUp",
|
174
|
-
"PageDown",
|
175
|
-
"Insert",
|
176
|
-
"Delete",
|
177
|
-
} | {f"F{i}" for i in range(1, 13)}
|
178
|
-
|
179
|
-
if key in special_keys:
|
180
|
-
return True
|
181
|
-
|
182
|
-
return bool(key.startswith(("^", "C-", "S-", "A-")))
|
183
|
-
|
184
|
-
def _get_key_sequence(self, key: str) -> str | None:
|
185
|
-
key_map = {
|
186
|
-
"Enter": "\r",
|
187
|
-
"Space": " ",
|
188
|
-
"Backspace": "\x08",
|
189
|
-
"Tab": "\t",
|
190
|
-
"Escape": "\x1b",
|
191
|
-
"Up": "\x1b[A",
|
192
|
-
"Down": "\x1b[B",
|
193
|
-
"Right": "\x1b[C",
|
194
|
-
"Left": "\x1b[D",
|
195
|
-
"Home": "\x1b[H",
|
196
|
-
"End": "\x1b[F",
|
197
|
-
"PageUp": "\x1b[5~",
|
198
|
-
"PageDown": "\x1b[6~",
|
199
|
-
"Insert": "\x1b[2~",
|
200
|
-
"Delete": "\x1b[3~",
|
201
|
-
"F1": "\x1b[11~",
|
202
|
-
"F2": "\x1b[12~",
|
203
|
-
"F3": "\x1b[13~",
|
204
|
-
"F4": "\x1b[14~",
|
205
|
-
"F5": "\x1b[15~",
|
206
|
-
"F6": "\x1b[17~",
|
207
|
-
"F7": "\x1b[18~",
|
208
|
-
"F8": "\x1b[19~",
|
209
|
-
"F9": "\x1b[20~",
|
210
|
-
"F10": "\x1b[21~",
|
211
|
-
"F11": "\x1b[23~",
|
212
|
-
"F12": "\x1b[24~",
|
213
|
-
}
|
214
|
-
|
215
|
-
if key in key_map:
|
216
|
-
return key_map[key]
|
217
|
-
|
218
|
-
if key.startswith("^") and len(key) == 2:
|
219
|
-
char = key[1].lower()
|
220
|
-
return chr(ord(char) - ord("a") + 1) if "a" <= char <= "z" else None
|
221
|
-
|
222
|
-
if key.startswith("C-") and len(key) == 3:
|
223
|
-
char = key[2].lower()
|
224
|
-
return chr(ord(char) - ord("a") + 1) if "a" <= char <= "z" else None
|
225
|
-
|
226
|
-
return None
|
227
|
-
|
228
|
-
def is_alive(self) -> bool:
|
229
|
-
if not self.process:
|
230
|
-
return False
|
231
|
-
return self.process.poll() is None
|
File without changes
|
File without changes
|
File without changes
|