Skip to content

Daytona API

DaytonaSandbox

pydantic_ai_backends.backends.daytona.DaytonaSandbox

Bases: BaseSandbox

Daytona cloud sandbox backend.

Creates an ephemeral Daytona sandbox for running commands and managing files in an isolated cloud environment. Uses native Daytona file APIs for read_bytes and write; all other file helpers are inherited from :class:BaseSandbox (shell-based).

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
class DaytonaSandbox(BaseSandbox):  # pragma: no cover
    """Daytona cloud sandbox backend.

    Creates an ephemeral Daytona sandbox for running commands and managing
    files in an isolated cloud environment.  Uses native Daytona file APIs
    for `read_bytes` and `write`; all other file helpers are inherited
    from :class:`BaseSandbox` (shell-based).
    """

    def __init__(
        self,
        api_key: str | None = None,
        sandbox_id: str | None = None,
        work_dir: str = "/home/daytona",
        startup_timeout: int = 180,
    ):
        """Initialise and start a Daytona sandbox.

        Args:
            api_key: Daytona API key.  Falls back to `DAYTONA_API_KEY` env var.
            sandbox_id: Optional identifier (generated by Daytona if omitted).
            work_dir: Working directory inside the sandbox.
            startup_timeout: Seconds to wait for the sandbox to become ready.

        Raises:
            ValueError: If no API key is provided or found in environment.
            RuntimeError: If the sandbox fails to start within *startup_timeout*.
        """
        from daytona import Daytona, DaytonaConfig

        resolved_key = api_key or os.environ.get("DAYTONA_API_KEY")
        if not resolved_key:
            raise ValueError("Daytona API key is required. Pass api_key= or set DAYTONA_API_KEY.")

        self._client: Any = Daytona(DaytonaConfig(api_key=resolved_key))
        self._sandbox: Any = self._client.create()
        self._work_dir = work_dir

        # Wait until sandbox is responsive
        self._wait_until_ready(startup_timeout)

        super().__init__(sandbox_id or self._sandbox.id)

    # ------------------------------------------------------------------
    # Lifecycle helpers
    # ------------------------------------------------------------------

    def _wait_until_ready(self, timeout: int) -> None:
        """Poll until the sandbox responds to a simple command."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            try:
                result = self._sandbox.process.exec("echo ready", timeout=5)
                if result.exit_code == 0:
                    return
            except Exception:
                pass
            time.sleep(2)

        # Clean up on failure
        try:
            self._client.delete(self._sandbox)
        finally:
            raise RuntimeError(f"Daytona sandbox failed to start within {timeout} seconds")

    # ------------------------------------------------------------------
    # SandboxProtocol — execute
    # ------------------------------------------------------------------

    def start(self) -> None:
        """No-op — Daytona sandboxes start automatically on creation."""

    def execute(self, command: str, timeout: int | None = None) -> ExecuteResponse:
        """Execute a command inside the Daytona sandbox.

        Args:
            command: Shell command string.
            timeout: Maximum execution time in seconds (`None` → 30 min).

        Returns:
            :class:`ExecuteResponse` with output, exit code, and truncation flag.
        """
        self._last_activity = time.time()
        effective_timeout = timeout if timeout is not None else 30 * 60
        try:
            result = self._sandbox.process.exec(
                command, cwd=self._work_dir, timeout=effective_timeout
            )
            output = result.result
            truncated = len(output) > _MAX_OUTPUT
            if truncated:
                output = output[:_MAX_OUTPUT]
            return ExecuteResponse(
                output=output,
                exit_code=result.exit_code,
                truncated=truncated,
            )
        except Exception as e:
            return ExecuteResponse(output=f"Error: {e}", exit_code=1, truncated=False)

    # ------------------------------------------------------------------
    # File I/O — native Daytona APIs
    # ------------------------------------------------------------------

    def exists(self, path: str) -> bool:
        """Check existence via Daytona's native file API.

        Falls back to `False` for any error (missing file, permission
        denied, transient API failure) — mirroring the broad exception
        handling in :meth:`read_bytes`.
        """
        try:
            info = self._sandbox.fs.get_file_info(path)
        except Exception:
            return False
        return not bool(getattr(info, "is_dir", False))

    def read_bytes(self, path: str) -> bytes:
        """Download file bytes via Daytona's native file API."""
        from daytona import FileDownloadRequest

        try:
            responses = self._sandbox.fs.download_files([FileDownloadRequest(source=path)])
            data = responses[0].result
            return data.encode() if isinstance(data, str) else data
        except Exception:
            # Return empty bytes on failure so callers cannot mistake an error
            # message for real file content.
            return b""

    def write(self, path: str, content: str | bytes) -> WriteResult:
        """Upload a file via Daytona's native file API.

        Creates parent directories automatically.

        Args:
            path: Absolute path inside the sandbox.
            content: File content (str or bytes).

        Returns:
            :class:`WriteResult` with path on success or error message.
        """
        from daytona import FileUpload

        try:
            # Ensure parent directory
            parent = str(PurePosixPath(path).parent)
            self.execute(f"mkdir -p {shlex.quote(parent)}")

            payload = content if isinstance(content, bytes) else content.encode()
            self._sandbox.fs.upload_files([FileUpload(source=payload, destination=path)])
            return WriteResult(path=path)
        except Exception as e:
            return WriteResult(error=f"Failed to write file: {e}")

    # ------------------------------------------------------------------
    # Edit — read → Python replace → write (same pattern as DockerSandbox)
    # ------------------------------------------------------------------

    def edit(
        self, path: str, old_string: str, new_string: str, replace_all: bool = False
    ) -> EditResult:
        """Edit a file by replacing strings.

        Reads the file, performs replacement in Python, writes back.

        Args:
            path: File path to edit.
            old_string: String to find.
            new_string: Replacement string.
            replace_all: Replace all occurrences (default: first only).

        Returns:
            :class:`EditResult` with path and occurrence count, or error.
        """
        try:
            if not self.exists(path):
                return EditResult(error=f"File not found: {path}")

            file_bytes = self.read_bytes(path)
            content = file_bytes.decode("utf-8", errors="replace")
            occurrences = content.count(old_string)

            if occurrences == 0:
                return EditResult(error="String not found in file")

            if occurrences > 1 and not replace_all:
                return EditResult(
                    error=f"String found {occurrences} times. "
                    "Use replace_all=True to replace all, or provide more context."
                )

            new_content = content.replace(old_string, new_string)
            write_result = self.write(path, new_content)

            if write_result.error:
                return EditResult(error=write_result.error)

            return EditResult(path=path, occurrences=occurrences)
        except Exception as e:
            return EditResult(error=f"Failed to edit file: {e}")

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    def is_alive(self) -> bool:
        """Check if the sandbox is responsive."""
        try:
            result = self.execute("echo ok", timeout=10)
            return result.exit_code == 0
        except Exception:
            return False

    def stop(self) -> None:
        """Delete the Daytona sandbox."""
        client = getattr(self, "_client", None)
        sandbox = getattr(self, "_sandbox", None)
        if client is not None and sandbox is not None:
            with contextlib.suppress(Exception):
                client.delete(sandbox)
            self._sandbox = None

    def __del__(self) -> None:
        """Cleanup sandbox on garbage collection."""
        if hasattr(self, "_sandbox"):
            self.stop()

