mcpField
P
Parselbox

Parselbox

本地服务
github.com开发工具35 周前更新

关于 Parselbox

Parselbox 是面向 AI Agent 的可嵌入 Python 沙箱运行时,可将 MCP 服务、API 和 Shell 作为 Python 对象调用,并在磁盘工作区中组合执行。它基于 Deno 和 Pyodide,提供文件、网络权限控制及后台任务等能力。

主要功能

  1. 01将 MCP 工具及 API 封装为 Python 对象
  2. 02提供带权限控制的隔离运行时
  3. 03集成磁盘工作区、挂载和文件持久化
  4. 04支持后台任务与交互式会话
  5. 05支持 Python、npm、JavaScript 和 WASM

适用场景

  1. 01在一段 Python 代码中编排多个工具
  2. 02在受控工作区处理输入和输出文件
  3. 03将耗时工具调用放入后台任务
  4. 04让 Agent 按需发现并组合工具

MCP 相关

Parselbox 提供独立 MCP 服务,可通过 STDIO 或 HTTP 连接。配置其他 MCP 服务后,它会把相应工具带入沙箱,供 Agent 作为 Python 对象调用。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"thesanjeetc-parselbox": {
"command": "uvx",
"args": [
"parselbox"
]
}
}
}
启动命令uvx parselbox

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypiparselbox0.1.1

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

Parselbox SDK Parselbox SDK

代码。文件系统。上下文。工具。
如果 Agent 有一个工具可以统御一切,会怎样?

许可证 PyPI - 版本 持续集成

Parselbox 是一个可嵌入的 Python 运行时,AI Agent 可以将工具作为代码调用——MCP 服务、API 和 shell 都会变成原生 Python 对象。内置磁盘支持的工作区、包和网络;由 Deno 和 Pyodide 驱动的单进程执行层。

https://github.com/user-attachments/assets/d4e43d16-3aa3-4e29-83c7-a1d3885b8045

提示

将 Parselbox MCP 与现有的 MCP 服务配置一起使用。Agent 可立即获得 Python 运行时、作为代码调用的 MCP 工具、技能支持和磁盘支持的工作区。

为什么选择 Parselbox?

Parselbox 将程序化工具调用打包为本地开源执行层。通过 uvx 将其添加到现有 MCP 配置中,或将 Python API 嵌入任何 Agent 技术栈。

除了代码执行,Parselbox 还提供有状态、磁盘支持的工作区,内含包、受控网络、后台任务和渐进式发现功能。

Deno + Pyodide 提供单进程运行时,并可明确控制文件系统和网络权限——无需容器或微型虚拟机。它还支持 JavaScript/npm 互操作、WASM/WASI 集成,以及通过 MCP Apps 实现的生成式 UI。

为什么需要执行层?

大多数 Agent 技术栈都将每项能力作为单独的工具提供。随着集成不断增加,工具架构会占用更多上下文。模型还必须保存中间结果、转换数据,并在调用之间传递值。

执行层将编排从上下文窗口转移到代码中。按需发现能力后,即可通过完整的控制流将其组合起来。单次执行便可协调多个工具,同时变量、状态和文件都保留在运行时中。

这样可以减少上下文膨胀和模型与工具之间的往返次数,并将复杂工作流表示为代码,而不是一连串独立调用。

功能

🔒 安全隔离

无需容器或虚拟机——只需一个轻量级的 Deno + Pyodide 进程(约 160 MB)。Deno 权限、内存上限、超时设置、网络允许列表。支持快照缓存和崩溃恢复。

🛠️ 将工具作为代码

MCP 服务、REST + OpenAPI、GraphQL、shell、函数和类——全部作为原生 Python 对象。有状态,可跨调用保留。自动转换为 Pydantic 类型。凭据保留在主机上。

🐍 多语言运行时

完整的 CPython,支持 js() 互操作——可将 JS 包作为原生 Python 使用。通过 require() 使用 npm、本地 TypeScript 和 .wasm 模块。虚拟 bash() 用于运行 shell。导入时自动安装包。

📦 WASM 工具

