PAD-12: per-terminal transport (direct Tailscale SSH or Warpgate, pre-session fallback, pinned host keys, agent key from Vault), one-call win10 launch and one-call collect, remote call timings, agentd ping

This commit is contained in:
jj.wang
2026-10-22 02:59:37 +00:00
parent 83bc0f243c
commit 5192e04088
23 changed files with 1850 additions and 132 deletions
+56 -5
View File
@@ -1,6 +1,6 @@
# 在精简 LXC 上部署 agentd(design §9 阶段 3)
调度器跑在一台**不装任何 agent CLI** 的 Debian 12 LXC(`agentd-01`)上;所有终端(win10、oregon、mac…)都经 Warpgate SSH 远程启动(`transport: warpgate-ssh`)。LXC 只做调度、回帖、通知。
调度器跑在一台**不装任何 agent CLI** 的 Debian 12 LXC(`agentd-01`)上;所有终端(win10、oregon、mac…)都是远程启动:经 Warpgate SSH(`transport: warpgate-ssh`),或经 Tailscale 直连并以 Warpgate 兜底(`transport: direct-ssh`,PAD-12,§9)。LXC 只做调度、回帖、通知。
实际部署(2026-10-21,PAD-11):`agentd-01` = PVE CT 600,Debian 12,tailnet `100.69.78.124`,Warpgate target `agentd-01`,登录 **root**(Vault skill `ssh-agentd-01`;没有另建部署用户,agentd 以 root 运行)。下面是实际做过的步骤;从 oregon 发命令都经 `agent-access.py … run --target agentd-01 --timeout N -- bash -c '…'`。
@@ -9,14 +9,15 @@ LXC 交付时已有 `python3`(3.11)、`python3-yaml`、`openssh-client`、`g
```bash
apt-get update && apt-get install -y python3 python3-yaml openssh-client git cron ca-certificates
```
`jsonschema` 可选,没有就用内置子集校验器。
`jsonschema` 可选,没有就用内置子集校验器。`direct-ssh` 要用的 `ssh`、`ssh-agent`、`ssh-add`、`ssh-keygen` 都在 `openssh-client` 包里(Debian 12:OpenSSH 9.2)。
## 2. 网络
| 去向 | 用途 |
|---|---|
| `nas.tail9531b.ts.net:8443`(tailnet) | Agent Vault 身份配置 `identity/agent.json`(含 vault token 与 Warpgate 主机公钥)→ LXC 已在 tailnet |
| Vault(身份配置里的地址) | 取 Plane token、Warpgate 密码 |
| Warpgate SSH `115.28.210.101:2222` | 所有终端的中继(LXC → win10 / oregon) |
| Warpgate SSH `115.28.210.101:2222` | `warpgate-ssh` 终端的中继,以及 `direct-ssh` 的兜底(LXC → win10 / oregon) |
| tailnet:win10 `100.123.209.19:22`、oregon `100.127.58.78:22` | `direct-ssh` 直连(PAD-12,§9) |
| `https://plane.kappiee.com` | Plane API |
| `https://git.kappiee.com` | `git pull`(公开仓库,无需凭据) |
@@ -48,7 +49,7 @@ mkdir -p /var/lib/plane-agentd /var/log/plane-agentd && chmod 700 /var/lib/plane
python3 -m unittest && python3 cli/agentd check-config
```
不需要本地配置文件,全部在仓库里(部署可复现):
- `config/agentd.yaml` = agentd-01 的实时配置:`state_dir: /var/lib/plane-agentd`(状态、心跳、锁),`log_dir: /var/log/plane-agentd`(`agentd.log`),`helpers_dir: tools/win10`,`vault.client: ~/.local/share/server-access-client/agent-access.py`(root 下即 `/root/...`)。终端 win10(warpgate-ssh/windows/schtasks)和 oregon(warpgate-ssh/linux/nohup,`run_root: /home/ubuntu/.gwent-board/agentd-runs`,`bins.claude`);没有 local 终端。
- `config/agentd.yaml` = agentd-01 的实时配置:`state_dir: /var/lib/plane-agentd`(状态、心跳、锁),`log_dir: /var/log/plane-agentd`(`agentd.log`),`helpers_dir: tools/win10`,`vault.client: ~/.local/share/server-access-client/agent-access.py`(root 下即 `/root/...`)。终端 win10(direct-ssh + 兜底 warpgate-ssh/windows/schtasks)和 oregon(direct-ssh + 兜底 warpgate-ssh/linux/nohup,`run_root: /home/ubuntu/.gwent-board/agentd-runs`,`bins.claude`);没有 local 终端。`agentd.ssh_key: AGENTD_SSH_KEY`,`agentd.ssh_dir: /run/plane-agentd/ssh`(ControlMaster 套接字和生成的 known_hosts,agentd 自己建,权限 0700)。
- `tools/win10/win10-ps.sh`、`win10-put.sh`:win10 helper(vault 客户端目录取 `AGENT_ACCESS_HOME`,默认 `~/.local/share/server-access-client`;身份取 `AGENT_ACCESS_CONFIG`)。相对 `helpers_dir` 按仓库根解析。
- `config/protocols/gwent-board-protocol.md`:看板协议(拼进每轮 prompt,LXC 本机读取)。来源是 gwent 仓库 `design/board-protocol.md`,**协议更新时复制过来提交**,然后 LXC `git pull`。
- `config/agentd.oregon.yaml`:oregon 作调度器时的配置(回滚用,见 runbook)。
@@ -67,7 +68,7 @@ python3 -m unittest && python3 cli/agentd check-config
```
*/2 * * * * /opt/plane-agentd/cron/agentd-tick-lxc.sh
```
`cron/agentd-tick-lxc.sh` = `flock -n /var/lib/plane-agentd/cron.lock timeout 900 python3 -I /root/.local/share/server-access-client/agent-access.py --config <identity> exec --secret-env PLANE_OREGON_API_TOKEN=… --secret-env PLANE_WINDOW10_API_TOKEN=… --timeout 850 -- python3 /opt/plane-agentd/cli/agentd --config /opt/plane-agentd/config/agentd.yaml tick --once >> /var/log/plane-agentd/cron.log 2>&1`。
`cron/agentd-tick-lxc.sh` = 拿 `/var/lib/plane-agentd/cron.lock`(`flock -n -E 75`)后执行 `timeout 900 python3 -I /root/.local/share/server-access-client/agent-access.py --config <identity> exec --secret-env PLANE_OREGON_API_TOKEN=… --secret-env PLANE_WINDOW10_API_TOKEN=… --secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY --timeout 850 -- python3 /opt/plane-agentd/cli/agentd --config /opt/plane-agentd/config/agentd.yaml tick --once`,输出追加到 `/var/log/plane-agentd/cron.log`;Vault 取不到 `AGENTD_SSH_KEY` 时不带它重跑一次(§9)。
注意:经中继装 crontab 时别在 `set -e` 下写 `(crontab -l | grep -v …; echo …) | crontab -`——没有旧 crontab 时 grep 返回 1,子 shell 提前退出,装进去的是空 crontab。直接 `printf '%s\n' '<行>' | crontab -`。
5. 验证:`/var/lib/plane-agentd/heartbeat.json` 的 `at` 每 2 分钟更新。
6. 冒烟经 LXC 调度器:在 oregon 上 `cli/agentd smoke GWENT oregon --watch`(`--watch` = 只建项并等待,不在本机 tick;oregon 上 `cli/agentd` 默认读 `config/agentd.yaml`,CLI 只需要 Plane token)。
@@ -122,3 +123,53 @@ python3 -m unittest && python3 cli/agentd check-config
单元文件复用 cron 脚本的 vault 客户端调用方式:`python3 -I /root/.local/share/server-access-client/agent-access.py --config <identity> exec --secret-env PLANE_WEBHOOK_SECRET=PLANE_WEBHOOK_SECRET --timeout 315360000 -- python3 /opt/plane-agentd/cli/agentd … webhook-serve --bind 10.77.10.249 --port 8790`,`Restart=always`。tick 由接收端经 `systemd-run --scope … cron/agentd-tick-lxc.sh` 拉起(同一把 `cron.lock`,`flock -n -E 75`)。
4. 启用 Plane 侧:`tools/plane-webhook.py patch <id> is_active=true`,然后 `systemctl restart plane-agentd-webhook`(若第 2 步之后密钥变过)。
5. 验证:随便改一个 GWENT 工作项 → `webhook.log` 出现 `decision=tick` 和 `tick start`/`tick done`;错签名请求得 401 且没有 tick;`journalctl -u cron` 里 cron 仍每 2 分钟一次;`systemctl restart plane-agentd-webhook` 后 `/healthz` 恢复。
## 9. 直连 SSH(PAD-12)
LXC 经 Tailscale 直连终端的 sshd,不经过 Warpgate(design §9"终端通道")。每个终端单独配置,Warpgate 留作兜底。前提:终端在 tailnet 上,LXC(`100.69.78.124`)能连到它的 22 端口。实测:win10 `100.123.209.19`(Windows OpenSSH,账号 administrator,直连 RTT 27 ms),oregon `100.127.58.78`(账号 ubuntu,RTT 233 ms)。
**上线顺序**:`cron/agentd-tick-lxc.sh` 带着 `--secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY`。agent-access.py 没有可选密钥,取不到键时报 `credential_unavailable`,整个 exec 失败;cron 脚本认出这种情况后会去掉这个参数再跑一次(cron.log 记一行 `AGENTD_SSH_KEY not available from Vault …`),所以先 pull 也不会停派单,只是直连终端一直走兜底。仍然建议第 1–3 步做完再 pull,免得每个 tick 多取一次 Vault。
1. **生成专用密钥**(在 LXC 上;私钥只进 Vault,不留在磁盘上):
```bash
umask 077; d=$(mktemp -d -p /dev/shm) # tmpfs:私钥不落到磁盘(磁盘上 shred 在日志式/写时复制文件系统上不可靠)
ssh-keygen -q -t ed25519 -N '' -C plane-agentd@agentd-01 -f "$d/agentd"
cat "$d/agentd.pub" # 公钥,下面两步要用
```
私钥 `$d/agentd` 原样(多行文本)写进 Vault 键 **`AGENTD_SSH_KEY`**:在 Vault UI 里粘贴,或者用与 `tools/plane-webhook.py` 的 `vault_store()` 相同的接口(`POST /v1/credentials`,agent 身份),读文件直接写,不打印。值里的换行被转义成 `\n` 也可以,agentd 会还原。写完删掉:`rm -rf "$d"`(在 tmpfs 上,删掉即可)。核对时只看长度:
`agent-access.py --config <identity> exec --secret-env K=AGENTD_SSH_KEY --timeout 60 -- python3 -c 'import os;print(len(os.environ["K"]))'`(ed25519 私钥约 400 字符)。
2. **oregon**(经 Warpgate 操作):在 `~ubuntu/.ssh/authorized_keys` 追加一行(目录 700、文件 600):
```
restrict,from="100.69.78.124" ssh-ed25519 AAAA…(上一步的公钥) plane-agentd@agentd-01
```
`restrict` 关掉所有转发、pty 和 `~/.ssh/rc`(agentd 用 `-T`,不需要 pty),不必再单独写 `no-port-forwarding` 等;`from=` 只放行 LXC 的 tailnet 地址。sshd 不认识 `restrict`(OpenSSH 7.2 之前)时会把整行判为无效:认证失败、终端走兜底,不会放宽权限;第 5 步的 ping 能看出来(`sshd -V` 或 Windows 上 `sshd.exe -V` 看版本)。可选的再一层:Tailscale ACL 只允许 LXC(`100.69.78.124` 或它的 tag)访问 win10/oregon 的 `:22`,这样密钥泄露了也没用。
3. **win10**(经 Warpgate,`tools/win10/win10-ps.sh`):Administrator 属于 Administrators 组,所以 sshd 读的是 `C:\ProgramData\ssh\administrators_authorized_keys`,不是用户目录下的文件。用 ASCII 追加同样的一行(别覆盖已有的行),再把 ACL 收紧到只有 Administrators 和 SYSTEM(用 SID,不受系统语言影响),否则 sshd 会忽略这个文件:
```powershell
$f = 'C:\ProgramData\ssh\administrators_authorized_keys'
$line = 'restrict,from="100.69.78.124" ssh-ed25519 AAAA… plane-agentd@agentd-01'
if ((Test-Path $f) -and (Get-Content -Raw $f) -and -not (Get-Content -Raw $f).EndsWith("`n")) { $line = "`r`n" + $line } # 原文件末尾没换行时,别把新行粘到最后一把钥匙后面
Add-Content -Path $f -Encoding ascii -Value $line
icacls $f /inheritance:r /grant '*S-1-5-32-544:F' /grant '*S-1-5-18:F'
```
顺便确认 sshd 的默认 shell 是 cmd.exe(`HKLM:\SOFTWARE\OpenSSH` 下没有 `DefaultShell`,或它指向 cmd.exe)。agentd 发的远程命令是 `powershell.exe -NoProfile -NonInteractive -EncodedCommand <引导>`,真正的脚本经 stdin 传过去。外层如果被改成 PowerShell,stdin 可能转不到内层;第 5 步的 ping 能直接发现这个问题。
4. **钉住的主机公钥**已写在 `config/agentd.yaml` 的 `terminals.<t>.ssh.host_key`,经两个通道核对过:Warpgate 读终端上的 `ssh_host_ed25519_key.pub`,与 tailnet 上 `ssh-keyscan -t ed25519` 的结果一致。新加终端照此办理:
```bash
ssh-keyscan -t ed25519 <tailnet ip> # LXC 上,直连
agent-access.py … run --target <t> -- cat /etc/ssh/ssh_host_ed25519_key.pub # 第二通道(Windows:type C:\ProgramData\ssh\ssh_host_ed25519_key.pub)
```
两边一致才能写进配置。agentd 每次调用都用它生成 known_hosts(`<ssh_dir>/known_hosts.<终端>`,别名 `agentd-<终端>`),全局和用户的 known_hosts 都不读。
5. **上线与验证**(在 LXC 上;拿着 cron 锁做,期间 cron 和 webhook 的 tick 会跳过,webhook 会自动重试):
```bash
cd /root/.local/share/server-access-client
flock /var/lib/plane-agentd/cron.lock bash -c '
git -C /opt/plane-agentd pull &&
for t in win10 oregon; do
python3 -I agent-access.py --config https://nas.tail9531b.ts.net:8443/identity/agent.json exec \
--secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY --timeout 120 -- \
python3 /opt/plane-agentd/cli/agentd ping $t --transport direct-ssh -n 3
done'
```
预期每个终端打印 3 行 `term=<t> via=direct op=ping ms=… rc=0` 和 `ping n: … ms PONG=…`。第 1 次要握手(win10 约 0.5–1.5 s,oregon 约 2–3 s),第 2、3 次复用连接,应明显更快。win10 若是 `timeout`,多半是 stdin 没到内层 PowerShell,见第 3 步。`--transport warpgate-ssh` 可顺带确认兜底仍然通(约 9 s)。
`cli/agentd check-config` 会多出 `direct-ssh …` 行,`cron/exec secret args` 末尾多了 `--secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY`。
6. **观察**:下一个 tick 后看 `/var/log/plane-agentd/remote.log`,应该只有 `via=direct` 行;出现 `via=fallback` 说明直连失败了,同时 `agentd.log` 会有一行原因。在 oregon 上 `cli/agentd smoke GWENT win10 --watch`,量"建项 → `[worker] 第 1 轮开始`",目标 ≤ 15 s(原来 91 s)。
7. **回滚**:把终端的 `transport` 改回 `warpgate-ssh`(提交、推送、LXC 上 `git pull`;`ssh`/`fallback` 可以留着)。cron 脚本里的 `--secret-env AGENTD_SSH_KEY=…` 可以保留:agentd 总会把它从子进程环境里去掉。Vault 键被删掉也不会停 tick(脚本会不带它重跑),但每个 tick 会多取一次 Vault,届时最好也删掉这一行。紧急情况下可在 LXC 上 `git -C /opt/plane-agentd checkout 83bc0f2 -- config/agentd.yaml`(旧配置,全部走 Warpgate,新代码照常能用)。要彻底撤销直连授权:删掉两台终端 authorized_keys 里那一行,再在 LXC 上关掉已经建好的 ControlMaster 连接(删 authorized_keys 不会断开它们,紧挨着的 webhook tick 能让它们活过 60 s):`for s in /run/plane-agentd/ssh/*-*; do [ -S "$s" ] && ssh -O exit -o ControlPath="$s" x; done`。
8. **轮换密钥**:照第 1 步生成新钥,先把新公钥加到两台终端,再更新 Vault `AGENTD_SSH_KEY`,然后删掉旧公钥的行,再照第 7 步关掉已有的 ControlMaster 连接(否则它们空闲 60 s 后才退出)。之后新连接就用新钥。改了某个终端钉住的 `host_key` 不需要这一步:套接字名里带着 pin 的指纹,新 pin 自动用新连接。
+12 -6
View File
@@ -3,7 +3,7 @@
Plane 看板驱动的通用 agent 调度服务:任何人/agent 在 Plane 建工作项并指派给一个"终端",agentd 在该终端起对应的 CLI agent(Codex、Claude Code、脚本…)执行,把结果贴回工作项并流转状态。调度是确定性代码,模型只出现在执行器和总指挥里。
## 1. 目标与非目标
- 目标:多项目隔离(一份配置一段)、多终端(本机/经 Warpgate 的远程机)、多 CLI(执行器适配器)、多轮(线程持久、进程不常驻)、失败可解释可重试、凭据只经 Vault 注入内存、任何会话可接手。
- 目标:多项目隔离(一份配置一段)、多终端(本机/经 Warpgate 或 Tailscale 直连的远程机)、多 CLI(执行器适配器)、多轮(线程持久、进程不常驻)、失败可解释可重试、凭据只经 Vault 注入内存、任何会话可接手。
- 非目标:不是工作流引擎(无 DAG/分支,顺序交给 Plane "blocked by"——**TODO:阶段 1 尚未实现**,调度不读 blocked-by 关系,被阻塞的 Todo 项照样会被拾起);不替代 Plane/Temporal 的界面;不做账号系统;不存密钥。
## 2. 架构
@@ -18,7 +18,7 @@ Plane ──webhook(HMAC)即时触发 / cron 轮询兜底──► agentd(
阶段 3 之后调度核心搬到 Temporal(workflow = 一个工作项的生命周期,activity = 起执行器/等结果/回帖),用 Temporal UI 看运行与重试。
## 3. 概念与契约
- **终端(terminal)**:怎么到达一台机器 — `transport`(local | warpgate-ssh)、`os`、`launch`(subprocess | schtasks | nohup)、`repo`、`plane_user`(回帖身份,对应 Vault 键)、`concurrency`、`capabilities`、可用 `executors`。
- **终端(terminal)**:怎么到达一台机器 — `transport`(local | warpgate-ssh | direct-ssh,§9)、`os`、`launch`(subprocess | schtasks | nohup)、`repo`、`plane_user`(回帖身份,对应 Vault 键)、`concurrency`、`capabilities`、可用 `executors`。
- **执行器(executor)**:某个 CLI 的适配器 — 起一轮的命令模板、续线程命令模板、`resume: true|false`、可用模型、默认推理强度、沙箱参数、最后消息取法、线程 ID 取法。
- **路由(routing)**:`by_assignee`(Plane 用户 → 终端)、`by_label`(`exec:*`、`model:*`、`effort:*`、`needs-*` 串行组、`cap:*` 能力、`budget:*` 时限)、每终端默认执行器。
- **工作项契约**:正文 = 任务书;评论 = 消息;执行器最后消息首行 ∈ `[review] | [question] | [blocked] | [progress]`;agentd 回帖格式固定(轮次、run id、退出码、日志路径)。
@@ -76,7 +76,7 @@ notify: {kind: none} # 钩子:核心每次改状态调用 Agentd.notify()
```
agentd/ 调度核心、plane 客户端、vault 注入、状态存储(json/sqlite)、心跳
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/transports/ local.py warpgate_ssh.py(远程 Linux/macOS:bash -c;Windows:PowerShell + schtasks;Warpgate 通道)direct_ssh.py(Tailscale 直连通道,PAD-12)
agentd/web/ 配置页面(阶段 2):读写 YAML + schema 校验 + 连通/冒烟测试 + 最近运行
cli/ agentd tick|new|show|comment|state|assign|smoke|webhook-serve|check-config
systemd/ plane-agentd-webhook.service(LXC 上的 webhook 接收端)
@@ -98,10 +98,16 @@ docs/ design.md executor-contract.md runbook.md
- 网络(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,复用既有登录。
- 阶段 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` 或 `direct-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`。
- 防重复启动(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 上就不必再复制这两个脚本。
- Windows 走 Warpgate 时仍用 helper 脚本 `win10-ps.sh` / `win10-put.sh`(仓库 `tools/win10/`,写死 `--target win10`)+ 计划任务:`agent-access.py run` 把 ssh 的 stdin 接到 /dev/null(`run_remote` 里 `stdin=subprocess.DEVNULL`),cmd.exe 命令行又限 8191 字符,文件只能按 2400 字符分块上传。走 `direct-ssh` 时不用 helper 脚本。收尾读取在 Warpgate 上也已合成一次调用(脚本足够短,放得进 `-EncodedCommand`)。
- **终端通道(PAD-12,2026-10-22)**:LXC 到每个远程终端走哪条路,按终端配置 `transport`:
- `warpgate-ssh`(原有,缺省):`agent-access.py run` 经 Warpgate SSH 网关(`115.28.210.101:2222`)。每次调用都要取 Vault 凭据、与网关握手、密码认证、网关再连终端,LXC 上实测中位数约 9 s(客户端 + vault 0.8 s、网关 banner/kex 1.8 s、密码认证 2.2 s、二跳 + 命令 2–5 s)。客户端写死 `ControlMaster=no`,连接不能复用,数据只能放 argv。一次 win10 启动约 10 次串行调用(busy 检查、mkdir/清空、prompt.md 分块上传 + 重置 + 解码、run.ps1、schtasks):GW-26-1 用了 91 s;收尾约 7 次,约 60 s。
- `direct-ssh`(新):LXC 经 Tailscale 直连终端的 sshd(`ssh: {host, user, port, host_key}`)。认证用一把专用 ed25519 密钥:Vault 键 `AGENTD_SSH_KEY` 经 `--secret-env` 进 tick 进程,载入本进程私有的 ssh-agent(0700 临时目录里的套接字,`ssh-add -` 从 stdin 读)后立即从环境变量删掉,子进程拿不到;不落盘,不进 argv、日志、Plane 评论或异常信息。主机公钥钉死在配置里,`StrictHostKeyChecking=yes`,known_hosts 由钉住的公钥生成,只含这一把。`ControlMaster=auto`,`ControlPersist=60`,套接字放在只有 root 可进的 `agentd.ssh_dir`(`/run/plane-agentd/ssh`),名字是 `<host_key 指纹 8 位>-%C`:pin 改了就换新连接,不会沿用按旧 pin 校验过的主连接。密钥在 CLI 启动时就从环境变量里拿走(第一次直连之前起的子进程也继承不到)。脚本走 stdin:Windows 是一段固定的 PowerShell 引导(`-EncodedCommand`),从 stdin 读真正脚本的 base64;Linux 是固定的 bash 加载器。所以每个操作恰好一次调用,不受 argv 长度限制。win10 启动(建目录、清空并遵守 HASPID 拒绝、写 UTF-8 无 BOM 的 prompt.md/run.ps1、注册并启动计划任务)一次;收尾一次(exit、最后消息、线程 ID、stderr 尾、事件尾)加删计划任务一次。oregon 启动一次,收尾一次。busy 检查和 poll 仍各一次。实测直连路径:win10 RTT 27 ms(握手到认证 0.24 s),oregon RTT 233 ms(握手 2.2 s)。目标:派单延迟 ≤ 15 s。
- `fallback: warpgate-ssh`(可选,需要 `target`):直连**在远程命令运行之前**失败时(ssh 退出 255、没有任何 stdout,且 stderr 里有 ssh 自己在行首打印的连接超时/拒绝/无路由/解析失败/认证失败/agent 错误,或者本进程没拿到密钥。远程加载器把脚本自己的 255 改报 254,所以 255 只可能来自 ssh;有 stdout 就说明命令已经跑过),这个操作改走 Warpgate 重做,本 tick 里该终端余下的操作也都走 Warpgate,`agentd.log` 记一行。**主机公钥不符(包括终端不提供 ssh-ed25519 主机密钥)绝不回退**:按安全事件处理,`agentd.log` 记 `SECURITY …` 行,该终端本 tick 标为 down。命令可能已经运行过的失败(超时、会话中途断开、认不出的 255)不会换路径重试。启动这种非幂等操作仍靠 `launching` 标记加下一 tick 的探测/接管(`resolve_launch`),不存在双启动的路径。直连的每个操作都只有一次远程调用,所以"整个操作换路径重做"不会重复任何副作用。
- **取舍**:直连绕过了 Warpgate 的会话录像和集中授权。访问控制改由终端自己负责:authorized_keys 只认这把专用密钥,并限定来源 `from="100.69.78.124"`(LXC 的 tailnet 地址)。用户已逐终端批准(win10、oregon,2026-10-22)。回滚:把该终端的 `transport` 改回 `warpgate-ssh`(`ssh`/`fallback` 可以留着,此时被忽略)。
- 观测:每次远程调用在 `<log_dir>/remote.log` 记一行(`term= via=direct|warpgate|fallback op= ms= rc=`)。部署后用 `agentd ping <终端> [--transport direct-ssh|warpgate-ssh]` 验证。步骤见 `docs/deploy-lxc.md` §9。
## 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 丢了/接收端挂了只是退回到轮询延迟。
@@ -109,6 +115,6 @@ docs/ design.md executor-contract.md runbook.md
- **路由**:`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 轮开始` 评论在启动调用返回之后才发,所以"建项 → 开始评论"受中继成本限制,而不是触发方式。
- **延迟构成**:投递 → tick 开始 ≈ 3 s 合并窗口;tick 本身 = vault 取 token(1–2 s)+ 读 Plane + 先 poll 每个在途运行(每个中继调用约 10 s)+ 启动(中继约 12 s;win10 上传更久)。`[worker] 第 k 轮开始` 评论在启动调用返回之后才发,所以"建项 → 开始评论"受中继成本限制,而不是触发方式。PAD-12 之后,直连终端每次调用不再是约 10 s,而是亚秒到约 2 s(§9 终端通道)。
- **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-22 已应用(同时重建了 webhook-consumer、outbox-poller、automation-consumer、beat-worker,见 deploy-lxc §8),webhook 已启用,cron 保留为兜底。Plane 建出的是 v2 webhook(`plane/webhook/bgtasks/webhook_task.py` 的 `send_webhook`,负载 `{version: v2, event, entity_type, data, …}`),签名同为 HMAC-SHA256(secret, json.dumps(payload)),`data.project` 路由照常,接收端无需改动。
- **心跳里的跳过原因**:每个 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: …`),同一项的原因变化时才写一行日志。
+3 -3
View File
@@ -26,11 +26,11 @@ agentd 不常驻:一轮运行比起它的那次 tick 活得久,所以执行
- `kill(handle)` 既用于超时,也用于取消(工作项在运行中被移出 In Progress);必须能在运行未结束时调用。取消后 core 会尝试 `collect(handle)` 取线程 ID,所以 `collect` 在未结束的运行上也不能抛出致命错误(抛异常会被忽略)。
## RunnerExecutor(现有三个适配器的共同形态)
1. 在终端上建运行目录 `<run_root>/<dir_name>`(清空旧文件;目录里已有 `pid.txt`/`launch.pid` 时拒绝并报 `HASPID`),写 `prompt.md`。transport 有 `start_run` 时(远程 POSIX),建目录、写 `prompt.md` 与 runner、启动合并成一次中继调用。
1. 在终端上建运行目录 `<run_root>/<dir_name>`(清空旧文件;目录里已有 `pid.txt`/`launch.pid` 时拒绝并报 `HASPID`),写 `prompt.md`。transport 有 `start_run` 时(远程终端),建目录、写 `prompt.md` 与 runner、启动由 transport 一起做:直连(direct-ssh)和远程 POSIX 是一次调用,Windows 经 Warpgate 时仍逐步上传。
2. 用配置里的命令模板生成命令:`run`(新线程)或 `resume`(续线程)。占位符:`{repo} {model} {effort} {tier} {out} {thread} {perm} {script} {run_dir}`;未知占位符原样保留。`{tier}` 展开为 `tier_arg`(如 `-c 'service_tier="fast"'`),无 tier 时为空。tier 来源:`tier:<x>` 标签或 `routing.by_label` 的 `tier`,否则执行器的 `tier_defaults[<model>]`;`tier:none` 表示不加档位参数。模板首词若在终端 `bins` 里,替换为绝对路径(Windows 前加 `&`)。参数请用单引号,bash 和 PowerShell 都认。
3. transport 生成 runner 脚本(Linux `run.sh`,Windows `run.ps1`):进入 repo,写 `pid.txt`/`started.txt`,把 `prompt.md` 从 stdin 喂给命令,stdout → `stdout_name`,stderr → `stderr.txt`,结束写 `exit.txt`(`exit=N at …`)。
4. transport 分离启动(本机 `setsid -f bash run.sh`;远程 Linux `setsid nohup bash run.sh &`,macOS 用 `perl POSIX::setsid`,`$!` 记入 `launch.pid`;Windows 一次性计划任务 `agentd-<run_id>`)。
5. `poll` = `exit.txt` 是否存在;`collect` 由各适配器从终端上保存的文件解析(远程时经中继读回,不依赖调度器本机有任何 CLI):
5. `poll` = `exit.txt` 是否存在;`collect` 由各适配器从终端上保存的文件解析(远程时读回,不依赖调度器本机有任何 CLI)。适配器把要读的东西列成 `COLLECT`(`("read", 文件)`、`("grep", 文件, 子串)`、`("tail", 文件, n)`),经 `transport.fetch` 一次调用取回;transport 没有 `fetch` 时逐个读:
| 适配器 | stdout | 最后消息 | 线程 ID |
|---|---|---|---|
@@ -41,7 +41,7 @@ agentd 不常驻:一轮运行比起它的那次 tick 活得久,所以执行
配置里的 `thread_from` / `last_message` 目前只是说明性字段,解析逻辑在适配器里。
## Transport 接口(`agentd/transports/`)
`os`、`join`、`quote`、`default_run_root`、`mkdir(path, clean)`(clean 遇到 pid 文件必须拒绝)、`probe(run_dir, names)`(存在的文件名列表,一次调用)、`write_text`、`read_text`(不存在返回 None)、`grep_first(path, needle)`、`tail(path, n)`、`runner(run_dir, cwd, command, stdout_name)`、`launch(task, script, run_dir)`、`kill(task, run_dir)`、`forget(task)`、`process_count(name)`(None = 不可达);可选 `start_run(task, run_dir, files, script_name)`(一次完成建目录 + 写文件 + 启动)。
`os`、`join`、`quote`、`default_run_root`、`mkdir(path, clean)`(clean 遇到 pid 文件必须拒绝)、`probe(run_dir, names)`(存在的文件名列表,一次调用)、`write_text`、`read_text`(不存在返回 None)、`grep_first(path, needle)`、`tail(path, n)`、`runner(run_dir, cwd, command, stdout_name)`、`launch(task, script, run_dir)`、`kill(task, run_dir)`、`forget(task)`、`process_count(name)`(None = 不可达);可选 `start_run(task, run_dir, files, script_name)`(建目录 + 写文件 + 启动)和 `fetch(run_dir, ops)`(一次读回多个结果,`collect` 用)。远程 transport 的脚本由一个"通道"送达:`warpgate-ssh`(`agent-access.py run`,只能走 argv)或 `direct-ssh`(Tailscale 直连 OpenSSH,脚本走 stdin,每个操作一次调用;可配 `fallback: warpgate-ssh`,见 design §9)。直连在命令运行前失败时抛 `Unreached`(`RoutedTransport` 据此换到兜底通道),主机公钥不符时抛 `HostKeyError`(不回退);`process_count` 不能吞掉 `Unreached`。
- `local`:Linux 本机文件 + `setsid -f`;kill = 对 `pid.txt` 的进程组发 SIGTERM。用于调度器与执行器同机的部署和测试。
- `warpgate-ssh` + `os: linux|darwin`(`PosixWarpgateTransport`):每个操作是一次 `agent-access.py --config <vault.config> run --target <target> -- bash -c '<script>'`(`relay()`,目标名取终端 `target`,缺省为终端名;客户端把 argv `shlex.join` 后交给目标账户的登录 shell,stdin 为 /dev/null)。文件:base64 放进 `printf %s '<b64>' | base64 -d`,超过 60000 字符分块追加再解码,回显字节数校验;读文件/`grep_first`/`tail` 的结果以 `B64=` 标记回传。runner 与本机相同(`run.sh`),`setsid nohup` 脱离 SSH 会话(pid = pgid = sid);kill = 对该进程组 `kill -TERM -- -<pid>` + `pkill -TERM -s <pid>`,最多等 5 s 后 KILL,`exit.txt` 已存在时不发信号(防 pid 复用);`process_count` = `pgrep -x`。`repo`/`run_root` 必须是远程绝对路径。每次调用约 10–14 s,所以启动只用一次调用。
- `warpgate-ssh` + `os: windows`(`WindowsWarpgateTransport`):经 `win10-ps.sh`(PowerShell `-EncodedCommand`)和 `win10-put.sh`(分块上传 + SHA-256 校验);文件读取用 base64 标记回传;kill = `taskkill /T /F /PID <pid.txt>` + `schtasks /End /Delete`。脚本目录由 `agentd.helpers_dir`(或终端 `helpers_dir`)配置;脚本目前写死 `--target win10`(TODO:改走同一个 `relay()`,argv 为 `powershell.exe -NoProfile -NonInteractive -EncodedCommand <b64>`,去掉 helper 脚本)。路径都放进 PowerShell 单引号字面量(`'` 双写);`schtasks /TR` 里脚本路径用 `\"…\"` 包裹,依赖 Windows PowerShell 5.1 的旧式原生参数传递(PowerShell 7.3+ 的 Standard 模式下需改写)。runner 进不了 repo 时写 `exit=97` 结束。
+14 -2
View File
@@ -19,13 +19,15 @@ cd /root/.local/share/server-access-client && python3 -I agent-access.py \
--config https://nas.tail9531b.ts.net:8443/identity/agent.json exec \
--secret-env PLANE_OREGON_API_TOKEN=PLANE_OREGON_API_TOKEN \
--secret-env PLANE_WINDOW10_API_TOKEN=PLANE_WINDOW10_API_TOKEN \
--secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY \
--timeout 900 -- python3 /opt/plane-agentd/cli/agentd tick --once
```
`AGENTD_SSH_KEY` 是 direct-ssh 终端的专用密钥(PAD-12,design §9)。手工运行时不加这个参数不会出错,只是直连终端整次 tick 都走 Warpgate 兜底(慢),`agentd.log` 里会有一行 `direct-ssh unavailable in this process`。但加了参数而 Vault 里没有这个键,agent-access.py 会以 `access-hub: credential_unavailable` 直接失败,agentd 根本不运行(它没有可选密钥);cron 脚本遇到这种情况会去掉这个参数再跑一次,手工运行则要自己去掉。
crontab(LXC root,每 2 分钟):
```
*/2 * * * * /opt/plane-agentd/cron/agentd-tick-lxc.sh
```
`cron/agentd-tick-lxc.sh` = `flock -n /var/lib/plane-agentd/cron.lock timeout 900 python3 -I agent-access.py --config <identity> exec --secret-env PLANE_OREGON_API_TOKEN=… --secret-env PLANE_WINDOW10_API_TOKEN=… --timeout 850 -- python3 /opt/plane-agentd/cli/agentd --config /opt/plane-agentd/config/agentd.yaml tick --once >> /var/log/plane-agentd/cron.log 2>&1`。空闲 tick 不打印任何东西(cron 日志为空是正常的),活着与否看 `heartbeat.json` 的 `at`;`skip: another tick holds …` 表示与手工 tick 撞锁,无害。
`cron/agentd-tick-lxc.sh` = 拿 `/var/lib/plane-agentd/cron.lock`(`flock -n -E 75`,撞锁退出 75)后执行 `timeout 900 python3 -I agent-access.py --config <identity> exec --secret-env PLANE_OREGON_API_TOKEN=… --secret-env PLANE_WINDOW10_API_TOKEN=… --secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY --timeout 850 -- python3 /opt/plane-agentd/cli/agentd --config /opt/plane-agentd/config/agentd.yaml tick --once`,输出追加到 `/var/log/plane-agentd/cron.log`(stderr 排在 stdout 之后)。如果这次 exec 只报了 `access-hub: credential_unavailable`/`credential_invalid`(退出 1,agentd 没运行),脚本在 cron.log 记一行 `AGENTD_SSH_KEY not available from Vault: tick repeated without it …`,去掉 `AGENTD_SSH_KEY` 再跑一次:直连终端走兜底,派单不中断。空闲 tick 不打印任何东西(cron 日志为空是正常的),活着与否看 `heartbeat.json` 的 `at`;`skip: another tick holds …` 表示与手工 tick 撞锁,无害。
实际配置 `config/agentd.yaml`(已提交,不含密钥):`state_dir: /var/lib/plane-agentd`、`log_dir: /var/log/plane-agentd`,所以下表的 `state/…` 在 LXC 上是 `/var/lib/plane-agentd/…`,`agentd.log` 在 `/var/log/plane-agentd/`。改配置/代码 = 在 oregon 提交并推送,然后 LXC 上 `cd /opt/plane-agentd && git pull`(下一个 tick 生效)。
oregon 上的 `cli/agentd` 只当 CLI 用(new/show/comment/state/assign,`smoke … --watch`);**不要在 oregon 上跑 `tick` 或不带 `--watch` 的 `smoke`**(那是第二个调度器)。
暂停一个终端(不再派新运行、也不探测,在途运行照常收尾):把它的 `concurrency` 设为 0。
@@ -48,6 +50,8 @@ cli/agentd state GWENT 12 Todo # Plane 状态名或配置键(todo/review
cli/agentd assign GWENT 12 window10
cli/agentd smoke GWENT oregon --watch # 建一个要求回复 "[review] ok" 的工作项,等排程中的调度器跑完,PASS 则移到 Done
cli/agentd smoke GWENT win10 --labels model:sol --watch # --labels 给冒烟项加标签(路由到模型/档位)
cli/agentd ping win10 [--transport direct-ssh|warpgate-ssh] [-n 3] # 在终端上跑一条空命令并计时,每次调用一行
# (direct-ssh 需要 --secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY,不需要 Plane token)
# 不带 --watch = 本机自己 tick(只在调度器所在机器上、且它的 cron 停着时用)
```
CLI 以 `--as <terminal>` / `agentd.cli_terminal` / 第一个有 token 的终端的身份操作。
@@ -78,6 +82,8 @@ cron 每 2 分钟一次的 tick 保留作兜底;另有常驻接收端,Plane
| `<log_dir>/agentd.log`(默认 `state/`) | 每次 tick 的动作与错误 |
| `state/heartbeat.json` | 最近一次 tick 的时间、在途运行、启动未确认的运行(`launching`)、本 tick 跳过的不可达终端(`down`)、没派出去的 Todo 项及原因(`skipped`:`[{project, item, terminal, reason}]`)、错误;超过 10 分钟不更新 = agentd 没在跑 |
| `<log_dir>/webhook.log` | webhook 接收端:每次投递的决定、每次触发的 tick(见上节) |
| `<log_dir>/remote.log` | 每次远程调用一行:`term=win10 via=direct\|warpgate\|fallback op=start\|fetch\|read\|busy\|probe\|kill\|forget… ms=… rc=…`,失败时末尾带 `unreached`/`hostkey`/`unknown`(PAD-12)。不回显到 cron.log;没有轮转,需要时手工截断 |
| `agentd.ssh_dir`(LXC `/run/plane-agentd/ssh`) | direct-ssh 的 ControlMaster 套接字(`<host_key 指纹 8 位>-<%C 哈希>`,空闲 60 s 后自动退出;手工关闭:`for s in /run/plane-agentd/ssh/*-*; do [ -S "$s" ] && ssh -O exit -o ControlPath="$s" x; done`)和由钉住公钥生成的 `known_hosts.<终端>`;root 0700,agentd 自己建。ssh-agent 在 `/tmp/agentd-agent-*`(每个 tick 进程一个,退出时删除) |
| `state/agentd.lock` | tick 互斥锁 |
| `state/runs/<run_id>/` | `transport: local` 终端的运行目录:`prompt.md run.sh pid.txt started.txt exit.txt stderr.txt` + 执行器输出 |
| oregon `/home/ubuntu/.gwent-board/agentd-runs/<run_id>/` | 远程 Linux 终端(`run_root`):同上,另有 `launch.pid`(启动时的 `$!`,与 `pid.txt` 相同);claude 输出 `out.json` |
@@ -111,7 +117,13 @@ Todo + 指派给终端用户 → agentd 选最小编号、终端有空位、串
| `run GW-n-k: nothing at …; relaunching in GW-n-k-r2` | 启动没有落地:同一 run id、新目录 `<run_id>-r2`(再失败 -r3…)重新启动,旧目录不清空。运行目录里已有 pid 文件时 transport 拒绝清空(`HASPID`),下一 tick 探测后接管 |
| heartbeat `launching` 一直不消失 | 终端探测不通(中继坏),标记保留并占着该终端的槽位;中继恢复后自动接管或重启。人工处理:停 cron,在终端上看 `<run_root>/<run_id>/`,再从状态文件删掉该项的 `launching` |
| `upload of … size mismatch` / `staging … size mismatch` | 远程写文件不完整(磁盘满、`run_root` 不可写);看远程 `run_root` |
| 远程 Linux 终端每步都慢 | 每次中继调用约 10–14 s(取 Vault 凭据 + SSH 握手)。一轮:启动 1 次(prompt 超过约 45 KB 时多几次分块上传)、每 tick poll 1 次、收尾 claude 约 3 次 |
| 远程终端每步都慢 | `warpgate-ssh`(或 `via=fallback`)每次调用约 9–14 s(取 Vault 凭据 + 网关握手 + 密码认证 + 二跳)。`direct-ssh` 首次握手 win10 约 0.5 s、oregon 约 2 s,之后 60 s 内复用连接。看 `remote.log` 的 `ms=`。一轮的调用次数:启动 1 次(Warpgate 上 prompt 超过约 45 KB 时多几次分块上传;win10 经 Warpgate 约 8–10 次中继调用),每 tick poll 1 次,收尾 1 次(win10 另加删计划任务 1 次) |
| `SECURITY <终端>: … host key verification FAILED …` | 终端出示的主机公钥和配置里钉住的不一致:可能是重装、sshd 重新生成了主机密钥,也可能是中间人。**不会回退到 Warpgate**,该终端本 tick 标为 down(`heartbeat.down`),每个 tick 都会再报一次。先经第二通道核对(Warpgate:`agent-access.py … run --target oregon -- cat /etc/ssh/ssh_host_ed25519_key.pub`,win10 `win10-ps.sh "Get-Content C:\ProgramData\ssh\ssh_host_ed25519_key.pub"`),确认是合法变更后再更新 `terminals.<t>.ssh.host_key`(deploy-lxc §9 第 4 步)。新的 pin 会换一个 ControlMaster 套接字名(`<pin 指纹>-%C`),不会复用按旧 pin 校验过的连接。终端根本不提供 ssh-ed25519 主机密钥(`no matching host key type`)也按这一行处理。等核对期间要继续派单:把 `transport` 改回 `warpgate-ssh` |
| `<终端>: direct-ssh failed before the command ran (…); using the warpgate-ssh fallback for the rest of this tick` | 直连在命令运行前就失败了(tailscale 断、sshd 停、authorized_keys 缺或 ACL 不对、密钥被拒)。这个操作已在 Warpgate 上重做,本 tick 余下的也走 Warpgate,下个 tick 再试直连。`remote.log` 里会出现 `via=fallback`。查原因:`cli/agentd ping <终端> --transport direct-ssh -n 1`,括号里是 ssh 的原话(如 `Permission denied (publickey)`、`connect to host … Connection timed out`) |
| `direct-ssh unavailable in this process: AGENTD_SSH_KEY is not set …` / `ssh-add failed …` | 进程没拿到密钥(手工运行没加 `--secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY`,或 cron 脚本因为 Vault 取不到键而去掉了它),或 Vault 里的值不是无口令的 OpenSSH 私钥。直连终端整次都走兜底;没配 `fallback` 的终端本 tick down |
| cron.log:`AGENTD_SSH_KEY not available from Vault: tick repeated without it …` | Vault 键 `AGENTD_SSH_KEY` 被删、改名或不可读,agent-access.py 拒绝了整个 exec。tick 已不带密钥重跑,直连终端每个 tick 都走 Warpgate 兜底(慢,但不停派单)。修复:按 deploy-lxc §9 第 1 步恢复这个键;`agent-access.py … list` 能看到键名是否还在 |
| 手工 `exec … --secret-env AGENTD_SSH_KEY=AGENTD_SSH_KEY` 只打印 `access-hub: credential_unavailable` | 同上,Vault 里没有这个键(也可能是某个 Plane token 键)。手工运行时去掉 `AGENTD_SSH_KEY` 那个参数即可 |
| `ssh administrator@… timeout after 150s` / `ssh … exit 255: …`(无 fallback 行) | 直连的命令**可能已经运行**(超时、会话中途断开、认不出的 255),按规则不换路径重试:终端本 tick down;启动的话保留 `launching` 标记,下一 tick 探测后接管或在 `-r2` 重启。win10 上每次都超时:多半是 stdin 没到 PowerShell(sshd 默认 shell 被改成了 PowerShell),见 deploy-lxc §9 第 3 步 |
| 工作项在运行中被人改了状态 | 运行还没结束:下一 tick 取消(kill + `[worker] 运行已取消…` 评论),不改状态。已经结束(`exit.txt` 已写):结果照常回帖,但不再改状态/重试 |
| Plane 回帖失败 | 在途记录保留,下一 tick 重新 collect 并回帖(可能重复一条评论) |
| 想手工终止一轮 | 把工作项移出 In Progress(如 Waiting)即可,下一 tick agentd 自己 kill 并释放槽位。中继不通、kill 一直失败时:在终端上手工结束(Linux 终端 `kill -TERM -- -$(cat pid.txt); pkill -TERM -s $(cat pid.txt)`,win10 `taskkill /T /F /PID <pid.txt>`),停 cron 后从 `state/agentd-state.json` 删掉该项的 `inflight` |
+5 -4
View File
@@ -12,8 +12,8 @@
| Plane 指派人 | 终端 | 执行器 | 默认模型 / effort / 档位 | 运行目录(日志和输出) |
|---|---|---|---|---|
| `window10` | win10(Warpgate + Windows 计划任务) | codex(`codex exec`,`workspace-write`) | `gpt-6.1-sol` / `medium` / `fast` | `C:\…\gwent\runtime\agentd\<run_id>\`(`prompt.md run.ps1 events.jsonl last.md stderr.txt exit.txt`) |
| `oregon` | oregon(Warpgate + `setsid nohup`) | claude-code(`claude -p --permission-mode acceptEdits`) | `sonnet` / (不传 effort)/ — | `/home/ubuntu/.gwent-board/agentd-runs/<run_id>/`(`out.json` 等) |
| `window10` | win10(Tailscale 直连 SSH,Warpgate 兜底;Windows 计划任务) | codex(`codex exec`,`workspace-write`) | `gpt-6.1-sol` / `medium` / `fast` | `C:\…\gwent\runtime\agentd\<run_id>\`(`prompt.md run.ps1 events.jsonl last.md stderr.txt exit.txt`) |
| `oregon` | oregon(Tailscale 直连 SSH,Warpgate 兜底;`setsid nohup`) | claude-code(`claude -p --permission-mode acceptEdits`) | `sonnet` / (不传 effort)/ — | `/home/ubuntu/.gwent-board/agentd-runs/<run_id>/`(`out.json` 等) |
| `kappiee` / `mac` / 无人 | 不派 | — | — | — |
- 两个终端的并发都是 1,按终端计数,跨项目共享。
@@ -44,7 +44,7 @@ Plane 里目前只建了 4 个标签:`model:astra`、`model:sol`、`effort:hig
- tick 运行中又来投递时,结束后再补跑一次。锁被占用(退出码 75)时每 15 s 重试,最多 2 min。
- 兜底:cron 每 2 分钟一次 tick,同一把 `cron.lock`。
- agentd 自己回帖、改状态也会产生投递,所以每个有动作的 tick 后面都会多一次空 tick(实测 11.1 s)。
2. **tick 的步骤**:先 poll 所有在途运行(每次中继调用约 10 s),再按编号从小到大挑 Todo 项。依次检查:中继是否通、并发槽位、串行组、busy 进程。
2. **tick 的步骤**:先 poll 所有在途运行(每个一次远程调用:直连亚秒到约 2 s,走 Warpgate 时约 10 s),再按编号从小到大挑 Todo 项。依次检查:终端是否可达、并发槽位、串行组、busy 进程。
- 启动前先把 `launching` 标记落盘,防止重复启动。
- 启动后把项改为 In Progress 并指派给终端账号,以终端账号发评论 `[worker] 第 k 轮开始:codex / gpt-6-astra / high @ win10,新线程,run=GW-26-1。`
3. **实测耗时**(runbook 记录和 LXC 的 webhook.log):
@@ -52,7 +52,8 @@ Plane 里目前只建了 4 个标签:`model:astra`、`model:sol`、`effort:hig
- **win10 启动的那次 tick 91.4 s**(GW-26:建项 01:25:14,开工评论 01:26:49)。
- oregon 启动的 tick 约 72 s。
- 空 tick 4.6–4.8 s。
- 瓶颈在中继调用(PAD-12),与触发方式无关。
- 以上是 Warpgate 时期的数字,瓶颈在中继调用,与触发方式无关。
- PAD-12 起两个终端都走 Tailscale 直连:win10 启动由约 10 次中继调用变成 busy 检查 + 1 次启动调用,收尾由约 7 次变成 2 次。目标是建项到开工评论 ≤ 15 s,上线后在这里补实测。直连不通时这一轮自动走 Warpgate,只是慢,派单行为不变(design §9)。
4. **结束时**:以终端账号回帖。
- 第一行是 `[codex] 第 k 轮结束(exit=…,run=…,终端 win10,日志 <运行目录>)`,下面是 agent 的最后消息原文(`<pre>` 包裹,超过 6000 字截断,并注明全文所在路径)。
- 再按最后消息首行的标签流转(`transition()`):