| 1 | import asyncio |
| 2 | import errno |
| 3 | from dataclasses import dataclass |
| 4 | import re |
| 5 | import shlex |
| 6 | import time |
| 7 | |
| 8 | from helpers.tool import Tool, Response |
| 9 | from helpers import files, rfc_exchange, projects, runtime, secrets, settings |
| 10 | from helpers.print_style import PrintStyle |
| 11 | from helpers.strings import truncate_text as truncate_text_string |
| 12 | from helpers.messages import truncate_text as truncate_text_agent |
| 13 | from helpers import plugins |
| 14 | |
| 15 | from plugins._code_execution.helpers.shell_local import LocalInteractiveSession |
| 16 | from plugins._code_execution.helpers.shell_ssh import SSHInteractiveSession |
| 17 | |
| 18 | |
| 19 | def _is_closed_pty_error(exc: BaseException) -> bool: |
| 20 | if isinstance(exc, RuntimeError): |
| 21 | message = str(exc) |
| 22 | if "TTYSpawn PTY is closed" in message or "TTYSpawn process has exited" in message: |
| 23 | return True |
| 24 | if isinstance(exc, OSError) and exc.errno in (errno.EBADF, errno.EIO, errno.EINVAL): |
| 25 | return True |
| 26 | cause = getattr(exc, "__cause__", None) |
| 27 | if cause and cause is not exc: |
| 28 | return _is_closed_pty_error(cause) |
| 29 | return False |
| 30 | |
| 31 | |
| 32 | def _group_multiline_command(command: str, powershell: bool = False) -> str: |
| 33 | body = command.rstrip("\n") |
| 34 | if "\n" not in body: |
| 35 | return body |
| 36 | opener = ". {" if powershell else "{" |
| 37 | return f"{opener}\n{body}\n}}" |
| 38 | |
| 39 | |
| 40 | @dataclass |
| 41 | class ShellWrap: |
| 42 | id: int |
| 43 | session: LocalInteractiveSession | SSHInteractiveSession |
| 44 | running: bool |
| 45 | |
| 46 | |
| 47 | @dataclass |
| 48 | class State: |
| 49 | ssh_enabled: bool |
| 50 | shells: dict[int, ShellWrap] |
| 51 | |
| 52 | |
| 53 | class CodeExecution(Tool): |
| 54 | |
| 55 | async def execute(self, **kwargs) -> Response: |
| 56 | |
| 57 | await self.agent.handle_intervention() # wait for intervention and handle it, if paused |
| 58 | |
| 59 | runtime_arg = self.args.get("runtime", "").lower().strip() |
| 60 | session = int(self.args.get("session", 0)) |
| 61 | self.allow_running = bool(self.args.get("allow_running", False)) |
| 62 | reset = bool(self.args.get("reset", False) or runtime_arg == "reset") |
| 63 | |
| 64 | cfg = _get_config(self.agent) |
| 65 | |
| 66 | if runtime_arg == "python": |
| 67 | response = await self.execute_python_code( |
| 68 | cfg, code=self.args["code"], session=session, reset=reset |
| 69 | ) |
| 70 | elif runtime_arg == "nodejs": |
| 71 | response = await self.execute_nodejs_code( |
| 72 | cfg, code=self.args["code"], session=session, reset=reset |
| 73 | ) |
| 74 | elif runtime_arg == "terminal": |
| 75 | response = await self.execute_terminal_command( |
| 76 | cfg, command=self.args["code"], session=session, reset=reset |
| 77 | ) |
| 78 | elif runtime_arg == "output": |
| 79 | response = await self.get_terminal_output( |
| 80 | cfg, session=session, timeouts=cfg["output_timeouts"] |
| 81 | ) |
| 82 | elif runtime_arg == "reset": |
| 83 | response = await self.reset_terminal(cfg, session=session) |
| 84 | else: |
| 85 | response = self.agent.read_prompt( |
| 86 | "fw.code.runtime_wrong.md", runtime=runtime_arg |
| 87 | ) |
| 88 | |
| 89 | if not response: |
| 90 | response = self.agent.read_prompt( |
| 91 | "fw.code.info.md", info=self.agent.read_prompt("fw.code.no_output.md") |
| 92 | ) |
| 93 | return Response(message=response, break_loop=False) |
| 94 | |
| 95 | def get_log_object(self): |
| 96 | import uuid |
| 97 | return self.agent.context.log.log( |
| 98 | type="code_exe", |
| 99 | heading=self.get_heading(), |
| 100 | content="", |
| 101 | kvps=self.args, |
| 102 | id=str(uuid.uuid4()), |
| 103 | ) |
| 104 | |
| 105 | def get_heading(self, text: str = ""): |
| 106 | if not text: |
| 107 | text = f"{self.name} - {self.args['runtime'] if 'runtime' in self.args else 'unknown'}" |
| 108 | session = self.args.get("session", None) |
| 109 | session_text = f"[{session}] " if session or session == 0 else "" |
| 110 | return f"icon://terminal {session_text}{truncate_text_string(text, 200)}" |
| 111 | |
| 112 | async def after_execution(self, response, **kwargs): |
| 113 | self.agent.hist_add_tool_result(self.name, response.message, id=self.log.id if self.log else "", **(response.additional or {})) |
| 114 | |
| 115 | async def prepare_state(self, cfg: dict, reset=False, session: int | None = None): |
| 116 | self.state: State | None = self.agent.get_data("_cet_state") |
| 117 | ssh_enabled = cfg["ssh_enabled"] |
| 118 | |
| 119 | # always reset state when ssh_enabled changes |
| 120 | if not self.state or self.state.ssh_enabled != ssh_enabled: |
| 121 | shells: dict[int, ShellWrap] = {} |
| 122 | else: |
| 123 | shells = self.state.shells.copy() |
| 124 | |
| 125 | # Only reset the specified session if provided |
| 126 | if reset and session is not None and session in shells: |
| 127 | await shells[session].session.close() |
| 128 | del shells[session] |
| 129 | elif reset and not session: |
| 130 | # Close all sessions if full reset requested |
| 131 | for s in list(shells.keys()): |
| 132 | await shells[s].session.close() |
| 133 | shells = {} |
| 134 | |
| 135 | # initialize local or remote interactive shell interface for session if needed |
| 136 | if session is not None and session not in shells: |
| 137 | cwd = await self.ensure_cwd() |
| 138 | if ssh_enabled: |
| 139 | ssh_pass = await _resolve_ssh_pass(cfg["ssh_pass"]) |
| 140 | shell = SSHInteractiveSession( |
| 141 | self.agent.context.log, |
| 142 | cfg["ssh_addr"], |
| 143 | cfg["ssh_port"], |
| 144 | cfg["ssh_user"], |
| 145 | ssh_pass, |
| 146 | cwd=cwd, |
| 147 | ) |
| 148 | else: |
| 149 | shell = LocalInteractiveSession(cwd=cwd) |
| 150 | |
| 151 | shells[session] = ShellWrap(id=session, session=shell, running=False) |
| 152 | await shell.connect() |
| 153 | |
| 154 | self.state = State(shells=shells, ssh_enabled=ssh_enabled) |
| 155 | self.agent.set_data("_cet_state", self.state) |
| 156 | return self.state |
| 157 | |
| 158 | async def execute_python_code(self, cfg: dict, session: int, code: str, reset: bool = False): |
| 159 | escaped_code = shlex.quote(code) |
| 160 | command = f"ipython -c {escaped_code}" |
| 161 | prefix = "python> " + self.format_command_for_output(code) + "\n\n" |
| 162 | return await self.terminal_session(cfg, session, command, reset, prefix) |
| 163 | |
| 164 | async def execute_nodejs_code(self, cfg: dict, session: int, code: str, reset: bool = False): |
| 165 | escaped_code = shlex.quote(code) |
| 166 | command = f"node /exe/node_eval.js {escaped_code}" |
| 167 | prefix = "node> " + self.format_command_for_output(code) + "\n\n" |
| 168 | return await self.terminal_session(cfg, session, command, reset, prefix) |
| 169 | |
| 170 | async def execute_terminal_command( |
| 171 | self, cfg: dict, session: int, command: str, reset: bool = False |
| 172 | ): |
| 173 | prefix = ( |
| 174 | ("bash>" if not runtime.is_windows() or cfg["ssh_enabled"] else "PS>") |
| 175 | + self.format_command_for_output(command) |
| 176 | + "\n\n" |
| 177 | ) |
| 178 | command = _group_multiline_command( |
| 179 | command, powershell=runtime.is_windows() and not cfg["ssh_enabled"] |
| 180 | ) |
| 181 | return await self.terminal_session(cfg, session, command, reset, prefix) |
| 182 | |
| 183 | async def terminal_session( |
| 184 | self, cfg: dict, session: int, command: str, reset: bool = False, prefix: str = "", timeouts: dict | None = None |
| 185 | ): |
| 186 | self.state = await self.prepare_state(cfg, reset=reset, session=session) |
| 187 | |
| 188 | await self.agent.handle_intervention() # wait for intervention and handle it, if paused |
| 189 | |
| 190 | # Check if session is running and handle it |
| 191 | if not self.allow_running: |
| 192 | if response := await self.handle_running_session(cfg, session): |
| 193 | return response |
| 194 | |
| 195 | # A strict-mode command can terminate the persistent shell itself. |
| 196 | # Recreate such a session lazily before accepting the next command. |
| 197 | if self.state.shells[session].session.is_terminated(): |
| 198 | await self.prepare_state(cfg, reset=True, session=session) |
| 199 | |
| 200 | # try again on lost connection |
| 201 | for i in range(2): |
| 202 | try: |
| 203 | self.state.shells[session].running = True |
| 204 | await self.state.shells[session].session.send_command(command) |
| 205 | |
| 206 | locl = ( |
| 207 | " (local)" |
| 208 | if isinstance(self.state.shells[session].session, LocalInteractiveSession) |
| 209 | else ( |
| 210 | " (remote)" |
| 211 | if isinstance(self.state.shells[session].session, SSHInteractiveSession) |
| 212 | else " (unknown)" |
| 213 | ) |
| 214 | ) |
| 215 | |
| 216 | PrintStyle( |
| 217 | background_color="white", font_color="#1B4F72", bold=True |
| 218 | ).print(f"{self.agent.agent_name} code execution output{locl}") |
| 219 | return await self.get_terminal_output( |
| 220 | cfg, |
| 221 | session=session, |
| 222 | prefix=prefix, |
| 223 | timeouts=(timeouts or cfg["code_exec_timeouts"]), |
| 224 | ) |
| 225 | |
| 226 | except Exception as e: |
| 227 | if _is_closed_pty_error(e) and i == 0: |
| 228 | PrintStyle.warning(f"Terminal session {session} was closed; resetting and retrying once.") |
| 229 | await self.prepare_state(cfg, reset=True, session=session) |
| 230 | continue |
| 231 | PrintStyle.error(str(e)) |
| 232 | raise |
| 233 | |
| 234 | def format_command_for_output(self, command: str): |
| 235 | short_cmd = command[:250] |
| 236 | short_cmd = " ".join(short_cmd.split()) |
| 237 | short_cmd = secrets.get_secrets_manager(self.agent.context).mask_values(short_cmd) |
| 238 | short_cmd = truncate_text_string(short_cmd, 100) |
| 239 | return f"{short_cmd}" |
| 240 | |
| 241 | async def get_terminal_output( |
| 242 | self, |
| 243 | cfg: dict, |
| 244 | session=0, |
| 245 | reset_full_output=True, |
| 246 | first_output_timeout=30, |
| 247 | between_output_timeout=15, |
| 248 | dialog_timeout=5, |
| 249 | max_exec_timeout=240, |
| 250 | sleep_time=0.5, |
| 251 | prefix="", |
| 252 | timeouts: dict | None = None, |
| 253 | ): |
| 254 | self.state = await self.prepare_state(cfg, session=session) |
| 255 | |
| 256 | # Override timeouts if a dict is provided |
| 257 | if timeouts: |
| 258 | first_output_timeout = timeouts.get("first_output_timeout", first_output_timeout) |
| 259 | between_output_timeout = timeouts.get("between_output_timeout", between_output_timeout) |
| 260 | dialog_timeout = timeouts.get("dialog_timeout", dialog_timeout) |
| 261 | max_exec_timeout = timeouts.get("max_exec_timeout", max_exec_timeout) |
| 262 | |
| 263 | prompt_patterns = cfg["prompt_patterns"] |
| 264 | dialog_patterns = cfg["dialog_patterns"] |
| 265 | |
| 266 | start_time = time.time() |
| 267 | last_output_time = start_time |
| 268 | full_output = "" |
| 269 | truncated_output = "" |
| 270 | got_output = False |
| 271 | |
| 272 | # if prefix, log right away |
| 273 | if prefix: |
| 274 | self.log.update(content=prefix) |
| 275 | |
| 276 | while True: |
| 277 | await asyncio.sleep(sleep_time) |
| 278 | try: |
| 279 | full_output, partial_output = await self.state.shells[session].session.read_output( |
| 280 | timeout=1, reset_full_output=reset_full_output |
| 281 | ) |
| 282 | except Exception as e: |
| 283 | if _is_closed_pty_error(e): |
| 284 | await self.prepare_state(cfg, reset=True, session=session) |
| 285 | self.mark_session_idle(session) |
| 286 | sysinfo = "Terminal session was closed and has been reset. Please run the command again." |
| 287 | response = self.agent.read_prompt("fw.code.info.md", info=sysinfo) |
| 288 | self.log.update(content=prefix + response) |
| 289 | return response |
| 290 | raise |
| 291 | reset_full_output = False # only reset once |
| 292 | |
| 293 | await self.agent.handle_intervention() |
| 294 | |
| 295 | now = time.time() |
| 296 | if partial_output: |
| 297 | PrintStyle(font_color="#85C1E9").stream(partial_output) |
| 298 | truncated_output = self.fix_full_output(full_output) |
| 299 | await self.set_progress(truncated_output) |
| 300 | heading = self.get_heading_from_output(truncated_output, 0) |
| 301 | self.log.update(content=prefix + truncated_output, heading=heading) |
| 302 | last_output_time = now |
| 303 | got_output = True |
| 304 | |
| 305 | # ``set -e``, ``exit``, or a lost SSH channel can end the managed |
| 306 | # shell without ever producing another prompt. Treat that process |
| 307 | # or channel termination as a definitive command end. |
| 308 | shell = self.state.shells[session].session |
| 309 | if shell.is_terminated(): |
| 310 | exit_code = shell.get_exit_code() |
| 311 | status = f" with exit code {exit_code}" if exit_code is not None else "" |
| 312 | sysinfo = self.agent.read_prompt( |
| 313 | "fw.code.shell_exit.md", status=status |
| 314 | ) |
| 315 | response = self.agent.read_prompt("fw.code.info.md", info=sysinfo) |
| 316 | if truncated_output: |
| 317 | response = truncated_output + "\n\n" + response |
| 318 | PrintStyle.warning(sysinfo) |
| 319 | heading = self.get_heading_from_output(truncated_output, 0, True) |
| 320 | self.log.update(content=prefix + response, heading=heading) |
| 321 | self.mark_session_idle(session) |
| 322 | return response |
| 323 | |
| 324 | if partial_output: |
| 325 | # Check for shell prompt at the end of output |
| 326 | last_lines = ( |
| 327 | truncated_output.splitlines()[-3:] if truncated_output else [] |
| 328 | ) |
| 329 | last_lines.reverse() |
| 330 | for idx, line in enumerate(last_lines): |
| 331 | line = line.strip() |
| 332 | line = line if len(line) <= 500 else line[:250] + line[-250:] # only check start and end on long lines |
| 333 | for pat in prompt_patterns: |
| 334 | if pat.search(line): |
| 335 | PrintStyle.info( |
| 336 | "Detected shell prompt, returning output early." |
| 337 | ) |
| 338 | last_lines.reverse() |
| 339 | heading = self.get_heading_from_output( |
| 340 | "\n".join(last_lines), idx + 1, True |
| 341 | ) |
| 342 | self.log.update(heading=heading) |
| 343 | self.mark_session_idle(session) |
| 344 | return truncated_output |
| 345 | |
| 346 | # Check for max execution time |
| 347 | if now - start_time > max_exec_timeout: |
| 348 | sysinfo = self.agent.read_prompt( |
| 349 | "fw.code.max_time.md", timeout=max_exec_timeout |
| 350 | ) |
| 351 | response = self.agent.read_prompt("fw.code.info.md", info=sysinfo) |
| 352 | if truncated_output: |
| 353 | response = truncated_output + "\n\n" + response |
| 354 | PrintStyle.warning(sysinfo) |
| 355 | heading = self.get_heading_from_output(truncated_output, 0) |
| 356 | self.log.update(content=prefix + response, heading=heading) |
| 357 | return response |
| 358 | |
| 359 | # Waiting for first output |
| 360 | if not got_output: |
| 361 | if now - start_time > first_output_timeout: |
| 362 | sysinfo = self.agent.read_prompt( |
| 363 | "fw.code.no_out_time.md", timeout=first_output_timeout |
| 364 | ) |
| 365 | response = self.agent.read_prompt("fw.code.info.md", info=sysinfo) |
| 366 | PrintStyle.warning(sysinfo) |
| 367 | self.log.update(content=prefix + response) |
| 368 | return response |
| 369 | else: |
| 370 | # Waiting for more output after first output |
| 371 | if now - last_output_time > between_output_timeout: |
| 372 | sysinfo = self.agent.read_prompt( |
| 373 | "fw.code.pause_time.md", timeout=between_output_timeout |
| 374 | ) |
| 375 | response = self.agent.read_prompt("fw.code.info.md", info=sysinfo) |
| 376 | if truncated_output: |
| 377 | response = truncated_output + "\n\n" + response |
| 378 | PrintStyle.warning(sysinfo) |
| 379 | heading = self.get_heading_from_output(truncated_output, 0) |
| 380 | self.log.update(content=prefix + response, heading=heading) |
| 381 | return response |
| 382 | |
| 383 | # potential dialog detection |
| 384 | if now - last_output_time > dialog_timeout: |
| 385 | last_lines = ( |
| 386 | truncated_output.splitlines()[-2:] if truncated_output else [] |
| 387 | ) |
| 388 | for line in last_lines: |
| 389 | for pat in dialog_patterns: |
| 390 | if pat.search(line.strip()): |
| 391 | PrintStyle.info( |
| 392 | "Detected dialog prompt, returning output early." |
| 393 | ) |
| 394 | |
| 395 | sysinfo = self.agent.read_prompt( |
| 396 | "fw.code.pause_dialog.md", timeout=dialog_timeout |
| 397 | ) |
| 398 | response = self.agent.read_prompt( |
| 399 | "fw.code.info.md", info=sysinfo |
| 400 | ) |
| 401 | if truncated_output: |
| 402 | response = truncated_output + "\n\n" + response |
| 403 | PrintStyle.warning(sysinfo) |
| 404 | heading = self.get_heading_from_output( |
| 405 | truncated_output, 0 |
| 406 | ) |
| 407 | self.log.update( |
| 408 | content=prefix + response, heading=heading |
| 409 | ) |
| 410 | return response |
| 411 | |
| 412 | async def handle_running_session( |
| 413 | self, |
| 414 | cfg: dict, |
| 415 | session=0, |
| 416 | reset_full_output=True, |
| 417 | prefix="" |
| 418 | ): |
| 419 | if not self.state or session not in self.state.shells: |
| 420 | return None |
| 421 | if not self.state.shells[session].running: |
| 422 | return None |
| 423 | |
| 424 | prompt_patterns = cfg["prompt_patterns"] |
| 425 | dialog_patterns = cfg["dialog_patterns"] |
| 426 | |
| 427 | try: |
| 428 | full_output, _ = await self.state.shells[session].session.read_output( |
| 429 | timeout=1, reset_full_output=reset_full_output |
| 430 | ) |
| 431 | except Exception as e: |
| 432 | if _is_closed_pty_error(e): |
| 433 | await self.prepare_state(cfg, reset=True, session=session) |
| 434 | self.mark_session_idle(session) |
| 435 | return None |
| 436 | raise |
| 437 | truncated_output = self.fix_full_output(full_output) |
| 438 | await self.set_progress(truncated_output) |
| 439 | heading = self.get_heading_from_output(truncated_output, 0) |
| 440 | |
| 441 | if self.state.shells[session].session.is_terminated(): |
| 442 | self.mark_session_idle(session) |
| 443 | return None |
| 444 | |
| 445 | last_lines = ( |
| 446 | truncated_output.splitlines()[-3:] if truncated_output else [] |
| 447 | ) |
| 448 | last_lines.reverse() |
| 449 | for line in last_lines: |
| 450 | for pat in prompt_patterns: |
| 451 | if pat.search(line.strip()): |
| 452 | PrintStyle.info( |
| 453 | "Detected shell prompt, returning output early." |
| 454 | ) |
| 455 | self.mark_session_idle(session) |
| 456 | return None |
| 457 | |
| 458 | has_dialog = False |
| 459 | for line in last_lines: |
| 460 | for pat in dialog_patterns: |
| 461 | if pat.search(line.strip()): |
| 462 | has_dialog = True |
| 463 | break |
| 464 | if has_dialog: |
| 465 | break |
| 466 | |
| 467 | if has_dialog: |
| 468 | sys_info = self.agent.read_prompt("fw.code.pause_dialog.md", timeout=1) |
| 469 | else: |
| 470 | sys_info = self.agent.read_prompt("fw.code.running.md", session=session) |
| 471 | |
| 472 | response = self.agent.read_prompt("fw.code.info.md", info=sys_info) |
| 473 | if truncated_output: |
| 474 | response = truncated_output + "\n\n" + response |
| 475 | PrintStyle(font_color="#FFA500", bold=True).print(response) |
| 476 | self.log.update(content=prefix + response, heading=heading) |
| 477 | return response |
| 478 | |
| 479 | def mark_session_idle(self, session: int = 0): |
| 480 | if self.state and session in self.state.shells: |
| 481 | self.state.shells[session].running = False |
| 482 | |
| 483 | async def reset_terminal(self, cfg: dict, session=0, reason: str | None = None): |
| 484 | if reason: |
| 485 | PrintStyle(font_color="#FFA500", bold=True).print( |
| 486 | f"Resetting terminal session {session}... Reason: {reason}" |
| 487 | ) |
| 488 | else: |
| 489 | PrintStyle(font_color="#FFA500", bold=True).print( |
| 490 | f"Resetting terminal session {session}..." |
| 491 | ) |
| 492 | |
| 493 | await self.prepare_state(cfg, reset=True, session=session) |
| 494 | response = self.agent.read_prompt( |
| 495 | "fw.code.info.md", info=self.agent.read_prompt("fw.code.reset.md") |
| 496 | ) |
| 497 | self.log.update(content=response) |
| 498 | return response |
| 499 | |
| 500 | def get_heading_from_output(self, output: str, skip_lines=0, done=False): |
| 501 | done_icon = " icon://done_all" if done else "" |
| 502 | |
| 503 | if not output: |
| 504 | return self.get_heading() + done_icon |
| 505 | |
| 506 | lines = output.splitlines() |
| 507 | for i in range(len(lines) - skip_lines - 1, -1, -1): |
| 508 | line = lines[i].strip() |
| 509 | if not line: |
| 510 | continue |
| 511 | return self.get_heading(line) + done_icon |
| 512 | |
| 513 | return self.get_heading() + done_icon |
| 514 | |
| 515 | def fix_full_output(self, output: str): |
| 516 | output = re.sub(r"(?<!\\)\\x[0-9A-Fa-f]{2}", "", output) |
| 517 | output = truncate_text_agent(agent=self.agent, output=output, threshold=1000000) |
| 518 | return output |
| 519 | |
| 520 | async def ensure_cwd(self) -> str | None: |
| 521 | project_name = projects.get_context_project_name(self.agent.context) |
| 522 | if project_name: |
| 523 | path = projects.get_project_folder(project_name) |
| 524 | else: |
| 525 | set = settings.get_settings() |
| 526 | path = set.get("workdir_path") |
| 527 | |
| 528 | if not path: |
| 529 | return None |
| 530 | |
| 531 | normalized = files.normalize_a0_path(path) |
| 532 | await runtime.call_development_function(make_dir, normalized) |
| 533 | return normalized |
| 534 | |
| 535 | |
| 536 | # ------------------------------------------------------------------ |
| 537 | # Internal |
| 538 | # ------------------------------------------------------------------ |
| 539 | |
| 540 | def _resolve_ssh_enabled(raw_value) -> bool: |
| 541 | val = str(raw_value).strip().lower() |
| 542 | if val == "auto": |
| 543 | return not runtime.is_dockerized() |
| 544 | return val in ("true", "1", "yes", "on") |
| 545 | |
| 546 | |
| 547 | def _resolve_ssh_addr(cfg_addr: str) -> str: |
| 548 | if cfg_addr: |
| 549 | return cfg_addr |
| 550 | set = settings.get_settings() |
| 551 | host = set.get("rfc_url", "localhost") |
| 552 | if "//" in host: |
| 553 | host = host.split("//")[1] |
| 554 | if ":" in host: |
| 555 | host = host.split(":")[0] |
| 556 | if host.endswith("/"): |
| 557 | host = host.rstrip("/") |
| 558 | return host or "localhost" |
| 559 | |
| 560 | |
| 561 | async def _resolve_ssh_pass(cfg_pass: str) -> str: |
| 562 | if cfg_pass: |
| 563 | return cfg_pass |
| 564 | return await rfc_exchange.get_root_password() |
| 565 | |
| 566 | |
| 567 | def _parse_patterns(raw, flags=0) -> list[re.Pattern]: |
| 568 | lines = [str(p) for p in raw] if isinstance(raw, list) else str(raw).splitlines() |
| 569 | return [re.compile(p.strip(), flags) for p in lines if p.strip()] |
| 570 | |
| 571 | |
| 572 | _TIMEOUT_KEYS = ("first_output_timeout", "between_output_timeout", "max_exec_timeout", "dialog_timeout") |
| 573 | |
| 574 | |
| 575 | def _parse_timeouts(cfg: dict, prefix: str, defaults: tuple[int, ...]) -> dict: |
| 576 | return { |
| 577 | key: int(cfg.get(f"{prefix}_{key}", default)) |
| 578 | for key, default in zip(_TIMEOUT_KEYS, defaults) |
| 579 | } |
| 580 | |
| 581 | |
| 582 | def _get_config(agent) -> dict: |
| 583 | cfg = plugins.get_plugin_config("_code_execution", agent=agent) or {} |
| 584 | |
| 585 | return { |
| 586 | "ssh_enabled": _resolve_ssh_enabled(cfg.get("ssh_enabled", "auto")), |
| 587 | "ssh_addr": _resolve_ssh_addr(str(cfg.get("ssh_addr", ""))), |
| 588 | "ssh_port": int(cfg.get("ssh_port", 55022)), |
| 589 | "ssh_user": str(cfg.get("ssh_user", "root")), |
| 590 | "ssh_pass": str(cfg.get("ssh_pass", "")), |
| 591 | "code_exec_timeouts": _parse_timeouts(cfg, "code_exec", (30, 15, 240, 5)), |
| 592 | "output_timeouts": _parse_timeouts(cfg, "output", (120, 60, 600, 5)), |
| 593 | "prompt_patterns": _parse_patterns(cfg.get("prompt_patterns", "")), |
| 594 | "dialog_patterns": _parse_patterns(cfg.get("dialog_patterns", ""), re.IGNORECASE), |
| 595 | } |
| 596 | |
| 597 | |
| 598 | def make_dir(path: str): |
| 599 | import os |
| 600 | os.makedirs(path, exist_ok=True) |