實際影片長度:15:55.000。原文、繁中、雙語可點擊句子跳轉影片。
0:01.800–0:03.466
zh大家好我是小木头
0:03.600–0:04.366
zh上一期视频
0:04.366–0:07.000
zh我们用 OpenAI 官方托管的沙箱环境
0:07.066–0:09.000
zh把 Agents API 跑通了
0:09.000–0:10.133
zh创建了 session
0:10.400–0:11.333
zh跑了命令
0:11.533–0:12.700
zh下载了 artifact
0:12.766–0:15.000
zh在任务执行中也加载了 Skill
0:15.733–0:17.466
zh官方托管的沙箱环境
0:17.466–0:18.900
zh只是执行环境的一种
0:19.066–0:20.533
zhAgents API 还支持另一种
0:20.533–0:22.200
zh环境类型叫 self_hosted
0:23.666–0:25.066
zh执行器可以跑在咱们
0:25.066–0:26.466
zh自己的基础设施里
0:26.533–0:27.966
zhOpenAI 只负责调度
0:28.266–0:30.166
zh这一期我分享一个刚刚
0:30.166–0:32.400
zh开源的小项目叫 apple-sandbox
0:32.933–0:35.200
zh它用 Apple 官方的 container 工具
0:35.200–0:37.366
zh在 Mac 上给每个 session 起一个
0:37.366–0:39.266
zh独立的轻量级的虚拟机
0:39.466–0:42.066
zh把 Agents API 的执行器跑在这里面
0:42.366–0:43.466
zh不需要云账号
0:43.466–0:44.733
zh不需要公网地址
0:44.733–0:45.900
zh也不需要 Docker
0:46.533–0:49.133
zh项目已经做过端到端的验证
0:49.266–0:52.166
zh在测试中能够看到真实的 session 的创建
0:52.300–0:53.566
zh真实的任务执行
0:53.766–0:55.333
zh结果文件也能够直接
0:55.333–0:57.066
zh落到咱们本机的磁盘上
0:58.400–1:00.500
zh今天的内容咱们分三块吧
1:00.500–1:01.966
zh先介绍这个项目是什么
1:02.000–1:02.866
zh架构是什么
1:02.900–1:04.733
zh然后重点说说它和 Agents
1:04.800–1:06.133
zhAPI 之间的连接方式
1:06.366–1:08.700
zh尤其是没有公网的地址的情况下
1:08.700–1:10.766
zh它是怎么和 OpenAI 协同工作的
1:10.800–1:12.966
zh最后用发布版本从安装到
1:12.966–1:14.933
zh跑任务完整的演示一遍
1:17.666–1:19.733
zhapple-sandbox 是一个用 Go 语言
1:19.733–1:21.066
zh编写的命令行工具
1:22.066–1:23.300
zh没有常驻进程
1:23.400–1:25.066
zh目前呢也没有守护程序
1:25.133–1:26.133
zh它只做一件事情
1:26.133–1:29.866
zh把 OpenAI 的 codex exec-server 这个执行器
1:29.866–1:31.666
zh跑进一个隔离的虚拟机里
1:32.000–1:35.066
zh底层用的是 Apple 去年开源的 container 项目
1:35.100–1:37.500
zh它和 Docker 最大的区别是隔离级别
1:37.866–1:40.800
zhDocker 上所有容器共享一个 Linux 内核
1:41.000–1:42.766
zhApple container 是每个容器单独
1:42.766–1:44.900
zh起一个轻量级的 Linux 虚拟机
1:45.100–1:48.400
zh基于 macOS 自带的 Virtualization.framework
1:48.866–1:50.866
zh所以在 apple-sandbox 里面
1:50.866–1:53.266
zh一个 session 就是一个 OCI 容器
1:53.400–1:55.266
zh也就是一台独立的虚拟机
1:55.500–1:58.100
zhagent 在里面装软件跑脚本
1:58.100–1:59.900
zh都不会离开这台虚拟机
2:00.600–2:02.900
zh从下往上来看看整个结构
2:02.900–2:04.266
zh目前呢是 4 层
2:04.300–2:06.466
zh最底下是 Apple Silicon 的 Mac
2:06.666–2:08.100
zh装好 container 工具
2:08.700–2:10.600
zh执行 container system start 把
2:10.600–2:11.800
zh它的服务启动起来
2:12.066–2:14.333
zh往上一层是 Apple container 本身
2:14.500–2:17.466
zh负责从 OCI 镜像启动虚拟机
2:17.600–2:19.766
zh再往上是执行器的镜像
2:20.166–2:21.233
zh基于 node 22 的精简版
2:22.900–2:26.166
zh安装了 CA 证书和 codex 这个 npm 包
2:26.300–2:29.966
zh入口脚本里面执行了 codex exec-server
2:30.500–2:33.666
zh最上面才是 apple-sandbox 这个 CLI
2:33.733–2:35.900
zh它负责创建工作目录
2:36.100–2:37.200
zh注入环境变量
2:37.200–2:38.400
zh调用 container run
2:38.966–2:40.000
zh然后呢就退出
2:40.000–2:41.666
zh把虚拟机留在后台跑
2:42.800–2:45.666
zh有一个设计方面的细节需要单独聊聊
2:45.933–2:48.666
zh每个 session 在本机的 sandboxes
2:48.666–2:50.166
zh目录下有一个自己的子目录
2:50.300–2:53.200
zh直接挂载成虚拟机里的 /workspace
2:53.600–2:54.800
zh所以文件进出不需要
2:54.800–2:56.400
zh任何上传下载接口
2:56.533–2:58.266
zh往那个目录里放一个文件
2:58.333–2:59.866
zh虚拟机里马上就能看到
3:00.266–3:01.800
zhagent 写出来的结果
3:01.800–3:03.666
zh本机磁盘上也能马上读到
3:04.133–3:06.266
zh虚拟机是随时可以销毁的
3:06.366–3:08.366
zh磁盘上这个目录才是真正的状态
3:08.900–3:10.000
zh命令一共 8 个
3:10.200–3:11.533
zhdoctor 检查环境
3:11.666–3:13.333
zhbuild 构建执行器镜像
3:13.533–3:14.900
zhrun 启动一个 session
3:15.133–3:16.066
zhlogs 看日志
3:16.533–3:17.666
zhls 列出 session
3:18.133–3:20.933
zhstop 和 rm 分别是停止以及删除
3:21.800–3:24.666
zh可以用 prune 命令按时间批量
3:24.666–3:26.133
zh地清理已经停掉的 session
3:30.366–3:33.200
zh再来看看它在整个链路里的位置
3:35.166–3:35.733
zh第一步
3:35.733–3:39.133
zh你的应用用普通的 API key 来调用 Agents API
3:39.700–3:42.166
zh创建一个 self_hosted 类型的 session
3:42.766–3:44.333
zh返回里除了 session ID
3:44.333–3:45.533
zh还有两个关键字段
3:45.900–3:49.133
zhenvironment.id 和一个远端的地址 remote_url
3:49.200–3:51.133
zh这两个值是给执行器用的
3:52.100–3:54.900
zh第二步在你的 OpenAI 平台上单独
3:54.900–3:56.800
zh建一个受限的 API key
3:57.200–4:00.333
zh拿普通的 API key 去注册执行器
4:00.733–4:02.666
zhAPI 会直接返回 403
4:03.200–4:04.533
zh通常会看到这么一个错误
4:04.533–4:08.000
zh说缺少 api.agents.environments.connect 这个 scope
4:08.866–4:11.200
zh第三步是把这个受限的 API key
4:11.200–4:13.766
zh放进环境变量 OPENAI_EXECUTOR_API_KEY
4:14.533–4:16.166
zh执行 apple-sandbox run
4:16.566–4:20.000
zh传入 environment.id 和 remote_url
4:20.466–4:22.166
zhCLI 会创建工作目录
4:22.333–4:24.733
zh把这三个值作为环境变量注入
4:25.400–4:27.766
zh调用 container run 启动一台虚拟机
4:28.200–4:30.533
zh虚拟机里的入口脚本把受限
4:30.533–4:33.500
zh的 API key 转成 codex exec-server
4:33.500–4:36.200
zh自己要求的变量名 CODEX_API_KEY
4:36.200–4:38.133
zh然后呢启动 exec-server
4:39.933–4:42.700
zh第四步 exec-server 用这个 API key 主动向
4:42.700–4:45.266
zhremote_url 发起一个 WebSocket 连接
4:45.366–4:46.166
zh完成注册
4:46.800–4:49.500
zhOpenAI 那边把 session 标记成活跃
4:49.866–4:52.966
zh之后每一轮任务都是 OpenAI 那边发命令
4:53.100–4:55.500
zh虚拟机在 /workspace 里执行
4:55.533–4:56.766
zh结果呢再回传
4:57.566–4:59.600
zh你的应用通过 Agents API 提交任务
4:59.700–5:00.933
zh订阅事件流
5:01.066–5:04.766
zh看到的和 OpenAI 托管的 sandbox 完全一样
5:05.000–5:06.066
zh整个过程里
5:06.133–5:08.900
zhapple-sandbox 从头到尾没有和 OpenAI 通信
5:08.966–5:10.933
zh它只负责把三个参数变成
5:10.933–5:12.933
zh一台跑着执行器的虚拟机
5:12.933–5:14.300
zh然后呢就退出了
5:17.400–5:20.333
zh这段是这期里面我想再聊聊的一部分
5:20.466–5:22.166
zh首先看托管的 sandbox
5:22.300–5:23.200
zh什么都不用管
5:23.200–5:24.566
zhOpenAI 自己起环境
5:25.400–5:27.700
zh如果大家看过 Cloudflare 官方
5:27.700–5:29.866
zh给 Agents API 写的参考实现
5:30.300–5:32.533
zh它是云端部署的典型形态
5:32.566–5:33.866
zh你部署一个 worker
5:34.000–5:35.333
zh公网地址有了
5:35.600–5:37.766
zhOpenAI 通过带签名的 webhook
5:37.800–5:39.600
zh把 session 的事件推送
5:42.166–5:43.533
zh有任务要执行
5:43.800–5:44.733
zh状态空闲
5:45.000–5:46.100
zh执行失败等等
5:46.100–5:48.533
zh这些呢都是事件会推送过来的
5:48.866–5:51.900
zh而 worker 收到事件再去启动或唤醒容器
5:52.000–5:55.666
zh容器里的执行器向 OpenAI 建立 WebSocket 连接
5:56.133–5:57.766
zh也就是说在那套方案里面
5:57.766–6:00.366
zhwebhook 和 WebSocket 各管一件事
6:00.933–6:03.366
zhWebSocket 是执行器和 OpenAI 之间
6:03.366–6:05.533
zh真正的交换命令和结果
6:06.866–6:08.800
zh永远都是从你这边主动出站
6:08.800–6:10.733
zh而 webhook 只解决一个问题
6:10.866–6:12.133
zhOpenAI 什么时候需要算力
6:12.133–6:14.066
zh怎么通知你把容器启动起来
6:14.966–6:17.800
zh而 apple-sandbox 跑在一台个人的 Mac 电脑上
6:17.800–6:18.966
zh没有公网地址
6:19.000–6:20.566
zh也就没法给 OpenAI 提供
6:20.566–6:22.266
zh一个接收 webhook 的地址
6:22.266–6:23.266
zh那怎么办呢
6:23.866–6:26.166
zh答案其实也在 OpenAI 自己文档里
6:26.700–6:27.966
zhself_hosted 环境
6:27.966–6:30.266
zh也就咱们这种自己运行的沙箱环境
6:30.466–6:31.933
zh有两种同样都可以
6:31.933–6:33.600
zh认为是正式的启动方式
6:33.866–6:35.666
zh一种叫 webhook-managed
6:36.000–6:38.766
zh就是刚才咱们谈到的像 Cloudflare 那种
6:38.900–6:40.366
zhOpenAI 决定什么时候要算力
6:40.666–6:41.766
zh推送事件给你
6:42.533–6:44.300
zh另一种呢叫 application-managed
6:44.500–6:46.966
zh你的应用创建 session 的那一刻
6:47.066–6:48.733
zh就已经拿到了环境的 ID
6:48.733–6:51.066
zh也就是 environment.id 和 remote_url
6:51.733–6:53.900
zh直接同步把算力呢启动起来
6:53.900–6:55.533
zh不需要等待 OpenAI 的通知
6:56.533–7:00.000
zhCloudflare、Vercel、Modal 的集成文档都把这两种
7:00.566–7:01.933
zh并列成为选项
7:01.966–7:03.300
zh而不是主备关系
7:04.300–7:06.700
zhapple-sandbox 实现的正是 application-managed
7:07.066–7:07.866
zh创建 session
7:07.900–7:08.933
zh拿到两个参数
7:08.933–7:11.000
zh立即执行 apple-sandbox run
7:11.066–7:12.500
zh三步呢是在一起的
7:13.000–7:15.733
zhwebhook 那条路解决的是规模化
7:15.733–7:17.700
zh场景下按需启动算力的问题
7:18.066–7:19.100
zh一个开发者呢
7:19.100–7:20.400
zh在自己的 Mac 上跑
7:20.400–7:23.100
zh其实呢并没有很具体的这方面的问题
7:23.900–7:25.266
zh真正的数据通道
7:25.600–7:26.600
zh两种方式是一样的
7:26.600–7:28.333
zh都是执行器主动出站
7:28.866–7:30.533
zh发起一个 WebSocket 连接
7:30.600–7:31.866
zh连接断了自己重连
7:32.133–7:34.866
zhOpenAI 从来不需要反向地连你的机器
7:35.066–7:36.533
zh所以少了公网地址
7:36.566–7:38.766
zh少的只是提前收通知这件事
7:39.166–7:41.733
zh那少了通知还剩一件事情要自己处理
7:41.966–7:43.133
zhsession 什么时候结束
7:43.300–7:44.700
zh虚拟机什么时候该销毁
7:45.400–7:47.400
zhapple-sandbox 没有去轮询 OpenAI
7:47.400–7:48.366
zh也并不需要
7:48.900–7:50.300
zhexec-server 有个特性
7:50.500–7:52.266
zh连接临时断开它会重连
7:52.266–7:54.366
zh但 OpenAI 彻底关闭 session 之后
7:54.366–7:55.333
zh它就退出了
7:55.500–7:56.300
zh容器随之呢
7:56.300–7:57.700
zh就进入了 stopped 状态
7:57.900–7:59.733
zh所以 prune 命令只看一件事
7:59.766–8:01.000
zh哪些容器已经停了
8:01.000–8:01.800
zh停了多久
8:02.066–8:03.800
enprune --older-than 1h
8:04.000–8:05.866
zh把停掉超过一小时的清理掉
8:06.300–8:08.400
zh手动跑或者挂一个 cron 都行
8:08.500–8:10.566
zh它永远呢不会碰正在运行的容器
8:11.533–8:14.133
zh当然了目前 apple-sandbox 还处于非常早期
8:14.133–8:15.500
zh在后续的开发中还会
8:15.500–8:16.966
zh逐渐完善这方面的功能
8:16.966–8:18.166
zh使它越来越健壮
8:20.400–8:22.400
zh把上面的机制串成状态图
8:22.400–8:24.266
zh一个 session 在沙箱里经历
8:24.266–8:25.700
zh的状态大致是这样的
8:26.200–8:27.600
zh执行 apple-sandbox run
8:27.733–8:28.766
zh容器被创建出来
8:28.766–8:30.133
zh这是 Created 状态
8:30.500–8:32.466
zh入口脚本启动 exec-server
8:32.466–8:33.733
zh向 OpenAI 注册
8:33.800–8:35.900
zh进入 Registering 状态
8:36.000–8:37.933
zh注册呢有两个分支
8:38.400–8:40.333
zhremote_url 或者 key 不对
8:40.333–8:41.766
zhexec-server 立即就拒绝
8:41.766–8:42.800
zh进入 Rejected
8:43.000–8:44.300
zh容器随即就停掉
8:46.866–8:47.466
zh错误信息呢
8:47.466–8:49.400
zh可以在 apple-sandbox logs 里看到
8:49.533–8:51.533
zh注册通过进入连接状态 Connected
8:51.800–8:53.533
zh之后 OpenAI 每下发一条
8:53.533–8:54.900
zh命令就进入 Executing
8:55.133–8:56.300
zh执行完回到 Connected
8:56.700–8:58.366
zh中间 WebSocket 掉线
8:58.366–8:59.500
zh进入 Reconnecting
9:00.100–9:01.500
zhexec-server 自己重连
9:01.533–9:02.933
zhapple-sandbox 并不参与
9:03.266–9:04.666
zhOpenAI 彻底关闭 session
9:05.066–9:06.400
zhexec-server 退出
9:06.400–9:08.166
zh容器呢就进入 stopped 状态
9:08.366–9:10.866
zh最后通过 rm 或者 prune 把它删掉
9:10.933–9:12.666
zh最终达到了 Removed 状态
9:13.400–9:14.666
zh从 CLI 的角度看
9:14.666–9:16.100
zhls 只报两种状态
9:16.133–9:17.066
zhrunning 和 stopped
9:17.400–9:19.966
zh中间那些状态全都在虚拟机里面发生
9:21.333–9:23.500
zh下面我们用发布版本完整跑一遍
9:23.566–9:26.966
zh项目在 GitHub 上 sugarforever/apple-sandbox
9:27.200–9:28.566
zh在发布版本这里
9:28.566–9:31.600
zhreleases 有一个 darwin_arm64 的压缩包
9:31.666–9:32.533
zh只有这个平台
9:32.533–9:33.733
zh因为 Apple container 呢
9:33.733–9:35.600
zh只能在 Apple Silicon 的 Mac 上跑
9:36.300–9:38.700
zh前置条件只有一个 brew install --cask container
9:38.733–9:40.700
zh装好 Apple 的 container 工具
9:40.966–9:43.366
zh然后按照 README 里的一条命令安装
9:43.533–9:46.666
zh从 GitHub Releases 拉取最新的压缩包
9:46.700–9:48.100
zh把二进制解压到用户
9:48.100–9:50.066
zh目录下的 .local/bin
9:50.200–9:51.300
zh并不需要 sudo
9:51.766–9:53.866
zh执行器镜像的 Dockerfile 和入口
9:53.866–9:55.600
zh脚本已经嵌在了二进制里
9:55.600–9:57.500
zh所以不需要克隆代码仓库
9:57.500–9:59.533
zh在任何一个空目录里都能操作
10:01.333–10:03.500
zh装好后先跑 apple-sandbox doctor
10:03.700–10:04.766
zh做 5 项检查
10:04.900–10:06.500
zh包含了像 Apple Silicon
10:06.800–10:08.800
zhHypervisor 支持 container 命令
10:08.866–10:10.066
zhcontainer 的服务状态
10:10.066–10:10.900
zh磁盘空间
10:10.966–10:12.566
zh全部 PASS 就可以了
10:12.900–10:14.566
zh如果服务状态检查失败
10:14.566–10:17.366
zh那么尝试运行 container system start 启动一下
10:17.366–10:18.266
zh再跑一次
10:18.666–10:22.000
zh然后 apple-sandbox build 来构建执行器的镜像
10:22.100–10:23.966
zh这一步要拉取 node 镜像
10:24.133–10:25.966
zh装 CA 证书装 codex
10:26.133–10:27.700
zh第一次呢会花一点时间
10:29.466–10:30.466
zh镜像准备好了
10:30.466–10:31.700
zh现在呢创建一个 session
10:32.200–10:35.266
zh向 /v1/agents/sessions 发一个 POST 请求
10:35.366–10:37.966
zh请求的头带上 agents=v1 这个
10:37.966–10:40.666
zhbeta 标记以及 OpenAI 的 API key
10:40.966–10:43.200
zh请求体里面带上 agent 和 session
10:43.666–10:45.933
zh模型我选择了 gpt-6-astra
10:46.100–10:48.700
zh系统指令是让它在 /workspace 里工作
10:48.700–10:50.766
zh每一步跑了什么命令都展示出来
10:50.800–10:53.466
zh生成的文件在结束前自己检查一遍
10:53.866–10:55.933
zhenvironment 指定类型是 self_hosted
10:55.966–10:57.700
zh工作目录是 /workspace
10:58.466–11:00.266
zhAPI 响应应该是 201
11:00.400–11:01.466
zh来看看三个字段
11:01.466–11:02.600
zh一个呢是 ID
11:03.100–11:05.266
zhID 是 sess 开头的一个 session ID
11:06.666–11:08.500
zh另外一个是 environment.id
11:08.666–11:10.766
zh还包含一个 remote_url
11:10.966–11:13.366
zh它是 api.openai.com 下面
11:13.366–11:14.866
zh一个 connect 路径的地址
11:15.966–11:18.000
zh这两个值原样地传给沙箱就行
11:18.000–11:19.000
zh并不需要改动
11:19.066–11:21.133
zh把响应存到 session.json
11:21.200–11:23.666
zh后面呢用三个变量咱们把它取出来
11:24.466–11:25.566
zh受限的 API key
11:25.566–11:27.066
zh我已经在 OpenAI 平台上
11:27.066–11:28.700
zh的 agents 部分创建好了
11:28.766–11:30.066
zh把它放在本机环境
11:30.066–11:32.466
zh变量 OPENAI_EXECUTOR_API_KEY 里面
11:33.500–11:34.733
zh现在启动沙箱
11:34.900–11:36.166
zh运行 apple-sandbox run
11:36.733–11:38.533
zh给它 session ID、environment.id
11:38.533–11:39.666
zh和 remote_url
11:39.733–11:41.800
zh命令马上返回 session 的名字
11:41.800–11:42.600
zh容器的 ID
11:42.600–11:43.866
zh本机的工作目录
11:43.900–11:45.733
zh还有查看日志的命令
11:46.700–11:47.766
enapple-sandbox ls
11:47.766–11:49.900
zh状态呢应该能看到它是 running
11:50.600–11:52.466
enapple-sandbox logs -f
11:52.500–11:53.566
zh可以跟一下日志
11:53.900–11:55.900
zh能够看到 exec-server 启动
11:55.900–11:57.366
zh向远程的 URL
11:57.366–11:59.100
zh也就是 remote_url 发起了注册
11:59.900–12:02.500
zh顺便验证一下它真的是一台虚拟机
12:02.733–12:04.300
zhcontainer exec 进去
12:04.300–12:05.766
zh执行 uname -a
12:05.766–12:07.466
zh看到的是 Linux 内核
12:07.466–12:08.900
zh跑在了一台 Mac 上
12:09.400–12:10.600
zh给它一个任务吧
12:11.100–12:13.300
zh先在本机 sandboxes 目录下
12:13.533–12:16.333
zh这个 session 的子目录里放一个 CSV 文件
12:16.766–12:17.766
zh内容很简单
12:17.766–12:18.766
zh4 行数据
12:18.800–12:20.200
zhitem 和 amount 两列
12:20.200–12:21.666
zh不需要任何上传接口
12:21.666–12:22.566
zh放进目录里
12:22.566–12:24.533
zh实际上就放进了虚拟机
12:24.533–12:26.133
zh里的 /workspace 目录
12:26.666–12:27.766
zh顺便说一句吧
12:28.533–12:31.333
zhOpenAI 托管的 sandbox 创建 session 的时候呢
12:31.333–12:33.166
zh可以用 environment.files 这个
12:33.166–12:35.966
zh字段内联地把文件带进去
12:36.066–12:38.333
zh同样的字段在 self_hosted 上
12:38.333–12:40.400
zhOpenAI 会返回 400
12:40.733–12:43.066
zh说 environment.files 是个未知参数
12:43.500–12:45.800
zh因此 self_hosted 类型的文件
12:45.800–12:47.533
zh进出只能靠你自己去挂载
12:48.266–12:49.200
zhAPI 并不支持
12:49.866–12:51.133
zh然后就提交任务吧
12:51.533–12:53.733
zhPOST 到 session 的 events 接口
12:53.766–12:54.700
zh事件类型呢
12:54.700–12:56.733
zh是 agent.session.input.message
12:57.133–13:01.300
zh内容是用 Python 读 /workspace/amounts.csv
13:01.466–13:03.366
zh验证 amount 列是数字
13:03.500–13:04.733
zh算行数和总数
13:04.733–13:06.766
zh把结果按固定格式写到
13:07.166–13:09.166
en/workspace/outputs/summary.json
13:10.100–13:11.966
zh写完读回来验证报告结果
13:12.866–13:14.866
zh应该能够看到返回是 202
13:14.966–13:17.400
zhsession 状态从 idle 变成了 in_progress
13:17.700–13:18.366
zh这时候呢
13:18.366–13:19.666
zh可以看两个地方
13:19.766–13:21.766
zh一个是 apple-sandbox 的日志
13:22.333–13:24.000
zh通过 apple-sandbox logs 命令
13:24.133–13:25.766
zh能看到执行器收到命令
13:25.766–13:26.566
zh执行命令
13:26.566–13:28.966
zh另一边是 Agents API 的事件流
13:29.500–13:30.533
zh等它跑完
13:30.533–13:33.800
zh直接用 cat 命令查看本机的
13:33.800–13:36.133
zhsandboxes 目录下的 summary.json 文件
13:36.466–13:37.266
zh应该有 4 行
13:37.266–13:38.466
zh总和呢 75
13:38.566–13:40.466
zhverified 是标记为了 true
13:40.666–13:42.600
zh这是 Python 在虚拟机里执行
13:42.700–13:45.266
zh通过挂载写到本机磁盘上的文件
13:47.200–13:48.300
zh没有 artifact 下载
13:48.300–13:49.966
zh也没有轮询任何的接口
13:50.133–13:51.533
zhself_hosted 的产物
13:51.600–13:55.066
zh这些 artifact 不会发布到 OpenAI 的 Artifacts 接口
13:55.066–13:56.900
zh文件呢就会在你自己的目录里
13:58.766–14:00.266
zh跑完了三步清理
14:00.333–14:02.100
zhapple-sandbox stop 停掉容器
14:02.266–14:03.966
zhapple-sandbox rm 删掉容器
14:03.966–14:07.100
zh再用 DELETE 请求把 OpenAI 那边的 session 删掉
14:07.300–14:09.500
zh如果不想一个一个地记 session ID
14:10.200–14:11.766
enapple-sandbox prune
14:11.766–14:12.566
en--older
14:12.766–14:14.100
en-than 1h
14:14.700–14:16.333
zh先加一个 --dry-run 的选项
14:16.333–14:17.533
zh看看会删哪些
14:17.766–14:19.466
zh再真正地去删掉它们
14:21.133–14:23.666
zh这里呢有几点值得注意的 README 呢
14:23.666–14:24.800
zh也已经写了
14:24.800–14:27.466
zh第一个出站网络没有做白名单
14:27.566–14:29.166
zhApple container 的网络只有两种
14:29.166–14:31.100
zh完全内网或者完全开放
14:31.300–14:32.966
zh没有按域名放行的机制
14:33.100–14:35.666
zh因此现在执行器是全开放出站的
14:35.666–14:38.933
zh已经确认它需要访问 api.openai.com
14:39.000–14:40.966
zh注册之后还需不需要别的地址呢
14:40.966–14:41.900
zh还没有验证
14:41.966–14:44.266
zh所以不建议拿它跑不受信任的任务
14:44.300–14:46.800
zh等加了代理或者白名单那一层
14:46.800–14:47.733
zh咱们再说吧
14:48.166–14:49.466
zh第二点是只有 CLI
14:49.466–14:50.766
zh目前呢还没有 SDK
14:51.000–14:52.666
zhTypeScript 或者 Python 的封装
14:52.666–14:54.700
zh其实呢就是包一层子进程调用
14:54.700–14:56.700
zh后面呢可能我们也会继续地添加
14:56.766–14:57.800
zh感兴趣的朋友呢也
14:57.800–14:59.166
zh可以来自己封装一下
14:59.500–15:02.266
zh第三 prune 判断 session 结束靠的是推断
15:02.400–15:04.400
zh容器停了就代表 session 结束了
15:04.400–15:06.533
zh这条推断依赖于 exec-server
15:06.800–15:09.600
zh在 OpenAI 彻底关闭连接之后会自己退出
15:09.733–15:10.566
zh目前文档呢
15:10.566–15:11.666
zh是这么描述的
15:12.133–15:13.900
zh端到端测试当中也没有反例
15:14.500–15:16.866
zh但连接中到停止这一步的转换
15:16.900–15:18.800
zh还没有被单独地去观测过
15:20.000–15:22.133
zh这一期我们把 Agents API 的 self_hosted
15:22.133–15:24.366
zh环境接到一台 Mac 电脑上
15:24.866–15:27.133
zhApple container 提供虚拟机级别的隔离
15:27.366–15:29.800
zh执行器主动出站连接 OpenAI
15:29.800–15:31.900
zh本机目录直接就当做工作区
15:32.066–15:33.200
zh没有公网地址
15:33.300–15:36.733
zh靠的是 application-managed 正式的启动方式
15:36.900–15:38.733
zh缺的只是提前收到通知
15:39.700–15:41.766
zh用 prune 命令我们做了补全
15:42.000–15:43.333
zh项目呢开源在 GitHub
15:43.333–15:44.300
zh欢迎大家试用
15:44.300–15:46.500
zh也欢迎提 issue 欢迎 PR
15:46.566–15:48.200
zh如果这期视频对你有帮助
15:48.266–15:48.900
zh欢迎关注
15:48.900–15:50.200
zh也欢迎一键三连吧
15:50.200–15:51.700
zh那我们就下期再见
0:01.800–0:03.466
大家好我是小木头
0:03.600–0:04.366
上一期视频
0:04.366–0:07.000
我们用 OpenAI 官方托管的沙箱环境
0:07.066–0:09.000
把 Agents API 跑通了
0:09.000–0:10.133
创建了 session
0:10.400–0:11.333
跑了命令
0:11.533–0:12.700
下载了 artifact
0:12.766–0:15.000
在任务执行中也加载了 Skill
0:15.733–0:17.466
官方托管的沙箱环境
0:17.466–0:18.900
只是执行环境的一种
0:19.066–0:20.533
Agents API 还支持另一种
0:20.533–0:22.200
环境类型叫 self_hosted
0:23.666–0:25.066
执行器可以跑在咱们
0:25.066–0:26.466
自己的基础设施里
0:26.533–0:27.966
OpenAI 只负责调度
0:28.266–0:30.166
这一期我分享一个刚刚
0:30.166–0:32.400
开源的小项目叫 apple-sandbox
0:32.933–0:35.200
它用 Apple 官方的 container 工具
0:35.200–0:37.366
在 Mac 上给每个 session 起一个
0:37.366–0:39.266
独立的轻量级的虚拟机
0:39.466–0:42.066
把 Agents API 的执行器跑在这里面
0:42.366–0:43.466
不需要云账号
0:43.466–0:44.733
不需要公网地址
0:44.733–0:45.900
也不需要 Docker
0:46.533–0:49.133
项目已经做过端到端的验证
0:49.266–0:52.166
在测试中能够看到真实的 session 的创建
0:52.300–0:53.566
真实的任务执行
0:53.766–0:55.333
结果文件也能够直接
0:55.333–0:57.066
落到咱们本机的磁盘上
0:58.400–1:00.500
今天的内容咱们分三块吧
1:00.500–1:01.966
先介绍这个项目是什么
1:02.000–1:02.866
架构是什么
1:02.900–1:04.733
然后重点说说它和 Agents
1:04.800–1:06.133
API 之间的连接方式
1:06.366–1:08.700
尤其是没有公网的地址的情况下
1:08.700–1:10.766
它是怎么和 OpenAI 协同工作的
1:10.800–1:12.966
最后用发布版本从安装到
1:12.966–1:14.933
跑任务完整的演示一遍
1:17.666–1:19.733
apple-sandbox 是一个用 Go 语言
1:19.733–1:21.066
编写的命令行工具
1:22.066–1:23.300
没有常驻进程
1:23.400–1:25.066
目前呢也没有守护程序
1:25.133–1:26.133
它只做一件事情
1:26.133–1:29.866
把 OpenAI 的 codex exec-server 这个执行器
1:29.866–1:31.666
跑进一个隔离的虚拟机里
1:32.000–1:35.066
底层用的是 Apple 去年开源的 container 项目
1:35.100–1:37.500
它和 Docker 最大的区别是隔离级别
1:37.866–1:40.800
Docker 上所有容器共享一个 Linux 内核
1:41.000–1:42.766
Apple container 是每个容器单独
1:42.766–1:44.900
起一个轻量级的 Linux 虚拟机
1:45.100–1:48.400
基于 macOS 自带的 Virtualization.framework
1:48.866–1:50.866
所以在 apple-sandbox 里面
1:50.866–1:53.266
一个 session 就是一个 OCI 容器
1:53.400–1:55.266
也就是一台独立的虚拟机
1:55.500–1:58.100
agent 在里面装软件跑脚本
1:58.100–1:59.900
都不会离开这台虚拟机
2:00.600–2:02.900
从下往上来看看整个结构
2:02.900–2:04.266
目前呢是 4 层
2:04.300–2:06.466
最底下是 Apple Silicon 的 Mac
2:06.666–2:08.100
装好 container 工具
2:08.700–2:10.600
执行 container system start 把
2:10.600–2:11.800
它的服务启动起来
2:12.066–2:14.333
往上一层是 Apple container 本身
2:14.500–2:17.466
负责从 OCI 镜像启动虚拟机
2:17.600–2:19.766
再往上是执行器的镜像
2:20.166–2:21.233
基于 node 22 的精简版
2:22.900–2:26.166
安装了 CA 证书和 codex 这个 npm 包
2:26.300–2:29.966
入口脚本里面执行了 codex exec-server
2:30.500–2:33.666
最上面才是 apple-sandbox 这个 CLI
2:33.733–2:35.900
它负责创建工作目录
2:36.100–2:37.200
注入环境变量
2:37.200–2:38.400
调用 container run
2:38.966–2:40.000
然后呢就退出
2:40.000–2:41.666
把虚拟机留在后台跑
2:42.800–2:45.666
有一个设计方面的细节需要单独聊聊
2:45.933–2:48.666
每个 session 在本机的 sandboxes
2:48.666–2:50.166
目录下有一个自己的子目录
2:50.300–2:53.200
直接挂载成虚拟机里的 /workspace
2:53.600–2:54.800
所以文件进出不需要
2:54.800–2:56.400
任何上传下载接口
2:56.533–2:58.266
往那个目录里放一个文件
2:58.333–2:59.866
虚拟机里马上就能看到
3:00.266–3:01.800
agent 写出来的结果
3:01.800–3:03.666
本机磁盘上也能马上读到
3:04.133–3:06.266
虚拟机是随时可以销毁的
3:06.366–3:08.366
磁盘上这个目录才是真正的状态
3:08.900–3:10.000
命令一共 8 个
3:10.200–3:11.533
doctor 检查环境
3:11.666–3:13.333
build 构建执行器镜像
3:13.533–3:14.900
run 启动一个 session
3:15.133–3:16.066
logs 看日志
3:16.533–3:17.666
ls 列出 session
3:18.133–3:20.933
stop 和 rm 分别是停止以及删除
3:21.800–3:24.666
可以用 prune 命令按时间批量
3:24.666–3:26.133
地清理已经停掉的 session
3:30.366–3:33.200
再来看看它在整个链路里的位置
3:35.166–3:35.733
第一步
3:35.733–3:39.133
你的应用用普通的 API key 来调用 Agents API
3:39.700–3:42.166
创建一个 self_hosted 类型的 session
3:42.766–3:44.333
返回里除了 session ID
3:44.333–3:45.533
还有两个关键字段
3:45.900–3:49.133
environment.id 和一个远端的地址 remote_url
3:49.200–3:51.133
这两个值是给执行器用的
3:52.100–3:54.900
第二步在你的 OpenAI 平台上单独
3:54.900–3:56.800
建一个受限的 API key
3:57.200–4:00.333
拿普通的 API key 去注册执行器
4:00.733–4:02.666
API 会直接返回 403
4:03.200–4:04.533
通常会看到这么一个错误
4:04.533–4:08.000
说缺少 api.agents.environments.connect 这个 scope
4:08.866–4:11.200
第三步是把这个受限的 API key
4:11.200–4:13.766
放进环境变量 OPENAI_EXECUTOR_API_KEY
4:14.533–4:16.166
执行 apple-sandbox run
4:16.566–4:20.000
传入 environment.id 和 remote_url
4:20.466–4:22.166
CLI 会创建工作目录
4:22.333–4:24.733
把这三个值作为环境变量注入
4:25.400–4:27.766
调用 container run 启动一台虚拟机
4:28.200–4:30.533
虚拟机里的入口脚本把受限
4:30.533–4:33.500
的 API key 转成 codex exec-server
4:33.500–4:36.200
自己要求的变量名 CODEX_API_KEY
4:36.200–4:38.133
然后呢启动 exec-server
4:39.933–4:42.700
第四步 exec-server 用这个 API key 主动向
4:42.700–4:45.266
remote_url 发起一个 WebSocket 连接
4:45.366–4:46.166
完成注册
4:46.800–4:49.500
OpenAI 那边把 session 标记成活跃
4:49.866–4:52.966
之后每一轮任务都是 OpenAI 那边发命令
4:53.100–4:55.500
虚拟机在 /workspace 里执行
4:55.533–4:56.766
结果呢再回传
4:57.566–4:59.600
你的应用通过 Agents API 提交任务
4:59.700–5:00.933
订阅事件流
5:01.066–5:04.766
看到的和 OpenAI 托管的 sandbox 完全一样
5:05.000–5:06.066
整个过程里
5:06.133–5:08.900
apple-sandbox 从头到尾没有和 OpenAI 通信
5:08.966–5:10.933
它只负责把三个参数变成
5:10.933–5:12.933
一台跑着执行器的虚拟机
5:12.933–5:14.300
然后呢就退出了
5:17.400–5:20.333
这段是这期里面我想再聊聊的一部分
5:20.466–5:22.166
首先看托管的 sandbox
5:22.300–5:23.200
什么都不用管
5:23.200–5:24.566
OpenAI 自己起环境
5:25.400–5:27.700
如果大家看过 Cloudflare 官方
5:27.700–5:29.866
给 Agents API 写的参考实现
5:30.300–5:32.533
它是云端部署的典型形态
5:32.566–5:33.866
你部署一个 worker
5:34.000–5:35.333
公网地址有了
5:35.600–5:37.766
OpenAI 通过带签名的 webhook
5:37.800–5:39.600
把 session 的事件推送
5:42.166–5:43.533
有任务要执行
5:43.800–5:44.733
状态空闲
5:45.000–5:46.100
执行失败等等
5:46.100–5:48.533
这些呢都是事件会推送过来的
5:48.866–5:51.900
而 worker 收到事件再去启动或唤醒容器
5:52.000–5:55.666
容器里的执行器向 OpenAI 建立 WebSocket 连接
5:56.133–5:57.766
也就是说在那套方案里面
5:57.766–6:00.366
webhook 和 WebSocket 各管一件事
6:00.933–6:03.366
WebSocket 是执行器和 OpenAI 之间
6:03.366–6:05.533
真正的交换命令和结果
6:06.866–6:08.800
永远都是从你这边主动出站
6:08.800–6:10.733
而 webhook 只解决一个问题
6:10.866–6:12.133
OpenAI 什么时候需要算力
6:12.133–6:14.066
怎么通知你把容器启动起来
6:14.966–6:17.800
而 apple-sandbox 跑在一台个人的 Mac 电脑上
6:17.800–6:18.966
没有公网地址
6:19.000–6:20.566
也就没法给 OpenAI 提供
6:20.566–6:22.266
一个接收 webhook 的地址
6:22.266–6:23.266
那怎么办呢
6:23.866–6:26.166
答案其实也在 OpenAI 自己文档里
6:26.700–6:27.966
self_hosted 环境
6:27.966–6:30.266
也就咱们这种自己运行的沙箱环境
6:30.466–6:31.933
有两种同样都可以
6:31.933–6:33.600
认为是正式的启动方式
6:33.866–6:35.666
一种叫 webhook-managed
6:36.000–6:38.766
就是刚才咱们谈到的像 Cloudflare 那种
6:38.900–6:40.366
OpenAI 决定什么时候要算力
6:40.666–6:41.766
推送事件给你
6:42.533–6:44.300
另一种呢叫 application-managed
6:44.500–6:46.966
你的应用创建 session 的那一刻
6:47.066–6:48.733
就已经拿到了环境的 ID
6:48.733–6:51.066
也就是 environment.id 和 remote_url
6:51.733–6:53.900
直接同步把算力呢启动起来
6:53.900–6:55.533
不需要等待 OpenAI 的通知
6:56.533–7:00.000
Cloudflare、Vercel、Modal 的集成文档都把这两种
7:00.566–7:01.933
并列成为选项
7:01.966–7:03.300
而不是主备关系
7:04.300–7:06.700
apple-sandbox 实现的正是 application-managed
7:07.066–7:07.866
创建 session
7:07.900–7:08.933
拿到两个参数
7:08.933–7:11.000
立即执行 apple-sandbox run
7:11.066–7:12.500
三步呢是在一起的
7:13.000–7:15.733
webhook 那条路解决的是规模化
7:15.733–7:17.700
场景下按需启动算力的问题
7:18.066–7:19.100
一个开发者呢
7:19.100–7:20.400
在自己的 Mac 上跑
7:20.400–7:23.100
其实呢并没有很具体的这方面的问题
7:23.900–7:25.266
真正的数据通道
7:25.600–7:26.600
两种方式是一样的
7:26.600–7:28.333
都是执行器主动出站
7:28.866–7:30.533
发起一个 WebSocket 连接
7:30.600–7:31.866
连接断了自己重连
7:32.133–7:34.866
OpenAI 从来不需要反向地连你的机器
7:35.066–7:36.533
所以少了公网地址
7:36.566–7:38.766
少的只是提前收通知这件事
7:39.166–7:41.733
那少了通知还剩一件事情要自己处理
7:41.966–7:43.133
session 什么时候结束
7:43.300–7:44.700
虚拟机什么时候该销毁
7:45.400–7:47.400
apple-sandbox 没有去轮询 OpenAI
7:47.400–7:48.366
也并不需要
7:48.900–7:50.300
exec-server 有个特性
7:50.500–7:52.266
连接临时断开它会重连
7:52.266–7:54.366
但 OpenAI 彻底关闭 session 之后
7:54.366–7:55.333
它就退出了
7:55.500–7:56.300
容器随之呢
7:56.300–7:57.700
就进入了 stopped 状态
7:57.900–7:59.733
所以 prune 命令只看一件事
7:59.766–8:01.000
哪些容器已经停了
8:01.000–8:01.800
停了多久
8:02.066–8:03.800
prune --older-than 1h
8:04.000–8:05.866
把停掉超过一小时的清理掉
8:06.300–8:08.400
手动跑或者挂一个 cron 都行
8:08.500–8:10.566
它永远呢不会碰正在运行的容器
8:11.533–8:14.133
当然了目前 apple-sandbox 还处于非常早期
8:14.133–8:15.500
在后续的开发中还会
8:15.500–8:16.966
逐渐完善这方面的功能
8:16.966–8:18.166
使它越来越健壮
8:20.400–8:22.400
把上面的机制串成状态图
8:22.400–8:24.266
一个 session 在沙箱里经历
8:24.266–8:25.700
的状态大致是这样的
8:26.200–8:27.600
执行 apple-sandbox run
8:27.733–8:28.766
容器被创建出来
8:28.766–8:30.133
这是 Created 状态
8:30.500–8:32.466
入口脚本启动 exec-server
8:32.466–8:33.733
向 OpenAI 注册
8:33.800–8:35.900
进入 Registering 状态
8:36.000–8:37.933
注册呢有两个分支
8:38.400–8:40.333
remote_url 或者 key 不对
8:40.333–8:41.766
exec-server 立即就拒绝
8:41.766–8:42.800
进入 Rejected
8:43.000–8:44.300
容器随即就停掉
8:46.866–8:47.466
错误信息呢
8:47.466–8:49.400
可以在 apple-sandbox logs 里看到
8:49.533–8:51.533
注册通过进入连接状态 Connected
8:51.800–8:53.533
之后 OpenAI 每下发一条
8:53.533–8:54.900
命令就进入 Executing
8:55.133–8:56.300
执行完回到 Connected
8:56.700–8:58.366
中间 WebSocket 掉线
8:58.366–8:59.500
进入 Reconnecting
9:00.100–9:01.500
exec-server 自己重连
9:01.533–9:02.933
apple-sandbox 并不参与
9:03.266–9:04.666
OpenAI 彻底关闭 session
9:05.066–9:06.400
exec-server 退出
9:06.400–9:08.166
容器呢就进入 stopped 状态
9:08.366–9:10.866
最后通过 rm 或者 prune 把它删掉
9:10.933–9:12.666
最终达到了 Removed 状态
9:13.400–9:14.666
从 CLI 的角度看
9:14.666–9:16.100
ls 只报两种状态
9:16.133–9:17.066
running 和 stopped
9:17.400–9:19.966
中间那些状态全都在虚拟机里面发生
9:21.333–9:23.500
下面我们用发布版本完整跑一遍
9:23.566–9:26.966
项目在 GitHub 上 sugarforever/apple-sandbox
9:27.200–9:28.566
在发布版本这里
9:28.566–9:31.600
releases 有一个 darwin_arm64 的压缩包
9:31.666–9:32.533
只有这个平台
9:32.533–9:33.733
因为 Apple container 呢
9:33.733–9:35.600
只能在 Apple Silicon 的 Mac 上跑
9:36.300–9:38.700
前置条件只有一个 brew install --cask container
9:38.733–9:40.700
装好 Apple 的 container 工具
9:40.966–9:43.366
然后按照 README 里的一条命令安装
9:43.533–9:46.666
从 GitHub Releases 拉取最新的压缩包
9:46.700–9:48.100
把二进制解压到用户
9:48.100–9:50.066
目录下的 .local/bin
9:50.200–9:51.300
并不需要 sudo
9:51.766–9:53.866
执行器镜像的 Dockerfile 和入口
9:53.866–9:55.600
脚本已经嵌在了二进制里
9:55.600–9:57.500
所以不需要克隆代码仓库
9:57.500–9:59.533
在任何一个空目录里都能操作
10:01.333–10:03.500
装好后先跑 apple-sandbox doctor
10:03.700–10:04.766
做 5 项检查
10:04.900–10:06.500
包含了像 Apple Silicon
10:06.800–10:08.800
Hypervisor 支持 container 命令
10:08.866–10:10.066
container 的服务状态
10:10.066–10:10.900
磁盘空间
10:10.966–10:12.566
全部 PASS 就可以了
10:12.900–10:14.566
如果服务状态检查失败
10:14.566–10:17.366
那么尝试运行 container system start 启动一下
10:17.366–10:18.266
再跑一次
10:18.666–10:22.000
然后 apple-sandbox build 来构建执行器的镜像
10:22.100–10:23.966
这一步要拉取 node 镜像
10:24.133–10:25.966
装 CA 证书装 codex
10:26.133–10:27.700
第一次呢会花一点时间
10:29.466–10:30.466
镜像准备好了
10:30.466–10:31.700
现在呢创建一个 session
10:32.200–10:35.266
向 /v1/agents/sessions 发一个 POST 请求
10:35.366–10:37.966
请求的头带上 agents=v1 这个
10:37.966–10:40.666
beta 标记以及 OpenAI 的 API key
10:40.966–10:43.200
请求体里面带上 agent 和 session
10:43.666–10:45.933
模型我选择了 gpt-6-astra
10:46.100–10:48.700
系统指令是让它在 /workspace 里工作
10:48.700–10:50.766
每一步跑了什么命令都展示出来
10:50.800–10:53.466
生成的文件在结束前自己检查一遍
10:53.866–10:55.933
environment 指定类型是 self_hosted
10:55.966–10:57.700
工作目录是 /workspace
10:58.466–11:00.266
API 响应应该是 201
11:00.400–11:01.466
来看看三个字段
11:01.466–11:02.600
一个呢是 ID
11:03.100–11:05.266
ID 是 sess 开头的一个 session ID
11:06.666–11:08.500
另外一个是 environment.id
11:08.666–11:10.766
还包含一个 remote_url
11:10.966–11:13.366
它是 api.openai.com 下面
11:13.366–11:14.866
一个 connect 路径的地址
11:15.966–11:18.000
这两个值原样地传给沙箱就行
11:18.000–11:19.000
并不需要改动
11:19.066–11:21.133
把响应存到 session.json
11:21.200–11:23.666
后面呢用三个变量咱们把它取出来
11:24.466–11:25.566
受限的 API key
11:25.566–11:27.066
我已经在 OpenAI 平台上
11:27.066–11:28.700
的 agents 部分创建好了
11:28.766–11:30.066
把它放在本机环境
11:30.066–11:32.466
变量 OPENAI_EXECUTOR_API_KEY 里面
11:33.500–11:34.733
现在启动沙箱
11:34.900–11:36.166
运行 apple-sandbox run
11:36.733–11:38.533
给它 session ID、environment.id
11:38.533–11:39.666
和 remote_url
11:39.733–11:41.800
命令马上返回 session 的名字
11:41.800–11:42.600
容器的 ID
11:42.600–11:43.866
本机的工作目录
11:43.900–11:45.733
还有查看日志的命令
11:46.700–11:47.766
apple-sandbox ls
11:47.766–11:49.900
状态呢应该能看到它是 running
11:50.600–11:52.466
apple-sandbox logs -f
11:52.500–11:53.566
可以跟一下日志
11:53.900–11:55.900
能够看到 exec-server 启动
11:55.900–11:57.366
向远程的 URL
11:57.366–11:59.100
也就是 remote_url 发起了注册
11:59.900–12:02.500
顺便验证一下它真的是一台虚拟机
12:02.733–12:04.300
container exec 进去
12:04.300–12:05.766
执行 uname -a
12:05.766–12:07.466
看到的是 Linux 内核
12:07.466–12:08.900
跑在了一台 Mac 上
12:09.400–12:10.600
给它一个任务吧
12:11.100–12:13.300
先在本机 sandboxes 目录下
12:13.533–12:16.333
这个 session 的子目录里放一个 CSV 文件
12:16.766–12:17.766
内容很简单
12:17.766–12:18.766
4 行数据
12:18.800–12:20.200
item 和 amount 两列
12:20.200–12:21.666
不需要任何上传接口
12:21.666–12:22.566
放进目录里
12:22.566–12:24.533
实际上就放进了虚拟机
12:24.533–12:26.133
里的 /workspace 目录
12:26.666–12:27.766
顺便说一句吧
12:28.533–12:31.333
OpenAI 托管的 sandbox 创建 session 的时候呢
12:31.333–12:33.166
可以用 environment.files 这个
12:33.166–12:35.966
字段内联地把文件带进去
12:36.066–12:38.333
同样的字段在 self_hosted 上
12:38.333–12:40.400
OpenAI 会返回 400
12:40.733–12:43.066
说 environment.files 是个未知参数
12:43.500–12:45.800
因此 self_hosted 类型的文件
12:45.800–12:47.533
进出只能靠你自己去挂载
12:48.266–12:49.200
API 并不支持
12:49.866–12:51.133
然后就提交任务吧
12:51.533–12:53.733
POST 到 session 的 events 接口
12:53.766–12:54.700
事件类型呢
12:54.700–12:56.733
是 agent.session.input.message
12:57.133–13:01.300
内容是用 Python 读 /workspace/amounts.csv
13:01.466–13:03.366
验证 amount 列是数字
13:03.500–13:04.733
算行数和总数
13:04.733–13:06.766
把结果按固定格式写到
13:07.166–13:09.166
/workspace/outputs/summary.json
13:10.100–13:11.966
写完读回来验证报告结果
13:12.866–13:14.866
应该能够看到返回是 202
13:14.966–13:17.400
session 状态从 idle 变成了 in_progress
13:17.700–13:18.366
这时候呢
13:18.366–13:19.666
可以看两个地方
13:19.766–13:21.766
一个是 apple-sandbox 的日志
13:22.333–13:24.000
通过 apple-sandbox logs 命令
13:24.133–13:25.766
能看到执行器收到命令
13:25.766–13:26.566
执行命令
13:26.566–13:28.966
另一边是 Agents API 的事件流
13:29.500–13:30.533
等它跑完
13:30.533–13:33.800
直接用 cat 命令查看本机的
13:33.800–13:36.133
sandboxes 目录下的 summary.json 文件
13:36.466–13:37.266
应该有 4 行
13:37.266–13:38.466
总和呢 75
13:38.566–13:40.466
verified 是标记为了 true
13:40.666–13:42.600
这是 Python 在虚拟机里执行
13:42.700–13:45.266
通过挂载写到本机磁盘上的文件
13:47.200–13:48.300
没有 artifact 下载
13:48.300–13:49.966
也没有轮询任何的接口
13:50.133–13:51.533
self_hosted 的产物
13:51.600–13:55.066
这些 artifact 不会发布到 OpenAI 的 Artifacts 接口
13:55.066–13:56.900
文件呢就会在你自己的目录里
13:58.766–14:00.266
跑完了三步清理
14:00.333–14:02.100
apple-sandbox stop 停掉容器
14:02.266–14:03.966
apple-sandbox rm 删掉容器
14:03.966–14:07.100
再用 DELETE 请求把 OpenAI 那边的 session 删掉
14:07.300–14:09.500
如果不想一个一个地记 session ID
14:10.200–14:11.766
apple-sandbox prune
14:11.766–14:12.566
--older
14:12.766–14:14.100
-than 1h
14:14.700–14:16.333
先加一个 --dry-run 的选项
14:16.333–14:17.533
看看会删哪些
14:17.766–14:19.466
再真正地去删掉它们
14:21.133–14:23.666
这里呢有几点值得注意的 README 呢
14:23.666–14:24.800
也已经写了
14:24.800–14:27.466
第一个出站网络没有做白名单
14:27.566–14:29.166
Apple container 的网络只有两种
14:29.166–14:31.100
完全内网或者完全开放
14:31.300–14:32.966
没有按域名放行的机制
14:33.100–14:35.666
因此现在执行器是全开放出站的
14:35.666–14:38.933
已经确认它需要访问 api.openai.com
14:39.000–14:40.966
注册之后还需不需要别的地址呢
14:40.966–14:41.900
还没有验证
14:41.966–14:44.266
所以不建议拿它跑不受信任的任务
14:44.300–14:46.800
等加了代理或者白名单那一层
14:46.800–14:47.733
咱们再说吧
14:48.166–14:49.466
第二点是只有 CLI
14:49.466–14:50.766
目前呢还没有 SDK
14:51.000–14:52.666
TypeScript 或者 Python 的封装
14:52.666–14:54.700
其实呢就是包一层子进程调用
14:54.700–14:56.700
后面呢可能我们也会继续地添加
14:56.766–14:57.800
感兴趣的朋友呢也
14:57.800–14:59.166
可以来自己封装一下
14:59.500–15:02.266
第三 prune 判断 session 结束靠的是推断
15:02.400–15:04.400
容器停了就代表 session 结束了
15:04.400–15:06.533
这条推断依赖于 exec-server
15:06.800–15:09.600
在 OpenAI 彻底关闭连接之后会自己退出
15:09.733–15:10.566
目前文档呢
15:10.566–15:11.666
是这么描述的
15:12.133–15:13.900
端到端测试当中也没有反例
15:14.500–15:16.866
但连接中到停止这一步的转换
15:16.900–15:18.800
还没有被单独地去观测过
15:20.000–15:22.133
这一期我们把 Agents API 的 self_hosted
15:22.133–15:24.366
环境接到一台 Mac 电脑上
15:24.866–15:27.133
Apple container 提供虚拟机级别的隔离
15:27.366–15:29.800
执行器主动出站连接 OpenAI
15:29.800–15:31.900
本机目录直接就当做工作区
15:32.066–15:33.200
没有公网地址
15:33.300–15:36.733
靠的是 application-managed 正式的启动方式
15:36.900–15:38.733
缺的只是提前收到通知
15:39.700–15:41.766
用 prune 命令我们做了补全
15:42.000–15:43.333
项目呢开源在 GitHub
15:43.333–15:44.300
欢迎大家试用
15:44.300–15:46.500
也欢迎提 issue 欢迎 PR
15:46.566–15:48.200
如果这期视频对你有帮助
15:48.266–15:48.900
欢迎关注
15:48.900–15:50.200
也欢迎一键三连吧
15:50.200–15:51.700
那我们就下期再见
0:01.800–0:03.466
zh大家好我是小木头
大家好我是小木头
0:03.600–0:04.366
zh上一期视频
上一期视频
0:04.366–0:07.000
zh我们用 OpenAI 官方托管的沙箱环境
我们用 OpenAI 官方托管的沙箱环境
0:07.066–0:09.000
zh把 Agents API 跑通了
把 Agents API 跑通了
0:09.000–0:10.133
zh创建了 session
创建了 session
0:10.400–0:11.333
zh跑了命令
跑了命令
0:11.533–0:12.700
zh下载了 artifact
下载了 artifact
0:12.766–0:15.000
zh在任务执行中也加载了 Skill
在任务执行中也加载了 Skill
0:15.733–0:17.466
zh官方托管的沙箱环境
官方托管的沙箱环境
0:17.466–0:18.900
zh只是执行环境的一种
只是执行环境的一种
0:19.066–0:20.533
zhAgents API 还支持另一种
Agents API 还支持另一种
0:20.533–0:22.200
zh环境类型叫 self_hosted
环境类型叫 self_hosted
0:23.666–0:25.066
zh执行器可以跑在咱们
执行器可以跑在咱们
0:25.066–0:26.466
zh自己的基础设施里
自己的基础设施里
0:26.533–0:27.966
zhOpenAI 只负责调度
OpenAI 只负责调度
0:28.266–0:30.166
zh这一期我分享一个刚刚
这一期我分享一个刚刚
0:30.166–0:32.400
zh开源的小项目叫 apple-sandbox
开源的小项目叫 apple-sandbox
0:32.933–0:35.200
zh它用 Apple 官方的 container 工具
它用 Apple 官方的 container 工具
0:35.200–0:37.366
zh在 Mac 上给每个 session 起一个
在 Mac 上给每个 session 起一个
0:37.366–0:39.266
zh独立的轻量级的虚拟机
独立的轻量级的虚拟机
0:39.466–0:42.066
zh把 Agents API 的执行器跑在这里面
把 Agents API 的执行器跑在这里面
0:42.366–0:43.466
zh不需要云账号
不需要云账号
0:43.466–0:44.733
zh不需要公网地址
不需要公网地址
0:44.733–0:45.900
zh也不需要 Docker
也不需要 Docker
0:46.533–0:49.133
zh项目已经做过端到端的验证
项目已经做过端到端的验证
0:49.266–0:52.166
zh在测试中能够看到真实的 session 的创建
在测试中能够看到真实的 session 的创建
0:52.300–0:53.566
zh真实的任务执行
真实的任务执行
0:53.766–0:55.333
zh结果文件也能够直接
结果文件也能够直接
0:55.333–0:57.066
zh落到咱们本机的磁盘上
落到咱们本机的磁盘上
0:58.400–1:00.500
zh今天的内容咱们分三块吧
今天的内容咱们分三块吧
1:00.500–1:01.966
zh先介绍这个项目是什么
先介绍这个项目是什么
1:02.000–1:02.866
zh架构是什么
架构是什么
1:02.900–1:04.733
zh然后重点说说它和 Agents
然后重点说说它和 Agents
1:04.800–1:06.133
zhAPI 之间的连接方式
API 之间的连接方式
1:06.366–1:08.700
zh尤其是没有公网的地址的情况下
尤其是没有公网的地址的情况下
1:08.700–1:10.766
zh它是怎么和 OpenAI 协同工作的
它是怎么和 OpenAI 协同工作的
1:10.800–1:12.966
zh最后用发布版本从安装到
最后用发布版本从安装到
1:12.966–1:14.933
zh跑任务完整的演示一遍
跑任务完整的演示一遍
1:17.666–1:19.733
zhapple-sandbox 是一个用 Go 语言
apple-sandbox 是一个用 Go 语言
1:19.733–1:21.066
zh编写的命令行工具
编写的命令行工具
1:22.066–1:23.300
zh没有常驻进程
没有常驻进程
1:23.400–1:25.066
zh目前呢也没有守护程序
目前呢也没有守护程序
1:25.133–1:26.133
zh它只做一件事情
它只做一件事情
1:26.133–1:29.866
zh把 OpenAI 的 codex exec-server 这个执行器
把 OpenAI 的 codex exec-server 这个执行器
1:29.866–1:31.666
zh跑进一个隔离的虚拟机里
跑进一个隔离的虚拟机里
1:32.000–1:35.066
zh底层用的是 Apple 去年开源的 container 项目
底层用的是 Apple 去年开源的 container 项目
1:35.100–1:37.500
zh它和 Docker 最大的区别是隔离级别
它和 Docker 最大的区别是隔离级别
1:37.866–1:40.800
zhDocker 上所有容器共享一个 Linux 内核
Docker 上所有容器共享一个 Linux 内核
1:41.000–1:42.766
zhApple container 是每个容器单独
Apple container 是每个容器单独
1:42.766–1:44.900
zh起一个轻量级的 Linux 虚拟机
起一个轻量级的 Linux 虚拟机
1:45.100–1:48.400
zh基于 macOS 自带的 Virtualization.framework
基于 macOS 自带的 Virtualization.framework
1:48.866–1:50.866
zh所以在 apple-sandbox 里面
所以在 apple-sandbox 里面
1:50.866–1:53.266
zh一个 session 就是一个 OCI 容器
一个 session 就是一个 OCI 容器
1:53.400–1:55.266
zh也就是一台独立的虚拟机
也就是一台独立的虚拟机
1:55.500–1:58.100
zhagent 在里面装软件跑脚本
agent 在里面装软件跑脚本
1:58.100–1:59.900
zh都不会离开这台虚拟机
都不会离开这台虚拟机
2:00.600–2:02.900
zh从下往上来看看整个结构
从下往上来看看整个结构
2:02.900–2:04.266
zh目前呢是 4 层
目前呢是 4 层
2:04.300–2:06.466
zh最底下是 Apple Silicon 的 Mac
最底下是 Apple Silicon 的 Mac
2:06.666–2:08.100
zh装好 container 工具
装好 container 工具
2:08.700–2:10.600
zh执行 container system start 把
执行 container system start 把
2:10.600–2:11.800
zh它的服务启动起来
它的服务启动起来
2:12.066–2:14.333
zh往上一层是 Apple container 本身
往上一层是 Apple container 本身
2:14.500–2:17.466
zh负责从 OCI 镜像启动虚拟机
负责从 OCI 镜像启动虚拟机
2:17.600–2:19.766
zh再往上是执行器的镜像
再往上是执行器的镜像
2:20.166–2:21.233
zh基于 node 22 的精简版
基于 node 22 的精简版
2:22.900–2:26.166
zh安装了 CA 证书和 codex 这个 npm 包
安装了 CA 证书和 codex 这个 npm 包
2:26.300–2:29.966
zh入口脚本里面执行了 codex exec-server
入口脚本里面执行了 codex exec-server
2:30.500–2:33.666
zh最上面才是 apple-sandbox 这个 CLI
最上面才是 apple-sandbox 这个 CLI
2:33.733–2:35.900
zh它负责创建工作目录
它负责创建工作目录
2:36.100–2:37.200
zh注入环境变量
注入环境变量
2:37.200–2:38.400
zh调用 container run
调用 container run
2:38.966–2:40.000
zh然后呢就退出
然后呢就退出
2:40.000–2:41.666
zh把虚拟机留在后台跑
把虚拟机留在后台跑
2:42.800–2:45.666
zh有一个设计方面的细节需要单独聊聊
有一个设计方面的细节需要单独聊聊
2:45.933–2:48.666
zh每个 session 在本机的 sandboxes
每个 session 在本机的 sandboxes
2:48.666–2:50.166
zh目录下有一个自己的子目录
目录下有一个自己的子目录
2:50.300–2:53.200
zh直接挂载成虚拟机里的 /workspace
直接挂载成虚拟机里的 /workspace
2:53.600–2:54.800
zh所以文件进出不需要
所以文件进出不需要
2:54.800–2:56.400
zh任何上传下载接口
任何上传下载接口
2:56.533–2:58.266
zh往那个目录里放一个文件
往那个目录里放一个文件
2:58.333–2:59.866
zh虚拟机里马上就能看到
虚拟机里马上就能看到
3:00.266–3:01.800
zhagent 写出来的结果
agent 写出来的结果
3:01.800–3:03.666
zh本机磁盘上也能马上读到
本机磁盘上也能马上读到
3:04.133–3:06.266
zh虚拟机是随时可以销毁的
虚拟机是随时可以销毁的
3:06.366–3:08.366
zh磁盘上这个目录才是真正的状态
磁盘上这个目录才是真正的状态
3:08.900–3:10.000
zh命令一共 8 个
命令一共 8 个
3:10.200–3:11.533
zhdoctor 检查环境
doctor 检查环境
3:11.666–3:13.333
zhbuild 构建执行器镜像
build 构建执行器镜像
3:13.533–3:14.900
zhrun 启动一个 session
run 启动一个 session
3:15.133–3:16.066
zhlogs 看日志
logs 看日志
3:16.533–3:17.666
zhls 列出 session
ls 列出 session
3:18.133–3:20.933
zhstop 和 rm 分别是停止以及删除
stop 和 rm 分别是停止以及删除
3:21.800–3:24.666
zh可以用 prune 命令按时间批量
可以用 prune 命令按时间批量
3:24.666–3:26.133
zh地清理已经停掉的 session
地清理已经停掉的 session
3:30.366–3:33.200
zh再来看看它在整个链路里的位置
再来看看它在整个链路里的位置
3:35.166–3:35.733
zh第一步
第一步
3:35.733–3:39.133
zh你的应用用普通的 API key 来调用 Agents API
你的应用用普通的 API key 来调用 Agents API
3:39.700–3:42.166
zh创建一个 self_hosted 类型的 session
创建一个 self_hosted 类型的 session
3:42.766–3:44.333
zh返回里除了 session ID
返回里除了 session ID
3:44.333–3:45.533
zh还有两个关键字段
还有两个关键字段
3:45.900–3:49.133
zhenvironment.id 和一个远端的地址 remote_url
environment.id 和一个远端的地址 remote_url
3:49.200–3:51.133
zh这两个值是给执行器用的
这两个值是给执行器用的
3:52.100–3:54.900
zh第二步在你的 OpenAI 平台上单独
第二步在你的 OpenAI 平台上单独
3:54.900–3:56.800
zh建一个受限的 API key
建一个受限的 API key
3:57.200–4:00.333
zh拿普通的 API key 去注册执行器
拿普通的 API key 去注册执行器
4:00.733–4:02.666
zhAPI 会直接返回 403
API 会直接返回 403
4:03.200–4:04.533
zh通常会看到这么一个错误
通常会看到这么一个错误
4:04.533–4:08.000
zh说缺少 api.agents.environments.connect 这个 scope
说缺少 api.agents.environments.connect 这个 scope
4:08.866–4:11.200
zh第三步是把这个受限的 API key
第三步是把这个受限的 API key
4:11.200–4:13.766
zh放进环境变量 OPENAI_EXECUTOR_API_KEY
放进环境变量 OPENAI_EXECUTOR_API_KEY
4:14.533–4:16.166
zh执行 apple-sandbox run
执行 apple-sandbox run
4:16.566–4:20.000
zh传入 environment.id 和 remote_url
传入 environment.id 和 remote_url
4:20.466–4:22.166
zhCLI 会创建工作目录
CLI 会创建工作目录
4:22.333–4:24.733
zh把这三个值作为环境变量注入
把这三个值作为环境变量注入
4:25.400–4:27.766
zh调用 container run 启动一台虚拟机
调用 container run 启动一台虚拟机
4:28.200–4:30.533
zh虚拟机里的入口脚本把受限
虚拟机里的入口脚本把受限
4:30.533–4:33.500
zh的 API key 转成 codex exec-server
的 API key 转成 codex exec-server
4:33.500–4:36.200
zh自己要求的变量名 CODEX_API_KEY
自己要求的变量名 CODEX_API_KEY
4:36.200–4:38.133
zh然后呢启动 exec-server
然后呢启动 exec-server
4:39.933–4:42.700
zh第四步 exec-server 用这个 API key 主动向
第四步 exec-server 用这个 API key 主动向
4:42.700–4:45.266
zhremote_url 发起一个 WebSocket 连接
remote_url 发起一个 WebSocket 连接
4:45.366–4:46.166
zh完成注册
完成注册
4:46.800–4:49.500
zhOpenAI 那边把 session 标记成活跃
OpenAI 那边把 session 标记成活跃
4:49.866–4:52.966
zh之后每一轮任务都是 OpenAI 那边发命令
之后每一轮任务都是 OpenAI 那边发命令
4:53.100–4:55.500
zh虚拟机在 /workspace 里执行
虚拟机在 /workspace 里执行
4:55.533–4:56.766
zh结果呢再回传
结果呢再回传
4:57.566–4:59.600
zh你的应用通过 Agents API 提交任务
你的应用通过 Agents API 提交任务
4:59.700–5:00.933
zh订阅事件流
订阅事件流
5:01.066–5:04.766
zh看到的和 OpenAI 托管的 sandbox 完全一样
看到的和 OpenAI 托管的 sandbox 完全一样
5:05.000–5:06.066
zh整个过程里
整个过程里
5:06.133–5:08.900
zhapple-sandbox 从头到尾没有和 OpenAI 通信
apple-sandbox 从头到尾没有和 OpenAI 通信
5:08.966–5:10.933
zh它只负责把三个参数变成
它只负责把三个参数变成
5:10.933–5:12.933
zh一台跑着执行器的虚拟机
一台跑着执行器的虚拟机
5:12.933–5:14.300
zh然后呢就退出了
然后呢就退出了
5:17.400–5:20.333
zh这段是这期里面我想再聊聊的一部分
这段是这期里面我想再聊聊的一部分
5:20.466–5:22.166
zh首先看托管的 sandbox
首先看托管的 sandbox
5:22.300–5:23.200
zh什么都不用管
什么都不用管
5:23.200–5:24.566
zhOpenAI 自己起环境
OpenAI 自己起环境
5:25.400–5:27.700
zh如果大家看过 Cloudflare 官方
如果大家看过 Cloudflare 官方
5:27.700–5:29.866
zh给 Agents API 写的参考实现
给 Agents API 写的参考实现
5:30.300–5:32.533
zh它是云端部署的典型形态
它是云端部署的典型形态
5:32.566–5:33.866
zh你部署一个 worker
你部署一个 worker
5:34.000–5:35.333
zh公网地址有了
公网地址有了
5:35.600–5:37.766
zhOpenAI 通过带签名的 webhook
OpenAI 通过带签名的 webhook
5:37.800–5:39.600
zh把 session 的事件推送
把 session 的事件推送
5:42.166–5:43.533
zh有任务要执行
有任务要执行
5:43.800–5:44.733
zh状态空闲
状态空闲
5:45.000–5:46.100
zh执行失败等等
执行失败等等
5:46.100–5:48.533
zh这些呢都是事件会推送过来的
这些呢都是事件会推送过来的
5:48.866–5:51.900
zh而 worker 收到事件再去启动或唤醒容器
而 worker 收到事件再去启动或唤醒容器
5:52.000–5:55.666
zh容器里的执行器向 OpenAI 建立 WebSocket 连接
容器里的执行器向 OpenAI 建立 WebSocket 连接
5:56.133–5:57.766
zh也就是说在那套方案里面
也就是说在那套方案里面
5:57.766–6:00.366
zhwebhook 和 WebSocket 各管一件事
webhook 和 WebSocket 各管一件事
6:00.933–6:03.366
zhWebSocket 是执行器和 OpenAI 之间
WebSocket 是执行器和 OpenAI 之间
6:03.366–6:05.533
zh真正的交换命令和结果
真正的交换命令和结果
6:06.866–6:08.800
zh永远都是从你这边主动出站
永远都是从你这边主动出站
6:08.800–6:10.733
zh而 webhook 只解决一个问题
而 webhook 只解决一个问题
6:10.866–6:12.133
zhOpenAI 什么时候需要算力
OpenAI 什么时候需要算力
6:12.133–6:14.066
zh怎么通知你把容器启动起来
怎么通知你把容器启动起来
6:14.966–6:17.800
zh而 apple-sandbox 跑在一台个人的 Mac 电脑上
而 apple-sandbox 跑在一台个人的 Mac 电脑上
6:17.800–6:18.966
zh没有公网地址
没有公网地址
6:19.000–6:20.566
zh也就没法给 OpenAI 提供
也就没法给 OpenAI 提供
6:20.566–6:22.266
zh一个接收 webhook 的地址
一个接收 webhook 的地址
6:22.266–6:23.266
zh那怎么办呢
那怎么办呢
6:23.866–6:26.166
zh答案其实也在 OpenAI 自己文档里
答案其实也在 OpenAI 自己文档里
6:26.700–6:27.966
zhself_hosted 环境
self_hosted 环境
6:27.966–6:30.266
zh也就咱们这种自己运行的沙箱环境
也就咱们这种自己运行的沙箱环境
6:30.466–6:31.933
zh有两种同样都可以
有两种同样都可以
6:31.933–6:33.600
zh认为是正式的启动方式
认为是正式的启动方式
6:33.866–6:35.666
zh一种叫 webhook-managed
一种叫 webhook-managed
6:36.000–6:38.766
zh就是刚才咱们谈到的像 Cloudflare 那种
就是刚才咱们谈到的像 Cloudflare 那种
6:38.900–6:40.366
zhOpenAI 决定什么时候要算力
OpenAI 决定什么时候要算力
6:40.666–6:41.766
zh推送事件给你
推送事件给你
6:42.533–6:44.300
zh另一种呢叫 application-managed
另一种呢叫 application-managed
6:44.500–6:46.966
zh你的应用创建 session 的那一刻
你的应用创建 session 的那一刻
6:47.066–6:48.733
zh就已经拿到了环境的 ID
就已经拿到了环境的 ID
6:48.733–6:51.066
zh也就是 environment.id 和 remote_url
也就是 environment.id 和 remote_url
6:51.733–6:53.900
zh直接同步把算力呢启动起来
直接同步把算力呢启动起来
6:53.900–6:55.533
zh不需要等待 OpenAI 的通知
不需要等待 OpenAI 的通知
6:56.533–7:00.000
zhCloudflare、Vercel、Modal 的集成文档都把这两种
Cloudflare、Vercel、Modal 的集成文档都把这两种
7:00.566–7:01.933
zh并列成为选项
并列成为选项
7:01.966–7:03.300
zh而不是主备关系
而不是主备关系
7:04.300–7:06.700
zhapple-sandbox 实现的正是 application-managed
apple-sandbox 实现的正是 application-managed
7:07.066–7:07.866
zh创建 session
创建 session
7:07.900–7:08.933
zh拿到两个参数
拿到两个参数
7:08.933–7:11.000
zh立即执行 apple-sandbox run
立即执行 apple-sandbox run
7:11.066–7:12.500
zh三步呢是在一起的
三步呢是在一起的
7:13.000–7:15.733
zhwebhook 那条路解决的是规模化
webhook 那条路解决的是规模化
7:15.733–7:17.700
zh场景下按需启动算力的问题
场景下按需启动算力的问题
7:18.066–7:19.100
zh一个开发者呢
一个开发者呢
7:19.100–7:20.400
zh在自己的 Mac 上跑
在自己的 Mac 上跑
7:20.400–7:23.100
zh其实呢并没有很具体的这方面的问题
其实呢并没有很具体的这方面的问题
7:23.900–7:25.266
zh真正的数据通道
真正的数据通道
7:25.600–7:26.600
zh两种方式是一样的
两种方式是一样的
7:26.600–7:28.333
zh都是执行器主动出站
都是执行器主动出站
7:28.866–7:30.533
zh发起一个 WebSocket 连接
发起一个 WebSocket 连接
7:30.600–7:31.866
zh连接断了自己重连
连接断了自己重连
7:32.133–7:34.866
zhOpenAI 从来不需要反向地连你的机器
OpenAI 从来不需要反向地连你的机器
7:35.066–7:36.533
zh所以少了公网地址
所以少了公网地址
7:36.566–7:38.766
zh少的只是提前收通知这件事
少的只是提前收通知这件事
7:39.166–7:41.733
zh那少了通知还剩一件事情要自己处理
那少了通知还剩一件事情要自己处理
7:41.966–7:43.133
zhsession 什么时候结束
session 什么时候结束
7:43.300–7:44.700
zh虚拟机什么时候该销毁
虚拟机什么时候该销毁
7:45.400–7:47.400
zhapple-sandbox 没有去轮询 OpenAI
apple-sandbox 没有去轮询 OpenAI
7:47.400–7:48.366
zh也并不需要
也并不需要
7:48.900–7:50.300
zhexec-server 有个特性
exec-server 有个特性
7:50.500–7:52.266
zh连接临时断开它会重连
连接临时断开它会重连
7:52.266–7:54.366
zh但 OpenAI 彻底关闭 session 之后
但 OpenAI 彻底关闭 session 之后
7:54.366–7:55.333
zh它就退出了
它就退出了
7:55.500–7:56.300
zh容器随之呢
容器随之呢
7:56.300–7:57.700
zh就进入了 stopped 状态
就进入了 stopped 状态
7:57.900–7:59.733
zh所以 prune 命令只看一件事
所以 prune 命令只看一件事
7:59.766–8:01.000
zh哪些容器已经停了
哪些容器已经停了
8:01.000–8:01.800
zh停了多久
停了多久
8:02.066–8:03.800
enprune --older-than 1h
prune --older-than 1h
8:04.000–8:05.866
zh把停掉超过一小时的清理掉
把停掉超过一小时的清理掉
8:06.300–8:08.400
zh手动跑或者挂一个 cron 都行
手动跑或者挂一个 cron 都行
8:08.500–8:10.566
zh它永远呢不会碰正在运行的容器
它永远呢不会碰正在运行的容器
8:11.533–8:14.133
zh当然了目前 apple-sandbox 还处于非常早期
当然了目前 apple-sandbox 还处于非常早期
8:14.133–8:15.500
zh在后续的开发中还会
在后续的开发中还会
8:15.500–8:16.966
zh逐渐完善这方面的功能
逐渐完善这方面的功能
8:16.966–8:18.166
zh使它越来越健壮
使它越来越健壮
8:20.400–8:22.400
zh把上面的机制串成状态图
把上面的机制串成状态图
8:22.400–8:24.266
zh一个 session 在沙箱里经历
一个 session 在沙箱里经历
8:24.266–8:25.700
zh的状态大致是这样的
的状态大致是这样的
8:26.200–8:27.600
zh执行 apple-sandbox run
执行 apple-sandbox run
8:27.733–8:28.766
zh容器被创建出来
容器被创建出来
8:28.766–8:30.133
zh这是 Created 状态
这是 Created 状态
8:30.500–8:32.466
zh入口脚本启动 exec-server
入口脚本启动 exec-server
8:32.466–8:33.733
zh向 OpenAI 注册
向 OpenAI 注册
8:33.800–8:35.900
zh进入 Registering 状态
进入 Registering 状态
8:36.000–8:37.933
zh注册呢有两个分支
注册呢有两个分支
8:38.400–8:40.333
zhremote_url 或者 key 不对
remote_url 或者 key 不对
8:40.333–8:41.766
zhexec-server 立即就拒绝
exec-server 立即就拒绝
8:41.766–8:42.800
zh进入 Rejected
进入 Rejected
8:43.000–8:44.300
zh容器随即就停掉
容器随即就停掉
8:46.866–8:47.466
zh错误信息呢
错误信息呢
8:47.466–8:49.400
zh可以在 apple-sandbox logs 里看到
可以在 apple-sandbox logs 里看到
8:49.533–8:51.533
zh注册通过进入连接状态 Connected
注册通过进入连接状态 Connected
8:51.800–8:53.533
zh之后 OpenAI 每下发一条
之后 OpenAI 每下发一条
8:53.533–8:54.900
zh命令就进入 Executing
命令就进入 Executing
8:55.133–8:56.300
zh执行完回到 Connected
执行完回到 Connected
8:56.700–8:58.366
zh中间 WebSocket 掉线
中间 WebSocket 掉线
8:58.366–8:59.500
zh进入 Reconnecting
进入 Reconnecting
9:00.100–9:01.500
zhexec-server 自己重连
exec-server 自己重连
9:01.533–9:02.933
zhapple-sandbox 并不参与
apple-sandbox 并不参与
9:03.266–9:04.666
zhOpenAI 彻底关闭 session
OpenAI 彻底关闭 session
9:05.066–9:06.400
zhexec-server 退出
exec-server 退出
9:06.400–9:08.166
zh容器呢就进入 stopped 状态
容器呢就进入 stopped 状态
9:08.366–9:10.866
zh最后通过 rm 或者 prune 把它删掉
最后通过 rm 或者 prune 把它删掉
9:10.933–9:12.666
zh最终达到了 Removed 状态
最终达到了 Removed 状态
9:13.400–9:14.666
zh从 CLI 的角度看
从 CLI 的角度看
9:14.666–9:16.100
zhls 只报两种状态
ls 只报两种状态
9:16.133–9:17.066
zhrunning 和 stopped
running 和 stopped
9:17.400–9:19.966
zh中间那些状态全都在虚拟机里面发生
中间那些状态全都在虚拟机里面发生
9:21.333–9:23.500
zh下面我们用发布版本完整跑一遍
下面我们用发布版本完整跑一遍
9:23.566–9:26.966
zh项目在 GitHub 上 sugarforever/apple-sandbox
项目在 GitHub 上 sugarforever/apple-sandbox
9:27.200–9:28.566
zh在发布版本这里
在发布版本这里
9:28.566–9:31.600
zhreleases 有一个 darwin_arm64 的压缩包
releases 有一个 darwin_arm64 的压缩包
9:31.666–9:32.533
zh只有这个平台
只有这个平台
9:32.533–9:33.733
zh因为 Apple container 呢
因为 Apple container 呢
9:33.733–9:35.600
zh只能在 Apple Silicon 的 Mac 上跑
只能在 Apple Silicon 的 Mac 上跑
9:36.300–9:38.700
zh前置条件只有一个 brew install --cask container
前置条件只有一个 brew install --cask container
9:38.733–9:40.700
zh装好 Apple 的 container 工具
装好 Apple 的 container 工具
9:40.966–9:43.366
zh然后按照 README 里的一条命令安装
然后按照 README 里的一条命令安装
9:43.533–9:46.666
zh从 GitHub Releases 拉取最新的压缩包
从 GitHub Releases 拉取最新的压缩包
9:46.700–9:48.100
zh把二进制解压到用户
把二进制解压到用户
9:48.100–9:50.066
zh目录下的 .local/bin
目录下的 .local/bin
9:50.200–9:51.300
zh并不需要 sudo
并不需要 sudo
9:51.766–9:53.866
zh执行器镜像的 Dockerfile 和入口
执行器镜像的 Dockerfile 和入口
9:53.866–9:55.600
zh脚本已经嵌在了二进制里
脚本已经嵌在了二进制里
9:55.600–9:57.500
zh所以不需要克隆代码仓库
所以不需要克隆代码仓库
9:57.500–9:59.533
zh在任何一个空目录里都能操作
在任何一个空目录里都能操作
10:01.333–10:03.500
zh装好后先跑 apple-sandbox doctor
装好后先跑 apple-sandbox doctor
10:03.700–10:04.766
zh做 5 项检查
做 5 项检查
10:04.900–10:06.500
zh包含了像 Apple Silicon
包含了像 Apple Silicon
10:06.800–10:08.800
zhHypervisor 支持 container 命令
Hypervisor 支持 container 命令
10:08.866–10:10.066
zhcontainer 的服务状态
container 的服务状态
10:10.066–10:10.900
zh磁盘空间
磁盘空间
10:10.966–10:12.566
zh全部 PASS 就可以了
全部 PASS 就可以了
10:12.900–10:14.566
zh如果服务状态检查失败
如果服务状态检查失败
10:14.566–10:17.366
zh那么尝试运行 container system start 启动一下
那么尝试运行 container system start 启动一下
10:17.366–10:18.266
zh再跑一次
再跑一次
10:18.666–10:22.000
zh然后 apple-sandbox build 来构建执行器的镜像
然后 apple-sandbox build 来构建执行器的镜像
10:22.100–10:23.966
zh这一步要拉取 node 镜像
这一步要拉取 node 镜像
10:24.133–10:25.966
zh装 CA 证书装 codex
装 CA 证书装 codex
10:26.133–10:27.700
zh第一次呢会花一点时间
第一次呢会花一点时间
10:29.466–10:30.466
zh镜像准备好了
镜像准备好了
10:30.466–10:31.700
zh现在呢创建一个 session
现在呢创建一个 session
10:32.200–10:35.266
zh向 /v1/agents/sessions 发一个 POST 请求
向 /v1/agents/sessions 发一个 POST 请求
10:35.366–10:37.966
zh请求的头带上 agents=v1 这个
请求的头带上 agents=v1 这个
10:37.966–10:40.666
zhbeta 标记以及 OpenAI 的 API key
beta 标记以及 OpenAI 的 API key
10:40.966–10:43.200
zh请求体里面带上 agent 和 session
请求体里面带上 agent 和 session
10:43.666–10:45.933
zh模型我选择了 gpt-6-astra
模型我选择了 gpt-6-astra
10:46.100–10:48.700
zh系统指令是让它在 /workspace 里工作
系统指令是让它在 /workspace 里工作
10:48.700–10:50.766
zh每一步跑了什么命令都展示出来
每一步跑了什么命令都展示出来
10:50.800–10:53.466
zh生成的文件在结束前自己检查一遍
生成的文件在结束前自己检查一遍
10:53.866–10:55.933
zhenvironment 指定类型是 self_hosted
environment 指定类型是 self_hosted
10:55.966–10:57.700
zh工作目录是 /workspace
工作目录是 /workspace
10:58.466–11:00.266
zhAPI 响应应该是 201
API 响应应该是 201
11:00.400–11:01.466
zh来看看三个字段
来看看三个字段
11:01.466–11:02.600
zh一个呢是 ID
一个呢是 ID
11:03.100–11:05.266
zhID 是 sess 开头的一个 session ID
ID 是 sess 开头的一个 session ID
11:06.666–11:08.500
zh另外一个是 environment.id
另外一个是 environment.id
11:08.666–11:10.766
zh还包含一个 remote_url
还包含一个 remote_url
11:10.966–11:13.366
zh它是 api.openai.com 下面
它是 api.openai.com 下面
11:13.366–11:14.866
zh一个 connect 路径的地址
一个 connect 路径的地址
11:15.966–11:18.000
zh这两个值原样地传给沙箱就行
这两个值原样地传给沙箱就行
11:18.000–11:19.000
zh并不需要改动
并不需要改动
11:19.066–11:21.133
zh把响应存到 session.json
把响应存到 session.json
11:21.200–11:23.666
zh后面呢用三个变量咱们把它取出来
后面呢用三个变量咱们把它取出来
11:24.466–11:25.566
zh受限的 API key
受限的 API key
11:25.566–11:27.066
zh我已经在 OpenAI 平台上
我已经在 OpenAI 平台上
11:27.066–11:28.700
zh的 agents 部分创建好了
的 agents 部分创建好了
11:28.766–11:30.066
zh把它放在本机环境
把它放在本机环境
11:30.066–11:32.466
zh变量 OPENAI_EXECUTOR_API_KEY 里面
变量 OPENAI_EXECUTOR_API_KEY 里面
11:33.500–11:34.733
zh现在启动沙箱
现在启动沙箱
11:34.900–11:36.166
zh运行 apple-sandbox run
运行 apple-sandbox run
11:36.733–11:38.533
zh给它 session ID、environment.id
给它 session ID、environment.id
11:38.533–11:39.666
zh和 remote_url
和 remote_url
11:39.733–11:41.800
zh命令马上返回 session 的名字
命令马上返回 session 的名字
11:41.800–11:42.600
zh容器的 ID
容器的 ID
11:42.600–11:43.866
zh本机的工作目录
本机的工作目录
11:43.900–11:45.733
zh还有查看日志的命令
还有查看日志的命令
11:46.700–11:47.766
enapple-sandbox ls
apple-sandbox ls
11:47.766–11:49.900
zh状态呢应该能看到它是 running
状态呢应该能看到它是 running
11:50.600–11:52.466
enapple-sandbox logs -f
apple-sandbox logs -f
11:52.500–11:53.566
zh可以跟一下日志
可以跟一下日志
11:53.900–11:55.900
zh能够看到 exec-server 启动
能够看到 exec-server 启动
11:55.900–11:57.366
zh向远程的 URL
向远程的 URL
11:57.366–11:59.100
zh也就是 remote_url 发起了注册
也就是 remote_url 发起了注册
11:59.900–12:02.500
zh顺便验证一下它真的是一台虚拟机
顺便验证一下它真的是一台虚拟机
12:02.733–12:04.300
zhcontainer exec 进去
container exec 进去
12:04.300–12:05.766
zh执行 uname -a
执行 uname -a
12:05.766–12:07.466
zh看到的是 Linux 内核
看到的是 Linux 内核
12:07.466–12:08.900
zh跑在了一台 Mac 上
跑在了一台 Mac 上
12:09.400–12:10.600
zh给它一个任务吧
给它一个任务吧
12:11.100–12:13.300
zh先在本机 sandboxes 目录下
先在本机 sandboxes 目录下
12:13.533–12:16.333
zh这个 session 的子目录里放一个 CSV 文件
这个 session 的子目录里放一个 CSV 文件
12:16.766–12:17.766
zh内容很简单
内容很简单
12:17.766–12:18.766
zh4 行数据
4 行数据
12:18.800–12:20.200
zhitem 和 amount 两列
item 和 amount 两列
12:20.200–12:21.666
zh不需要任何上传接口
不需要任何上传接口
12:21.666–12:22.566
zh放进目录里
放进目录里
12:22.566–12:24.533
zh实际上就放进了虚拟机
实际上就放进了虚拟机
12:24.533–12:26.133
zh里的 /workspace 目录
里的 /workspace 目录
12:26.666–12:27.766
zh顺便说一句吧
顺便说一句吧
12:28.533–12:31.333
zhOpenAI 托管的 sandbox 创建 session 的时候呢
OpenAI 托管的 sandbox 创建 session 的时候呢
12:31.333–12:33.166
zh可以用 environment.files 这个
可以用 environment.files 这个
12:33.166–12:35.966
zh字段内联地把文件带进去
字段内联地把文件带进去
12:36.066–12:38.333
zh同样的字段在 self_hosted 上
同样的字段在 self_hosted 上
12:38.333–12:40.400
zhOpenAI 会返回 400
OpenAI 会返回 400
12:40.733–12:43.066
zh说 environment.files 是个未知参数
说 environment.files 是个未知参数
12:43.500–12:45.800
zh因此 self_hosted 类型的文件
因此 self_hosted 类型的文件
12:45.800–12:47.533
zh进出只能靠你自己去挂载
进出只能靠你自己去挂载
12:48.266–12:49.200
zhAPI 并不支持
API 并不支持
12:49.866–12:51.133
zh然后就提交任务吧
然后就提交任务吧
12:51.533–12:53.733
zhPOST 到 session 的 events 接口
POST 到 session 的 events 接口
12:53.766–12:54.700
zh事件类型呢
事件类型呢
12:54.700–12:56.733
zh是 agent.session.input.message
是 agent.session.input.message
12:57.133–13:01.300
zh内容是用 Python 读 /workspace/amounts.csv
内容是用 Python 读 /workspace/amounts.csv
13:01.466–13:03.366
zh验证 amount 列是数字
验证 amount 列是数字
13:03.500–13:04.733
zh算行数和总数
算行数和总数
13:04.733–13:06.766
zh把结果按固定格式写到
把结果按固定格式写到
13:07.166–13:09.166
en/workspace/outputs/summary.json
/workspace/outputs/summary.json
13:10.100–13:11.966
zh写完读回来验证报告结果
写完读回来验证报告结果
13:12.866–13:14.866
zh应该能够看到返回是 202
应该能够看到返回是 202
13:14.966–13:17.400
zhsession 状态从 idle 变成了 in_progress
session 状态从 idle 变成了 in_progress
13:17.700–13:18.366
zh这时候呢
这时候呢
13:18.366–13:19.666
zh可以看两个地方
可以看两个地方
13:19.766–13:21.766
zh一个是 apple-sandbox 的日志
一个是 apple-sandbox 的日志
13:22.333–13:24.000
zh通过 apple-sandbox logs 命令
通过 apple-sandbox logs 命令
13:24.133–13:25.766
zh能看到执行器收到命令
能看到执行器收到命令
13:25.766–13:26.566
zh执行命令
执行命令
13:26.566–13:28.966
zh另一边是 Agents API 的事件流
另一边是 Agents API 的事件流
13:29.500–13:30.533
zh等它跑完
等它跑完
13:30.533–13:33.800
zh直接用 cat 命令查看本机的
直接用 cat 命令查看本机的
13:33.800–13:36.133
zhsandboxes 目录下的 summary.json 文件
sandboxes 目录下的 summary.json 文件
13:36.466–13:37.266
zh应该有 4 行
应该有 4 行
13:37.266–13:38.466
zh总和呢 75
总和呢 75
13:38.566–13:40.466
zhverified 是标记为了 true
verified 是标记为了 true
13:40.666–13:42.600
zh这是 Python 在虚拟机里执行
这是 Python 在虚拟机里执行
13:42.700–13:45.266
zh通过挂载写到本机磁盘上的文件
通过挂载写到本机磁盘上的文件
13:47.200–13:48.300
zh没有 artifact 下载
没有 artifact 下载
13:48.300–13:49.966
zh也没有轮询任何的接口
也没有轮询任何的接口
13:50.133–13:51.533
zhself_hosted 的产物
self_hosted 的产物
13:51.600–13:55.066
zh这些 artifact 不会发布到 OpenAI 的 Artifacts 接口
这些 artifact 不会发布到 OpenAI 的 Artifacts 接口
13:55.066–13:56.900
zh文件呢就会在你自己的目录里
文件呢就会在你自己的目录里
13:58.766–14:00.266
zh跑完了三步清理
跑完了三步清理
14:00.333–14:02.100
zhapple-sandbox stop 停掉容器
apple-sandbox stop 停掉容器
14:02.266–14:03.966
zhapple-sandbox rm 删掉容器
apple-sandbox rm 删掉容器
14:03.966–14:07.100
zh再用 DELETE 请求把 OpenAI 那边的 session 删掉
再用 DELETE 请求把 OpenAI 那边的 session 删掉
14:07.300–14:09.500
zh如果不想一个一个地记 session ID
如果不想一个一个地记 session ID
14:10.200–14:11.766
enapple-sandbox prune
apple-sandbox prune
14:11.766–14:12.566
en--older
--older
14:12.766–14:14.100
en-than 1h
-than 1h
14:14.700–14:16.333
zh先加一个 --dry-run 的选项
先加一个 --dry-run 的选项
14:16.333–14:17.533
zh看看会删哪些
看看会删哪些
14:17.766–14:19.466
zh再真正地去删掉它们
再真正地去删掉它们
14:21.133–14:23.666
zh这里呢有几点值得注意的 README 呢
这里呢有几点值得注意的 README 呢
14:23.666–14:24.800
zh也已经写了
也已经写了
14:24.800–14:27.466
zh第一个出站网络没有做白名单
第一个出站网络没有做白名单
14:27.566–14:29.166
zhApple container 的网络只有两种
Apple container 的网络只有两种
14:29.166–14:31.100
zh完全内网或者完全开放
完全内网或者完全开放
14:31.300–14:32.966
zh没有按域名放行的机制
没有按域名放行的机制
14:33.100–14:35.666
zh因此现在执行器是全开放出站的
因此现在执行器是全开放出站的
14:35.666–14:38.933
zh已经确认它需要访问 api.openai.com
已经确认它需要访问 api.openai.com
14:39.000–14:40.966
zh注册之后还需不需要别的地址呢
注册之后还需不需要别的地址呢
14:40.966–14:41.900
zh还没有验证
还没有验证
14:41.966–14:44.266
zh所以不建议拿它跑不受信任的任务
所以不建议拿它跑不受信任的任务
14:44.300–14:46.800
zh等加了代理或者白名单那一层
等加了代理或者白名单那一层
14:46.800–14:47.733
zh咱们再说吧
咱们再说吧
14:48.166–14:49.466
zh第二点是只有 CLI
第二点是只有 CLI
14:49.466–14:50.766
zh目前呢还没有 SDK
目前呢还没有 SDK
14:51.000–14:52.666
zhTypeScript 或者 Python 的封装
TypeScript 或者 Python 的封装
14:52.666–14:54.700
zh其实呢就是包一层子进程调用
其实呢就是包一层子进程调用
14:54.700–14:56.700
zh后面呢可能我们也会继续地添加
后面呢可能我们也会继续地添加
14:56.766–14:57.800
zh感兴趣的朋友呢也
感兴趣的朋友呢也
14:57.800–14:59.166
zh可以来自己封装一下
可以来自己封装一下
14:59.500–15:02.266
zh第三 prune 判断 session 结束靠的是推断
第三 prune 判断 session 结束靠的是推断
15:02.400–15:04.400
zh容器停了就代表 session 结束了
容器停了就代表 session 结束了
15:04.400–15:06.533
zh这条推断依赖于 exec-server
这条推断依赖于 exec-server
15:06.800–15:09.600
zh在 OpenAI 彻底关闭连接之后会自己退出
在 OpenAI 彻底关闭连接之后会自己退出
15:09.733–15:10.566
zh目前文档呢
目前文档呢
15:10.566–15:11.666
zh是这么描述的
是这么描述的
15:12.133–15:13.900
zh端到端测试当中也没有反例
端到端测试当中也没有反例
15:14.500–15:16.866
zh但连接中到停止这一步的转换
但连接中到停止这一步的转换
15:16.900–15:18.800
zh还没有被单独地去观测过
还没有被单独地去观测过
15:20.000–15:22.133
zh这一期我们把 Agents API 的 self_hosted
这一期我们把 Agents API 的 self_hosted
15:22.133–15:24.366
zh环境接到一台 Mac 电脑上
环境接到一台 Mac 电脑上
15:24.866–15:27.133
zhApple container 提供虚拟机级别的隔离
Apple container 提供虚拟机级别的隔离
15:27.366–15:29.800
zh执行器主动出站连接 OpenAI
执行器主动出站连接 OpenAI
15:29.800–15:31.900
zh本机目录直接就当做工作区
本机目录直接就当做工作区
15:32.066–15:33.200
zh没有公网地址
没有公网地址
15:33.300–15:36.733
zh靠的是 application-managed 正式的启动方式
靠的是 application-managed 正式的启动方式
15:36.900–15:38.733
zh缺的只是提前收到通知
缺的只是提前收到通知
15:39.700–15:41.766
zh用 prune 命令我们做了补全
用 prune 命令我们做了补全
15:42.000–15:43.333
zh项目呢开源在 GitHub
项目呢开源在 GitHub
15:43.333–15:44.300
zh欢迎大家试用
欢迎大家试用
15:44.300–15:46.500
zh也欢迎提 issue 欢迎 PR
也欢迎提 issue 欢迎 PR
15:46.566–15:48.200
zh如果这期视频对你有帮助
如果这期视频对你有帮助
15:48.266–15:48.900
zh欢迎关注
欢迎关注
15:48.900–15:50.200
zh也欢迎一键三连吧
也欢迎一键三连吧
15:50.200–15:51.700
zh那我们就下期再见
那我们就下期再见

