diff --git a/config/agentd.yaml b/config/agentd.yaml index 6585cb8..d94f030 100644 --- a/config/agentd.yaml +++ b/config/agentd.yaml @@ -16,7 +16,7 @@ agentd: # Plane webhook receiver (`agentd webhook-serve`, systemd plane-agentd-webhook.service; docs/runbook.md "Webhook"). # One workspace-level Plane webhook (work items + comments) -> POST http://10.77.10.249:8790/plane -> one tick. webhook: - bind: 10.77.10.249 # LAN only (Plane VM 10.77.10.68 is on the same /24) + bind: 10.77.10.249 # LAN only (Plane VM 506 10.77.10.69 is on the same /24) port: 8790 path: /plane secret_key: PLANE_WEBHOOK_SECRET # Vault key name; the value is injected by agent-access.py exec --secret-env diff --git a/docs/deploy-lxc.md b/docs/deploy-lxc.md index b89f01d..5d77fc3 100644 --- a/docs/deploy-lxc.md +++ b/docs/deploy-lxc.md @@ -20,6 +20,13 @@ apt-get update && apt-get install -y python3 python3-yaml openssh-client git cro | `https://plane.kappiee.com` | Plane API | | `https://git.kappiee.com` | `git pull`(公开仓库,无需凭据) | +## 2.1 到 NAS 的直连路径(OPNsense 静态端口 NAT,2026-10-21) +PVE 站点是双层 NAT(OPNsense → 192.168.1.1),默认的随机源端口会让 tailscale 打洞失败、只能走 DERP(此前取 vault 身份端点 30/30 超时)。修法: +- **OPNsense 出站 NAT 规则**(用户在 GUI 里添加,因为 opnsense 中继账号 kappiee 无权限,我们只读):描述 `Tailscale agentd-01 static-port`,接口 WAN,源 `10.77.10.249/32`,目标 any,协议 UDP,勾选"静态端口"(`staticnatport 1`)。从 kpe-edge-01 的 `Tailscale edge static-port`(10.77.10.110/32)规则克隆而来。加规则后要清掉旧状态(Firewall → Diagnostics → States,按源 IP)。 +- **LXC tailscaled 用 UDP 41642**:`/etc/default/tailscaled` 里 `PORT="41642"`(备份 `.bak-20261021144838`),必须与 edge 的 41641 不同,否则两台机器的静态端口映射会在同一 NAS 端点上撞元组。 +- **验证**(LXC 上):`tailscale ping 100.89.188.28` 应得 `pong from nas … via 223.102.82.206:1549`(不是 `via DERP`);`tailscale netcheck` 应 `UDP: true`、`MappingVariesByDestIP: false`;身份端点循环(注意 `agent.json` 含 bearer token,只看状态码和耗时):`for i in $(seq 30); do curl -s -o /dev/null -w '%{http_code} %{time_total}\n' --max-time 15 https://nas.tail9531b.ts.net:8443/identity/agent.json; done`。2026-10-21 实测 30/30 成功、中位数 0.089 s,ping 5 ms。 +- **回滚**:在 OPNsense GUI 删掉该规则(Firewall → NAT → Outbound)并清状态;LXC 上还原 `/etc/default/tailscaled.bak-20261021144838` 后 `systemctl restart tailscaled`。回滚后路径退回 DERP,调度器应同时切回 oregon(runbook)。 + ## 3. Agent Vault 客户端 只复制程序文件(客户端没有落盘的 token 缓存;oregon 目录里的 `agent-service-check.py` 是运维工具,不需要): ```bash @@ -102,7 +109,7 @@ python3 -m unittest && python3 cli/agentd check-config 5. 提交、推送、LXC `git pull`;`cli/agentd check-config`,然后 `cli/agentd smoke GWENT mac --watch`。 ## 8. Webhook 接收端(PAD-7) -1. 网络:Plane VM 10.77.10.68 与 LXC 10.77.10.249 同在 10.77.10.0/24,接收端只听 `10.77.10.249:8790`(LXC 内无防火墙)。**Plane 侧前提**:Plane 默认拒绝内网 webhook 地址("The webhook URL is not allowed. Provide a publicly reachable HTTP or HTTPS URL."),要在 Plane 的 api 与 worker 容器环境里加 `WEBHOOK_ALLOWED_IPS=10.77.10.249/32` 并重建这两个容器(design §10)。纯 http 本身是允许的(http/https 都行)。 +1. 网络:Plane VM(PVE VM 506 plane-02,10.77.10.69,prime-cli 安装;没有 VM 505)与 LXC 10.77.10.249 同在 10.77.10.0/24,接收端只听 `10.77.10.249:8790`(LXC 内无防火墙)。**Plane 侧前提**:Plane 默认拒绝内网 webhook 地址("The webhook URL is not allowed. Provide a publicly reachable HTTP or HTTPS URL."),要在 Plane 的 api(创建时检查)与 worker(每次投递检查)容器环境里设 `WEBHOOK_ALLOWED_IPS=10.77.10.249/32`(逗号分隔 IP/CIDR)并重建这两个容器(design §10)。Plane 3.3.3 没有 compose override 文件:prime-cli 由 `/opt/plane/plane.env` 渲染 `/var/plane/docker-compose.yml`,两处都已有空的 `WEBHOOK_ALLOWED_IPS`(compose 里每个服务一行 `WEBHOOK_ALLOWED_IPS: ""`)。做法:备份两个文件为 `.bak-pad7-<时间戳>` → 把 `plane.env` 的 `WEBHOOK_ALLOWED_IPS=` 与渲染 compose 里的空值改为 `10.77.10.249/32` → `docker compose -p plane -f /var/plane/docker-compose.yml up -d --no-deps api worker`(先 `--dry-run`;不要用 prime-cli,它要 TTY)。**状态(2026-10-21):尚未应用**(自动权限分类器拦下了改动),webhook 未创建,接收端保持停用;Plane 签名/负载与接收端已核对一致(v1 路径,无需改接收端)。纯 http 本身是允许的(http/https 都行)。 2. 密钥:Plane 建 webhook 时生成、只显示一次 → `tools/plane-webhook.py create http://10.77.10.249:8790/plane` 直接写进 Vault `PLANE_WEBHOOK_SECRET`(建出来是停用状态;在 oregon 上以 kappiee 管理员会话运行,见 runbook "Webhook")。核对长度:`agent-access.py … exec --secret-env S=PLANE_WEBHOOK_SECRET -- python3 -c 'import os;print(len(os.environ["S"]))'`。 3. 代码与服务(LXC): ```bash diff --git a/docs/design.md b/docs/design.md index 4b1b68b..bb63328 100644 --- a/docs/design.md +++ b/docs/design.md @@ -95,7 +95,8 @@ docs/ design.md executor-contract.md runbook.md 配置里只有 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。 +- 网络(2026-10-21):LXC 到 NAS 的 tailnet 路径依赖 OPNsense 的静态端口出站 NAT 规则 + LXC tailscaled 固定 UDP 41642,已直连(详见 docs/deploy-lxc.md §2.1);曾因 DERP 导致调度器暂回 oregon,已于同日切回 LXC。 +- Plane VM(Plane 3.3.3,PVE VM 506 plane-02)只跑 Plane:它是设备化节点(固定版本、每日备份停写路径),不放 agentd。 - 阶段 1–2:agentd 跑在 oregon(已有 Vault 客户端、Warpgate 中继脚本、Claude Code、现有 worker 状态),cron 拉起;配置页面同进程,只监听 tailnet 地址。 - 阶段 3 起(**2026-10-21 PAD-11 已切换**):迁到 PVE 上的精简 LXC `agentd-01`(CT 600,root,仓库 `/opt/plane-agentd`,状态 `/var/lib/plane-agentd`,日志 `/var/log/plane-agentd`;1 vCPU / 1–2 GB,tailnet + 内网 10.77.10.0/24,与 Plane/Temporal 同网段,webhook 走内网)。**LXC 上不装任何 agent CLI**,只有 Python 3.10+、PyYAML、Agent Vault 客户端、ssh 客户端;所有终端(win10、oregon、mac…)一律远程启动(`transport: warpgate-ssh`),LXC 只负责调度、回帖、通知。对外访问经 Warpgate HTTP target,复用既有登录。 - 远程启动的通用实现(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`。 @@ -109,5 +110,5 @@ docs/ design.md executor-contract.md runbook.md - **合并与补跑**:第一条投递后等 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`。 +- **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`(改 `/opt/plane/plane.env` 与渲染的 `/var/plane/docker-compose.yml`,重建 api/worker,见 deploy-lxc §8)。截至 2026-10-21 尚未应用,webhook 未启用,调度靠 cron 轮询。 - **心跳里的跳过原因**:每个 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: …`),同一项的原因变化时才写一行日志。 diff --git a/docs/runbook.md b/docs/runbook.md index e0b93da..7b14516 100644 --- a/docs/runbook.md +++ b/docs/runbook.md @@ -156,3 +156,8 @@ Todo + 指派给终端用户 → agentd 选最小编号、终端有空位、串 - 做法:LXC 删 cron 行、停用 `plane-agentd-webhook`、等 `agentd.lock`/`cron.lock` 空闲;`agentd-state.json`(sha256 7201077b…eb87,两端一致)拷到 oregon `~/.gwent-board/agentd/`(旧文件备份为 `.bak-<时间戳>`);oregon 恢复 `*/2` 的 `cron/agentd-tick.sh`。LXC 上的文件、服务单元均未删除。`config/agentd.oregon.yaml` 无需改动(`check-config` 通过)。 - 验证:oregon 首个 tick 14:20:01 接管,在途的 GW-5-1(win10 Codex)已结束,结果回帖并进入 Review;heartbeat `errors=[]`、`inflight=[]`,下一 tick(14:22)同样。 - 重新切回 LXC 之前:先 `crontab -r`(oregon)并等 tick 结束,再按 deploy-lxc §5 把状态拷回 LXC,并重新 enable webhook 服务;任何时刻只能有一个调度器。 + +## 切换记录(2026-10-21,网络修复后重新切回 LXC) +- 网络:OPNsense 增加出站 NAT 静态端口规则 `Tailscale agentd-01 static-port`(10.77.10.249/32,UDP,WAN),LXC tailscaled 改听 UDP 41642;LXC 到 NAS 直连(`tailscale ping` 5 ms via 223.102.82.206:1549,身份端点 30/30 成功、中位 0.089 s)。详见 deploy-lxc §2.1。 +- 切换:oregon crontab 清空(备份 `~/.gwent-board/crontab.backup-20261021-152423-recut`),状态文件(sha256 `54b747db…7155`,两端一致;LXC 旧文件备份 `agentd-state.json.bak-20261021-152504`)拷到 LXC,装 LXC crontab `*/2 … cron/agentd-tick-lxc.sh`。在途的 GW-18 带过去(LXC 上为 GW-18-2),之后的 run-1 结果已回帖。heartbeat errors=[]、down=[]、skipped=[],只有一个调度器。 +- Webhook(PAD-7)仍未启用:`plane-agentd-webhook` inactive/disabled;Plane 的 `WEBHOOK_ALLOWED_IPS` 尚未设置(见 deploy-lxc §8 的具体改法,Plane 实为 VM 506 / 10.77.10.69)。PLANE_WEBHOOK_SECRET 仍是临时值。待办:应用该改动后做 §8 第 2–5 步;考虑轮换 Plane 的 AES_SECRET_KEY(排查时被打印进了一次会话记录)。