AI 写的代码放心跑:Kubernetes 官方出品的 Agent 沙箱,一行 YAML 配置即可

一句话看懂

它不是又一个容器沙箱运行时,而是给「沙箱」这件事补一层 Kubernetes 原生 API。 Deployment 管的是可复制的无状态副本,StatefulSet 管的是有编号的一组 Pod,而 AI Agent 要的恰好是两者中间那块空白:只有一个容器、要长期活着、要有自己的名字和磁盘、要能睡一觉再醒。agent-sandbox 把这块空白做成了 Sandbox 这个 CRD,一行 YAML 就能开一个。

它是什么,不是什么

是: 一个 Sandbox CRD + 控制器,声明式地管理「隔离的、有状态的、单例的」工作负载;外加三个扩展资源(SandboxTemplate / SandboxClaim / SandboxWarmPool)把批量申领做成产品级能力。

不是: 一个隔离运行时。README 里那句话是整个项目定位的关键——“Agent Sandbox is a sandbox orchestrator. It delegates low-level container isolation to secure Sandbox Runtimes”。它自己不做沙箱,它只是把 runtimeClassName 填进 Pod,剩下的隔离强度由 gVisor、Kata 或 Firecracker 决定。

这个「不做什么」的边界值得夸一句。同类项目常见的失败方式是把编排和运行时焊死,最后既打不过专用运行时的隔离能力,也跟不上 Kubernetes API 的演进节奏。这里选的是反过来:API 层可以换实现,实现层可以换 API,中间用 RuntimeClass 这个 Kubernetes 本来就有的标准插槽接上。

  • 出品方 Kubernetes SIG Apps(kubernetes-sigs)
  • 核心 API agents.x-k8s.io/v1beta1 · Sandbox
  • 最新版本 v1.0.7(2026-10-15),近五周每周一个 patch
  • 语言 / 许可证 Go · Apache-2.0
  • 客户端 Python(k8s-agent-sandbox)· Go · TypeScript 在建
  • 隔离来源 runtimeClassName → gVisor / Kata / Firecracker
  • 起步成本 一次 kubectl apply,8 行 YAML
  • 项目起点 2025-08 建仓,2026 年进入 v1.0.x

一行 YAML 之后,控制器到底建了什么

官方给的最小示例确实就这么多:

1
2
3
4
5
6
7
8
9
10
apiVersion: agents.x-k8s.io/v1beta1
kind: Sandbox
metadata:
name: my-sandbox
spec:
podTemplate:
spec:
containers:
- name: my-container
image: <IMAGE>

spec 里只有三块东西:podTemplate(必填)、volumeClaimTemplates(可选,创建后不可变)、service(要不要顺手建一个 headless Service)。控制器收到之后做的事很直白——建 Pod、按需要建 Service、按模板建 PVC,然后把结论写回 status:serviceFQDN、podIPs、nodeName,加四类 condition。

控制面与数据面分离
图:左边是声明式 API 与控制器(管生命周期),下方是它不实现的隔离层,右边是真正跑命令的数据面。三层各自可换。

有两个细节暴露了作者是写 Kubernetes 的老手。

其一,没有独立的 “Running” condition。 代码注释里把这件事说得很清楚:spec.operatingMode: Running 是用户意图,Ready=True 才是观测事实,Pod 起没起被吸收进 Ready 的 reason(DependenciesNotReady)。这一刀切掉了「状态显示 Running 但实际连不上」这类经典歧义。

其二,InvalidConfiguration 这个 reason 的存在。 Sandbox 的名字允许到 253 字符,但它派生的 Service 名必须是 DNS-1035 label,上限 63。控制器遇到这种名字不会热循环重试,而是直接判定为永久失败——因为重试一万次也不会成功。能想到给「不可能恢复的错误」单独留一个 reason,是踩过坑的人才会做的设计。

休眠是真的,但它不是 checkpoint

operatingMode 在 Running 和 Suspended 之间切换,就是这一期标题里那句「睡一觉再醒」的实现。机制是:删掉 Pod,但保留 Sandbox 对象和它的 PVC;恢复时把同一块盘干净地重新挂上去。

生命周期状态机
图:Running ⇄ Suspended 由 operatingMode 驱动,shutdownTime 到期走另一条路。注意 podIPs / nodeName 在挂起时会被清空而不是留旧值。

这里必须把边界说清楚,否则很容易踩:

