host 63823fc2e3 逐任务真实成本:上游用量事件 + 价目表计价
此前每任务只记一条 estimated(字符数/4,金额为空),注释称
host 协议不暴露真实 token 数。本轮实测推翻了该前提:
- account/usage/read 在中继上是死的(要求 ChatGPT 账号登录),
  不是来源,已在 cost.py docstring 写明死胡同
- app-server 每上游补全发 thread/tokenUsage/updated,last 是该次
  补全的用量;中继 5 调用回合实测 last 之和等于最后 total
  (input/output/cached/reasoning 四路都对得上)

实现:host.py run_turn 从事件窗口按 threadId+turnId 汇总用量,
重试/goal 各轮累加,dispatch/run JSON 与 goal 用量行透传;
sandbox 三路解析(dispatch JSON 解析改用完整 stdout,顺带修
了长 text 下 failed 被误判 completed 的潜 bug);worker 每任务
记一条 actual(重试/fallback 用量累加进同一条),无用量时回落
estimated;cost.py 价目表(默认 OpenAI 挂牌价,env 可覆盖,
无价模型 usd 记 None、报表进 unpriced_actual_tokens 而非 0 美元),
cost_records 加 cached_input_tokens 列(SQLite/PG 两支迁移);
报表与按天行新增 cached_input_tokens/unpriced_actual_tokens。
测试:host 32、cloud 483 全过。
2026-10-26 18:41:27 +08:00
2026-10-26 00:11:40 +08:00
2026-10-26 00:11:40 +08:00
2026-10-26 00:11:40 +08:00
2026-10-26 00:11:40 +08:00
2026-10-26 00:11:40 +08:00

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。
  • 流程(远端和登录齐了之后):
    1. host.py github-workflow 生成 CI workflow(已生成的文件 要提交进仓库,GitHub Actions 才会跑它);
    2. 任务 completed 后 host.py pr --task <名>:push 分支 + gh pr create,pr_url/pr_number 写回任务记录;
    3. host.py pr-checks --task <名>:看 CI 全过没有;
    4. 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。

S
Description
codex-app-server, imported from GitHub main, with Gitea CI
Readme
9.4 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 87.8%
HTML 6.7%
TypeScript 3.8%
Shell 1.1%
CSS 0.5%