第4章 构建执行沙盒与安全边界
本章你将学到:
- 为什么不能让AI生成的代码直接在你的电脑上裸跑
- 用纯Python实现一个轻量级执行沙盒,不需要Docker
- 沙盒的五层安全机制:临时文件、超时终止、模块白名单、输出限制、网络隔离
- 如何测试沙盒本身是否可靠
- 将沙盒封装为Agent可调用的
execute_python工具本章你将产出 :一个经过安全测试的执行沙盒模块
sandbox.py,以及一个新增了execute_python工具的Agent全部章节 :收录在专栏《AI应用工程化实战教程》之【智能体工具使用实战】
4.1 一个必须面对的问题
第3章结束时,你的Agent能读文件了。它成功读取了 scores.csv,描述了数据的基本情况------有多少行、多少列、各列的名称。
但有一个问题它始终没解决:计算。
你让它分析成绩数据,它能告诉你"这份数据包含10名学生、4门课程",但当你追问"高数的平均分是多少"时,它要么凭"感觉"估算一个数字,要么老老实实告诉你"我无法执行计算,建议你使用pandas的mean()函数"。
你在第1章就见过这个场景了。现在Agent有了 read_file,但它依然缺一只真正能干活的"手"------执行代码的能力。
如果Agent能调用一个 execute_python 工具,它就可以自己写一段 pandas 代码,在数据上跑一遍,拿到精确的结果。这正是我们要添加的第二个工具。
但在动手之前,有一个必须正视的问题:AI生成的代码,你敢直接在自己的电脑上跑吗?
4.2 AI生成的代码不可信
2024年,一位开发者在让AI帮他写一段数据处理脚本时,AI生成的代码里包含了一行 os.remove("/")。幸亏他在执行前看了一眼。
2025年,另一个开发者让AI写一段测试脚本,AI引入了一个 while True 的死循环。他的电脑风扇狂转了五分钟,直到强制关机。
这不是AI"故意作恶"。而是语言模型不理解"安全"------它只是根据训练数据中的模式生成文本。它不知道 os.remove("/") 意味着什么,不知道死循环会让CPU占满,不知道有些库你不希望它调用。
当你的Agent学会执行代码之后,它会自己写代码、自己执行。你不是那个在编辑器里写完代码、检查一遍、再点运行的人。Agent是一个自动化的流程------它收到任务,生成代码,调用 execute_python,然后拿结果。
这意味着:你必须给Agent造一个受控的执行环境,让它在里面"随便搞",搞不出事。
这个受控的执行环境,就是沙盒。
4.3 轻量级沙盒的设计思路
提到"沙盒",很多人首先想到Docker容器------完整的文件系统隔离、网络隔离、资源限制。Docker确实是工业级沙盒的标准方案,但它有两个问题:
第一,学习曲线陡。 装Docker、理解镜像、配置容器、处理权限------这对第一次接触容器化的本科生来说,认知负荷远超本章的教学目标。我们的重点是"让Agent安全地执行代码",不是"学会Docker"。
第二,它太重了。 你只是想让Agent执行一段 pandas 计算,就启动一个完整的Linux容器?光是容器启动的时间可能比代码执行本身还长。
好在,对于我们的场景------执行有限功能的Python代码片段------我们完全可以用纯Python实现一个足够安全的轻量沙盒。
设计思路很简单:
| 安全目标 | 实现方式 |
|---|---|
| 防止删除/修改你的文件 | 代码写入临时文件,执行完自动删除 |
| 防止死循环占满CPU | 设置超时时间,超时强制终止 |
| 防止调用危险函数 | 限制可用的模块,只允许 pandas、numpy、math 等安全模块 |
| 防止输出撑爆内存 | 限制stdout和stderr的最大长度 |
| 防止访问网络 | 在subprocess中不提供网络访问(或通过防火墙规则) |
核心实现就靠一个Python标准库模块:subprocess。它能启动一个独立的子进程,在这个子进程里执行代码。子进程有自己的内存空间,出问题不会影响主进程。你可以设置超时,超时了就杀掉子进程。你可以捕获它的输出,限制输出大小。执行完毕,子进程销毁,一切都消失。
4.4 沙盒的完整实现
4.4.1 用Trae生成沙盒代码
在Trae的AI对话面板中输入:
在项目 data-analyst-agent 中创建一个 sandbox.py 模块。
这个模块实现一个 execute_python(code: str, timeout: int = 10) -> dict 函数,用于在受控环境中执行Python代码。
要求:
## 核心实现
- 使用 subprocess.run 在子进程中执行代码
- 代码写入 tempfile.NamedTemporaryFile(后缀.py),执行完自动删除
- 设置 timeout 参数,超时则强制终止子进程
- 捕获 stdout 和 stderr
- 返回统一格式的字典
## 返回格式
{
"success": True/False,
"stdout": "标准输出内容(截断到5000字符)",
"stderr": "标准错误输出(截断到5000字符)",
"returncode": 0或其他,
"error": "如果有超时或其他异常,在此说明"
}
## 安全机制
1. 临时文件隔离:代码写入临时文件,文件名随机,执行后自动删除
2. 超时终止:默认10秒,可在调用时自定义
3. 模块白名单:在执行前检查代码中的 import 语句。只允许以下模块:
- pandas, numpy, math, statistics, json, csv, collections, itertools, datetime, re, string, decimal, fractions
4. 输出限制:stdout 和 stderr 各截断到5000字符
5. 禁止危险关键字:代码中不得包含 os.system, subprocess, eval, exec, __import__, open, shutil, sys., os., pathlib 等危险调用
## 代码风格
- 清晰的注释,说明每一层安全机制
- 类型注解
- docstring 说明用法和限制
4.4.2 审查生成的代码
Trae生成 sandbox.py 后,打开它,对照以下清单逐项检查:
核心实现检查:
- 是否使用了
tempfile.NamedTemporaryFile? - 文件后缀是否为
.py? - 是否设置了
delete=False(因为需要在另一个进程中读取)? - 执行后是否用
os.unlink()手动删除了临时文件? - 是否使用了
subprocess.run并传入了timeout? - 返回格式是否与要求一致?
安全机制检查:
- 是否定义了白名单模块列表?
- 是否在代码中扫描了
import语句并与白名单对比? - 是否定义了危险关键字列表并扫描了代码?
- stdout和stderr是否做了截断?
- 异常情况(超时、代码中有危险操作)是否返回了清晰的错误信息?
如果发现缺失或实现不当,在Trae对话面板中要求修正。例如:
sandbox.py 中,你只检查了 "import os" 这种形式,但 "from os import system" 这种形式不会被检测到。请同时检查两种 import 形式。
或者:
sandbox.py 中,白名单模块检查后如果发现非法模块,应该返回 "success": false 和具体的错误信息,而不是抛出异常。
以下是沙盒核心逻辑的参考实现(供对照,不是让你手写):
python
import subprocess
import tempfile
import os
# 模块白名单
ALLOWED_MODULES = {
'pandas', 'numpy', 'math', 'statistics', 'json', 'csv',
'collections', 'itertools', 'datetime', 're', 'string',
'decimal', 'fractions'
}
# 危险关键字
DANGEROUS_KEYWORDS = [
'os.system', 'subprocess', 'eval(', 'exec(', '__import__',
'shutil', 'sys.', 'os.', 'pathlib', 'requests', 'urllib',
'socket', 'ftp', 'http'
]
def check_code_safety(code: str) -> tuple:
"""检查代码是否安全。返回 (is_safe, error_message)"""
# 检查危险关键字
for keyword in DANGEROUS_KEYWORDS:
if keyword in code:
return False, f"代码包含危险操作:{keyword}"
# 检查 import 语句(简单字符串扫描)
import_lines = [l for l in code.split('\n')
if l.strip().startswith('import ') or l.strip().startswith('from ')]
for line in import_lines:
# 提取模块名
parts = line.strip().split()
if parts[0] == 'import':
modules = [p.split('.')[0] for p in parts[1:] if p != 'as']
else: # from X import Y
modules = [parts[1].split('.')[0]]
for mod in modules:
if mod not in ALLOWED_MODULES:
return False, f"不允许导入模块:{mod}(白名单之外的模块)"
return True, None
4.5 安全机制逐层拆解
现在花一点时间,理解沙盒每一层安全机制的设计逻辑。这不仅是审查代码,更是培养设计安全系统的工程思维。
第一层:临时文件隔离
每次执行代码,都创建一个随机的临时文件(如 /tmp/tmpabc123.py),把代码写进去,执行完立刻删除。这样做的好处是:即使代码里写入了某些文件,也只会影响临时文件(已经被删了),不会污染你的项目目录。每次执行都是"全新的",上一次执行的残留不会影响下一次。
第二层:超时终止
subprocess.run(timeout=10) 意味着如果代码在10秒内没有执行完,子进程会被强制杀死。这防止了死循环永远运行下去。10秒是一个合理的默认值------大多数数据分析操作(计算平均值、筛选数据)都在1秒内完成。如果你的数据特别大,可以在调用时传入更大的 timeout。
第三层:模块白名单
这是最核心的一层。代码在执行前会被扫描------它导入了哪些模块?如果发现不在白名单里的模块,直接拒绝执行。
白名单的设计原则是最小权限 :只给Agent完成任务必需的模块,不给多余的。数据分析需要 pandas 和 numpy,数学计算需要 math,文件解析需要 csv 和 json。os 和 sys 不需要------Agent不应该有能力操作操作系统。requests 不需要------Agent不应该能发起网络请求。
第四层:输出限制
如果Agent写了一段代码,生成了10万行输出,你的终端和内存都会撑爆。所以 stdout 和 stderr 各截断到5000字符------足够看到计算结果和错误信息,但不会造成资源问题。
第五层:危险关键字扫描
模块白名单能拦住 import os,但如果Agent不写 import 而是直接调用内置的危险函数呢?危险关键字扫描就是为此设计的------它直接在代码文本中搜索 os.system、subprocess、eval、exec 等字符串。只要出现,就拒绝执行。
当然,这种基于关键字扫描的方式不是100%安全的(比如有人可以把危险代码混淆),但对于我们的教学场景已经足够。工业级沙盒会使用更复杂的技术(如seccomp、ptrace),但那超出了本书的范围。
4.6 【避坑指南】AI生成代码的五种危险操作
当Agent开始自己写代码并自动执行时,你可能会遇到以下情况。提前了解,能帮你快速诊断问题。
| 危险操作 | 表现 | 沙盒防御层 | 如果沙盒没有拦住 |
|---|---|---|---|
| 死循环 | while True: pass |
超时终止(10秒后强制杀掉) | CPU风扇狂转,Agent卡住不动 |
| 文件删除 | os.remove("important.txt") |
模块白名单(os不允许)+ 临时文件隔离 | 项目文件被删除 |
| 网络请求 | requests.get("恶意网址") |
模块白名单(requests不允许)+ 危险关键字 | 数据泄露 |
| 系统命令 | os.system("rm -rf /") |
危险关键字扫描 + 模块白名单 | 灾难性后果 |
| 内存炸弹 | [0] * 10**12 或无限递归 |
超时终止(内存耗尽前进程被杀死) | 系统卡死 |
如果Agent生成的代码被沙盒拦截了怎么办?
这本身就是一个很好的学习机会。在终端日志里,你会看到Agent调用了 execute_python,但沙盒返回了 {"success": false, "error": "代码包含危险操作:os.system"}。Agent拿到这个结果后,通常会尝试修改代码------去掉危险操作,换一种安全的方式。这个过程就是"Agent在沙盒的约束下学会安全编程"。
如果你想让Agent更好地理解沙盒的限制,在它的系统提示词中加入:
你可以使用 execute_python 工具来执行Python代码。但请注意:
- 只允许使用以下模块:pandas, numpy, math, statistics, json, csv, collections, itertools, datetime, re, string, decimal, fractions
- 不要使用 os、sys、subprocess、requests 等模块
- 代码必须在10秒内完成执行
- 如果沙盒拒绝了你的代码,请检查是否有不允许的导入或操作,修改后重试
4.7 测试沙盒本身是否可靠
沙盒是给你Agent用的安全基础设施。在把它交给Agent之前,你需要先验证它自己是否可靠。
创建 test_sandbox.py:
python
"""测试沙盒的安全性和功能"""
from sandbox import execute_python
# 测试1:正常代码
print("测试1:正常计算")
result = execute_python("print(sum([1,2,3,4,5]))")
print(f"结果: {result}\n")
# 测试2:死循环(应该被超时终止)
print("测试2:死循环")
result = execute_python("while True: pass", timeout=3)
print(f"结果: {result}\n")
# 测试3:尝试导入 os(应该被白名单拦截)
print("测试3:尝试导入os")
result = execute_python("import os\nprint(os.getcwd())")
print(f"结果: {result}\n")
# 测试4:尝试执行系统命令(应该被关键字扫描拦截)
print("测试4:尝试执行系统命令")
result = execute_python("import os\nos.system('ls')")
print(f"结果: {result}\n")
# 测试5:超长输出(应该被截断)
print("测试5:超长输出")
result = execute_python("for i in range(10000): print(f'Line {i}')")
print(f"stdout长度: {len(result.get('stdout', ''))}\n")
# 测试6:pandas 操作(应该正常执行)
print("测试6:pandas操作")
result = execute_python("""
import pandas as pd
data = {'name': ['Alice', 'Bob'], 'score': [90, 85]}
df = pd.DataFrame(data)
print(df.describe())
""")
print(f"结果: {result}")
运行测试:
bash
python test_sandbox.py
期望结果:
- 测试1:返回
success: true,stdout为15 - 测试2:返回
success: false,error中包含"超时" - 测试3:返回
success: false,error中包含"不允许导入模块:os" - 测试4:返回
success: false(要么被模块白名单拦,要么被关键字扫描拦) - 测试5:返回
success: true,但stdout被截断到5000字符 - 测试6:返回
success: true,stdout中包含pandas的描述统计
如果所有测试都通过,你的沙盒可以交付给Agent使用了。
4.8 将沙盒封装为Agent工具
沙盒就绪,现在把它添加到Agent的工具箱里。
4.8.1 设计 execute_python 工具描述
在Agent看来,execute_python 是一个可以执行Python代码的工具。它需要知道:这个工具能做什么、什么时候该用它、有什么限制。
打开 agent.py,在Trae对话面板中输入:
请修改 agent.py,增加第二个工具 execute_python。
工具描述:
- 名称:execute_python
- 用途:在安全沙盒中执行Python代码,返回执行结果。适用于需要计算、数据处理、统计分析的任务。
- 限制:只允许使用 pandas, numpy, math, statistics, json, csv, collections, itertools, datetime, re, string, decimal, fractions 模块。代码需在10秒内完成。不支持网络访问、文件系统操作(除了沙盒内部的临时文件)。
- 参数:
- code(必填,字符串):要执行的Python代码
- timeout(可选,整数,默认10):超时时间(秒)
实现:
- 导入 sandbox 模块中的 execute_python 函数
- 在 TOOL_MAP 中添加 "execute_python"
- 在 tools 列表中添加新工具的定义
- 在系统提示词中补充:当需要计算、统计、数据处理时,使用 execute_python 工具
4.8.2 审查新增的工具代码
确认以下几点:
- 工具定义中的
name是否与TOOL_MAP的键一致? -
description中是否说明了限制(白名单模块、超时)? - 系统提示词中是否更新了
execute_python的使用指引? -
sandbox.execute_python的导入路径是否正确?
4.9 集成测试:读取数据并执行计算
现在 agent.py 有了两个工具:read_file 和 execute_python。
运行以下测试,观察Agent如何组合使用它们:
bash
python agent.py
修改 agent.py 底部的测试请求为:
python
run_agent("请读取 scores.csv,然后计算高数的平均分。")
观察终端日志。你期望看到的流程是:
第1轮:Agent调用 read_file("scores.csv")
第2轮:Agent调用 execute_python("import pandas as pd\ndf = pd.read_csv('scores.csv')\nprint(df['高数'].mean())")
第3轮:Agent基于两次工具调用的结果,生成最终分析
如果Agent在第2轮写入的代码中尝试 df = pd.read_csv('scores.csv'),这是可以的------pandas 在白名单中,read_csv 是pandas的正常功能。沙盒不会拦截它。
但注意:沙盒中的代码执行环境和Agent的主进程是隔离的。 在沙盒里 pd.read_csv('scores.csv') 能读到文件,是因为 scores.csv 在项目根目录,而沙盒子进程的工作目录继承了主进程的目录。如果你想让沙盒更安全,可以在 sandbox.py 中切换工作目录到一个临时文件夹------但那样Agent就必须在代码中处理文件路径问题。当前方案在安全性和可用性之间取了一个平衡。
4.10 本章小结
- AI生成的代码不可信------语言模型不理解安全,它生成的代码必须放在受控环境中执行。
- 轻量级沙盒不需要Docker ------用
subprocess+tempfile+ 超时 + 模块白名单,纯Python即可实现足够安全的执行环境。 - 五层安全机制:临时文件隔离、超时终止、模块白名单、输出限制、危险关键字扫描。每一层解决一类安全威胁。
- 沙盒本身必须先测试------在交给Agent使用之前,用各种攻击场景验证沙盒的可靠性。
- Agent现在有了两只手 :
read_file读取数据,execute_python执行计算。组合使用,它就能完成第1章那个"分析成绩单"的任务。
下一章,我们将为Agent添加第三个工具------write_file,并构建一个完整的 ToolManager 类来管理工具箱。Agent将能够读取数据、执行计算、保存分析报告------一个完整的数据分析闭环。
课后练习
- 运行
test_sandbox.py中的全部六个测试,确认沙盒正常工作。如果某个测试失败了,分析是哪一层安全机制没有生效。 - 修改
sandbox.py,在白名单中增加matplotlib.pyplot(但matplotlib可能依赖os等模块------这会触发什么安全拦截?你如何解决这个矛盾?) - 运行集成测试------让Agent读取
scores.csv并计算高数平均分。如果Agent在代码中写了import os然后被沙盒拒绝,观察Agent的后续行为------它会怎么修改代码? - (进阶)在
sandbox.py中增加第六层安全机制:限制子进程的内存使用。提示:在Linux/macOS上可以使用resource模块,在Windows上可以研究job objects。