Docs: webhook receiver (runbook, design §10, deploy-lxc §8), PAD-7 rollout record
Runbook: service, endpoint, signature, logs, secret rotation, disabling, heartbeat skipped reasons, new failure rows. Design: webhook trigger design, notify hook, Plane SSRF guard (WEBHOOK_ALLOWED_IPS). Deploy: install steps. Record: synthetic-delivery acceptance and measured latencies.
This commit is contained in:
+21
-8
@@ -8,12 +8,12 @@ Plane 看板驱动的通用 agent 调度服务:任何人/agent 在 Plane 建
|
||||
|
||||
## 2. 架构
|
||||
```
|
||||
Plane ──webhook v2(HMAC)/ 轮询兜底──► agentd(接收端 + 调度核心 + 执行器适配器)
|
||||
Plane ──webhook(HMAC)即时触发 / cron 轮询兜底──► agentd(接收端 + 调度核心 + 执行器适配器)
|
||||
│ 按 assignee→终端、label→执行器/模型,串行组,重试,超时,心跳
|
||||
┌───────────────┼───────────────┐
|
||||
codex(远程/本机) claude-code(本机/远程) shell
|
||||
└───────────────┴───────────────┘
|
||||
执行器只产出"最后消息 + 产物路径";agentd 统一回帖、改状态、通知(ntfy)
|
||||
执行器只产出"最后消息 + 产物路径";agentd 统一回帖、改状态、通知(notify 钩子,暂为空操作)
|
||||
```
|
||||
阶段 3 之后调度核心搬到 Temporal(workflow = 一个工作项的生命周期,activity = 起执行器/等结果/回帖),用 Temporal UI 看运行与重试。
|
||||
|
||||
@@ -34,7 +34,7 @@ Plane ──webhook v2(HMAC)/ 轮询兜底──► agentd(接收端 + 调
|
||||
| 环境类(焦点丢失/弹窗/断网) | 当 blocked 处理,暂停同串行组的派发 | Waiting |
|
||||
| 运行中工作项被移出 In Progress | 结束任务进程,回帖"运行已取消:工作项状态被改为 X",释放终端槽位/串行组,保留线程 | 不改 |
|
||||
| 连续 `max_progress_rounds`(默认 12)轮 `[progress]` | 回帖说明,不再续跑 | Waiting → 应答者 |
|
||||
| agentd 自身 | cron 拉起;心跳 10 min 无更新 → ntfy 报警 | — |
|
||||
| agentd 自身 | cron 拉起(webhook 只是提前触发);心跳 10 min 无更新 → 报警(notify 钩子,未实现) | — |
|
||||
回到 Todo 的工作项无条件重新拾起;逻辑性失败永不自动重试。
|
||||
|
||||
阶段 1 的具体取舍(均有单测):
|
||||
@@ -58,7 +58,6 @@ executors:
|
||||
projects:
|
||||
GWENT:
|
||||
id: 79cb5a60-aa4f-4138-9864-1dd70426e665
|
||||
webhook_secret_key: PLANE_WEBHOOK_GWENT
|
||||
states: {todo: Todo, running: In Progress, waiting: Waiting, review: Review, done: Done}
|
||||
reviewer: oregon
|
||||
routing:
|
||||
@@ -67,7 +66,10 @@ projects:
|
||||
default_executor: {win10: codex, oregon: claude-code}
|
||||
retry: {transient_max: 3, backoff_minutes: [2, 5, 10]}
|
||||
timeout_hours: 4
|
||||
notify: {ntfy_topic: gwent, on: [waiting, review, failed]}
|
||||
webhook: {bind: 10.77.10.249, port: 8790, path: /plane, secret_key: PLANE_WEBHOOK_SECRET,
|
||||
tick_command: [systemd-run, --scope, --quiet, --collect, /opt/plane-agentd/cron/agentd-tick-lxc.sh],
|
||||
debounce_seconds: 3, retry_seconds: 15, retry_window_seconds: 120}
|
||||
notify: {kind: none} # 钩子:核心每次改状态调用 Agentd.notify(),none = 空操作
|
||||
```
|
||||
|
||||
## 6. 仓库布局
|
||||
@@ -76,7 +78,8 @@ agentd/ 调度核心、plane 客户端、vault 注入、状态存储
|
||||
agentd/executors/ codex.py claude_code.py shell.py(适配器契约见 docs/executor-contract.md)
|
||||
agentd/transports/ local.py warpgate_ssh.py(远程 Linux/macOS:bash -c 经中继;Windows:helper 脚本 + schtasks)
|
||||
agentd/web/ 配置页面(阶段 2):读写 YAML + schema 校验 + 连通/冒烟测试 + 最近运行
|
||||
cli/ agentd new|show|comment|state|assign|smoke
|
||||
cli/ agentd tick|new|show|comment|state|assign|smoke|webhook-serve|check-config
|
||||
systemd/ plane-agentd-webhook.service(LXC 上的 webhook 接收端)
|
||||
config/ agentd.yaml(示例 + 实际)、schema.json
|
||||
docs/ design.md executor-contract.md runbook.md
|
||||
```
|
||||
@@ -84,12 +87,12 @@ docs/ design.md executor-contract.md runbook.md
|
||||
## 7. 阶段
|
||||
1. **核心**:配置驱动调度(轮询)、codex-remote(win10) + claude-local(oregon) 两个执行器、线程续接与重建、失败说明与重试、心跳、CLI;gwent 项目作为首个租户切换过来(替换现有 board-worker-plane)。
|
||||
2. **配置页面**:单页,三个标签(终端/执行器/路由)+ 状态条;保存即校验、提交 Git、热加载;不显示密钥。
|
||||
3. **Webhook + ntfy**:Plane webhook v2 签名校验、即时触发;ntfy 通知 Waiting/Review/失败;轮询保留兜底。前提:Plane VM 进 tailnet 或接收端放在可达位置。
|
||||
3. **Webhook + 通知**:Plane webhook 签名校验、即时触发,轮询保留兜底——**PAD-7 已实现**(§10)。通知 Waiting/Review/失败:只留了空操作钩子 `notify: {kind: none}`(没有 ntfy 服务)。
|
||||
4. **Temporal**:调度核心迁为 workflow/activity,获得重试/超时/历史/UI;agentd 变为 worker + webhook 入口。
|
||||
5. **Gitea 联动**:执行器产物以分支/PR 提交并回帖链接;可选 Gitea webhook 回写 Plane。
|
||||
|
||||
## 8. 安全
|
||||
配置里只有 Vault 键名;token 由 `agent-access.py exec --secret-env` 注入 agentd 进程;执行器不持有 Plane 凭据、不能直接改工作项;webhook 按项目签名校验;页面仅 tailnet 可达,登录走既有网关。
|
||||
配置里只有 Vault 键名;token 由 `agent-access.py exec --secret-env` 注入 agentd 进程;执行器不持有 Plane 凭据、不能直接改工作项;webhook 是工作区级的一个密钥(Vault 键 `PLANE_WEBHOOK_SECRET`,只经 `--secret-env` 进接收端进程,不传给子进程),HMAC 校验失败 401,只认配置里的项目;接收端只听内网地址;页面仅 tailnet 可达,登录走既有网关。
|
||||
|
||||
## 9. 部署位置
|
||||
- Plane VM(plane-01)只跑 Plane:它是设备化节点(固定版本、每日备份停写路径),不放 agentd。
|
||||
@@ -98,3 +101,13 @@ docs/ design.md executor-contract.md runbook.md
|
||||
- 远程启动的通用实现(P1-6,已实现):Linux/macOS 经 Warpgate SSH(`agent-access.py run --target <终端 target> -- bash -c …`)上传 `prompt.md`/`run.sh` 并一次调用内启动,`setsid nohup` 脱离会话(macOS 用 perl `POSIX::setsid`),轮询 `exit.txt`,从终端上的输出文件取回最后消息/线程 ID,kill 按进程组 + 会话;中继目标名由终端配置给出。oregon 已切到这条路径(调度器暂仍在 oregon,启动经 Warpgate 回到本机)。部署步骤:`docs/deploy-lxc.md`。
|
||||
- 防重复启动(PAD-11):启动调用之前先把计划中的运行(run id、终端、handle、记账字段)作为 `launching` 标记落盘;启动调用失败/超时或 agentd 中途退出时,下一 tick 探测运行目录(`exit.txt`/`launch.pid`/`pid.txt`):有 → 接管;无 → 同一 run id 换新目录 `<run_id>-r2` 重启(不清空旧目录);探测不通 → 保留标记并占住终端槽位。transport 拒绝清空含 pid 文件的运行目录。某终端中继失败时本 tick 余下操作跳过该终端,其他终端照常。
|
||||
- Windows 仍用 helper 脚本 `win10-ps.sh` / `win10-put.sh`(仓库 `tools/win10/`,写死 `--target win10`)+ 计划任务。**TODO(统一)**:Windows 也改走 `relay()`(argv = `powershell.exe -EncodedCommand …`,文件同样 base64 分块),删掉对 helper 脚本与 `helpers_dir` 的依赖,LXC 上就不必再复制这两个脚本。
|
||||
|
||||
## 10. Webhook 触发(PAD-7)
|
||||
- **形态**:`agentd webhook-serve`(`agentd/webhook.py`,只用标准库)常驻在 LXC 上(systemd `plane-agentd-webhook`),Plane 工作区级 webhook(事件 `issue` + `issue_comment`)投到 `http://10.77.10.249:8790/plane`。它不自己调度,只是**提前跑一次 cron 的 tick 脚本**:调度仍然只有一份代码、一把锁、一个状态文件;cron 每 2 分钟照跑,webhook 丢了/接收端挂了只是退回到轮询延迟。
|
||||
- **校验**:`X-Plane-Signature` = HMAC-SHA256(密钥, 请求体) 的十六进制(Plane 签 `json.dumps(payload)` 并原样发送);常量时间比较,先比原始字节、再比重新序列化的结果。缺失/不符 → 401,不触发。
|
||||
- **路由**:`data.project` ∈ 配置的项目 → 200 并触发;否则(别的项目、删除事件没有项目)→ 202 忽略。不按事件类型或操作者过滤:agentd 自己回帖/改状态也会产生投递,结果是每次有动作的 tick 之后多一次 tick(空闲 tick 不写 Plane,不会循环)。
|
||||
- **合并与补跑**:第一条投递后等 3 s,期间的投递合并成一次 tick;tick 运行中又来的投递 → 结束后再跑一次(运行中的 tick 可能在变化之前读过 Plane)。锁被占(cron/手工 tick;脚本 `flock -n -E 75`,`cli tick` 撞上 agentd 状态锁也退出 75)→ 每 15 s 重试,最长 2 min,之后交给 cron。
|
||||
- **进程隔离**:tick 用 `systemd-run --scope` 跑在独立 cgroup 里,重启/停止接收端不会杀掉正在跑的 tick(tick 可能正在启动远程运行)。
|
||||
- **延迟构成**:投递 → tick 开始 ≈ 3 s 合并窗口;tick 本身 = vault 取 token(1–2 s)+ 读 Plane + 先 poll 每个在途运行(每个中继调用约 10 s)+ 启动(中继约 12 s;win10 上传更久)。`[worker] 第 k 轮开始` 评论在启动调用返回之后才发,所以"建项 → 开始评论"受中继成本限制,而不是触发方式。
|
||||
- **Plane 的 SSRF 防护**:Plane(≥1.4 / 商业版 3.x)拒绝解析到私有/CGNAT 地址的 webhook URL(创建时和每次投递时都查),除非 Plane 的环境里有 `WEBHOOK_ALLOWED_IPS`(CIDR 列表)或 `WEBHOOK_ALLOWED_HOSTS`。内网接收端需要在 Plane 的 api 与 worker 容器环境里加 `WEBHOOK_ALLOWED_IPS=10.77.10.249/32`。
|
||||
- **心跳里的跳过原因**:每个 tick 把没派出去的 Todo 项和原因写进 `heartbeat.json` 的 `skipped`(`concurrency 1/1 (GW-15)`、`serialize_group game held by GW-4`、`busy_process codex (1 running, not started by agentd)`、`relay down`、`no route for assignee kappiee`、`start failed: …`),同一项的原因变化时才写一行日志。
|
||||
|
||||
Reference in New Issue
Block a user