148 lines
9.0 KiB
Markdown
148 lines
9.0 KiB
Markdown
# Gitea Webhook Agent 部署与运维设计
|
||
|
||
> 本文是 [PRD.md](./PRD.md) 与 [ARCHITECTURE.md](./ARCHITECTURE.md) 的运行手册。
|
||
> 它定义部署、凭据、恢复和故障处置边界;Gitea 仍是唯一主交互入口,cc-web 页面
|
||
> 只承担配置、镜像和运维控制。
|
||
|
||
## 1. 运行基线
|
||
|
||
MVP 采用单 Gitea 实例(`instanceId=default`)和专用 `ccweb-bot`。同仓库永远
|
||
串行,跨仓库允许并行,不设置全局并发上限。生产环境至少需要:
|
||
|
||
| 项目 | 基线 |
|
||
|---|---|
|
||
| Node/Bun | 使用项目锁定的运行时和已验收的单文件发布包;不要混用系统 Node |
|
||
| 进程 | 一个 cc-web worker;外部 Codex App Server 按会话复用 |
|
||
| 持久目录 | `WORKFLOW_DATA_ROOT`(任务/审计/租约)与 `WORKSPACE_ROOT`(Git 工作区)分离 |
|
||
| 网络 | Gitea Webhook 入站、Gitea REST/MCP 出站、Codex App Server 本地 stdio/JSON-RPC |
|
||
| 时间 | 主机启用 NTP;持久化时间全部使用 UTC ISO-8601 |
|
||
| 访问 | 反向代理终止 TLS;Webhook 路径只允许 POST 和受限请求体大小 |
|
||
|
||
建议为 worker、工作区和日志使用独立系统用户。`WORKFLOW_DATA_ROOT` 仅 worker
|
||
可读写,`WORKSPACE_ROOT` 不允许 Web 静态服务暴露,备份目录使用不同权限主体。
|
||
|
||
## 2. 配置与凭据
|
||
|
||
配置按“非敏感文件 → Secret 管理 → 线程级注入”分层。示例(值均为占位符):
|
||
|
||
```ini
|
||
CC_WEB_GITEA_HOST=https://gitea.example
|
||
CC_WEB_GITEA_INSTANCE_ID=default
|
||
CC_WEB_GITEA_BOT_LOGIN=ccweb-bot
|
||
GITEA_WEBHOOK_SECRET_FILE=/etc/ccweb/secrets/gitea-webhook
|
||
GITEA_BOT_TOKEN_FILE=/etc/ccweb/secrets/gitea-bot-token
|
||
CC_WEB_GITEA_WORKFLOW_STATE=/var/lib/ccweb/gitea-workflow/state.json
|
||
CC_WEB_GITEA_WORKSPACE_ROOT=/var/lib/ccweb/workspaces
|
||
```
|
||
|
||
cc-web 管理页对应接口为:
|
||
|
||
- `GET /api/gitea-workflow/config`:返回地址、Bot 用户名、工作区和凭据是否已配置;不会返回 Secret/Token。
|
||
- `PUT /api/gitea-workflow/config`:保存上述配置,敏感字段写入受保护的运行配置;保存后立即更新当前进程的 Webhook、Git 和 gitea-mcp 线程配置,不要求重启 cc-web。
|
||
- `GET /api/gitea-workflow/overview`、`/tasks`、`/audit`:状态镜像。
|
||
- `POST /api/gitea-workflow/control/pause|resume`、仓库 `enable|disable`、任务 `cancel|abort`:需要 cc-web 登录 Bearer Token;服务端自动记录操作来源和请求幂等 ID,页面不要求用户填写审计字段。
|
||
|
||
- 内部部署可以不配置 Webhook Secret;配置后入口启用 HMAC-SHA256 验签。Bot Token
|
||
只在 gitea-mcp/REST 出站边界使用,不能把 Bot Token 当作 Webhook Secret。
|
||
- Secret 文件权限为 `0600`、属主为 worker;日志、审计、评论正文和 Git remote
|
||
URL 禁止写入 Secret。审计只记录 `secretRef/tokenRef` 和不可逆指纹。
|
||
- `thread/start.config.mcp_servers.gitea.env` 使用线程级 secret 引用或运行时
|
||
注入值;不能把某个来源会话 ID、Bot Token 或 Gitea Secret 放在长驻
|
||
app-server 的进程级全局环境中。
|
||
- `gitea-mcp` 随 cc-web 源码/发布目录放在 `bin/gitea-mcp`,不需要为每个服务单独
|
||
安装。只有替换版本时才使用 `CC_WEB_GITEA_MCP_COMMAND` 指向本机绝对路径;可选的
|
||
`CC_WEB_GITEA_MCP_STARTUP_TIMEOUT_MS` 控制 initialize 预检超时(默认 8000ms,
|
||
上限 30000ms)。cc-web 发布流程不从网络自动下载或安装外部二进制。
|
||
- 轮换 Secret 时先配置新值并验证签名,再撤销旧值;未配置 Secret 的内部入口应
|
||
由反向代理/网络白名单限制来源。Bot Token 更新后立即作用于后续 MCP 线程。
|
||
|
||
## 3. Webhook 与反向代理
|
||
|
||
Gitea 只向反向代理暴露的 HTTPS 路径发送 Webhook。代理应:
|
||
|
||
1. 严格保留原始请求 body 和 `X-Gitea-Signature`、`X-Gitea-Delivery`、
|
||
`X-Gitea-Event` 头,不做 JSON 重排后再交给验签层。
|
||
2. 限制方法为 POST、请求体大小(建议 1 MiB)和连接超时;超限返回 413。
|
||
3. 只将来自 Gitea 网段的请求转发到 worker;不要在代理层伪造签名或 delivery。
|
||
4. 配置了 Secret 时,worker 验签失败返回 401;解析失败返回 400,首次入队返回
|
||
202,重复 delivery/忽略事件返回 200。未配置 Secret 的内部入口仍需由代理
|
||
限制来源;持久化不可用时返回 503。
|
||
5. 代理访问日志脱敏 `Authorization`、签名值和正文;保留 request ID 以便与
|
||
`deliveryKey` 关联。
|
||
|
||
验签必须针对 raw body 计算 HMAC-SHA256,并使用常量时间比较。解析后的 mention
|
||
和仓库字段不能替代 raw body 验签;缺少 delivery header 时只能使用可靠 payload
|
||
ID,否则拒绝入队。
|
||
|
||
## 4. 启动、健康检查与发布
|
||
|
||
启动顺序固定为:
|
||
|
||
1. 检查配置文件、Secret 文件权限、数据目录可写性和工作区根目录是否在允许
|
||
根下;禁止自动 `reset --hard` 或 `clean`。
|
||
2. 打开持久 Store,执行租约过期扫描和恢复扫描;`queued` 重新入队,
|
||
`running/preparing/aborting` 按架构规则转为带 `interrupted_by_restart` 的
|
||
`retry_wait`,终态不重跑。
|
||
3. 启动 Scheduler 和受控的 Codex App Server;完成 `initialize` 后发送
|
||
`initialized`,再 best-effort 探测 goals 与 collaboration mode。
|
||
4. 仅在健康检查通过后把 Webhook 代理切入当前 worker。
|
||
|
||
健康检查至少包含:Store 读写探针、租约扫描完成、Gitea `/api/v1/version` 或
|
||
等价只读探针、Codex App Server initialize smoke,以及线程级 gitea-mcp
|
||
stdio `initialize` 预检。健康检查不得创建评论、修改仓库或打印凭据。
|
||
|
||
发布采用“先旁路验证、再切流”:在 staging 用回归 fixtures 和真实 Gitea 测试
|
||
仓库验证 HMAC、入队、MCP 配置与回帖确认;生产切换前确认旧 worker 已停止领取
|
||
新任务且租约已释放。回滚只切换到上一份已验收发布包和数据快照,不删除当前
|
||
工作区或审计。
|
||
|
||
## 5. 日常运维控制
|
||
|
||
| 操作 | 影响 | 约束 |
|
||
|---|---|---|
|
||
| 全局 pause | 停止领取新任务,已运行任务继续 | 页面按钮直接执行,服务端记录操作 |
|
||
| resume | 恢复调度 | 先检查工作区和 Gitea 连通性 |
|
||
| repo disable | 拒绝该仓库新触发 | 不强杀运行中任务,保留历史 |
|
||
| cancel queued | 取消未启动任务 | 幂等;不得取消已领取任务 |
|
||
| abort running | 向 Codex App 请求取消 | 等待确认或超时后释放租约并审计 |
|
||
| replay delivery | 仅用于核对/恢复 | 先查 `deliveryKey`,禁止绕过去重直接执行 |
|
||
|
||
所有控制操作必须通过受保护的运维 API;服务端生成操作记录和幂等 ID。API 不接受
|
||
任意 Git 命令、模型切换或 MCP 工具参数。
|
||
|
||
## 6. 监控与告警
|
||
|
||
结构化日志只输出关联 ID:`deliveryKey、taskId、repoKey、sessionKey、threadId、
|
||
turnId、leaseId、errorCode`。建议监控:Webhook 401/400/503 比例、队列年龄、锁等待、
|
||
运行任务数量、`retry_wait` 数量、`blocked_workspace` 数量、MCP 握手失败、回帖确认
|
||
缺失和 REST 兜底次数。
|
||
|
||
以下情况应告警并暂停自动扩容:连续 HMAC 失败、同一 delivery payload hash
|
||
变化、租约频繁过期、回帖兜底连续失败、工作区出现未知 owner 或路径越界、
|
||
Bot Token/Secret 可能泄露。告警正文不得包含 Secret、完整评论正文或源码。
|
||
|
||
## 7. 备份、恢复与数据保留
|
||
|
||
- 备份 Store 的任务、会话、turn、评论索引、控制面和 append-only 审计;
|
||
`webhook_deliveries` 至少保留覆盖 Gitea 重试窗口的记录。
|
||
- 工作区是可重建缓存,备份前先记录 `repoKey、HEAD、dirty 状态`;不要把
|
||
未提交用户修改静默覆盖或当成可恢复快照。
|
||
- 恢复顺序:停止领取 → 恢复 Store → 校验版本/唯一键 → 执行租约恢复扫描 →
|
||
只读检查工作区 → 启动 Scheduler → 逐步 resume。恢复后优先核对
|
||
`deliveryKey → taskId → turnId → commentId` 链路,避免重复回帖。
|
||
- 清理策略必须由管理员显式启用并保留审计;不得因磁盘告警直接删除运行中
|
||
任务、未确认评论或工作区。
|
||
|
||
## 8. 故障处置速查
|
||
|
||
| 症状 | 先查 | 安全动作 |
|
||
|---|---|---|
|
||
| Webhook 全部 401 | Secret 引用、raw body、代理头 | 暂停切流,验证单个 fixture,不打印签名 |
|
||
| 任务堆积 | pause、锁、并发槽位、租约 | 不手工改状态;修复后让恢复扫描接管 |
|
||
| 工作区 blocked | `git status`、owner、锁文件 | 保留现场,管理员审查后再解除;禁止 reset/clean |
|
||
| App/MCP 不可达 | initialize、stdio stderr、hostRef、`gitea-mcp` 可执行文件 | 命令缺失、进程启动失败、握手失败或超时均立即进入 `failed` 并发送一条 `giteabot:` 失败回执;修复后重新触发 |
|
||
| MCP 成功但无评论 | resource/task/turn 标记、Gitea 查询 | 只允许一次 reply_retry,随后 REST 兜底 |
|
||
| 重启后重复执行 | stateVersion、leaseId、终态记录 | 先 pause,修复 Store/租约,再恢复;禁止批量重放 |
|
||
|
||
每次事故结束后导出关联审计、错误码、重试次数和最终评论 ID,形成可复盘记录。
|