最近把 Agent 接到云沙箱上以后,我反复碰到同一个别扭的地方。
命令已经能跑了。stdout 也能流回来了。沙箱也能按会话存活一段时间了。
但真正让工作流卡住的,往往不是下一条 ls,而是:
文件怎么进去,改完以后又怎么出来。
这个问题看起来很土。
土到很多人会下意识用三种办法糊过去:先打一个 zip 丢进去;再不行就 base64 塞进命令;再不行就让 Agent 自己 curl 一张预签名链接。
这三种办法都能“看起来能用”。
也正是因为看起来能用,文件通道才最容易被当成附件功能,而不是 Runtime 的一部分。
上一篇文章里我写过:对 Agent 来说,云沙箱更像一台临时电脑,而不是一次函数调用。那篇文章把问题停在 Runtime。这篇文章想把 Runtime 再往里拆一层:
如果沙箱是 Agent 的临时电脑,那么文件通道就是这块电脑上的磁盘接口。
磁盘接口一旦设计错了,Agent 并不会变笨。
它只会开始用更危险、更贵、更不稳定的方式自己“发明”磁盘。
一. 为什么执行通道不够
1. Agent 真正碰的不是进程,是树
一个 Coding Agent 接到任务后,脑子里通常不是:
运行命令 A
运行命令 B
运行命令 C
而是:
/workspace
├── README.md
├── src/
│ └── main.py
├── tests/
└── output/
它要做的事情更接近:
命令只是手段。
被反复读写、反复观察的,是 Workspace 这棵树。
所以,如果一个沙箱产品只提供:
创建环境
执行命令
销毁环境
它其实只覆盖了 Agent 工作流的中间那一截。
两端的文件,仍然是调用方自己需要处理的负担。
2. 整包进、整包出,适合开始和结束,不适合中间
云沙箱里最经典的文件模型,是归档:
input_workspace_url
↓
解压到 /workspace
↓
执行命令
↓
把 /workspace 再打成包
↓
output_workspace_url
这个模型非常干净。
一次性任务尤其适合它。因为一次性任务的生命周期就是:
准备 → 执行 → 销毁
输入在开始前就应该准备好,输出在结束时一次性拿走。中间不需要随机插入一个 PDF,也不需要中途把某个日志文件单独抽出来。
会话型沙箱不一样。
会话会活几分钟到几小时。Agent 可能:
- 先创建空环境
- 再塞进用户刚刚上传的简历
- 跑完一轮解析
- 再补一份补充材料
- 改一个中间 JSON
- 把某一页预览图拿出来给人看
- 最后才销毁并导出整棵树
如果你强迫它每次都重新打包、重新创建、重新 hydrate,表面上还是“有文件能力”,实际上你把会话退回成了一次性任务。
状态没了。
已安装的依赖没了。
正在听的端口没了。
Agent 刚才建立的工作记忆,也一起没了。
所以归档通道解决的是:
任务边界上的世界迁移。
它不解决:
世界还活着的时候,怎么处理其中的一个文件。
3. 用命令通道传文件,是最常见的“假文件接口”
没有单文件 API 时,工程师会很快发明这一套:
把文件变成文本
↓
塞进命令的 stdin 或参数
↓
在沙箱里重定向到磁盘
↓
需要读回来时再 cat / base64
短文本勉强能活。
一到真实文件,问题会成串出现:
- 二进制会坏
- 特殊字节会和 shell 解析打架
- 大文件会打满命令输出上限
- 参数里可能出现密钥、签名、路径
- 写文件和正在跑的命令抢同一个执行槽
- Agent 会把“传文件”理解成又一次工具调用,于是开始自己拼命令
更麻烦的是安全语义。
命令通道的本意是:在隔离环境里执行用户意图。
文件通道的本意是:在隔离环境的指定位置放置或取出字节。
两件事混在一起后,平台很难再回答这些基本问题:
- 这次调用到底有没有执行任意命令?
- 文件最终落在了哪?
- 有没有经过路径校验?
- 失败时有没有留下半截文件?
- 输出截断到底是命令失败,还是文件传输失败?
一旦这几个问题说不清,文件就不再是一等能力,而只是命令的副作用。
Agent Runtime 最怕的就是副作用。
因为 Agent 会把副作用当成可组合的工具,继续往上叠。
4. 让沙箱自己去下载,会把文件问题变成网络问题
还有一种更“聪明”的办法:
文件已经在对象存储里了,给沙箱一个 URL,让它自己拉。
对调用方很省事。
对平台很危险。
因为这一步会立刻碰到另一类经典问题:
- URL 指向哪里?
- DNS 解析到了哪些地址?
- 重定向会不会把请求带进内网?
- 鉴权头会不会跟着跳?
- 失败时临时文件清不清理?
- 沙箱出网策略能不能覆盖这次下载?
也就是说,文件传输一旦改成“沙箱主动取”,安全边界就从路径穿越,变成了 SSRF、出网策略和凭证泄漏。
这不是不能做。很多平台的 input_workspace_url 就是这条路,而且必须做得很严。
但它仍然不适合充当会话中途的随机文件口。
原因很具体:
- 调用方先要把文件变成一个可下载地址,等于又引入一轮对象存储协议。
- 每次中途补文件都要走“生成地址 → 沙箱拉取 → 解压或落盘”,延迟和失败模式都更重。
- Agent 如果被允许自己拼 URL,平台很难区分“用户给的输入”和“模型临时编出来的地址”。
所以我会把下载 URL 看成:
受控的文件导入。
而不是:
活着的磁盘 API。
二. 文件通道到底在解决什么
如果把云沙箱看成一台临时电脑,文件相关的能力其实有三层,不该混成一个按钮。
三层的职责完全不同。
| 通道 | 解决的问题 | 典型时机 | 不该拿来做的事 |
|---|---|---|---|
| 导入归档 | 把一个已有世界搬进去 | 创建时 | 中途塞一个小文件 |
| 单文件读写 | 在活着的世界里放/取单个对象 | 会话中途 | 当对象存储用 |
| 导出归档 | 把世界打包带走并释放占用 | 销毁/到期 | 每次命令后都打一次包 |
很多人觉得“有文件功能了”。
真正要问的是:
你提供的是哪一层,以及你有没有把三层的失败模式分开。
1. 单文件通道的产品形状,其实非常克制
一个适合 Agent 的会话文件接口,通常长这样:
创建会话
↓
按路径写入单个文件
↓
执行命令
↓
再读回某个结果文件
↓
必要时继续写、继续跑
↓
销毁会话并导出整棵 /workspace
注意几个细节点。
第一,它是 单文件,不是网盘。
一次请求对应一个路径、一份字节。不在这个接口上做列表、批量、秒传、断点续传、目录同步。那些是对象存储或工作区协议的事。
第二,它只打在 还活着的会话 上。
一次性任务没有“中途”。文件应该在创建前准备好,结束后从归档取。把单文件读写挂到一次性执行上,会把两种生命周期揉在一起。
第三,路径是工作区相对路径,不是沙箱任意路径。
resume.pdf 的意思是 /workspace/resume.pdf。
不是 /etc/passwd,不是 /home/sandbox/.ssh,也不是 ../../proc。
第四,读成功时返回的是文件字节,不是又包一层业务 JSON。
写成功可以回一个很薄的条目信息:绝对路径、名字、类型。读成功如果还套 envelope,浏览器、SDK、Agent 工具都会很难把它当文件用。
这些克制看起来像“功能少”。
但对 Agent Runtime 来说,少即是边界。
2. 控制面和数据面为什么不该拆到沙箱域名上
行业里有一种很直观的做法:
每个沙箱有自己的地址,文件请求直接打到沙箱里的小服务。
优点是像一台真电脑。
缺点也非常像一台真电脑:
- 每个沙箱都要暴露一个文件代理
- 鉴权容易变成 URL 上的短时签名
- 调用方要同时记住 API 域名和沙箱域名
- 沙箱网络策略、证书、生命周期都要为这个小服务让路
另一种做法是反过来:
调用方
↓
统一的 API 入口(Token / 网关身份)
↓
调度到持有该会话的执行节点
↓
在该会话的 /workspace 上落盘或读盘
文件请求仍然是会话的一部分,而不是沙箱自己开的后门。
对调用方,这意味着:
- 鉴权模型和创建会话、执行命令是同一套
- 路径里的会话 ID 就是业务主键,不是执行节点内部的实例号
- 不需要理解沙箱内部有没有文件代理进程
对平台,这意味着:
- 文件通道可以复用租户隔离、会话存在性检查、忙闲状态
- 不把数据面权限下放到“谁拿到沙箱地址谁就能读盘”
- 执行节点可以升级文件能力,而不必在用户环境里再跑一个常驻文件服务
我越来越倾向于后一种。
不是因为它更“云原生”,而是因为 Agent 已经够容易把环境搞乱了。文件通道如果再变成沙箱内的另一个服务,攻击面和心智负担会一起加大。
三. 路径、大小、并发:文件通道的三条硬边界
文件接口一旦公开,攻击者和 Agent 都会立刻开始试探边界。
而且 Agent 的试探往往不是恶意的。
它只是在完成任务。
所以这三条边界必须写进模型,而不是写进“请调用方注意”。
1. 路径边界:只开放工作区,而且必须在解析后仍然如此
沙箱里通常有好几块可写面,职责并不一样:
/workspace 任务世界,导入导出都围绕它
/home/sandbox 进程的 HOME,配置和缓存
/tmp 临时文件,有容量上限
rootfs 只读
单文件通道如果把这三块可写面全打开,表面上更像 Linux。
实际上会把平台推进两个坑:
- Agent 把依赖、密钥、缓存写进 HOME,销毁时你却只导出
/workspace - 路径一旦允许绝对路径,符号链接和
..会迅速变成逃逸题
更稳妥的第一期模型是:
HTTP/OpenAPI/MCP 的文件口,只服务于 /workspace。
相对路径拼到工作区根。
绝对路径必须规范化后仍落在工作区里。
.. 直接拒绝。
符号链接不能成为逃出工作区的跳板。
目录不能当文件读。
不存在的路径不能读。
覆盖已有文件可以允许,因为 Agent 改文件就是这个动作;但覆盖仍然必须落在同一个安全根下面。
父目录可以隐式创建。
这对 Agent 很重要。否则它每次写 src/app/main.py 之前,都得先发明一轮 mkdir -p。
那又会把文件通道打回命令通道。
有一个很容易忽略的点:
对外错误里不要回显完整内部路径、查询参数或存储地址。
调用方只需要知道:路径不合法、文件不存在、太大、会话忙。不需要知道执行节点把文件落在了哪台机器的哪个目录。
2. 大小边界:传输上限不是磁盘上限
这是我见过最容易写错文档的地方。
至少有三种完全不同的“大”:
1. 单次 API 读写允许传输多少字节
2. 沙箱里用命令生成的单个文件可以多大
3. 整棵 /workspace 最终导出时可以多大
三者不该用同一个数字,也不该用同一个错误码糊弄过去。
一个比较清楚的切法是:
- 单次全量读写有上限。例如几十 MiB。这是控制面内存、网关超时、同步 RPC 的保护,不是磁盘配额。
- 沙箱内部用命令生成更大文件,可以允许。Agent 跑构建、导出视频、写数据集,本来就会超过 API 同步口。
- 整棵工作区另有总量上限。销毁导出时如果超了,应该明确失败,而不是静默截断。
于是调用方的策略也变得清楚:
MCP 场景还要再拆一层。
模型上下文装不下几十兆原始字节。即使平台允许 HTTP 一次拉 64MiB,给模型的读工具也通常应该按 1MiB 左右分块,用偏移量续读。
写也一样。
让模型一次生成整份大文件,既不经济,也不稳定。更自然的方式是:大文件走归档或多次分块,小文件走单次写入。
这里有一个产品上的原则:
拒绝,并且告诉调用方这是传输上限,而不是“沙箱太弱写不下”。
否则 Agent 会改用命令通道硬写,边界等于没设。
3. 并发边界:文件和命令抢的是同一块活世界
会话型沙箱通常同一时刻只允许一条命令。
文件通道必须回答:命令正在跑时,能不能写文件?
有两种诱惑:
- 让写文件走另一条旁路,显得更“实时”
- 让写文件排队,等命令结束自动落盘
第一期更安全的答案往往是失败得干脆:
会话忙,就拒绝这次文件请求。
原因不是偷懒。
而是这两件事会在同一棵目录树上交错:
命令正在写 output.json
↕
文件接口同时覆盖 output.json
谁先谁后,对 Agent 都是不可观测的。
更糟的是半截写入:命令读到一半文件,或导出时打到一半文件。
所以忙时 fail-fast,其实是把时序交给调用方。
对 Agent 来说,正确姿势通常是:
等命令结束
↓
写/读文件
↓
再发起下一条命令
这和人类用 IDE 的直觉也不远:编译正在跑的时候,你不会同时用另一只手覆盖源文件,还期望结果可解释。
四. 读和写的不对称,是有意的
文件上传和下载看起来对称,实现约束并不对称。
1. 写:字节进得去,语义要停在工作区
写请求需要明确几件事:
- 这是哪一个会话
- 要写到工作区的哪一个相对路径
- 字节从哪来:原始流,或表单里的文件字段
- 写完以后文件的身份是什么
成功响应不必把文件再回传一遍。
回一个条目就够了:
path: /workspace/resume.pdf
name: resume.pdf
type: file
失败则应该是稳定类别,而不是存储系统原话。
常见类别大致是:
- 未认证
- 会话不存在或不属于当前身份
- 会话不是可写状态
- 路径不合法
- 文件太大
- 会话正忙
- 下游暂时不可用
“覆盖已有文件”建议允许。
Agent 的工作方式就是反复改同一份文件。禁止覆盖会逼它先删后写,而删除如果还要走命令,边界又花了。
“自动创建父目录”也建议允许。
否则 src/a/b/c.py 这种正常项目结构,会退化成一串准备工作。
不建议在第一期做的,是把写入做成公开可分享的上传地址。
因为那会把“谁能往这个会话里塞文件”从 Token 鉴权,变成“谁拿到链接谁就能写”。会话还活着的时候,这个权限模型通常过宽。
2. 读:成功是裸字节,失败才是 JSON
读是下载,不是查询。
所以成功响应更像:
Content-Type: application/octet-stream
Body: 文件本身
失败才回到统一错误包。
这个不对称会让接口文档看起来“不优雅”。
但对调用方极重要。
Agent、浏览器、脚本都可以把一次成功读取直接存盘。如果成功时还套 code/message/data,每个人都要写一个“从 JSON 里把文件抠出来”的解析器,还得处理编码和截断。
读还有几条必须拒绝的对象:
- 目录
- 符号链接
- 工作区以外的路径
- 不存在的文件
- 超过单次传输上限的文件
最后一条特别容易被理解成产品缺陷。
其实它在保护同步读取这条路。超大文件的正确出口是:
- 工具层分块读
- 或者会话结束后的归档下载
不要把文件接口理解成对象存储的 GetObject。
它没有桶,没有 key 列表,没有生命周期规则,也没有公开 CDN。它只是:
在某个仍属于你的临时电脑上,读取工作区里的一个普通文件。
3. 导入导出:文件通道的两端,仍然是归档
单文件接口再好,也不该取代归档。
创建会话时,input_workspace_url 解决的是“把已有项目一次性搬进来”。它面对的是 zip / tar.gz,而不是单个简历。
销毁会话时,导出解决的是“把 Agent 留下的世界带回去”。命令结束并不自动导出,这是会话和一次性任务的关键差别。
这里有几个原则值得单独说清。
第一,导出的是 /workspace,不是整台沙箱磁盘。HOME 和 /tmp 里的东西,默认不该被当成任务产物。
第二,工作区太大时,主动销毁可以拒绝并保持会话仍在。这样调用方还能进去删文件再试。到期回收则必须释放占用,能导出就导出,不能导出也要结束计费。
第三,下载链接必须是临时的。它是结果的领取凭证,不是又一个长期文件服务。文档、日志、示例里都不该出现真实密钥和长期签名。
第四,从上一轮产物继续工作,最干净的方式是把上一轮的输出归档,作为下一轮的输入归档。单文件通道负责中途,归档通道负责世代。
五. 安全:文件通道最容易被低估的地方
云沙箱的安全宣传经常停在“代码不在你的机器上跑”。
文件通道会重新打开几扇门。
1. 路径穿越不是理论题
只要接受 path,就会出现:
../../somewhere
/workspace/../...
看起来在工作区里的符号链接
原则只有一句:
以规范化之后、解析符号链接之后的最终落点为准,而不是以用户传入的字符串为准。
字符串看起来安全,不代表最终打开的是工作区文件。
这也是为什么文件通道不该复用命令通道。命令通道很难对“最终落点”做同样严格的断言。
2. 文件下载不是 SSRF,工作区导入才是
这两件事经常被写成同一个“下载”。
它们不是。
会话文件读取:平台把已经在沙箱工作区里的字节,交给已经鉴权的调用方。危险主要是路径逃逸和越权读别人的会话。
工作区导入:平台或沙箱去拉一个外部 URL。危险主要是把请求打到元数据地址、内网、链路本地,或把鉴权头带到不可信主机。
所以导入 URL 必须走另一套规则:只接受绝对 HTTP/HTTPS,拒绝用户名密码,校验所有解析结果是不是公网单播,重定向每次重验,限制跳数和大小。配置里的对象存储走 SDK,而不是再当普通 URL 去抓。
单文件通道反而应该避开这套复杂性。
它不接 URL。
它接字节。
谁能把字节送进来,只取决于会话鉴权和会话状态。
3. 鉴权发生在统一入口,而不是文件名里
文件路径里不要放身份。
不要让 path 能覆盖 Token 解析出来的用户或团队。
不要用查询参数当长期密钥。
OpenAPI 用 Token,控制台用网关身份,两条路可以对称暴露同一套文件能力,但谁也不能用请求体“我是谁”改写鉴权结果。
一个实用的检查是:
换一个 Token,去读别人的会话文件,应当得到“不存在”,而不是“没权限但文件确实在”。
不泄露归属,本身就是隔离的一部分。
4. 日志里不能出现文件内容
文件通道会经过 API、调度、执行节点。
沿途日志如果图方便把路径、URL、请求体打出来,等于把用户文件复制到了日志系统。
原则仍然土:
- 记会话 ID、大小、成功或失败类别
- 不记 Token、Cookie、签名查询
- 不记文件内容
- 不把存储内部地址返回给调用方
文件能力越方便,这条越重要。因为方便意味着调用频率会上升。
六. 对 Agent 工作流意味着什么
把上面的边界合在一起,Agent 的文件使用方式会从“想办法把字节弄进去”,变成一套稳定动作。
1. 推荐工作流
1. 创建会话,拿到会话 ID
2. 小文件直接写入 /workspace
3. 大项目用归档在创建时导入
4. 执行命令,观察输出
5. 按需读回某个结果文件,决定下一步
6. 需要时延长会话,而不是复制环境
7. 结束后销毁,领取整棵工作区
对应到工具层,Agent 不需要理解容器、存储和调度。
它只需要会用几个高层动作:
- 创建环境
- 写文件
- 读文件
- 执行命令
- 观察输出
- 销毁环境
这和 MCP 把沙箱本身变成 Tool 的方向是一致的。
上一篇写过:Agent 应该使用能力,而不是基础设施。文件通道补上的,正是当时那张能力清单里最空的两格。
2. 一次性任务仍然不该假装自己是会话
有了文件通道之后,会有一种新的误用:
既然能传文件了,干脆所有任务都开会话。
这通常更贵,也更容易忘了销毁。
判断标准还是状态。
没有中间状态、不需要反复改同一棵树,就走一次性执行:导入、运行、拿归档、自动释放。
需要连续改文件、装依赖、看结果、再改文件,才开会话,并用单文件通道作为中途的手。
文件能力不应该抹平这两种计算形态。它只是让会话形态终于完整。
3. 调用方最容易踩的几个坑
这些坑和聪明程度无关,和模型有关。
把 Worker 内部实例 ID 当成会话 ID。
后续读写文件、执行命令、销毁,用的都是创建结果里的业务 ID。
命令还在跑就写文件。
应等待命令结束。忙就是忙,不要靠重试把交错写入变成“偶尔成功”。
用文件接口读目录。
目录不是文件。需要看树,用命令;需要带走树,用导出。
把单次传输上限当成磁盘坏了。
超限后换分块或归档,而不是改用 cat 绕过。
不销毁。
会话按开着的时间计费。文件再方便,占用也还在。用完必须关电脑。
把临时下载链接写进文档当永久地址。
链接过期是特性。产物要持久化,就在拿到的那一刻存到自己的对象存储里。
七. 这类能力现在长什么样
把文件通道补齐以后,云沙箱更接近一个完整的临时 Runtime:
隔离的计算
+
可续期的生命周期
+
命令与实时输出
+
活着的工作区
+
进入和离开沙箱的归档
缺了文件通道时,它只是“能跑命令的隔离环境”。
有了文件通道,它才开始像 Agent 的电脑:盘还在,文件还能继续改,人(或模型)可以中途塞资料、中途抽结果。
我看过的几类产品,大致都在逼近这个模型,只是接口哲学不同。
有的方案把文件服务放进沙箱内部,让每个环境更像一台带小网关的主机。
有的方案把一次性函数做强,文件主要靠对象存储和归档周转。
还有一种更轻的切法:不要求你先换掉整个 Agent 框架,只把 Runtime 这一层交出去,用统一 API 和 MCP 工具来创建会话、读写工作区文件、执行命令、最后销毁。
百智云目前把 Cloud Sandbox 放在 Isolated Agent Runtime 这条线上,接入是 API 与 MCP。会话文件也落在统一入口下,而不是再给每个沙箱一个独立文件域名。
想对照公开约定的话,可以直接看控制台生成的接口说明:Huskbox API 集成文档 产品形态概览在 百智云云沙箱
这些链接适合当对照,不适合当答案
真正值得带走的仍然是设计模型:
归档负责世界的世代,单文件负责世界还活着的时候,命令负责改变这个世界。
八. 总结
Agent 的工作对象是 Workspace,不是某一条命令。
命令是手,文件树才是桌子上的稿纸。
整包导入导出,解决的是任务开始和结束。
会话中途再靠打包重建,等于把临时电脑反复关机。
不要用命令通道或随意 URL 冒充磁盘接口。
前者把文件变成副作用,后者把文件变成网络攻击面。
单文件通道必须又窄又硬。
只对存活会话、只对 /workspace、只对普通文件、忙则拒绝、超限则拒绝。
传输上限不是磁盘上限。
小文件走同步读写,大文件走分块或归档,整棵树走销毁导出。
读成功就给字节,写成功就给条目,错误给稳定类别。
文件接口首先是文件,其次才是 API。
上一篇的结论是:Agent 需要的不是更聪明的下一跳,而是一个临时存在、有明确边界的计算世界。
这一篇不过是把那句话写具体一点:
没有可靠的文件通道,所谓 Runtime 仍然只是一个会执行命令的空房间。
房间可以很安全。
但 Agent 进不去自己的书桌,也就很难真正把活干完
参考阅读
百智云云沙箱|Agent 云端运行环境
定位为独立的 Isolated Agent Runtime,支持 API 与 MCP。会话文件约定见控制台生成的开发者文档。
产品页 · API 集成文档
阿里云 FC Agent Sandbox
面向 AI Agent 和代码执行的云端隔离运行环境。
查看文档
腾讯云 Agent 沙箱服务 AGSX
面向智能体的沙箱执行环境。
查看产品