cloud: production control plane (API, auth/RBAC, queue, sandbox, memory/KB, fallback/cost, webhooks)
This commit is contained in:
1 parent
b97c34465f
commit
b74d99e959
17 files changed
+3420
No files matched your search
+191
@@ -0,0 +1,191 @@
|
||||
# 云端控制面(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。
|
||||
|
||||
## 启动方式
|
||||
|
||||
```bash
|
||||
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):
|
||||
|
||||
```python
|
||||
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 写进文件或聊天记录):
|
||||
|
||||
```python
|
||||
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` 表。两者配合
|
||||
才算一个完整项目,不要只调一处。
|
||||
|
||||
## 测试
|
||||
|
||||
```bash
|
||||
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 锁上限,生产量上来后需评估
|
||||
换库或单写者队列。
|
||||
Reference in new issue
Block a user