AI Agent 的沙箱到底是什么:六种执行环境实测,一张能做与不能做的对照表
基于 Docker Desktop 4.77(Engine 29.5.3,cgroup v2)+ WSL Ubuntu 26.04 + 云主机 Docker 29.6.1 实测。容器级别统一使用
python:3.11-slim镜像;无隔离基线为 WSL 原生 Python 3.14.4。测试时间 2026-09-20,27 项探针 × 6 种执行环境,另有 Dify 1.17 平台沙箱 17 项逐项对照。完整探针脚本、每级完整命令与逐项原始数据见文末「复现材料」。
📖 摘要:同一份 27 项探针 × 6 种执行环境(无隔离基线
+ 五级隔离),实测 AI Agent
沙箱的能力边界。四条最意外的结果:容器默认身份是
root(能力集
CapEff=a80425fb,能写系统目录);只读根加可写 /tmp
加一条出网的路,agent
照样能给自己装包;资源超限不是抛异常,是被
SIGKILL(退出码 137),agent 自己的 try/except
抓不住;状态保不保留由容器生命周期决定,跟隔离级别无关。文中另附
Dify 1.17 两套沙箱的实测对照(代码节点沙箱实测:根目录只有 5 项、写 /tmp
报 PermissionError、DNS 与子进程报 operation not
permitted)、四个反直觉发现背后的坑,以及一张企业落地检查表——选隔离级别看任务要碰什么、要活多久、出错代价能不能回滚。
一、我们对沙箱的理解,是从一次报错开始的
我们给客户做一个故障诊断助手时,用 Dify 的代码节点写了段数据处理:把中间结果落到文件,下一个节点读回来。
跑的时候一直报错。查了半天才发现——代码节点跑在一个沙箱里,文件系统是只读的,连
/tmp
都不给写。后来我们在门户里记过一句:「沙箱禁写文件」。
但那句话只回答了一个问题:有些事在里面做不了。到底哪些做不了?换成别的隔离方式又会怎样?如果我自己给 agent 搭一个执行环境,该收多紧?——没人给过一张能对照的表。
于是我们决定自己跑一遍。同一份探针,从「完全不隔离」一路测到「云端远端执行」,看看每一步换来什么、又失去了什么。
这篇文章就是那张表,以及表背后我们真正学到的东西。
二、先划边界:沙箱不只是「安全设备」
要讲清一个东西,先排除它不是的东西。我们踩过的坑,一半来自把沙箱错认成别的东西。
它不只是传统意义上的安全设备。 提到沙箱,多数人想到的是防攻击、防逃逸——那是安全领域的传统沙箱,对手是恶意代码,目标是「攻不破」。agent 场景多了一层:你要防的是一个过度积极的执行者。它没想搞破坏,它只是想完成任务,于是顺手删了旧文件、把密钥写进日志、装了一堆没人要的包、跑了一个通宵的循环。所以在多租户或跑不可信代码的场景里,安全隔离依然重要;而在跑自己人或自己写的 agent 时,重点就变成了「出事可控、可回滚、可审计」。两种目标并存,只是权重随场景变——这是本文全部实测的出发点。
它不是「一个开关」。 没有「打开沙箱」这个动作。它是一组限制的集合:文件能写哪里、网络能连什么、能用多少资源、能活多久。每一项都可以单独调,也可以一项都不调。
它不是 Docker 的同义词。 容器是最常用的一种载体,不是唯一一种。进程级限制、微虚拟机(microVM,一种轻量虚拟化方案,隔离强度接近虚拟机、启动速度接近容器)、远端独立机器,都能当沙箱用——区别在隔离强度和代价(后面实测数据会看到这个取舍有多具体)。
它不是越强越好。 这是最反直觉的一条。隔离越强,agent 能干的活越少:写不了文件就跑不了数据处理,断了网就装不了依赖,只读根就存不下中间结果。选隔离级别不是选安全等级,是选任务边界——这是这篇文章想讲透的事。
三、本质:沙箱回答三个问题
把上面那组限制归一下类,沙箱其实只回答三个问题:
第一,它能碰什么? 文件系统(哪些路径可写)、网络(能不能出网、能不能连内网)、进程(能不能起子进程)。这一层决定 agent 的能力边界。
第二,它能用多少? CPU 时间、内存、进程数、单次执行时长。这一层决定 agent 的资源边界——也是失败模式最隐蔽的一层(后面会看到为什么)。
第三,它能活多久? 一次性任务用完即毁,还是长驻保留工作区?重启之后,上一次的中间结果还在不在?这一层决定 agent 的状态边界,也是「能不能连续干活」的关键。
三个问题合起来就是一句话:
沙箱 = agent 的「可做集」+ 越界的代价。
「可做集」是被设计出来的,「代价」是被测出来的——这就是为什么光看配置文档不够,得跑一遍。
四、实测:同一份探针,六种执行环境
方法与环境
我们写了一份 27 项的探针脚本,覆盖五组:文件系统(写 /tmp、写工作目录、写系统目录、读系统文件、列根目录)、网络(DNS、HTTPS 出网、裸 TCP 直连、连宿主机、连网桥网关)、执行(CPU 循环、内存分配、子进程、fork、sleep)、运行时(运行身份与能力集、环境变量、可用库、能否装包)、状态(同进程读写、跨运行残留)。每项独立捕获异常,记录原始返回值——不做「成功/失败」的二次判断,只记录当时发生了什么。
需要说明两处刻意的差异,避免读者横向误读:
- 容器级别(1、2、3、4、5)统一
python:3.11-slim;无隔离基线(0)跑在 WSL 原生环境(Python 3.14.4)。基线只用来回答「没有隔离时这些操作本来能不能做」,不用于比较运行时行为、可用库和装包结果——那些差异来自环境本身,不是隔离带来的。 - 云端两级(4、5)跑在公网服务器上(4 核 / 7.5GB 内存 / Docker 29.6.1),测的是「本地发指令、远端执行」这个形态,与其他级别的差异里混了网络往返的影响(探针自身耗时 15.51s,其中约 10s 是断网导致的网络探测超时)。
六种配置(完整命令,可照抄复现)
0 无隔离基线——WSL 原生直接跑:
cd /tmp/probe && python3 probe.py wsl-native1 普通容器——默认权限,不加任何限制:
docker run --rm -v "$P:/work" -w /work python:3.11-slim python /work/probe.py docker-default2 收紧容器——只读根 + 独立可写 tmpfs + 断网 + 去能力 + 限额 + 非 root:
docker run --rm \
--read-only \
--tmpfs /tmp:rw,size=64m \
--network none \
--cap-drop=ALL \
--security-opt no-new-privileges \
--memory=512m \
--pids-limit=128 \
--user 1000:1000 \
-v "$P/probe.py:/probe.py:ro" \
python:3.11-slim python /probe.py docker-hardened(--read-only
只让根文件系统只读;/tmp 要可写必须单独挂
tmpfs——这两行是配套的,少了第二行,写 /tmp 会拿到
Read-only file system。生产环境建议再加
noexec,nosuid。)
3 只读根 + 工作区卷——给一个可写工作区,其余锁死:
docker run --rm \
--read-only \
--tmpfs /tmp:rw,size=256m \
-v "$P/workspace:/work" -w /work \
-v "$P/probe.py:/probe.py:ro" \
--memory=1g --pids-limit=256 \
--cap-drop=ALL --security-opt no-new-privileges \
--user 1000:1000 \
python:3.11-slim python /probe.py docker-workspace4 云端一次性沙箱——在公网服务器上跑一次性容器,配置同第 2 级(含断网):
# 本地发起,远端执行;--rm 用完即毁
ssh $SERVER "docker run --rm --network none \
--cap-drop=ALL --security-opt no-new-privileges \
--memory=512m --pids-limit=128 --user 1000:1000 \
-v /tmp/probe/probe.py:/probe.py:ro \
python:3.11-slim python /probe.py cloud-ephemeral"5 云端长驻沙箱——同一个容器被调用两次,测状态:
ssh $SERVER "docker run -d --name probe-agent --cap-drop=ALL \
--memory=1g --pids-limit=256 \
-v /tmp/probe/probe.py:/probe.py:ro \
python:3.11-slim sleep infinity"
ssh $SERVER "docker exec probe-agent python /probe.py cloud-run1"
ssh $SERVER "docker exec probe-agent python /probe.py cloud-run2"(第 5 级没有显式指定 --user,所以它跑成了
root——这个「忘了写」恰恰成了本文第一个发现的证据。)
结果对照表
✅ 可以做 | ❌ 不能做 | ⚠️ 有条件(见备注)
| 能力项 | 0 无隔离 | 1 普通容器 | 2 收紧容器 | 3 只读根+工作区卷 | 4 云端一次性 | 5 云端长驻 |
|---|---|---|---|---|---|---|
| 写 /tmp | ✅ | ✅ | ✅(tmpfs) | ✅(tmpfs) | ✅(tmpfs) | ✅ |
| 写工作目录 | ✅ | ✅ | ❌ 只读 | ✅(工作卷) | ❌ | ✅ |
| 写系统目录 /usr/lib | ❌ 非 root | ✅ | ❌ 只读 | ❌ 只读 | ❌ | ✅ |
| 读 /etc/passwd | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 列根目录 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| DNS 解析公网 | ✅ | ✅ | ❌ 断网 | ✅ | ❌ 断网 | ✅ |
| HTTPS 请求出网 | ✅ | ✅ | ❌ 断网 | ✅ | ❌ 断网 | ✅ |
| 裸 TCP 直连公网 443 | ❌ 超时 | ❌ 超时 | ❌ 不可达 | ❌ 超时 | ❌ 不可达 | ❌ 超时 |
| 连宿主机(host.docker.internal:80) | ❌ 不存在 | ✅ | ❌ 断网 | ✅ | ❌ 不存在 | ❌ 不存在 |
| 连 docker 网桥网关(172.17.0.1:80) | ❌ | ✅ | ❌ 断网 | ✅ | ❌ 断网 | ✅ |
| 起子进程 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| fork 子进程 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 给自己装包 | ❌ 无 pip | ✅ | ❌ 断网 | ✅ | ❌ 断网 | ✅ |
| 运行身份 | uid 1000 | root | uid 1000 | uid 1000 | uid 1000 | root |
| 进程能力集 CapEff | — | a80425fb(默认 14 项) |
0(全空) |
0 |
0 |
a80425fb |
| 状态:一次性容器重建后 | 保留 | 不保留 | 不保留 | 卷内保留 | 不保留 | — |
| 状态:同一容器再次调用 | 保留 | 保留 | 保留 | 保留 | — | 保留(实测 1 行 → 2 行) |
关于「连宿主机」与「连内网」两行:它们不是一回事。测「连宿主机」用的是
Docker Desktop 提供的 host.docker.internal:80(Linux
上这个域名不存在,所以云端那两级直接解析失败);测「连网桥网关」用的是容器默认网桥的网关地址
172.17.0.1:80。两者通,只说明宿主机在网桥网段上对容器有监听,不等于容器能扫通内网其他服务——那还取决于自定义网络、路由策略和防火墙。企业环境里请按实际网络策略单独验证,不要照表推断。
四个反直觉的发现
一、容器的默认身份是 root,能力集也是满的。 1 级和 5
级都没显式指定用户,跑出来
uid=0,CapEff=00000000a80425fb(Docker 的默认
14 项能力),因此「写系统目录」这一项在普通容器里是 ✅。对比第 2
级(--cap-drop=ALL --user 1000)——CapEff=0000000000000000,一项能力都不剩。要非
root、要空能力集,必须自己写参数。这不是 Docker
的缺陷,是默认值的选择:容器默认面向「跑服务」,而服务场景里 root
是常态。
需要说明前提:这个结论成立的前提是镜像本身没有声明
USER(python:3.11-slim
没有),且宿主机未启用用户命名空间重映射。换成带 USER
指令的镜像,或开启 remap 的宿主机,结果会变——所以别记「容器就是
root」,要记「显式指定身份」。
二、只读根不等于「锁住了运行期安装」。
这是最有价值的一条。我们在第 3 级(只读根 + 可写 /tmp + 有网)里试了一句
pip install --target /tmp/libs six——装成功了,而且当场
import 可用(six 1.17.0)。反过来,第 2 级同样的命令(只读根 +
tmpfs,但断网)直接失败。结论是有条件的:只读根 + 可写临时区 +
一条出网的路 = agent
能在运行期给自己加工具;三个条件缺一个都装不上。想真正锁死,这几项得一起上。反过来也成立:如果任务确实需要装包(很多
agent
任务需要),那你就得接受「它在运行期会改变自己的行为」这个事实,并把「装了什么」记进审计。
三、资源超限不是异常,是死亡。 我们给第 2 级设了
--memory=512m,然后让探针分配 2GB:容器退出码
137——cgroup 判定内存超限后,内核直接发
SIGKILL,进程通常来不及做任何处理。两次对照都在限额内正常:分配 256MB
成功;不限内存时分配 2GB 也成功。这意味着一个现实问题:agent
自己的 try/except
兜不住这类失败。它看不到「内存不足」,它连同没落盘的日志一起消失。正确做法在外部:检查退出码、用
docker inspect 看 OOMKilled
字段、中间结果落盘、必要时重试。同理,--pids-limit 是防
fork 炸弹的那道闩,--cpus
配额则直接决定它跑多快——我们实测:同一段 3×10⁷ 次循环,无配额 1.22
秒,--cpus=0.5 后 2.76 秒,限流确实生效。
四、状态保不保留,由容器生命周期决定,跟隔离级别无关。
我们跑了三组对照:两个全新的一次性容器各跑一次,第二次运行时探针看到的历史记录都是
1 行(只有本次),互不影响;同一个长驻容器连续两次
exec,第二次看到 2
行(上一次的痕迹还在)。挂了卷的第 3
级则是「容器重建、卷内状态还在」。这条最容易忽略,因为它不出现在
docker run 的参数语义里——同一条
--read-only 命令,配 --rm 还是配长驻,agent
的「记忆」完全不同。
一个需要谨慎解读的现象
在全部六种环境里,「裸 TCP 直连公网 443
端口」都失败(超时或不可达),而「HTTPS
请求出网」在允许联网的级别都成功。本机 WSL
与云端表现一致,所以我们判断这是网络环境的代理层特性,不是沙箱本身的限制——测试目标是
1.1.1.1:443(裸 TCP)与
https://pypi.org/simple/(HTTPS)。它揭示的是一个现实:agent
沙箱的联网形态很少是「全通」或「全断」,更常见的是「看起来不通、实际走代理白名单」。所以测网络边界时,一定要测到应用层的真实请求,别只
ping 端口——这条在 Dify 的沙箱设计里会看到同样的影子。
五、平台案例:Dify 1.17 为什么配两套沙箱
实测到这里,一个自然的问题冒出来:真实产品怎么做?我们正好在本地跑着 Dify 1.17,于是把它拆开看。
先看两套沙箱的定义(来自 docker-compose 与容器实配):
dify-sandbox:0.2.15——工作流代码节点用的执行沙箱。docker inspect显示容器Config.User=root,未设内存限制(Memory=0)、未设 PID 限制、未 drop 任何能力;出网走独立的ssrf_proxy(Squid)容器。langgenius/dify-agent-local-sandbox:1.17.0——Agent 模式 shell 工作区用的沙箱。compose 注释写得很直白:它没有直连api服务的路由,只挂在agent_sandbox_network(agent_backend 通过它访问 5004 端口的 shellctl)和local_sandbox_proxy_network两个网络上;所有非 agent_backend 与 localhost 的流量都被强制走agent_ssrf_proxy(Squid,3128),白名单只放行 agent_backend 的/agent-stub/和 Dify API 的/files/*。它还有两个命名卷(home、workspace),状态是刻意保留的。
再说一个容易踩的坑:配置文件优先级高于环境变量。
我们读沙箱配置时发现 worker_timeout: 5,而官方
.env.example 与 compose 默认值都是
15(WORKER_TIMEOUT: ${SANDBOX_WORKER_TIMEOUT:-15}),容器内环境变量也确实是
WORKER_TIMEOUT=15。原因是
volumes/sandbox/conf/config.yaml
被挂载进容器,这份静态文件覆盖了环境变量——我们环境里它被改成了
5 秒(文件时间 2026-09-02)。所以:文章里的
worker_timeout: 5
是我们环境的实际值,不是官方默认值;你改超时的时候,先确认改的是
env 还是这份 conf,否则会出现「改了没生效」。
代码节点沙箱的实测行为(我们直接调它的沙箱接口,逐项单测 17 项):
- 列根目录只有 5
项:
python.so、opt、etc、usr、tmp——一个白名单式的极简文件系统,没有 /home、/proc、/sys、/dev。 - 写
/tmp:PermissionError: [Errno 13] Permission denied。 - 读
/etc/passwd:FileNotFoundError——这个文件在里面根本不存在。 - DNS 解析、HTTPS 请求、裸
socket、起子进程、
sleep:全部报operation not permitted(系统调用被 seccomp 策略拦下)。 - 运行身份
uid=10019(非 root),环境变量只有 3 个(其中两个是代理地址),DNS 与网络相关操作一律走代理。 - 可用库:
requests有,numpy、pandas、yaml、bs4没有——这是本镜像预装集合的差异,不代表「沙箱不支持这些库」(官方文档给出的代码节点依赖清单更宽,自建镜像时可按需预装)。
为什么是两套?
因为两个任务要的东西相反。代码节点是「给我一段代码,算出个结果」——不需要文件系统、不需要网络、不需要长期活着,所以把边界收到最紧,只留
Python 运行时的最小底盘,用极窄的 seccomp 白名单兜住系统调用。Agent
shell
工作区是「给我一个地方干活」——要装依赖、要跑命令、要保留中间产物,所以它必须有可写空间和受控的出网,隔离重点从「关死」转成「管住出口
+
留痕」,并配上认证令牌(SHELLCTL_AUTH_TOKEN,我们环境里是空值
= 未启用,生产必须设)。
顺着这条线,还有两点值得企业读者留意:
- 这组配置是「内部执行限制强、容器层默认弱」的典型组合:seccomp 与 chroot 收得很紧,但容器本身以 root 运行、无内存/PID 限制、未 drop 能力。内部限制挡的是代码,容器层挡的是逃逸——两层要分开评估,生产环境建议按本文第 2/3 级把容器层补齐。
- 官方注释自己标注了一处已知限制(compose 中 shellctl
网络段原文):
sandbox can access agent backend through this network, this is a known limitation——沙箱可以经该网络访问 agent_backend,且注释同时提示「agent 运行时虽遵守 HTTP(S)_PROXY,但通过 shellctl 通道仍可能执行任意代码」。也就是说,local_sandbox 的定位是「服务可信用户、提供功能与状态」,不是「比代码节点沙箱更硬的隔离」。多租户或跑不可信代码时,仍需额外的租户级隔离(独立容器/Pod、独立网络与凭证)。
这个取舍正好印证了前面那句话:隔离强度不是安全等级,是任务边界。同一个产品里,两个任务形态不同,答案就不同——而且差异大到必须上两套实现。
六、怎么选:一句判据 + 一张落地检查表
把实测数据收成一句能用的判据——按三个问题选隔离级别:任务要碰什么、要活多久、出错的代价能不能回滚。
| 你的任务要碰什么 | 建议级别 | 理由 |
|---|---|---|
| 只算数据、不碰文件不联网 | 收紧容器 + 断网 + 只读根 | 边界最紧,跑得最快,出事无损失 |
| 要写中间结果、读项目文件 | 只读根 + 工作区卷 + 非 root | 给一个可写口子,其余锁死 |
| 要装依赖、调外部 API | 只读根 + 可写 /tmp + 出网白名单 | 接受「运行期会变」,但记录装了什么 |
| 要长时间连续干活、保状态 | 长驻容器 + 工作区卷 | 状态留在卷上,容器本身可随时重建 |
| 要跑在别的机器上 | 远端沙箱 | 拿到的是更强的物理与网络边界;实际隔离强度仍取决于容器配置,不配
--user、不限额、不断网,远端也一样裸 |
企业落地检查表(前七项有本文实测支撑,后三项是通用做法,标注区分):
| 检查项 | 具体做法 | 依据 |
|---|---|---|
| 1 身份 | 显式 --user uid:gid,不依赖镜像默认;确认
CapEff 为空 |
实测:默认容器 root + 满能力集 |
| 2 文件系统 | 只读根 + --tmpfs /tmp:rw,noexec,nosuid,size=… +
工作卷(只读写给工作区) |
实测:缺 tmpfs 写 /tmp 直接报错;卷决定状态 |
| 3 网络 | 默认断网;要出网走代理白名单;内网按服务名 + 防火墙放开 | 实测:断网与出网是两套结果;代理层现象 |
| 4 资源 | --memory、--pids-limit、--cpus、单次执行超时、输出大小上限 |
实测:OOM 是 137 被杀;CPU 配额 1.22s→2.76s |
| 5 依赖 | 预装白名单;运行期安装需审计(记装了什么、装到哪) | 实测:只读根+tmpfs+有网仍可装包成功 |
| 6 状态 | 一次性 vs 长驻分清;中间结果落卷;任务结束可销毁 | 实测:一次性容器两次运行互不影响 |
| 7 超时与失败形态 | 外部检查退出码与 OOMKilled,不依赖
try/except;中间结果先落盘 |
实测:超限被 SIGKILL,进程无机会处理 |
| 8 凭证 | 不进环境变量明文;用 secret 挂载或平台注入;日志脱敏 | 通用做法(本文未实测) |
| 9 可观测 | 退出码、OOM、超时、网络拒绝、装包记录、文件写入路径留痕 | 通用做法(本文未实测) |
| 10 版本与威胁模型 | 镜像固定 tag 不用 latest;沙箱配置随应用版本化;可信用户 / 不可信代码 / 多租户三档分别定级 | 通用做法(本文未实测) |
七、五个坑
| 坑 | 现象 | 后果 | 对策 |
|---|---|---|---|
| 以为容器默认是安全默认 | 普通容器
uid=0、CapEff=a80425fb,能写系统目录 |
agent 可改运行环境 | 显式 --user + --cap-drop=ALL |
| 只读根就当锁住了 | 只读根 + 可写 /tmp + 有网 → 照样装上包 | 运行期行为被悄悄改变 | 只读根与断网同开;或记录安装行为 |
| 用 try/except 兜内存失败 | 超限时被 SIGKILL,退出码 137 | 任务无声消失,日志一起没了 | 外部查退出码与 OOMKilled;中间结果落盘 |
| 忽略容器生命周期 | 一次性容器两次运行互不影响 | 以为「记住了」,其实每次从零开始 | 要状态就用卷或长驻容器 |
| 改配置只改 env | 挂载的 config.yaml 覆盖环境变量 |
改了没生效,超时还是旧值 | 先确认改的是 env 还是挂在容器里的 conf 文件 |
八、写在最后
我们做这次实测的起点,是一句「沙箱禁写文件」的踩坑记录。跑完之后,我们手里多了一张表,也多了一个判断:沙箱的价值不在「关得多严」,而在「边界说得清」。
agent 不需要被关起来,它需要知道哪里能走、走错了会怎样、以及别人怎么知道它走过哪里。这三件事说清楚,一个能写文件、能联网、能装包的 agent,比一个什么都不能做的 agent 有用得多,也比一个什么都能做的 agent 安全得多。
沙箱不是把 agent 关起来,是提前告诉它哪里能走,以及走错的代价。
复现材料
探针脚本(probe.py,跨环境通用,Python
3.8+)。核心结构是「每项独立 try/except +
记录原始返回值」,摘录关键几项:
def rec(item, fn):
t0 = time.time()
try:
v = fn()
RESULTS.append({"item": item, "ok": True, "value": v, "ms": int((time.time()-t0)*1000)})
except Exception as e:
RESULTS.append({"item": item, "ok": False,
"error": "%s: %s" % (type(e).__name__, str(e)[:300])})
# 文件系统:写绝对路径 / 写相对路径 / 写系统目录 / 读系统文件
def a2():
open("/tmp/probe_write_abs.txt", "w").write("probe"); return "写入成功"
def a6():
open("/usr/lib/probe_write_test.txt", "w").write("probe"); return "写入成功"
# 网络:DNS / TCP / HTTP / 宿主机 / 网桥网关
def b1(): return socket.gethostbyname("pypi.org")
def b2():
s = socket.create_connection(("1.1.1.1", 443), timeout=4); s.close(); return "TCP 连通"
def b3():
import urllib.request
with urllib.request.urlopen("https://pypi.org/simple/", timeout=6) as r:
return "HTTP %s" % r.status
# 执行:CPU / 内存 / 子进程 / fork
def c1():
t = time.time(); x = 0
for i in range(10**7): x += i
return "1e7 循环 %.2fs" % (time.time() - t)
def c2():
b = bytearray(256*1024*1024); b[0] = 1; return "分配 256MB 成功"
# 身份与能力集
def d2():
return {"uid": os.getuid(), "gid": os.getgid()}
# 状态:同进程读写 + 跨运行残留
def e2():
p = "/tmp/probe_state.txt"
if os.path.exists(p):
return "上次运行留下 %d 行" % len(open(p).readlines())
return "无历史文件"环境变量项只统计键名个数与疑似密钥的键名,绝不输出值——探针本身也不该成为泄漏渠道。
完整 27 项结果。文中对照表呈现了 17 行(部分行为合并展示),其余各组的详细项(工作目录取值、列根目录返回值、磁盘可用空间、sys.path、环境变量键名样例、进程信息等)以 JSON 形式保留,结论与正文一致,无反向结果。
环境信息:Docker Desktop 4.77.0 / Engine
29.5.3(cgroup v2)/ WSL Ubuntu 26.04(内核
6.18.33.1-microsoft-standard-WSL2,Python 3.14.4)/ 云主机 Docker
29.6.1(4 核、7.5GB、公网)/ 镜像 python:3.11-slim(Docker
Hub,未固定 digest——如需严格复现请自行固定)/ 测试日期 2026-09-20。
Dify 侧环境:Dify 1.17.0 自托管(docker
compose),dify-sandbox:0.2.15、dify-agent-local-sandbox:1.17.0、ubuntu/squid:latest;代码节点探针经
POST http://sandbox:8194/v1/sandbox/run
逐项调用(每次只跑一项,因为沙箱内一处异常会让整段代码失败)。
常见问题
AI Agent 的沙箱和传统安全沙箱有什么区别?
对手不同。传统安全沙箱防恶意代码逃逸,目标是「攻不破」,隔离越强越好;agent 的沙箱多了一类对手——「执行者好心办坏事」:误删文件、把密钥写进日志、装没用的包、跑收不住的循环。所以在可信用户场景里,设计重点通常是「边界清晰 + 状态可管理 + 出错可回滚」;而在多租户或跑不可信代码时,安全隔离依然是硬要求。文中那张六环境对照表就是按这个视角列的,两边都能用。
隔离开得越强越好吗?
不是,代价很具体。我们实测:只读根加上断网,agent 连中间结果都存不下、依赖也装不了,等于把一个能干活的执行体变成了一台计算器。反过来,只给一个可写目录加一条出网的路,它就能在运行期给自己装工具——我们在只读根加可写 /tmp 的配置里实测装包成功。合理做法是按任务形态选:先问「它要碰什么、要活多久、出错能不能回滚」,再决定收哪一项。
搭 agent 沙箱该用 Docker 还是 Kubernetes?
取决于你要管几个 agent、几类任务。单机、少量任务,Docker 加本文第 2/3 级那套参数就够了,启动快、参数直观;要按任务动态起停、要资源配额与网络策略统一管理、要多租户隔离,就该上 Kubernetes——它的 Pod 级隔离、NetworkPolicy、ResourceQuota 正好覆盖本文检查表的第 2、3、4 项,且能做「每个任务一个 Pod,用完即毁」。判断标准不是技术先进,是**你的任务是否需要「按需创建、批量管理、按租户隔离」**这三件事。
多租户场景怎么升级?
单容器共享的方案撑不住多租户,这是 Dify 官方注释都承认的边界(沙箱可经 shellctl 网络访问 agent_backend)。升级路径分三步:先把「一个租户一个容器/Pod」作为起点,凭证按租户注入、网络按租户隔离;再把工作卷按租户拆分,禁止跨租户挂载;最后把「谁在什么时候装了什么包、写了哪些路径、连了哪些域名」记成可查询的审计记录。微虚拟机(microVM)适合再往上一档——隔离强度接近虚拟机,代价是启动与运维成本。
搭 agent 沙箱一定要用容器吗?
不一定,但容器是目前成本和可控性最平衡的一档。进程级限制更轻但边界更模糊;microVM 隔离更强但启动和运维成本高,适合多租户或高风险任务;远端独立机器适合「本地主机绝对不能被碰」的场景,代价是延迟和状态同步。我们这次实测的五级容器配置加一级远端,就是想说明这个梯度——选哪一档取决于任务,不取决于技术偏好。
相关文章:AI Agent 的 Harness 到底是什么:规则、约束,还是别的(agent 的约束体系怎么搭)|Dify 应用的可演进性:好的应用,改动是加法而不是重写(应用设计要留位置给未来)|为什么有了 AI 你反而更累了?因为你在错误的层级上作战(把力气花在系统上而不是结果上)