| | """File operation interfaces and implementations for local and sandbox environments.""" |
| |
|
| | import asyncio |
| | from pathlib import Path |
| | from typing import Optional, Protocol, Tuple, Union, runtime_checkable |
| |
|
| | from app.config import SandboxSettings |
| | from app.exceptions import ToolError |
| | from app.sandbox.client import SANDBOX_CLIENT |
| |
|
| |
|
| | PathLike = Union[str, Path] |
| |
|
| |
|
| | @runtime_checkable |
| | class FileOperator(Protocol): |
| | """Interface for file operations in different environments.""" |
| |
|
| | async def read_file(self, path: PathLike) -> str: |
| | """Read content from a file.""" |
| | ... |
| |
|
| | async def write_file(self, path: PathLike, content: str) -> None: |
| | """Write content to a file.""" |
| | ... |
| |
|
| | async def is_directory(self, path: PathLike) -> bool: |
| | """Check if path points to a directory.""" |
| | ... |
| |
|
| | async def exists(self, path: PathLike) -> bool: |
| | """Check if path exists.""" |
| | ... |
| |
|
| | async def run_command( |
| | self, cmd: str, timeout: Optional[float] = 120.0 |
| | ) -> Tuple[int, str, str]: |
| | """Run a shell command and return (return_code, stdout, stderr).""" |
| | ... |
| |
|
| |
|
| | class LocalFileOperator(FileOperator): |
| | """File operations implementation for local filesystem.""" |
| |
|
| | encoding: str = "utf-8" |
| |
|
| | async def read_file(self, path: PathLike) -> str: |
| | """Read content from a local file.""" |
| | try: |
| | return Path(path).read_text(encoding=self.encoding) |
| | except Exception as e: |
| | raise ToolError(f"Failed to read {path}: {str(e)}") from None |
| |
|
| | async def write_file(self, path: PathLike, content: str) -> None: |
| | """Write content to a local file.""" |
| | try: |
| | Path(path).write_text(content, encoding=self.encoding) |
| | except Exception as e: |
| | raise ToolError(f"Failed to write to {path}: {str(e)}") from None |
| |
|
| | async def is_directory(self, path: PathLike) -> bool: |
| | """Check if path points to a directory.""" |
| | return Path(path).is_dir() |
| |
|
| | async def exists(self, path: PathLike) -> bool: |
| | """Check if path exists.""" |
| | return Path(path).exists() |
| |
|
| | async def run_command( |
| | self, cmd: str, timeout: Optional[float] = 120.0 |
| | ) -> Tuple[int, str, str]: |
| | """Run a shell command locally.""" |
| | process = await asyncio.create_subprocess_shell( |
| | cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE |
| | ) |
| |
|
| | try: |
| | stdout, stderr = await asyncio.wait_for( |
| | process.communicate(), timeout=timeout |
| | ) |
| | return ( |
| | process.returncode or 0, |
| | stdout.decode(), |
| | stderr.decode(), |
| | ) |
| | except asyncio.TimeoutError as exc: |
| | try: |
| | process.kill() |
| | except ProcessLookupError: |
| | pass |
| | raise TimeoutError( |
| | f"Command '{cmd}' timed out after {timeout} seconds" |
| | ) from exc |
| |
|
| |
|
| | class SandboxFileOperator(FileOperator): |
| | """File operations implementation for sandbox environment.""" |
| |
|
| | def __init__(self): |
| | self.sandbox_client = SANDBOX_CLIENT |
| |
|
| | async def _ensure_sandbox_initialized(self): |
| | """Ensure sandbox is initialized.""" |
| | if not self.sandbox_client.sandbox: |
| | await self.sandbox_client.create(config=SandboxSettings()) |
| |
|
| | async def read_file(self, path: PathLike) -> str: |
| | """Read content from a file in sandbox.""" |
| | await self._ensure_sandbox_initialized() |
| | try: |
| | return await self.sandbox_client.read_file(str(path)) |
| | except Exception as e: |
| | raise ToolError(f"Failed to read {path} in sandbox: {str(e)}") from None |
| |
|
| | async def write_file(self, path: PathLike, content: str) -> None: |
| | """Write content to a file in sandbox.""" |
| | await self._ensure_sandbox_initialized() |
| | try: |
| | await self.sandbox_client.write_file(str(path), content) |
| | except Exception as e: |
| | raise ToolError(f"Failed to write to {path} in sandbox: {str(e)}") from None |
| |
|
| | async def is_directory(self, path: PathLike) -> bool: |
| | """Check if path points to a directory in sandbox.""" |
| | await self._ensure_sandbox_initialized() |
| | result = await self.sandbox_client.run_command( |
| | f"test -d {path} && echo 'true' || echo 'false'" |
| | ) |
| | return result.strip() == "true" |
| |
|
| | async def exists(self, path: PathLike) -> bool: |
| | """Check if path exists in sandbox.""" |
| | await self._ensure_sandbox_initialized() |
| | result = await self.sandbox_client.run_command( |
| | f"test -e {path} && echo 'true' || echo 'false'" |
| | ) |
| | return result.strip() == "true" |
| |
|
| | async def run_command( |
| | self, cmd: str, timeout: Optional[float] = 120.0 |
| | ) -> Tuple[int, str, str]: |
| | """Run a command in sandbox environment.""" |
| | await self._ensure_sandbox_initialized() |
| | try: |
| | stdout = await self.sandbox_client.run_command( |
| | cmd, timeout=int(timeout) if timeout else None |
| | ) |
| | return ( |
| | 0, |
| | stdout, |
| | "", |
| | ) |
| | except TimeoutError as exc: |
| | raise TimeoutError( |
| | f"Command '{cmd}' timed out after {timeout} seconds in sandbox" |
| | ) from exc |
| | except Exception as exc: |
| | return 1, "", f"Error executing command in sandbox: {str(exc)}" |
| |
|