用 MicroPython 和 WASM 运行沙箱化 Python 代码
Simon Willison··作者 Simon Willison
关键信息
这个包目前仍处于 alpha 阶段,因此它还是一种早期方案,而不是已经完全加固的生产级沙箱。它采用了运行在 WebAssembly 中的 MicroPython,而相关的 PyPI 条目提到可以通过内存和 fuel 限制来执行,这说明资源控制是其设计的一部分。
资讯摘要
Simon Willison 说,他已经就沙箱化代码执行实验了好几年,而现在这套方案似乎最接近他一直在寻找的特性。 他把这个成果发布为一个名为 micropython-wasm 的 alpha 包,并且已经把它用在 Datasette Agent 的一个插件 datasette-agent-micropython 中。 他的动机来自 Datasette、LLM 和 sqlite-utils 这类大量依赖插件的项目。 这些插件虽然让软件可以快速扩展,但当前插件代码会在应用内部拥有完整权限。 这意味着有缺陷或恶意的插件可能破坏应用,或者泄露私有数据。
Willison 希望能在不允许未授权文件访问、网络访问或其他危险操作的情况下运行这类插件式代码。 他还希望同样的隔离能力也能用于 Datasette 的其他场景,例如 enrichments、定时运行的代码,以及把已批准的 JSON 处理成 SQLite 表行的小型任务。 他明确提出的要求包括:可以直接从 PyPI 干净安装、对内存和 CPU 有资源限制,以及限制文件访问。 这篇文章的核心观点是,WebAssembly 作为沙箱层很有前景,而 MicroPython 则提供了一个可以编译成 WASM 的轻量级 Python 运行时。

