Files
codex-app-server/cloud
..

云端控制面(cloud/)

本目录是 host.py 本地编排之上的云端架构实施:控制面 server + 已就绪的模块(认证、队列、沙箱、记忆、知识库、模型路由、成本、 webhook)。零第三方依赖,只用 Python 标准库。

诚实边界:这是一个本地可跑的控制面演示,不是已建成的真实云服务。 本 server 不跑模型回合、不连真实 GitHub/Slack/云服务。生产前仍缺 的外部条件见文末“生产前仍缺什么”一节,不要绕过去。

架构:谁负责什么

客户端 / GitHub / Slack
        │  HTTP + X-API-Key(webhook 靠签名,见下)
        ▼
cloud/server.py(控制面,ThreadingHTTPServer)
  ├─ 项目 / 任务:建、查、取消、心跳、stale 回收
  ├─ 认证 / 权限 / 审计:cloud/auth.py(AuthStore)
  └─ webhook 入站:cloud/webhooks.py(验签 + 解析,只解析不执行)
        │  (后续由 worker 接线)
        ▼
worker(尚未接成 HTTP 端点,本版需另行调用模块)
  ├─ 派发顺序:cloud/queue.py(order_tasks / ready_tasks /
  │   budget_exceeded / TaskQueue;状态机与 host.py 对齐)
  ├─ 执行:cloud/sandbox.py(LocalExecutor 调 host.py CLI 做只读
  │   封装;DockerExecutor / CloudExecutor 仍是占位,未接通)
  ├─ 记忆:cloud/memory.py(MemoryStore:任务摘要,供 prompt 上下文)
  ├─ 知识库:cloud/kb.py(KBStore:按项目隔离的文档检索)
  ├─ 模型路由:cloud/models.py(ModelRouter / record_fallback:
  │   只决定 fallback,不跑模型回合)
  ├─ 成本:cloud/cost.py(CostStore / fetch_account_usage;
  │   usage 来自调用方注入的 client,本模块不联网)
  └─ 通知:cloud/webhooks.py(NotificationService:webhook + Slack
      占位分发;Web Push / APNs / FCM 明确未接,需要证书才做)

状态库:单一 SQLite 文件。AuthStore 用 users / project_members / audit_log 三张表;server 用 projects / tasks 两张表,表名不冲突。 同一个 db_path 传给两边即可。不要再用共享 JSON 文件当多用户状态库。

并发:ThreadingHTTPServer 多线程处理请求,每个请求内新建 SQLite 连接(server 表用新连接,AuthStore 每次请求新建一个实例),绝不在 多线程 handler 间共用一个连接。注意:AuthStore 自身持有一个连接, server 因此不在 handler 间共享 AuthStore 实例。

任务状态机(与 host.py 对齐):

queued → running → completed / failed / interrupted / merged / merge-failed

本版 server 只流转 queued → running(心跳)→ interrupted(取消 / sweep 回收)。completed / merged 由 worker / 控制面后续调用方在 跑完模型回合、合完分支后回写,本 server 不提供“跑模型”端点,也不 要假装一次 POST 就能让任务变 completed。

启动方式

cd ~/workspace/codex-app-server
python3 -m cloud.server [--host 127.0.0.1] [--port 8765] [--db <路径>]
# 默认:host 127.0.0.1,port 8765,db 为当前目录下的 cloud-control-plane.db

测试用(不自动 serve):

from cloud.server import create_server
server = create_server(db_path, host="127.0.0.1", port=8765)
server.serve_forever()

用户与 API key 由调用方先用 AuthStore 建(明文 key 只返回一次, 库里只存 sha256 哈希,绝不把 key 写进文件或聊天记录):

from cloud.auth import AuthStore
store = AuthStore(db_path)
user_id, api_key_plain = store.create_user("alice")

API 列表