影片筆記:在 MacOS 上给 OpenAI Agents API 接一个沙箱:apple-sandbox 开源实测

<!-- VIDEONOTE_PUBLICATION_WARNINGS_BEGIN -->

⚠️ 可先使用,仍有待修正項目

本筆記已完成可閱讀的逐字稿與時間跳轉;下列為待處理問題,建議播放原片確認。

  • 待修正:逐字稿全文快速校讀未完成(已保留自動字幕/原始逐字稿並繼續生成筆記。;未完成的全文速讀項目可在精進模式回聽確認。)

<!-- VIDEONOTE_PUBLICATION_WARNINGS_END -->

一句話總結

影片介紹並實測了開源專案 apple-sandbox,該專案利用 Apple 開源的 container 工具與 Virtualization.framework,在本地 Mac 電腦上為 OpenAI Agents API 的 self_hosted 模式啟動獨立的輕量級 Linux 虛擬機,實現了無需雲端帳號、公网地址或 Docker 的本地隔離執行環境。

核心重點

  1. 本地隔離架構:apple-sandbox 基於 Apple Silicon Mac 的 Virtualization.framework,為每個 Session 啟動獨立的輕量級 Linux 虛擬機,而非像 Docker 那樣共享內核。
  2. Application-managed 模式:採用此啟動方式,應用端主動創建 Session 並啟動算力,執行器(exec-server)主動通過 WebSocket 出站連接 OpenAI,無需 OpenAI 推送 Webhook 通知。
  3. 無縫文件存取:透過目錄掛載機制,本機 sandboxes 目錄下的文件直接映射為虛擬機內的 /workspace,實現本機寫入即虛擬機讀取,無需額外的上傳下載接口。
  4. 資源清理機制:不依賴輪詢 OpenAI 狀態,而是依賴 exec-server 在連接關閉後自動退出的特性,透過 prune 命令清理已停止的容器。
  5. 安全與限制:目前僅支援 Apple Silicon Mac,出站網路未做白名單限制,且目前僅提供 CLI 工具,無官方 SDK。

