Parselbox
Parselbox es una capa de ejecución local y de código abierto que convierte servidores MCP, APIs, shells y funciones del host en objetos nativos de Python. Los agentes descubren capacidades bajo demanda y las componen con código, archivos, paquetes y tareas en segundo plano en un único espacio de trabajo con estado.
Documentación

Código. Sistema de archivos. Contexto. Herramientas.
¿Qué pasaría si los agentes tuvieran una sola herramienta para gobernarlos a todos?
Parselbox es un runtime de Python incrustable donde los agentes de IA llaman herramientas como código: los servidores MCP, las API y los shells se convierten en objetos nativos de Python. Espacio de trabajo respaldado por disco, paquetes y red integrados; una capa de ejecución de un solo proceso impulsada por Deno y Pyodide.
https://github.com/user-attachments/assets/d4e43d16-3aa3-4e29-83c7-a1d3885b8045
[!TIP] Coloca el Parselbox MCP junto a las configuraciones existentes de servidores MCP. Los agentes obtienen al instante un runtime de Python, herramientas MCP como código, soporte para habilidades y un espacio de trabajo respaldado por disco.
¿Por qué Parselbox?
Parselbox empaqueta la llamada programática de herramientas como una capa de ejecución local de código abierto. Insértalo en una configuración MCP existente con uvx, o integra la API de Python en cualquier pila de agentes.
Más allá de la ejecución de código, Parselbox proporciona un espacio de trabajo con estado y respaldado por disco, con paquetes, red controlada, tareas en segundo plano y descubrimiento progresivo.
Deno + Pyodide ofrecen un runtime de un solo proceso con permisos explícitos de sistema de archivos y red, sin necesidad de contenedores ni microVMs. También habilita la interoperabilidad con JavaScript/npm, la integración WASM/WASI y la interfaz de usuario generativa a través de MCP Apps.
¿Por qué una capa de ejecución?
La mayoría de las pilas de agentes exponen cada capacidad como una herramienta separada. A medida que crecen las integraciones, los esquemas de herramientas consumen más contexto. El modelo también debe cargar resultados intermedios, transformar datos y enrutar valores entre llamadas.
Una capa de ejecución saca la orquestación de la ventana de contexto y la lleva al código. Las capacidades se descubren bajo demanda y luego se componen con control de flujo completo. Una sola ejecución puede coordinar múltiples herramientas mientras las variables, el estado y los archivos permanecen dentro del runtime.
El resultado es menos inflado de contexto, menos viajes de ida y vuelta entre modelo y herramienta, y flujos de trabajo complejos expresados como código en lugar de cadenas de llamadas individuales.
Características
🔒 Aislamiento seguro
Sin contenedores, sin VMs: solo un proceso único y ligero de Deno + Pyodide (~160 MB). Permisos de Deno, límites de memoria, tiempos de espera, listas de permitidos de red. Caché de instantáneas y recuperación ante fallos.
🛠️ Herramientas como código
Servidores MCP, REST + OpenAPI, GraphQL, shell, funciones y clases: todos objetos nativos de Python. Con estado entre llamadas. Conversión automática con Pydantic. Las credenciales permanecen en el host.
🐍 Runtime políglota
CPython completo con interoperabilidad js(): usa paquetes JS como Python nativo. require() para npm, TypeScript local y módulos .wasm. bash() virtual para shell. Instalación automática de paquetes al importar.
📦 Herramientas WASM
require() cualquier .wasm: las exportaciones de bibliotecas se convierten en métodos de Python, los programas WASI en comandos invocables; coloca uno en bin/ para ejecutarlo también desde bash(). En proceso, hereda los montajes y permisos del sandbox, no instala nada en el host.
⚡ Tareas en segundo plano
Agrega .task() a cualquier llamada: fan-out paralelo con asyncio.gather, verifica el progreso, sigue los registros, maneja sesiones interactivas con send(), espera después.
📁 Integración del sistema de archivos
Espacio de trabajo respaldado por disco: montajes del host (ro/rw), archivos de entrada en /files/, salidas persistidas en directorios reales. Los archivos nuevos y modificados se detectan y devuelven por llamada.
🔍 Divulgación progresiva
help(), search(), inspect(), preview(): los agentes descubren solo lo que necesitan, cuando lo necesitan.
🎨 Interfaz de usuario generativa
display() renderiza HTML en línea en el chat (MCP Apps), con Tailwind + daisyUI inyectados. O sirve una aplicación completa: servidor HTTP integrado con archivos estáticos, recarga en vivo, carga de archivos y rutas @api que se componen entre herramientas.
Contenido
- Inicio rápido
- Guía de usuario
- Referencia de configuración
- Arquitectura
- Seguridad
- Trabajo relacionado
Inicio rápido
Parselbox usa Deno para el runtime de sandbox seguro.
1. Instala Deno
# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex
2. Instala Parselbox
pip install parselbox
API de Parselbox
Conecta cualquier herramienta al sandbox: servidores MCP, REST/GraphQL, shells, objetos del host—y el agente las llama como Python nativo, componiéndolas con control de flujo real sobre un espacio de trabajo respaldado por disco y ambos ecosistemas de paquetes, Python y npm.
Ejemplo:
import asyncio
import os
from textwrap import dedent
from parselbox import Parselbox
from parselbox.bridge import HTTPBridge, ShellBridge
class Analytics:
def summarize(self, repos: list) -> dict:
"""Aggregate repo stats."""
stars = [r["stars"] for r in repos]
return {"count": len(repos), "avg_stars": round(sum(stars) / len(stars))}
config = {"mcpServers": {"playwright": {"command": "npx", "args": ["@playwright/mcp@latest"]}}}
async def main():
async with Parselbox(
mcp=config,
context={
"analytics": Analytics(),
"github": HTTPBridge(base_url="https://api.github.com", token=os.environ["GITHUB_TOKEN"]),
"sh": ShellBridge("bash"),
},
network=True,
allow_runtime_packages=True,
packages=["numpy", "npm:lodash"],
output_dir="./workspace",
) as sbx:
# Discover available tools
await sbx.execute_code("sbx.search('navigate|get')")
# Scrape Hacker News for GitHub links in a real browser
await sbx.execute_code(dedent("""
import re
playwright.browser_navigate(url="https://news.ycombinator.com")
text = playwright.browser_snapshot()
repos = re.findall(r'github\\.com/([\\w.-]+/[\\w.-]+)', text)[:5]
"""))
# Fetch star counts in parallel, then summarize via the context bridge
await sbx.execute_code(dedent("""
import asyncio
results = await asyncio.gather(*[github.get.task(f"/repos/{r}") for r in repos])
repo_data = [{"name": r["data"]["name"], "stars": r["data"]["stargazers_count"]}
for r in results if r.get("ok")]
analytics.summarize(repo_data)
"""))
# Chart it — matplotlib auto-installs on import
result = await sbx.execute_code(dedent("""
import matplotlib.pyplot as plt
plt.barh([r["name"] for r in repo_data], [r["stars"] for r in repo_data])
plt.savefig("chart.png")
"""))
print(result.files) # ['chart.png']
image = sbx.read_file("chart.png")
# every result carries .output, .files, .stdout, .stderr, .error
# Serve the whole sandbox as an MCP server
await sbx.run_mcp()
asyncio.run(main())
Parselbox MCP
La CLI de Parselbox ejecuta un servidor MCP independiente: cada opción del sandbox está disponible como una bandera.
STDIO
[!TIP] El truco del "loopback":
- Agrega el Parselbox MCP junto a tus servidores MCP existentes.
- Apunta
--mcpal mismo archivo de configuración.- Al iniciar, Parselbox se conecta a los otros servidores, expone sus herramientas dentro del sandbox y arranca su propio servidor MCP.
No te preocupes: Parselbox detecta y evita conectarse a sí mismo. Sin bucles infinitos de perdición.
Ejemplo:
{
"mcpServers": {
"github": {},
"linear": {},
"parselbox": {
"command": "uvx",
"args": ["parselbox", "--mcp", "/absolute/path/to/mcp.json"]
}
}
}
HTTP
uvx parselbox --mcp mcp.json --transport http --port 9000
{
"mcpServers": {
"parselbox": {
"type": "http",
"url": "http://localhost:9000/mcp"
}
}
}
Ejemplo completo
uvx parselbox \
--mcp ./mcp.json \
--transport http \
--host 0.0.0.0 \
--port 8080 \
--file hello.txt \
--mount ./datasets:/data:rw \
--output-dir ./outputs \
--packages pandas,matplotlib \
--package-dir ./cache \
--allow-runtime-packages \
--network \
--serve 3000 \
--memory 2048 \
--timeout 60 \
--env MY_API_KEY=...
Agentes de Parselbox
import asyncio
from parselbox import Parselbox
from agents import Agent, Runner, function_tool
sandbox = Parselbox(
mcp={"mcpServers": {"playwright": {"command": "npx", "args": ["@playwright/mcp@latest"]}}},
output_dir="./outputs",
allow_runtime_packages=True,
)
agent = Agent(
name="Research Assistant",
model="gpt-5.5",
instructions=f"You are a world-class research assistant.\n\n{sandbox.get_prompt()}",
tools=[function_tool(sandbox.get_tool())],
)
async def main():
async with sandbox:
result = await Runner.run(
agent,
"Scrape Wikipedia's 'List of highest-grossing films' with the Playwright MCP. "
"Plot a bar chart of the top 10 and save it as ./plot.png",
max_turns=30,
)
print(result.final_output)
asyncio.run(main())
Guía de usuario
1. Herramientas como código
El puente de contexto expone objetos de Python del host dentro del sandbox:
context: funciones y espacios de nombres como herramientas invocables. La ejecución se pausa, se ejecuta en el host y devuelve el resultado.globals: valores estáticos (cadenas, números, diccionarios) copiados al sandbox.mcp: configuración del servidor MCP (diccionario o ruta). Aparece como espacios de nombres invocables dentro del sandbox.
Clases simples se envuelven automáticamente: cada método público se convierte en una herramienta invocable; los métodos que comienzan con _ permanecen privados:
from parselbox import Parselbox
class Calculator:
def add(self, a: float, b: float) -> float:
"""Add two numbers."""
return a + b
async with Parselbox(context={"calc": Calculator()}) as sbx:
await sbx.execute_code("calc.add(a=10, b=20)")
Subclase Bridge para espacios de nombres anidados (rastreados automáticamente); anota un parámetro con un modelo de Pydantic y los diccionarios pasados se convierten automáticamente:
from parselbox import Parselbox
from parselbox.bridge import Bridge
from pydantic import BaseModel
class Coordinate(BaseModel):
x: float
y: float
z: float = 0.0
class Sensors(Bridge):
def temperature(self) -> float:
"""Read temperature in celsius."""
return 23.5
class Robot(Bridge):
def __init__(self):
self.sensors = Sensors()
def move(self, to: Coordinate) -> dict:
"""Move robot to a position."""
return {"position": [to.x, to.y, to.z], "status": "reached"}
async with Parselbox(context={"robot": Robot()}) as sbx:
await sbx.execute_code("robot.move(to={'x': 1, 'y': 2})")
await sbx.execute_code("robot.sensors.temperature()")
Parselbox incluye puentes para REST, GraphQL y shell:
from parselbox import Parselbox
from parselbox.bridge import HTTPBridge, GraphQLBridge, ShellBridge
api = HTTPBridge(
spec="https://petstore3.swagger.io/api/v3/openapi.json",
base_url="https://petstore3.swagger.io/api/v3",
)
gql = GraphQLBridge("https://countries.trevorblades.com/graphql")
sh = ShellBridge("ssh -T user@host")
mcp = {"mcpServers": {"deepwiki": {"type": "http", "url": "https://mcp.deepwiki.com/mcp"}}}
async with Parselbox(context={"api": api, "gql": gql, "sh": sh}, mcp=mcp, network=True) as sbx:
await sbx.execute_code('api.search("GET /pet/*")')
await sbx.execute_code('api.get("/pet/1")')
await sbx.execute_code('gql.graphql(query="{ continents { name } }")')
await sbx.execute_code('gql.graphql(query="{ languages { code name } }")')
await sbx.execute_code('term = sh.shell.task()')
await sbx.execute_code('term.send("df -h")')
await sbx.execute_code("sbx.search('ask|read')")
await sbx.execute_code("deepwiki.read_wiki_structure(repoName='pyodide/pyodide')")
await sbx.execute_code("deepwiki.ask_question(question='What is Pyodide?', repoName='pyodide/pyodide')")
Ejecutable: bridges.py
2. Tareas en segundo plano
Cada llamada de contexto y MCP también tiene una forma .task() que se ejecuta en el host sin bloquear el sandbox, para fan-out paralelo, trabajos de larga duración y sesiones interactivas:
job = sh.exec.task(command="ffmpeg -i in.mp4 out.mp4") # returns a task immediately
job.status() # TaskStatus(state, elapsed, message, logfile)
job.tail(5) # last lines of the task's live log
job.send("q") # message a running interactive process
await job.wait(timeout=120) # block until done — or just `await job`
job.cancel()
# parallel fan-out
import asyncio
results = await asyncio.gather(*[api.get.task(f"/items/{i}") for i in range(5)])
Las herramientas MCP transmiten su progreso y notificaciones de registro al archivo de registro de la tarea. Un método personalizado Bridge emite de la misma manera con self.log(), y lee lo que el sandbox puso en cola vía send() con self.recv():
from parselbox.bridge import Bridge
class Exporter(Bridge):
def run(self, rows: int) -> str:
for i in range(rows):
self.log(f"row {i}/{rows}") # appended to task.logfile → tail()
for msg in self.recv(): # messages queued by task.send()
self.log(f"got: {msg}")
return "done"
Sesiones interactivas: ShellBridge.shell() mantiene stdin abierto, por lo que una tarea puede manejar un proceso en vivo con send():
session = sh.shell.task() # a live shell — state persists within the session
session.send("x=21")
session.send("echo $((x * 2))")
import asyncio
await asyncio.sleep(1) # give it a beat
session.tail(1) # "42"
session.cancel()
Un primer comando opcional lanza cualquier REPL como sesión, por ejemplo sh.shell.task("python3 -i").
Ejecutable: tasks.py
3. Integración del sistema de archivos
Parselbox se ejecuta en el sistema de archivos virtual de Pyodide, con el directorio de trabajo, archivos de entrada, montajes y paquetes respaldados por directorios reales del host; el acceso está controlado por los permisos de Deno al inicio.
| Método | Nivel de acceso | Descripción |
|---|---|---|
| files | Lectura / Escritura | Directorio temporal en /files/. Los archivos de entrada se copian aquí; las subidas del servidor se almacenan aquí. |
| mounts | Configurable | Mapea directorios del host a /mnt/{name}. Modo: ro (predeterminado) o rw. |
| output_dir | Lectura / Escritura | Mapea el directorio de trabajo a un directorio del host para persistir archivos. Si no se proporciona, usa un directorio temporal (borrado al cerrar). |
[!NOTE]
/workspacesiempre está respaldado por un directorio real del host:output_dir(persistente) o un directorio temporal efímero (borrado al cerrar), lo que permite streaming de Deno,resolvePath()yrequire()para módulos locales.- Cruza el límite con
sandbox.read_file(path)(strpara texto,bytespara binario) ysandbox.write_file(path, content); unoutput_dirpersistente también es legible directamente.- Los montajes con
target="skills"son reportados porsbx.info()y descubribles víabash("ls /mnt/skills/").
Ejemplo:
from parselbox import Parselbox, Mount
async with Parselbox(
files=["data.csv"], # Read/write at /files/data.csv
mounts=[
Mount("./datasets", "/data", "ro"), # Read-only at /mnt/data
Mount("./workspace", "/work", "rw"), # Read/write at /mnt/work
],
output_dir="./outputs" # Sandbox files persisted here
) as sandbox:
# Write a file into the sandbox from the host
sandbox.write_file("greeting.txt", "Hello from host!")
code = """
content = open('/files/data.csv').read() # input file
ref = open('/mnt/data/reference.json').read() # read-only mount
open('/mnt/work/processed.txt', 'w').write(content) # read/write mount
open('result.txt', 'w').write("Done!") # working dir -> output_dir
"""
result = await sandbox.execute_code(code)
# New / modified files are detected and returned
print(result.files) # ['result.txt', 'greeting.txt']
sandbox.read_file("result.txt")
Alcanza los mismos archivos desde un shell con bash():
bash("echo 'hello from bash' > note.txt && cat note.txt") # shell over the workspace
Ejecutable: filesystem.py · bash.py
4. Paquetes y red
Paquetes
Pyodide admite paquetes de Python puro y muchos paquetes con extensiones C, que deben estar precompilados para Pyodide: numpy, pandas y más vienen incluidos.
from parselbox import Parselbox, Mount
# preload Python + npm packages on startup
Parselbox(packages=["numpy", "pandas", "npm:lodash"])
# local wheel — mount its dir so Deno can read the host path
Parselbox(packages=["file:///host/wheels/pkg.whl"],
mounts=[Mount("./wheels", "wheels", "ro")])
# remote wheel — needs network access
Parselbox(packages=["https://example.com/pkg.whl"], network=True)
# autoload as imports appear (only official domains when network=False)
Parselbox(allow_runtime_packages=True)
[!NOTE] Las instalaciones de paquetes escriben directamente al disco: un directorio temporal por defecto (borrado al salir). Configura
package_dirpara persistirlos entre sesiones, de modo que el próximo arranque sea instantáneo sin re-descargas.
Red
Después de la carga inicial de paquetes, la red está bloqueada por defecto. El acceso se configura con los controles de permisos de Deno vía --allow-net / --deny-net. Todo el HTTP desde código en el sandbox (requests, httpx, fetch) pasa por el fetch() de Deno.
# Block everything (default)
Parselbox(network=False)
# Allow specific domains (Python API only)
Parselbox(network=["api.github.com:443"])
# Allow everything
Parselbox(network=True)
[!NOTE] La bandera
--networkde la CLI es solo un interruptor booleano. Las listas de permitidos por dominio están disponibles a través de la API de Python.
Proxy e inyección de credenciales
Pyodide no es un límite de seguridad: el código en el sandbox puede leer variables de entorno vía js('Deno.env.get("KEY")'), así que nunca pases credenciales reales en env. En su lugar, ejecuta un proxy de inyección de credenciales en el host y bloquea el sandbox hacia él:
async with Parselbox(
network=["127.0.0.1:8900"], # sandbox can ONLY reach the proxy
env={
"OPENAI_BASE_URL": "http://127.0.0.1:8900/v1",
"OPENAI_API_KEY": "phantom-token", # harmless; the real key lives on the proxy
},
) as sbx:
await sbx.execute_code("import openai; openai.OpenAI().chat.completions.create(...)")
La mayoría de los SDK aceptan una anulación base_url. Para una intercepción independiente del SDK, configura HTTP_PROXY/HTTPS_PROXY/DENO_CERT y enruta todo a través de un proxy MITM: el fetch() de Deno los respeta a nivel de proceso.
Ejecutable: basics.py
5. Interoperabilidad con JavaScript
Parselbox ejecuta Python dentro del motor V8 de Deno vía Pyodide, por lo que Python y JavaScript comparten la misma memoria del proceso: la interoperabilidad es fluida.
js(): ejecuta JavaScript desde Python
# Basic — auto converts args and results
js("return data.map(x => x * 2)", data=[1, 2, 3]) # [2, 4, 6]
# Callbacks — Python functions auto-proxied, no create_proxy needed
js("return items.filter(fn)", items=[1,2,3,4,5], fn=lambda x, *_: x > 3) # [4, 5]
# Async + Web APIs (Intl, Crypto, URL, TextEncoder)
js("return crypto.randomUUID()")
Cada llamada js() se ejecuta en un ámbito nuevo y sin estado. Los callables de Python se auto-proxean y se limpian después de la llamada. Los binarios también se convierten: los resultados Uint8Array/ArrayBuffer se vuelven bytes de Python, y los argumentos bytes se vuelven Uint8Array.
require(): importa paquetes npm, módulos locales y WASM
# npm packages — returns proxy + auto-injects in js() scope (alias= to rename)
lodash = require("lodash")
lodash.chunk([1, 2, 3, 4], 2) # [[1, 2], [3, 4]]
# Callbacks work with require'd packages
lodash.sortBy(data, lambda x, *_: x["age"])
# Also available in js()
js("return lodash.invert({a: 1, b: 2})")
# Local TypeScript — compiled by Deno, hot-reloads; can import npm internally
require("./math_utils.ts").fibonacci(10)
# .wasm modules & WASI binaries load too — see WASM Tools
# Instances keep their methods — chain them
dayjs = require("dayjs")
dayjs("2026-06-15").add(30, "day").format("YYYY-MM-DD") # "2026-07-15"
# Class constructors auto-detect `new`
color = require("color")
color("red").darken(0.5).hex() # "#800000"
# Chains work with Python callbacks
lodash(data).filter(lambda x, *_: x["pay"] > 100).sortBy(lambda x, *_: -x["pay"]).value()
Streaming de Deno (archivos grandes)
Para archivos demasiado grandes para la memoria, usa streams de Deno vía resolvePath():
js("""
const path = resolvePath("sample.txt");
const info = await Deno.stat(path);
return { size: info.size, isFile: info.isFile };
""")
Los callbacks de Python funcionan dentro de pipelines de streaming: Deno lee, JS analiza, Python clasifica cada línea.
Escribir e importar módulos
# Python module — write it, import it
open("helpers.py", "w").write("def double(x): return x * 2")
from helpers import double
double(21) # 42
# TypeScript module — compiled by Deno
open("transform.ts", "w").write("export function upper(s: string) { return s.toUpperCase(); }")
require("./transform.ts").upper("hello") # "HELLO"
Incluso puedes compilar un lenguaje a WebAssembly dentro del sandbox y luego require() la salida.
bash(): comandos de shell
Un bash de JavaScript puro (just-bash) sobre el mismo espacio de trabajo. Los pipes y coreutils funcionan, y curl está respaldado por fetch. Cada llamada está aislada (cd/export no persisten); los cambios en el sistema de archivos sí.
bash("echo hello > note.txt && cat note.txt | tr a-z A-Z") # "HELLO"
bash("grep -rn hello . | wc -l")
bash("curl -s https://api.github.com/zen") # network rules still apply
Ejecutable: javascript.py · bash.py
6. Herramientas WASM
Pyodide solo puede cargar paquetes construidos para él — por lo que pandoc, ruby o shellcheck están fuera de alcance, y no hay apt-get en un sandbox de un solo proceso. Parselbox cierra esa brecha con WASI: cualquier programa compilado a WebAssembly se convierte en una herramienta, sin instalación en el host.
Una capacidad faltante es solo un archivo.
Dos tipos de .wasm
require() inspecciona el módulo y elige la forma correcta:
# Library module (no imports) — its exports become methods
require("./fib.wasm").fib(20) # 6765
# Command module (a WASI program) — becomes a callable command
pandoc = require("./pandoc.wasm")
r = pandoc(["-f", "markdown", "-t", "html5"], stdin="# Report")
r["stdout"].decode() # '<h1 id="report">Report</h1>'
Un comando devuelve {"exit": int, "stdout": bytes, "stderr": str, "missing": [...]} — missing lista cualquier syscall que el binario solicitó y que no está implementada, por lo que las brechas aparecen como datos en lugar de un fallo.
[!IMPORTANT] Las compilaciones de Emscripten no son compilaciones WASI. Gran parte del "wasm" de npm (
sql.js,ffmpeg.wasm,tesseract.js) está compilado con Emscripten y necesita su propio pegamento JavaScript — impórtalos como paquetes npm (require("sql.js")), no como archivos.wasmsueltos. Ambas rutas funcionan;require()te dice cuál necesita un binario.
run(args=None, stdin="", env=None, preopens=None, argv0=None)
stdin—strobytes;stdoutsiempre regresa comobytes.preopens— otorga directorios invitados adicionales, p. ej.preopens={"/usr": "vendor/usr"}para un binario que espera su propio árbol.argv0— algunos binarios se despachan según su nombre de programa (lld se convierte enwasm-ldal estilo busybox).
Binarios como comandos bash()
Un binario de comando WASI (un .wasm que exporta _start) en el directorio bin/ de un montaje se convierte en un comando de shell, utilizable junto con los coreutils de JavaScript de bash(). Los binarios se descubren por llamada, por lo que una herramienta escrita a mitad de sesión funciona de inmediato.
open("bin/pandoc.wasm", "wb").write(pandoc_bytes)
bash("pandoc -f markdown -t plain notes.md | head -3 | tr a-z A-Z")
# ^^ compiled pandoc ^^ just-bash builtins
Monta una carpeta bin/ de solo lectura para enviar un conjunto de herramientas fijo que el agente pueda usar pero no modificar — nada instalado en el host — o haz que obtenga un .wasm en bin/ en tiempo de ejecución, lo que funciona incluso cuando la red del sandbox está restringida a un solo host en la lista de permitidos.
Incluso puedes construir uno desde el código fuente en el proceso — obtén un clang WASI + wasm-ld en bin/, compila C a .wasm, y luego require() el resultado. Sin cadena de herramientas en el host, nada instalado.
[!NOTE]
- Auto-detectado como WASI
preview1owasi_unstable(preview0). No soportado: sockets, sleeps reales, componentes preview2.- Los módulos compilados se cachean por ruta (invalidados al reconstruir), por lo que un binario de 50MB se compila una vez por sesión.
Ejecutable: pandoc.py — obtén un binario WASI · compile_c.py — compila C → wasm en el sandbox
7. Divulgación Progresiva
El kit de herramientas sbx permite a los agentes descubrir capacidades bajo demanda en lugar de cargar todo en el contexto de antemano. Disponible como sbx.* dentro del sandbox.
| Función | Descripción |
|---|---|
sbx.help() | Devuelve una guía completa para usar el sandbox. |
sbx.info() | Obtén información del entorno del sandbox — contexto, paquetes, red, montajes, servir, etc. |
sbx.search(pattern) | Busca herramientas en todos los espacios de nombres por nombre, descripción o parámetro. |
sbx.inspect(tools) | Obtén esquemas detallados y documentación para herramientas. |
sbx.preview(data) | Resume estructuras de datos grandes o anidadas — conserva claves, trunca contenido. |
El sandbox también expone un builtin help() para introspección por objeto:
# Sandbox guide
help()
# Namespace tree view — shows all methods with hierarchy
help(robot)
# Remote namespace 'robot' — methods execute on the host and return results.
# Methods:
# ├── sensors
# │ └── temperature()
# └── move()
# Tool details — description, parameters, output schema
help(robot.move)
# {"description": "Move robot to position.", "parameters": {...}, "output": {...}}
# Works on local objects too
help(len)
Ejemplo:
# Discover what's available
sbx.info()
# Search for tools across all namespaces
sbx.search("repo|query")
# Get tool signatures before calling
sbx.inspect(["github.search_repositories", "db.query", "robot.move"])
# Parallel execution with .task
import asyncio
results = await asyncio.gather(*[api.fetch.task(id=i) for i in ids])
# Inspect unknown response structure
sbx.preview(results)
Ejecutable: toolkit.py
8. UI Generativa
Los agentes pueden mostrar resultados de dos maneras: en línea en la conversación con display(), o como una aplicación web completa con serve.
Widgets en línea — display()
Cualquier HTML que un agente pase a display() se renderiza como un widget debajo de su resultado, en hosts que soportan MCP Apps.
await sbx.execute_code("""
display("<h1>Q3 Revenue</h1><p class='text-lg'>Up <b>12%</b> to $4.1M</p>")
""")
Tailwind y daisyUI se inyectan automáticamente, por lo que el marcado simple se estiliza sin un paso de compilación, y pbx.call("/api/route", body) dentro del HTML alcanza los manejadores @api cuando serve está activado. display() también acepta una ruta a un archivo HTML en el espacio de trabajo. Una vista por ejecución — la última llamada gana.
Activado por defecto. run_mcp(ui=False) lo desactiva, lo que deja de anunciar display() al agente y elimina el renderizador de la herramienta. El HTML renderizado siempre está en result.view independientemente:
result = await sbx.execute_code('display("<b>done</b>")')
result.view # full HTML document, or None if display() wasn't called
Aplicaciones web — serve
La opción serve inicia un servidor HTTP de Deno dentro del sandbox — los agentes construyen aplicaciones web completas sobre la marcha.
# SDK
sandbox = Parselbox(serve=3000)
# CLI
uvx parselbox --serve 3000
Archivos estáticos: Cualquier archivo escrito en el directorio de trabajo de Pyodide se sirve automáticamente:
open("index.html", "w").write("<h1>Hello World</h1>")
open("style.css", "w").write("h1 { color: blue; }")
Servido en sus propias rutas, con / resolviendo a index.html; los archivos subidos y de entrada viven bajo /files/*.
Manejadores de API: Define endpoints usando decoradores estilo FastAPI:
@api.get("/items")
def list_items(params):
limit = int(params.get("limit", 10))
return items[:limit]
@api.post("/items")
def create_item(body):
return {"id": len(items) + 1, "name": body["name"]}
Las rutas se prefijan con /api/ automáticamente. Verbos: @api.get/post/put/patch/delete.
Los manejadores pueden llamar a herramientas MCP, funciones de contexto y cualquier código del sandbox:
@api.get("/dashboard")
async def dashboard(params):
import asyncio
sensors, orders = await asyncio.gather(
robot.sensors.temperature.task(),
store.get.task("/orders", params={"limit": 5}),
)
return {"temperature": sensors, "recent_orders": orders}
Endpoints integrados:
| Endpoint | Método | Descripción |
|---|---|---|
/_upload | POST | Subida de archivos (datos de formulario multipart) |
/_live | GET | Flujo SSE — los navegadores conectados se actualizan cuando los archivos estáticos cambian (activado por defecto) |
/_routes | GET | Lista los manejadores de API registrados |
curl -F "file=@photo.png" http://localhost:3000/_upload
# {"uploaded": [{"name": "photo.png", "path": "/files/photo.png", "size": 12345}]}
Ejecutable: display.py · serve.py
9. Hooks del Sandbox
Los hooks interceptan eventos del ciclo de vida del sandbox — registran ejecuciones, aprueban llamadas de herramientas, aplican políticas. Pásalos a través del parámetro hooks.
from parselbox import Parselbox, Callback, ExecutionResult
from parselbox.hooks import Hook
class AuditHook(Hook):
async def pre_execute(self, code: str):
print(f"Executing: {code[:80]}...")
async def post_execute(self, result: ExecutionResult):
print(f"Result: {result.output}")
async def pre_tool_call(self, callback: Callback):
if "drop" in str(callback.kwargs).lower():
raise PermissionError("DROP statements are blocked")
async def post_tool_call(self, callback: Callback, result):
print(f"Tool {callback.name} returned")
async with Parselbox(
context={"db": db},
hooks=[AuditHook()],
) as sbx:
await sbx.execute_code("db.query(sql='SELECT 1')")
ElicitHook — un hook integrado que usa la elicitación de MCP para aprobación con intervención humana. Habilítalo vía --elicit (CLI) o run_mcp(elicit=True) (API). Solo se activa si el cliente MCP anuncia capacidad de elicitación — de lo contrario, es un no-op.
# CLI
uvx parselbox --mcp mcp.json --elicit
# API
await sandbox.run_mcp(elicit=True)
| Hook | Disparador | Casos de uso |
|---|---|---|
pre_execute | Antes de que el código se ejecute | Registro, verificaciones de políticas, sanitización de código |
post_execute | Después de que el código se completa | Rutas de auditoría, validación de resultados |
pre_tool_call | Antes de una llamada de contexto/MCP | Puertas de aprobación, limitación de velocidad, bloqueo |
post_tool_call | Después de que una llamada de contexto/MCP regresa | Registro, transformación de resultados |
Ejecutable: hooks.py
Referencia de Configuración
Parselbox tiene las siguientes opciones de configuración:
from parselbox import Parselbox, Mount
sandbox = Parselbox(
context=dict(db=db, notify=send_alert), # Proxied functions and namespaces
globals=dict(name="hi", threshold=0.5), # Static values copied into sandbox
files=["./input.txt"], # Read/write files at /files/
mounts=[
Mount("./datasets", "/data", "ro"), # Read-only mount
Mount("./workspace", "/work", "rw"), # Read/write mount
],
output_dir="./outputs", # Persist sandbox files
packages=["numpy", "npm:lodash"], # Install on startup (Python + npm)
package_dir="./cache", # Persist package cache across sessions
allow_runtime_packages=True, # Auto-install from imports (default: False)
network=True, # True, False, or ["domain:port", ...] (API only)
mcp="./mcp.json", # Connect MCP servers (path or dict)
serve=8080, # Enable web server on port
memory=2048, # WASM memory limit in MB (default: 2048)
timeout=60, # Execution timeout in seconds (default: 60, 0 disables)
hooks=[AuditHook()], # Lifecycle hooks
env={ # Custom env vars (available in Python os.environ)
"OPENAI_BASE_URL": "http://proxy/v1", # SDK base_url overrides for reverse proxy
"OPENAI_API_KEY": "phantom", # Phantom tokens (real keys on proxy)
"HTTP_PROXY": "http://proxy:8080", # Deno-level proxy (filtered from os.environ)
"DENO_CERT": "/path/to/ca.pem", # Custom CA for MITM proxy
},
)
Arquitectura
Parselbox ejecuta el código del agente en un proceso de Deno con Pyodide (CPython en WebAssembly) — sin contenedores, sin VMs. El sandbox con permisos restringidos funciona en un espacio de trabajo temporal aislado, sin red y sin acceso al host más allá de los montajes que otorgues; el host tiene las credenciales. Cada llamada de herramienta es un viaje de ida y vuelta entre ellos:
1. exec HOST ──▶ SANDBOX your code runs, permission-jailed
2. callback HOST ◀── SANDBOX code calls a tool as native Python
3. result HOST ──▶ SANDBOX host runs it with the real credentials
Las herramientas parecen Python nativo dentro del sandbox, pero se ejecutan en el host — por lo que las credenciales nunca entran al sandbox.
Seguridad
El límite de Parselbox es el sistema de permisos de Deno — el sandbox comienza sin nada y obtiene solo lo que configures.
- Sistema de archivos — espacio de trabajo temporal aislado (borrado al salir); lectura/escritura solo a rutas que pases (
files,mountscomoro/rw,output_dir). Las escrituras de caché de paquetes se bloquean después del inicio a menos queallow_runtime_packages=True. - Red — desactivada por defecto (revocada antes de que tu código se ejecute). Opta por
network=True, una lista de permitidosnetwork=["host:port", ...], oallow_runtime_packages=True(solo dominios de paquetes). Para APIs autenticadas, colócale un proxy al frente — consulta Proxy e Inyección de Credenciales. - Herramientas compiladas (WASI) — sin sockets, por lo que un binario no tiene red propia; solo ve los montajes que otorgues (
roaplicado por Deno), y un proceso descontrolado se mata con el tiempo de espera de ejecución. - Límites de recursos — memoria WASM limitada por instancia a nivel de V8 (por defecto 2048 MB), heap de JS limitado, tiempo de espera por ejecución (por defecto 60s →
KeyboardInterrupt), reconexión automática si el proceso de Deno muere. - Puente de contexto — solo los objetos que pases son alcanzables, y solo sus métodos públicos; los servidores MCP exponen su conjunto completo de herramientas.
Trabajo Relacionado
- Ejecución de código con MCP (Anthropic)
- Code Mode (Cloudflare)
- smolagents (Hugging Face)
- Deno + Pyodide Sandbox (Simon Willison)