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

Parselbox SDK Parselbox SDK

Código. Sistema de archivos. Contexto. Herramientas.
¿Qué pasaría si los agentes tuvieran una sola herramienta para gobernarlos a todos?

License PyPI - Version CI

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

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":

  1. Agrega el Parselbox MCP junto a tus servidores MCP existentes.
  2. Apunta --mcp al mismo archivo de configuración.
  3. 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étodoNivel de accesoDescripción
filesLectura / EscrituraDirectorio temporal en /files/. Los archivos de entrada se copian aquí; las subidas del servidor se almacenan aquí.
mountsConfigurableMapea directorios del host a /mnt/{name}. Modo: ro (predeterminado) o rw.
output_dirLectura / EscrituraMapea el directorio de trabajo a un directorio del host para persistir archivos. Si no se proporciona, usa un directorio temporal (borrado al cerrar).

[!NOTE]

  • /workspace siempre 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() y require() para módulos locales.
  • Cruza el límite con sandbox.read_file(path) (str para texto, bytes para binario) y sandbox.write_file(path, content); un output_dir persistente también es legible directamente.
  • Los montajes con target="skills" son reportados por sbx.info() y descubribles vía bash("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_dir para 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 --network de 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 .wasm sueltos. Ambas rutas funcionan; require() te dice cuál necesita un binario.

run(args=None, stdin="", env=None, preopens=None, argv0=None)
  • stdinstr o bytes; stdout siempre regresa como bytes.
  • 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 en wasm-ld al 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 preview1 o wasi_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ónDescripció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:

EndpointMétodoDescripción
/_uploadPOSTSubida de archivos (datos de formulario multipart)
/_liveGETFlujo SSE — los navegadores conectados se actualizan cuando los archivos estáticos cambian (activado por defecto)
/_routesGETLista 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)
HookDisparadorCasos de uso
pre_executeAntes de que el código se ejecuteRegistro, verificaciones de políticas, sanitización de código
post_executeDespués de que el código se completaRutas de auditoría, validación de resultados
pre_tool_callAntes de una llamada de contexto/MCPPuertas de aprobación, limitación de velocidad, bloqueo
post_tool_callDespués de que una llamada de contexto/MCP regresaRegistro, 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, mounts como ro/rw, output_dir). Las escrituras de caché de paquetes se bloquean después del inicio a menos que allow_runtime_packages=True.
  • Red — desactivada por defecto (revocada antes de que tu código se ejecute). Opta por network=True, una lista de permitidos network=["host:port", ...], o allow_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 (ro aplicado 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

Construido con Deno, Pyodide y just-bash (Vercel).