詳細大綱

一、 專案介紹與核心架構

  • 專案目標:讓 OpenAI 的 Agents API 支援 self_hosted(自託管)執行環境。
  • 技術基礎:
  • 使用 Apple 官方開源的 container 工具。
  • 基於 macOS 的 Virtualization.framework。
  • 與 Docker 的區別:Docker 共享 Linux 內核,Apple container 為每個容器啟動獨立的輕量級 Linux 虛擬機。
  • 架構分層:
  1. 硬體層:Apple Silicon Mac。
  2. 系統層:Apple container 服務(透過 container system start 啟動)。
  3. 執行器層:基於 Node 22 的精簡版鏡像,安裝 CA 證書與 codex npm 包,執行 codex exec-server。
  4. CLI 層:apple-sandbox 命令行工具(Go 語言編寫,無常駐進程/守護程序)。
  • 目錄掛載機制:
  • 本機 sandboxes 目錄下的 Session 子目錄直接掛載為虛擬機內的 /workspace。
  • 文件進出無需上傳/下載接口,本機寫入即為虛擬機讀取,虛擬機寫入即為本機可讀。

二、 與 OpenAI Agents API 的協作機制

  • 啟動模式對比:
  • Webhook-managed:典型雲端部署(如 Cloudflare),OpenAI 透過帶簽名的 Webhook 推送事件通知啟動算力,執行器再建立 WebSocket 連接。
  • Application-managed:apple-sandbox 採用此模式。應用創建 Session 時直接獲取 environment.id 和 remote_url,同步啟動算力,無需等待 OpenAI 通知。
  • 連接流程:
  1. 應用使用普通 API Key 創建 self_hosted 類型 Session,獲取 session ID、environment.id 和 remote_url。
  2. 在 OpenAI 平台創建受限的 API Key,用於註冊執行器。
  3. 執行 apple-sandbox run,將受限 API Key 注入環境變數 OPENAI_EXECUTOR_API_KEY。
  4. 虛擬機內的入口腳本將受限 Key 轉換為 CODEX_API_KEY 並啟動 exec-server。
  5. exec-server 使用 API Key 主動向 remote_url 發起 WebSocket 連接完成註冊。
  6. OpenAI 標記 Session 為活躍,後續任務由 OpenAI 下發命令,虛擬機在 /workspace 執行並回傳結果。
  • 關鍵特性:
  • apple-sandbox CLI 本身不與 OpenAI 通信,僅負責啟動虛擬機。
  • 數據通道始終為執行器主動出站(Outbound),OpenAI 不需反向連接用戶機器。
  • 缺少公网地址僅影響「提前收到 Webhook 通知」,不影響數據傳輸。