通过 require() 加载任意 .wasm 文件——库导出内容会变成 Python 方法,WASI 程序会变成可调用命令;将其放入 bin/,也可以通过 bash() 运行。进程内运行,继承沙箱的挂载点和权限,不会在主机上安装任何内容。

⚡ 后台任务

在任意调用后添加 .task()——使用 asyncio.gather 并行分发,检查进度、查看日志末尾、通过 send() 驱动交互式会话,并在之后等待结果。

📁 文件系统集成

由磁盘支持的工作区 — 宿主机挂载(ro/rw),输入文件位于 /files/,输出会持久保存到真实目录。每次调用都会检测并返回新建和修改的文件。

🔍 渐进式信息披露

help()、search()、inspect()、preview() — Agent 只在需要时发现所需内容。

🎨 生成式 UI

display() 会在聊天中内联渲染 HTML(MCP Apps),并注入 Tailwind + daisyUI。也可以提供完整应用 — 内置 HTTP 服务,支持静态文件、实时重载、文件上传,以及可跨工具组合的 @api 路由。


目录

快速入门

Parselbox 使用 Deno 作为安全的沙箱运行时。

1. 安装 Deno

# macOS / Linux
curl -fsSL https://deno.land/install.sh | sh

# Windows (PowerShell)
irm https://deno.land/install.ps1 | iex

2. 安装 Parselbox

pip install parselbox
Parselbox API

将任意工具接入沙箱 — MCP 服务、REST/GraphQL、shell、宿主对象 — 然后 Agent 就能像调用原生 Python 一样调用它们,在由磁盘支持的工作区以及 Python 和 npm 包生态中,通过真实控制流将它们组合起来。

示例:

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

Parselbox CLI 可运行独立的 MCP 服务 — 所有沙箱选项均可通过标志指定。

STDIO

提示

“回环”技巧:

  1. 将 Parselbox MCP 添加到现有 MCP 服务中。
  2. 将 --mcp 指向同一个配置文件。
  3. 启动时,Parselbox 会连接其他服务,在沙箱中公开其工具,并启动自己的 MCP 服务。

不必担心 — Parselbox 会检测并避免连接自身。不会陷入无休止的循环。

示例:

{
  "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"
    }
  }
}
完整示例
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=...

Parselbox Agents
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())

用户指南

1. 代码即工具

上下文桥接器会在沙箱中公开宿主 Python 对象:

  • context — 函数和命名空间作为可调用工具。执行会暂停,由宿主执行并返回结果。
  • globals — 静态值(字符串、数字、字典),会复制到沙箱中。
  • mcp — MCP 服务配置(字典或路径)。在沙箱中显示为可调用的命名空间。

普通类会自动包装 — 每个公开方法都会成为可调用工具;以下划线 _ 开头的方法保持私有:

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

继承 Bridge 可创建嵌套命名空间(自动遍历);为参数添加 Pydantic 模型注解后,传入的字典会自动转换为该模型:

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 内置了用于 REST、GraphQL 和 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')")

可运行:bridges.py

2. 后台任务

每个 context 和 MCP 调用都有对应的 .task() 形式,可在宿主上运行而不阻塞沙箱 — 适用于并行扇出、长时间运行的作业和交互式会话:

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)])

MCP 工具会将进度和日志通知流式写入任务的日志文件。自定义 Bridge 方法也可通过 self.log() 发出相同内容,并通过 self.recv() 读取沙箱用 send() 排入队列的内容:

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"

交互式会话 — ShellBridge.shell() 会保持 stdin 打开,因此任务可以通过 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()

可选的首条命令会以会话形式启动任意 REPL — 例如 sh.shell.task("python3 -i")。

可运行:tasks.py

3. 文件系统集成

Parselbox 在 Pyodide 的虚拟文件系统上运行,工作目录、输入文件、挂载目录和软件包都由真实的宿主目录提供支持——访问权限由 Deno 在启动时通过权限控制进行限制。

方法访问级别说明
files读 / 写/files/ 下的临时目录。输入文件会复制到此处;服务端上传的文件也存储在此处。
mounts可配置将宿主目录映射到 /mnt/{name}。模式:ro(默认)或 rw。
output_dir读 / 写将工作目录映射到宿主目录以持久保存文件。如果未提供,则默认为临时目录(关闭时清除)。

