Files
codex-app-server/CHANGELOG.md
T
host f8acdffb4a feat: web search MCP v1 (oh-my-pi pattern, Python stdlib)
web_search auto chain Perplexity/Exa/Tavily/Brave/SearXNG/DuckDuckGo
with pinned-provider no-fallback, web_fetch with head+tail truncation,
codex-home seeding helper (idempotent, never writes keys).
Tests: cloud/tests/test_websearch_mcp.py 12/12 (mocked HTTP).
Not wired into worker, not deployed, paid backends not live-tested.
2026-10-28 17:14:22 +08:00

298 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CHANGELOG
语义化版本见 `VERSION`(当前 0.1.0)。以下按日期倒序,只列已交付
的大项,每项附实测证据;未建成的东西在“诚实边界”里明说,不夸大。
## 2026-10-28:网页搜索 MCP v1(oh-my-pi 模式,Python 实现)
- 新增 `cloud/websearch.py` + `cloud/websearch_mcp.py`(纯标准库):
stdio MCP 服务器暴露 `web_search` / `web_fetch`,provider 链
按 Perplexity → Exa → Tavily → Brave → SearXNG → DuckDuckGo
的 auto 顺序回退,pin 单个名字时不回退;抓取去脚本样式、
默认 15000 字符头尾截断并标 `[TRUNCATED]`;key 只走进程
环境变量,错误串不含 key(单测断言)。
- 新增 `ensure_codex_home_websearch`:把两个脚本拷进租户
codex-home,缺失时追加 `[mcp_servers.websearch]`,已有
section 绝不重写、租户改过的脚本绝不覆盖。
- 单测:`cloud/tests/test_websearch_mcp.py` 12 个全部通过
(链式回退、pin 不回退、全部失败、Perplexity 答案与引用、
Brave/Tavily 映射、DuckDuckGo HTML 解析、抓取截断与拒绝、
MCP 帧、配置合并幂等),不联网、不需要真实 key。
诚实边界:付费后端只以 mock 验证请求字段与映射,未用真实
key 实测;worker 自动播种未接线、生产 egress 白名单未改、
本批只本地提交,未推、未建 PR、未合并、未部署。
## 2026-10-28:长 goal 任务异常退出可断点续跑(不再 terminal failed)
- 实弹事故:11:49 认领的 8 轮 goal 长任务跑约 60 分钟后终局
failed、无 goal 结论;host-state 仍是 goal-running、
goal_rounds_done=0、turns 为空——第 1 轮 turn 从 11:53 起
再无进展,600 秒回合超时未触发(同期探针脚本与 autoscaler
一并静默,12:48:48 全局 OOM 杀掉另一租户的 supermemory-ser
后系统恢复,12:48:56 任务判 failed)。根因链:宿主机内存
挤兑致全机停滞 + 恢复时 goal 进程异常退出、无结论行,云端
旧语义(returncode 非 0 一律 failed)把可续跑的断点判成终局。
- 修法:host.py `run_goal_loop` 回合异常(turn 抛错、验收起
不来、检查点落盘失败)不再向上传播,统一记 interrupted、
打印 "goal interrupted after rounds: <原因>" 标记行、exit 0;
cloud/sandbox.py 四处 goal 结论解析(local goal、docker
goal、docker 两处 resume)同步新语义:interrupted 标记 →
interrupted;returncode 非 0 但无结论 → interrupted(断点在
盘上,保住 /goal-resume 活路);有 met/unmet 结论的非 0 退出
仍 failed,rc==0 无结论仍 failed;worker 终局审计名与状态
对齐(interrupted 记 worker_interrupted,不再误记 failed)。
- 单测:sandbox 新测 5 个(local 无结论非 0 → interrupted、
非 0 带 unmet 结论仍 failed、interrupted 标记、docker goal 与
docker 两处 resume 无结论非 0 → interrupted),host 新测 1 个
(turn 抛错 → interrupted + 标记行 + 落盘原因),并更新 1 个
旧测(首轮前落盘断言:旧“异常上传播 + 状态 goal-running”
改为新语义)。
诚实边界:导致恢复时退出的确切异常已不可考(任务行与执行
结果已被探针清理删掉,stderr 无存档);本次只修“异常退出
的终局语义”,不修宿主机内存挤兑本身(另一租户容器,不动);
本批只本地提交,未部署、未做生产实弹复验,部署与复验另等
用户点头。
## 2026-10-26:收尾(Docker 沙箱实测 + 云端合并状态)
- Docker 沙箱执行器接通并在外部主机上真实跑通:镜像
`codex-app-server:latest`(codex 0.164.0-alpha.9,codex-resources
必须放在 codex 二进制旁边、且 bwrap 进 PATH,否则容器内报
bubblewrap is unavailable)、worker `--executor docker`(额外
挂载 `/app/work` 与 `/var/lib/codex-app-server`,容器用户取
宿主服务用户 uid/gid)、Docker 任务 triple 加函数约 18 秒完成、
容器内合并后主仓库含 add + mul2 + triple、容器经 --rm 清理。
诚实边界:这台主机/内核禁非特权 user namespace,bwrap 在
主机和容器内都起不来,因此生产模式是 Docker 隔离 + 容器内
danger-full-access(host-config-docker.json),不是双层沙箱。
- 云端合并状态:新增 `POST /tasks/{id}/merge`,只同步状态、
不执行合并(合并仍由 host.py merge 执行,成功后再调本接口);
仅 completed 可改为 merged,maintainer 以上(approve_merge),
其它状态与重复调用返回 409,越权 403,审计记 merge_task。
- 实测证据:云端测试 219 个、host 测试 27 个、合并状态单测
4 个,全部通过(`python3 -m unittest discover -s cloud/tests`
与 `python3 -m unittest discover -s tests`)。
## 2026-10-26:缺口补齐(云端控制面第二批)
- 记忆 / KB / 成本 / 模型接成 HTTP 端点:记忆写/查/ supersede、
KB 索引/搜索(按项目隔离)、按任务与按项目/按日的成本(实际与
估算分开)、模型总览;全部走 `X-API-Key` + 项目 RBAC,
401 / 403 / 404 语义与跨项目隔离均有测试。
- SQLite 并发:共享 db 助手(WAL + busy_timeout + 有界重试),
并注明单机上限,Postgres 是下一阶段,不是本版已做。
- OAuth 脚手架:`cloud/oauth.py`,state 10 分钟过期、生成授权 URL、
回调验 state 后用可注入的 HTTP 客户端换 code;secret 只走环境变量,
没有假登录页,真实 OAuth 应用注册仍缺。
- 推送订阅与分发:`cloud/push.py`,webhook / slack / webpush /
apns / fcm 通道,即时/摘要/静默偏好,只发状态变更事件;缺发送方/
凭证时报 error,绝不静默报成功;真实推送凭证未配、真实推送未发。
- 实测证据:云端测试从 102 个涨到 157 个,全部通过
(`python3 -m unittest discover -s cloud/tests`);
host 测试同期 27 个,全部通过
(`python3 -m unittest discover -s tests`);
另有一次 live smoke:记忆写 201/读通、KB 索引/搜索命中、
OAuth 登录 URL 只返回不跳转、推送分发报“无发送方”错误、
无 key 查项目返回 401。
## 2026-10-26:云端控制面(第一批)
- 新 `cloud/` 包,零第三方依赖、只用 Python 标准库:
`server.py`(ThreadingHTTPServer + SQLite 状态,任务状态机与
host.py 对齐,含心跳与 stale 回收)、`auth.py`(API key 只存
SHA-256 哈希、按项目 RBAC owner / maintainer / contributor /
viewer、追加式审计日志)、`queue.py`(依赖拓扑 + 优先级 +
预算门)、`sandbox.py`(Local / Docker / Cloud 三种执行器,
缺 Docker 或缺云账号时明确报错、不静默降级)、`memory.py`
(线程摘要,可删/可 supersede,只取 top-k)、`kb.py`
(SQLite FTS,LIKE 兜底,按项目隔离)、`models.py`
(404 / 429 / 5xx / 超时才 fallback,不对 401 / 400 fallback,
实际用到的模型如实记录)、`cost.py`(读不到账单时返回
available=false,估算值明确标 estimated,实际与估算分开)、
`webhooks.py`(GitHub HMAC + Slack 签名与重放窗口,只解析,
只在状态变更时推送)。
- 实测证据:云端测试 102 个、host 测试 27 个,全部通过;
smoke:health 正常、无 key 查项目返回 401;代码里无密钥。
- 诚实边界:这是本地可跑的控制面演示,不是已建成的真实云服务;
仍缺常驻宿主、GitHub 远端 + PAT、Slack App、云容器账号、
定时/worker 跑模型时的模型 key 供给方式、真实 OAuth 应用注册、
真实推送凭证;server 本身不跑模型回合。
## 2026-10-26:GitHub PR / CI 一条线
- `host.py github-workflow`:生成 `.github/workflows/host-verify.yml`
(checkout + python + unittest),幂等。
- `host.py pr`:校验任务已完成且有分支,push 分支 + `gh pr create`,
成功后把 pr_url / pr_number 写回任务记录。
- `host.py pr-checks`:解析 `gh pr checks`,all_pass 仅在 checks
非空且全部 pass 时为真,空 checks 不算过。
- `host.py pr-merge`:先查 checks,未全过拒绝且不调 merge,
全过才 `gh pr merge --merge` 并标 merged,不动本地主分支
(与本地 `merge` 是两条线)。
- 实测证据:全部 gh 调用走可 mock 的 run_gh;测试从 19 个涨到
27 个,全部通过,含本地 bare 远端 push + 假 gh PR(写回编号 42)
与 workflow 幂等。
- 诚实边界:真实 PR 本轮没跑——本仓没有 git remote,gh 未登录;
真实 PR 仍缺远端与经 Secure Vault 的 PAT,那两样齐了才能跑。
## 2026-10-26:第二阶段(daemon / health / 自动恢复)
- `host.py daemon`:双 fork 到 PID 1,pidfile + host-daemon.log,
每轮跑 recover + run-due(各为独立 host.py 子进程);
`daemon-status` / `daemon-stop` 配套。
- `host.py health`:不跑模型,只查二进制、配置、state 有效性、
备份、daemon pid、server initialize + list、缺失 worktree;
失败时 exit 1。
- `host.py restore-state`:state 损坏/丢失时从 .bak / .bak.1 /
.bak.2 恢复,损坏文件改名为 .corrupt.<时间戳> 留证。
- `host.py recover`:超过 `--older-than`(默认 3600 秒)未更新的
running 任务标 interrupted 并记日志,可用 `run` 续跑;
故意不自动重跑模型回合,避免未经批准花 relay 额度。
- 附带修掉一个静默丢数据洞:旧 state 读取把 JSON 解析错误
当空 state 吞掉。
- 实测证据:daemon pid 的 PPID 为 1,测试中每 5 秒 tick、
stop 后 pidfile 删除;health 先 ok=true(7 个 server 线程),
损坏 state 后 ok=false / exit 1,restore 后 ok=true(备份版本 9);
recover 把 2 小时未动的 running 标 interrupted、1 分钟的保留;
测试从 15 个涨到 19 个,全部通过。
- 诚实边界:daemon 本身活不过平台 VM 回收(与微信桥 serve 同理);
VM 上更稳的定时是 Muse cron 调 run-due / health,不是只靠 daemon
或 crontab;本轮没有自动装 Muse cron 或 crontab,因为 run-due
到期会花模型额度,装定时需用户明确指定间隔与是否允许自动跑模型。
## 2026-10-26:第二类缺口(全部补齐)
- 合并自动验测 + 回滚:`merge` 默认仓库有 tests/ 就跑 unittest,
验测失败 reset 回合并前 HEAD,分支/worktree 保留;
`--no-verify` 跳过,`--verify` 可指定命令。
- 任务依赖 + 优先级 + 自动重试:jobs 里 depends_on / priority,
拓扑排序 + 按波次跑,依赖失败时依赖任务标 skipped 不跑;
依赖任务从依赖分支建 worktree(有血缘关系),不再文件拷贝;
dispatch / parallel 里 `--max-retries`,失败后同 prompt 再跑,
重试前查预算,默认 0、行为不变。
- 评审评论回流:`review-comment --file --line --comment` 把评论
存进任务,`review` 列出 open 评论,`fix-review` 按 open 评论
逐条修、修完标 resolved。
- server 崩溃重连:Server.ensure_alive,死了重拉并 resume thread
(thread 在 CODEX_HOME 里不丢);request 遇 BrokenPipe 重试一次;
单测 kill 进程后重拉 + list 能通。
- 成本预算 + 失败通知:配置 max_daily_turns,按 turn 数算
(协议不暴露真实 token 数,这是诚实边界),超预算不派发/
不重试;usage 记在 state 里,`doctor` 可看;失败时写
host-run.log + 跑配置的 notify_command(本地 shell,
模板 {task} {status});`failures` 列失败任务,`doctor` 列
失败/缺失 worktree/open 评论。
- 多机器 state 版本:state 加 version / last_writer / updated_at,
保存时磁盘版本比加载时新就记 STATE-CONFLICT 日志并合并保留
双方任务,不静默丢;真多机中心同步仍是检测、不是云同步。
- 合并时不再提交缓存文件:merge 前清 worktree 的
__pycache__ / *.pyc,主仓的 .pyc 改动先还原;单测覆盖 pyc 不入库。
- 实测证据:测试从 3 个涨到 15 个,全部通过;模型验证在
gap2 / gap3 仓上跑通(依赖并行、评审评论修复、带自动验测的合并),
并在实测中拦下两处真问题:文件拷贝依赖分支导致重复加同一段的
合并冲突(-X ours 兜底的新增行核对拦下一次 mul2 丢失并回滚,
后改从依赖分支建 worktree 修复)、merge 的 git add -A 提交
__pycache__ / *.pyc(已修)。
- 诚实边界:独立任务在同一文件位置追加仍会合并冲突,这是 git
自身行为,host abort / 回滚、不猜合并结果;多机共享 state
现在能检测、不静默丢,但 ChatGPT 云端式中心同步不在本地 host
范围内。
## 2026-10-26:生产加固
- state 文件锁(fcntl,只锁短读/写;保存时按任务/自动化名合并
磁盘状态,多进程并发写不丢对方任务;双进程 0.5 秒窗口竞写实测
双方任务都保留)。
- 项目配置 host-config.json(`init` / `config` 命令,CLI > 配置 >
默认;API key 绝不进配置文件,只走环境变量)。
- 定时自动化:run-due 在 host-run.log 记派发/跳过,
`automations --cron-line` 只打印 crontab 行不装机,
`--install-cron` 幂等装机;本轮测试里没有实际装 crontab,
避免未经批准的模型花费,且 VM crontab 可能活不过平台回收,
Muse cron 调 run-due 是更稳的 VM 选项。
- 诚实边界:锁只防同一台机器,不防两台机器共享同一 state 文件;
host.py 本身不常驻,仍需外部调度器触发 run-due。
## 2026-10-26:真实项目跑一轮(在打包前)
- 在 codex-app-server 本仓(非敏感的 wechat-bridge)上 dogfood:
先 git init(初始提交,.gitignore 排除 367M bin/、work/、
host-state*.json、host-run.log、本地 host 配置),真实任务用
单独的 host-state-real.json。
- 并行真实任务:real-tests 加 tests/test_host.py(3 个 unittest,
全部通过,host.py 本身未改);real-backup 加 save_state 备份轮转
(.bak / .bak.1 / .bak.2,在锁内)。
- 只读委派评审 real-backup 后发现多步轮转非原子、有截断拷贝风险、
无 fsync、fcntl-less 平台问题;便宜的修复已在 fix 回合里落地
(with open 读 state / 写临时文件、修掉 real-tests 发现的
ResourceWarning、备份经临时文件 + os.replace 拷贝)。
- 两任务都合并进 main,main 测试全过且无警告,手动双保存确认
.bak 里是上一版。
- 真实项目才有的教训:非 git 项目要先 git init 才能用
worktree / 合并门;缺 .gitignore 会把 bin/ 提交进去;
真实任务与玩具任务必须用不同的 state 文件。
- 记录在 TEST-REPORT.md;本轮打包前仍欠的下一步是:安装文档、
host 快速开始、版本/变更日志、合并后主仓自动验测(那一步当时
仍是手动)——其中合并后自动验测已在“第二类缺口”里补上,
安装文档与版本/变更日志由本 CHANGELOG 与 install.sh / README
快速开始补上。
## 2026-10-25:host 编排(多线程并行、worktree 隔离、评审/合并门)
- 单个 app-server 进程托管多 thread,后台读线程按 threadId 分发
turn / completed,多 thread 的 turn 真正并发,不是轮流排队。
- `dispatch` 自动 `git worktree add -b host/<task>`,一任务一
worktree;非 git 仓库退化为独立目录拷贝。
- `review`:status + diff + untracked;`merge`:先在 worktree
提交,再 `--no-ff` 合并进主仓,冲突自动 abort;
`discard`:删 worktree 和分支。
- `run`(resume + 新回合)、`fork`、`archive`、`list`;
`delegate`:子 thread(默认 read-only)跑完,结果可
`--report-to-parent` 汇总回主 thread。
- `goal`:同一 thread 多回合,直到 `--verify` 验收命令通过
或到 `--max-rounds`。
- `automations --add` + `run-due`:到期任务自动派发,结果进
host-state.json 评审队列;未到期不重复派发(幂等实测)。
- 边界实测:read-only thread 的写文件请求被拦下、文件确实没建;
untrusted + deny 时 agent 收到审批拒绝、没拿到命令输出;
模型内 3 子代理并行检查视角确实不同;turn/interrupt 后状态
为 interrupted,`compact` 需先 resume(host.py 的 compact 命令
已包好这一步);合并冲突时主仓干净、冲突 worktree 留着人工解;
隔离 CODEX_HOME 下的 MCP 工具能被发现并调用(MCP 审批是
elicitation 协议、不是通用 decision 字段,host.py 已分流处理,
修过一轮才通);同一 thread 内换模型续跑、turn 的 model 字段
随之改变;turn/steer 能把进行中的长任务改短;3 并行 worktree
互不污染;review 时 diff 为空也要看 untracked;不存在的模型
导致 task failed 并带上游 404 错误、host 不崩。
- 工具执行修复(VM 专属):系统裸 codex 二进制缺两个同伴
(codex-code-mode-host 与 codex-resources/bwrap),工具会
fail closed;配对目录 `bin/`(codex + codex-code-mode-host +
codex-resources/bwrap,约 367M,官方 0.164.0-alpha.9)经
start.sh 优先使用;relay 支持的模型回合实测 10.3 秒内在
work/xcm-test/test-result.txt 写出“codex-ok”。Mac 上正常
npm 安装自带这三件套,不需要 bin/。
- 协议层:start.sh 启动官方 codex app-server(0.164.0-alpha.9),
零依赖 Python stdio JSON-RPC 客户端(initialize / thread/start /
turn/start、审批、resume / fork),宿主级并行多 thread 编排,
配置示例与官方协议 schema(generate-json-schema 导出)。
隔离 CODEX_HOME 下验证到模型边界为止:initialize / thread/start /
turn/start 与事件流都通,turn 随后因缺模型登录返回 401 failed,
这是登录边界、不是协议 bug。
- 诚实边界:云端派发、推送通知、Slack / GitHub 触发、GUI inline
diff 评论、Computer Use 都不在开源里、没有复刻;本版是本地
host + 本地可跑的控制面演示,不是真实云服务。
## 诚实边界(贯穿所有版本)
- 没有建成真实云服务:cloud/ 是本地可跑的控制面演示,
生产仍缺常驻机器、GitHub 远端 + PAT、Slack App、云容器账号、
模型 key 供给方式、真实 OAuth 应用注册、真实推送凭证。
- 没有真实 PR、没有真实推送:PR 只做到代码 + 本地可测部分
(假 gh、本地 bare 远端);推送只做到订阅与分发逻辑,
缺凭证时明确报错。
- 没有真实 token 成本:协议不暴露真实 token 数,预算按 turn
数算;成本端点里实际与估算严格分开,估算值标 estimated。
- 没有在任何文件、state、日志或汇报里写密钥:API key、PAT、
Slack token、OAuth secret 一律只走环境变量或 Secure Vault,
配置文件里只放 env_key 这类名字,不放值。