|
19 | 19 | import sys |
20 | 20 | import threading |
21 | 21 | import time |
| 22 | +import uuid |
22 | 23 | from queue import Empty, Full, Queue |
23 | 24 | from typing import Any, Dict, List, Optional |
24 | 25 |
|
@@ -1228,6 +1229,134 @@ def shell_ask_user_for_help(self, id: str, prompt: str) -> str: |
1228 | 1229 | except EOFError: |
1229 | 1230 | return f"User input interrupted for session '{id}'." |
1230 | 1231 |
|
| 1232 | + def shell_write_content_to_file(self, content: str, file_path: str) -> str: |
| 1233 | + r"""Writes the specified content to a file at the given path. |
| 1234 | +
|
| 1235 | + Args: |
| 1236 | + content (str): The content to write to the file. |
| 1237 | + file_path (str): The path to the file where the content should |
| 1238 | + be written. Can be absolute or relative to working_dir. |
| 1239 | +
|
| 1240 | + Returns: |
| 1241 | + str: A confirmation message indicating success or an error message. |
| 1242 | + """ |
| 1243 | + # For local backend, resolve relative paths to working_dir |
| 1244 | + if not self.use_docker_backend and self.working_dir: |
| 1245 | + if not os.path.isabs(file_path): |
| 1246 | + file_path = os.path.normpath( |
| 1247 | + os.path.join(self.working_dir, file_path) |
| 1248 | + ) |
| 1249 | + else: |
| 1250 | + file_path = os.path.normpath(file_path) |
| 1251 | + file_path = os.path.abspath(file_path) |
| 1252 | + |
| 1253 | + # Safe mode path containment check for local backend |
| 1254 | + if self.safe_mode: |
| 1255 | + working_dir_normalized = os.path.normpath( |
| 1256 | + os.path.abspath(self.working_dir) |
| 1257 | + ) |
| 1258 | + # Use os.path.commonpath for secure path containment check |
| 1259 | + try: |
| 1260 | + common = os.path.commonpath( |
| 1261 | + [file_path, working_dir_normalized] |
| 1262 | + ) |
| 1263 | + if common != working_dir_normalized: |
| 1264 | + return ( |
| 1265 | + "Error: Cannot write to a file outside of the " |
| 1266 | + "working directory in safe mode." |
| 1267 | + ) |
| 1268 | + except ValueError: |
| 1269 | + # Paths are on different drives (Windows) or invalid |
| 1270 | + return ( |
| 1271 | + "Error: Cannot write to a file outside of the " |
| 1272 | + "working directory in safe mode." |
| 1273 | + ) |
| 1274 | + |
| 1275 | + log_entry = ( |
| 1276 | + f"--- Writing content to file at {time.ctime()} ---\n" |
| 1277 | + f"> {file_path}\n" |
| 1278 | + ) |
| 1279 | + if self.use_docker_backend: |
| 1280 | + temp_host_path = None |
| 1281 | + try: |
| 1282 | + # Ensure parent directory exists in container |
| 1283 | + parent_dir = os.path.dirname(file_path) |
| 1284 | + if parent_dir: |
| 1285 | + quoted_dir = shlex.quote(parent_dir) |
| 1286 | + mkdir_cmd = f'sh -lc "mkdir -p {quoted_dir}"' |
| 1287 | + mkdir_exec = self.docker_api_client.exec_create( |
| 1288 | + self.container.id, mkdir_cmd |
| 1289 | + ) |
| 1290 | + self.docker_api_client.exec_start(mkdir_exec['Id']) |
| 1291 | + |
| 1292 | + # Write content to a temporary file on the host inside log_dir |
| 1293 | + temp_file_name = f"temp_{uuid.uuid4().hex}.txt" |
| 1294 | + temp_host_path = os.path.join(self.log_dir, temp_file_name) |
| 1295 | + with open(temp_host_path, "w", encoding="utf-8") as f: |
| 1296 | + f.write(content) |
| 1297 | + # Copy the temporary file into the Docker container |
| 1298 | + dest_path_in_container = file_path |
| 1299 | + container_dest = ( |
| 1300 | + f"{self.container.name}:{dest_path_in_container}" |
| 1301 | + ) |
| 1302 | + subprocess.run( |
| 1303 | + ['docker', 'cp', temp_host_path, container_dest], |
| 1304 | + check=True, |
| 1305 | + capture_output=True, |
| 1306 | + text=True, |
| 1307 | + ) |
| 1308 | + |
| 1309 | + log_entry += f"\n-------- \n{content}\n--------\n" |
| 1310 | + with open(self.blocking_log_file, "a", encoding="utf-8") as f: |
| 1311 | + f.write(log_entry + "\n") |
| 1312 | + return ( |
| 1313 | + f"Content successfully written to '{file_path}' " |
| 1314 | + f"in Docker container." |
| 1315 | + ) |
| 1316 | + except subprocess.CalledProcessError as e: |
| 1317 | + log_entry += f"--- Error ---\n{e.stderr}\n" |
| 1318 | + with open(self.blocking_log_file, "a", encoding="utf-8") as f: |
| 1319 | + f.write(log_entry + "\n") |
| 1320 | + return ( |
| 1321 | + f"Error writing to file '{file_path}' " |
| 1322 | + f"in Docker container: {e.stderr}" |
| 1323 | + ) |
| 1324 | + except Exception as e: |
| 1325 | + log_entry += f"--- Error ---\n{e}\n" |
| 1326 | + with open(self.blocking_log_file, "a", encoding="utf-8") as f: |
| 1327 | + f.write(log_entry + "\n") |
| 1328 | + return ( |
| 1329 | + f"Error writing to file '{file_path}' " |
| 1330 | + f"in Docker container: {e}" |
| 1331 | + ) |
| 1332 | + finally: |
| 1333 | + # Clean up the temporary file |
| 1334 | + if temp_host_path and os.path.exists(temp_host_path): |
| 1335 | + try: |
| 1336 | + os.remove(temp_host_path) |
| 1337 | + except OSError: |
| 1338 | + pass |
| 1339 | + |
| 1340 | + else: |
| 1341 | + try: |
| 1342 | + # Ensure parent directory exists |
| 1343 | + parent_dir = os.path.dirname(file_path) |
| 1344 | + if parent_dir: |
| 1345 | + os.makedirs(parent_dir, exist_ok=True) |
| 1346 | + |
| 1347 | + with open(file_path, "w", encoding="utf-8") as f: |
| 1348 | + f.write(content) |
| 1349 | + |
| 1350 | + log_entry += f"\n-------- \n{content}\n--------\n" |
| 1351 | + with open(self.blocking_log_file, "a", encoding="utf-8") as f: |
| 1352 | + f.write(log_entry + "\n") |
| 1353 | + return f"Content successfully written to '{file_path}'." |
| 1354 | + except Exception as e: |
| 1355 | + log_entry += f"--- Error ---\n{e}\n" |
| 1356 | + with open(self.blocking_log_file, "a", encoding="utf-8") as f: |
| 1357 | + f.write(log_entry + "\n") |
| 1358 | + return f"Error writing to file '{file_path}': {e}" |
| 1359 | + |
1231 | 1360 | def __enter__(self): |
1232 | 1361 | r"""Context manager entry.""" |
1233 | 1362 | return self |
@@ -1275,6 +1404,7 @@ def get_tools(self) -> List[FunctionTool]: |
1275 | 1404 | return [ |
1276 | 1405 | FunctionTool(self.shell_exec), |
1277 | 1406 | FunctionTool(self.shell_view), |
| 1407 | + FunctionTool(self.shell_write_content_to_file), |
1278 | 1408 | FunctionTool(self.shell_write_to_process), |
1279 | 1409 | FunctionTool(self.shell_kill_process), |
1280 | 1410 | FunctionTool(self.shell_ask_user_for_help), |
|
0 commit comments