说明

  • /workspace 始终由真实的宿主目录提供支持——output_dir(持久保存)或临时目录(关闭时清除)——因此可以使用 Deno 流、resolvePath() 和 require() 加载本地模块。
  • 使用 sandbox.read_file(path)(文本返回 str,二进制返回 bytes)和 sandbox.write_file(path, content) 跨越边界;持久化的 output_dir 也可直接读取。
  • target="skills" 的挂载会通过 sbx.info() 报告,也可通过 bash("ls /mnt/skills/") 发现。

示例:

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

也可以通过 bash() 在 shell 中访问相同的文件:

bash("echo 'hello from bash' > note.txt && cat note.txt")   # shell over the workspace

可运行: filesystem.py · bash.py

4. 软件包与网络
软件包

Pyodide 支持纯 Python 软件包和许多 C 扩展软件包,这些软件包必须针对 Pyodide 预构建——numpy、pandas 等软件包已包含在内。

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)

说明

安装软件包时会直接写入磁盘——默认写入临时目录(退出时清除)。设置 package_dir 可在会话之间保留软件包,这样下次启动时无需重新下载,可立即完成。

网络

初始软件包加载完成后,网络默认处于阻止状态。可通过 Deno 的权限控制使用 --allow-net / --deny-net 配置访问权限。来自沙箱代码的所有 HTTP 请求(requests、httpx、fetch)都会通过 Deno 的 fetch() 路由。

# Block everything (default)
Parselbox(network=False)

# Allow specific domains (Python API only)
Parselbox(network=["api.github.com:443"])

# Allow everything
Parselbox(network=True)

说明

CLI 的 --network 标志仅为布尔开关。域名允许列表可通过 Python API 配置。

代理与凭据注入

Pyodide 不是安全边界——沙箱代码可以通过 js('Deno.env.get("KEY")') 读取环境变量,因此绝不要在 env 中传入真实凭据。应改为在宿主上运行注入凭据的代理,并将沙箱限制为仅访问该代理:

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(...)")

大多数 SDK 都支持覆盖 base_url。若要以与 SDK 无关的方式拦截请求,可改为设置 HTTP_PROXY/HTTPS_PROXY/DENO_CERT,并通过中间人代理路由所有流量——Deno 的 fetch() 会在进程级别遵循这些设置。

可运行: basics.py

5. JavaScript 互操作

Parselbox 通过 Pyodide 在 Deno 的 V8 引擎中运行 Python,因此 Python 和 JavaScript 共享同一进程内存——互操作非常顺畅。

js() — 从 Python 执行 JavaScript
# 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()")

每次调用 js() 都会在全新、无状态的作用域中运行。Python 可调用对象会自动代理,并在调用后清理。二进制数据也会自动转换——Uint8Array/ArrayBuffer 结果会转换为 Python bytes,bytes 参数则会转换为 Uint8Array。

require() — 导入 npm 软件包、本地模块和 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()
Deno 流处理(大文件)

对于过大而无法装入内存的文件,可通过 resolvePath() 使用 Deno 流:

js("""
    const path = resolvePath("sample.txt");
    const info = await Deno.stat(path);
    return { size: info.size, isFile: info.isFile };
""")

流处理管道中也可以使用 Python 回调——由 Deno 读取、JS 解析,再由 Python 对每一行进行分类。

编写和导入模块
# 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"

甚至可以在沙箱中将某种语言编译为 WebAssembly,然后使用 require() 导入编译结果。

bash() — Shell 命令