保住的是磁盘,不是内存。 挂起只保证文件系统状态不丢,进程、打开的 socket、解释器里的变量全部消失。想要「连内存一起冻住」,得看 PodSnapshot 那条线(仓库里有 podsnapshot-golden-warmpool 示例和对应回归测试),而不是这个字段。

「按流量自动唤醒」目前还没有。 README 里那句 automatic resume on network connection 写在 Desired Sandbox Characteristics 一节——那是设计目标清单。roadmap 上 Auto Suspend/Resume 和 Scale to Zero 都还标着 Planned。现在你能用的是手动 patch 一个字段。

热池申领:一次「改归属」,不是一次「建 Pod」

如果要给 Agent 或 RL 训练循环开沙箱,冷启动那几秒是真正的成本。扩展资源解决的就是这个:SandboxTemplate 定义蓝图,SandboxWarmPool 提前按 replicas 养一批已 Ready 的 Sandbox,SandboxClaim 从里面认领一个。

热池申领路径
图:快路径只把 ownerReference 从池换给 claim,Pod 一个都不新建;慢路径才按模板冷启。

关键在于认领动作根本不涉及调度:挑一个没被领走的 Sandbox,把 owner 从池换成这个 claim,打上 agents.x-k8s.io/launch-type: warm,结束。项目自述的批量认领吞吐能做到每秒 300 个沙箱,roadmap 里下一步是把端到端申领延迟从 200ms 往 100ms、50ms 压。这种量级不是靠优化调度器得来的,是靠把「创建」换成「改标签」得来的。

但快路径有三个坑,都写在字段注释里:

  1. claim.spec.env 和 volumeClaimTemplates 会强制冷启。 热池里的 Pod 没有你要的变量和盘,只能重建。注释是直说的:“adding this field means the Sandbox will always be cold-started”。想保住热路径,配置就得进模板,不能进申领。
  2. 模板变更的传播是保守的。 updateStrategy 默认 OnReplenish:已经 stale 的沙箱不会被立刻删掉,要等你手动删或者被领走时才补新的。只有显式设成 Recreate 才会立即重建未领走的部分——而两种策略都绝不动已认领的沙箱。
  3. 补池流量会打爆 API Server 的公平性配额。 项目专门写了 docs/apf-insulation.md,教你用 FlowSchema 和 PriorityLevelConfiguration 把「认领」和「批量补池」隔开。这个文档的存在本身就说明有人在大流量下撞过墙。

数据面:Router 管 HTTP,sandboxd 管执行

创建走 API,跑命令走另一条路。sandbox-router 是一个无状态反向代理,靠 X-Sandbox-ID / X-Sandbox-Namespace / X-Sandbox-Port 这几个头把流量扇给集群里成千上万个沙箱 Pod——不给每个沙箱建 Service,这是它能规模化的前提。解析优先级是:显式 Pod IP > informer 缓存 > DNS 兜底。

命令执行的真正落点是 Pod 内的 sandboxd,它来自 KEP-539.2「运行时接口标准化」,故意用了两套协议:

1
2
3
sandboxd
├── gRPC :9090 → ProcessService Start / Execute / WriteStdin / SendSignal / ResizeTTY
└── HTTP :8080 → FilesystemService /v1/files · /v1/health · /v1/metadata

进程走 gRPC 是因为 Start 是一个长连接服务端流,stdout/stderr 要持续吐;文件走 REST 是因为字节可以直接当 application/octet-stream 传,任何 HTTP 客户端不用生成桩就能用。SDK 依赖的是这两份 wire contract,而不是 sandboxd 这个二进制本身——这意味着第三方运行时只要实现同样的 proto 和 OpenAPI 就算兼容。

一个会当场咬人的坑: Router 接受客户端的 HTTP/2,但在上游连接上禁用了 HTTP/2,所以它载不动 gRPC。USER_GUIDE 原话是 “it is not a complete sandboxd transport”。想跑 ProcessService,得直连 Pod IP 或者 kubectl port-forward,不能指望 Router 帮你转。

浏览器侧还有另一层现实:网页导航、<iframe src> 和 new WebSocket(url) 都没法带自定义请求头,这是平台限制不是实现问题。Router 的解法是 --path-routing-prefix,把身份塞进 URL 路径 /router/<ns>/<id>/<port>/...。配套还有两个容易忽略的正确决定:升级请求不受 --proxy-timeout 约束(否则 code-server 的 WebSocket 会在 3 分钟被准时掐掉,客户端看到 1006),以及升级时剥掉 Origin(因为 Host 已被改写,留着会被后端的 CSRF 校验拒掉)。