__init__(api_key=None, sandbox_id=None, work_dir='/home/daytona', startup_timeout=180)

Initialise and start a Daytona sandbox.

Parameters:

Name Type Description Default
api_key str | None

Daytona API key. Falls back to DAYTONA_API_KEY env var.

None
sandbox_id str | None

Optional identifier (generated by Daytona if omitted).

None
work_dir str

Working directory inside the sandbox.

'/home/daytona'
startup_timeout int

Seconds to wait for the sandbox to become ready.

180

Raises:

Type Description
ValueError

If no API key is provided or found in environment.

RuntimeError

If the sandbox fails to start within startup_timeout.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def __init__(
    self,
    api_key: str | None = None,
    sandbox_id: str | None = None,
    work_dir: str = "/home/daytona",
    startup_timeout: int = 180,
):
    """Initialise and start a Daytona sandbox.

    Args:
        api_key: Daytona API key.  Falls back to `DAYTONA_API_KEY` env var.
        sandbox_id: Optional identifier (generated by Daytona if omitted).
        work_dir: Working directory inside the sandbox.
        startup_timeout: Seconds to wait for the sandbox to become ready.

    Raises:
        ValueError: If no API key is provided or found in environment.
        RuntimeError: If the sandbox fails to start within *startup_timeout*.
    """
    from daytona import Daytona, DaytonaConfig

    resolved_key = api_key or os.environ.get("DAYTONA_API_KEY")
    if not resolved_key:
        raise ValueError("Daytona API key is required. Pass api_key= or set DAYTONA_API_KEY.")

    self._client: Any = Daytona(DaytonaConfig(api_key=resolved_key))
    self._sandbox: Any = self._client.create()
    self._work_dir = work_dir

    # Wait until sandbox is responsive
    self._wait_until_ready(startup_timeout)

    super().__init__(sandbox_id or self._sandbox.id)

execute(command, timeout=None)

Execute a command inside the Daytona sandbox.

Parameters:

Name Type Description Default
command str

Shell command string.

required
timeout int | None

Maximum execution time in seconds (None → 30 min).

None

Returns:

Type Description
ExecuteResponse

class:ExecuteResponse with output, exit code, and truncation flag.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def execute(self, command: str, timeout: int | None = None) -> ExecuteResponse:
    """Execute a command inside the Daytona sandbox.

    Args:
        command: Shell command string.
        timeout: Maximum execution time in seconds (`None` → 30 min).

    Returns:
        :class:`ExecuteResponse` with output, exit code, and truncation flag.
    """
    self._last_activity = time.time()
    effective_timeout = timeout if timeout is not None else 30 * 60
    try:
        result = self._sandbox.process.exec(
            command, cwd=self._work_dir, timeout=effective_timeout
        )
        output = result.result
        truncated = len(output) > _MAX_OUTPUT
        if truncated:
            output = output[:_MAX_OUTPUT]
        return ExecuteResponse(
            output=output,
            exit_code=result.exit_code,
            truncated=truncated,
        )
    except Exception as e:
        return ExecuteResponse(output=f"Error: {e}", exit_code=1, truncated=False)

