huggingface / huggingface/smolagents
Add exec-sandbox executor (self-hosted QEMU microVM sandbox)
- 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
Assessment
This issue has not been assessed yet.