三、 狀態管理與資源清理

  • Session 狀態週期:
  • Created:容器創建。
  • Registering:啟動 exec-server 並註冊。若失敗則進入 Rejected 並停止。
  • Connected:註冊成功,等待命令。
  • Executing:執行 OpenAI 下發的命令。
  • Reconnecting:WebSocket 掉線,exec-server 自動重連。
  • Stopped:OpenAI 徹底關閉 Session,exec-server 退出,容器停止。
  • Removed:透過 rm 或 prune 刪除。
  • 清理機制:
  • 不輪詢 OpenAI,而是依賴 exec-server 在連接徹底關閉後退出的特性。
  • 使用 prune 命令檢查已停止的容器,清理超過指定時間(如 --older-than 1h)的 Session。
  • 確保不會誤刪正在運行的容器。

四、 端到端實作演示

  • 前置準備:
  • 平台:僅支援 Apple Silicon Mac (darwin_arm64)。
  • 安裝:brew install --cask container 及下載 apple-sandbox 二進制至 ~/.local/bin。
  • 檢查:執行 apple-sandbox doctor 確認環境(Apple Silicon、Hypervisor、container 服務狀態等)。
  • 構建與啟動:
  • 執行 apple-sandbox build 構建執行器鏡像(拉取 Node 鏡像、安裝證書與 codex)。
  • 透過 API 創建 Session:POST 至 /v1/agents/sessions,設定 environment 為 self_hosted,工作目錄為 /workspace。
  • 獲取響應中的 ID、environment.id 和 remote_url。
  • 任務執行:
  • 執行 apple-sandbox run 並傳入上述三個參數。
  • 驗證:透過 container exec 進入虛擬機確認為 Linux 內核。
  • 文件處理:在本機 sandboxes 目錄放置 CSV 文件,虛擬機直接讀取(註:self_hosted 類型不支援 environment.files 字段,需依賴掛載)。
  • 提交任務:POST 至 Session 的 events 接口,觸發 Agent 執行 Python 腳本處理數據並寫入 /workspace/outputs/summary.json。
  • 結果驗證:本機直接讀取生成的 JSON 文件,確認結果正確。
  • 清理:
  • 執行 apple-sandbox stop 和 rm 刪除容器。
  • 使用 DELETE 請求刪除 OpenAI 端的 Session。
  • 或使用 prune --dry-run 預覽並批量清理舊 Session。

五、 注意事項與限制

  • 網路安全:目前出站網路未做白名單限制(僅內網或全開放),建議暫勿運行不受信任的任務。
  • SDK 支援:目前僅提供 CLI,無 TypeScript 或 Python SDK,需自行封裝子進程調用。
  • 狀態推斷風險:prune 依賴「容器停止即代表 Session 結束」的推斷,目前雖無反例,但連接中到停止的轉換過程尚未被單獨觀測過。

工具 / 模型 / 名詞整理

  • OpenAI 相關:
  • Agents API
  • OpenAI 官方托管沙箱環境
  • codex exec-server (執行器)
  • codex (npm 包)
  • API Key (普通 API Key, 受限 API Key)
  • api.openai.com
  • /v1/agents/sessions
  • environment.files
  • Artifacts 接口
  • Apple 相關:
  • Apple Silicon Mac
  • container (Apple 官方開源工具)
  • Virtualization.framework
  • Hypervisor
  • 第三方/開源專案:
  • apple-sandbox (專案名稱)
  • Go 語言
  • Docker
  • Cloudflare (提及作為 Webhook 方案的參考實現)
  • Vercel
  • Modal
  • GitHub (sugarforever/apple-sandbox)
  • brew (Homebrew)
  • 技術與協議:
  • OCI 容器
  • WebSocket
  • Webhook
  • Linux 內核
  • Node 22
  • CA 證書
  • Python
  • CSV
  • JSON

操作流程整理

  1. 環境準備:
  • 確認硬體為 Apple Silicon Mac。
  • 安裝 Apple container 工具:brew install --cask container。
  • 下載並安裝 apple-sandbox CLI 至 ~/.local/bin。
  • 執行 apple-sandbox doctor 檢查環境完整性。
  1. 構建執行器鏡像:
  • 執行 apple-sandbox build。
  • 系統會拉取 Node 鏡像,安裝 CA 證書與 codex npm 包。
  1. 創建 Session:
  • 使用普通 API Key,POST 請求至 /v1/agents/sessions。
  • 設定 environment 為 self_hosted,工作目錄為 /workspace。
  • 從響應中獲取 session ID、environment.id 和 remote_url。
  1. 啟動沙箱:
  • 在 OpenAI 平台創建受限 API Key。
  • 執行 apple-sandbox run,傳入 session ID、environment.id、remote_url 及受限 API Key。
  • CLI 啟動虛擬機,內部腳本啟動 exec-server 並主動連接 OpenAI。
  1. 執行任務:
  • 將數據文件(如 CSV)放入本機對應的 sandboxes 目錄。
  • 透過 API 向 Session 提交事件(Events),觸發 Agent 執行腳本。
  • Agent 在虛擬機 /workspace 執行並寫入結果(如 JSON)。
  1. 獲取結果與清理:
  • 在本機 sandboxes 目錄直接讀取生成的結果文件。
  • 執行 apple-sandbox stop 或 rm 刪除容器。
  • 使用 prune 命令批量清理過期 Session。

值得注意的限制或風險

  1. 硬體限制:僅支援 Apple Silicon Mac (darwin_arm64)。
  2. 網路安全:目前出站網路未做白名單限制,建議暫勿運行不受信任的任務。
  3. 開發者體驗:目前僅提供 CLI 工具,無 TypeScript 或 Python SDK,開發者需自行封裝子進程調用。
  4. 狀態推斷風險:prune 命令依賴「容器停止即代表 Session 結束」的推斷,若連接中到停止的轉換過程出現異常,可能導致清理邏輯誤判。
  5. API 兼容性:self_hosted 類型不支援 environment.files 字段,若使用該字段會返回 400 錯誤,需依賴目錄掛載機制存取文件。

逐字稿辨識疑點

  • gpt-6-astra:逐字稿中演示時選擇的模型為 gpt-6-astra。目前 OpenAI 公開模型列表中無此標準名稱,可能為內部測試模型、口誤或聽寫錯誤。
  • codex exec-server:逐字稿多次提及此名稱。OpenAI 官方文件通常稱之為 "Executor" 或 "Code Interpreter" 相關組件,具體組件名稱需查證是否為 codex 專案內的特定服務名稱。
  • Apple container 工具:逐字稿稱 Apple 去年開源了 container 工具。需查證 Apple 官方是否確實以該名稱開源此工具,或是否為其他專案(如 containerd 或 Apple 內部的某個專案)的誤稱。
  • environment.files 返回 400:逐字稿稱在 self_hosted 類型中,使用 environment.files 字段會返回 400 未知參數。需查證 OpenAI Agents API 文件是否確實如此規定,或是否為特定版本的行為。
  • prune 命令的參數格式:逐字稿中提及 prune --older-than 1h 以及 prune --older。需確認該 CLI 工具的實際參數格式是否一致。

可延伸追問

  1. apple-sandbox 專案的開源地址與貢獻指南為何?
  2. 若需支援非 Apple Silicon 架構(如 Intel Mac 或 Linux),架構上需要如何調整?
  3. 如何為 apple-sandbox 開發官方 SDK 以簡化集成流程?
  4. 針對出站網路未做白名單的限制,未來版本是否有計畫加入網路過濾機制?
  5. gpt-6-astra 是否為 OpenAI 內部測試模型,或為影片講者的口誤?

尚未產生學習筆記

請在 Telegram 指令最後加上「學習」,例如:videonote 網址 英文 雙語 學習