Ejecutar agentes Running Deep en un backend local OpenSandbox
Cómo conectar OpenSandbox con Deep Agents a través de BaseSandbox, sincronizar código asíncrono y síncrono, y ejecutar un agente de análisis de datos de forma segura en un entorno Docker local.
Fuentes: Upgrade Your Deep Agent With a Local Open-Source Sandbox
Por qué los agentes necesitan un lugar seguro para ejecutar código
Los asistentes de chat como ChatGPT, Claude, Gemini y Qwen aceptan archivos como imágenes, PDFs y hojas de cálculo. Muchas solicitudes relacionadas con esos archivos, como describir una imagen, resumir un PDF o extraer un fragmento de un documento, están dentro de las capacidades nativas de un modelo de lenguaje. Sin embargo, si se le entrega un archivo .csv y se le pide un análisis estadístico completo con gráficos, el mero razonamiento ya no es suficiente; se requiere manipulación real de datos, cálculos y generación de gráficos.
Un LLM no puede realizar esos cálculos ni dibujos por sí mismo, pero puede escribir un script en Python que utilice pandas, NumPy y Matplotlib para hacerlo. Una vez que el script se ejecuta, la tarea restante del modelo es convertir los resultados en un informe legible. Las herramientas de tipo agente son las que hacen posible ejecutar esos scripts.
No obstante, ejecutar directamente en su propia máquina código escrito por IA es una mala idea. La ejecución no supervisada representa un grave riesgo para la seguridad y la estabilidad: podría modificar la configuración del sistema, eliminar archivos críticos o exponer datos personales. Por eso existen las cajas de arena, entornos aislados y seguros donde el agente puede ejecutar su código sin acceder al sistema operativo subyacente. Este material se centra en las cajas de arena para la biblioteca Deep Agents de Python.
Qué es una caja de arena y por qué los agentes la necesitan
Aquí, un entorno aislado significa un ambiente contenedor, generalmente un contenedor, en el cual el código se ejecuta sin ningún tipo de contacto con el resto de la máquina. Su sistema de archivos y procesos son propios, y nada permite acceder a los archivos, credenciales o red del host a menos que se conceda acceso intencionadamente. Los desarrolladores pueden considerarlo una comodidad, pero para un agente autónomo es casi una necesidad, y las razones son prácticas:
- El agente elige el código, no el usuario. Incluso un agente bien comportado a veces eliminará el archivo incorrecto o ejecutará una orden que nadie pretendió, y sin aislamiento los daños afectan a la máquina real.
- La inyección de comandos es real. Si el agente lee contenido no confiable, como una página web o un archivo de otra persona, una instrucción oculta redactada con astucia puede intentar redirigir sus acciones siguientes. Un entorno aislado limita los daños a un contenedor desechable.
- Unos entornos limpios ayudan. Cada tarea es más fácil de manejar cuando comienza en un entorno conocido y vacío, sin heredar estado de ejecuciones anteriores.
- Agentes en paralelo. Gracias a la isolación, un
pip installde un agente no puede chocar con el de otro, y un entorno aislado dañado no afectará al proceso completo. - Límites de recursos. Un agente atrapado en un bucle infinito, o uno que carga todo un conjunto de datos en la memoria dos veces, debería agotar los recursos de un contenedor desechable y no los de tu ordenador.
Por eso los frameworks orientados a la programación de agentes, incluidos Deep Agents, tratan la sandbox como un concepto de primera categoría. Deep Agents incluye una abstracción llamada BaseSandbox, además de integraciones oficiales con algunos proveedores de sandboxes en la nube. Al dirigir el agente hacia uno de ellos, este dispone de un lugar real e aislado para ejecutar código.
Todos estos proveedores se alojan en la nube y se cobran por minuto o según el uso. Esto es adecuado para entornos de producción, pero es más de lo necesario para desarrollar, probar o demostrar un agente localmente.
OpenSandbox como opción local
OpenSandbox es un entorno de sandbox gratuito y de código abierto que funciona con su propio Docker. Ofrece las mismas funcionalidades básicas que las opciones de pago:
- Inicio de un contenedor aislado.
- Ejecución de comandos en él.
- Lectura y escritura de archivos allí.
- Deshabilitación del mismo posteriormente.
Inicie su servidor de plano de control localmente, diríjalo hacia Docker, y aparecerá un endpoint local que crea y gestiona entornos aislados cada vez que se le solicite. Nadie lo vende como un producto alojado; es una infraestructura que usted mismo opera, adecuada para el desarrollo local, las pruebas y demostraciones como esta. Lo que queda es conectarlo a Deep Agents, lo cual no incluye integración con OpenSandbox.
El contrato BaseSandbox
BaseSandbox es una clase abstracta, y la integración se reduce a cuatro miembros.
class BaseSandbox(ABC):
def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse: ...
@property
def id(self) -> str: ...
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]: ...
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]: ...
executeejecuta una orden en el entorno aislado y devuelve unExecuteResponse.ides un identificador único para el backend del entorno aislado.upload_filesenvía varios archivos al entorno aislado.download_filesextrae varios archivos de allí.
Una vez que existen estos cuatro componentes, Deep Agents crea automáticamente ls, read_file, write_file, edit_file, glob y grep sobre la función execute(). No es necesario codificar nada más para ellos. Esta es la superficie de integración completa, lo que significa que cualquier entorno aislado capaz de ejecutar órdenes de shell y mover archivos hacia adentro y hacia afuera es apto. El proceso parece sencillo, hasta que comienza la codificación real y aparecen los errores.
Cómo funciona OpenSandbox
OpenSandbox utiliza contenedores Docker para crear y gestionar entornos aislados. Su SDK en Python permite crear y administrar dichos entornos, ejecutar comandos de shell, manejar archivos y monitorear recursos. Para su instalación se requiere que Docker esté instalado y en ejecución, además del propio servidor:
# Generate a starter config
uvx opensandbox-server init-config ~/.sandbox.toml --example docker
# Start the server
uvx opensandbox-server
Después de eso, el SDK se conecta al servidor en ejecución y expone todas las funcionalidades que puede realizar OpenSandbox.
La clase de integración
Se trata del esqueleto que debe completarse siguiendo la abstracción BaseSandbox. Los métodos se añaden uno por uno.
create()
class MinimalOpenSandboxBackend(BaseSandbox):
def __init__(self, sandbox: Sandbox, runner: AsyncRunner):
self._sandbox = sandbox
self._runner = runner
__init__ es sencillo. Contiene dos objetos de los cuales depende todo lo demás: la conexión activa Sandbox con el servidor y el AsyncRunner que la gestiona. Ninguno de ellos se crea manualmente, ya que ambos provienen de un método de fábrica. La clase también termina teniendo más métodos de los cuatro que requiere BaseSandbox, siendo el más notable el método de clase .create(). La razón se hace más clara al ver cómo realmente se crea un objeto de sandbox.
Tanto la conexión Sandbox como el ejecutor en segundo plano deben existir antes de que __init__ pueda utilizarlos, y por eso create() constituye un paso separado. Aquí está el código.
from opensandbox import Sandbox
from opensandbox.config import ConnectionConfig
IMAGE = "opensandbox/code-interpreter:v1.1.0"
ENTRYPOINT = ["/opt/code-interpreter/code-interpreter.sh"]
@classmethod
def create(cls, api_key: str | None = None) -> "MinimalOpenSandboxBackend":
runner = AsyncRunner()
config = ConnectionConfig(domain="localhost:8080", api_key=api_key)
sandbox = runner.run(
Sandbox.create(IMAGE, entrypoint=ENTRYPOINT, connection_config=config, timeout=timedelta(minutes=30))
)
return cls(sandbox, runner)
Hay dos detalles que destacan. domain="localhost:8080" establece una conexión con el servidor iniciado anteriormente mediante uvx opensandbox-server. timeout=timedelta(minutes=30) funciona como medida de seguridad: incluso si se olvida llamar a kill(), el entorno aislado se desactivará de todos modos en lugar de permanecer en segundo plano. También hay que prestar atención a IMAGE y ENTRYPOINT, ya que deben ser compatibles con la versión de OpenSandbox que se esté utilizando.
Acerca del ejecutor: Sandbox.create es una corutina, pero BaseSandbox.create debe devolver un objeto normal en lugar de algo que se espere. Hasta que se conozcan los detalles, considere runner.run(coro) como “espere a que termine esta llamada asíncrona y luego deme lo que devolvió”. Esto explica por qué Sandbox.create(...) se pasa a runner.run(...) en lugar de esperarse directamente. Cómo funciona AsyncRunner en su interior se explica un poco más adelante.
id
La creación ya se ha completado, por lo que los métodos restantes se ocupan de una sandbox que ya está en ejecución.
@property
def id(self) -> str:
return self._sandbox.id
Devuelve el id de la caja de arena subyacente. BaseSandbox lo exige como propiedad, y es útil en los registros de actividad.
execute()
from deepagents.backends.protocol import ExecuteResponse
def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
execution = self._runner.run(self._sandbox.commands.run(command))
stdout = "\n".join(c.text for c in execution.logs.stdout)
stderr = "\n".join(c.text for c in execution.logs.stderr)
output = "\n".join(p for p in (stdout, stderr) if p)
return ExecuteResponse(output=output, exit_code=execution.exit_code or 0)
Engañosamente simple es este caso. OpenSandbox entrega la salida de los comandos como una lista de fragmentos, uno por línea, y para entonces el salto de línea ya ha sido eliminado. Por lo tanto, la respuesta debe reconstruirse adecuadamente para que el agente pueda manejarla posteriormente. Tampoco se llama directamente a execute(). ls, glob y grep en Deep Agents se basan en él y leen la salida línea por línea. Si se les proporciona una respuesta mal formada, no coinciden con nada y devuelven silenciosamente resultados vacíos. ls devuelve [], no hay rastro de error que atribuya la falla al join, y la sandbox parece vacía cuando en realidad no lo está.
Es más importante de lo que parece, porque execute() no sirve solo para uso directo. Constituye la base de ls, glob y grep en Deep Agents, y todos ellos analizan la salida línea por línea. Una respuesta con formato incorrecto hace que no encuentren nada y devuelvan silenciosamente resultados vacíos. ls entonces muestra [] sin rastro que indique el problema en join, y la zona de pruebas parece vacía cuando en realidad no lo está.
El resto es más sencillo: stdout y stderr se concatenan, se omite el que esté vacío, y la falta de un código de salida se considera éxito (0) en lugar de permanecer como None.
upload_files()
from opensandbox.models import WriteEntry
from deepagents.backends.protocol import FileUploadResponse
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
entries = [WriteEntry(path=path, data=data, mode=644) for path, data in files]
try:
self._runner.run(self._sandbox.files.write_files(entries))
return [FileUploadResponse(path=p) for p, _ in files]
except Exception as exc:
return [FileUploadResponse(path=p, error=str(exc)) for p, _ in files]
Cada par (ruta, bytes) se envuelve en un WriteEntry con permisos ordinarios (el propietario puede leer y escribir, mientras que los demás solo pueden leer), y todo el lote se envía mediante una sola llamada a write_files().
download_files()
from deepagents.backends.protocol import FileDownloadResponse
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
results = []
for path in paths:
try:
content = self._runner.run(self._sandbox.files.read_bytes(path))
results.append(FileDownloadResponse(path=path, content=content))
except Exception as exc:
results.append(FileDownloadResponse(path=path, error=str(exc)))
return results
Lo importante es read_bytes en lugar de read_file. El SDK ofrece ambas opciones, pero read_file asume que el texto está en formato UTF-8. Un entorno aislado que ejecuta el código de un LLM no puede saber si el archivo siguiente solicitado es un CSV o un gráfico recién generado, por lo que read_bytes es la única opción segura para ambos casos.
kill()
def kill(self) -> None:
self._runner.run(self._sandbox.kill())
self._runner.shutdown()
El proceso de desmontaje realiza dos acciones importantes: termina el contenedor de sandbox y cierra explícitamente el hilo de fondo del ejecutor. Si se omite el segundo paso, el hilo, junto con el bucle de eventos que contiene, permanece activo incluso después de que el sandbox haya desaparecido.
Esa es toda la clase: dos métodos de ciclo de vida (create, kill) que rodean a los cuatro que exige BaseSandbox (id, execute, upload_files, download_files).
El puente sincrónico/asincrónico
Hay un elemento que aún no se ha explicado: self._runner.run(...), la parte que hace que cada llamada al SDK asíncrono parezca sincrónica desde el punto de vista de BaseSandbox.
BaseSandbox es síncrono. execute(), upload_files() y download_files() son simples llamadas a funciones sin await. Sin embargo, el SDK de OpenSandbox es asíncrono: Sandbox.create(), sandbox.commands.run() y sandbox.files.write_files() son todas corutinas. AsyncRunner existe para cerrar esa brecha.
El enfoque es sencillo. Se crea un bucle de eventos, se le asigna un hilo de fondo propio y se mantiene activo mientras exista el entorno de pruebas. Todo lo demás utiliza únicamente run(), el cual envía una corutina a ese bucle a través de run_coroutine_threadsafe, bloquea el código que la llama hasta que se complete y devuelve el resultado. En el caso de execute(), self._runner.run(some_coroutine) se parece exactamente a una llamada a función ordinaria.
Un bucle de larga duración, en lugar de uno nuevo por cada llamada, también permite que la conexión funcione como una sesión continua y no como algo que se desmonta y vuelve a construir en cada execute(). shutdown() se encarga de la limpieza al detener el bucle y finalizar el hilo de manera ordenada, por lo que kill() simplemente lo llama.
Un agente de análisis de datos en la sandbox
Para probar la integración, una notebook de Jupyter comienza estableciendo variables de entorno y constantes, importando paquetes y, lo más importante, creando un objeto MinimalOpenSandboxBackend. Para ello se necesita una clave API que debes crear tú mismo y establecer en el archivo ~/.sandbox.toml.
import asyncio
import nest_asyncio
import threading
from datetime import timedelta
from pathlib import Path
from deepagents import create_deep_agent
from deepagents.backends.protocol import ExecuteResponse, FileDownloadResponse, FileUploadResponse
from deepagents.backends.sandbox import BaseSandbox
from langchain.chat_models import init_chat_model
from opensandbox import Sandbox
from opensandbox.config import ConnectionConfig
from opensandbox.models import WriteEntry
# nest_asyncio for running async functions in Jupyter.
nest_asyncio.apply()
IMAGE = "opensandbox/code-interpreter:v1.1.0"
ENTRYPOINT = ["/opt/code-interpreter/code-interpreter.sh"]
backend = MinimalOpenSandboxBackend.create(api_key="SANDBOX_API_KEY")
print("Sandbox ready:", backend.id)
A continuación viene un modelo de chat. Los modelos locales a través de Ollama también funcionarían, pero el modelo aún debe manejar tareas de tipo agente. Esta ejecución utiliza un modelo Gemini Flash.
llm = init_chat_model(
model="gemini-3.5-flash",
model_provider="google_genai",
api_key=os.environ["GOOGLE_API_KEY"],
max_tokens=14750,
max_retries=5,
)
Luego, el propio agente.
agent = create_deep_agent(
model=llm,
system_prompt=(
"You are a Python coding assistant with sandbox access. "
"You specialize in performing data analysis and data visualization with python,"
"you generate clear reports with good looking charts using seaborn."
),
backend=backend,
)
Todo lo construido hasta ahora se refleja en el argumento backend=backend. A partir de ese único objeto, Deep Agents genera los comandos ls, read_file, write_file, glob y grep en sí, utilizando la función personalizada execute() como base.
A continuación se carga algunos datos reales mediante upload_files(). El conjunto de datos es falso y consta de 1,000 clientes.
csv_bytes = Path("customers-1000.csv").read_bytes()
results = backend.upload_files([("/workspace/customers-1000.csv", csv_bytes)])
for r in results:
if r.error:
print(f"Upload failed for {r.path}: {r.error}")
else:
print(f"Uploaded {r.path}")
Luego, el agente recibe la tarea real.
result = agent.invoke({
"messages": "Perform a deep exploratory data analysis on the customers-1000.csv file "
"and summarize the findings in a markdown report with clear charts."
})
En esta etapa, el agente trabaja solo. Con sus herramientas e infraestructura, puede escribir código de pandas y seaborn, ejecutarlo en el entorno aislado mediante execute(), almacenar los gráficos en el sistema de archivos del entorno aislado y seguir iterando hasta que exista un informe completo en formato markdown.
Queda un problema: parte del informe permanece dentro del entorno aislado. Su formato markdown hace referencia a los gráficos mediante rutas locales al entorno aislado, que no existen en el ordenador principal. Para que sea legible se necesitan dos pasos: descargar las imágenes de los gráficos con download_files() y reescribir las rutas de las imágenes en el formato markdown de modo que apunten a las copias locales. Un enfoque menos problemático consiste en que el agente devuelva una salida estructurada, por ejemplo, un campo con el informe y una lista explícita de rutas de imágenes, en lugar de buscar enlaces dentro de un formato markdown sin estructura. El procesamiento posterior consiste simplemente en descargar cada ruta indicada y sustituirla.
Después de eso, aquí está lo que la zona de pruebas generó a partir del CSV en bruto.
# Deep Exploratory Data Analysis: Customer Acquisition and Profiling
**Dataset:** `customers-1000.csv`
**Analysis Period:** Jan 2020 – May 2022
---
## 1. Executive Summary
This report presents a comprehensive exploratory data analysis (EDA) of a customer database containing 1,000 unique records. The analysis delves into geographical distributions, sign-up temporal trends, domain & technical alignments, and name demographics to uncover actionable insights for strategic growth.
### Key Takeaways
1. **Unprecedented Global Reach:** The customer base is extraordinarily decentralized, spanning **240 countries** across all **7 continents** (including Antarctica). No single country represents more than 1.2% of the customer base. Africa (24.7%) and Asia (22.6%) are the leading regions, followed by Europe (18.5%) and North America (16.1%).
2. **Stable Acquisition Trends:** Customer subscriptions are remarkably stable, averaging roughly **34-35 new customers per month** across 2020 and 2021. This consistency is maintained across all continents year-over-year, indicating a highly standardized, globally distributed customer acquisition channel.
3. **Mid-Week and Weekend Consistency:** Subscriptions are evenly spread across the days of the week, with a minor peak on Friday and Saturday, and a minor trough on Thursday.
4. **B2B / Synthetic Profile Characteristics:** The dataset shows zero domain overlap between customer email domains and company websites (0.0% exact match across 923 unique domains). Combined with the near 1-to-1 ratio of customers to companies, this suggests a highly B2B-centric profile (one representative per enterprise) or synthetically generated profiles with randomized fields.
5. **Standardized TLD Footprint:** The `.com` top-level domain (TLD) dominates both emails (61.2%) and corporate websites (58.8%). The remaining distribution is evenly split among `.org`, `.net`, `.biz`, and `.info`.
---
## 2. Dataset Structure & Data Integrity
The initial dataset contains **1,000 rows** and **12 columns**. An inspection of data integrity reveals excellent completeness:
- **Zero Missing Values:** Every column is 100% populated.
- **Zero Duplicates:** There are no duplicate rows, and the `Customer Id` column contains 1,000 unique identifiers.
- **Data Types:** All columns are stored as object/string types except for `Index` (integer).
### Data Preprocessing & Feature Engineering
To enable deep exploratory analysis, several features were engineered:
1. **Temporal Features:** `Subscription Date` was parsed as a datetime object, allowing the extraction of `Sub_Year`, `Sub_Month`, `Sub_Month_Name`, `Sub_Day_of_Week`, and `Sub_Year_Month` (period).
2. **Geographical Mapping:** Using the `pycountry` and `pycountry-convert` libraries, coupled with a manual fallback dictionary for territories, each of the 240 countries was successfully mapped to its respective **Continent**.
3. **Domain & Technical Profiles:** Email domains (`Email_Domain`), email TLDs (`Email_TLD`), and website TLDs (`Website_TLD`) were extracted to analyze the technical profiling of users.
---
## 3. Geographical Analysis
### Continent-Level Distribution
The geographic reach of this customer base is truly global. Rather than being concentrated in a single dominant market like North America or Europe, customers are spread across all continents:
| Continent | Customer Count | Percentage |
| :--- | :---: | :---: |
| **Africa** | 247 | 24.7% |
| **Asia** | 226 | 22.6% |
| **Europe** | 185 | 18.5% |
| **North America** | 161 | 16.1% |
| **Oceania** | 107 | 10.7% |
| **South America** | 54 | 5.4% |
| **Antarctica** | 20 | 2.0% |
#### Chart 1: Customer Distribution by Continent

