feat: support MCP elicitation and rebuild release
This commit is contained in:
510
docs/gitea-workflow/ARCHITECTURE.md
Normal file
510
docs/gitea-workflow/ARCHITECTURE.md
Normal file
@@ -0,0 +1,510 @@
|
||||
# Gitea Webhook 驱动 ccweb 工作流架构设计
|
||||
|
||||
> 设计基线:[PRD.md](./PRD.md)
|
||||
> 目标:定义并实现可实施、可恢复、可审计的协议与数据边界。
|
||||
> MVP:单 Gitea 实例、全局 `ccweb-bot`。
|
||||
|
||||
## 1. 设计原则与边界
|
||||
|
||||
1. **事件先持久化,执行后置**:Webhook 线程只做验签、去重、规范化和入队,
|
||||
不同步等待 Codex App 或 Gitea 回帖。
|
||||
2. **仓库是互斥单元,资源是会话单元**:`repoKey` 决定工作区锁,
|
||||
`sessionKey` 决定 Issue/PR 独立线程;二者不能混用。
|
||||
3. **所有外部动作可重放且幂等**:delivery、起始回执、MCP 最终回帖和 REST
|
||||
兜底均带关联键,重复请求只能得到已有结果。
|
||||
4. **Gitea 是事实来源**:cc-web 的状态是工作流镜像;最终用户内容必须能在
|
||||
Gitea Issue/PR 中查到,MCP 返回成功不等于回帖已落库。
|
||||
5. **运行时协议贴近 Codex App**:初始化后发送 `initialized`,能力探测失败只
|
||||
降级记录;使用 collaboration mode 时参数放入 `settings`,不重复传顶层字段。
|
||||
6. **线程级注入来源上下文**:Gitea MCP 配置随 `thread/start.config` 注入,
|
||||
不使用长驻 app-server 的进程级环境承载某个来源会话的上下文。
|
||||
|
||||
当前实现采用项目现有 JSON 持久化(`lib/gitea-workflow-store.js`),并由
|
||||
`server.js` 挂接 HTTP 路由、`lib/gitea-workflow-*.js` 提供领域实现、`public/`
|
||||
提供管理镜像。所有实现仍必须满足下文的原子写、唯一约束和恢复语义。
|
||||
|
||||
## 2. 逻辑拓扑
|
||||
|
||||
```text
|
||||
┌──────────────┐ HMAC Webhook ┌──────────────────────────┐
|
||||
│ Gitea 实例 │ ─────────────────────────▶ │ Webhook Adapter │
|
||||
│ Issue/PR │ │ 验签/规范化/去重/入队 │
|
||||
└──────┬───────┘ └──────────┬───────────────┘
|
||||
│ REST 查询/兜底 │
|
||||
│ ▼
|
||||
│ ┌──────────────────────┐
|
||||
│ │ Workflow Store │
|
||||
│ │ tasks/sessions/audit │
|
||||
│ └──────────┬───────────┘
|
||||
│ │
|
||||
│ ┌──────────▼───────────┐
|
||||
│ │ Scheduler │
|
||||
│ │ repo lock │
|
||||
│ └──────┬───────┬────────┘
|
||||
│ │ │
|
||||
│ ┌────────────▼─┐ ┌───▼─────────────┐
|
||||
│ │ Workspace │ │ Session Manager │
|
||||
│ │ clone/fetch │ │ thread/turn │
|
||||
│ └──────┬────────┘ └───┬─────────────┘
|
||||
│ │ │ JSON-RPC
|
||||
│ │ ▼
|
||||
│ │ ┌──────────────────┐
|
||||
│ └─────▶│ Codex App Server │
|
||||
│ │ codexapp + yolo │
|
||||
│ └────────┬─────────┘
|
||||
│ │ stdio MCP
|
||||
│ ▼
|
||||
└─────────────────────────────────────────────────┌───────────────┐
|
||||
│ gitea-mcp │
|
||||
│ 官方 stdio │
|
||||
└───────────────┘
|
||||
|
||||
运维 API/UI ──只读查询/控制──▶ Workflow Store + Scheduler + Audit
|
||||
```
|
||||
|
||||
组件职责:
|
||||
|
||||
| 组件 | 职责 | 不负责 |
|
||||
|---|---|---|
|
||||
| Webhook Adapter | 原始 body 验签、delivery 去重、Bot/mention 过滤、规范化 | 启动 Agent、直接执行 Git |
|
||||
| Workflow Store | 任务、会话、turn、评论、仓库、控制面和审计的持久化 | 远程调用 |
|
||||
| Scheduler | FIFO、每仓库 lease、恢复扫描 | 解析业务 prompt |
|
||||
| Workspace Manager | 自动登记、clone/fetch、分支上下文、脏目录保护 | 修改用户代码内容 |
|
||||
| Session Manager | Codex App initialize、thread/turn、事件序列、取消 | 直接向 Gitea 发最终正文 |
|
||||
| MCP Bridge | 为线程启动官方 `gitea-mcp` stdio 并传递 host/token | 代替 Agent 决策 |
|
||||
| Reply Verifier | 查询评论、校验 taskId/turnId/resourceKey、触发有限补偿 | 无条件重复回帖 |
|
||||
| Control/Audit API | 暂停、停用、取消、中止、查询和审计导出 | 让用户切换 Agent 模式 |
|
||||
|
||||
## 3. 标识符与幂等键
|
||||
|
||||
| 名称 | 格式/来源 | 唯一性与用途 |
|
||||
|---|---|---|
|
||||
| `instanceId` | 固定 `default` | 为未来多实例预留命名空间 |
|
||||
| `repoKey` | `default:<owner>/<repo>` | 仓库锁、工作区和并发分区 |
|
||||
| `resourceKey` | `<repoKey>:<kind>:<number>` | Issue/PR 资源身份 |
|
||||
| `sessionKey` | 同 `resourceKey` | 一个资源一个 Codex App thread |
|
||||
| `deliveryKey` | `default:<X-Gitea-Delivery>` | Webhook 去重;无 header 时使用可靠 payload ID,否则拒绝 |
|
||||
| `taskId` | `gitea-task-` + deliveryKey 的 SHA-256 前缀 | 一次用户评论触发的一次可审计任务;同一 delivery 重放保持不变 |
|
||||
| `threadId` | Codex App 返回值 | 跨任务复用同一资源上下文 |
|
||||
| `turnId` | Codex App 每轮返回值 | 最终回帖确认和恢复边界 |
|
||||
| `commentId` | Gitea 返回值 | 状态/最终/兜底评论幂等引用 |
|
||||
| `leaseId` | UUID + expiry | Scheduler 持有仓库锁和全局槽位的租约 |
|
||||
|
||||
所有唯一键写入必须在同一事务/原子文件替换中完成。重复请求返回原记录,不
|
||||
生成新的 `taskId`、`turnId` 或评论。
|
||||
|
||||
## 4. 持久化数据模型
|
||||
|
||||
字段名是跨模块契约;时间统一 ISO-8601 UTC,所有枚举使用小写 snake_case。
|
||||
|
||||
### 4.1 `webhook_deliveries`
|
||||
|
||||
```json
|
||||
{
|
||||
"deliveryKey": "default:8f7…",
|
||||
"deliveryId": "8f7…",
|
||||
"eventType": "issue_comment",
|
||||
"receivedAt": "2026-08-24T08:00:00Z",
|
||||
"signatureValid": true,
|
||||
"normalized": true,
|
||||
"taskId": "task-uuid-or-null",
|
||||
"status": "accepted|ignored|duplicate|rejected",
|
||||
"payloadHash": "sha256:…",
|
||||
"expiresAt": "2026-09-23T08:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
`deliveryKey` 唯一;保留期必须覆盖 Gitea 可能的重试窗口。验签失败可只写安全
|
||||
审计,不写入可用于重放的业务 payload。
|
||||
|
||||
### 4.2 `repositories`
|
||||
|
||||
```json
|
||||
{
|
||||
"repoKey": "default:acme/widget",
|
||||
"instanceId": "default",
|
||||
"owner": "acme",
|
||||
"name": "widget",
|
||||
"cloneUrl": "https://gitea.example/acme/widget.git",
|
||||
"workspacePath": "/workspace/default/acme/widget",
|
||||
"defaultBranch": "main",
|
||||
"status": "active|disabled|blocked",
|
||||
"registeredAt": "2026-08-24T08:00:00Z",
|
||||
"lastFetchAt": "2026-08-24T08:02:00Z",
|
||||
"version": 3
|
||||
}
|
||||
```
|
||||
|
||||
注册采用 compare-and-swap:并发首次触发只能产生一条记录。Token 不落在
|
||||
`cloneUrl`;`workspacePath` 必须经过根目录规范化,禁止路径穿越。
|
||||
|
||||
### 4.3 `sessions`
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionKey": "default:acme/widget:issue:42",
|
||||
"resourceKey": "default:acme/widget:issue:42",
|
||||
"kind": "issue",
|
||||
"number": 42,
|
||||
"threadId": "thread_…",
|
||||
"runtime": {"app": "codexapp", "mode": "yolo"},
|
||||
"mcp": {"server": "gitea", "transport": "stdio", "hostRef": "gitea-main"},
|
||||
"status": "active|waiting_user|closed",
|
||||
"lastTaskId": "task-uuid",
|
||||
"lastTurnId": "turn_…",
|
||||
"createdAt": "2026-08-24T08:03:00Z",
|
||||
"updatedAt": "2026-08-24T08:10:00Z",
|
||||
"version": 7
|
||||
}
|
||||
```
|
||||
|
||||
`sessionKey` 唯一,`threadId` 创建后不可因普通重试而替换;只有明确恢复失败
|
||||
并经审计后才允许新线程。
|
||||
|
||||
### 4.4 `tasks`
|
||||
|
||||
```json
|
||||
{
|
||||
"taskId": "task-uuid",
|
||||
"deliveryKey": "default:8f7…",
|
||||
"repoKey": "default:acme/widget",
|
||||
"resourceKey": "default:acme/widget:issue:42",
|
||||
"sessionKey": "default:acme/widget:issue:42",
|
||||
"commentId": 101,
|
||||
"author": "alice",
|
||||
"instruction": "请修复……",
|
||||
"state": "queued",
|
||||
"attempt": 0,
|
||||
"replyAttempt": 0,
|
||||
"threadId": "thread_…",
|
||||
"turnId": null,
|
||||
"leaseId": null,
|
||||
"nextRetryAt": null,
|
||||
"errorCode": null,
|
||||
"createdAt": "2026-08-24T08:00:01Z",
|
||||
"updatedAt": "2026-08-24T08:00:01Z",
|
||||
"stateVersion": 1
|
||||
}
|
||||
```
|
||||
|
||||
任务状态必须与 PRD 第 6 节一致;`stateVersion` 防止并发 worker 的旧写覆盖新写。
|
||||
|
||||
### 4.5 `turns`、`comments`、`audit_events` 与控制面
|
||||
|
||||
```json
|
||||
{
|
||||
"turn": {
|
||||
"turnId": "turn_…",
|
||||
"taskId": "task-uuid",
|
||||
"threadId": "thread_…",
|
||||
"kind": "normal|reply_retry",
|
||||
"status": "started|completed|interrupted|failed|cancelled",
|
||||
"eventSeq": 18,
|
||||
"startedAt": "…",
|
||||
"completedAt": null
|
||||
},
|
||||
"comment": {
|
||||
"commentKey": "task-uuid:status:running",
|
||||
"taskId": "task-uuid",
|
||||
"kind": "status|mcp_final|rest_fallback",
|
||||
"resourceKey": "default:acme/widget:issue:42",
|
||||
"turnId": "turn_…",
|
||||
"commentId": 102,
|
||||
"bodyHash": "sha256:…",
|
||||
"status": "pending|sent|confirmed|failed"
|
||||
},
|
||||
"audit_event": {
|
||||
"eventId": "audit-uuid",
|
||||
"taskId": "task-uuid-or-null",
|
||||
"action": "webhook.accept|task.claim|mcp.call|reply.confirm|…",
|
||||
"actor": "system|admin:alice|gitea:user:alice",
|
||||
"fromState": "running",
|
||||
"toState": "verifying_reply",
|
||||
"deliveryId": "8f7…",
|
||||
"turnId": "turn_…",
|
||||
"commentId": 102,
|
||||
"errorCode": null,
|
||||
"timestamp": "…",
|
||||
"metadata": {"attempt": 0}
|
||||
},
|
||||
"control": {
|
||||
"globalPaused": false,
|
||||
"pauseReason": null,
|
||||
"updatedBy": "admin:alice",
|
||||
"updatedAt": "…",
|
||||
"version": 4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`commentKey` 是起始/异常结果回执的幂等键;最终回帖以 `taskId+turnId` 为主键,REST
|
||||
兜底不能覆盖已确认的 MCP 评论。
|
||||
|
||||
## 5. 处理时序与事务边界
|
||||
|
||||
### 5.1 Webhook 接收
|
||||
|
||||
1. 读取 raw body 和签名 header;HMAC 失败立即 401。
|
||||
2. 提取 delivery/event header,计算 `payloadHash`;在
|
||||
`webhook_deliveries` 上做唯一插入。
|
||||
3. 已存在 delivery 时返回原处理结果;首次插入后解析事件。
|
||||
4. 过滤非评论、无 mention、Bot 自评论、停用仓库;分别落为
|
||||
`ignored/rejected`,并追加审计。
|
||||
5. 在一个原子操作中登记仓库(若需要)、创建 Session 引用和 Task,状态为
|
||||
`received` → `queued`,写入 delivery 与 task 的关联。
|
||||
6. 立即返回 202(已入队)和 `taskId`;不等待 clone、Codex App 或评论发送。
|
||||
|
||||
### 5.2 调度与工作区
|
||||
|
||||
1. Scheduler 扫描 `queued`、到期 `retry_wait` 和管理员解除的
|
||||
`blocked_workspace`。
|
||||
2. 先检查全局暂停、仓库 active,再以租约方式获取 repo lock。
|
||||
3. CAS 将任务置为 `preparing`;租约含 `leaseId/owner/expiresAt`,心跳续租。
|
||||
4. Workspace Manager 创建目录或执行安全 fetch。失败按错误矩阵决定
|
||||
`blocked_workspace`、`retry_wait` 或 `failed`。
|
||||
5. 任务入队后发送唯一的幂等 `giteabot: 已收到` 起始回执,再将任务置为
|
||||
`running` 并启动/恢复 turn;运行中、等待用户和成功状态不再额外发评论。
|
||||
|
||||
### 5.3 Codex App 与 MCP
|
||||
|
||||
1. 对长驻 app-server 只初始化一次;成功后发送 `initialized` notification。
|
||||
2. best-effort 探测 `experimentalFeature/enablement/set(goals=true)` 和
|
||||
`collaborationMode/list`;探测失败只记日志并保留降级能力。
|
||||
3. `thread/start` 注入工作区 cwd 和线程级 `mcp_servers.gitea`;若使用
|
||||
collaboration mode,`model/reasoning_effort/developer_instructions` 放在
|
||||
`collaborationMode.settings`,顶层不得重复。
|
||||
4. `turn/start` 仅传本轮指令和关联元数据,落库返回的 `turnId`;按事件序号
|
||||
更新 `turns.eventSeq`,重复事件不重复处理。
|
||||
5. turn 完成后根据 stop reason 判断 `waiting_user`、执行失败或进入
|
||||
`verifying_reply`。
|
||||
|
||||
### 5.4 回帖确认与补偿
|
||||
|
||||
1. 仅当 Agent 通过 gitea-mcp 发起最终评论后,才把任务置为
|
||||
`verifying_reply`。
|
||||
2. Reply Verifier 通过 Gitea API/MCP 查询原资源评论,匹配 Bot 作者、
|
||||
`taskId`、`turnId` 和 resourceKey 标记;确认成功则 `succeeded`。
|
||||
3. 只有 Gitea 查询明确返回“评论缺失”时,才将 `replyAttempt += 1` 并最多创建
|
||||
一个 `reply_retry` turn;prompt 明确“只补发上一轮最终文本,不重复修改”。
|
||||
查询异常/状态未知不自动写操作,直接标记 `failed_reply` 并发送状态告警。
|
||||
4. `reply_retry` 仍无法确认时,使用 REST Bot Token 发一条带同样关联标记的兜底评论;
|
||||
成功为 `succeeded_with_rest_fallback`,失败为 `failed_reply`。每一步都写审计。
|
||||
|
||||
### 5.5 重启恢复
|
||||
|
||||
启动恢复扫描按以下顺序执行:
|
||||
|
||||
1. 未过期租约保留 owner;过期租约释放 repo lock 并记录事件。
|
||||
2. `queued` 直接重新入队;`retry_wait` 按 `nextRetryAt` 等待。
|
||||
3. `running/preparing/aborting` 若无活跃 worker,标记
|
||||
`retry_wait(errorCode=interrupted_by_restart)`;超过执行重试上限则 `failed`。
|
||||
4. `waiting_user`、所有终态和已确认评论不自动重跑。
|
||||
5. 恢复过程使用 `stateVersion` CAS,单实例 Scheduler 只能产生一个有效租约。
|
||||
|
||||
### 5.6 规范状态与转移契约
|
||||
|
||||
实现层必须使用以下完整枚举;状态名、终态和转移条件与 PRD 第 6 节保持一致:
|
||||
|
||||
| 当前状态 | 允许的下一状态 | 触发者/守卫 |
|
||||
|---|---|---|
|
||||
| `received` | `queued` / `ignored` / `duplicate` / `rejected` | Webhook Adapter 完成规范化 |
|
||||
| `queued` | `preparing` / `cancelled` | Scheduler 已取得全局槽位和仓库租约;取消请求 |
|
||||
| `preparing` | `running` / `blocked_workspace` / `retry_wait` / `failed` | Workspace/Session Manager 结果 |
|
||||
| `running` | `waiting_user` / `verifying_reply` / `retry_wait` / `aborting` / `failed` | Codex App stop reason、错误或管理员中止 |
|
||||
| `waiting_user` | `queued` / `cancelled` / `failed` | 新评论、取消请求或会话不可恢复 |
|
||||
| `verifying_reply` | `succeeded` / `retry_wait` / `failed_reply` | Reply Verifier 结果;仅允许一次 reply retry |
|
||||
| `retry_wait` | `preparing` / `running` / `verifying_reply` / `failed` | `nextRetryAt` 到期且未超过上限 |
|
||||
| `blocked_workspace` | `queued` / `cancelled` | 管理员确认工作区可用或取消 |
|
||||
| `aborting` | `aborted` / `failed` | Codex App 取消确认或超时 |
|
||||
| `succeeded` / `succeeded_with_rest_fallback` / `failed` / `failed_reply` / `cancelled` / `aborted` / `ignored` / `duplicate` / `rejected` | 无 | 终态,不得创建新 turn |
|
||||
|
||||
每次转移都必须带 `fromState/toState/stateVersion` 和审计事件;未知状态或非法
|
||||
转移拒绝写入并告警。`aborting` 释放资源前不得被误标为 `aborted`,
|
||||
`succeeded_with_rest_fallback` 必须保留原始 MCP 回帖失败原因。
|
||||
|
||||
## 6. 接口契约
|
||||
|
||||
### 6.1 Webhook HTTP
|
||||
|
||||
**Endpoint**:`POST /api/gitea/webhook`(路径可由部署配置映射,但语义固定)
|
||||
|
||||
请求要求:
|
||||
|
||||
```http
|
||||
Content-Type: application/json
|
||||
X-Gitea-Event: issue_comment
|
||||
X-Gitea-Delivery: 8f7e…
|
||||
X-Gitea-Signature: <HMAC-SHA256(raw_body, webhook_secret)>
|
||||
```
|
||||
|
||||
适配器至少抽取以下规范化结构;原始 payload 只按安全保留策略保存 hash:
|
||||
|
||||
```json
|
||||
{
|
||||
"instanceId": "default",
|
||||
"deliveryId": "8f7e…",
|
||||
"eventType": "issue_comment|pull_request_comment",
|
||||
"repo": {"owner": "acme", "name": "widget", "cloneUrl": "…"},
|
||||
"resource": {"kind": "issue|pull_request", "number": 42},
|
||||
"comment": {"id": 101, "body": "@ccweb-bot 请修复…", "createdAt": "…"},
|
||||
"actor": {"login": "alice", "isBot": false},
|
||||
"refs": {"base": "main", "head": "feature/x"}
|
||||
}
|
||||
```
|
||||
|
||||
响应语义:
|
||||
|
||||
| HTTP | 场景 | 业务效果 |
|
||||
|---|---|---|
|
||||
| 202 | 验签通过且首次入队 | 返回 `{deliveryId, taskId, state:"queued"}` |
|
||||
| 200 | 重复 delivery、合法但忽略的事件 | 返回原 task 或 `{status:"ignored"}` |
|
||||
| 400 | JSON/必需字段无法解析 | 不入队,记录 `invalid_payload` |
|
||||
| 401 | HMAC 缺失或错误 | 不入队,不暴露内部原因 |
|
||||
| 409 | 仓库被管理员停用且策略为拒绝 | 返回 `repository_disabled`,不创建运行任务 |
|
||||
| 500/503 | 持久化不可用 | Gitea 可重试;不得声称已入队 |
|
||||
|
||||
### 6.2 控制与查询 API(逻辑契约)
|
||||
|
||||
实现可映射到现有 cc-web 路由,但字段和幂等语义保持不变:
|
||||
|
||||
| 方法 | 逻辑路径 | 作用 |
|
||||
|---|---|---|
|
||||
| GET | `/api/gitea-workflow/overview` | 全局暂停、运行槽位、队列摘要 |
|
||||
| GET | `/api/gitea-workflow/tasks?repoKey=&resourceKey=&state=` | 分页查询任务 |
|
||||
| GET | `/api/gitea-workflow/tasks/:taskId` | 任务、turn、评论和错误详情 |
|
||||
| POST | `/api/gitea-workflow/control/pause` / `resume` | 全局暂停/恢复;body 带 reason |
|
||||
| POST | `/api/gitea-workflow/repos/:repoKey/disable` / `enable` | 仓库停用/启用 |
|
||||
| POST | `/api/gitea-workflow/tasks/:taskId/cancel` | 仅取消 queued/waiting_user |
|
||||
| POST | `/api/gitea-workflow/tasks/:taskId/abort` | 请求中止 running turn |
|
||||
| GET | `/api/gitea-workflow/audit?...` | 按关联 ID/时间范围查询审计 |
|
||||
|
||||
控制请求必须带操作者身份、幂等 `requestId` 和 `reason`;重复 requestId 返回首次
|
||||
结果,不重复发送取消或评论。API 不接收模型、MCP 工具参数或任意 Git 命令。
|
||||
|
||||
### 6.3 Codex App JSON-RPC 形状
|
||||
|
||||
初始化链路:
|
||||
|
||||
```text
|
||||
initialize → initialized(notification)
|
||||
→ experimentalFeature/enablement/set(goals=true) [best effort]
|
||||
→ collaborationMode/list [best effort]
|
||||
→ thread/start
|
||||
```
|
||||
|
||||
线程配置契约(示意):
|
||||
|
||||
```json
|
||||
{
|
||||
"cwd": "/workspace/default/acme/widget",
|
||||
"collaborationMode": {
|
||||
"settings": {
|
||||
"model": "<固定配置>",
|
||||
"reasoning_effort": "<固定配置>",
|
||||
"developer_instructions": "<工作流系统约束>"
|
||||
}
|
||||
},
|
||||
"mcp_servers": {
|
||||
"gitea": {
|
||||
"command": "gitea-mcp",
|
||||
"args": ["-t", "stdio", "-H", "https://gitea.example"],
|
||||
"env": {"GITEA_ACCESS_TOKEN": "<secret-ref>"}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当运行时拒绝 `collaborationMode` 或返回 unknown field 时,记录一次
|
||||
`capability_downgrade`,本轮退回普通 turn;不能因为 goals/list 探测失败而终止
|
||||
app-server。使用 collaboration mode 时,`turn/start` 顶层不得再传 `model` 或
|
||||
`effort`。
|
||||
|
||||
### 6.4 MCP 与 Gitea REST 回帖
|
||||
|
||||
起始/异常回执与最终回帖分离:起始 `已收到` 和必要的异常终态回执由 cc-web
|
||||
通过 Gitea REST 发送,使用 `commentKey=taskId:status:state` 幂等;正常最终答案
|
||||
必须优先由官方 gitea-mcp 发出,REST 只允许作为确认失败后的最终正文兜底。这样
|
||||
既能让用户知道任务已接收,又不会用一串运行状态文本污染 Issue/PR 讨论串。
|
||||
|
||||
架构对官方 gitea-mcp 的具体工具名做一层逻辑适配,避免版本差异泄漏到任务状态:
|
||||
|
||||
```json
|
||||
{
|
||||
"operation": "gitea.comment.create",
|
||||
"resource": {"kind": "issue|pull_request", "number": 42},
|
||||
"body": "最终答案\n<!-- ccweb taskId=… turnId=… -->",
|
||||
"idempotencyKey": "task-uuid:turn_…"
|
||||
}
|
||||
```
|
||||
|
||||
适配器必须将工具返回映射为 `{accepted, commentId, errorCode}`,并再次查询确认。
|
||||
REST 兜底使用 Bot Token 调用 Gitea 的对应 Issue/PR 评论接口,body 必须复用
|
||||
`taskId/turnId` 标记;若 REST 返回 2xx 但查询不到,状态仍为 `failed_reply`,不能
|
||||
虚报成功。
|
||||
|
||||
## 7. 并发、租约与一致性
|
||||
|
||||
- 调度只依赖 `repoKey` 级 lease;没有全局 semaphore 或全局并发槽位。
|
||||
- 仓库 lease 的键为 `repoKey`,TTL 必须短于 worker 心跳间隔的可容忍失联窗口;
|
||||
释放必须校验 `leaseId`,防止旧 worker 解锁新 worker 的租约。
|
||||
- 领取任务采用 `stateVersion` CAS:`queued → preparing` 成功者才可继续。
|
||||
- 任务状态、评论幂等记录和审计事件先写入,再执行外部调用;外部调用结果以
|
||||
append-only 事件补写,重试读取最后已知结果。
|
||||
- 进程内锁只作性能优化,正确性依赖持久 lease;单实例 MVP 仍按可恢复模型设计,
|
||||
以便未来横向扩展。
|
||||
|
||||
## 8. 失败场景矩阵
|
||||
|
||||
| 场景 | 检测 | 状态/动作 | 是否重试 |
|
||||
|---|---|---|---|
|
||||
| HMAC 缺失/错误 | 常量时间校验失败 | 401、`rejected` 审计 | 否 |
|
||||
| delivery 重复 | 唯一键冲突 | 返回原结果、`duplicate` | 否 |
|
||||
| payload 不完整 | schema 校验失败 | 400、`invalid_payload` | 否 |
|
||||
| 仓库停用 | `repositories.status=disabled` | 拒绝新任务 | 否 |
|
||||
| clone/fetch 鉴权失败 | Git exit code/HTTP 401 | `retry_wait`,超过上限 `failed` | 最多 1 次 |
|
||||
| 工作区脏或冲突 | git status/lock 检查 | `blocked_workspace`,不 reset | 管理员解除后 1 次 |
|
||||
| 全局暂停 | control CAS | 保持 `queued`,不领取 | 恢复后 |
|
||||
| 租约过期 | TTL/心跳超时 | 释放资源并恢复扫描 | 最多 1 次 |
|
||||
| Codex App 不可达 | JSON-RPC timeout | `retry_wait` | 最多 1 次 |
|
||||
| gitea-mcp 命令缺失 | 启动前可执行文件检查失败 | `failed` + 一条 `giteabot:` 失败结果回执 | 不重试,修复命令后重新触发 |
|
||||
| MCP 进程启动失败 | stdio 子进程启动即退出 | `failed` + 一条 `giteabot:` 失败结果回执 | 不重试,修复命令后重新触发 |
|
||||
| MCP initialize 握手失败/超时 | stdio JSON-RPC `initialize` 无成功响应 | `failed` + 一条 `giteabot:` 失败结果回执 | 不重试,修复命令后重新触发 |
|
||||
| turn 被重启中断 | 无活跃 worker | `retry_wait(interrupted_by_restart)` | 最多 1 次 |
|
||||
| 用户补充问题 | stop reason/waiting 信号 | `waiting_user`,保留 thread | 新评论触发 |
|
||||
| 管理员中止超时 | cancel 无确认 | `failed` 或 `aborted`,强制释放 lease 并审计 | 否 |
|
||||
| MCP 最终回帖缺失 | 查询无匹配评论 | 一次 `reply_retry` turn | 仅 1 次 |
|
||||
| REST 兜底失败 | 非 2xx/查询不到 | `failed_reply` + 告警 | 不自动重试 |
|
||||
| Bot 自评论回调 | actor/login 匹配 Bot | `ignored` | 否 |
|
||||
|
||||
错误码建议使用稳定小写值(如 `invalid_signature`、`duplicate_delivery`、
|
||||
`workspace_dirty`、`app_server_timeout`、`mcp_reply_missing`),展示文本可本地化。
|
||||
|
||||
## 9. 安全与审计设计
|
||||
|
||||
### 9.1 信任边界
|
||||
|
||||
Webhook Secret 只在入口使用;Bot Token 只在 Gitea MCP/REST 出站边界使用;
|
||||
Codex App 收到的评论、Issue 描述、代码和 PR 内容全部视为不可信数据。系统提示
|
||||
明确禁止将这些内容解释为更改运行时权限、读取 Secret 或关闭审计的指令。
|
||||
|
||||
### 9.2 权限与凭据
|
||||
|
||||
- 官方 gitea-mcp 按已确认需求开放写权限,但通过固定实例 host、专用 Bot Token、
|
||||
运维暂停/中止和完整审计进行约束。
|
||||
- 日志仅记录 `hostRef/tokenRef`,不记录 Authorization、完整命令环境或原始 Secret。
|
||||
- Git clone/fetch 的凭据使用短生命周期注入,完成后清理;remote URL 不含 Token。
|
||||
|
||||
### 9.3 审计完整性
|
||||
|
||||
每个外部副作用前写入 intent 事件,完成后写入 result 事件;事件不可更新,只能
|
||||
追加补偿或人工操作事件。审计关联链必须可由
|
||||
`deliveryKey → taskId → sessionKey/threadId → turnId → commentId` 重建。
|
||||
|
||||
## 10. 验收与实现交接
|
||||
|
||||
实现方需按 [PRD.md](./PRD.md) 的 AC-01~AC-13 编写协议 mock、状态迁移、重启、
|
||||
并发、回帖确认和安全测试。任何无法满足的契约必须在对应审计/错误码中显式降级,
|
||||
不得静默跳过。
|
||||
|
||||
实现已按本文边界落地;后续修改不得绕过验签、状态机、幂等键和回帖核验,也不得
|
||||
新增未审计的外部写操作。
|
||||
84
docs/gitea-workflow/CODEX-INTEGRATION.md
Normal file
84
docs/gitea-workflow/CODEX-INTEGRATION.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Codex App / Gitea Workflow 挂接说明
|
||||
|
||||
本文件对应 `lib/gitea-workflow-codex.js` 的独立协议适配器,以及 `server.js`
|
||||
中保持薄层的三个挂接点。
|
||||
|
||||
## 线程级 gitea-mcp
|
||||
|
||||
Webhook runner 为每个任务创建工作区和 token 后,将如下对象随本轮消息传入:
|
||||
|
||||
```js
|
||||
{
|
||||
giteaWorkflow: {
|
||||
taskId: task.taskId,
|
||||
kind: 'normal',
|
||||
mcp: { host: 'https://gitea.example', accessToken: token },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`codexAppThreadConfig()` 会将其合并为:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcp_servers.gitea": {
|
||||
"type": "stdio",
|
||||
"command": "gitea-mcp",
|
||||
"args": ["-t", "stdio", "-H", "https://gitea.example"],
|
||||
"env": {
|
||||
"GITEA_HOST": "https://gitea.example",
|
||||
"GITEA_ACCESS_TOKEN": "<线程级凭据>"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
token 不写入 app-server 进程环境,也不通过 dynamicTools 注入。使用
|
||||
`collaborationMode` 时,模型与推理参数仍只放在 `settings`。
|
||||
|
||||
`gitea-mcp` 二进制随 cc-web 交付,源码部署默认查找
|
||||
`<cc-web>/bin/gitea-mcp`,single-exe 部署默认查找
|
||||
`<发布目录>/bin/gitea-mcp`。只有需要替换官方版本时才配置
|
||||
`CC_WEB_GITEA_MCP_COMMAND` 指向本机绝对路径;不要求每个服务各自安装一份。
|
||||
命令不可执行时,任务在启动 Codex App 前以 `gitea_mcp_not_found` 快速失败,不会
|
||||
让对话长期停留在 `running`。
|
||||
|
||||
在启动 Codex App 前,cc-web 还会用同一份线程级 command/args/env 做一次最小
|
||||
stdio `initialize` 预检。预检只验证进程能启动并返回 JSON-RPC initialize 响应,
|
||||
不调用 Gitea 工具;进程启动失败、返回 MCP error 或在
|
||||
`CC_WEB_GITEA_MCP_STARTUP_TIMEOUT_MS`(默认 8 秒)内无响应时,分别记录
|
||||
`gitea_mcp_start_failed`、`gitea_mcp_handshake_failed` 或
|
||||
`gitea_mcp_handshake_timeout`,任务进入 `failed`,并由统一队列监听发送一条
|
||||
`giteabot: 任务失败:...` 回执。cc-web 不会自动下载或安装 gitea-mcp。
|
||||
|
||||
## turn 生命周期
|
||||
|
||||
- `startCodexAppTurn()` 在 `turn/start` 返回后调用适配器的
|
||||
`handleTurnStarted({ taskId, threadId, turnId, kind })`。
|
||||
- `handleCodexAppTurnComplete()` 保留原有消息落盘和生命周期广播,并让既有
|
||||
Gitea waiter 调用 `settleGiteaWorkflowTurn()` 做回执查询、一次隐藏补触发和
|
||||
REST 兜底。
|
||||
- 运行时通知可用 `extractTurnId()` / `extractThreadId()` 兼容
|
||||
`params.turnId`、`params.turn.id`、`params.item.turnId` 等形状。
|
||||
|
||||
## 回执与 waiting_user
|
||||
|
||||
`buildWorkflowMarker()` 生成版本化 HTML 注释:
|
||||
|
||||
```html
|
||||
<!-- ccweb-gitea v="1" taskId="..." turnId="..." resourceKey="..." kind="final" -->
|
||||
```
|
||||
|
||||
只有同时匹配资源、taskId、turnId(含补发轮次允许的历史标识集合)和 Bot 身份的评论才算
|
||||
`confirmed`。查询异常为 `unknown`,不得据此再次执行修改、补发或 REST 覆盖;只有确认
|
||||
评论缺失才允许同一 thread 发起一次 `reply_retry`,其提示词只允许补发上一轮最终文本,
|
||||
仍未确认才进入 REST 兜底。
|
||||
|
||||
Agent 需要澄清或报告阻塞时使用同一标识结构但将 `kind` 设为 `waiting_user`。cc-web
|
||||
确认该标识后直接把任务置为 `waiting_user`,不触发回帖补发;用户再次 `@ccweb-bot`
|
||||
时创建新 turn,并复用原 Issue/PR 的 session/thread。
|
||||
|
||||
`queueWaitingUserComment()` 为用户的后续评论创建新 task/turn,但复用父任务的
|
||||
`sessionKey` 与 `threadId`;Bot 自评论按稳定 user id 优先、login 兜底过滤。
|
||||
带有完整 `ccweb-gitea` 隐藏标记的评论即使作者身份字段缺失,也按自身回执忽略,
|
||||
避免回执 Webhook 形成循环。
|
||||
147
docs/gitea-workflow/DEPLOYMENT.md
Normal file
147
docs/gitea-workflow/DEPLOYMENT.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 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,形成可复盘记录。
|
||||
255
docs/gitea-workflow/PRD.md
Normal file
255
docs/gitea-workflow/PRD.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Gitea Webhook 驱动 ccweb 工作流 PRD
|
||||
|
||||
> 文档状态:已确认需求基线(MVP)
|
||||
> 适用范围:单个 Gitea 实例、一个全局 `ccweb-bot` 账号
|
||||
> 相关设计:[ARCHITECTURE.md](./ARCHITECTURE.md)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
本功能把 Gitea Issue/PR 的普通评论作为 ccweb 的任务入口。用户在评论中
|
||||
提及 `@ccweb-bot` 后,cc-web 验证 Webhook、准备持久工作区、复用该
|
||||
Issue/PR 的独立 Codex App 会话,并通过官方 `gitea-mcp` 完成代码研究、修改
|
||||
和回帖。Gitea 是唯一主交互入口;cc-web 页面只提供镜像、运维和审计能力。
|
||||
|
||||
目标:
|
||||
|
||||
1. 让用户无需离开 Gitea 即可驱动一次或连续多轮工程任务。
|
||||
2. 保证同一仓库的工作区和任务严格串行,不同仓库可并行;MVP 不设置额外的
|
||||
全局并发上限。
|
||||
3. 在进程重启、网络抖动、MCP 回帖失败时,任务状态、会话和回执仍可恢复、
|
||||
重试且不重复执行或重复回帖。
|
||||
4. 对 Webhook、Bot Token、工作区写入和每次工具调用提供可追溯审计,并允许运
|
||||
维人员暂停、取消排队和中止运行中的 turn。
|
||||
|
||||
## 2. 已确认决策
|
||||
|
||||
| 主题 | 决策 |
|
||||
|---|---|
|
||||
| Gitea 实例 | MVP 只支持一个配置的 Gitea 实例,`instanceId=default` |
|
||||
| Bot | 一个全局 `ccweb-bot`,用于触发识别、起始回执和 REST 兜底 |
|
||||
| 触发 | Issue 或 PR 的普通评论正文包含 `@ccweb-bot`;评论作者不能是 Bot 自身 |
|
||||
| 自动接入 | 合法触发首次发现未登记仓库时自动登记并 clone;管理员可停用 |
|
||||
| 工作区 | 可配置工作区根目录下按仓库持久 checkout,后续使用 fetch 更新 |
|
||||
| 并发 | 同仓库串行;跨仓库并行;MVP 不设置全局运行上限 |
|
||||
| Agent | 固定 Codex App(`codexapp`)+ `yolo`,不在 Gitea 交互中暴露模式切换 |
|
||||
| MCP | 官方 `gitea-mcp`,stdio 传输;线程级注入 Gitea host/token |
|
||||
| 会话 | 每个 Issue/PR 一个独立持久会话;后续同资源评论进入同一会话 |
|
||||
| 交互 | Gitea 为主;cc-web 页面只展示状态、队列和审计,不替代 Gitea 对话 |
|
||||
| 回复 | 先发一条“已收到”回执;正常最终答案由 gitea-mcp 发回原 Issue/PR;缺失时一次补触发,最后 REST 兜底 |
|
||||
| 安全 | Webhook HMAC、delivery 去重、Bot 自评论过滤、Token/Secret 分离、审计 |
|
||||
| 恢复 | `queued` 重启后继续;`waiting_user` 保留;`running` 视为中断并最多重试一次 |
|
||||
| 运维 | 全局暂停、仓库停用、取消排队、中止 turn、查看审计 |
|
||||
|
||||
## 3. 范围
|
||||
|
||||
### 3.1 In Scope
|
||||
|
||||
- Gitea Webhook 接收、原始请求 HMAC 校验和 delivery 幂等。
|
||||
- Issue/PR 普通评论事件的规范化、提及解析和 Bot 自评论抑制。
|
||||
- 未登记仓库自动接入、持久 clone/fetch、脏工作区保护。
|
||||
- 任务队列、同仓库锁、跨仓库并行。
|
||||
- Issue/PR 独立会话与 Codex App turn 生命周期管理。
|
||||
- 线程级官方 `gitea-mcp` stdio 配置和 Gitea 主交互。
|
||||
- 起始“已收到”回执、MCP 最终回帖确认、一次补触发和 REST 兜底;不发送运行中、等待用户或成功状态评论。
|
||||
- 暂停、停用、取消排队、中止、重启恢复、审计和可观测字段。
|
||||
- 可供前端/运维使用的只读状态和控制接口契约(实现不在本 PRD 变更范围)。
|
||||
|
||||
### 3.2 Out of Scope
|
||||
|
||||
- 多 Gitea 实例、跨 Forge 统一协议和 GitHub/GitLab 适配。
|
||||
- 非评论触发(Push、定时任务、Issue 创建、Actions 等)。
|
||||
- 让普通用户在 cc-web 中切换模型、推理强度或 yolo/plan 模式。
|
||||
- 自定义 dynamicTools 代替 MCP;手动填写 MCP 工具参数的 UI。
|
||||
- 自动清理用户未提交修改、`reset --hard`、强制覆盖其他会话的工作区。
|
||||
- 细粒度按用户/路径的写权限策略(Bot 全写权限是已确认前提);只记录风险并提供暂停/审计。
|
||||
|
||||
## 4. 角色与核心场景
|
||||
|
||||
| 角色 | 能力 |
|
||||
|---|---|
|
||||
| Gitea 用户 | 在 Issue/PR 评论 `@ccweb-bot`,查看状态、追问、确认结果 |
|
||||
| `ccweb-bot` | 发送起始回执、作为 MCP/REST 身份回帖;Bot 自己的评论不会再次触发 |
|
||||
| 运维管理员 | 全局暂停/恢复、仓库停用/启用、取消排队、中止 turn、查审计 |
|
||||
| 系统 | 验签、去重、排队、clone/fetch、启动会话、确认回帖、重试和恢复 |
|
||||
|
||||
典型链路:
|
||||
|
||||
```text
|
||||
Gitea 普通评论 @ccweb-bot
|
||||
│
|
||||
▼
|
||||
验签 → delivery 去重 → 解析 Issue/PR → 自动接入仓库
|
||||
│ │
|
||||
└────────────── 持久化任务并入队 ◄──┘
|
||||
│
|
||||
同仓库锁串行,跨仓并行 ────┘
|
||||
▼
|
||||
Codex App/yolo + gitea-mcp(stdio)
|
||||
│
|
||||
已收到回执 → MCP 最终回帖 → turnId/评论确认
|
||||
│
|
||||
补触发一次 → REST 兜底(必要时)
|
||||
```
|
||||
|
||||
## 5. 功能需求
|
||||
|
||||
### FR-01 触发与事件规范化
|
||||
|
||||
- 接受 Gitea 配置的 Issue/PR 评论 Webhook;事件适配器将不同版本的事件名称
|
||||
归一为 `issue_comment` 或 `pull_request_comment`。
|
||||
- 仅当正文包含独立 mention `@ccweb-bot` 时触发,mention 后的文本作为任务
|
||||
指令;没有有效指令时仍创建可追踪任务并要求用户补充。
|
||||
- 记录原始 `deliveryId`、事件类型、仓库、资源类型/编号、评论 ID、作者和
|
||||
接收时间。无法解析的事件进入 `ignored`,不启动 Agent。
|
||||
- Bot 自己发出的状态/最终/兜底评论必须被识别并忽略,防止自触发环路。
|
||||
|
||||
### FR-02 Webhook 安全与幂等
|
||||
|
||||
- 在读取或持久化业务字段前,使用原始 body 和配置 Secret 计算 HMAC-SHA256,
|
||||
采用常量时间比较;验签失败返回 401,不入队。
|
||||
- `deliveryKey = instanceId + ":" + deliveryId` 唯一;已处理或处理中重复
|
||||
delivery 返回 200(或约定的幂等响应)但不创建新任务。
|
||||
- Secret、Bot Token、MCP 启动参数不得写入评论、普通日志或审计明文;审计只
|
||||
记录凭据引用/哈希指纹。
|
||||
|
||||
### FR-03 仓库自动接入与工作区
|
||||
|
||||
- 通过合法 Webhook 首次发现仓库时原子创建 `RepositoryRecord`;未登记不因
|
||||
任务排队失败而丢失事件。
|
||||
- 按 `workspaceRoot/<instanceId>/<owner>/<repo>` 建立持久目录。首次使用
|
||||
`clone`,后续任务在仓库锁内 `fetch`;凭据通过临时环境或 credential helper
|
||||
注入,不把 Token 写入 remote URL。
|
||||
- 检测到未提交修改、冲突或目录被外部占用时,不执行 reset/clean/覆盖;任务
|
||||
进入 `blocked_workspace`,写入审计并等待管理员处理。
|
||||
- PR 任务必须记录 base/head ref 和源仓库信息;Issue 任务使用仓库默认分支,
|
||||
除非指令明确指定已允许的分支。
|
||||
|
||||
### FR-04 队列、锁与并发
|
||||
|
||||
- 调度器维护持久任务队列;同 `repoKey` 同时最多一个 `preparing/running`
|
||||
任务,跨仓库可并行。
|
||||
- 不设置额外全局运行信号量;`queued` 任务只受全局暂停和各自仓库锁影响,暂停时不再领取新任务。
|
||||
- 队列顺序默认 FIFO;同一资源的新评论可合并为下一轮输入,但不得跳过已持久
|
||||
化的任务或破坏评论顺序。
|
||||
- 取消排队只允许作用于尚未启动的任务;中止操作必须向 Codex App 发送取消,
|
||||
并等待 `aborted` 或超时后释放锁。
|
||||
|
||||
### FR-05 会话与 Agent 运行时
|
||||
|
||||
- `sessionKey = instanceId:owner/repo:kind:number`,其中 `kind` 为 `issue` 或
|
||||
`pull_request`;同一资源永远复用同一个 `threadId`。
|
||||
- 每个新任务创建一个 `turnId`;持久化 `threadId/turnId`、启动时间、最后事件
|
||||
序号和重试次数,支持断线后恢复监听。
|
||||
- 启动参数固定为 `codexapp` 与 `yolo`。`model`、`reasoning_effort`、
|
||||
`developer_instructions` 放入 `collaborationMode.settings`(若运行时启用
|
||||
collaborationMode),不得在顶层重复传递。
|
||||
- `thread/start.config.mcp_servers.gitea` 线程级注入官方 gitea-mcp stdio;每
|
||||
个线程使用对应实例的 host/token,不使用进程全局来源会话变量替代。
|
||||
- Prompt 必须包含仓库/资源标识、评论上下文、工作区路径、预期回帖格式和安全
|
||||
边界;评论正文视为不可信输入,不得覆盖系统约束。
|
||||
|
||||
### FR-06 Gitea 交互与回帖确认
|
||||
|
||||
- 合法任务只发送一条起始 `giteabot: 已收到` 回执;运行中、等待用户和成功状态
|
||||
不再额外发送评论,避免污染 Issue/PR 讨论串。回执仍带不可见关联标记
|
||||
(`taskId`、`state`、可选 `turnId`)用于去重和审计。
|
||||
- 正常最终答案必须由 gitea-mcp 的 Issue/PR comment 工具发送;cc-web 记录
|
||||
MCP 返回的 `commentId` 或工具调用结果,并通过 Gitea 查询确认实际存在。
|
||||
- 确认必须同时匹配 `resourceKey`、`taskId`、`turnId`(或等价不可见标记)和
|
||||
Bot 作者,避免把旧评论当作本轮回执。
|
||||
- 仅在确认 Gitea 评论查询结果为“缺失”时,发起一次“只补发回执、不重复修改”的
|
||||
隐藏 turn;补发仍缺失才以 Bot Token 经 Gitea REST 发布最终正文。查询结果为
|
||||
`unknown` 时不自动重试或覆盖,任务进入 `failed_reply`,由终态兜底回执告知用户,
|
||||
避免把已经成功落库的 MCP 回复重复发布。
|
||||
|
||||
### FR-07 重试、恢复与状态一致性
|
||||
|
||||
- 网络/进程级暂时错误使用持久 `retry_wait` 和指数退避;执行 turn 因重启被
|
||||
中断时最多自动重试一次,禁止无界重试。
|
||||
- `queued` 重启后恢复调度;`waiting_user` 保留原会话等待新评论;`running`
|
||||
恢复为 `retry_wait` 并带 `interrupted_by_restart` 原因;终态不重新执行。
|
||||
- 所有状态变更、评论发送、MCP 工具调用和重试都追加审计事件,状态写入遵循
|
||||
版本号/单调序列,防止旧事件覆盖新状态。
|
||||
|
||||
### FR-08 运维控制与审计
|
||||
|
||||
- 全局 `pause/resume`:暂停只阻止新任务领取,已运行任务继续或由管理员另行中止。
|
||||
- 仓库 `disable/enable`:停用后拒绝该仓库的新触发并保留历史;正在运行的任务
|
||||
不强制杀死。
|
||||
- `cancel queued`、`abort running turn` 必须幂等,响应包含当前状态和操作者。
|
||||
- 审计事件至少包含 `eventId、taskId、sessionKey、actor、action、fromState、
|
||||
toState、deliveryId、turnId、commentId、errorCode、timestamp`;支持按仓库、
|
||||
资源、任务和时间范围查询。
|
||||
|
||||
## 6. 状态机
|
||||
|
||||
### 6.1 任务状态
|
||||
|
||||
| 状态 | 含义 | 可转移 |
|
||||
|---|---|---|
|
||||
| `received` | 已验签并完成事件规范化 | `queued`、`ignored`、`duplicate`、`rejected` |
|
||||
| `queued` | 等待仓库锁和全局槽位 | `preparing`、`cancelled` |
|
||||
| `preparing` | 正在登记仓库、clone/fetch、构造线程 | `running`、`blocked_workspace`、`retry_wait`、`failed` |
|
||||
| `running` | Codex App turn 执行中 | `waiting_user`、`verifying_reply`、`retry_wait`、`aborting`、`failed` |
|
||||
| `waiting_user` | Agent 需要用户补充,保留会话 | `queued`(新评论)、`cancelled`、`failed` |
|
||||
| `verifying_reply` | 已收到 turn 完成,等待确认最终评论 | `succeeded`、`retry_wait`、`failed_reply` |
|
||||
| `retry_wait` | 到达退避时间,等待有限次重试 | `preparing`、`running`、`verifying_reply`、`failed` |
|
||||
| `blocked_workspace` | 工作区脏/冲突/权限问题 | `queued`(管理员解除后)、`cancelled` |
|
||||
| `aborting` | 已请求取消,等待 Codex App 确认 | `aborted`、`failed` |
|
||||
| `succeeded` | 最终回帖已确认 | 终态 |
|
||||
| `succeeded_with_rest_fallback` | 使用 REST 兜底回帖成功 | 终态 |
|
||||
| `failed` / `failed_reply` | 执行或回帖不可恢复失败 | 终态 |
|
||||
| `cancelled` / `aborted` | 排队取消或运行中止 | 终态 |
|
||||
| `ignored` / `duplicate` / `rejected` | 未触发、重复或安全拒绝 | 终态 |
|
||||
|
||||
### 6.2 状态不变量
|
||||
|
||||
1. 只有 `preparing/running/aborting` 持有仓库锁;同一 `repoKey` 同时最多一个
|
||||
运行中任务。
|
||||
2. 终态任务不得创建新 turn、状态评论或重试;重复 Webhook 只能返回原任务。
|
||||
3. `verifying_reply` 必须引用本轮 `turnId`;`failed_reply` 不代表代码执行必然
|
||||
失败,必须在审计中区分 `executionError` 与 `replyError`。
|
||||
4. `waiting_user` 的新评论创建新 `taskId`,但复用同一 `sessionKey/threadId`。
|
||||
|
||||
## 7. 非功能需求与风险
|
||||
|
||||
| 类别 | 要求 |
|
||||
|---|---|
|
||||
| 安全 | HMAC 常量时间校验、凭据隔离、Bot 自触发抑制、敏感字段脱敏 |
|
||||
| 一致性 | delivery/task/session/turn/comment 均有唯一键;状态变更可重放且幂等 |
|
||||
| 可恢复性 | 进程重启不丢队列、会话映射和回帖确认上下文 |
|
||||
| 可观测性 | 结构化日志 + 审计事件 + task/session/turn 关联 ID |
|
||||
| 性能 | Webhook 验签与入队不等待 Agent 完成;并发只受仓库锁约束 |
|
||||
| 数据保留 | 任务/会话/审计至少保留到管理员显式清理策略生效;工作区持久保留 |
|
||||
| 风险 | gitea-mcp 全写权限意味着 Bot 能修改代码和 Gitea 内容;通过 HMAC、暂停、
|
||||
中止、审计和脏工作区保护降低风险,不声称实现细粒度授权 |
|
||||
|
||||
## 8. 验收标准与需求覆盖矩阵
|
||||
|
||||
每个验收项都应能在集成测试、协议 mock 或只读审计中独立验证。
|
||||
|
||||
| ID | 验收标准 | 覆盖需求/设计 |
|
||||
|---|---|---|
|
||||
| AC-01 | 合法 Issue 普通评论含 `@ccweb-bot` 创建任务;无 mention、非评论或 Bot 自评论不创建任务 | FR-01 |
|
||||
| AC-02 | HMAC 错误返回 401 且无业务写入;相同 delivery 重放不增加任务数 | FR-02 |
|
||||
| AC-03 | 首次合法触发自动登记仓库并 clone;再次触发只 fetch;脏工作区不被 reset/覆盖 | FR-03 |
|
||||
| AC-04 | 同仓库两个任务严格串行;两个仓库可并行;不额外施加全局并发上限 | FR-04 |
|
||||
| AC-05 | 同一 Issue/PR 多条评论复用一个 `threadId`,每条任务有唯一 `turnId` | FR-05 |
|
||||
| AC-06 | Codex App 运行时固定 `codexapp+yolo`;官方 gitea-mcp 以线程级 stdio 配置启动 | FR-05 |
|
||||
| AC-07 | 合法任务仅有一条“已收到”起始回执;成功正文由 MCP/REST 兜底承担,失败或控制终态最多一条结果回执,Bot 评论不会回触发 | FR-06 |
|
||||
| AC-08 | MCP 最终回帖能按 `taskId+turnId+resourceKey` 确认;缺失只补发一次,随后 REST 兜底 | FR-06 |
|
||||
| AC-09 | 重启后 queued 继续、waiting_user 保留、running 最多重试一次;终态不重跑 | FR-07 |
|
||||
| AC-10 | pause 阻止新领取;仓库 disable 拒绝新任务;cancel/abort 幂等且释放相应资源 | FR-08 |
|
||||
| AC-11 | 审计可按 task/session/repo 查询,包含状态迁移、delivery、turn、comment 和错误字段 | FR-08 |
|
||||
| AC-12 | PRD 与 ARCHITECTURE 的状态枚举、字段名、默认并发、回帖错误语义完全一致 | 文档一致性 |
|
||||
| AC-13 | 生产挂接包含 `server.js` 路由、`lib/gitea-workflow-*` 核心模块、管理页与离线回归;不新增第三方依赖 | 交付约束 |
|
||||
|
||||
## 9. 交付边界与后续演进
|
||||
|
||||
当前仓库已提供 MVP 实现:`POST /api/gitea/webhook`、持久任务/队列、工作区维护、
|
||||
Codex App 线程级 gitea-mcp 注入、回帖确认/兜底、管理 API/UI 和离线回归。实现与
|
||||
测试必须继续以本 PRD 的 FR/AC 编号作为审计索引。
|
||||
|
||||
后续可独立评估:多实例、细粒度授权、审批门、工作区隔离容器、任务预算、
|
||||
SSE/前端实时镜像和更多 Forge 适配;这些不改变 MVP 的 `sessionKey`、幂等和回帖
|
||||
确认约束。
|
||||
155
docs/gitea-workflow/TESTING.md
Normal file
155
docs/gitea-workflow/TESTING.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Gitea Webhook Agent 测试设计
|
||||
|
||||
> 本文与 [PRD.md](./PRD.md) 的 FR/AC 以及
|
||||
> [ARCHITECTURE.md](./ARCHITECTURE.md) 的字段、状态和错误矩阵对齐。独立协议
|
||||
> 回归入口为:
|
||||
>
|
||||
> ```bash
|
||||
> timeout 60s node scripts/gitea-webhook-regression.js
|
||||
> ```
|
||||
>
|
||||
> 脚本只使用 Node 内置模块和 `fixtures/gitea-workflow/*.json`,不启动
|
||||
> `server.js`、不访问网络、不写业务 Store,适合实现落地前后的离线验收。
|
||||
|
||||
## 1. 测试金字塔
|
||||
|
||||
| 层级 | 目标 | 本轮入口 |
|
||||
|---|---|---|
|
||||
| 契约/纯函数 | HMAC、事件规范化、mention/Bot 过滤、幂等键 | `gitea-webhook-regression.js` |
|
||||
| 状态模型 | queued/running/retry_wait/终态恢复和有限重试 | 同上 + 实现方状态机单测 |
|
||||
| 协议集成 | HTTP header、Gitea REST、Codex JSON-RPC、MCP stdio | mock server/adapter 集成测试 |
|
||||
| 端到端 | 真实 Gitea 测试仓库到最终评论 | staging 手工/CI smoke,不纳入离线脚本 |
|
||||
| 运维验收 | 重启、备份恢复、pause/disable/abort、告警脱敏 | staging runbook 演练 |
|
||||
|
||||
离线脚本是最小门禁;当前仓库已提供以下专项门禁:
|
||||
|
||||
```bash
|
||||
node scripts/gitea-webhook-regression.js
|
||||
node scripts/gitea-webhook-workspace-unit.js
|
||||
node scripts/gitea-workflow-codex-unit.js
|
||||
node scripts/gitea-workflow-core-unit.js
|
||||
node scripts/gitea-workflow-management-unit.js
|
||||
```
|
||||
|
||||
仍需在 staging 用真实 Gitea 验证出站回帖确认;纯函数通过不能替代真实凭据和
|
||||
网络链路验收。
|
||||
|
||||
## 2. Fixtures 约定
|
||||
|
||||
所有 fixture 均为 JSON,放在 `fixtures/gitea-workflow/`,不含真实 Secret、Token、
|
||||
源码或生产 URL。字段约定:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "valid-issue-comment",
|
||||
"headers": {
|
||||
"x-gitea-event": "issue_comment",
|
||||
"x-gitea-delivery": "delivery-001"
|
||||
},
|
||||
"payload": {
|
||||
"repository": {"owner": "acme", "name": "widget"},
|
||||
"issue": {"number": 42},
|
||||
"comment": {"id": 101, "body": "@ccweb-bot 请修复"},
|
||||
"sender": {"login": "alice", "is_bot": false}
|
||||
},
|
||||
"expected": {"accepted": true, "resourceKind": "issue"}
|
||||
}
|
||||
```
|
||||
|
||||
脚本运行时用固定测试 Secret 计算签名;签名不落盘。需要拒绝签名时复制
|
||||
payload 后篡改 raw body 或签名头。fixture 变更必须同步场景 ID 和预期结果。
|
||||
|
||||
## 3. 需求覆盖矩阵
|
||||
|
||||
| 场景 ID | 验证内容 | Fixture/断言 | 对应 AC |
|
||||
|---|---|---|---|
|
||||
| WH-01 | 合法 Issue 评论 mention 触发,规范化 owner/repo/number/comment | `valid-issue-comment.json` | AC-01 |
|
||||
| WH-02 | 合法 PR 评论进入 `pull_request` 资源,保留 base/head | `valid-pr-comment.json` | AC-01/03 |
|
||||
| WH-03 | HMAC 缺失/错误不写业务任务,常量时间比较 | `valid-issue-comment.json` 篡改签名 | AC-02 |
|
||||
| WH-04 | 相同 `instanceId:deliveryId` 重放返回 duplicate,不增 task | valid fixture 两次提交 | AC-02 |
|
||||
| WH-05 | 无 mention、非评论事件、Bot 自评论、停用仓库均 ignored/rejected | `ignored-events.json`、`bot-self-comment.json` | AC-01/10 |
|
||||
| Q-01 | 重启后 queued 保留;running 变 `retry_wait(interrupted_by_restart)` | `queue-recovery.json` | AC-09 |
|
||||
| Q-02 | 已有重试次数达到上限后进入 failed;waiting_user/终态不重跑 | 同上 | AC-09 |
|
||||
| Q-03 | 同仓库运行锁互斥;跨仓库可以并行,不设置额外全局活动上限 | 脚本内最小状态快照 | AC-04 |
|
||||
| R-00 | 合法触发仅产生一条 `giteabot: 已收到` 起始回执;成功路径不产生运行中/等待/已完成状态评论 | server.js 回执策略断言 | AC-07 |
|
||||
| R-01 | MCP 回帖成功但查询缺失只创建一个 reply_retry | `reply-failure.json` | AC-08 |
|
||||
| R-02 | 补发仍失败才 REST 兜底,结果为 succeeded_with_rest_fallback | 同上 | AC-08 |
|
||||
| MCP-01 | `thread/start.config.mcp_servers.gitea` 为线程级 stdio 配置 | `mcp-thread-config.json` | AC-06 |
|
||||
| MCP-02 | collaborationMode 字段放 settings,顶层无 model/effort 重复字段 | 同上 | AC-06 |
|
||||
| MCP-03 | gitea-mcp 命令缺失、进程启动即退、initialize 返回 error、握手超时分别映射稳定错误码 | `gitea-mcp-probe-unit.js` + mock stdio fixture | AC-06/07 |
|
||||
| MCP-04 | 预检失败后任务只产生一条 `giteabot:` 失败终态回执;线程级注入仍保留且不自动安装二进制 | `gitea-mcp-probe-unit.js` 源码守门 | AC-06/07/11 |
|
||||
| SEC-01 | Secret/token 不进入日志、评论、clone URL 或 fixture | 全部 fixture + 输出扫描 | AC-02/11 |
|
||||
| SEC-02 | 路径穿越、任意命令、越权控制参数被拒绝 | 实现方安全单测 | AC-03/10/11 |
|
||||
|
||||
## 4. 状态、恢复与并发测试
|
||||
|
||||
实现方 Store 测试必须验证以下不变量:
|
||||
|
||||
1. `deliveryKey`、`sessionKey`、`taskId+turnId`、`commentKey` 唯一;重复请求
|
||||
返回原记录且不创建新 turn/comment。
|
||||
2. 同一 `repoKey` 同时最多一个 `preparing/running/aborting`;不同 `repoKey`
|
||||
可以并行;pause 只阻止领取,不强制终止运行中任务。
|
||||
3. 重启扫描用 `stateVersion`/`leaseId` CAS,旧 worker 不能覆盖新租约;
|
||||
`running` 最多自动恢复一次,超过上限进入 `failed`。
|
||||
4. `waiting_user` 保留 thread,新评论创建新 task 但复用 `sessionKey/threadId`;
|
||||
所有终态不再自动执行。
|
||||
5. `blocked_workspace` 不执行 reset/clean;管理员解除后才可重新入队。
|
||||
|
||||
故障注入至少覆盖进程在“intent 已落库、外部调用未完成”和“外部返回成功、
|
||||
结果未确认”两个窗口崩溃,恢复后必须通过幂等键继续,而非盲目重复修改。
|
||||
|
||||
## 5. 回帖重试测试
|
||||
|
||||
按以下顺序模拟:
|
||||
|
||||
1. MCP comment 返回 accepted,但 Gitea 查询无匹配标记:状态进入
|
||||
`verifying_reply`,只允许一个 `reply_retry` turn。
|
||||
2. `reply_retry` 仍无评论:调用一次 REST 兜底,body 复用 `taskId/turnId/resourceKey`
|
||||
标记;查询成功为 `succeeded_with_rest_fallback`。
|
||||
3. REST 返回 2xx 但查询仍为空:状态必须是 `failed_reply`,不能虚报成功,也不得
|
||||
无限重试。
|
||||
4. 原 MCP 评论已确认后重复 webhook/重启:不得再发送起始、状态、最终或兜底评论。
|
||||
|
||||
测试日志只输出 `taskId/turnId/commentId/errorCode`,不得输出正文中的 Secret 或
|
||||
完整 Authorization。
|
||||
|
||||
## 6. MCP/JSON-RPC 协议测试
|
||||
|
||||
mock app-server 必须断言初始化顺序为:`initialize` → `initialized` → best-effort
|
||||
`experimentalFeature/enablement/set`、`collaborationMode/list` → `thread/start`。
|
||||
能力探测失败只记录降级,不阻断启动。
|
||||
|
||||
另需运行 `node scripts/gitea-mcp-probe-unit.js`,它会以独立 mock stdio 进程验证
|
||||
gitea-mcp 的 initialize 预检,不需要真实 Gitea、Token 或 Docker。
|
||||
|
||||
`thread/start` 断言:
|
||||
|
||||
- `config.mcp_servers.gitea` 每个线程单独携带 host/token 引用和 stdio command;
|
||||
- `CC_WEB_SOURCE_SESSION_ID` 等来源上下文不能只存在进程环境;
|
||||
- 使用 collaboration mode 时,`model`、`reasoning_effort`、
|
||||
`developer_instructions` 只在 `collaborationMode.settings`,顶层不存在重复
|
||||
`model`/`effort`;拒绝 collaborationMode 时可降级普通 turn。
|
||||
|
||||
## 7. 安全与运维验收
|
||||
|
||||
- 验签使用 raw body + HMAC-SHA256 + 常量时间比较;缺失/错误签名 401 且无业务写入。
|
||||
- 事件正文、Issue 描述、代码和 PR 内容均视为不可信 prompt;不能改变系统约束、
|
||||
读取 Secret、关闭审计或注入任意命令。
|
||||
- Bot 自评论、停用仓库、非评论事件和无 mention 事件不启动 Agent;每个决定有
|
||||
审计 action/errorCode。
|
||||
- clone/fetch 使用不含 Token 的 remote URL;工作区路径必须在允许根目录内,
|
||||
路径穿越和脏目录覆盖均拒绝。
|
||||
- 控制 API 需要管理员身份、reason 和 idempotency key;取消/中止幂等。
|
||||
- 日志和审计脱敏检查覆盖 Secret、Bot Token、Authorization、MCP args/env。
|
||||
|
||||
## 8. 执行清单
|
||||
|
||||
```bash
|
||||
node --check scripts/gitea-webhook-regression.js
|
||||
timeout 60s node scripts/gitea-webhook-regression.js
|
||||
git diff --check
|
||||
```
|
||||
|
||||
CI 失败时保留 stdout/stderr、fixture 名称和场景 ID;不得重试隐藏不稳定失败。
|
||||
真实 Gitea staging 验收完成后,应将 delivery/task/thread/turn/comment 关联链和
|
||||
最终状态写入审计报告,作为发布批准依据。
|
||||
Reference in New Issue
Block a user