Skip to content

Stage 12:给 Agent 一张工作台,但别顺手把整台电脑钥匙也给它——Workspace 与 Sandbox

Language: English | 简体中文

Stage 11 结束时,我们已经能让多个 Agent 分工。但真实任务很快会从“大家聊一聊”变成:把文件改一下、生成报告、运行测试、执行脚本、把结果保存成 Artifact。Stage 08 的 Skill 甚至允许目录里带 Script。

到这里,一个不能再拖的问题出现了:

Agent 如果真的能读写文件、运行程序,它究竟能碰到这台机器的多少东西?

最危险的答案是:“反正都是 Python,直接 subprocess.run() 吧。”

这就像给实习生一张临时办公桌,然后顺手把机房、财务室和 CEO 抽屉的钥匙串一起挂在桌角。

Stage 12 要做两件事。第一,建立 Workspace:让一次 Run 有明确的文件边界和 Artifact 区域。第二,建立一个受限 Command Runner,用它讨论执行目录、可执行程序、环境变量、Timeout 和输出上限。

但必须先说清楚:

本章的标准库 Subprocess Runner 不是安全 Sandbox。

它是用来理解 Sandbox 需要哪些边界,而不是用几行 Python 宣布自己已经隔离了恶意代码。


1. 为什么 Agent 需要 Workspace?

普通聊天程序的主要状态可能都在 Message 里。长一点的 Agent 任务却经常有输入文件、中间草稿、下载资料、生成代码、测试结果和最终 Artifact。

如果所有东西都散在当前进程工作目录,很快就回答不了:哪些文件属于这次 Run?下一个 Run 能不能看到它们?用户能下载哪一个?Agent 可以修改仓库里的什么?清理时哪些可以删?

Workspace 的第一层价值,就是把“一次任务的工作区”变成明确对象。


2. 一次 Run 一个 Root,是一个很好的起点

本章创建:

workspace = AgentWorkspace.create(
    "/tmp/runs/run-001"
)

之后所有文件都相对这个 Root:

run-001/
├── input.txt
├── work/
│   └── check.py
└── artifacts/
    └── result.txt

这不代表所有生产系统都必须用本地目录。Workspace 完全可以映射到对象存储、远程 Sandbox 或持久卷。

但抽象应该先存在:

Agent 操作的是自己的 Workspace,不是“这台机器随便哪个 Path”。


3. Path Traversal:../ 是一只很小但很有战斗力的字符串

假设你提供:

workspace.read_text(path)

如果 path 可以是 ../../../../etc/passwd,那所谓 Workspace Root 就只是墙上贴的一张“请勿越界”海报。

所以本章所有路径都经过:

target = (root / relative).resolve()

然后检查真实目标是否仍在 Root 内。绝对路径也直接拒绝。

notes/a.txt
    -> allowed

../secret.txt
    -> rejected

/etc/passwd
    -> rejected

这叫 Path Confinement。它不是整个 Sandbox,但它是文件边界最基本的一层。


你可能会想:“我禁止 .. 不就行了?”

不够。

因为 Workspace 里可以出现 Symlink:

workspace/link -> /etc

然后用户读取 link/passwd,字符串里根本没有 ..

这也是为什么本章先 resolve(),再检查真实目标是否仍在 Root 内。文件系统边界最好基于 Canonical Path,而不是字符串长相。


5. Artifact 和临时工作文件最好不是一个概念

Agent 做任务时可能产生一大堆中间文件,例如下载页、Scratch Script、Notes 和 Debug JSON。用户真正关心的也许只有 artifacts/report.pdf

所以推荐在 Workspace 内至少区分:

work/
artifacts/

work/ 是 Agent 干活的地方,artifacts/ 是准备交付、保存或导出的结果。

这和 Stage 14 的长时任务会再次连接:Artifact 应该能脱离模型 Context 和当前进程继续存在。


6. 现在才轮到 Command Runner

文件边界有了以后,我们才谈执行。

本章 Runner 接受:

runner.run(
    [python, "work/check.py"],
    timeout_seconds=2,
)

注意是参数数组,不是 shell=True

["python", "check.py"]"python check.py; rm -rf ..." 属于完全不同的解析边界。默认 shell=False 可以少一层 Shell 字符串解释。

这并不意味着命令因此“安全”,但少给一个解释器通常是好事。


7. Executable Allowlist:不是所有程序都值得提供

本章创建 Runner 时声明:

CommandRunner(
    workspace,
    allowed_executables={"python3"},
)

如果模型尝试 shcurlssh,不在 Allowlist 就拒绝。

这和 Stage 09 的 Tool Permission 是同一个思路:

机器上安装了
!=
Agent 有权执行

当然,允许 Python 本身已经是一项很强的能力。Python 可以读文件、开网络、启动子进程,所以 Executable Allowlist 只是能力面的一层,不是完整隔离。


8. cwd 很重要

Runner 强制:

cwd=workspace.root

这样 Script 使用相对路径时自然落在当前 Run Workspace。

如果不固定 CWD,同一份 Script 在不同启动位置可能读写完全不同的文件。可重复执行需要明确工作目录。


9. 环境变量不要默认整包继承

你的服务进程可能有:

DATABASE_URL
OPENAI_API_KEY
GITHUB_TOKEN
AWS_SECRET_ACCESS_KEY

如果 subprocess.run() 默认继承全部 Environment,那么刚获得脚本执行能力的 Agent 子进程可能顺便获得服务进程所有 Credential。

本章 Runner 从很小的 Environment 开始:

env = {
    "PATH": ...,
    "PYTHONIOENCODING": "utf-8",
}