### Country-Level Distribution (Top 15 Countries)
The country-level distribution exhibits a heavy tail, with the 1,000 customers distributed across **240 distinct nations**. This indicates that the average number of customers per country is only **4.17**.
The top countries by customer density are:
- **Liechtenstein:** 12 customers (1.2%)
- **Gabon:** 10 customers (1.0%)
- **China, Bangladesh, Reunion, Nigeria, Luxembourg:** 9 customers each (0.9%)
This extreme dispersion suggests a borderless, digital-first product that appeals universally across jurisdictions without localized geographic bias.
#### Chart 2: Top 15 Countries by Customer Count

---
## 4. Temporal Analysis (Subscription Trends)
... (Trimmed to keep blog (estimated read time short))
Una vista recortada del informe generado:
De un cuaderno de notas a un paquete PyPI
Un cuaderno de notas muestra cómo encajan las partes, pero un paquete que cualquiera pueda instalar con pip es más útil. Crear deepagents_opensandbox_backend significó reparar todo aquello que fallaba únicamente cuando otras personas lo ejecutaban en configuraciones que el autor nunca había probado. La interfaz pública permaneció igual, con los mismos métodos BaseSandbox y las mismas funciones create() y kill(). Los cambios ocurrieron en el interior:
- Un bucle compartido. Los backends ahora comparten un único
_BackgroundLoopa nivel de módulo y ya no ejecutan hilos separados cada uno. - Advertencia de seguridad por defecto. La API no está autenticada, por lo que
create()ahora genera unaInsecureSandboxWarningcuando no se proporciona unaapi_key, mientras que anteriormente se conectaba en silencio. - Solución alternativa en Docker Desktop. En Docker Desktop, conectarse directamente a un puerto de contenedor suele causar tiempos de espera excesivos. Al establecer
use_server_proxy=True, el tráfico se envía a través del servidor, lo que evita el problema. - Validación de rutas.
upload_filesydownload_filesrechazan las rutas relativas desde el principio, de modo que el servidor no puede resolverlas silenciosamente en directorios incorrectos. - Errores más claros. El cliente verifica la información del archivo para distinguir entre “permiso denegado” y “archivo no encontrado”, asignando a cada caso el código de error correspondiente, en lugar de transmitir errores brutos que puedan engañar.
- Tiempos de espera efectivos. El parámetro
timeoutsolía ser ignorado. Ahora se pasa al servidor para detener comandos que se han descontrolado, en lugar de dejarlos sin manejo una vez que el cliente abandona la operación. - Se eliminaron los métodos asíncronos manuales. Las versiones realmente asíncronas como
aexecutegeneraban salidas vacías e invisibles dentro deToolNodede LangGraph, por lo que se eliminaron; ahora el código recurre a los métodos síncronos habituales que se ejecutan en un pool de hilos.
Esa brecha separa una demostración que funciona en una laptop de un paquete listo para que cualquiera lo instale con pip install. Viene incluida una suite de pruebas de conformidad, ejecutada contra las pruebas de entorno aislado estándar de Deep Agents, y la instalación se realiza de la siguiente manera:
!pip install deepagents-opensandbox-backend
Es la misma integración descrita anteriormente, con los bordes afilados suavizados.
Leer más
- Diez plataformas de agentes de código abierto autohospedables, ordenadas según las necesidades reales de una empresa — Compare diez plataformas de agentes de IA de código abierto para negocios: enterprise no-code, Activepieces, Kestra, Windmill, Dify, Langflow, Coze Studio, MaxKB, AnythingLLM y RAGFlow.
- Construyendo un agente de soporte LEPA con LangGraph — Conecte los nodos SupportState, intake y classify, así como los bordes condicionales y START/END para que una solicitud de soporte escolar pueda aclarar la situación o dirigirse a la documentación correspondiente.