Docker sandbox real test + cloud merge status endpoint

- DockerExecutor: extra mounts (host.py work/state, repo, codex-home
  at same paths), worker --docker-extra-mounts, container user
  configurable; Dockerfile copies codex-resources beside the codex
  binary and puts bwrap on PATH (previous /usr/local/lib layout
  made codex report bubblewrap unavailable inside the container).
- Real Docker task on external host: triple added in ~18s, merged
  inside a container, main repo now add+mul2+triple, --rm cleanup.
  Honest boundary: this host/kernel forbids unprivileged user
  namespaces, so bwrap fails on host and in containers; production
  mode here is Docker isolation + inner danger-full-access, not
  double sandboxing.
- Cloud merge status: POST /tasks/{id}/merge syncs status only
  (merge itself stays with host.py merge); completed -> merged,
  maintainer+ (approve_merge), other states and repeat calls 409,
  audit merge_task. Tests: cloud 223, host 27, all passing.
This commit is contained in:
host committed 2026-10-26 02:46:10 +08:00
1 parent 631580beab
commit 07a8eaaa73
9 files changed
+1082 -37

No files matched your search

+54 -6
View File
@@ -42,9 +42,11 @@ queued → running → completed / failed / interrupted / merged / merge-failed
```
本版 server 只流转 queued → running(心跳)→ interrupted(取消 /
sweep 回收)。`completed` / `merged` 由 worker / 控制面后续调用方在
跑完模型回合、合完分支后回写,本 server 不提供“跑模型”端点,也不
要假装一次 POST 就能让任务变 completed。
sweep 回收),以及 completed → merged(见下表 `POST /tasks/{id}/merge`)。
`completed` 由 worker 在跑完模型回合后回写;`merged` 只由合并
状态接口同步,不执行合并本身——合并仍由 host.py merge(本地
或 Docker 容器内)执行,成功后再调本接口,云端不该在任务
completed 时自动改主分支。
## 启动方式
@@ -166,6 +168,7 @@ psycopg,写明“需安装 psycopg”),绝不静默回落到 SQLite,
| `GET /tasks?project_id=...` | 该项目 `read` | 返回该项目任务列表:`{"tasks": [...]}`;无权限 `403` |
| `GET /tasks/{id}` | 该任务所属项目 `read` | 无权限 `403`;不存在 `404` |
| `POST /tasks/{id}/cancel` | 该项目 `create_task` | `queued` / `running` 改为 `interrupted` 并记审计 `cancel_task`;已是终态(completed / failed / interrupted / merged / merge-failed)返回 `400`;不存在 `404` |
| `POST /tasks/{id}/merge` | 该项目 `approve_merge`(maintainer 以上) | 只同步状态,不执行合并:仅 `completed` 可改为 `merged` 并记审计 `merge_task`,其它状态(含已 merged 的重复调用)返回 `409`;不存在 `404`,越权 `403`,无 key `401` |
| `POST /tasks/{id}/heartbeat` | 该项目 `create_task` | 更新 `last_heartbeat = now`;状态为 `queued` 则改为 `running`;返回任务 |
| `POST /admin/sweep` | 认证即可 | 调 `sweep_stale_tasks(db_path, threshold_seconds=300)`(body 可传 `threshold_seconds` 覆盖默认值);把 `running` 且 `last_heartbeat`(无心跳则用 `updated_at`)早于 `now - threshold` 的任务改为 `interrupted` 并记审计 `sweep_interrupted`;返回 `{"swept": [ids]}` |
| `POST /tasks/{id}/cost` | 该任务所属项目 `read` | 成本聚合:`{task_id, project_id, total_tokens, usd_micros, turns, fallback_count, actual_tokens, estimated_tokens}`;真数与估算分开;不存在 `404`,越权 `403` |
@@ -258,9 +261,11 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
- **Slack App**:需要真实 Slack App(签名密钥、斜杠命令、事件订阅)
才能接通 `SLACK_SIGNING_SECRET` 背后的入站;目前只有验签与解析,
且 `NotificationService` 的 Slack 发送端仍是占位注入。
- **云容器账号**:`CloudExecutor` 仍是占位(无账号即抛错),Docker
执行也未接通容器挂载 / 网络隔离 / 资源限额;接通前不要宣称任务
已在云端或容器里跑。
- **云容器账号**:`CloudExecutor` 仍是占位(无账号即抛错)。
Docker 执行已接通(见下文“Docker 沙箱执行器”),但真实
docker run、镜像构建、资源限额实测仍需在外部主机上做:
本 VM 没有 Docker,本轮未连外部主机、未向真实外部服务发
请求、未跑真实模型回合,不要宣称 Docker 沙箱已在真实环境验证。
- **模型 key 供给方式**:这是当前最实在的卡点。上次
real-nightly-tests automation 就卡在这里:定时进程手里没有中转站
key,key 从不存文件、只在进程环境里 transient 供给。控制面要跑
@@ -308,3 +313,46 @@ interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳
的 Store 连接已统一走 cloud.db 的 WAL + busy_timeout 连接
(Postgres 后端仍是骨架,驱动缺失时启动即失败、不先建 SQLite
文件,这一点迁移后未退化)。
## Docker 沙箱执行器(本轮新增)
- 用法:worker 选 docker 执行器——
`python3 -m cloud.worker --db <库> --executor docker
[--docker-image codex-app-server:latest] [--docker-cpus 2]
[--docker-memory 2g] [--docker-user 1000:1000]
[--docker-worktrees-root <目录>] [--docker-timeout <秒>]`;
或在代码里直接传 `DockerExecutor(...)` 给 `Worker(executor=...)`。
选 docker 但本机无 docker 时任务明确失败,error_text 含
docker 缺失,绝不静默降级为本地执行;选 local 时行为不变。
- 命令构造(示例,任务 id 为 t1、worktree 为 /tmp/wt-t1 时):
`docker run --rm --name codex-t1 --user 1000:1000 --cpus 2
--memory 2g -v /tmp/wt-t1:/work -v <代码目录>:/app:ro -w /work
-e XCM_API_KEY codex-app-server:latest python3 /app/host.py
dispatch --name t1 --task <提示>`。要点:只挂任务自己的
worktree(可写)和代码目录(只读),绝不挂宿主根目录、绝不
挂任何含密钥的目录;容器名由任务 id 派生(仅安全字符);
`-e` 只传 env_key 名,不带值,key 缺失时容器根本不启动;
资源限额默认 2 核 / 2G,可配置;容器内进程以非 root 用户跑。
- 镜像:`cloud/docker/Dockerfile`,基于 python:3.12-slim,装
git,复制调用方提供的配对 codex 二进制(codex +
codex-code-mode-host + codex-resources)。构建命令示例(在
仓库根目录,构建上下文就是仓库根):
`docker build -f cloud/docker/Dockerfile
-t codex-app-server:latest --build-arg CODEX_BIN_DIR=bin .`
bin/ 不进仓库,构建主机上没有配对二进制就构建失败;代码
不在镜像里,运行时只读挂载到 /app,代码改了不用重新构建,
codex 版本变了才需要重新构建;镜像里不含任何 key。
- 清理与取消:`docker run --rm`,正常失败也自动删容器;超时
时先 docker kill 再 docker rm -f;worker 取消时按任务派生的
容器名 docker kill,不只改数据库状态。
- 网络白名单的诚实边界:docker run 默认 bridge 网络,本执行
器不做 egress 白名单——真正的“只放行模型 API”需要宿主防火
墙或代理规则,那是部署项,不是本轮代码假装做到的,不要宣称
已实现网络白名单。另有一项待实测:codex 自带的 bubblewrap
沙箱在容器内是否可用,取决于宿主的容器权限配置,本镜像不
假装解决;若 bwrap 不可用,host.py 的 sandbox 参数仍可退到
read-only / workspace-write 的控制面约束。
- 仍需在外部主机上实测(本 VM 没有 Docker,本轮未连外部主机):
真实 docker build、真实 docker run、资源限额在真实负载下的
表现、bwrap 在容器内的可用性、以及取消时容器确实消失。