大家好我是小木头 上一期视频 我们用 OpenAI 官方托管的沙箱环境 把 Agents API 跑通了 创建了 session 跑了命令 下载了 artifact 在任务执行中也加载了 Skill 官方托管的沙箱环境 只是执行环境的一种 Agents API 还支持另一种 环境类型叫 self_hosted 执行器可以跑在咱们 自己的基础设施里 OpenAI 只负责调度 这一期我分享一个刚刚 开源的小项目叫 apple-sandbox 它用 Apple 官方的 container 工具 在 Mac 上给每个 session 起一个 独立的轻量级的虚拟机 把 Agents API 的执行器跑在这里面 不需要云账号 不需要公网地址 也不需要 Docker 项目已经做过端到端的验证 在测试中能够看到真实的 session 的创建 真实的任务执行 结果文件也能够直接 落到咱们本机的磁盘上 今天的内容咱们分三块吧 先介绍这个项目是什么 架构是什么 然后重点说说它和 Agents API 之间的连接方式 尤其是没有公网的地址的情况下 它是怎么和 OpenAI 协同工作的 最后用发布版本从安装到 跑任务完整的演示一遍 apple-sandbox 是一个用 Go 语言 编写的命令行工具 没有常驻进程 目前呢也没有守护程序 它只做一件事情 把 OpenAI 的 codex exec-server 这个执行器 跑进一个隔离的虚拟机里 底层用的是 Apple 去年开源的 container 项目 它和 Docker 最大的区别是隔离级别 Docker 上所有容器共享一个 Linux 内核 Apple container 是每个容器单独 起一个轻量级的 Linux 虚拟机 基于 macOS 自带的 Virtualization.framework 所以在 apple-sandbox 里面 一个 session 就是一个 OCI 容器 也就是一台独立的虚拟机 agent 在里面装软件跑脚本 都不会离开这台虚拟机 从下往上来看看整个结构 目前呢是 4 层 最底下是 Apple Silicon 的 Mac 装好 container 工具 执行 container system start 把 它的服务启动起来 往上一层是 Apple container 本身 负责从 OCI 镜像启动虚拟机 再往上是执行器的镜像 基于 node 22 的精简版 安装了 CA 证书和 codex 这个 npm 包 入口脚本里面执行了 codex exec-server 最上面才是 apple-sandbox 这个 CLI 它负责创建工作目录 注入环境变量 调用 container run 然后呢就退出 把虚拟机留在后台跑 有一个设计方面的细节需要单独聊聊 每个 session 在本机的 sandboxes 目录下有一个自己的子目录 直接挂载成虚拟机里的 /workspace 所以文件进出不需要 任何上传下载接口 往那个目录里放一个文件 虚拟机里马上就能看到 agent 写出来的结果 本机磁盘上也能马上读到 虚拟机是随时可以销毁的 磁盘上这个目录才是真正的状态 命令一共 8 个 doctor 检查环境 build 构建执行器镜像 run 启动一个 session logs 看日志 ls 列出 session stop 和 rm 分别是停止以及删除 可以用 prune 命令按时间批量 地清理已经停掉的 session 再来看看它在整个链路里的位置 第一步 你的应用用普通的 API key 来调用 Agents API 创建一个 self_hosted 类型的 session 返回里除了 session ID 还有两个关键字段 environment.id 和一个远端的地址 remote_url 这两个值是给执行器用的 第二步在你的 OpenAI 平台上单独 建一个受限的 API key 拿普通的 API key 去注册执行器 API 会直接返回 403 通常会看到这么一个错误 说缺少 api.agents.environments.connect 这个 scope 第三步是把这个受限的 API key 放进环境变量 OPENAI_EXECUTOR_API_KEY 执行 apple-sandbox run 传入 environment.id 和 remote_url CLI 会创建工作目录 把这三个值作为环境变量注入 调用 container run 启动一台虚拟机 虚拟机里的入口脚本把受限 的 API key 转成 codex exec-server 自己要求的变量名 CODEX_API_KEY 然后呢启动 exec-server 第四步 exec-server 用这个 API key 主动向 remote_url 发起一个 WebSocket 连接 完成注册 OpenAI 那边把 session 标记成活跃 之后每一轮任务都是 OpenAI 那边发命令 虚拟机在 /workspace 里执行 结果呢再回传 你的应用通过 Agents API 提交任务 订阅事件流 看到的和 OpenAI 托管的 sandbox 完全一样 整个过程里 apple-sandbox 从头到尾没有和 OpenAI 通信 它只负责把三个参数变成 一台跑着执行器的虚拟机 然后呢就退出了 这段是这期里面我想再聊聊的一部分 首先看托管的 sandbox 什么都不用管 OpenAI 自己起环境 如果大家看过 Cloudflare 官方 给 Agents API 写的参考实现 它是云端部署的典型形态 你部署一个 worker 公网地址有了 OpenAI 通过带签名的 webhook 把 session 的事件推送 有任务要执行 状态空闲 执行失败等等 这些呢都是事件会推送过来的 而 worker 收到事件再去启动或唤醒容器 容器里的执行器向 OpenAI 建立 WebSocket 连接 也就是说在那套方案里面 webhook 和 WebSocket 各管一件事 WebSocket 是执行器和 OpenAI 之间 真正的交换命令和结果 永远都是从你这边主动出站 而 webhook 只解决一个问题 OpenAI 什么时候需要算力 怎么通知你把容器启动起来 而 apple-sandbox 跑在一台个人的 Mac 电脑上 没有公网地址 也就没法给 OpenAI 提供 一个接收 webhook 的地址 那怎么办呢 答案其实也在 OpenAI 自己文档里 self_hosted 环境 也就咱们这种自己运行的沙箱环境 有两种同样都可以 认为是正式的启动方式 一种叫 webhook-managed 就是刚才咱们谈到的像 Cloudflare 那种 OpenAI 决定什么时候要算力 推送事件给你 另一种呢叫 application-managed 你的应用创建 session 的那一刻 就已经拿到了环境的 ID 也就是 environment.id 和 remote_url 直接同步把算力呢启动起来 不需要等待 OpenAI 的通知 Cloudflare、Vercel、Modal 的集成文档都把这两种 并列成为选项 而不是主备关系 apple-sandbox 实现的正是 application-managed 创建 session 拿到两个参数 立即执行 apple-sandbox run 三步呢是在一起的 webhook 那条路解决的是规模化 场景下按需启动算力的问题 一个开发者呢 在自己的 Mac 上跑 其实呢并没有很具体的这方面的问题 真正的数据通道 两种方式是一样的 都是执行器主动出站 发起一个 WebSocket 连接 连接断了自己重连 OpenAI 从来不需要反向地连你的机器 所以少了公网地址 少的只是提前收通知这件事 那少了通知还剩一件事情要自己处理 session 什么时候结束 虚拟机什么时候该销毁 apple-sandbox 没有去轮询 OpenAI 也并不需要 exec-server 有个特性 连接临时断开它会重连 但 OpenAI 彻底关闭 session 之后 它就退出了 容器随之呢 就进入了 stopped 状态 所以 prune 命令只看一件事 哪些容器已经停了 停了多久 prune --older-than 1h 把停掉超过一小时的清理掉 手动跑或者挂一个 cron 都行 它永远呢不会碰正在运行的容器 当然了目前 apple-sandbox 还处于非常早期 在后续的开发中还会 逐渐完善这方面的功能 使它越来越健壮 把上面的机制串成状态图 一个 session 在沙箱里经历 的状态大致是这样的 执行 apple-sandbox run 容器被创建出来 这是 Created 状态 入口脚本启动 exec-server 向 OpenAI 注册 进入 Registering 状态 注册呢有两个分支 remote_url 或者 key 不对 exec-server 立即就拒绝 进入 Rejected 容器随即就停掉 错误信息呢 可以在 apple-sandbox logs 里看到 注册通过进入连接状态 Connected 之后 OpenAI 每下发一条 命令就进入 Executing 执行完回到 Connected 中间 WebSocket 掉线 进入 Reconnecting exec-server 自己重连 apple-sandbox 并不参与 OpenAI 彻底关闭 session exec-server 退出 容器呢就进入 stopped 状态 最后通过 rm 或者 prune 把它删掉 最终达到了 Removed 状态 从 CLI 的角度看 ls 只报两种状态 running 和 stopped 中间那些状态全都在虚拟机里面发生 下面我们用发布版本完整跑一遍 项目在 GitHub 上 sugarforever/apple-sandbox 在发布版本这里 releases 有一个 darwin_arm64 的压缩包 只有这个平台 因为 Apple container 呢 只能在 Apple Silicon 的 Mac 上跑 前置条件只有一个 brew install --cask container 装好 Apple 的 container 工具 然后按照 README 里的一条命令安装 从 GitHub Releases 拉取最新的压缩包 把二进制解压到用户 目录下的 .local/bin 并不需要 sudo 执行器镜像的 Dockerfile 和入口 脚本已经嵌在了二进制里 所以不需要克隆代码仓库 在任何一个空目录里都能操作 装好后先跑 apple-sandbox doctor 做 5 项检查 包含了像 Apple Silicon Hypervisor 支持 container 命令 container 的服务状态 磁盘空间 全部 PASS 就可以了 如果服务状态检查失败 那么尝试运行 container system start 启动一下 再跑一次 然后 apple-sandbox build 来构建执行器的镜像 这一步要拉取 node 镜像 装 CA 证书装 codex 第一次呢会花一点时间 镜像准备好了 现在呢创建一个 session 向 /v1/agents/sessions 发一个 POST 请求 请求的头带上 agents=v1 这个 beta 标记以及 OpenAI 的 API key 请求体里面带上 agent 和 session 模型我选择了 gpt-6-astra 系统指令是让它在 /workspace 里工作 每一步跑了什么命令都展示出来 生成的文件在结束前自己检查一遍 environment 指定类型是 self_hosted 工作目录是 /workspace API 响应应该是 201 来看看三个字段 一个呢是 ID ID 是 sess 开头的一个 session ID 另外一个是 environment.id 还包含一个 remote_url 它是 api.openai.com 下面 一个 connect 路径的地址 这两个值原样地传给沙箱就行 并不需要改动 把响应存到 session.json 后面呢用三个变量咱们把它取出来 受限的 API key 我已经在 OpenAI 平台上 的 agents 部分创建好了 把它放在本机环境 变量 OPENAI_EXECUTOR_API_KEY 里面 现在启动沙箱 运行 apple-sandbox run 给它 session ID、environment.id 和 remote_url 命令马上返回 session 的名字 容器的 ID 本机的工作目录 还有查看日志的命令 apple-sandbox ls 状态呢应该能看到它是 running apple-sandbox logs -f 可以跟一下日志 能够看到 exec-server 启动 向远程的 URL 也就是 remote_url 发起了注册 顺便验证一下它真的是一台虚拟机 container exec 进去 执行 uname -a 看到的是 Linux 内核 跑在了一台 Mac 上 给它一个任务吧 先在本机 sandboxes 目录下 这个 session 的子目录里放一个 CSV 文件 内容很简单 4 行数据 item 和 amount 两列 不需要任何上传接口 放进目录里 实际上就放进了虚拟机 里的 /workspace 目录 顺便说一句吧 OpenAI 托管的 sandbox 创建 session 的时候呢 可以用 environment.files 这个 字段内联地把文件带进去 同样的字段在 self_hosted 上 OpenAI 会返回 400 说 environment.files 是个未知参数 因此 self_hosted 类型的文件 进出只能靠你自己去挂载 API 并不支持 然后就提交任务吧 POST 到 session 的 events 接口 事件类型呢 是 agent.session.input.message 内容是用 Python 读 /workspace/amounts.csv 验证 amount 列是数字 算行数和总数 把结果按固定格式写到 /workspace/outputs/summary.json 写完读回来验证报告结果 应该能够看到返回是 202 session 状态从 idle 变成了 in_progress 这时候呢 可以看两个地方 一个是 apple-sandbox 的日志 通过 apple-sandbox logs 命令 能看到执行器收到命令 执行命令 另一边是 Agents API 的事件流 等它跑完 直接用 cat 命令查看本机的 sandboxes 目录下的 summary.json 文件 应该有 4 行 总和呢 75 verified 是标记为了 true 这是 Python 在虚拟机里执行 通过挂载写到本机磁盘上的文件 没有 artifact 下载 也没有轮询任何的接口 self_hosted 的产物 这些 artifact 不会发布到 OpenAI 的 Artifacts 接口 文件呢就会在你自己的目录里 跑完了三步清理 apple-sandbox stop 停掉容器 apple-sandbox rm 删掉容器 再用 DELETE 请求把 OpenAI 那边的 session 删掉 如果不想一个一个地记 session ID apple-sandbox prune --older -than 1h 先加一个 --dry-run 的选项 看看会删哪些 再真正地去删掉它们 这里呢有几点值得注意的 README 呢 也已经写了 第一个出站网络没有做白名单 Apple container 的网络只有两种 完全内网或者完全开放 没有按域名放行的机制 因此现在执行器是全开放出站的 已经确认它需要访问 api.openai.com 注册之后还需不需要别的地址呢 还没有验证 所以不建议拿它跑不受信任的任务 等加了代理或者白名单那一层 咱们再说吧 第二点是只有 CLI 目前呢还没有 SDK TypeScript 或者 Python 的封装 其实呢就是包一层子进程调用 后面呢可能我们也会继续地添加 感兴趣的朋友呢也 可以来自己封装一下 第三 prune 判断 session 结束靠的是推断 容器停了就代表 session 结束了 这条推断依赖于 exec-server 在 OpenAI 彻底关闭连接之后会自己退出 目前文档呢 是这么描述的 端到端测试当中也没有反例 但连接中到停止这一步的转换 还没有被单独地去观测过 这一期我们把 Agents API 的 self_hosted 环境接到一台 Mac 电脑上 Apple container 提供虚拟机级别的隔离 执行器主动出站连接 OpenAI 本机目录直接就当做工作区 没有公网地址 靠的是 application-managed 正式的启动方式 缺的只是提前收到通知 用 prune 命令我们做了补全 项目呢开源在 GitHub 欢迎大家试用 也欢迎提 issue 欢迎 PR 如果这期视频对你有帮助 欢迎关注 也欢迎一键三连吧 那我们就下期再见