安全边界:三个默认值比一句「支持 gVisor」有用得多

跑 LLM 生成的代码,真正的风险不是「没有沙箱」,是配了沙箱却漏了旁路。这一层它做了三件具体的事:

  • automountServiceAccountToken 默认 false(经 SandboxTemplate 创建时)。被攻破的 Pod 拿不到 ServiceAccount token,就够不着 API Server。裸 Sandbox 没有这个默认值,得自己上 ValidatingAdmissionPolicy。
  • Managed NetworkPolicy:模板默认生成一份共享策略——ingress 只放 Router,egress 只放公网,内网 RFC1918 和云元数据地址一律拦掉。代价也写得直白:默认拒绝会连 sidecar 的端口一起挡掉,不显式放行就会健康检查失败。
  • 系统保留标签过滤。 这条最容易被忽略但很要命:Service 靠 agents.x-k8s.io/sandbox-name-hash 这个标签选 Pod,如果用户能在 podTemplate.metadata.labels 里随便写这个 key,租户 B 就能把自己的 Pod 标成租户 A 的哈希,让 A 的流量投递到自己这里。控制器的做法是把 agents.x-k8s.io/ 和 extensions.agents.x-k8s.io/ 下的键全部视为保留,先过滤再合并,且哈希标签在合并之后由控制器自己写。威胁模型里同时承认:哈希目前仍是 FNV-1a,加强到截断 SHA-256 另案跟踪。

Router 侧也有对应的账:它转发前会剥掉 Authorization(否则任何沙箱都能拿调用者的凭证去戳 API Server),对 X-Sandbox-Pod-IP 做 IP 段校验挡住 169.254.169.254 这类目标。但默认授权器是 AllowAll(为了兼容 Python 客户端),也就是说那道 IP 段检查目前是唯一一道 SSRF 防线——官方自己建议生产环境换自定义 authorizer。

适合谁,不适合谁

该用它

  • 要在集群里跑不可信生成代码:Agent 的 code interpreter、浏览器沙箱、aider / openhands 这类编码 Agent,仓库里都有对应示例。
  • RL / 评测循环:SWE-bench、R2E-Gym 这种要高频开合的负载,热池 + 认领是决定性优势,还有 Gymnasium、Nemo-Gym 集成。
  • 要「持久云开发环境」:一人一个带盘、有稳定域名、能挂起的 Jupyter / code-server。
  • 需要 K8s 原生治理:RBAC、NetworkPolicy、HPA、GitOps 全都直接复用,不用另学一套调度系统。

先别用

  • 只想在笔记本上跑两个沙箱:它的成本是集群。E2B / Modal / Daytona 这类托管服务起步更快。
  • 要内存级快照或按流量自动唤醒:一个还没进 API,一个在 roadmap 上。
  • 不能接受 v1beta1 的变更风险:发布号是 v1.0.7,但 API 版本仍是 v1beta1,v1alpha1 及其转换 webhook 已被整体删掉——官方并不承诺向后兼容。
  • 多沙箱塞进一个 Pod:Planned,现在是一个 Sandbox 一个 Pod。
  • 指望它替你隔离:不填 runtimeClassName,你得到的就是一个普通 runc 容器。

结论

一句判词

推荐指数:★★★★☆(4 / 5)

它做对的最重要一件事,是没有试图成为运行时。编排归 API,隔离归 RuntimeClass,执行归 sandboxd 的两份 wire contract,流量归 Router——四层各自可替换,这正是 Kubernetes 生态里一个组件能活得久的形态。热池用「改归属」换掉「建 Pod」,也是这一期里我最想指出的技术判断:延迟不是调出来的,是省掉的。

扣一分扣在时间上:自动挂起 / 唤醒、scale-to-zero、模板滚动更新、MCP server、TypeScript SDK 全都还挂在 roadmap 上,而 API 仍是 v1beta1 且明确不承诺兼容。如果你的 Agent 平台今年就要上线,这些得自己补一层。

如果你已经有 Kubernetes 集群、并且真的在为一个「LLM 刚写出来的、没人看过第二眼的 pip install」找地方安放它——这一期是我为这个场景写过最省心的推荐:8 行 YAML,先跑起来再说。

评论Comments