read_bytes(path)

Download file bytes via Daytona's native file API.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def read_bytes(self, path: str) -> bytes:
    """Download file bytes via Daytona's native file API."""
    from daytona import FileDownloadRequest

    try:
        responses = self._sandbox.fs.download_files([FileDownloadRequest(source=path)])
        data = responses[0].result
        return data.encode() if isinstance(data, str) else data
    except Exception:
        # Return empty bytes on failure so callers cannot mistake an error
        # message for real file content.
        return b""

write(path, content)

Upload a file via Daytona's native file API.

Creates parent directories automatically.

Parameters:

Name Type Description Default
path str

Absolute path inside the sandbox.

required
content str | bytes

File content (str or bytes).

required

Returns:

Type Description
WriteResult

class:WriteResult with path on success or error message.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def write(self, path: str, content: str | bytes) -> WriteResult:
    """Upload a file via Daytona's native file API.

    Creates parent directories automatically.

    Args:
        path: Absolute path inside the sandbox.
        content: File content (str or bytes).

    Returns:
        :class:`WriteResult` with path on success or error message.
    """
    from daytona import FileUpload

    try:
        # Ensure parent directory
        parent = str(PurePosixPath(path).parent)
        self.execute(f"mkdir -p {shlex.quote(parent)}")

        payload = content if isinstance(content, bytes) else content.encode()
        self._sandbox.fs.upload_files([FileUpload(source=payload, destination=path)])
        return WriteResult(path=path)
    except Exception as e:
        return WriteResult(error=f"Failed to write file: {e}")

edit(path, old_string, new_string, replace_all=False)

Edit a file by replacing strings.

Reads the file, performs replacement in Python, writes back.

Parameters:

Name Type Description Default
path str

File path to edit.

required
old_string str

String to find.

required
new_string str

Replacement string.

required
replace_all bool

Replace all occurrences (default: first only).

False

Returns:

Type Description
EditResult

class:EditResult with path and occurrence count, or error.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def edit(
    self, path: str, old_string: str, new_string: str, replace_all: bool = False
) -> EditResult:
    """Edit a file by replacing strings.

    Reads the file, performs replacement in Python, writes back.

    Args:
        path: File path to edit.
        old_string: String to find.
        new_string: Replacement string.
        replace_all: Replace all occurrences (default: first only).

    Returns:
        :class:`EditResult` with path and occurrence count, or error.
    """
    try:
        if not self.exists(path):
            return EditResult(error=f"File not found: {path}")

        file_bytes = self.read_bytes(path)
        content = file_bytes.decode("utf-8", errors="replace")
        occurrences = content.count(old_string)

        if occurrences == 0:
            return EditResult(error="String not found in file")

        if occurrences > 1 and not replace_all:
            return EditResult(
                error=f"String found {occurrences} times. "
                "Use replace_all=True to replace all, or provide more context."
            )

        new_content = content.replace(old_string, new_string)
        write_result = self.write(path, new_content)

        if write_result.error:
            return EditResult(error=write_result.error)

        return EditResult(path=path, occurrences=occurrences)
    except Exception as e:
        return EditResult(error=f"Failed to edit file: {e}")

exists(path)

Check existence via Daytona's native file API.

Falls back to False for any error (missing file, permission denied, transient API failure) — mirroring the broad exception handling in :meth:read_bytes.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def exists(self, path: str) -> bool:
    """Check existence via Daytona's native file API.

    Falls back to `False` for any error (missing file, permission
    denied, transient API failure) — mirroring the broad exception
    handling in :meth:`read_bytes`.
    """
    try:
        info = self._sandbox.fs.get_file_info(path)
    except Exception:
        return False
    return not bool(getattr(info, "is_dir", False))

start()

No-op — Daytona sandboxes start automatically on creation.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def start(self) -> None:
    """No-op — Daytona sandboxes start automatically on creation."""

stop()

Delete the Daytona sandbox.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def stop(self) -> None:
    """Delete the Daytona sandbox."""
    client = getattr(self, "_client", None)
    sandbox = getattr(self, "_sandbox", None)
    if client is not None and sandbox is not None:
        with contextlib.suppress(Exception):
            client.delete(sandbox)
        self._sandbox = None

is_alive()

Check if the sandbox is responsive.

Source code in src/pydantic_ai_backends/backends/daytona.py
Python
def is_alive(self) -> bool:
    """Check if the sandbox is responsive."""
    try:
        result = self.execute("echo ok", timeout=10)
        return result.exit_code == 0
    except Exception:
        return False