资讯正文
几年来,我一直在尝试用不同的方法在沙箱中运行代码,而我最近这次尝试感觉终于可能具备了我一直在寻找的所有特性。我已经把它作为一个名为 <a href="https://github.com/simonw/micropython-wasm">micropython-wasm</a> 的 alpha 包发布出来,并且我正在把它用于 <a href="https://github.com/datasette/datasette-agent">Datasette Agent</a> 的一个代码执行沙箱插件,名为 <a href="https://github.com/datasette/datasette-agent-micropython">datasette-agent-micropython</a>。
我的几个核心开源项目——<a href="https://datasette.io/">Datasette</a>、<a href="https://llm.datasette.io/">LLM</a>,甚至还有 <a href="https://sqlite-utils.datasette.io/">sqlite-utils</a>——都支持插件。
我非常喜欢插件这种扩展软件的机制。一个设计精良的插件系统,几乎可以把尝试新事物所涉及的风险降到零——即使是最疯狂的想法,也不会对核心应用本身留下持久影响。我的软件可以在一夜之间长出一个新功能,而我甚至不必审查一个 pull request!
不过这里有一个重大缺点:我所有的插件系统都使用 Python 和 <a href="https://pluggy.readthedocs.io/en/latest/">Pluggy</a>,而插件代码是在我的应用程序内部以完整权限执行的。一个有 bug 的或恶意的插件都可能破坏一切,或者泄露私有数据。
我很希望能够在一种环境中运行类似插件的代码,让它无法读取未获批准的文件,无法连接网络,或者总体上以可能对应用其他部分或用户计算机造成风险或伤害的方式运行。
我感兴趣的不只是插件。就 Datasette 而言,我还想支持许多功能,其中任意代码执行会很有用。我已经在 <a href="https://enrichments.datasette.io/">Datasette Enrichments</a> 上做过相关实验,在那里可以用代码来转换存储在表中的值。我很想构建一种机制:你可以按计划运行代码,从一个已批准的位置获取 JSON,运行一小段代码把它重新格式化为字典列表,然后把这些内容作为行插入 SQLite 数据库表中。
### 我对沙箱的要求
我的目标是在我自己的 Python 应用程序中安全地执行代码。以下是我需要的功能:
- 依赖项必须能够<strong>干净地从 PyPI 安装</strong>,必要时还包括跨多个平台的二进制 wheel。我不希望使用我软件的人,除了直接安装我的 Python 包之外,还需要做任何额外步骤。
- 被执行的代码必须同时受到<strong>内存</strong>和<strong>CPU</strong>限制。我不希望 <code>while True: s += "longer string"</code> 让我的应用程序或用户的电脑崩溃。
- <strong>文件访问必须受到严格控制</strong>。要么完全不能访问文件系统,要么由我精确定义哪些文件可以被读取、哪些文件可以被写入。
- <strong>网络访问也要受到控制</strong>。沙箱中的代码不应在不经过我完全控制的一层中介的情况下与任何东西通信。
- 支持与<strong>宿主函数</strong>交互。如果我不能谨慎地向其运行的代码暴露精选的平台功能,沙箱就没什么用。
- 它必须<strong>稳健、受支持且文档清晰</strong>。我已经数不清见过多少个沙箱项目了,这些仓库里还写着警告,表示它们并没有积极维护!
### WebAssembly 在这里看起来非常有前景
Web 浏览器运行在最恶劣的环境中,面对恶意代码时尤其如此。它们的工作几乎是在每次页面加载时都下载并执行来自网络的不受信任代码。
鉴于这一点,JavaScript 引擎本应是沙箱的绝佳候选。但遗憾的是,这些引擎也极其复杂,而且并不是为了方便嵌入其他项目而设计的。我见过的大多数 Python 中的 V8 项目维护都不频繁,而且都附带警告,提醒不要把它们用于完全不受信任的代码。
WebAssembly 是一个<strong>好得多</strong>的候选方案。它从一开始就被设计用来支持我关心的所有特性,并且在浏览器中经过了将近十年的测试。<a href="https://pypi.org/project/wasmtime">wasmtime</a> Python 库把 WASM 带到了 Python 中,仍在积极维护,而且提供二进制 wheel。
### 在 WebAssembly 中运行 MicroPython
像 wasmtime 这样的 WebAssembly 引擎可以运行 WebAssembly 二进制文件。有些编程语言,比如 Rust,很容易直接编译成 WebAssembly。像 JavaScript 和 Python 这样的动态语言则更难——它们支持诸如 <code>eval()</code> 之类的语言原语,这意味着它们在运行时需要一个完整的解释器。
要运行 Python,我们需要一个完整的 Python 解释器编译成 WebAssembly,并以一种便于向它输入代码、挂接宿主函数以及访问结果的方式进行封装。
Pyodide 提供了一个很出色的包,可以在浏览器中使用 WebAssembly 运行 Python,但在服务器端 Python 中使用 Pyodide 并不受支持。我能找到的最新建议来自 <a href="https://github.com/pyodide/pyodide/discussions/5145">2024 年 10 月</a>,其中写道:“Pyodide 是由 Emscripten 工具链构建的,只能在浏览器或 Node.js 中运行。”
前几天,我决定看看 <a href="https://micropython.org">MicroPython</a> 是否适合作为一种方案。MicroPython 网站上写道:
<blockquote>
<p>MicroPython 是 Python 3 编程语言的一个轻量而高效的实现,它包含了 Python 标准库的一个小子集,并经过优化,可运行在微控制器和受限环境中。</p>
</blockquote>
WebAssembly 对我来说当然就像一个受限环境!
<h4 id="building-the-first-version">构建第一个版本</h4>
我让 GPT-5.5 Pro <a href="https://chatgpt.com/share/6a1e2a5c-58b8-8328-ba1c-0e6aadb0a051">帮我做了一些研究</a>,结果找到了 <a href="https://github.com/micropython/micropython/pull/13676">这个针对 MicroPython 的 PR</a>,作者是 <a href="https://github.com/yamt">Yamamoto Takahashi</a>,标题为“Experimental WASI support for ports/unix”。
随后它生成了这份 <a href="https://github.com/simonw/micropython-wasm/blob/c08fbd2276b15dc8c9bdff82845f750971f45647/research.md">research.md 文档</a>,于是我让 Codex Desktop 和 GPT-5.5 high <a href="https://gist.github.com/simonw/27461a16d76f28f8619c609444d544fe">一起上手处理</a>,看看会发生什么:
<blockquote>
<p><code>read the research.md document and build this. You will probably need to write a script that compiles a custom WASM version of MicroPython as part of this project - fetch the MicroPython code to a /tmp directory for this as part of that script.</code></p>
</blockquote>
它成功了。我现在有了一个原型 Python 库,能够在 WebAssembly 沙箱中执行 Python 代码!
最棘手的问题是如何解决解释器状态的持久化。我们在这里使用的 WASM 构建版本只暴露了一个入口点:它启动解释器,运行代码,然后在最后停止解释器。
对于一次性脚本来说,这完全没问题,但对于 Datasette Agent,我希望变量和函数能够常驻内存,这样我就可以在多次代码执行调用之间复用它们。
与编码代理一起工作的一个很棒的地方在于,你可以很快从一个想法走到一个概念验证。我下达了这样的提示:
<p><code>为了让变量保持驻留:如果我们在 micropython 自身内部运行代码,让它调用一个宿主函数 get_next_python_code(),再把返回内容传给 eval() —— 而且这个宿主函数会一直阻塞,直到有新代码可用,也许是通过在带有队列的线程中运行?这会不会,或者类似的思路,能帮助解决这里的问题?</code></p>
<p>经过一些迭代,我们得到了一个可用的版本!现在你可以在 Python 代码里这样做:</p>
<pre><span class="pl-k">from</span> <span class="pl-s1">micropython_wasm</span> <span class="pl-k">import</span> <span class="pl-v">MicroPythonSession</span>
<span class="pl-k">with</span> <span class="pl-en">MicroPythonSession</span>() <span class="pl-k">as</span> <span class="pl-s1">session</span>:
<span class="pl-en">print</span>(<span class="pl-s1">session</span>.<span class="pl-c1">run</span>(<span class="pl-s">"x = 10<span class="pl-cce">\n</span>print(x)"</span>).<span class="pl-c1">stdout</span>)
<span class="pl-en">print</span>(<span class="pl-s1">session</span>.<span class="pl-c1">run</span>(<span class="pl-s">"x += 5<span class="pl-cce">\n</span>print(x)"</span>).<span class="pl-c1">stdout</span>)
<span class="pl-en">print</span>(<span class="pl-s1">session</span>.<span class="pl-c1">run</span>(<span class="pl-s">"print(x * 2)"</span>).<span class="pl-c1">stdout</span>)</pre>
<p>在底层,这会启动一个线程,建立一个请求队列,然后为 <code>session.run()</code> 命令向该队列发送消息,每次都在结果回复队列上等待该次执行的返回值。在 WASM 内部,MicroPython 解释器会阻塞,等待宿主函数 <code>__session_next__()</code> 返回下一行代码;它会对这行代码执行 <code>eval()</code>,然后在每个代码块成功执行后调用 <code>__session_result__({"id": request_id, "ok": True})</code>。</p>
<p>另一个复杂之处是支持宿主函数,这样我的 Python 库就可以选择性地暴露一些函数,供在 MicroPython 中运行的代码调用。</p>
<p>Codex 最终用 <a href="https://github.com/simonw/micropython-wasm/blob/0.1a1/micropython_wasm/usercmodule/host/hostmodule.c">78 行 C 代码</a> 解决了这个问题,而这段代码最终会被编译进我随包分发的 <a href="https://github.com/simonw/micropython-wasm/blob/0.1a1/micropython_wasm/artifacts/micropython-wasi.wasm">362KB WebAssembly 二进制</a> 中。</p>
<p>我绝不是 C 程序员,但我读过这段 C 代码,也让两个不同的模型向我解释过它(这里是 <a href="https://claude.ai/share/62f74371-cc3c-44f2-b406-33d03513de9e">Claude 的解释</a>),而且我还对它进行了大量测试。</p>
<p>使用 WebAssembly 的好处在于,如果这段 C 代码最终被证明有致命缺陷,最坏的情况也不过是 WebAssembly 执行失败并抛出异常。对此风险,我完全可以接受。</p>
内存限制已由 wasmtime 直接支持。CPU 限制要稍微难一些:wasmtime 提供了一个“fuel”概念,用来限制一次 WebAssembly 调用可以执行多少操作,这很适合这个问题,但这些单位并不容易直观理解。我现在正在试验把默认“fuel”设置为 2000 万,不过我还不确定这是不是最合适的值。
自己试试看
<code>micropython-wasm</code> 的 alpha 版现在已经<a href="https://pypi.org/project/micropython-wasm">上线到 PyPI</a>。
你可以按照<a href="https://github.com/simonw/micropython-wasm">README 中的说明</a>,从你自己的 Python 代码中试用它。我还在<a href="https://github.com/simonw/micropython-wasm/releases/tag/0.1a2">0.1a2 版本</a>中添加了一个简单的 CLI 模式,这意味着你可以先不安装它,直接用 <code>uvx</code> 这样试用:
uvx micropython-wasm -c 'print("Hello world")'
# 让它运行到耗尽 fuel:
uvx micropython-wasm -c 's = ""; while True: s += "longer"'
# 输出:micropython-wasm: guest exited with code 1
你也可以在 <a href="https://agent.datasette.io/">Datasette Agent</a> 中这样试用:
uvx llm keys set openai
# 粘贴一个 OpenAI key,然后:
uvx --with datasette-agent \
--with datasette-agent-micropython \
--prerelease allow \
datasette --internal internal.db \
-s plugins.datasette-llm.default_model gpt-5.5 \
--root -o
然后访问 <a href="http://127.0.0.1:8001/-/agent">http://127.0.0.1:8001/-/agent</a> 并运行这个提示:
<code>show me some micropython</code>
<p><img alt="聊天应用界面的截图,深蓝灰色标题栏左侧写着“home”,右侧写着“root”并带有汉堡菜单图标。下面是一行导航,左侧有“← Back”和“Chat”,右侧有“EXPORT”按钮。一个蓝色用户消息气泡写着“show me some micropython”。下方有一个折叠的思考区,内容为“▸Thinking: … to show the result clearly. After that, I can wrap up with a brief explanation!”;其后是“▶ Tool: execute_micropython”标签。接着是一段代码块:“# A tiny MicroPython example: blink-style logic + Fibonacci” / “def fib(n):” / “ a, b = 0, 1” / “ out = []” / “ for _ in range(n):” / “ out.append(a)” / “ a, b = b, a + b” / “ return out” / 'print("Hello from MicroPython!")' / 'print("First 10 Fibonacci numbers:", fib(10))' / “# MicroPython often runs on microcontrollers, e.g.:” / “# from machine import Pin” / “# led = Pin(2, Pin.OUT)” / “# led.value(1) # turn LED on” / “# led.value(0) # turn LED off”。在一条水平分隔线下方是输出:“Hello from MicroPython!” / “First 10 Fibonacci numbers: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]”,后面跟着“▶ Result: execute_micropython”标签。底部是一个文本输入框,占位提示为“Type a message...”,右侧有一个蓝色“Send”按钮。" src="https://static.simonwillison.net/static/2026/micropython-in-datasette-agent.jpg" /></p>
<p>你可以通过使用你的 GitHub 账户登录 <a href="https://agent.datasette.io">agent.datasette.io</a>,来试用在 Datasette Agent 中运行的那个插件的在线演示。</p>
<p>我已经抱怨过那些不成熟、维护松散的沙箱库,如今却自己做了一个,这真是极具讽刺意味!</p>
<p>我特意把它标成了 alpha 版本;对于那些不愿承担重大风险的人,我还不准备推荐它。</p>
<p>我已经对它进行了足够多的测试,因此自己使用它没有问题。我已经发布了第一个使用它的插件,<a href="https://github.com/datasette/datasette-agent-micropython">datasette-agent-micropython</a>。我还在那个 Datasette Agent 插件里锁定了 GPT-5.5 xhigh,并<a href="https://gist.github.com/simonw/5de497c44d25f9fd459c8aa2c959fe4a">挑战它从沙箱中逃脱</a>,到目前为止它还没有成功。</p>
<p>我希望这一实现能够说服一些拥有专业安全团队、且问题重要程度很高的公司,承诺把 Python 运行在 WebAssembly 中作为一种沙箱方案,并开源他们自己的解决方案。</p>
标签:python、sandboxing、ai、datasette、webassembly、generative-ai、llms、ai-assisted-programming、codex、datasette-agent、micropython
来源与参考
收录于 2026-06-07