Skip to content

Add exec-sandbox code executor (self-hosted QEMU microVMs) #4643

@clemlesne

Description

@clemlesne

Add exec-sandbox code executor (self-hosted QEMU microVMs)

Summary

Proposing exec-sandbox as a new BaseCodeExecutor backend. It runs code in ephemeral QEMU microVMs with hardware-level isolation (KVM on Linux, HVF on macOS) — self-hosted, no cloud account needed.

Why

  • Self-hosted — ContainerCodeExecutor needs Docker, GkeCodeExecutor needs GKE, VertexAiCodeExecutor needs Vertex. exec-sandbox runs on bare metal with just QEMU. Data never leaves the machine — relevant for customer data privacy, regulated environments, and cheaper at scale.
  • macOS + Linux — HVF on macOS, KVM on Linux. Same codebase for dev and prod.
  • Fast — ~1-2ms from warm pool, ~200ms from memory snapshots.
  • Stronger isolation than containers — Hardware-level VM boundary vs Docker process isolation.
  • Apache-2.0

How it maps

exec-sandbox is a Python library with an async API. A BaseCodeExecutor subclass bridges async → sync, following the same pattern as ContainerCodeExecutor:

import asyncio
from exec_sandbox import Scheduler

class ExecSandboxCodeExecutor(BaseCodeExecutor):
    stateful: bool = True

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._loop = asyncio.new_event_loop()
        self._scheduler = self._loop.run_until_complete(Scheduler().__aenter__())
        self._sessions: dict[str, Session] = {}

    def execute_code(self, invocation_context, code_execution_input):
        # Reuse session for stateful execution
        eid = code_execution_input.execution_id
        if eid and eid in self._sessions:
            session = self._sessions[eid]
        else:
            session = self._loop.run_until_complete(
                self._scheduler.session(language="python")
            )
            if eid:
                self._sessions[eid] = session

        result = self._loop.run_until_complete(session.exec(code_execution_input.code))
        return CodeExecutionResult(
            stdout=result.stdout,
            stderr=result.stderr,
        )

Usage:

agent = LlmAgent(
    code_executor=ExecSandboxCodeExecutor(),
    ...
)

Open questions

  • Async/sync bridge — exec-sandbox is fully async. The sketch uses a dedicated event loop. A background-thread loop would be safer if called from an existing async context.
  • File I/OCodeExecutionInput.input_files and CodeExecutionResult.output_files could use exec-sandbox's native write_file/read_file APIs instead of writing to a shared volume.
  • Shipping — External package (exec-sandbox[adk] extra) vs upstream in code_executors/ following the GkeCodeExecutor pattern from PR feat(code_executors): Add GkeCodeExecutor for sandboxed code execution on GKE #1629.

Links

  • Repo · PyPI
  • Reference: gke_code_executor.py in code_executors/ (same integration pattern)

Happy to submit a PR if there's interest.

Metadata

Metadata

Assignees

No one assigned

    Labels

    tools[Component] This issue is related to tools

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions