工作清單

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

逐字稿文字預覽

大家好我是小木头
上一期视频
我们用 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/sum

來源:transcript.txt

本次執行流程與問題處理

  1. 下達指令

    指令:videonote https://www.youtube.com/watch?v=E8tyud31EVM --language zh --adopt-source-subtitles

    來源:https://www.youtube.com/watch?v=E8tyud31EVM

    辨識語言:zh | 參數:--language zh --adopt-source-subtitles

  2. 預期流程

    依指令產生的處理順序:

    1. 採用來源字幕(略過語音辨識)
    2. 來源選擇與品質檢查
    3. 文字校正與句子單元
    4. 筆記與發布
    5. 公開網頁發布
  3. 1. 下載與語音辨識

    工具:videonote-transcribe-url(主辨識管線:下載、Whisper 辨識與選稿)

    處理:下載音訊、以 Whisper 辨識並選出最佳逐字稿

    產出:yt_dlp_meta.json(影片元資料)、audio_16k_mono.wav(16kHz 單聲道音訊)、transcript.whisper.json(Whisper 辨識結果)、transcript.json(正式逐字稿)、transcript_selection_report.json(來源選擇報告)

    耗時:14.0 秒

    關係:依序執行

    結果:完成

  4. 2. 全文快速校讀

    工具:videonote_rapid_transcript_review.py

    處理:先速讀完整逐字稿,再修正高可信突兀詞句並列出待確認項目

    產出:rapid_transcript_review_report.json(產出檔案)

    耗時:46.2 秒

    關係:依序執行

    結果:完成

  5. 3. 建立句子單元

    工具:videonote-build-sentence-units

    處理:將逐字稿切成帶時間軸的句子單元

    產出:sentence_units.json(句子單元)

    耗時:0.2 秒

    關係:依序執行

    結果:完成

  6. 4. 時間軸稽核

    工具:videonote_timeline_audit.py

    處理:檢查時間軸與音訊對齊

    產出:timeline_audit_report.json(時間軸稽核)

    耗時:0.2 秒

    關係:依序執行

    結果:完成

  7. 5. 品質檢查

    工具:videonote_controlled_quality.py(檢查時間軸與品質)

    處理:檢查語言、時間軸與可讀性品質

    產出:controlled_quality_report.json(品質報告)、language_sanity_report.json(語言檢查)

    耗時:0.3 秒

    關係:依序執行

    結果:完成

  8. 6. 播放時間軸回填

    工具:videonote_rebase_display_timeline.py

    處理:將網頁播放時間軸回填為校正字幕時間

    產出:display_timeline_rebase_report.json(時間軸回填報告)

    耗時:0.3 秒

    關係:依序執行

    結果:完成

  9. 7. 來源句整理

    工具:videonote_prepare_source_entries.py

    處理:整理來源句子供時間軸與頁面使用

    產出:sentence_units.json(句子單元)

    耗時:0.3 秒

    關係:依序執行

    結果:完成

  10. 8. 生成筆記摘要

    工具:videonote-generate-note(生成筆記摘要)

    處理:生成筆記摘要與重點

    產出:note.md(筆記 Markdown)、note.html(筆記 HTML)

    耗時:124.2 秒

    關係:依序執行

    結果:完成

  11. 9. 建立筆記頁面

    工具:videonote-render-html(產生筆記 HTML)

    處理:產生可閱讀的筆記 HTML 頁面

    產出:note.html(筆記 HTML)

    耗時:0.3 秒

    關係:依序執行

    結果:完成

  12. 10. 發布公開頁面

    工具:videonote-publish(發布公開頁面)

    處理:發布到公開網站與每日列表

    產出:公開筆記網址、result.json(管線結果)

    耗時:0.4 秒

    關係:依序執行

    結果:完成

手動來源字幕

自動來源字幕

Whisper 字幕

Qwen 字幕

Parakeet 字幕

MOSS 字幕

聲學時間證據(PyAnnote / WhisperX)

校正前後逐字稿

目前待解決問題與初步改善方向

進一步精進字幕與筆記
使用 Workbench 上傳校正字幕、中文參考稿或回饋 SRT,重新產生雙語與學習單元。
開啟 Workbench