全部返回 JSON,Content-Type: application/json。除 GET /health 与 /webhooks/* 外,所有端点要求请求头 X-API-Key,无 key 或 key 错返回 401 {"error": "unauthorized"}。/webhooks/* 不走 X-API-Key, 因为外部服务无法持本站 API key,改靠各自的签名认证(见下)。

方法与路径 权限 说明
GET /health 无需认证 200 {"ok": true, "service": "cloud-control-plane"}
POST /projects 认证即可 body {name, id?};无 id 则生成 uuid;调用者记为 owner(调 AuthStore.create_project,同时写 projects 表并记审计 create_project);返回 201 + 项目
GET /projects 认证即可 只返回调用者是成员的项目:{"projects": [...]}
POST /tasks create_task body {project_id, prompt, depends_on?, priority?, idempotency_key?};无权限 403;同 project_id + idempotency_key 已存在则返回已有任务(200,不建重复);否则建 queued 任务并记审计 create_task,返回 201
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}/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 /webhooks/github GitHub 签名 见“webhook 端点”
POST /webhooks/slack Slack 签名 见“webhook 端点”

单个项目 / 任务以裸对象返回;列表以 {"projects": [...]} / {"tasks": [...]} 包装。任务对象字段:id、project_id、prompt、 status、idempotency_key、depends_on(list)、priority、 created_by、created_at、updated_at、last_heartbeat。

函数 sweep_stale_tasks(db_path, threshold_seconds=300) -> list 独立 可单测,不依赖 HTTP;回收动作的审计 user_id 记为 "system"。

webhook 端点(加分项,已实现;只解析,不执行任务)

  • POST /webhooks/github:用 webhooks.verify_github_signature 验签(请求头 X-Hub-Signature-256),secret 从环境变量 GITHUB_WEBHOOK_SECRET 读。未设 secret 返回 503;验签失败 返回 401;成功返回 parse_github_pr_comment 的结果(PR 评论含 /fix-review 时返回 fix_review 触发点,否则 {"result": null})。 真正执行 fix-review 由控制面后续调 host.py 完成,本端点不执行。
  • POST /webhooks/slack:用 verify_slack_signature 验签(请求头 X-Slack-Signature / X-Slack-Request-Timestamp,含 300 秒防重放), secret 从环境变量 SLACK_SIGNING_SECRET 读。未设返回 503; 验签失败返回 401;成功返回 parse_slack_command 的结果,不执行任务。

权限表

角色高低:viewer < contributor < maintainer < owner。未加入项目一律 无权限。动作的最低角色(AuthStore.check_permission 的实际规则):

动作 viewer contributor maintainer owner
read(读项目任务) ✅ ✅ ✅ ✅
create_task(建任务 / 取消 / 心跳) ❌ ✅ ✅ ✅
approve_merge(批合并,worker 侧用) ❌ ❌ ✅ ✅
manage_project(仅 owner,改成员等) ❌ ❌ ❌ ✅

说明:manage_project 的最低角色是 owner 本人,不是 maintainer; 改成员(AuthStore.set_member)只有 owner 能做,否则抛 PermissionError。审计(audit / list_audit)只追加、只读,本模块 不提供删除或修改审计记录的方法。

注意一个实际签名细节:AuthStore.create_project(project_id, owner_user_id) 只写 project_members 的 owner 成员记录,不建项目 记录本身;项目记录(name 等)由 server 写 projects 表。两者配合 才算一个完整项目,不要只调一处。

测试

cd ~/workspace/codex-app-server
python3 -m unittest discover -s cloud/tests -v   # 模块单测 + 集成单测
python3 -m unittest discover -s tests -v          # 原有 host.py 测试

cloud/tests/test_server.py 用 http.client 对本地临时端口的 server 做真实 HTTP 测试(临时 SQLite 文件与临时端口,跑完关闭 server), 覆盖:无 key / 错误 key 401、非成员读项目 403、viewer 建任务 403、 幂等键第二次返回同一任务且任务数不增加、cancel 把 running 任务变 interrupted 且终态任务 cancel 返回 400、心跳后 sweep 不扫刚心跳的 任务、sweep 扫掉心跳过期(手动改旧 last_heartbeat 或 threshold=0) 的 running 任务、GET /health 不认证也通、GET /projects 只返回成员项目, 以及两个 webhook 端点的 503 / 401 / 验签成功解析。

生产前仍缺什么

以下每一项都是外部条件,缺了就不能算生产可用,不要绕过去:

  • 常驻机器 / 托管服务:本 VM 会被平台定时回收,跑在这里只能算 演示。生产需要一台常驻机器或托管服务(以及相应的进程守护与 重启策略),否则控制面会随 VM 回收一起消失。
  • GitHub 远端 + PAT:codex-app-server 目前仍无 git 远端,gh 也未登录;真实 PR 流程(host.py 的 pr / pr-checks / pr-merge) 需要先建远端并用 Secure Vault 供给 PAT,PAT 绝不写文件。
  • Slack App:需要真实 Slack App(签名密钥、斜杠命令、事件订阅) 才能接通 SLACK_SIGNING_SECRET 背后的入站;目前只有验签与解析, 且 NotificationService 的 Slack 发送端仍是占位注入。
  • 云容器账号:CloudExecutor 仍是占位(无账号即抛错),Docker 执行也未接通容器挂载 / 网络隔离 / 资源限额;接通前不要宣称任务 已在云端或容器里跑。
  • 模型 key 供给方式:这是当前最实在的卡点。上次 real-nightly-tests automation 就卡在这里:定时进程手里没有中转站 key,key 从不存文件、只在进程环境里 transient 供给。控制面要跑 真实模型回合,必须先由用户定下供给方式(例如常驻进程的环境、 Secure Vault 注入、或其它经用户明确同意的渠道),而不是把 key 写进配置文件、数据库或代码里凑合。定下之前,定时自动化与 worker 的模型回合都只能停在“失败路径可用、模型跑不起来”的状态。
  • 其它待接线(非外部条件,但同样未完成):memory / KB / 成本 / 模型路由尚未接成 HTTP 端点;OAuth(GitHub/Google)未接,首版只有 API key + 哈希认证;Web Push / APNs / FCM 推送未接,需要证书才做; 单机 SQLite 在高并发写下仍有 busy 锁上限,生产量上来后需评估 换库或单写者队列。