需要额外值时显式传入。

原则是:

Credential 不是“运行环境的一部分”,而是需要明确授予的 Capability。


10. Timeout 这次比 Stage 09 更强,但仍不是完美终止

Stage 09 的 Python Handler 使用 Cooperative Deadline,因为在线程里很难安全杀死任意工作。

Stage 12 进入了 Subprocess 边界。subprocess.run(..., timeout=...) 可以在超时后终止直接子进程,这比“我不等线程了”更强。

但仍然别宣布“现在所有子孙进程都会被完美清理”。复杂 Process Tree、Daemon、外部服务副作用仍然需要更系统的 Process Group、Container 或 Sandbox 生命周期管理。

准确说法是:

独立进程给了我们比普通函数更清晰的终止边界,但还不是完整安全隔离。


11. Output 也需要 Budget

一个程序可以打印一千万个字符。如果 Runtime 把完整 stdout 塞回模型 Context,你刚刚用一行代码创造了自己的 Context DDoS。

所以 Runner 有:

max_output_chars

超出后追加 ...[truncated]

这和 Stage 07 的 Context Budget 是同一类问题:

任何会进入模型或持久化系统的数据流,都需要上限。


12. Workspace 不是 Security Sandbox

这是本章最需要严谨的一点。

我们的 Runner 做了 Path Confinement、Executable Allowlist、Controlled CWD、Reduced Environment、Timeout、Output Limit 和 shell=False

很好。

但允许执行 Python 时,代码仍然可能 import socketimport subprocess 或直接调用操作系统能力。如果 OS 权限允许,它仍然可能访问 Workspace 外的资源。

所以:

bounded subprocess wrapper
!=
security sandbox

真正更强的隔离通常还需要独立 OS 用户、Namespace、Container/VM、Filesystem Mount Policy、Network Policy、Syscall Controls、Resource Limits 和 Credential Isolation。

具体技术会因部署环境变化。这章的价值是让你知道一个 Sandbox 需要回答哪些问题,而不是背某个云产品名字。


13. Container 也不是“完美 Sandbox”同义词

Container 能提供很有价值的 Filesystem View、Process Namespace、Resource Limit 和 Network Configuration。

但安全强度取决于配置、Runtime、Kernel、Mount、Capability 和 Credential。

例如把 /var/run/docker.sock 直接挂进 Container,很多“隔离感”会立刻变得很哲学。

所以:

Container 是隔离工具,不是自动安全证明。

同样的原则也适用于托管 Sandbox 产品:要看它真正隔离什么。


14. Network 应该是显式策略

有些 Agent 任务需要网络,有些完全不需要。

如果一个只做本地代码格式检查的 Script 默认可以访问互联网,攻击面被无意义地放大。

真正 Sandbox 常会有 network = off 或 Destination Allowlist。

本章标准库 Runner 没有能力可靠实现 OS 级 Network Isolation,所以不会伪造一个 network=False 参数然后假装它真的管用了。

这就是技术严谨性:

没实现的隔离,不要写成配置项 cosplay。


15. Skill Script 到这里才终于有执行位置

Stage 08 我们故意没有执行 Skill 中的任意 Script。

现在可以把关系接起来:

Skill
    ↓ procedure says run script
Host Policy
    ↓ decides whether script is allowed
Workspace
    ↓ provides bounded files
Runner / Sandbox
    ↓ executes under environment policy
Artifact
    ↓ result

Skill 自己仍然不拥有执行权。它只是程序性指导和资源包。


16. Workspace 和 Durable State 也不是一回事

一个临时 Workspace 可以随着 Compute 消失。Checkpoint 和 Task Ledger 则应该保存在 Durable Store。

所以:

Workspace
    -> current compute's working files

Checkpoint
    -> resumable execution state

Artifact Store
    -> durable outputs worth keeping

有些系统会把 Workspace 本身做成持久卷,那只是实现选择,语义仍然值得分开。

Stage 14 会处理“Worker 消失以后怎么根据 Durable State 重建工作环境”。


17. Cleanup 也是生命周期的一部分

创建临时 Workspace 很容易,清理很容易被忘。

如果每个 Run 都留下下载文件、依赖缓存、模型生成脚本和大日志,磁盘迟早会用一种非常直接的方式提醒你生命周期设计不完整。

所以 Workspace 应该有:

create
use
export artifacts
cleanup

哪些 Artifact 需要长期保留,应该由应用明确决定。


18. 运行完整代码

python stages/12-agent-workspace-sandbox/code/demo.py
python stages/12-agent-workspace-sandbox/code/checks.py

Demo 会在临时 Workspace 里创建 Input 和 Script,用受限 Runner 执行,再生成 artifacts/result.txt

检查覆盖相对路径、../ Escape、Absolute Path、Executable Allowlist、CWD、Timeout、Output Truncation,以及服务进程的额外环境变量不会自动传进子进程。


19. 下一章为什么是 Production Service?

到这里,我们的 Agent 已经不只是 Notebook Demo。它有 Durable State、External Tools、Memory、Context、Skills、Guardrails、Eval、Multi-Agent、Workspace 和 Subprocess。

真正部署时,一个更现实的问题出现了:

多个用户同时发请求怎么办?

谁创建 Run?Request、Thread、Run、User、Tenant 怎么区分?任务太长不能一直占着 HTTP 连接怎么办?如何 Backpressure?进程重启以后 Run Status 去哪找?服务什么时候算 Ready?

所以下一章 Stage 13,我们把 Agent 从“程序”变成“服务”。真正麻烦,也从这里开始变得很像普通分布式系统工程。