refactor: Extract code execution tool to plugin
linuztx committed
Mar 8, 2026 at 09:52 UTC
63651deb9ec921c8b9327fe4ea6394c0764bcb91
27 files changed
+416
-181
agent.py
-5
@@ -319,11 +319,6 @@ class AgentConfig:
319
browser_http_headers: dict[str, str] = field(
320
default_factory=dict
321
) # Custom HTTP headers for browser requests
322
- code_exec_ssh_enabled: bool = True
323
- code_exec_ssh_addr: str = "localhost"
324
- code_exec_ssh_port: int = 55022
325
- code_exec_ssh_user: str = "root"
326
- code_exec_ssh_pass: str = ""
322
additional: Dict[str, Any] = field(default_factory=dict)
323
324
docker/run/fs/exe/run_A0.sh
-5
@@ -11,8 +11,3 @@ exec python /a0/run_ui.py \
11
--dockerized=true \
12
--port=80 \
13
--host="0.0.0.0"
14
- # --code_exec_ssh_enabled=true \
15
- # --code_exec_ssh_addr="localhost" \
16
- # --code_exec_ssh_port=22 \
17
- # --code_exec_ssh_user="root" \
18
- # --code_exec_ssh_pass="toor"
docker/run/fs/exe/run_tunnel_api.sh
+1
-7
@@ -15,10 +15,4 @@ exec python /a0/run_tunnel.py \
15
--dockerized=true \
16
--port=80 \
17
--tunnel_api_port=55520 \
18
- --host="0.0.0.0" \
19
- --code_exec_docker_enabled=false \
20
- --code_exec_ssh_enabled=true \
21
- # --code_exec_ssh_addr="localhost" \
22
- # --code_exec_ssh_port=22 \
23
- # --code_exec_ssh_user="root" \
24
- # --code_exec_ssh_pass="toor"
18
+ --host="0.0.0.0"
helpers/settings.py
+3
-28
@@ -112,9 +112,7 @@ class Settings(TypedDict):
112
rfc_url: str
113
rfc_password: str
114
rfc_port_http: int
115
- rfc_port_ssh: int
115
117
- shell_interface: Literal['local','ssh']
116
websocket_server_restart_enabled: bool
117
uvicorn_access_logs_enabled: bool
118
@@ -189,7 +187,6 @@ class ModelProvider(ProvidersFO):
187
class SettingsOutputAdditional(TypedDict):
188
chat_providers: list[ModelProvider]
189
embedding_providers: list[ModelProvider]
192
- shell_interfaces: list[FieldOption]
190
agent_subdirs: list[FieldOption]
191
knowledge_subdirs: list[FieldOption]
192
stt_models: list[FieldOption]
@@ -231,7 +228,6 @@ def convert_out(settings: Settings) -> SettingsOutput:
228
additional = SettingsOutputAdditional(
229
chat_providers=get_providers("chat"),
230
embedding_providers=get_providers("embedding"),
234
- shell_interfaces=[{"value": "local", "label": "Local Python TTY"}, {"value": "ssh", "label": "SSH"}],
231
is_dockerized=runtime.is_dockerized(),
232
agent_subdirs=[{"value": item["key"], "label": item["label"]}
233
for item in subagents.get_all_agents_list()
@@ -269,7 +265,6 @@ def convert_out(settings: Settings) -> SettingsOutput:
265
additional["chat_providers"] = _ensure_option_present(additional.get("chat_providers"), current.get("util_model_provider"))
266
additional["chat_providers"] = _ensure_option_present(additional.get("chat_providers"), current.get("browser_model_provider"))
267
additional["embedding_providers"] = _ensure_option_present(additional.get("embedding_providers"), current.get("embed_model_provider"))
272
- additional["shell_interfaces"] = _ensure_option_present(additional.get("shell_interfaces"), current.get("shell_interface"))
268
additional["agent_subdirs"] = _ensure_option_present(additional.get("agent_subdirs"), current.get("agent_profile"))
269
additional["knowledge_subdirs"] = _ensure_option_present(additional.get("knowledge_subdirs"), current.get("agent_knowledge_subdir"))
270
additional["stt_models"] = _ensure_option_present(additional.get("stt_models"), current.get("stt_model_size"))
@@ -549,8 +544,6 @@ def get_default_settings() -> Settings:
544
rfc_url=get_default_value("rfc_url", "localhost"),
545
rfc_password="",
546
rfc_port_http=get_default_value("rfc_port_http", 55080),
552
- rfc_port_ssh=get_default_value("rfc_port_ssh", 55022),
553
- shell_interface=get_default_value("shell_interface", "local" if runtime.is_dockerized() else "ssh"),
547
websocket_server_restart_enabled=get_default_value("websocket_server_restart_enabled", True),
548
uvicorn_access_logs_enabled=get_default_value("uvicorn_access_logs_enabled", False),
549
stt_model_size=get_default_value("stt_model_size", "base"),
@@ -750,27 +743,9 @@ def set_root_password(password: str):
743
744
745
def get_runtime_config(set: Settings):
753
- if runtime.is_dockerized():
754
- return {
755
- "code_exec_ssh_enabled": set["shell_interface"] == "ssh",
756
- "code_exec_ssh_addr": "localhost",
757
- "code_exec_ssh_port": 22,
758
- "code_exec_ssh_user": "root",
759
- }
760
- else:
761
- host = set["rfc_url"]
762
- if "//" in host:
763
- host = host.split("//")[1]
764
- if ":" in host:
765
- host, port = host.split(":")
766
- if host.endswith("/"):
767
- host = host[:-1]
768
- return {
769
- "code_exec_ssh_enabled": set["shell_interface"] == "ssh",
770
- "code_exec_ssh_addr": host,
771
- "code_exec_ssh_port": set["rfc_port_ssh"],
772
- "code_exec_ssh_user": "root",
773
- }
746
+ # SSH config is now managed by the code_execution plugin.
747
+ # This function is kept for backward compatibility but returns an empty dict.
748
+ return {}
749
750
751
def create_auth_token() -> str:
initialize.py
+1
-10
@@ -82,13 +82,8 @@ def initialize_agent(override_settings: dict | None = None):
82
knowledge_subdirs=[current_settings["agent_knowledge_subdir"], "default"],
83
mcp_servers=current_settings["mcp_servers"],
84
browser_http_headers=current_settings["browser_http_headers"],
85
- # code_exec params get initialized in _set_runtime_config
86
- # additional = {},
85
)
86
89
- # update SSH and docker settings
90
- _set_runtime_config(config, current_settings)
91
-
87
# update config with runtime args
88
_args_override(config)
89
@@ -175,8 +170,4 @@ def _args_override(config):
170
setattr(config, key, value)
171
172
178
-def _set_runtime_config(config: AgentConfig, set: settings.Settings):
179
- ssh_conf = settings.get_runtime_config(set)
180
- for key, value in ssh_conf.items():
181
- if hasattr(config, key):
182
- setattr(config, key, value)
173
+
plugins/code_execution/default_config.yaml
new
+36
@@ -0,0 +1,36 @@
1
+# Shell interface: false = local TTY, true = SSH
2
+ssh_enabled: false
3
+
4
+# SSH credentials (used when ssh_enabled is true)
5
+ssh_addr: localhost
6
+ssh_port: 22
7
+ssh_user: root
8
+ssh_pass: ""
9
+
10
+# Timeouts for python/nodejs/terminal runtimes (seconds)
11
+code_exec_first_output_timeout: 30
12
+code_exec_between_output_timeout: 15
13
+code_exec_max_exec_timeout: 180
14
+code_exec_dialog_timeout: 5
15
+
16
+# Timeouts for "output" runtime (seconds)
17
+output_first_output_timeout: 90
18
+output_between_output_timeout: 45
19
+output_max_exec_timeout: 300
20
+output_dialog_timeout: 5
21
+
22
+# Shell prompt detection patterns (one regex per line)
23
+# When matched, output is returned immediately without waiting for timeout
24
+prompt_patterns: |
25
+ (\(venv\)).+[$#] ?$
26
+ root@[^:]+:[^#]+# ?$
27
+ [a-zA-Z0-9_.-]+@[^:]+:[^$#]+[$#] ?$
28
+ \(?.*\)?\s*PS\s+[^>]+> ?$
29
+
30
+# Dialog detection patterns (one regex per line, case-insensitive)
31
+# When matched after dialog_timeout, control is returned to the agent
32
+dialog_patterns: |
33
+ Y/N
34
+ yes/no
35
+ :\s*$
36
+ \?\s*$
plugins/code_execution/extensions/python/system_prompt/_20_code_execution_prompt.py
new
+14
@@ -0,0 +1,14 @@
1
+from helpers.extension import Extension
2
+from agent import LoopData
3
+
4
+
5
+class CodeExecutionPrompt(Extension):
6
+
7
+ async def execute(
8
+ self,
9
+ system_prompt: list[str] = [],
10
+ loop_data: LoopData = LoopData(),
11
+ **kwargs,
12
+ ):
13
+ prompt = self.agent.read_prompt("agent.system.tool.code_exe.md")
14
+ system_prompt.append(prompt)
plugins/code_execution/extensions/webui/get_message_handler/code-exe-handler.js
new
+7
@@ -0,0 +1,7 @@
1
+import { drawMessageCodeExe } from "/js/messages.js";
2
+
3
+export default async function registerCodeExeHandler(extData) {
4
+ if (extData?.type === "code_exe") {
5
+ extData.handler = drawMessageCodeExe;
6
+ }
7
+}
plugins/code_execution/helpers/__init__.py
plugins/code_execution/helpers/shell_local.py
renamed
+3
-2
@@ -4,8 +4,9 @@ import subprocess
4
import time
5
import sys
6
from typing import Optional, Tuple
7
-from helpers import tty_session, runtime
8
-from helpers.shell_ssh import clean_string
7
+from helpers import runtime
8
+from plugins.code_execution.helpers import tty_session
9
+from plugins.code_execution.helpers.shell_ssh import clean_string
10
11
class LocalInteractiveSession:
12
def __init__(self, cwd: str|None = None):
plugins/code_execution/helpers/shell_ssh.py
renamed
plugins/code_execution/helpers/tty_session.py
renamed
plugins/code_execution/plugin.yaml
new
+8
@@ -0,0 +1,8 @@
1
+name: Code Execution
2
+description: Provides terminal, Python, and Node.js code execution via local TTY or SSH.
3
+version: 1.0.0
4
+always_enabled: true
5
+settings_sections:
6
+ - agent
7
+per_project_config: true
8
+per_agent_config: true
plugins/code_execution/prompts/agent.system.tool.code_exe.md
renamed
plugins/code_execution/prompts/fw.code.info.md
renamed
plugins/code_execution/prompts/fw.code.max_time.md
renamed
plugins/code_execution/prompts/fw.code.no_out_time.md
renamed
plugins/code_execution/prompts/fw.code.no_output.md
renamed
plugins/code_execution/prompts/fw.code.pause_dialog.md
renamed
plugins/code_execution/prompts/fw.code.pause_time.md
renamed
plugins/code_execution/prompts/fw.code.reset.md
renamed
plugins/code_execution/prompts/fw.code.running.md
renamed
plugins/code_execution/prompts/fw.code.runtime_wrong.md
renamed
plugins/code_execution/tools/code_execution_tool.py
renamed
+140
-98
@@ -1,31 +1,93 @@
1
import asyncio
2
from dataclasses import dataclass
3
+import re
4
import shlex
5
import time
6
+
7
from helpers.tool import Tool, Response
6
-from helpers import files, rfc_exchange, projects, runtime, settings
8
+from helpers import files, projects, runtime, settings
9
from helpers.print_style import PrintStyle
8
-from helpers.shell_local import LocalInteractiveSession
9
-from helpers.shell_ssh import SSHInteractiveSession
10
from helpers.strings import truncate_text as truncate_text_string
11
from helpers.messages import truncate_text as truncate_text_agent
12
-import re
12
+from helpers import plugins
13
+
14
+from plugins.code_execution.helpers.shell_local import LocalInteractiveSession
15
+from plugins.code_execution.helpers.shell_ssh import SSHInteractiveSession
16
+
17
+
18
+def _get_config(agent) -> dict:
19
+ cfg = plugins.get_plugin_config("code_execution", agent=agent) or {}
20
+
21
+ # SSH / TTY switch
22
+ ssh_enabled = bool(cfg.get("ssh_enabled", False))
23
+
24
+ # SSH credentials
25
+ ssh_addr = str(cfg.get("ssh_addr", "localhost"))
26
+ ssh_port = int(cfg.get("ssh_port", 22))
27
+ ssh_user = str(cfg.get("ssh_user", "root"))
28
+ ssh_pass = str(cfg.get("ssh_pass", ""))
29
+
30
+ # Timeouts for python/nodejs/terminal runtimes
31
+ code_exec_timeouts = {
32
+ "first_output_timeout": int(cfg.get("code_exec_first_output_timeout", 30)),
33
+ "between_output_timeout": int(cfg.get("code_exec_between_output_timeout", 15)),
34
+ "max_exec_timeout": int(cfg.get("code_exec_max_exec_timeout", 180)),
35
+ "dialog_timeout": int(cfg.get("code_exec_dialog_timeout", 5)),
36
+ }
37
+
38
+ # Timeouts for "output" runtime
39
+ output_timeouts = {
40
+ "first_output_timeout": int(cfg.get("output_first_output_timeout", 90)),
41
+ "between_output_timeout": int(cfg.get("output_between_output_timeout", 45)),
42
+ "max_exec_timeout": int(cfg.get("output_max_exec_timeout", 300)),
43
+ "dialog_timeout": int(cfg.get("output_dialog_timeout", 5)),
44
+ }
45
+
46
+ # Prompt patterns (one regex per line, or a list)
47
+ prompt_patterns_raw = cfg.get(
48
+ "prompt_patterns",
49
+ r"(\(venv\)).+[$#] ?$" + "\n"
50
+ + r"root@[^:]+:[^#]+# ?$" + "\n"
51
+ + r"[a-zA-Z0-9_.-]+@[^:]+:[^$#]+[$#] ?$" + "\n"
52
+ + r"\(?.*\)?\s*PS\s+[^>]+> ?$",
53
+ )
54
+ if isinstance(prompt_patterns_raw, list):
55
+ prompt_lines = [str(p) for p in prompt_patterns_raw]
56
+ else:
57
+ prompt_lines = str(prompt_patterns_raw).splitlines()
58
+ prompt_patterns = [
59
+ re.compile(p.strip())
60
+ for p in prompt_lines
61
+ if p.strip()
62
+ ]
63
+
64
+ # Dialog patterns (one regex per line, or a list)
65
+ dialog_patterns_raw = cfg.get(
66
+ "dialog_patterns",
67
+ "Y/N\nyes/no\n:\\s*$\n\\?\\s*$",
68
+ )
69
+ if isinstance(dialog_patterns_raw, list):
70
+ dialog_lines = [str(p) for p in dialog_patterns_raw]
71
+ else:
72
+ dialog_lines = str(dialog_patterns_raw).splitlines()
73
+ dialog_patterns = [
74
+ re.compile(p.strip(), re.IGNORECASE)
75
+ for p in dialog_lines
76
+ if p.strip()
77
+ ]
78
+
79
+ return {
80
+ "ssh_enabled": ssh_enabled,
81
+ "ssh_addr": ssh_addr,
82
+ "ssh_port": ssh_port,
83
+ "ssh_user": ssh_user,
84
+ "ssh_pass": ssh_pass,
85
+ "code_exec_timeouts": code_exec_timeouts,
86
+ "output_timeouts": output_timeouts,
87
+ "prompt_patterns": prompt_patterns,
88
+ "dialog_patterns": dialog_patterns,
89
+ }
90
14
-# Timeouts for python, nodejs, and terminal runtimes.
15
-CODE_EXEC_TIMEOUTS: dict[str, int] = {
16
- "first_output_timeout": 30,
17
- "between_output_timeout": 15,
18
- "max_exec_timeout": 180,
19
- "dialog_timeout": 5,
20
-}
21
-
22
-# Timeouts for output runtime.
23
-OUTPUT_TIMEOUTS: dict[str, int] = {
24
- "first_output_timeout": 90,
25
- "between_output_timeout": 45,
26
- "max_exec_timeout": 300,
27
- "dialog_timeout": 5,
28
-}
91
92
@dataclass
93
class ShellWrap:
@@ -33,6 +95,7 @@ class ShellWrap:
95
session: LocalInteractiveSession | SSHInteractiveSession
96
running: bool
97
98
+
99
@dataclass
100
class State:
101
ssh_enabled: bool
@@ -41,51 +104,37 @@ class State:
104
105
class CodeExecution(Tool):
106
44
- # Common shell prompt regex patterns (add more as needed)
45
- prompt_patterns = [
46
- re.compile(r"\\(venv\\).+[$#] ?$"), # (venv) ...$ or (venv) ...#
47
- re.compile(r"root@[^:]+:[^#]+# ?$"), # root@container:~#
48
- re.compile(r"[a-zA-Z0-9_.-]+@[^:]+:[^$#]+[$#] ?$"), # user@host:~$
49
- re.compile(r"\(?.*\)?\s*PS\s+[^>]+> ?$"), # PowerShell prompt like (base) PS C:\...>
50
- ]
51
- # potential dialog detection
52
- dialog_patterns = [
53
- re.compile(r"Y/N", re.IGNORECASE), # Y/N anywhere in line
54
- re.compile(r"yes/no", re.IGNORECASE), # yes/no anywhere in line
55
- re.compile(r":\s*$"), # line ending with colon
56
- re.compile(r"\?\s*$"), # line ending with question mark
57
- ]
58
-
107
async def execute(self, **kwargs) -> Response:
108
109
await self.agent.handle_intervention() # wait for intervention and handle it, if paused
110
63
- runtime = self.args.get("runtime", "").lower().strip()
111
+ runtime_arg = self.args.get("runtime", "").lower().strip()
112
session = int(self.args.get("session", 0))
113
self.allow_running = bool(self.args.get("allow_running", False))
66
- reset = bool(self.args.get("reset", False) or runtime == "reset")
114
+ reset = bool(self.args.get("reset", False) or runtime_arg == "reset")
115
68
- if runtime == "python":
116
+ if runtime_arg == "python":
117
response = await self.execute_python_code(
118
code=self.args["code"], session=session, reset=reset
119
)
72
- elif runtime == "nodejs":
120
+ elif runtime_arg == "nodejs":
121
response = await self.execute_nodejs_code(
122
code=self.args["code"], session=session, reset=reset
123
)
76
- elif runtime == "terminal":
124
+ elif runtime_arg == "terminal":
125
response = await self.execute_terminal_command(
126
command=self.args["code"], session=session, reset=reset
127
)
80
- elif runtime == "output":
128
+ elif runtime_arg == "output":
129
+ cfg = _get_config(self.agent)
130
response = await self.get_terminal_output(
82
- session=session, timeouts=OUTPUT_TIMEOUTS
131
+ session=session, timeouts=cfg["output_timeouts"]
132
)
84
- elif runtime == "reset":
133
+ elif runtime_arg == "reset":
134
response = await self.reset_terminal(session=session)
135
else:
136
response = self.agent.read_prompt(
88
- "fw.code.runtime_wrong.md", runtime=runtime
137
+ "fw.code.runtime_wrong.md", runtime=runtime_arg
138
)
139
140
if not response:
@@ -105,7 +154,6 @@ class CodeExecution(Tool):
154
def get_heading(self, text: str = ""):
155
if not text:
156
text = f"{self.name} - {self.args['runtime'] if 'runtime' in self.args else 'unknown'}"
108
- # text = truncate_text_string(text, 60) # don't truncate here, log.py takes care of it
157
session = self.args.get("session", None)
158
session_text = f"[{session}] " if session or session == 0 else ""
159
return f"icon://terminal {session_text}{text}"
@@ -115,9 +163,11 @@ class CodeExecution(Tool):
163
164
async def prepare_state(self, reset=False, session: int | None = None):
165
self.state: State | None = self.agent.get_data("_cet_state")
166
+ cfg = _get_config(self.agent)
167
+ ssh_enabled = cfg["ssh_enabled"]
168
+
169
# always reset state when ssh_enabled changes
119
- if not self.state or self.state.ssh_enabled != self.agent.config.code_exec_ssh_enabled:
120
- # initialize shells dictionary if not exists
170
+ if not self.state or self.state.ssh_enabled != ssh_enabled:
171
shells: dict[int, ShellWrap] = {}
172
else:
173
shells = self.state.shells.copy()
@@ -132,21 +182,16 @@ class CodeExecution(Tool):
182
await shells[s].session.close()
183
shells = {}
184
135
- # initialize local or remote interactive shell interface for session 0 if needed
185
+ # initialize local or remote interactive shell interface for session if needed
186
if session is not None and session not in shells:
187
cwd = await self.ensure_cwd()
138
- if self.agent.config.code_exec_ssh_enabled:
139
- pswd = (
140
- self.agent.config.code_exec_ssh_pass
141
- if self.agent.config.code_exec_ssh_pass
142
- else await rfc_exchange.get_root_password()
143
- )
188
+ if ssh_enabled:
189
shell = SSHInteractiveSession(
190
self.agent.context.log,
146
- self.agent.config.code_exec_ssh_addr,
147
- self.agent.config.code_exec_ssh_port,
148
- self.agent.config.code_exec_ssh_user,
149
- pswd,
191
+ cfg["ssh_addr"],
192
+ cfg["ssh_port"],
193
+ cfg["ssh_user"],
194
+ cfg["ssh_pass"],
195
cwd=cwd,
196
)
197
else:
@@ -155,7 +200,7 @@ class CodeExecution(Tool):
200
shells[session] = ShellWrap(id=session, session=shell, running=False)
201
await shell.connect()
202
158
- self.state = State(shells=shells, ssh_enabled=self.agent.config.code_exec_ssh_enabled)
203
+ self.state = State(shells=shells, ssh_enabled=ssh_enabled)
204
self.agent.set_data("_cet_state", self.state)
205
return self.state
206
@@ -174,13 +219,17 @@ class CodeExecution(Tool):
219
async def execute_terminal_command(
220
self, session: int, command: str, reset: bool = False
221
):
177
- prefix = ("bash>" if not runtime.is_windows() or self.agent.config.code_exec_ssh_enabled else "PS>") + self.format_command_for_output(command) + "\n\n"
222
+ cfg = _get_config(self.agent)
223
+ prefix = (
224
+ ("bash>" if not runtime.is_windows() or cfg["ssh_enabled"] else "PS>")
225
+ + self.format_command_for_output(command)
226
+ + "\n\n"
227
+ )
228
return await self.terminal_session(session, command, reset, prefix)
229
230
async def terminal_session(
231
self, session: int, command: str, reset: bool = False, prefix: str = "", timeouts: dict | None = None
232
):
183
-
233
self.state = await self.prepare_state(reset=reset, session=session)
234
235
await self.agent.handle_intervention() # wait for intervention and handle it, if paused
@@ -189,11 +238,12 @@ class CodeExecution(Tool):
238
if not self.allow_running:
239
if response := await self.handle_running_session(session):
240
return response
192
-
241
+
242
+ cfg = _get_config(self.agent)
243
+
244
# try again on lost connection
245
for i in range(2):
246
try:
196
-
247
self.state.shells[session].running = True
248
await self.state.shells[session].session.send_command(command)
249
@@ -210,11 +260,14 @@ class CodeExecution(Tool):
260
PrintStyle(
261
background_color="white", font_color="#1B4F72", bold=True
262
).print(f"{self.agent.agent_name} code execution output{locl}")
213
- return await self.get_terminal_output(session=session, prefix=prefix, timeouts=(timeouts or CODE_EXEC_TIMEOUTS))
263
+ return await self.get_terminal_output(
264
+ session=session,
265
+ prefix=prefix,
266
+ timeouts=(timeouts or cfg["code_exec_timeouts"]),
267
+ )
268
269
except Exception as e:
270
if i == 1:
217
- # try again on lost connection
271
PrintStyle.error(str(e))
272
await self.prepare_state(reset=True, session=session)
273
continue
@@ -222,13 +275,8 @@ class CodeExecution(Tool):
275
raise e
276
277
def format_command_for_output(self, command: str):
225
- # truncate long commands
278
short_cmd = command[:200]
227
- # normalize whitespace for cleaner output
279
short_cmd = " ".join(short_cmd.split())
229
- # replace any sequence of ', ", or ` with a single '
230
- # short_cmd = re.sub(r"['\"`]+", "'", short_cmd) # no need anymore
231
- # final length
280
short_cmd = truncate_text_string(short_cmd, 100)
281
return f"{short_cmd}"
282
@@ -236,16 +284,14 @@ class CodeExecution(Tool):
284
self,
285
session=0,
286
reset_full_output=True,
239
- first_output_timeout=30, # Wait up to x seconds for first output
240
- between_output_timeout=15, # Wait up to x seconds between outputs
241
- dialog_timeout=5, # potential dialog detection timeout
242
- max_exec_timeout=180, # hard cap on total runtime
287
+ first_output_timeout=30,
288
+ between_output_timeout=15,
289
+ dialog_timeout=5,
290
+ max_exec_timeout=180,
291
sleep_time=0.5,
292
prefix="",
293
timeouts: dict | None = None,
294
):
247
-
248
- # if not self.state:
295
self.state = await self.prepare_state(session=session)
296
297
# Override timeouts if a dict is provided
@@ -255,6 +301,10 @@ class CodeExecution(Tool):
301
dialog_timeout = timeouts.get("dialog_timeout", dialog_timeout)
302
max_exec_timeout = timeouts.get("max_exec_timeout", max_exec_timeout)
303
304
+ cfg = _get_config(self.agent)
305
+ prompt_patterns = cfg["prompt_patterns"]
306
+ dialog_patterns = cfg["dialog_patterns"]
307
+
308
start_time = time.time()
309
last_output_time = start_time
310
full_output = ""
@@ -277,7 +327,6 @@ class CodeExecution(Tool):
327
now = time.time()
328
if partial_output:
329
PrintStyle(font_color="#85C1E9").stream(partial_output)
280
- # full_output += partial_output # Append new output
330
truncated_output = self.fix_full_output(full_output)
331
self.set_progress(truncated_output)
332
heading = self.get_heading_from_output(truncated_output, 0)
@@ -291,7 +340,7 @@ class CodeExecution(Tool):
340
)
341
last_lines.reverse()
342
for idx, line in enumerate(last_lines):
294
- for pat in self.prompt_patterns:
343
+ for pat in prompt_patterns:
344
if pat.search(line.strip()):
345
PrintStyle.info(
346
"Detected shell prompt, returning output early."
@@ -343,12 +392,11 @@ class CodeExecution(Tool):
392
393
# potential dialog detection
394
if now - last_output_time > dialog_timeout:
346
- # Check for dialog prompt at the end of output
395
last_lines = (
396
truncated_output.splitlines()[-2:] if truncated_output else []
397
)
398
for line in last_lines:
351
- for pat in self.dialog_patterns:
399
+ for pat in dialog_patterns:
400
if pat.search(line.strip()):
401
PrintStyle.info(
402
"Detected dialog prompt, returning output early."
@@ -374,14 +422,18 @@ class CodeExecution(Tool):
422
async def handle_running_session(
423
self,
424
session=0,
377
- reset_full_output=True,
425
+ reset_full_output=True,
426
prefix=""
427
):
428
if not self.state or session not in self.state.shells:
429
return None
430
if not self.state.shells[session].running:
431
return None
384
-
432
+
433
+ cfg = _get_config(self.agent)
434
+ prompt_patterns = cfg["prompt_patterns"]
435
+ dialog_patterns = cfg["dialog_patterns"]
436
+
437
full_output, _ = await self.state.shells[session].session.read_output(
438
timeout=1, reset_full_output=reset_full_output
439
)
@@ -394,7 +446,7 @@ class CodeExecution(Tool):
446
)
447
last_lines.reverse()
448
for idx, line in enumerate(last_lines):
397
- for pat in self.prompt_patterns:
449
+ for pat in prompt_patterns:
450
if pat.search(line.strip()):
451
PrintStyle.info(
452
"Detected shell prompt, returning output early."
@@ -402,9 +454,9 @@ class CodeExecution(Tool):
454
self.mark_session_idle(session)
455
return None
456
405
- has_dialog = False
457
+ has_dialog = False
458
for line in last_lines:
407
- for pat in self.dialog_patterns:
459
+ for pat in dialog_patterns:
460
if pat.search(line.strip()):
461
has_dialog = True
462
break
@@ -412,7 +464,7 @@ class CodeExecution(Tool):
464
break
465
466
if has_dialog:
415
- sys_info = self.agent.read_prompt("fw.code.pause_dialog.md", timeout=1)
467
+ sys_info = self.agent.read_prompt("fw.code.pause_dialog.md", timeout=1)
468
else:
469
sys_info = self.agent.read_prompt("fw.code.running.md", session=session)
470
@@ -422,14 +474,12 @@ class CodeExecution(Tool):
474
PrintStyle(font_color="#FFA500", bold=True).print(response)
475
self.log.update(content=prefix + response, heading=heading)
476
return response
425
-
477
+
478
def mark_session_idle(self, session: int = 0):
427
- # Mark session as idle - command finished
479
if self.state and session in self.state.shells:
480
self.state.shells[session].running = False
481
482
async def reset_terminal(self, session=0, reason: str | None = None):
432
- # Print the reason for the reset to the console if provided
483
if reason:
484
PrintStyle(font_color="#FFA500", bold=True).print(
485
f"Resetting terminal session {session}... Reason: {reason}"
@@ -439,7 +489,6 @@ class CodeExecution(Tool):
489
f"Resetting terminal session {session}..."
490
)
491
442
- # Only reset the specified session while preserving others
492
await self.prepare_state(reset=True, session=session)
493
response = self.agent.read_prompt(
494
"fw.code.info.md", info=self.agent.read_prompt("fw.code.reset.md")
@@ -453,9 +502,7 @@ class CodeExecution(Tool):
502
if not output:
503
return self.get_heading() + done_icon
504
456
- # find last non-empty line with skip
505
lines = output.splitlines()
458
- # Start from len(lines) - skip_lines - 1 down to 0
506
for i in range(len(lines) - skip_lines - 1, -1, -1):
507
line = lines[i].strip()
508
if not line:
@@ -465,11 +512,8 @@ class CodeExecution(Tool):
512
return self.get_heading() + done_icon
513
514
def fix_full_output(self, output: str):
468
- # remove any single byte \xXX escapes
515
output = re.sub(r"(?<!\\)\\x[0-9A-Fa-f]{2}", "", output)
470
- # Strip every line of output before truncation
471
- # output = "\n".join(line.strip() for line in output.splitlines())
472
- output = truncate_text_agent(agent=self.agent, output=output, threshold=1000000) # ~1MB, larger outputs should be dumped to file, not read from terminal
516
+ output = truncate_text_agent(agent=self.agent, output=output, threshold=1000000)
517
return output
518
519
async def ensure_cwd(self) -> str | None:
@@ -487,9 +531,7 @@ class CodeExecution(Tool):
531
await runtime.call_development_function(make_dir, normalized)
532
return normalized
533
534
+
535
def make_dir(path: str):
536
import os
537
os.makedirs(path, exist_ok=True)
493
-
494
-
495
-
\ No newline at end of file
plugins/code_execution/webui/config.html
new
+202
@@ -0,0 +1,202 @@
1
+<html>
2
+<head>
3
+ <title>Code Execution</title>
4
+</head>
5
+
6
+<body>
7
+ <div x-data>
8
+ <template x-if="$store.pluginSettings.settings">
9
+ <div>
10
+ <div class="section-title">Code Execution</div>
11
+ <div class="section-description">
12
+ Configuration for the code execution tool. Controls the shell interface, SSH credentials, execution timeouts, and prompt/dialog detection patterns.
13
+ </div>
14
+
15
+ <!-- Shell Interface -->
16
+ <div class="field">
17
+ <div class="field-label">
18
+ <div class="field-title">Use SSH</div>
19
+ <div class="field-description">
20
+ When enabled, code execution connects via SSH using the credentials below. When disabled, a local Python TTY is used.
21
+ </div>
22
+ </div>
23
+ <div class="field-control">
24
+ <label class="toggle">
25
+ <input type="checkbox" x-model="$store.pluginSettings.settings.ssh_enabled" />
26
+ <span class="toggler"></span>
27
+ </label>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- SSH credentials (shown only when SSH is enabled) -->
32
+ <template x-if="$store.pluginSettings.settings.ssh_enabled">
33
+ <div>
34
+ <div class="field">
35
+ <div class="field-label">
36
+ <div class="field-title">SSH Address</div>
37
+ <div class="field-description">Hostname or IP of the SSH target.</div>
38
+ </div>
39
+ <div class="field-control">
40
+ <input type="text" x-model="$store.pluginSettings.settings.ssh_addr" placeholder="localhost" />
41
+ </div>
42
+ </div>
43
+
44
+ <div class="field">
45
+ <div class="field-label">
46
+ <div class="field-title">SSH Port</div>
47
+ <div class="field-description">SSH port on the target host.</div>
48
+ </div>
49
+ <div class="field-control">
50
+ <input type="number" min="1" max="65535" x-model.number="$store.pluginSettings.settings.ssh_port" />
51
+ </div>
52
+ </div>
53
+
54
+ <div class="field">
55
+ <div class="field-label">
56
+ <div class="field-title">SSH User</div>
57
+ <div class="field-description">Username for SSH login.</div>
58
+ </div>
59
+ <div class="field-control">
60
+ <input type="text" x-model="$store.pluginSettings.settings.ssh_user" placeholder="root" />
61
+ </div>
62
+ </div>
63
+
64
+ <div class="field">
65
+ <div class="field-label">
66
+ <div class="field-title">SSH Password</div>
67
+ <div class="field-description">Password for SSH login.</div>
68
+ </div>
69
+ <div class="field-control">
70
+ <input type="password" autocomplete="off" x-model="$store.pluginSettings.settings.ssh_pass" />
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </template>
75
+
76
+ <!-- Execution timeouts -->
77
+ <div class="section-title">Execution Timeouts</div>
78
+ <div class="section-description">
79
+ Timeouts (in seconds) for python, nodejs, and terminal runtimes.
80
+ </div>
81
+
82
+ <div class="field">
83
+ <div class="field-label">
84
+ <div class="field-title">First output timeout</div>
85
+ <div class="field-description">Seconds to wait for the first output before returning control to the agent.</div>
86
+ </div>
87
+ <div class="field-control">
88
+ <input type="number" min="1" x-model.number="$store.pluginSettings.settings.code_exec_first_output_timeout" />
89
+ </div>
90
+ </div>
91
+
92
+ <div class="field">
93
+ <div class="field-label">
94
+ <div class="field-title">Between output timeout</div>
95
+ <div class="field-description">Seconds to wait between output chunks before returning control.</div>
96
+ </div>
97
+ <div class="field-control">
98
+ <input type="number" min="1" x-model.number="$store.pluginSettings.settings.code_exec_between_output_timeout" />
99
+ </div>
100
+ </div>
101
+
102
+ <div class="field">
103
+ <div class="field-label">
104
+ <div class="field-title">Max execution timeout</div>
105
+ <div class="field-description">Hard cap on total execution time in seconds.</div>
106
+ </div>
107
+ <div class="field-control">
108
+ <input type="number" min="1" x-model.number="$store.pluginSettings.settings.code_exec_max_exec_timeout" />
109
+ </div>
110
+ </div>
111
+
112
+ <div class="field">
113
+ <div class="field-label">
114
+ <div class="field-title">Dialog detection timeout</div>
115
+ <div class="field-description">Seconds of idle output before checking for dialog prompts.</div>
116
+ </div>
117
+ <div class="field-control">
118
+ <input type="number" min="1" x-model.number="$store.pluginSettings.settings.code_exec_dialog_timeout" />
119
+ </div>
120
+ </div>
121
+
122
+ <!-- Output runtime timeouts -->
123
+ <div class="section-title">Output Runtime Timeouts</div>
124
+ <div class="section-description">
125
+ Timeouts (in seconds) for the "output" runtime (waiting for long-running processes).
126
+ </div>
127
+
128
+ <div class="field">
129
+ <div class="field-label">
130
+ <div class="field-title">First output timeout</div>
131
+ <div class="field-description">Seconds to wait for the first output.</div>
132
+ </div>
133
+ <div class="field-control">
134
+ <input type="number" min="1" x-model.number="$store.pluginSettings.settings.output_first_output_timeout" />
135
+ </div>
136
+ </div>
137
+
138
+ <div class="field">
139
+ <div class="field-label">
140
+ <div class="field-title">Between output timeout</div>
141
+ <div class="field-description">Seconds to wait between output chunks.</div>
142
+ </div>
143
+ <div class="field-control">
144
+ <input type="number" min="1" x-model.number="$store.pluginSettings.settings.output_between_output_timeout" />
145
+ </div>
146
+ </div>
147
+
148
+ <div class="field">
149
+ <div class="field-label">
150
+ <div class="field-title">Max execution timeout</div>
151
+ <div class="field-description">Hard cap on total wait time in seconds.</div>
152
+ </div>
153
+ <div class="field-control">
154
+ <input type="number" min="1" x-model.number="$store.pluginSettings.settings.output_max_exec_timeout" />
155
+ </div>
156
+ </div>
157
+
158
+ <div class="field">
159
+ <div class="field-label">
160
+ <div class="field-title">Dialog detection timeout</div>
161
+ <div class="field-description">Seconds of idle output before checking for dialog prompts.</div>
162
+ </div>
163
+ <div class="field-control">
164
+ <input type="number" min="1" x-model.number="$store.pluginSettings.settings.output_dialog_timeout" />
165
+ </div>
166
+ </div>
167
+
168
+ <!-- Patterns -->
169
+ <div class="section-title">Detection Patterns</div>
170
+ <div class="section-description">
171
+ Regular expressions used to detect shell prompts and interactive dialogs. One pattern per line.
172
+ </div>
173
+
174
+ <div class="field">
175
+ <div class="field-label">
176
+ <div class="field-title">Prompt patterns</div>
177
+ <div class="field-description">
178
+ When any pattern matches the last output lines, execution is considered complete and output is returned immediately.
179
+ </div>
180
+ </div>
181
+ <div class="field-control">
182
+ <textarea rows="5" x-model="$store.pluginSettings.settings.prompt_patterns"></textarea>
183
+ </div>
184
+ </div>
185
+
186
+ <div class="field">
187
+ <div class="field-label">
188
+ <div class="field-title">Dialog patterns</div>
189
+ <div class="field-description">
190
+ When any pattern matches after the dialog timeout, control is returned to the agent to handle the interactive prompt.
191
+ </div>
192
+ </div>
193
+ <div class="field-control">
194
+ <textarea rows="4" x-model="$store.pluginSettings.settings.dialog_patterns"></textarea>
195
+ </div>
196
+ </div>
197
+ </div>
198
+ </template>
199
+ </div>
200
+</body>
201
+
202
+</html>
tools/input.py
+1
-1
@@ -1,6 +1,6 @@
1
from agent import Agent, UserMessage
2
from helpers.tool import Tool, Response
3
-from tools.code_execution_tool import CodeExecution
3
+from plugins.code_execution.tools.code_execution_tool import CodeExecution
4
5
6
class Input(Tool):
webui/components/settings/developer/dev.html
-25
@@ -12,22 +12,6 @@
12
Parameters for A0 framework development. RFCs (remote function calls) are used to call functions on another A0 instance. You can develop and debug A0 natively on your local system while redirecting some functions to A0 instance in docker. This is crucial for development as A0 needs to run in standardized environment to support all features.
13
</div>
14
15
- <div class="field">
16
- <div class="field-label">
17
- <div class="field-title">Shell Interface</div>
18
- <div class="field-description">
19
- Terminal interface used for Code Execution Tool. Local Python TTY works locally in both dockerized and development environments. SSH always connects to dockerized environment (automatically at localhost or RFC host address).
20
- </div>
21
- </div>
22
- <div class="field-control">
23
- <select x-model="$store.settings.settings.shell_interface">
24
- <template x-for="option in $store.settings.additional?.shell_interfaces" :key="option.value">
25
- <option :value="option.value" :selected="option.value === $store.settings.settings.shell_interface" x-text="option.label"></option>
26
- </template>
27
- </select>
28
- </div>
29
- </div>
30
-
15
<template x-if="!$store.settings.additional?.is_dockerized">
16
<div>
17
<div class="field">
@@ -68,15 +52,6 @@
52
</div>
53
</div>
54
71
- <div class="field">
72
- <div class="field-label">
73
- <div class="field-title">RFC SSH port</div>
74
- <div class="field-description">SSH port for dockerized instance of A0.</div>
75
- </div>
76
- <div class="field-control">
77
- <input type="number" x-model.number="$store.settings.settings.rfc_port_ssh" />
78
- </div>
79
- </div>
55
</div>
56
</template>
57