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,63 @@
|
|
1
|
+
from typing import Any
|
2
|
+
|
3
|
+
from strix.tools.registry import register_tool
|
4
|
+
|
5
|
+
|
6
|
+
@register_tool(sandbox_execution=False)
|
7
|
+
def create_vulnerability_report(
|
8
|
+
title: str,
|
9
|
+
content: str,
|
10
|
+
severity: str,
|
11
|
+
) -> dict[str, Any]:
|
12
|
+
validation_error = None
|
13
|
+
if not title or not title.strip():
|
14
|
+
validation_error = "Title cannot be empty"
|
15
|
+
elif not content or not content.strip():
|
16
|
+
validation_error = "Content cannot be empty"
|
17
|
+
elif not severity or not severity.strip():
|
18
|
+
validation_error = "Severity cannot be empty"
|
19
|
+
else:
|
20
|
+
valid_severities = ["critical", "high", "medium", "low", "info"]
|
21
|
+
if severity.lower() not in valid_severities:
|
22
|
+
validation_error = (
|
23
|
+
f"Invalid severity '{severity}'. Must be one of: {', '.join(valid_severities)}"
|
24
|
+
)
|
25
|
+
|
26
|
+
if validation_error:
|
27
|
+
return {"success": False, "message": validation_error}
|
28
|
+
|
29
|
+
try:
|
30
|
+
from strix.cli.tracer import get_global_tracer
|
31
|
+
|
32
|
+
tracer = get_global_tracer()
|
33
|
+
if tracer:
|
34
|
+
report_id = tracer.add_vulnerability_report(
|
35
|
+
title=title,
|
36
|
+
content=content,
|
37
|
+
severity=severity,
|
38
|
+
)
|
39
|
+
|
40
|
+
return {
|
41
|
+
"success": True,
|
42
|
+
"message": f"Vulnerability report '{title}' created successfully",
|
43
|
+
"report_id": report_id,
|
44
|
+
"severity": severity.lower(),
|
45
|
+
}
|
46
|
+
import logging
|
47
|
+
|
48
|
+
logging.warning("Global tracer not available - vulnerability report not stored")
|
49
|
+
|
50
|
+
return { # noqa: TRY300
|
51
|
+
"success": True,
|
52
|
+
"message": f"Vulnerability report '{title}' created successfully (not persisted)",
|
53
|
+
"warning": "Report could not be persisted - tracer unavailable",
|
54
|
+
}
|
55
|
+
|
56
|
+
except ImportError:
|
57
|
+
return {
|
58
|
+
"success": True,
|
59
|
+
"message": f"Vulnerability report '{title}' created successfully (not persisted)",
|
60
|
+
"warning": "Report could not be persisted - tracer module unavailable",
|
61
|
+
}
|
62
|
+
except (ValueError, TypeError) as e:
|
63
|
+
return {"success": False, "message": f"Failed to create vulnerability report: {e!s}"}
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<tools>
|
2
|
+
<tool name="create_vulnerability_report">
|
3
|
+
<description>Create a vulnerability report for a discovered security issue.
|
4
|
+
|
5
|
+
Use this tool to document a specific verified security vulnerability.
|
6
|
+
Put ALL details in the content field - affected URLs, parameters, proof of concept, remediation steps, CVE references, CVSS scores, technical details, impact assessment, etc.
|
7
|
+
|
8
|
+
DO NOT USE:
|
9
|
+
- For general security observations without specific vulnerabilities
|
10
|
+
- When you don't have concrete vulnerability details
|
11
|
+
- When you don't have a proof of concept, or still not 100% sure if it's a vulnerability
|
12
|
+
- For tracking multiple vulnerabilities (create separate reports)
|
13
|
+
- For reporting multiple vulnerabilities at once. Use a separate create_vulnerability_report for each vulnerability.
|
14
|
+
</description>
|
15
|
+
<parameters>
|
16
|
+
<parameter name="title" type="string" required="true">
|
17
|
+
<description>Clear, concise title of the vulnerability</description>
|
18
|
+
</parameter>
|
19
|
+
<parameter name="content" type="string" required="true">
|
20
|
+
<description>Complete vulnerability details including affected URLs, technical details, impact, proof of concept, remediation steps, and any relevant references. Be comprehensive and include everything relevant.</description>
|
21
|
+
</parameter>
|
22
|
+
<parameter name="severity" type="string" required="true">
|
23
|
+
<description>Severity level: critical, high, medium, low, or info</description>
|
24
|
+
</parameter>
|
25
|
+
</parameters>
|
26
|
+
<returns type="Dict[str, Any]">
|
27
|
+
<description>Response containing success status and message</description>
|
28
|
+
</returns>
|
29
|
+
</tool>
|
30
|
+
</tools>
|
@@ -0,0 +1,53 @@
|
|
1
|
+
from typing import Any, Literal
|
2
|
+
|
3
|
+
from strix.tools.registry import register_tool
|
4
|
+
|
5
|
+
from .terminal_manager import get_terminal_manager
|
6
|
+
|
7
|
+
|
8
|
+
TerminalAction = Literal["new_terminal", "send_input", "wait", "close"]
|
9
|
+
|
10
|
+
|
11
|
+
@register_tool
|
12
|
+
def terminal_action(
|
13
|
+
action: TerminalAction,
|
14
|
+
inputs: list[str] | None = None,
|
15
|
+
time: float | None = None,
|
16
|
+
terminal_id: str | None = None,
|
17
|
+
) -> dict[str, Any]:
|
18
|
+
def _validate_inputs(action_name: str, inputs: list[str] | None) -> None:
|
19
|
+
if not inputs:
|
20
|
+
raise ValueError(f"inputs parameter is required for {action_name} action")
|
21
|
+
|
22
|
+
def _validate_time(time_param: float | None) -> None:
|
23
|
+
if time_param is None:
|
24
|
+
raise ValueError("time parameter is required for wait action")
|
25
|
+
|
26
|
+
def _validate_action(action_name: str) -> None:
|
27
|
+
raise ValueError(f"Unknown action: {action_name}")
|
28
|
+
|
29
|
+
manager = get_terminal_manager()
|
30
|
+
|
31
|
+
try:
|
32
|
+
match action:
|
33
|
+
case "new_terminal":
|
34
|
+
return manager.create_terminal(terminal_id, inputs)
|
35
|
+
|
36
|
+
case "send_input":
|
37
|
+
_validate_inputs(action, inputs)
|
38
|
+
assert inputs is not None
|
39
|
+
return manager.send_input(terminal_id, inputs)
|
40
|
+
|
41
|
+
case "wait":
|
42
|
+
_validate_time(time)
|
43
|
+
assert time is not None
|
44
|
+
return manager.wait_terminal(terminal_id, time)
|
45
|
+
|
46
|
+
case "close":
|
47
|
+
return manager.close_terminal(terminal_id)
|
48
|
+
|
49
|
+
case _:
|
50
|
+
_validate_action(action) # type: ignore[unreachable]
|
51
|
+
|
52
|
+
except (ValueError, RuntimeError) as e:
|
53
|
+
return {"error": str(e), "terminal_id": terminal_id, "snapshot": "", "is_running": False}
|
@@ -0,0 +1,114 @@
|
|
1
|
+
<tools>
|
2
|
+
<tool name="terminal_action">
|
3
|
+
<description>Perform terminal actions using a terminal emulator instance. Each terminal instance
|
4
|
+
is PERSISTENT and remains active until explicitly closed, allowing for multi-step
|
5
|
+
workflows and long-running processes.</description>
|
6
|
+
<parameters>
|
7
|
+
<parameter name="action" type="string" required="true">
|
8
|
+
<description>The terminal action to perform: - new_terminal: Create a new terminal instance. This MUST be the first action for each terminal tab. - send_input: Send keyboard input to the specified terminal. - wait: Pause execution for specified number of seconds. Can be also used to get the current terminal state (screenshot, output, etc.) after using other tools. - close: Close the specified terminal instance. This MUST be the final action for each terminal tab.</description>
|
9
|
+
</parameter>
|
10
|
+
<parameter name="inputs" type="string" required="false">
|
11
|
+
<description>Required for 'new_terminal' and 'send_input' actions: - List of inputs to send to terminal. Each element in the list MUST be one of the following: - Regular text: "hello", "world", etc. - Literal text (not interpreted as special keys): prefix with "literal:" e.g., "literal:Home", "literal:Escape", "literal:Enter" to send these as text - Enter - Space - Backspace - Escape: "Escape", "^[", "C-[" - Tab: "Tab" - Arrow keys: "Left", "Right", "Up", "Down" - Navigation: "Home", "End", "PageUp", "PageDown" - Function keys: "F1" through "F12" Modifier keys supported with prefixes: - ^ or C- : Control (e.g., "^c", "C-c") - S- : Shift (e.g., "S-F6") - A- : Alt (e.g., "A-Home") - Combined modifiers for arrows: "S-A-Up", "C-S-Left" - Inputs MUST in all cases be sent as a LIST of strings, even if you are only sending one input. - Sending Inputs as a single string will NOT work.</description>
|
12
|
+
</parameter>
|
13
|
+
<parameter name="time" type="string" required="false">
|
14
|
+
<description>Required for 'wait' action. Number of seconds to pause execution. Can be fractional (e.g., 0.5 for half a second).</description>
|
15
|
+
</parameter>
|
16
|
+
<parameter name="terminal_id" type="string" required="false">
|
17
|
+
<description>Identifier for the terminal instance. Required for all actions except the first 'new_terminal' action. Allows managing multiple concurrent terminal tabs. - For 'new_terminal': if not provided, a default terminal is created. If provided, creates a new terminal with that ID. - For other actions: specifies which terminal instance to operate on. - Default terminal ID is "default" if not specified.</description>
|
18
|
+
</parameter>
|
19
|
+
</parameters>
|
20
|
+
<returns type="Dict[str, Any]">
|
21
|
+
<description>Response containing: - snapshot: raw representation of current terminal state where you can see the output of the command - terminal_id: the ID of the terminal instance that was operated on</description>
|
22
|
+
</returns>
|
23
|
+
<notes>
|
24
|
+
Important usage rules:
|
25
|
+
1. PERSISTENCE: Terminal instances remain active and maintain their state (environment
|
26
|
+
variables, current directory, running processes) until explicitly closed with the
|
27
|
+
'close' action. This allows for multi-step workflows across multiple tool calls.
|
28
|
+
2. MULTIPLE TERMINALS: You can run multiple terminal instances concurrently by using
|
29
|
+
different terminal_id values. Each terminal operates independently.
|
30
|
+
3. Terminal interaction MUST begin with 'new_terminal' action for each terminal instance.
|
31
|
+
4. Only one action can be performed per call.
|
32
|
+
5. Input handling:
|
33
|
+
- Regular text is sent as-is
|
34
|
+
- Literal text: prefix with "literal:" to send special key names as literal text
|
35
|
+
- Special keys must match supported key names
|
36
|
+
- Modifier combinations follow specific syntax
|
37
|
+
- Control can be specified as ^ or C- prefix
|
38
|
+
- Shift (S-) works with special keys only
|
39
|
+
- Alt (A-) works with any character/key
|
40
|
+
6. Wait action:
|
41
|
+
- Time is specified in seconds
|
42
|
+
- Can be used to wait for command completion
|
43
|
+
- Can be fractional (e.g., 0.5 seconds)
|
44
|
+
- Snapshot and output are captured after the wait
|
45
|
+
- You should estimate the time it will take to run the command and set the wait time accordingly.
|
46
|
+
- It can be from a few seconds to a few minutes, choose wisely depending on the command you are running and the task.
|
47
|
+
7. The terminal can operate concurrently with other tools. You may invoke
|
48
|
+
browser, proxy, or other tools (in separate assistant messages) while maintaining
|
49
|
+
active terminal sessions.
|
50
|
+
8. You do not need to close terminals after you are done, but you can if you want to
|
51
|
+
free up resources.
|
52
|
+
9. You MUST end the inputs list with an "Enter" if you want to run the command, as
|
53
|
+
it is not sent automatically.
|
54
|
+
10. AUTOMATIC SPACING BEHAVIOR:
|
55
|
+
- Consecutive regular text inputs have spaces automatically added between them
|
56
|
+
- This is helpful for shell commands: ["ls", "-la"] becomes "ls -la"
|
57
|
+
- This causes problems for compound commands: [":", "w", "q"] becomes ": w q"
|
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.
|
66
|
+
</notes>
|
67
|
+
<examples>
|
68
|
+
# Create new terminal with Node.js (default terminal)
|
69
|
+
<function=terminal_action>
|
70
|
+
<parameter=action>new_terminal</parameter>
|
71
|
+
<parameter=inputs>["node", "Enter"]</parameter>
|
72
|
+
</function>
|
73
|
+
|
74
|
+
# Create a second (parallel) terminal instance for Python
|
75
|
+
<function=terminal_action>
|
76
|
+
<parameter=action>new_terminal</parameter>
|
77
|
+
<parameter=terminal_id>python_terminal</parameter>
|
78
|
+
<parameter=inputs>["python3", "Enter"]</parameter>
|
79
|
+
</function>
|
80
|
+
|
81
|
+
# Send command to the default terminal
|
82
|
+
<function=terminal_action>
|
83
|
+
<parameter=action>send_input</parameter>
|
84
|
+
<parameter=inputs>["require('crypto').randomBytes(1000000).toString('hex')",
|
85
|
+
"Enter"]</parameter>
|
86
|
+
</function>
|
87
|
+
|
88
|
+
# Wait for previous action on default terminal
|
89
|
+
<function=terminal_action>
|
90
|
+
<parameter=action>wait</parameter>
|
91
|
+
<parameter=time>2.0</parameter>
|
92
|
+
</function>
|
93
|
+
|
94
|
+
# Send multiple inputs with special keys to current terminal
|
95
|
+
<function=terminal_action>
|
96
|
+
<parameter=action>send_input</parameter>
|
97
|
+
<parameter=inputs>["sqlmap -u 'http://example.com/page.php?id=1' --batch", "Enter", "y",
|
98
|
+
"Enter", "n", "Enter", "n", "Enter"]</parameter>
|
99
|
+
</function>
|
100
|
+
|
101
|
+
# WRONG: Vim command with automatic spacing (becomes ": w q")
|
102
|
+
<function=terminal_action>
|
103
|
+
<parameter=action>send_input</parameter>
|
104
|
+
<parameter=inputs>[":", "w", "q", "Enter"]</parameter>
|
105
|
+
</function>
|
106
|
+
|
107
|
+
# CORRECT: Vim command using literal prefix (becomes ":wq")
|
108
|
+
<function=terminal_action>
|
109
|
+
<parameter=action>send_input</parameter>
|
110
|
+
<parameter=inputs>[":", "literal:wq", "Enter"]</parameter>
|
111
|
+
</function>
|
112
|
+
</examples>
|
113
|
+
</tool>
|
114
|
+
</tools>
|
@@ -0,0 +1,231 @@
|
|
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
|