smolagents 安全代码执行完全指南:从本地 AST 沙箱到 E2B、Modal、Blaxel 与 Docker 远程隔离
【免费下载链接】smolagents🤗 smolagents: a barebones library for agents that think in code.项目地址: https://gitcode.com/gh_mirrors/smo/smolagents
smolagents 的核心设计是让 LLM 直接以 Python 代码的形式表达行动(Code Agent),因此"由 LLM 生成的代码在哪里执行、如何被执行"就成为了整个框架安全性的第一道防线。本文以 secure_code_execution 官方教程 为主体,结合仓库源码,系统讲解本地LocalPythonExecutor的逐节点 AST 解释防护机制,以及 Blaxel、E2B、Modal、Docker 四种远程沙箱的接入方式、多智能体适配与最佳实践。读完本文,你将掌握如何评估不同执行环境的威胁模型,并为自己的 Agent 应用选择并落地一套安全、可运维的代码执行方案。
提示:如果你对 Agent 框架还不熟悉,建议先阅读 intro to agents(智能体导论) 和 guided tour of smolagents(smolagents 快速导览),再回到本文。
为什么让 LLM 写代码比写 JSON 工具调用更安全(也更需要安全机制)
多项研究(如 Executable Code Actions Elicit Better LLM Agents 等)表明:让 LLM 以代码形式编写其行动(工具调用),效果优于行业通用的"把工具调用写成 JSON 形式的工具名 + 参数"的格式。原因在于代码语言本身就是为表达计算机操作而精心设计的:
- 可组合性(Composability):你可以像定义 Python 函数一样嵌套动作、复用一组动作,而 JSON 很难做到;
- 对象管理(Object management):
generate_image这类动作的输出对象在 JSON 中难以存储与传递; - 通用性(Generality):代码天然可以表达计算机能做的任何事情;
- 训练语料优势(Representation in LLM training corpus):大量高质量"动作代码"已经存在于 LLM 的训练语料中,直接生成代码可以充分利用这一点。
正是基于这个判断,smolagents 将重点放在 Code Agent(具体来说是 Python Agent)上,而"让 LLM 写代码"也意味着必须在 Python 解释器层面投入更高成本去构建安全执行环境——这正是本文要解决的核心问题。
本地执行的风险:默认行为与四种攻击向量
默认情况下,CodeAgent会在你自己的环境中运行 LLM 生成的代码(在 agents.py 源码 中,executor_type的默认值为"local")。这本身就带有固有风险,恶意代码可能通过以下四种途径进入你的系统:
- 纯 LLM 失误(Plain LLM error):LLM 远非完美,可能在试图帮忙时无意生成有害命令。该风险虽低,但已有 LLM 尝试执行潜在危险代码的实例被观察到;
- 供应链攻击(Supply chain attack):运行不受信任或被攻陷的 LLM,可能让系统暴露在有害代码生成之下。使用知名模型 + 安全推理基础设施时此风险极低,但理论上依然存在;
- 提示注入(Prompt injection):Agent 浏览网页时可能到达包含恶意指令的网站,从而把攻击注入 Agent 的上下文记忆;
- 公共 Agent 被滥用(Exploitation of publicly accessible agents):暴露给公众的 Agent 可能被恶意行为者利用,通过构造对抗性输入来滥用其执行能力。
一旦恶意代码被执行(无论有意还是无意),它可能损坏文件系统、滥用本地或云端资源、滥用 API 服务,甚至危及网络安全。在 智能体谱系(spectrum of agency) 中,Code Agent 把更高的"能动性"交给了 LLM,这天然与更高的风险相伴。
因此,安全的思路是:用递增的设置成本换取递进的安全等级——但要清醒地认识到,没有任何方案能做到 100% 安全。
第一层防护:LocalPythonExecutor 本地执行器
为了加上第一层安全措施,smolagents 并没有使用原生的 Python 解释器,而是从零重新构建了一个更安全的LocalPythonExecutor。其核心实现位于 src/smolagents/local_python_executor.py,整体思路是:把代码解析成抽象语法树(AST),然后逐条操作(operation by operation)执行,并在执行过程中始终强制执行一系列安全规则。
防护规则一:默认禁止导入,白名单机制
默认情况下,导入(import)是被禁止的,除非用户把模块显式加入授权列表。源码在 utils.py 中定义了默认的内置授权模块集合BASE_BUILTIN_MODULES:
BASE_BUILTIN_MODULES = [ "collections", "datetime", "itertools", "math", "queue", "random", "re", "stat", "statistics", "time", "unicodedata", ]LocalPythonExecutor.__init__(见 local_python_executor.py)会把用户传入的additional_authorized_imports与上述默认集合取并集,得到最终生效的authorized_imports,并且会在初始化时调用_check_authorized_imports_are_installed校验这些模块确实已安装,否则直接抛出InterpreterError。
子模块的访问默认同样被禁止,必须逐项授权。你也可以用通配符形式,例如传入numpy.*即可同时放行numpy及其全部子包(如numpy.random、numpy.a.b)。这一点由check_import_authorized函数实现:它先把授权列表构建成一棵"导入树"(build_import_tree),再逐级匹配待导入路径——一旦某级节点是*,即视为整棵子树放行(见 local_python_executor.py)。
需要特别警惕的是:一些看起来无害的包可能暴露危险的子模块。例如random包就能通过random._os触及潜在危险的os模块——这正是"子模块必须显式授权"这一规则存在的意义。
防护规则二:危险模块与危险函数黑名单
即使在白名单之外,解释器还维护了两张黑名单(见 local_python_executor.py):
DANGEROUS_MODULES = [ "builtins", "io", "multiprocessing", "os", "pathlib", "pty", "shutil", "socket", "subprocess", "sys", ] DANGEROUS_FUNCTIONS = [ "builtins.compile", "builtins.eval", "builtins.exec", "builtins.globals", "builtins.locals", "builtins.__import__", "os.popen", "os.system", "posix.system", ]任何对这类模块/函数的访问都会被check_safer_result拦截,并抛出InterpreterError。此外,nodunder_getattr会拒绝以__xxx__形式访问 dunder 属性,只有__init__、__str__、__repr__三个白名单方法(ALLOWED_DUNDER_METHODS)例外(见 local_python_executor.py 与 local_python_executor.py)。
防护规则三:操作数上限与执行超时
解释器对基本操作(elementary operations)的总数设有上限,防止死循环与资源膨胀。从源码可以看到三组关键常量(local_python_executor.py):
| 常量 | 默认值 | 作用 |
|---|---|---|
MAX_OPERATIONS | 10_000_000 | 单次代码执行中 AST 节点求值总数上限,evaluate_ast每执行一个节点都会计数并检查(见 local_python_executor.py) |
MAX_WHILE_ITERATIONS | 1_000_000 | while循环迭代次数上限,超限抛出InterpreterError(见 local_python_executor.py) |
MAX_EXECUTION_TIME_SECONDS | 30 | 单次代码执行的最大秒数,通过timeout装饰器(基于ThreadPoolExecutor)实现,设为None可禁用 |
DEFAULT_MAX_LEN_OUTPUT | 50_000 | print输出最大字符数,超出部分由truncate_content截断 |
防护规则四:未定义操作直接报错
任何没有在自定义解释器中显式定义的操作都会抛出错误——因为执行器并非依赖真实 Python 运行时,而是递归遍历 AST 节点,只对白名单内的节点类型(赋值、函数定义、类定义、if/for/while、lambda、推导式等)提供求值实现。对于print这类内置工具,解释器也做了替换(BASE_PYTHON_TOOLS中的print指向custom_print,输出被收集进_print_outputs而非直接打到终端)。
实战验证:亲手触发这些防护
官方教程给出了可直接运行的验证代码,你可以原样复制执行:
from smolagents.local_python_executor import LocalPythonExecutor # 设置自定义执行器,仅授权 "numpy" 包 custom_executor = LocalPythonExecutor(["numpy"]) # 用于美化打印错误的工具函数 def run_capture_exception(command: str): try: custom_executor(command) except Exception as e: print("ERROR:\n", e) # 未定义命令无法工作 harmful_command = "!echo Bad command" run_capture_exception(harmful_command) # >>> ERROR: invalid syntax (<unknown>, line 1) # 除非显式加入 additional_authorized_imports,否则 os 不会被导入 harmful_command = "import os; exit_code = os.system('echo Bad command')" run_capture_exception(harmful_command) # >>> ERROR: Code execution failed at line 'import os' due to: InterpreterError: Import of os is not allowed. Authorized imports are: ['statistics', 'numpy', 'itertools', 'time', 'queue', 'collections', 'math', 'random', 're', 'datetime', 'stat', 'unicodedata'] # 即使在已授权包里,危险子模块也不会被导入 harmful_command = "import random; random._os.system('echo Bad command')" run_capture_exception(harmful_command) # >>> ERROR: Code execution failed at line 'random._os.system('echo Bad command')' due to: InterpreterError: Forbidden access to module: os # 死循环会在 N 次操作后被中断 harmful_command = """ while True: pass """ run_capture_exception(harmful_command) # >>> ERROR: Code execution failed at line 'while True: pass' due to: InterpreterError: Maximum number of 1000000 iterations in While loop exceeded这些防护机制同样有对应的单元测试覆盖,例如 tests/test_local_python_executor.py 中验证了"不能覆盖工具名"(test_assignment_cannot_overwrite_tool)、各类语法节点求值、递归函数等行为,可作为你深入理解执行器行为的入口。
必须清醒认识的边界:本地沙箱并非万能
教程明确给出了警告:没有任何本地 Python 沙箱能做到完全安全。虽然LocalPythonExecutor相比标准解释器提供了显著的安全改进,但一个坚定的攻击者或经过微调的恶意 LLM 仍可能找到漏洞并伤害你的环境。例如:
- 如果你放行了
Pillow等图片处理包,LLM 可能生成代码创建成千上万个大型图片文件来填满硬盘; - 更高级的逃逸技术可能利用授权包内部的深层漏洞。
因此,想获得真正稳健的安全隔离,唯一的办法是把 LLM 生成的代码放到远程执行环境(如 E2B 或 Docker)中运行。使用可信推理供应商的知名 LLM 时,恶意攻击风险很低,但并非为零;对于高安全要求的应用或不太可信的模型,应当考虑远程执行沙箱。
沙箱化执行的两条路线
在 smolagents 中,沙箱化代码执行主要有两种方案,它们的安全属性与能力边界各不相同:
- 在沙箱中仅运行代码片段(Approach 1):只把 Agent 生成的 Python 代码片段放进沙箱执行,Agent 系统其余部分仍留在本地环境。通过
executor_type="blaxel"、executor_type="e2b"、executor_type="modal"或executor_type="docker"即可简单启用,但不支持多智能体(multi-agents),且仍需在本地环境与沙箱之间传递状态数据; - 在沙箱中运行整个 Agent 系统(Approach 2):把 Agent、模型、工具全部放进沙箱环境运行。隔离性更好,但需要更多手工配置,并可能要把敏感凭据(如 API Key)传给沙箱。
在源码层面,这两条路线分别对应 remote_executors.py 中的四个远程执行器类(E2BExecutor、DockerExecutor、ModalExecutor、BlaxelExecutor)与 agents.py 中create_python_executor的executor_type分发逻辑:
def create_python_executor(self) -> PythonExecutor: if self.executor_type not in {"local", "blaxel", "e2b", "modal", "docker"}: raise ValueError(f"Unsupported executor type: {self.executor_type}") if self.executor_type == "local": return LocalPythonExecutor(...) ... remote_executors = {"blaxel": BlaxelExecutor, "e2b": E2BExecutor, "docker": DockerExecutor, "modal": ModalExecutor} return remote_executorsself.executor_type所有远程执行器都继承自RemotePythonExecutor基类。它依赖SafeSerializer在本地与沙箱之间传输变量与最终答案,并暴露一个值得注意的参数allow_pickle(默认False,推荐保持关闭):开启后,无法安全 JSON 序列化的对象会回退到 pickle 序列化——pickle 反序列化可以执行任意代码,只有当你完全信任执行环境时才应开启(见 remote_executors.py)。
下面依次介绍四种远程沙箱的接入方式。使用方式与官方教程保持一致:先安装对应 extra 包,再在CodeAgent初始化时传executor_type参数。
Blaxel 沙箱:毫秒级冷启动的托管沙箱
安装
- 在 blaxel.ai 注册账号;
- 安装依赖:
pip install 'smolagents[blaxel]'快速开始
只需在 Agent 初始化时加上executor_type="blaxel":
from smolagents import InferenceClientModel, CodeAgent with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="blaxel") as agent: agent.run("Can you give me the 100th Fibonacci number?")使用
with语句把 Agent 作为上下文管理器使用,可以确保任务完成后 Blaxel 沙箱被立即清理;你也可以手动调用 Agent 的cleanup()方法达到同样效果。
工作流程:每次agent.run()开始时,Agent 状态被发送到 Blaxel 服务端;模型仍在本地环境被调用,但生成的代码会被送往沙箱执行,只有输出结果被返回。Blaxel 提供从休眠状态 25ms 内快速启动的虚拟机,并在空闲后缩回零资源(同时保留内存状态),非常适合需要快速、安全代码执行的 Agent 应用。
如果需要更强的隔离,可以把整个 Agent 托管到 Blaxel 远程运行,实现 Agent、模型、工具三者的完整沙箱化。
E2B 沙箱:云端代码解释器
安装
- 在 e2b.dev 注册账号;
- 安装依赖:
pip install 'smolagents[e2b]'快速开始
from smolagents import InferenceClientModel, CodeAgent with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="e2b") as agent: agent.run("Can you give me the 100th Fibonacci number?")同样建议使用with上下文管理器保证沙箱即时清理,或手动调用cleanup()。每次agent.run()开始时 Agent 状态被发送到 E2B 服务端,模型调用留在本地,代码在沙箱内执行并只返回输出。
E2B 下的多智能体:需要把 Agent 完全搬进沙箱
由于对托管 Agent(managed agent)的调用需要发起模型请求,而 smolagents 不会把密钥(secrets)传给远程沙箱,模型调用会缺少凭据——因此 Approach 1 暂不适用于更复杂的多智能体场景。要在 E2B 中运行多智能体,需要把 Agent 完全放进 E2B 运行:
from e2b_code_interpreter import Sandbox import os # 创建沙箱 sandbox = Sandbox() # 安装所需包 sandbox.commands.run("pip install smolagents") def run_code_raise_errors(sandbox, code: str, verbose: bool = False) -> str: execution = sandbox.run_code( code, envs={'HF_TOKEN': os.getenv('HF_TOKEN')} ) if execution.error: execution_logs = "\n".join([str(log) for log in execution.logs.stdout]) logs = execution_logs logs += execution.error.traceback raise ValueError(logs) return "\n".join([str(log) for log in execution.logs.stdout]) # 定义你的 Agent 应用 agent_code = """ import os from smolagents import CodeAgent, InferenceClientModel # 初始化子 Agent agent = CodeAgent( model=InferenceClientModel(token=os.getenv("HF_TOKEN"), provider="together"), tools=[], name="coder_agent", description="This agent takes care of your difficult algorithmic problems using code." ) manager_agent = CodeAgent( model=InferenceClientModel(token=os.getenv("HF_TOKEN"), provider="together"), tools=[], managed_agents=[agent], ) # 运行 Agent response = manager_agent.run("What's the 20th Fibonacci number?") print(response) """ # 在沙箱中运行 Agent 代码 execution_logs = run_code_raise_errors(sandbox, agent_code) print(execution_logs)这里的要点是:把HF_TOKEN通过envs参数注入沙箱,让沙箱内的模型调用具备凭据;而"managed agent"机制(即把agent作为managed_agents=[agent]传入manager_agent)正是 多智能体教程 中介绍的用法,此处只是把整个体系搬进了 E2B。
从源码看,E2BExecutor(remote_executors.py)通过e2b_code_interpreter.Sandbox执行代码,并会把final_answer工具替换为抛出FinalAnswerException的形式来判定任务结束、取回最终答案——这是远程执行器与本地执行器在机制上的关键差异。
Modal 沙箱:按需 Serverless 容器
安装
- 在 modal.com/signup 注册账号;
- 安装依赖:
pip install 'smolagents[modal]'快速开始
from smolagents import InferenceClientModel, CodeAgent with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="modal") as agent: agent.run("What is the 42th Fibonacci number?")with上下文管理器保证 Modal 沙箱在任务完成后被即时清理(源码中ModalExecutor.cleanup()调用sandbox.terminate()终止沙箱)。运行机制上,Agent 状态与InferenceClientModel生成的代码会被发送到 Modal 沙箱中安全执行。从源码看,ModalExecutor(remote_executors.py)会在沙箱内启动jupyter kernelgateway,并通过加密端口隧道(encrypted_ports)建立 WebSocket 连接来执行代码、回传结果。
Docker 沙箱:自托管容器隔离
安装
- 在你的系统上安装 Docker;
- 安装依赖:
pip install 'smolagents[docker]'快速开始
与 E2B 类似,只需在 Agent 初始化时加上executor_type="docker":
from smolagents import InferenceClientModel, CodeAgent with CodeAgent(model=InferenceClientModel(), tools=[], executor_type="docker") as agent: agent.run("Can you give me the 100th Fibonacci number?")with语句保证 Docker 容器在任务完成后立即清理(源码中DockerExecutor.cleanup()会依次执行container.stop()与container.remove())。
Docker 高级用法:自定义沙箱解释器
如果要在 Docker 中运行多智能体系统,需要在一个沙箱中配置自定义解释器。官方教程给出的方案分为两步。
第一步,编写 Dockerfile 构建带有限权限的沙箱镜像:
FROM python:3.10-bullseye # 安装构建依赖 RUN apt-get update && \ apt-get install -y --no-install-recommends \ build-essential \ python3-dev && \ pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir smolagents && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # 设置工作目录 WORKDIR /app # 以受限权限运行 USER nobody # 默认命令 CMD ["python", "-c", "print('Container ready')"]第二步,创建一个沙箱管理器来运行代码:
import docker import os from typing import Optional class DockerSandbox: def __init__(self): self.client = docker.from_env() self.container = None def create_container(self): try: image, build_logs = self.client.images.build( path=".", tag="agent-sandbox", rm=True, forcerm=True, buildargs={}, # decode=True ) except docker.errors.BuildError as e: print("Build error logs:") for log in e.build_log: if 'stream' in log: print(log['stream'].strip()) raise # 用安全约束创建容器并配置日志 self.container = self.client.containers.run( "agent-sandbox", command="tail -f /dev/null", # 保持容器运行 detach=True, tty=True, mem_limit="512m", cpu_quota=50000, pids_limit=100, security_opt=["no-new-privileges"], cap_drop=["ALL"], environment={ "HF_TOKEN": os.getenv("HF_TOKEN") }, ) def run_code(self, code: str) -> Optional[str]: if not self.container: self.create_container() # 在容器内执行代码 exec_result = self.container.exec_run( cmd=["python", "-c", code], user="nobody" ) # 收集全部输出 return exec_result.output.decode() if exec_result.output else None def cleanup(self): if self.container: try: self.container.stop() except docker.errors.NotFound: # 容器已被移除,这是预期情况 pass except Exception as e: print(f"Error during cleanup: {e}") finally: self.container = None # 清空引用 # 使用示例: sandbox = DockerSandbox() try: # 定义你的 Agent 代码 agent_code = """ import os from smolagents import CodeAgent, InferenceClientModel # 初始化 Agent agent = CodeAgent( model=InferenceClientModel(token=os.getenv("HF_TOKEN"), provider="together"), tools=[] ) # 运行 Agent response = agent.run("What's the 20th Fibonacci number?") print(response) """ # 在沙箱中运行代码 output = sandbox.run_code(agent_code) print(output) finally: sandbox.cleanup()这段示例体现了 Docker 沙箱的完整安全配置思路:
- 资源限制:
mem_limit="512m"限制内存、cpu_quota=50000限制 CPU、pids_limit=100限制进程数; - 权限最小化:
security_opt=["no-new-privileges"]禁止提权、cap_drop=["ALL"]丢弃全部 Linux capabilities、容器内以USER nobody和user="nobody"运行; - 凭据注入:通过
environment传入HF_TOKEN,避免把密钥写死在代码里; - 资源清理:
cleanup()负责停止容器,避免悬挂容器持续占用资源。
需要说明的是,仓库自带的DockerExecutor(remote_executors.py)默认使用python:3.12-bullseye+jupyter_kernel_gateway镜像,通过 Jupyter Kernel Gateway 与 WebSocket 执行代码,并且会在每次启动时生成随机鉴权令牌(KG_AUTH_TOKEN)——教程中给出的DockerSandbox是另一种"完全自建"的轻量方案,你可以按需二选一。仓库还提供了完整的可运行示例 examples/sandboxed_execution.py 供参考。
沙箱通用最佳实践
以下实践对 Blaxel、E2B、Modal、Docker 沙箱普遍适用:
- 资源管理:设置内存与 CPU 上限;实现执行超时;监控资源使用;
- 安全:以最小权限运行;禁用不必要的网络访问;使用环境变量存放密钥;
- 环境:保持依赖最小化;固定包版本;如果使用基础镜像,请定期更新;
- 清理:始终确保资源被正确清理,尤其是 Docker 容器,避免悬挂容器持续消耗资源。
两种沙箱路线的安全对比与选型
路线一:仅在沙箱中运行代码片段
- 优点:通过一个简单参数(
executor_type="blaxel"/"e2b"/"docker"/"modal")即可启用;无需把 API Key 传给沙箱;对本地环境的保护更好;配合 Blaxel 的休眠技术可实现快速执行(启动 <25ms); - 缺点:不支持多智能体(托管 Agent);仍需在本地环境与沙箱之间传输状态;仅限于代码执行这一环节。
路线二:在沙箱中运行整个 Agent 系统
- 优点:支持多智能体;整个 Agent 系统完全隔离;对复杂 Agent 架构更灵活;
- 缺点:需要更多手工配置;可能需要把敏感 API Key 传入沙箱;由于操作更复杂,延迟可能更高。
选型建议:对于架构相对简单的多数应用,路线一能在安全性与易用性之间取得良好平衡;对于需要完全隔离的复杂多智能体系统,路线二虽然配置成本更高,但能提供更好的安全保证。
总结
smolagents 的安全代码执行体系是分层递进的:本地场景由LocalPythonExecutor通过"AST 逐节点解释 + 导入白名单 + 危险模块/函数黑名单 + 操作数与超时上限"提供第一层防护,适合可信模型的日常使用;当风险容忍度较低、或涉及多智能体与高敏感业务时,应切换到 Blaxel、E2B、Modal、Docker 等远程沙箱,让 LLM 生成的代码在隔离环境中执行。无论选择哪一层,都请牢记:没有任何执行环境是绝对安全的,合理的威胁建模、最小权限原则与严格的资源清理,才是 Agent 应用长期安全运行的根本保障。
相关阅读:多智能体系统 | Agent 参考文档 | 模型接入指南
【免费下载链接】smolagents🤗 smolagents: a barebones library for agents that think in code.项目地址: https://gitcode.com/gh_mirrors/smo/smolagents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考