huggingface / huggingface/smolagents

Add exec-sandbox executor (self-hosted QEMU microVM sandbox)

Open
#2,000 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
29.3k
Forks
3k
Avg merge
17m
Merged PRs (30d)
2

Description

## Summary

Proposing [exec-sandbox](https://github.com/dualeai/exec-sandbox) as a new remote executor. It runs untrusted code in ephemeral QEMU microVMs with hardware-level isolation (KVM on Linux, HVF on macOS) — no cloud account or API key required.

## Why

- **Self-hosted** — Most existing executors (E2B, Modal, Blaxel) require a cloud account. exec-sandbox runs on bare metal with just QEMU. Data never leaves the machine — relevant for customer data privacy and regulated environments. Cheaper at scale.
- **Stateful REPL** — Persistent Python REPL with state across `exec()` calls, same semantics as the Jupyter kernels used by other remote executors.
- **Fast** — ~1-2ms from warm pool, ~200ms from memory snapshots.
- **macOS + Linux** — One codebase (HVF / KVM). Apache-2.0.

## How it maps

exec-sandbox is a Python library with an async API. A `RemotePythonExecutor` subclass bridges async → sync via a dedicated event loop. The key method is `run_code_raise_errors`:

```python
import asyncio
from exec_sandbox import Scheduler

class ExecSandboxExecutor(RemotePythonExecutor):
def __init__(self, additional_imports, logger, allow_pickle=False, **kwargs):
super().__init__(additional_imports, logger, allow_pickle)
self._loop = asyncio.new_event_loop()
self._scheduler = self._loop.run_until_complete(Scheduler().__aenter__())
# NOTE: packages require pinned versions (e.g. ["pandas==2.2.0"]),
# but smolagents passes bare module names — see open questions below.
self._session = self._loop.run_until_complete(
self._scheduler.session(
language="python",
packages=additional_imports or None,
)
)
self.installed_packages = additional_imports or []

def run_code_raise_errors(self, code: str) -> CodeOutput:
result = self._loop.run_until_complete(self._session.exec(code))
# Detect FinalAnswerException from stderr traceback
if result.exit_code != 0 and self.FINAL_ANSWER_EXCEPTION in result.stderr:
for line in reversed(result.stderr.splitlines()):
if f"{self.FINAL_ANSWER_EXCEPTION}:" in line:
value = line.partition(f"{self.FINAL_ANSWER_EXCEPTION}: ")[2]
return CodeOutput(
output=self._deserialize_final_answer(value, self.allow_pickle),
logs=result.stdout, is_final_answer=True,
)
return CodeOutput(output=None, logs=result.stdout, is_final_answer=True)
if result.exit_code != 0:
raise AgentError(result.stderr, self.logger)
return CodeOutput(output=None, logs=result.stdout, is_final_answer=False)

def install_packages(self, packages):
return packages or [] # no-op: packages provisioned at session creation
```

Usable today without forking smolagents:

```python
agent = CodeAgent(tools=[...], model=model, executor=ExecSandboxExecutor(...))
```

For upstream: add to the `remote_executors` dict in `create_python_executor()` + `pyproject.toml` optional extra.

## Open questions

- **Packages** — exec-sandbox requires pinned versions (`pandas==2.2.0`) and validates against a top-10k allowlist. smolagents' `additional_authorized_imports` are bare module names. Needs a mapping convention.
- **FinalAnswerException** — Other executors detect it via Jupyter kernel metadata (`ename`). exec-sandbox surfaces it as a Python traceback in stderr. The sketch parses the last exception line — works but less clean.
- **Async/sync** — The sketch uses a dedicated event loop. A background-thread loop would be safer if smolagents is called from an existing async context.

## Links

- [Repo](https://github.com/dualeai/exec-sandbox) · [PyPI](https://pypi.org/project/exec-sandbox/)
- Reference PR for adding a backend: #1791 (Blaxel)

Happy to submit a PR if there's interest.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.