Sandboxing Agentic AI Workflows with WebAssembly
Agentic AI workflows
often involve the execution of
large language model (LLM)
-generated code to perform tasks like creating data visualizations. However, this code should be sanitized and executed in a safe environment to mitigate risks from
prompt injection
and errors in the returned code. Sanitizing Python with regular expressions and restricted runtimes is insufficient, and hypervisor isolation with virtual machines is development and resource intensive.
This post illustrates how you can gain the benefits of browser sandboxing for operating system and user isolation using
WebAssembly (Wasm)
, a binary instruction format for a stack-based virtual machine. This increases the security of your application without significant overhead.
Securing agentic tool use
One of the recent evolutions in LLM application development is exposing tools
—
functions, applications, or APIs that the LLM can call and use the response from. For example, if the application needs to know the weather in a specific location, it could call a weather API and use the results to craft an appropriate response.
Python code execution is a powerful tool for extending LLM applications. LLMs are adept at writing Python code, and by executing that code, they can execute more advanced workflows such as data visualization. Extended with Python function calling, a text-based LLM would have the ability to generate image plots for users. However, it’s difficult to dynamically analyze the LLM-generated Python to ensure it meets the intended specification and doesn’t introduce broader application security risks. If you find yourself executing LLM-generated Python to extend your agentic application, this post is for you.
Structuring the agent workflow
In the simplest agentic workflow, your LLM may generate Python that you eventually pass to
eval
. For example, a prompt equivalent to
generate the python code to make a bar chart in plotly
would return
import plotly.graph_objects as go\n\nfig = go.Figure(data=go.Bar(x=["A", "B", "C"], y=[10, 20, 15]))\nfig.update_layout(title="Bar Chart Example")
. Your agent would pass this into
eval
to generate the plot as shown in Figure 1.
Figure 1. The simplest agentic workflow between the user, their machine, and the AI model used to generate a plot with Python
In Step 1, the user provides their prompt to the application. In Step 2, the application provides any additional prompt context and augmentation to the LLM. In Step 3, the LLM returns the code to be executed by the tool-calling agent. In Step 4, that code is executed on the host operating system to generate the plot, which is returned to the user in Step 5.
Notice that the
eval
is performed on the server in Step 4, a significant security risk. It is prudent to implement controls to mitigate that risk to the server, application, and users. The easiest controls to implement are at the application layer, and filtering and sanitization are often done with regular expressions and restricted Python runtimes. However, these application-layer mitigations are rarely sufficient and can usually be bypassed.
For instance, a regular expression may attempt to exclude calls to
os
but miss
subprocess
or not identify that there are ways to eventually reach those functions from various dependency internals. A more robust solution might be to only execute the LLM-generated Python in a micro virtual machine like
Firecracker
, but this is resource and engineering intensive.
As an alternative, consider shifting the execution into the user’s browser. Browsers use sandboxes to isolate web page code and scripts from the user’s infrastructure. The browser sandbox is what prevents webpages from accessing local filesystems or viewing webcams without authorization, for example.
Using Python in the browser
Pyodide
is a port of CPython into Wasm to create a sandbox that may be used inside existing JavaScript virtual machines. This means that we can execute Python client-side to inherit all of the security benefits of a browser sandbox.
By designing the application to serve HTML with the Pyodide runtime and the LLM-generated code as shown in Figure 2, application developers can shift the execution into the users’ browsers, gaining the security of sandboxing and preventing any cross-user contamination.
Figure 2. An agentic workflow modified to use Pyodide to shift the plot generation from the server into the user’s browser
The main difference in this architecture is that instead of executing the tool on the application server, the application instead returns HTML to the user in Step 4. When the user views that HTML in their browser, Pyodide executes the LLM-provided Python in the sandbox
and renders the visualization.
This modification should require minimal change in prompting strategies, as the LLM-generated code can usually be templated into a static HTML document. For example, this function takes the LLM-provided code, and prepares it before dispatching it for execution with
executeCode
.
window.runCode = async (LLMCode) => {
     try {
         console.log('Starting code execution process...');
         const pyodide = await initPyodide();
                
         const wrappedCode = [
             'import plotly.graph_objects as go',
             'import json',
             'try:',
             '    ' + LLMCode.replace(/\\n/g, '\\n    '),
             '    if "fig" in locals():',
             '        plotJson = fig.to_json()',
             '    else:',
             '        raise Exception("No \\'fig\\' variable found after code execution")',
             'except Exception as e:',
             '    print(f"Python execution error: {str(e)}")',
             '    raise'
         ].join('\\n');

         await executeCode(pyodide, wrappedCode);
       ...
If your code has Python dependencies (like
Plotly
), you can install them with
micropip
in the client-side Javascript. Micropip supports python wheels from PyPI, including many with C extensions.
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install('plotly');
Improving application security with Wasm
Imagine a scenario where the LLM returns malicious code, either as a result of prompt injection or error. In the case of the simplest agentic workflow, the call to
eval
results in the compromise of the application, potentially impacting the host operating system and other users as shown in Figure 3.
Figure 3. The malicious code is executed on the server, impacting that service and its users
However, after applying the Wasm flow, there are two possible cases illustrated in Figure 4. First, the application may throw an error because the malicious Python code cannot be executed in the narrowly scoped Pyodide runtime (that is, a missing dependency).
Error: Traceback (most recent call last): File 
     "/lib/python311.zip/_pyodide/_base.py", line 499, in eval_code 
     .run(globals, locals) ^^^^^^^^^^^^^^^^^^^^ File 
     "/lib/python311.zip/_pyodide/_base.py", line 340, in run coroutine = 
     eval(self.code, globals, locals) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File 
     "<exec>", line 2, in <module> ModuleNotFoundError: No module named 
     'pymetasploit3'
Second, if the code does execute, it is restricted to the browser sandbox which greatly limits any potential impact to the end user’s device.
Figure 4. The malicious code propagates into the HTML, but either cannot be executed on the user’s device or has minimal impact due to the browser sandbox
In either case, using Pyodide improves security controls for the querying user while reducing risk to application resources and adjacent users.
Get started
Sandboxing LLM-generated Python with WebAssembly offers a convenient approach, requiring minimal changes to existing prompts and architectures. It is cost-effective by reducing compute requirements, and provides both host and user isolation with improved security of the service and its users. It is more robust than regular expressions or restricted Python libraries, and lighter weight than containers or virtual machines.
To get started improving application security for your agentic workflows using Wasm, check out this
example
on GitHub. Learn more about
AI agents and agentic workflows
.
借助 WebAssembly 实现沙箱 Agentic AI 工作流
代理 AI 工作流通常
涉及执行由
大语言模型 (LLM)
生成的代码，以执行创建数据可视化等任务。但是，此代码应在安全环境中清理和执行，以降低提示
注入的风险
和返回代码中的错误。使用正则表达式和受限运行时清理 Python 是不够的，而且虚拟机的 Hypervisor 隔离需要大量的开发和资源。
本文将介绍如何使用
WebAssembly (Wasm) (一种基于堆栈的虚拟机的二进制指令格式)
，利用浏览器沙盒实现操作系统和用户隔离。这提高了应用的安全性，且不会产生重大开销。
确保代理工具的使用
LLM 应用开发的近期变化之一是公开工具，即 LLM 可以调用并使用响应的函数、应用或 API。例如，如果应用需要了解特定地点的天气，它可以调用天气 API，并使用结果制定适当的响应。
Python 代码执行是用于扩展 LLM 应用的强大工具。LLM 擅长编写 Python 代码，通过执行这些代码，他们可以执行更高级的工作流程，例如数据可视化。通过扩展 Python 函数调用，基于文本的 LLM 将能够为用户生成图像绘图。但是，很难动态分析 LLM 生成的 Python，以确保其满足预期规范，并且不会引入更广泛的应用程序安全风险。如果您正在执行由 LLM 生成的 Python 来扩展代理应用程序，本文就是为您准备的。
构建智能体工作流程
在最简单的代理工作流中，您的 LLM 可能会生成 Python，并最终传递给
eval
。例如，相当于
generate the python code to make a bar chart in plotly
的提示符将返回
import plotly.graph_objects as go\n\nfig = go.Figure(data=go.Bar(x=["A", "B", "C"], y=[10, 20, 15]))\nfig.update_layout(title="Bar Chart Example")
。您的代理会将其传递给
eval
，以生成如图 1 所示的图形。
图 1、用户、机器和 AI 模型之间最简单的代理工作流，用于使用 Python 生成图形
在第 1 步中，用户向应用程序提供提示。在第 2 步中，应用程序将为 LLM 提供任何其他提示上下文和增强功能。在第 3 步中，LLM 返回由工具调用代理执行的代码。在第 4 步中，在主机操作系统上执行该代码以生成图形，并在第 5 步中返回给用户。
请注意，第 4 步中的
eval
在服务器上执行，存在重大安全风险。为谨慎起见，应实施控制以降低服务器、应用程序和用户面临的风险。最简单的控制是在应用层，过滤和清理通常使用正则表达式和受限的 Python 运行时完成。但是，这些应用层缓解措施远远不够，通常可以绕过。
例如，正则表达式可能会尝试排除对
os
的调用，但错过
subprocess
或不确定是否有方法最终从各种依赖项内部访问这些函数。更可靠的解决方案可能是仅在微型虚拟机 (如
Firecracker
) 中执行 LLM 生成的 Python，但这需要大量资源和工程。
或者，可以考虑将执行转移到用户的浏览器中。浏览器使用沙盒将网页代码和脚本与用户的基础设施隔离开来。例如，浏览器沙盒可防止网页访问本地文件系统或未经授权查看网络摄像头。
在浏览器中使用 Python
Pyodide
是将 CPython 移植到 Wasm 中，以创建可在现有 JavaScript 虚拟机中使用的沙盒。这意味着我们可以执行 Python 客户端，以继承浏览器沙盒的所有安全优势。
如图 2 所示，通过设计应用以提供具有 Pyodide 运行时和 LLM 生成代码的 HTML，应用开发者可以将执行转移到用户的浏览器中，从而获得沙盒的安全性并防止任何跨用户污染。
图 2、经过修改的代理工作流，可使用 Pyodide 将图形生成从服务器转移到用户浏览器
此架构的主要区别在于，应用程序在第 4 步中将 HTML 返回给用户，而不是在应用服务器上执行该工具。当用户在浏览器中查看该 HTML 时，Pyodide 会在沙盒中执行 LLM 提供的 Python 并渲染可视化效果。
由于 LLM 生成的代码通常可以模板化为静态 HTML 文档，因此此类修改应尽可能减少提示策略的更改。例如，此函数会接收 LLM 提供的代码，并在发送代码以供使用
executeCode
执行之前进行准备。
window.runCode = async (LLMCode) => {
     try {
         console.log('Starting code execution process...');
         const pyodide = await initPyodide();
                
         const wrappedCode = [
             'import plotly.graph_objects as go',
             'import json',
             'try:',
             '    ' + LLMCode.replace(/\\n/g, '\\n    '),
             '    if "fig" in locals():',
             '        plotJson = fig.to_json()',
             '    else:',
             '        raise Exception("No \\'fig\\' variable found after code execution")',
             'except Exception as e:',
             '    print(f"Python execution error: {str(e)}")',
             '    raise'
         ].join('\\n');

         await executeCode(pyodide, wrappedCode);
       ...
如果您的代码具有 Python 依赖项（例如
Plotly
），您可以使用
micropip
在客户端 Javascript 中进行安装。Micropip 支持 PyPI 中的 Python wheel，包括许多具有 C 扩展的 wheel。
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
await micropip.install('plotly');
使用 Wasm 提高应用安全性
想象一下，在这种情况下，LLM 由于提示注入或错误而返回恶意代码。在最简单的代理工作流中，对
eval
的调用会损害应用程序，并可能影响主机操作系统和其他用户，如图 3 所示。
图 3、恶意代码在服务器上执行，会影响该服务及其用户
但是，应用 Wasm 流后，图 4 显示了两种可能的情况。首先，由于无法在狭义范围的 Pyodide 运行时 (即缺失依赖项) 中执行恶意 Python 代码，因此应用程序可能会引发错误。
Error: Traceback (most recent call last): File 
     "/lib/python311.zip/_pyodide/_base.py", line 499, in eval_code 
     .run(globals, locals) ^^^^^^^^^^^^^^^^^^^^ File 
     "/lib/python311.zip/_pyodide/_base.py", line 340, in run coroutine = 
     eval(self.code, globals, locals) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File 
     "<exec>", line 2, in <module> ModuleNotFoundError: No module named 
     'pymetasploit3'
其次，如果代码确实执行，则仅限于浏览器沙盒，这极大地限制了对最终用户设备的任何潜在影响。
图 4、恶意代码传播到 HTML 中，但由于浏览器沙盒的原因，无法在用户的设备上执行或影响极小
在这两种情况下，使用 Pyodide 可改善查询用户的安全控制，同时降低应用资源和相邻用户面临的风险。
开始使用
使用 WebAssembly 将 LLM 生成的 Python 沙箱化提供了一种便捷的方法，只需对现有提示和架构进行少量更改即可。它通过降低计算需求实现成本效益，并提供主机和用户隔离，同时提高服务及其用户的安全性。它比正则表达式或受限 Python 库更稳健，比容器或虚拟机更轻。
要开始使用 Wasm 提高代理工作流的应用程序安全性，请查看 GitHub 上的此
示例
。详细了解 AI
代理和代理工作流
。