基于纯 JavaScript 的 bash([just-bash](https://github.com/vercel-labs/just-bash),运行于同一工作区。管道和 coreutils 均可使用,curl 由 fetch 提供支持。每次调用彼此隔离(cd/export 不会持久生效);文件系统更改会保留。

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

可运行: javascript.py · bash.py

6. WASM 工具

Pyodide 只能加载专为它构建的包,因此无法使用 pandoc、ruby 或 shellcheck,而且单进程沙箱中没有 apt-get。Parselbox 通过 WASI 弥补这一缺口:任何编译为 WebAssembly 的程序都能成为工具,无需在宿主机上安装。

缺少的能力,只需一个文件即可补足。

两种 .wasm

require() 会检查模块并选择相应的形式:

# 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>'

命令会返回 {"exit": int, "stdout": bytes, "stderr": str, "missing": [...]}——missing 会列出二进制文件调用但尚未实现的系统调用,让缺失能力以数据形式显现,而不是导致崩溃。

重要

Emscripten 构建产物并非 WASI 构建产物。 npm 中许多“wasm”(sql.js、ffmpeg.wasm、tesseract.js)都是使用 Emscripten 编译的,需要各自的 JavaScript 胶水代码——应将它们作为 npm 包导入(require("sql.js")),而非直接导入 .wasm 文件。两种方式都可用;require() 会告诉你二进制文件需要哪种方式。

run(args=None, stdin="", env=None, preopens=None, argv0=None)
  • stdin — str 或 bytes;stdout 始终以 bytes 返回。
  • preopens — 授予额外的来宾目录,例如,对于需要自带目录树的二进制文件,可使用 preopens={"/usr": "vendor/usr"}。
  • argv0 — 某些二进制文件会根据程序名称进行分派(lld 会像 busybox 一样变为 wasm-ld)。
将二进制文件用作 bash() 命令

挂载目录的 bin/ 中的 WASI 命令二进制文件(导出 _start 的 .wasm)会成为 Shell 命令,可与 bash() 的 JavaScript coreutils 一同使用。每次调用时都会发现二进制文件,因此在会话中途写入的工具可以立即使用。

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

以只读方式挂载 bin/ 文件夹,即可提供固定工具集供 Agent 使用但无法修改——宿主机上无需安装任何内容。也可以让它在运行时将 .wasm 下载到 bin/,即使沙箱网络仅允许访问一个加入允许列表的主机,也能正常工作。

你甚至可以在进程内从源代码构建工具——将 WASI clang 和 wasm-ld 下载到 bin/,把 C 编译为 .wasm,然后对结果调用 require()。无需宿主机工具链,也无需安装任何内容。

说明

  • 会自动检测为 WASI preview1 或 wasi_unstable(preview0)。不支持:套接字、实际休眠、preview2 组件。
  • 编译后的模块会按路径缓存(重建时失效),因此 50MB 的二进制文件在每个会话中只需编译一次。

可运行: pandoc.py — 下载 WASI 二进制文件 · compile_c.py — 在沙箱中编译 C → wasm

7. 渐进式披露

sbx 工具包让 Agent 按需发现能力,而非一开始就将所有内容加载到上下文中。可在沙箱内通过 sbx.* 使用。

函数说明
sbx.help()返回沙箱使用指南。
sbx.info()获取沙箱环境信息——包括上下文、包、网络、挂载、serve 等。
sbx.search(pattern)按名称、描述或参数搜索所有命名空间中的工具。
sbx.inspect(tools)获取工具的详细架构和文档。
sbx.preview(data)概述大型或嵌套的数据结构——保留键并截断内容。

沙箱还提供了 help() 内置函数,可用于查看对象的详细信息:

# 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)

示例:

# 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)

可运行: toolkit.py

8. 生成式 UI

Agent 可以通过两种方式呈现结果:使用 display() 内嵌在对话中,或使用 serve 构建完整的 Web 应用。

内联小组件 — display()

在支持 MCP Apps 的宿主环境中,Agent 传递给 display() 的任何 HTML 都会在结果下方渲染为小组件。

await sbx.execute_code("""
    display("<h1>Q3 Revenue</h1><p class='text-lg'>Up <b>12%</b> to $4.1M</p>")
""")

Tailwind 和 daisyUI 会自动注入,因此无需构建步骤,普通标记也能应用样式;启用 serve 时,HTML 中的 pbx.call("/api/route", body) 会调用 @api 处理程序。display() 也接受工作区中的 HTML 文件路径。每次执行只显示一个视图——以最后一次调用为准。

默认启用。 run_mcp(ui=False) 会将其关闭,不再向 Agent 广告 display(),并从工具中移除渲染器。无论如何,渲染后的 HTML 始终位于 result.view 上:

result = await sbx.execute_code('display("<b>done</b>")')
result.view          # full HTML document, or None if display() wasn't called
Web 应用 — serve

serve 选项会在沙箱内启动 Deno HTTP 服务器——Agent 可即时构建完整的 Web 应用。

# SDK
sandbox = Parselbox(serve=3000)

# CLI
uvx parselbox --serve 3000

静态文件: 写入 Pyodide 工作目录的所有文件都会自动提供服务:

open("index.html", "w").write("<h1>Hello World</h1>")
open("style.css", "w").write("h1 { color: blue; }")

文件会通过各自的路径提供服务,其中 / 解析为 index.html;上传和输入文件位于 /files/* 下。

API 处理程序: 使用 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"]}

路由会自动添加 /api/ 前缀。动词:@api.get/post/put/patch/delete。

处理程序可以调用 MCP 工具、上下文函数以及任何沙箱代码:

@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}

内置端点:

端点方法说明
/_uploadPOST文件上传(multipart 表单数据)
/_liveGETSSE 流——静态文件变更时,已连接的浏览器会刷新(默认启用)
/_routesGET列出已注册的 API 处理程序
curl -F "[email protected]" http://localhost:3000/_upload
# {"uploaded": [{"name": "photo.png", "path": "/files/photo.png", "size": 12345}]}

可运行示例: display.py · serve.py

9. 沙箱钩子

钩子会拦截沙箱生命周期事件——记录执行情况、审批工具调用、实施策略。通过 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 — 内置钩子,使用 MCP elicitation 实现人工参与审批。通过 --elicit(CLI)或 run_mcp(elicit=True)(API)启用。只有 MCP 客户端声明支持 elicitation 时才会触发,否则不执行任何操作。

# CLI
uvx parselbox --mcp mcp.json --elicit

# API
await sandbox.run_mcp(elicit=True)
钩子触发时机用途
pre_execute代码运行前日志记录、策略检查、代码清理
post_execute代码完成后审计跟踪、结果验证
pre_tool_call上下文/MCP 调用前审批门控、速率限制、阻止调用
post_tool_call上下文/MCP 调用返回后日志记录、结果转换

可运行示例: hooks.py


配置参考

Parselbox 的配置选项如下:

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
    },
)

架构

Parselbox 在单个 Deno 进程中通过 Pyodide(WebAssembly 中的 CPython)运行 Agent 代码——无需容器或虚拟机。受权限限制的沙箱在隔离的临时工作区中运行,没有网络,也无法访问宿主机(已授予挂载权限的内容除外);宿主机保存凭据。每次工具调用都是两者之间的一次往返:

  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

在沙箱中,工具看起来像原生 Python,但实际在宿主机上执行——因此凭据绝不会进入沙箱。


安全性

Parselbox 的边界是 Deno 的权限系统——沙箱启动时不授予任何权限,只获取你配置的权限。

  • 文件系统 — 隔离的临时工作区(退出时清空);仅对你传入的路径进行读写(files、mounts,设为 ro/rw,以及 output_dir)。启动后,除非设置 allow_runtime_packages=True,否则会锁定对包缓存的写入。
  • 网络 — 默认关闭(在你的代码运行前撤销)。你可以通过 network=True、允许列表 network=["host:port", ...] 或 allow_runtime_packages=True(仅限包域名)选择启用。对于需要身份验证的 API,请通过代理转发——参见代理与凭据注入。
  • 编译型工具(WASI) — 不支持套接字,因此二进制程序无法自行联网;它只能访问你授权的挂载(由 Deno 强制执行 ro),超时后会终止失控的进程。
  • 资源限制 — 在 V8 层面为每个实例限制 WASM 内存(默认 2048 MB),限制 JS 堆,设置单次执行超时时间(默认 60s → KeyboardInterrupt),并在 Deno 进程退出时自动重新连接。
  • 上下文桥接 — 只能访问你传入的对象,且仅限其公开方法;MCP 服务会公开其完整工具集。

相关项目

基于 Deno、Pyodide 和 just-bash(Vercel)构建。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。