The production worker runs host.py inside a container where /app is mounted read-only, so the default state path /app/host-state.json fails instantly with Read-only file system. The Docker config must set state to /work/host-state.json (the task worktree mount, writable) and sandbox to danger-full-access (host kernel forbids untrusted user namespaces, so Docker itself is the isolation layer). This file was previously host-local only, so PR #9's deploy-clean step deleted it and every Docker task since then has failed in ~1s with the generic 'host execution failed' (stderr was discarded from the task record, so the breakage was invisible to health checks, which only look at the worker heartbeat). Committing it makes deploys restore it. No secrets in this file; the model key stays in the environment.
Codex app-server(无 GUI 版,照搬开源核心)
目标:不做桌面 UI、不重写 agent 循环,把 Codex 开源能给到的全部 原样接过来,做成一个可以被任何客户端(pi agent、你自己的 App、 脚本)调用的 app-server。
一句话架构
你的客户端 / pi agent
│ stdio JSON-RPC(或 unix / ws,见 start.sh)
▼
client.py ──► codex app-server(官方二进制,codex-core 原样)
│ thread/start · turn/start · 事件流 · 审批请求
▼
agent 循环 + 沙箱 + 工具 + MCP + skills + 线程持久化
本仓库自己写的只有宿主层:client.py(协议客户端)、
run_parallel.py(多 thread 编排)、start.sh、config.example.toml。
agent 循环、沙箱、工具执行、上下文压缩、线程存储,全部是官方
codex 二进制里的,不是我复刻的简化版。
实测版本:本机 codex-cli 0.164.0-alpha.9,协议定义就是从它
导出的,见 protocol/。
目录
start.sh:启动官方 app-server,支持 stdio / unix / ws 三种传输。client.py:零依赖 Python 客户端,演示完整链路。run_parallel.py:宿主层多 agent 编排(见下文“子代理”)。config.example.toml:沙箱、审批、MCP、features 的配置示例。protocol/:codex app-server generate-json-schema原样导出的 协议 schema(v1 + v2),客户端的方法名和字段以它为准。examples/subagent-prompt.txt:让主 agent 派生子代理的 prompt 模板。examples/jobs.json:并行审查的 job 示例。work/job-a、work/job-b:并行示例用的两个只读样例目录。
快速开始
1. 先确认登录(这一步不在开源里)
在你自己的机器上先跑一次 codex login(ChatGPT 登录),
或配好带 API key 的 model provider。否则会出现下面实测里
的状况:thread 能建、turn 能发,但模型调用返回 401,
turn 以 failed 结束——这是登录问题,不是 app-server 问题。
2. 跑一回合
cd ~/workspace/codex-app-server
python3 client.py --prompt "修掉 README 里的错别字" \
--cwd /path/to/your/repo \
--sandbox workspace-write --approval-policy on-request
只看协议、不跑模型:
python3 client.py --list-threads
python3 client.py --thread-id <上次的 thread id> --prompt "继续"
3. 换传输方式
./start.sh --listen unix:///tmp/codex.sock
./start.sh --listen ws://127.0.0.1:8080
# 再用你自己的客户端连这个 socket / ws,而不是 stdio
4. 配置
把 config.example.toml 复制到 ~/.codex/config.toml 再改,
不要把登录信息、API key、你自己的 relay 地址写进本仓库。
沙箱三档:read-only / workspace-write / danger-full-access;
审批三档:untrusted / on-request / never。
开源照搬清单(全部来自官方 app-server)
| 能力 | 来源 | 说明 |
|---|---|---|
| Agent 循环 | codex-core | 推理 → 工具 → 结果 → 再推理,直到 turn 完成 |
| Thread 生命周期 | app-server | thread/start、resume、fork、archive、delete、list,事件历史持久化 |
| Turn 与流式事件 | app-server | turn/start、turn/steer、turn/interrupt、turn/completed,agent 文本/工具事件流式回推 |
| 沙箱 | codex-core | macOS Seatbelt、Linux Landlock/bubblewrap;本机 VM 的系统 codex 缺 host 和 bwrap,工具会 fail closed,已用 bin/ 配对三件套修复,见 TEST-REPORT.md“工具执行修复” |
| 审批 | app-server | 命令、改文件、权限、MCP 询问等请求回推给客户端裁决;也可配 approvalsReviewer=auto_review 让子代理先审 |
| 工具 | codex-core | shell、文件读写、搜索、改代码(apply_patch)、图片等 |
| MCP / skills / 插件 | app-server | thread 内按需加载,features list 可看本版开关 |
| AGENTS.md | codex-core | 全局、仓库根、子目录三层合并,thread 启动时自动读 |
| 上下文压缩 | codex-core | 长会话自动 compact,不用客户端自己截断 |
| exec-server 拆分 | codex-core | 推理(app-server)与执行(exec-server)可分开部署,见 codex exec-server --help |
| Goal / 多 agent 开关 | features | 本版 multi_agent 已是 stable,codex features list 可验证 |
子代理:两层,不要混
内置子代理(开源已给)
主 agent 在 prompt 明确要求时派生 worker / explorer 等子代理,
各自独立上下文、并行跑、主 agent 汇总。模板见
examples/subagent-prompt.txt,要点是:分工写清楚、要求分开
列结果、最后合并去重、只列有证据的项。
限制(重要):子代理继承父 thread 的沙箱和审批设置, 不是独立权限边界。“只读评审”是指令约束,不是技术隔离。 真要隔离,改代码的 job 必须走下面一层。
宿主编排(开源没给,自己写,已给示例)
run_parallel.py 就是这一层:宿主并行开多个 thread,
每个 thread 一个独立工作目录(生产上用 git worktree),
最后宿主收集结果再汇总。桌面版“多线程指挥台”的感觉
主要就是这一层,不是 app-server 本身。
python3 run_parallel.py --jobs examples/jobs.json
示例默认是只读审查(read-only + never),要并行改代码时: 每个 job 换成各自的 worktree、改成 workspace-write, 事后逐个 review diff 再合并,冲突仍需人工处理—— core 里没有自动解决冲突的调度器。
宿主编排(host.py,无 GUI,对齐 ChatGPT / Codex 桌面版宿主)
ChatGPT / Codex 桌面版作为宿主,本质不是再写一个 agent,而是
管好这几件:线程、并行、隔离、委派、长任务、评审、自动化、
边界。host.py 就是这一层,一个 app-server 进程托管多个 thread,
后台读线程按 threadId 分发 turn/completed,所以多 thread 的
turn 可以真正并发(不是轮流排队)。
| ChatGPT / 桌面版宿主能力 | host.py | 状态 |
|---|---|---|
| 多 thread 并行 | host.py parallel --jobs …,一个 app-server 进程内多 thread 并发 |
已实测 |
| 一任务一 git worktree 隔离 | dispatch 自动 git worktree add -b host/<task>,非 git 仓库退化为独立目录拷贝 |
已实测 |
| diff 评审 | host.py review <task>:status + diff + untracked |
已实测 |
| 评审后合并 / 丢弃 | merge:先在 worktree 提交,再 --no-ff 合并进主仓,冲突自动 abort;discard:删 worktree 和分支 |
已实测(host-b 已合并进主仓) |
| 线程续跑 / fork / 归档 | run(resume + 新回合)、fork、archive、list |
已实测 |
| 宿主级子代理委派 | delegate:子 thread(默认 read-only)跑完,结果可 --report-to-parent 汇总回主 thread |
已实测 |
| 模型内子代理 | prompt 明确要求派生,见 examples/subagent-prompt.txt;与宿主委派是两层,别混 |
已实测(模板 + 协议) |
| 长任务 / 目标循环 | goal:同一 thread 多回合,直到 --verify 验收命令通过或到 --max-rounds |
已实测(1 轮通过) |
| 自动化 / 评审队列 | automations --add + run-due:到期的任务自动派发,结果进 host-state.json 评审队列 |
已实测 |
| 沙箱边界 | read-only thread 的写文件请求被拦下,文件确实没建出来 | 已实测 |
| 审批控制 | untrusted + deny 时 agent 明确收到审批拒绝,没拿到命令输出 | 已实测 |
| 模型内子代理 | 3 子代理并行检查并汇总,视角是真的不同,不是重复一句话 | 已实测 |
| 中断 / 续跑控制 | turn/interrupt 后状态为 interrupted;compact 需先 resume(host.py 的 compact 命令已包好这一步) |
已实测 |
| 合并冲突处理 | 先合的进主仓,后合同一行的冲突 abort,主仓干净,冲突 worktree 留着人工解 | 已实测 |
| 自动化幂等 | 未到期的自动化 run-due 时不重复派发 | 已实测 |
| 宿主提供 MCP 工具 | 隔离 CODEX_HOME 注册 stdio MCP,agent 能发现并调用;注意 MCP 审批是 elicitation 协议,不是通用 decision 字段,host.py 已分流处理 | 已实测(修过一轮才通) |
| 模型切换 | 同一 thread 内换模型续跑,turn 的 model 字段随之改变 | 已实测 |
| 运行中改向 | turn/steer(expectedTurnId 校验)能把进行中的长任务改成短任务 |
已实测 |
| 3 并行 | 3 个 worktree 任务同时跑,全部完成且互不污染 | 已实测 |
| 未追踪文件评审 | review 时 diff 为空也要看 untracked 栏 | 已实测 |
| 失败不崩宿主 | 不存在的模型导致 task failed 并带上游 404 错误,host 不崩 | 已实测 |
| 生产级项目闭环 | 真实仓:并行功能开发 + 测试套件验收 + 评审修复闭环 + 合并链,主仓从 2 个测试涨到 17 个全部通过 | 已实测 |
| 目标循环反馈 | 验收失败时把命令和失败输出喂给下一轮,否则 agent 会原地不动 | 已实测(修过一轮才通) |
| 中断恢复 | dispatch/delegate/goal/run-due 先落盘再跑 turn,中断后可用 run 续跑 | 已实测(委派超时后修) |
| fork 隔离 | fork 建新 worktree + 拷工作区文件(跳过 .git)+ turn 级 sandboxPolicy 覆盖 read-only 重置 | 已实测(修过两轮才通) |
| 跨进程 state 竞态 | host-state.json 单文件,多 host.py 进程同时写会丢任务,同一进程内并行安全 | 已修:锁文件 + 保存时合并,实测双进程并发保存不丢 |
| 项目配置 | 每次都要传 model/repo/state 等参数 | 已修:host-config.json + init/config,CLI 优先于配置,密钥只走环境变量 |
| 定时自动化 | run-due 要手动跑,跑完没日志 | 已修:host-run.log 记录派发/跳过,automations --cron-line 生成 crontab 行,--install-cron 才装机 |
| 合并后自动验测 + 回滚 | merge 合完不验主仓测试,合坏了靠人发现 | 已修:merge 默认仓库有 tests/ 就跑 unittest,验测失败 reset 回合并前 HEAD,分支/worktree 保留;--no-verify 跳过,--verify 可指定命令 |
| 任务依赖 + 优先级 | parallel 是扁平的,没有依赖和优先级 | 已修:jobs 里 depends_on/priority,拓扑排序 + 按波次跑,依赖失败时依赖任务标 skipped 不跑;依赖任务从依赖分支建 worktree(有血缘关系),不是文件拷贝 |
| 失败自动重试 | 失败任务就停在 failed,不重试 | 已修:dispatch/parallel 里 --max-retries,失败后同 prompt 再跑,重试前查预算;默认 0,行为不变 |
| 评审评论回流 | 评审只看 diff,修要另起 run prompt,评论没地方钉 | 已修:review-comment --file --line --comment 把评论存进任务,review 时列出 open 评论,fix-review 按 open 评论逐条修,修完标 resolved |
| server 崩溃重连 | app-server 死了,host 就 BrokenPipe 失败 | 已修:Server.ensure_alive,死了重拉并 resume thread(thread 在 CODEX_HOME 里不丢);request 遇 BrokenPipe 重试一次;单测 kill 进程后重拉 + list 能通 |
| 成本预算 | 没有预算,模型跑多少全靠自觉 | 已修:配置 max_daily_turns,按 turn 数算(协议不暴露真实 token 数,这是诚实边界),超预算不派发/不重试;usage 记在 state 里,doctor 可看 |
| 失败通知 | 失败只有 state 里的 status,跑完没人知道 | 已修:失败时写 host-run.log + 跑配置的 notify_command(本地 shell,模板 {task} {status});failures 命令列失败任务,doctor 列失败/缺失 worktree/open 评论 |
| 多机器 state 冲突 | 锁只防同一台机器,多机器共享 state 文件会静默覆盖 | 部分修:state 加 version/last_writer/updated_at,保存时磁盘版本比加载时新就记 STATE-CONFLICT 日志并合并保留双方任务,不静默丢;真多机同步仍需中心服务(见下“没对齐”) |
| 合并时提交缓存文件 | merge 的 git add -A 会把 pycache/*.pyc 也提交进主仓,下次合并被 .pyc 改动挡住 | 已修:merge 前清 worktree 的 pycache/*.pyc,主仓的 .pyc 改动先还原;单测覆盖 pyc 不入库 |
| 守护进程 + 健康检查 + 自动恢复 | host.py 跑完就退出,不常驻;state 坏了静默当空 state;中断任务一直停在 running | 已修:daemon(双 fork 到 PID 1,循环 recover + run-due)、health(不跑模型,查二进制/配置/state/server/daemon)、restore-state(坏了从 .bak 恢复并留底)、recover(超过阈值的 running 标 interrupted,可用 run 续跑,不自动重跑模型回合) |
| GitHub PR / CI 一条线 | 本地 merge 完就完,没有 PR、没有 CI 门 | 已修(代码 + 本地可测部分):github-workflow 生成 .github/workflows/host-verify.yml(checkout + python + unittest,幂等);pr(push 分支 + gh pr create,成功后 pr_url/pr_number 写回任务记录);pr-checks(解析 gh pr checks,all_pass 仅全 pass 为真,空 checks 不算过);pr-merge(先查 checks,未全过拒绝且不调 merge,全过才 gh pr merge --merge 并标 merged,不动本地主分支)。gh 调用走 run_gh(),单测用假 gh。真实 PR 本轮没跑:本仓没有 git remote,gh 未登录,见下“GitHub PR/CI 的外部条件”。 |
| 边界:cwd、沙箱、审批、模型 | dispatch / turn 参数里定,见 config.example.toml |
已实测 |
| 任务登记 | host-state.json:task -> thread/worktree/状态/diff;agent 状态本身仍在 CODEX_HOME,不另存第二份 |
已实测 |
没对齐的部分(诚实说,不是偷工):
- 云端派发、推送通知、Slack / GitHub 触发:那是 ChatGPT 托管服务,
开源里没有。
run-due要靠你自己的 cron 定时调,才等于“定时自动化”。 - GUI 的 inline 评论、side-by-side diff、点按钮批准:无 GUI 版 本来就不要这层,评审就是 git diff 文本 + merge/discard 命令。
- Computer Use:另一条管线,不在宿主编排里。
- 并行改同一文件:worktree 能防互相踩,但两个分支都在文件末尾 追加时,合并第二支仍可能冲突,host 会 abort 等人工解冲突, 不会自动猜合并结果——这和桌面版一样,是正确做法,不是 bug。 2026-10-26 补了一层安全网:merge 冲突时先试 -X ours,解完 后逐行核对任务分支的新增行是否都在主仓里,缺一行就回滚 (gap2 实测拦下了一次 -X ours 丢 mul2 的情况);依赖任务 现在从依赖分支建 worktree,依赖先合、依赖任务后合时不再 重复加同一段。
- 多机器同步:state 现在有 version 和冲突日志,不会静默丢 任务,但两台机器共享一个 state 文件仍不是 ChatGPT 云端 那种中心同步——要真多机,得另起一个中心 state 服务, 这不在本地 host 的范围里。
- 守护进程的边界:daemon 本身也活不过 VM 平台回收(和 微信桥 serve 一样,回收后要重拉),所以 VM 上更稳的定时 仍是 Muse cron 定时调 run-due/health,而不是只靠 daemon 或 crontab;daemon 适合常驻机器,VM 上是“会话内常驻 + 回收后重拉”的定位。
宿主快速开始(最小闭环)
下面全部是 host.py --help 核对过的真实子命令和参数,
不要凭印象编命令。先装检查,再走闭环;不要在示例里
直接派发真实模型任务来“试一下”——dispatch 一步就会跑模型。
cd ~/workspace/codex-app-server
./install.sh # 只检查 python3、git、codex,不写密钥、不跑模型
python3 host.py health # 不跑模型:查二进制、配置、state、server、daemon
python3 host.py init --repo /path/to/your/repo
# 上一步写 host-config.json(CLI > 配置 > 默认;key 绝不进配置文件,只走环境变量)
python3 host.py dispatch --name example-task --task "你要做的事" --repo /path/to/your/repo
# ^ 这一步跑模型:需要模型 key 已走环境变量提供,且会建 git worktree host/example-task
python3 host.py review --task example-task # 看 status + diff + untracked + open 评审评论
python3 host.py merge --task example-task # 默认仓库有 tests/ 就跑 unittest,验测失败自动回滚
# 合并不了就别硬合:python3 host.py discard --task example-task # 删 worktree 和分支
python3 host.py daemon --interval 300 # 双 fork 常驻:每轮 recover + run-due
python3 host.py daemon-status
python3 host.py daemon-stop
三条边界,闭环前先知道:
health不跑模型、不花额度,随时可跑;daemon-status/daemon-stop同样不跑模型。daemon在这台 VM 上活不过平台回收(和微信桥 serve 同理): 回收后要重拉;VM 上更稳的定时是 Muse cron 调health/run-due,而不是只靠 daemon 或 crontab。run-due到期会跑模型、会花额度:先用python3 host.py automations --add --name <名> --task "<任务>" --repo <仓> --interval 86400登记自动化,再确认模型 key 已走环境变量提供,才让定时去调python3 host.py run-due;未到期的自动化 run-due 时不重复派发。- 云端控制面演示另起一端(不跑模型回合):
python3 -m cloud.server(默认 127.0.0.1:8765), 除GET /health、/webhooks/*、/oauth/*外都要X-API-Key请求头。
GitHub PR/CI 的外部条件(真实 PR 仍缺这两样)
PR/CI 这条线本轮只做了代码和本地可测部分,真实 PR 没有跑, 原因如实写在这里,不是没做完就含糊过去:
- 仓库远端:本仓(codex-app-server)目前没有 git remote
origin,
git remote -v为空。pr 命令会先查 remote,没有就 明确报错,不静默失败。以后给仓库加 remote 再跑:git remote add origin <url>。 - gh 登录:本机有
gh,但gh auth status显示未登录。 不要在本轮登录,也不要把 PAT 写进任何文件、state、日志或 聊天记录;以后用户给 PAT 时走 Secure Vault,再跑gh auth login。没有登录时 pr/pr-checks/pr-merge 会把 gh 的错误原文打印出来,不编造 PR URL。 - 流程(远端和登录齐了之后):
host.py github-workflow生成 CI workflow(已生成的文件 要提交进仓库,GitHub Actions 才会跑它);- 任务 completed 后
host.py pr --task <名>:push 分支 + gh pr create,pr_url/pr_number 写回任务记录; host.py pr-checks --task <名>:看 CI 全过没有;host.py pr-merge --task <名>:全过才合并,合并由 GitHub 完成,不动本地主分支——和本地merge命令是两条 线,不要混用:本地 merge 是直接合进本地主仓,PR merge 是走 GitHub 的 PR 流程。
- 两条线不冲突的原因:本地 merge 用 merge_task()(自动验测
- 回滚),PR merge 用 pr-merge(checks 门 + GitHub 合并); 一个任务要么走本地 merge,要么走 PR,不要两边都合。
开源没给到的(别指望照搬)
- 模型本身、ChatGPT 登录、计费、Codex Cloud 云端沙箱。
- GitHub / Slack 等渠道的托管派发。
- 桌面版的 diff 评审 UI、多线程看板、通知。
- Computer Use(屏幕操作)是另一条管线,不在 CLI 中间层里;
本版 features 里虽然有
computer_use开关,但那不是 “复制 app-server 就自动得到一个桌面操作 agent”。
安全
- 常驻服务不要用
danger-full-access,也不要把--dangerously-bypass-approvals-and-sandbox写进启动脚本, 那是给外部已经沙箱化的环境准备的。 - 审批策略默认:自动化用
never+workspace-write或更严; 交互使用再用on-request,由客户端(人)逐条裁决。 client.py --approval auto会放行所有审批请求,只用在 你完全信任的仓库和一次性任务里,默认是 deny。- ws 传输对外暴露时务必配
--ws-auth,不要裸奔在公网。
本机实测记录(2026-10-25)
隔离 CODEX_HOME(不污染你真实的 ~/.codex)下跑:
python3 client.py --codex-home /tmp/codex-client-test \
--cwd work/job-a --sandbox read-only --approval-policy never \
--prompt "只用一句话回答:2+2等于几?不要调用任何工具。"
结果:
initialize成功,返回 codexHome / platformOs。thread/start成功,拿到 thread id,model 为默认模型。turn/start成功,收到turn/started、userMessage 事件。- 模型调用处失败:
401 Unauthorized: Missing bearer token, 随后turn/completed状态为 failed。 原因:隔离目录里没有任何登录/provider 密钥,且默认 provider 指向 api.openai.com。这是预期内的登录边界, 不是协议或客户端 bug。在你已登录的机器上同一条链路 会直接出结果,不需要改代码。
完整输出见 TEST-REPORT.md。