Spaces:
Running
Running
| """File editor tools — view, create, and edit files in E2B sandbox.""" | |
| from __future__ import annotations | |
| import logging | |
| from frontier_agent.core.tool import tool | |
| from plugins.tools._deliverable_policy import output_write_error | |
| from plugins.tools._path_auth import _authorized_local_path | |
| from plugins.tools._sandbox import ( | |
| aget_sandbox, | |
| arun_sandbox_cmd, | |
| asandbox_write_file, | |
| ) | |
| logger = logging.getLogger(__name__) | |
| _MAX_OUTPUT_CHARS = 16_000 | |
| def _truncate(text: str, max_chars: int = _MAX_OUTPUT_CHARS) -> str: | |
| """Truncate output if too long.""" | |
| if len(text) > max_chars: | |
| return text[:max_chars] + "\n\n[... output truncated]" | |
| return text | |
| async def file_editor_view(path: str, view_range: str = "") -> str: | |
| """View file contents or list directory in E2B sandbox. | |
| Args: | |
| path: Absolute path to file or directory. | |
| view_range: Optional line range like "1-50" to view specific lines. | |
| Returns: | |
| File contents with line numbers, or directory listing. | |
| """ | |
| if not path or not path.strip(): | |
| return "Error: path is required." | |
| local_path, _reason = _authorized_local_path(path) | |
| if local_path is not None: | |
| from plugins.tools.ignore_rules import discover_repo_root, should_ignore_path | |
| if local_path.is_dir(): | |
| repo_root = discover_repo_root(local_path) | |
| entries: list[str] = [] | |
| for entry in sorted(local_path.rglob("*")): | |
| if should_ignore_path(entry, repo_root): | |
| continue | |
| try: | |
| rel = entry.relative_to(local_path) | |
| except ValueError: | |
| rel = entry | |
| if len(rel.parts) > 2: | |
| continue | |
| entries.append(str(rel)) | |
| if len(entries) >= 100: | |
| break | |
| return "\n".join(entries) if entries else f"(empty directory: {path})" | |
| if not local_path.is_file(): | |
| return f"Error: File not found: {path}" | |
| try: | |
| lines = local_path.read_text(encoding="utf-8").splitlines() | |
| except Exception as e: | |
| return f"Error viewing {path}: {e}" | |
| if view_range and "-" in view_range: | |
| start_s, end_s = view_range.split("-", 1) | |
| start = max(int(start_s.strip() or "1"), 1) | |
| end = max(int(end_s.strip() or str(start)), start) | |
| else: | |
| start, end = 1, len(lines) | |
| rendered = "\n".join(f"{idx}: {line}" for idx, line in enumerate(lines[start - 1:end], start=start)) | |
| return _truncate(rendered or "(empty file)") | |
| try: | |
| sandbox = await aget_sandbox() | |
| except RuntimeError as e: | |
| return f"Error: {e}" | |
| try: | |
| check = await arun_sandbox_cmd( | |
| sandbox, | |
| f"test -d {path} && echo DIR || echo FILE", timeout=10, | |
| ) | |
| is_dir = "DIR" in check.stdout | |
| if is_dir: | |
| result = await arun_sandbox_cmd( | |
| sandbox, | |
| f"find {path} -maxdepth 2 -not -path '*/.git/*' " | |
| f"-not -path '*/node_modules/*' -not -path '*/__pycache__/*' " | |
| f"| head -100 | sort", | |
| timeout=15, | |
| ) | |
| return _truncate(result.stdout or f"(empty directory: {path})") | |
| # File view | |
| if view_range: | |
| parts = view_range.split("-") | |
| if len(parts) == 2: | |
| start, end = parts[0].strip(), parts[1].strip() | |
| cmd = f"sed -n '{start},{end}p' {path} | cat -n" | |
| else: | |
| cmd = f"cat -n {path}" | |
| else: | |
| cmd = f"cat -n {path}" | |
| result = await arun_sandbox_cmd(sandbox, cmd, timeout=15) | |
| if result.exit_code != 0: | |
| return f"Error: {result.stderr or 'File not found'}" | |
| return _truncate(result.stdout or "(empty file)") | |
| except Exception as e: | |
| return f"Error viewing {path}: {e}" | |
| async def file_editor_create(path: str, content: str) -> str: | |
| """Create a new file in E2B sandbox with the given content. | |
| Use this to create new files. For modifying existing files, use file_editor_str_replace instead. | |
| Args: | |
| path: Absolute path for the new file. | |
| content: The file content to write. | |
| Returns: | |
| Confirmation message. | |
| """ | |
| if not path or not path.strip(): | |
| return "Error: path is required." | |
| deliverable_error = output_write_error(path) | |
| if deliverable_error: | |
| return f"Error: {deliverable_error}" | |
| local_path, reason = _authorized_local_path(path, write_access=True) | |
| if local_path is not None: | |
| try: | |
| local_path.parent.mkdir(parents=True, exist_ok=True) | |
| local_path.write_text(content, encoding="utf-8") | |
| lines = content.count("\n") + (1 if content else 0) | |
| return f"File created: {path} ({lines} lines, {len(content)} bytes)" | |
| except Exception as e: | |
| return f"Error creating {path}: {e}" | |
| try: | |
| sandbox = await aget_sandbox() | |
| except RuntimeError as e: | |
| return f"Error: {e}" if "allowed" not in reason.lower() else f"Access denied: {reason}" | |
| try: | |
| import os | |
| parent = os.path.dirname(path) | |
| if parent: | |
| await arun_sandbox_cmd( | |
| sandbox, f"mkdir -p {parent}", timeout=10, | |
| ) | |
| # Write via python in sandbox (avoids files.write permission issues) | |
| ok, err = await asandbox_write_file(sandbox, path, content) | |
| if not ok: | |
| return f"Error creating {path}: {err}" | |
| result = await arun_sandbox_cmd( | |
| sandbox, f"wc -l < {path}", timeout=5, | |
| ) | |
| lines = result.stdout.strip() if result.stdout else "?" | |
| return f"File created: {path} ({lines} lines, {len(content)} bytes)" | |
| except Exception as e: | |
| return f"Error creating {path}: {e}" | |
| async def file_editor_str_replace(path: str, old_str: str, new_str: str) -> str: | |
| """Replace a string occurrence in a file in E2B sandbox. | |
| The old_str must appear exactly once in the file (for safety). | |
| Use file_editor_view first to see the file contents. | |
| Args: | |
| path: Absolute path to the file. | |
| old_str: The exact string to find and replace. | |
| new_str: The replacement string. | |
| Returns: | |
| Confirmation with a diff summary. | |
| """ | |
| if not path or not old_str: | |
| return "Error: path and old_str are required." | |
| deliverable_error = output_write_error(path) | |
| if deliverable_error: | |
| return f"Error: {deliverable_error}" | |
| local_path, reason = _authorized_local_path(path, write_access=True) | |
| if local_path is not None: | |
| if not local_path.is_file(): | |
| return f"Error: Cannot read {path}: file not found" | |
| try: | |
| content = local_path.read_text(encoding="utf-8") | |
| except Exception as e: | |
| return f"Error editing {path}: {e}" | |
| count = content.count(old_str) | |
| if count == 0: | |
| return ( | |
| f"Error: old_str not found in {path}. " | |
| "Make sure the string matches exactly (including whitespace and indentation)." | |
| ) | |
| if count > 1: | |
| return ( | |
| f"Error: old_str found {count} times in {path}. " | |
| "Provide a more specific string that appears exactly once." | |
| ) | |
| try: | |
| local_path.write_text(content.replace(old_str, new_str, 1), encoding="utf-8") | |
| except Exception as e: | |
| return f"Error editing {path}: {e}" | |
| old_lines = old_str.count("\n") + 1 | |
| new_lines = new_str.count("\n") + 1 | |
| return f"Replaced in {path}: {old_lines} line(s) → {new_lines} line(s)" | |
| try: | |
| sandbox = await aget_sandbox() | |
| except RuntimeError as e: | |
| return f"Error: {e}" if "allowed" not in reason.lower() else f"Access denied: {reason}" | |
| try: | |
| # Read current content via cat (more reliable than files.read for all paths) | |
| result = await arun_sandbox_cmd(sandbox, f"cat {path}", timeout=10) | |
| if result.exit_code != 0: | |
| return f"Error: Cannot read {path}: {result.stderr or 'file not found'}" | |
| content = result.stdout | |
| count = content.count(old_str) | |
| if count == 0: | |
| return ( | |
| f"Error: old_str not found in {path}. " | |
| "Make sure the string matches exactly (including whitespace and indentation)." | |
| ) | |
| if count > 1: | |
| return ( | |
| f"Error: old_str found {count} times in {path}. " | |
| "Provide a more specific string that appears exactly once." | |
| ) | |
| # Replace and write back via python in sandbox (avoids files.write permission issues) | |
| new_content = content.replace(old_str, new_str, 1) | |
| ok, err = await asandbox_write_file(sandbox, path, new_content) | |
| if not ok: | |
| return f"Error writing {path}: {err}" | |
| # Show diff stats | |
| old_lines = old_str.count("\n") + 1 | |
| new_lines = new_str.count("\n") + 1 | |
| return f"Replaced in {path}: {old_lines} line(s) → {new_lines} line(s)" | |
| except Exception as e: | |
| return f"Error editing {path}: {e}" | |