Files
plane-agentd/docs/runbook.md
T

36 KiB
Raw Blame History

运行手册

安装

git clone https://git.kappiee.com/kappiee/plane-agentd.git && cd plane-agentd
python3 -m pip install --user pyyaml          # 唯一依赖;有 jsonschema 时用它校验,否则用内置子集校验器
cp config/agentd.example.yaml config/agentd.local.yaml   # git-ignored;或用 AGENTD_CONFIG 指向别处
cli/agentd check-config                       # 校验 + 列出各终端 token 环境变量是否已注入 + cron 所需 --secret-env 参数
python3 -m unittest                           # 离线单测

调度器迁到不装 agent CLI 的 LXC:见 docs/deploy-lxc.md。 配置里只写 Vault 键名(token_key)。agentd 读同名环境变量,token 只由 agent-access.py exec --secret-env KEY=KEY 注入进程内存;不要 export 到 shell、不要写文件。agentd 起的所有子进程(本机 runner/执行器、win10 中继脚本)拿到的环境都去掉了全部 token_key 与 webhook_secret_key 变量。

运行

调度器 2026-10-21(PAD-11)起跑在 LXC agentd-01(root,仓库 /opt/plane-agentd),部署细节见 docs/deploy-lxc.md。任何时候只能有一个调度器排程。 单次 tick(在 LXC 上):

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 = 拿 /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。

常驻替代:... exec ... -- cli/agentd tick --loop --interval 120(vault 会话超时要够长,cron 更稳)。 执行器的二进制一律用终端 bins 写绝对路径(如 claude: /home/ubuntu/.local/bin/claude):cron 和 SSH 非登录 shell 的 PATH 都只有系统目录。

执行器二进制的解析(bins 或 PATH)

命令模板的首词(codex、claude…)若在该终端的 bins 里,替换为那里的绝对路径;否则原样交给终端的 shell 按 PATH 查找。没有任何其他探测或回退:

  • oregon(2026-10-21 起 warpgate-ssh,os linux):agentd 经中继 agent-access.py run --target oregon -- bash -c … 在 oregon 上 setsid nohup bash run.sh,runner 的环境是 sshd 给 ubuntu 用户的非登录环境(PATH = 系统默认,没有 ~/.local/bin),所以 claude 必须写进 bins。transport: local(runner 继承 agentd 进程环境,cron 下 PATH 只有 /usr/bin:/bin)仍可用于调度器与执行器同机的部署和测试。示例配置的 oregon 列了 codex 执行器但没有 bins.codex,即假定 codex 在 PATH 上——目前 oregon 没装 codex(which codex 为空),带 exec:codex 的 oregon 工作项会以 exit 127(command not found,类型=crash,非临时性)转 Waiting。要用就装 codex 并加 bins: {codex: <绝对路径>},否则从 oregon 的 executors 去掉 codex。
  • win10(warpgate-ssh):runner 是计划任务里的 powershell.exe -File run.ps1,以创建任务的用户(Administrator,交互登录)运行,PATH 取该用户的环境。示例配置用 bins.codex 写死 codex.exe 绝对路径;去掉它就等于假定 Administrator 的 PATH 里有 codex。
  • check-config 不检查二进制是否存在;首次接入一个终端用 agentd smoke <PROJECT> <terminal> 验证。

人工操作(同样在 exec --secret-env 下):

cli/agentd new GWENT "T-010 标题" --file brief.md --assign window10 --labels model:astra,needs-game --module M1 --state Todo
cli/agentd show GWENT GW-12 3        # 状态/指派/标签 + 最近 3 条评论
cli/agentd comment GWENT 12 reply.md # 或 - 读 stdin
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 的终端的身份操作。

Webhook(PAD-7:Plane 事件即时触发 tick)

cron 每 2 分钟一次的 tick 保留作兜底;另有常驻接收端,Plane 一有工作项/评论变化就立刻触发一次 tick。

  • 服务:LXC plane-agentd-webhook.service(单元文件在仓库 systemd/,Restart=always)= agent-access.py … 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。只听内网地址;密钥只在进程环境里,agentd 起的子进程(tick 脚本)环境里去掉了它。
  • 端点:POST http://10.77.10.249:8790/plane;GET /healthz → {"ok": true, "pending", "queued", "running", "retrying_since", "ticks", "last": {rc, seconds, deliveries, at}}。
  • Plane 侧:工作区级 webhook(kappiee 工作区设置 → Webhooks,或 tools/plane-webhook.py),订阅"工作项"(issue)和"评论"(issue_comment)。Plane 会把所有项目的事件都发来,接收端按 data.project 只认配置里的项目。
  • 签名:头 X-Plane-Signature = 用 webhook 密钥对请求体做 HMAC-SHA256 的十六进制摘要(Plane 签的是 json.dumps(payload),发出的正是这串字节;接收端先比原始请求体,再比重新序列化的结果,均为常量时间比较)。缺失/不对 → 401,什么都不触发。其他头:X-Plane-Event(issue / issue_comment)、X-Plane-Delivery(每次投递的 UUID)。请求体:{"event","action"(created/updated/deleted),"webhook_id","workspace_id","workspace_slug","data":{…,"project":<uuid>},"activity":{field,new_value,old_value,actor…}};删除事件的 data 只有 id(没有项目 → 202 忽略,由 cron 兜底)。
  • 触发逻辑:配置项目的投递 → 200,3 s(debounce_seconds)内的投递合并成一次 tick;tick 运行期间又来的投递 → 它结束后再跑一次(正在跑的 tick 可能在变化之前就读过 Plane)。tick 命令就是 cron 脚本(systemd-run --scope … cron/agentd-tick-lxc.sh,在独立 scope 里跑,重启接收端不会杀掉正在跑的 tick)。脚本持 flock -n -E 75:锁被 cron/手工 tick 占着 → 退出码 75 → 每 15 s 重试,最长 2 min,之后交给下一个 cron tick。tick 本身的输出照旧进 cron.log/agentd.log。
  • 日志 /var/log/plane-agentd/webhook.log:每次投递一行(delivery=<X-Plane-Delivery> event=issue/updated project=GWENT item=<uuid> decision=tick|202 ignored|401 bad signature|401 missing signature),每次 tick 两行(tick start (n deliveries: …)、tick done rc=0 in 12.3s / tick busy …, retry in 15s / giving up)。Plane 侧的投递记录:tools/plane-webhook.py logs <webhook id>。
  • 查看:systemctl status plane-agentd-webhook、journalctl -u plane-agentd-webhook、curl -s http://10.77.10.249:8790/healthz、tail -f /var/log/plane-agentd/webhook.log。
  • 轮换密钥(在 oregon 上,kappiee 管理员会话,密钥不出进程、直接写进 Vault):
    cd ~/.local/share/server-access-client && python3 -I agent-access.py --config https://nas.tail9531b.ts.net:8443/identity/agent.json exec \
      --secret-env PLANE_KAPPIEE_PASSWORD=PLANE_KAPPIEE_PASSWORD --timeout 90 -- \
      python3 -I /home/ubuntu/Documents/wjj/project/plane-agentd/tools/plane-webhook.py regenerate <webhook id>   # id: … plane-webhook.py list
    
    然后 LXC 上 systemctl restart plane-agentd-webhook(接收端只在启动时取一次密钥;轮换到重启之间的投递会 401,cron 兜底)。核对:agent-access.py … exec --secret-env S=PLANE_WEBHOOK_SECRET -- python3 -c 'import os;print(len(os.environ["S"]))'。
  • 停用:只停接收端 systemctl disable --now plane-agentd-webhook(Plane 投递会失败并重试,多次失败后 Plane 自动停用该 webhook 并给管理员发邮件);连 Plane 侧一起停:plane-webhook.py patch <id> is_active=false。cron 不受影响。
  • 通知:配置里的 notify: {kind: none} 是钩子,核心每次改状态都调用 Agentd.notify(),目前什么都不做(没有 ntfy 服务)。

文件

路径 内容
state/agentd-state.json 每项目每工作项:轮次、线程 ID 与所在终端/执行器、线程轮数、各轮标签、已见评论、重试计数/下次重试、在途运行(handle、deadline)
<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
<repo>\runtime\agentd\<run_id>\(win10) 同上,run.ps1;codex 输出 events.jsonl last.md

工作项生命周期

Todo + 指派给终端用户 → agentd 选最小编号、终端有空位、串行组空闲的项 → In Progress,评论 [worker] 第 k 轮开始… → 执行器结束后回帖 [<executor>] 第 k 轮结束(exit, run, 终端, 日志) + 最后消息:

  • [review] → Review,指派 reviewer;[question]/[blocked] → Waiting,指派 answerer(默认 reviewer);[progress] → Todo,留在原终端,下一轮续线程;首行无标签 → 按 review 处理并注明。
  • 回到 Todo 的项总会被重新拾起;续线程时只发送上次之后的新评论(按评论 ID 判断,[worker] 评论永远跳过)。
  • 线程重建:换终端/执行器、续线程失败、线程轮数 ≥ max_rounds_per_thread → 新线程,首轮输入附交接摘要(各轮标签 + 上一轮最后消息),开始评论里注明原因与"附交接摘要"。临时性错误的自动重试算同一轮,不计入线程轮数(run id 照常递增)。
  • 连续 max_progress_rounds(项目配置,默认 12)轮 [progress] → 不再续跑,回帖说明并转 Waiting 指派 answerer;人工移回 Todo 后重新计数。
  • 运行中工作项被移出 In Progress(任何状态)→ 下一 tick 结束该运行(kill),回帖 [worker] 运行已取消:工作项状态被改为 <状态>,释放终端槽位与串行组,保留线程 ID(移回 Todo 时续原线程)。kill 失败则保留槽位,下一 tick 再试。
  • 未实现(TODO):Plane 的 "blocked by" 关系不参与调度——被阻塞的项只要在 Todo 且指派给终端就会被拾起。需要先后顺序时,先只把前一项放进 Todo。

常见故障

现象 原因 / 处理
no Plane token in the environment / tick 报 no Plane token 没在 agent-access.py exec --secret-env … 下运行,或键名不对;check-config 看 MISSING
skip: another tick holds state/agentd.lock 上一 tick 还在跑(win10 上传慢);正常(tick 此时退出码 75)。持续出现则查是否有卡死的 agentd 进程
Todo 项一直不动、心跳看着空闲 看 heartbeat.json 的 skipped:concurrency n/m (GW-x) 槽位被谁占着;serialize_group … held by …;busy_process codex (… not started by agentd) = win10 上有不是 agentd 起的(或上一轮还没退干净的)codex.exe;relay down;no route for assignee … = 指派的用户不是终端。原因第一次出现时 agentd.log/cron.log 有一行 GW-n not dispatched (<终端>): <原因>
webhook.log 里 decision=401 签名不对:Plane 侧密钥轮换后接收端没重启,或不是 Plane 发的。systemctl restart plane-agentd-webhook
Plane 建 webhook 报 "The webhook URL is not allowed…" Plane 的 SSRF 防护拒绝内网地址:Plane api/worker 环境需 WEBHOOK_ALLOWED_IPS=10.77.10.249/32(design §10)
[worker] 第 k 轮失败:类型=crash(临时性)…重试 n/N 网络/中继/429/5xx:按 backoff_minutes 自动重试,续原线程;超过 transient_max 转 Waiting
类型=timeout 超过 timeout_hours(或 budget:2h 标签);进程树已结束,转 Waiting。看运行目录日志决定拆分任务或加预算
类型=crash(非临时)且是续线程 自动在新线程重跑一次(附交接摘要);再失败转 Waiting
类型=env 匹配 retry.env_patterns(焦点丢失/弹窗/断网等),转 Waiting,同串行组暂停到该项离开 Waiting
win10 unreachable; skipping / busy (N codex running…) 中继不通,或 win10 上有不是 agentd 起的 codex(busy_processes);等它结束
schtasks launch … failed / upload … failed 中继或 Windows 计划任务问题;手工 win10-ps.sh "schtasks /Query /TN agentd-<run>" 查看
relay oregon exit 255: … / relay timeout / <terminal> unreachable, skipping it for the rest of this tick 中继(Vault、Warpgate、目标 sshd)不通;该终端本 tick 余下的 poll/启动全部跳过(heartbeat.down),其他终端照常。poll 失败下一 tick 再试。手工:agent-access.py --config … run --target oregon --timeout 30 -- bash -c 'echo ok'
run GW-n-k: unconfirmed launch found …; adopting it 启动调用失败/超时(或 agentd 在启动中途退出),但终端上已有 exit.txt/launch.pid/pid.txt:接管该运行,不重复启动;开始评论注明"启动未确认、已接管"
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
远程终端每步都慢 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
已连续 N 轮 [progress] 达到 max_progress_rounds:看最近几轮是否在原地打转;拆分任务或补充说明后移回 Todo

回滚到 oregon(LXC 调度器出问题时)

  1. 停 LXC:agent-access.py … run --target agentd-01 -- bash -c 'crontab -r; flock /var/lib/plane-agentd/cron.lock true'(删 crontab,等正在跑的 tick 结束)。
  2. 状态拷回 oregon:LXC /var/lib/plane-agentd/agentd-state.json → oregon ~/.gwent-board/agentd/agentd-state.json(base64 经中继读回,比对 sha256)。在途运行照常被 oregon 接着 poll。
  3. oregon 恢复 cron:crontab ~/.gwent-board/crontab.backup-20261021-pad11(即 */2 * * * * /home/ubuntu/Documents/wjj/project/plane-agentd/cron/agentd-tick.sh)。cron/agentd-tick.sh 用 config/agentd.oregon.yaml(state_dir: ~/.gwent-board/agentd,日志 ~/.gwent-board/agentd/agentd.log、~/.gwent-board/agentd-cron.log),与 LXC 配置只差路径。
  4. 看 oregon 的 ~/.gwent-board/agentd/heartbeat.json 更新。再切回 LXC = 反过来做一遍(docs/deploy-lxc.md §5)。

上线记录(2026-10-22,PAD-12:直连 SSH + 单次调用派单)

  • 代码 5192e04:direct-ssh 传输(会话建立前失败才回落 Warpgate,主机公钥不符绝不回落)、主机公钥固定、Vault 密钥载入 ssh-agent、复用连接、win10 一次调用启动和一次调用收尾、remote.log、agentd ping。148 个单测通过;经正确性、安全两轮 opus 审查后修正。
  • 密钥:在 LXC 的 /dev/shm 生成 ed25519,私钥只存在 Vault AGENTD_SSH_KEY(指纹 SHA256:+6uooRFFB3tX4fQ+Sfl+3FmLm9KJpPYpSPzoTcCq/2M),临时文件已删。公钥行 restrict,from="100.69.78.124" ssh-ed25519 … plane-agentd@agentd-01:
    • oregon:~ubuntu/.ssh/authorized_keys,备份 .bak-pad12-20261022-025955;
    • win10:C:\ProgramData\ssh\administrators_authorized_keys,备份 .bak-pad12-20261022-110041,ACL 未变(SYSTEM 和 Administrators 完全控制)。win10 的 sshd 是 OpenSSH_9.5p1,DefaultShell 未设(即 cmd.exe)。
    • 两台的主机公钥经 ssh-keyscan 和经 Warpgate 读公钥文件两路核对一致后,固定写进配置。
  • 部署 03:01(持 cron.lock 执行 git pull 和 ping)。
    • ping:oregon 首次 3.9 s(含建立主连接),复用 0.77 s;win10 首次 0.52 s,复用 0.17 s。
    • 部署时 win10 的第一次调用有一次 60 s 超时。随后 3 轮新建主连接的首次调用都是 0.52 s,复现不出,推测是 tailnet 线路空闲后刚恢复。这类超时不回落,该终端本 tick 算不可用、下个 tick 重试;在 remote.log 里继续观察。
  • 冒烟(真实 webhook):
    • GW-27 win10:建项 03:06:17.9 → 开工评论 03:06:28.4,10.5 s(之前 95 s)。其中 busy 0.22 s、start 0.32 s;收尾 read、fetch、forget 三次调用共 0.9 s。
    • GW-28 oregon:建项 03:10:09.5 → 开工 03:10:23.6,14.1 s(start 4.1 s,含新建主连接);03:10:32.6 结果已贴回。
  • 剩下的延迟在收尾:运行结束后要等下一次 tick(cron 2 分钟,或别的投递触发)才收。迁 Temporal(PAD-8)时用定时器轮询解决。
  • 回滚:把该终端的 transport 改回 warpgate-ssh 再 pull。撤销访问:删掉两台 authorized_keys 里的那一行,再对 /run/plane-agentd/ssh/ 下的主连接执行 ssh -O exit(见 deploy-lxc §9)。

上线记录(2026-10-22,PAD-7:Plane 侧 webhook 启用)

  • 01:14 Plane VM 506:plane.env 与渲染 compose 设 WEBHOOK_ALLOWED_IPS=10.77.10.249/32(备份 *.bak-pad7-20261022-011453),--dry-run 确认后只重建 api、worker、webhook-consumer、outbox-poller、automation-consumer、beat-worker(01:15:43–01:16:33);01:18:26 实测 API 已返回 200(之前未测,停机上限约 2.5 分钟),api 容器 4 分钟内转 healthy。
  • 01:19 tools/plane-webhook.py create http://10.77.10.249:8790/plane → 201(白名单生效),密钥写入 Vault PLANE_WEBHOOK_SECRET(替换临时值);01:22:30 systemctl enable --now plane-agentd-webhook(LXC 代码 1b4d965,单元文件与仓库一致);01:22:57 patch c039c471-… is_active=true。
  • Plane 建出的是 v2 webhook:负载 {version: v2, event, entity_type, data, …},由 plane/webhook/bgtasks/webhook_task.py 的 send_webhook 发出,签名 HMAC-SHA256(secret, json.dumps(payload)),与接收端一致;data.project 路由到 GWENT 正常。
  • 实测(真实投递):评论 GW-25 → 01:24:34 投递到达 → 01:24:37 tick → 4.6 s 完成;GW-25 改 Done 同样 3 s 后 tick。建项 GW-26(01:25:14)→ 01:25:15 投递 → 01:25:18 tick 开始 → 01:26:49 [worker] 第 1 轮开始(win10 codex/astra):建项 → 开工 95 s,其中 tick 本身 91 s(中继、上传任务书、计划任务启动);只靠 cron 时在这之前还要平均多等约 60 s。剩下的成本都在中继调用上(PAD-12)。
  • cron */2 保留为兜底(同一把 cron.lock);oregon crontab 仍为空,只有一个调度器。

上线记录(2026-10-21,PAD-7:webhook 接收端)

  • 13:25 LXC 上 plane-agentd-webhook 启用(6dc7e72),cron 不变。Plane 侧 webhook 未建成:Plane 3.3.3 对 http://10.77.10.249:8790/plane 返回 400 "The webhook URL is not allowed. Provide a publicly reachable HTTP or HTTPS URL."(SSRF 防护拒绝内网地址,见 design §10);需要在 Plane 的 api/worker 环境加 WEBHOOK_ALLOWED_IPS=10.77.10.249/32,之后按 deploy-lxc §8 第 2–5 步建 webhook。在此之前 Vault 里的 PLANE_WEBHOOK_SECRET 是临时随机值(plane_wh_interim_…),plane-webhook.py create 会用 Plane 生成的密钥覆盖它。
  • 验收用的是在 LXC 上按 Plane 格式签名的合成投递(/root/lxc_send.sh good|bad|missing|other):错签名、缺签名 → 401,不触发;别的项目 → 202;正确签名 → 200,3 s 后 tick 开始。tick 运行中又来一条 → 结束后补跑一次(13:27 两条投递:224 s 的 tick——收尾 GW-15、在 win10 启动 GW-5——之后接着 24 s 的补跑);那段时间 cron 的 tick 撞锁立即退出,cron 本身照常每 2 分钟触发。
  • 冒烟 GW-17(oregon claude-code):建项 13:43:15.8 → 合成投递 13:43:41(经 oregon→LXC 中继发出,人为多了约 25 s)→ tick 开始 13:43:44 → [worker] 第 1 轮开始 13:44:56 → [review] ok 13:47:54,PASS 并移到 Done。投递 → tick 开始 = 3 s;tick 开始 → 开始评论 = 72 s(这一 tick 先 poll win10 上在途的 GW-5,再经中继启动 oregon;同期 cron 的启动 tick 也要 70–77 s)。真实 Plane 投递约 1 s 到达时,建项 → 开始评论预计约 75 s,而只靠 cron 是平均约 60 s 等待 + 同样的 tick 时长。≤10 s 的目标在"开始评论"这个口径上达不到:瓶颈是中继调用(每次约 10 s,vault + Warpgate SSH)和启动流程,不是触发方式。
  • GW-16(第一次冒烟):webhook 触发的 tick 里 LXC→中继报 access-hub: config_private_transport_failed(LXC 经 tailnet 取 nas…/identity/agent.json 15 s 超时;13:38 的 cron tick 也撞上一次),启动失败;下一 cron tick 探测无运行目录 → GW-16-1-r2 重新启动,PASS(防重复启动机制按设计工作)。
  • 重启:tick 运行中 systemctl restart plane-agentd-webhook,tick(在 systemd-run --scope 里)没被杀、照常结束(heartbeat 13:50:10,errors=[]);接收端约 19 s 后重新监听(取 vault 密钥 + 启动)。

切换记录(2026-10-21,PAD-11:调度器迁到 LXC agentd-01)

  • 代码:防重复启动(launching 标记 + 探测接管/换目录重启)与"中继不通的终端本 tick 跳过"随本次一起上线(4f45d36);LXC 用仓库里的 config/agentd.yaml,oregon 回滚用 config/agentd.oregon.yaml。
  • 切换前 LXC 手工 tick --once(oregon cron 仍在跑,Plane 上无 Todo 项):空闲、无错误;LXC 经 helper 探测 win10(codex 计数)与经中继探测 oregon 运行目录均正常。
  • 切换时 oregon 刚收尾 GW-11-1(11:53 由 oregon 派出、11:57 [review]),无在途运行。11:59 删除 oregon crontab 行(备份 ~/.gwent-board/crontab.backup-20261021-pad11,oregon crontab 现为空),状态文件(sha256 106c2bce…)复制到 LXC,12:00 装 LXC crontab,12:02:09 第一个 cron tick(heartbeat 更新,errors=[])。
  • 冒烟(oregon 上 smoke … --watch,由 LXC cron 派发):GW-12 oregon claude-code/sonnet 12:04:20 启动 → 12:06:47 [review] ok PASS;GW-13 win10 codex/gpt-6.1-sol/fast 12:05:58 启动 → 12:07:45 [review] ok PASS。续线程:GW-12 评论"回复 [review] ok 2"后移回 Todo,12:08:20 第 2 轮续 session 2348d52f…(两轮 out.json 的 session_id 相同),12:10:44 [review] ok 2。两项均移到 Done。

切换记录(2026-10-21,P1-5)

  • 旧 worker gwent-design/tools/board-worker-plane.py(cron board-worker-plane-cron.sh)已从 crontab 移除;脚本与 ~/.gwent-board/plane-state.json、plane-worker.log 保留作历史,不要再启用(两者会抢同一批 window10 的 Todo 项)。
  • 切换时旧 worker 无在途运行(GW-6 的 board-6-2 已于 06:30 以 [blocked] 结束,项在 Waiting)。GW-6 的 codex 线程 01a15ce3… 已迁入 agentd 状态(runs=2、thread_on=[win10, codex]、旧评论标为已见),移回 Todo 并指派 window10 时 agentd 续这个线程,run id 从 GW-6-3 起。
  • 运行目录变了:旧 worker 用 win10 runtime\board\board-<n>-<k>,agentd 用 runtime\agentd\GW-<n>-<k>。
  • 冒烟:oregon(GW-7,claude-code/sonnet,约 30 s)与 win10(GW-8,codex/gpt-6.1-sol/fast,约 3.5 min)均 PASS。win10 上验证过:codex exec resume 也会输出 thread.started(与原线程同 ID);续线程时 -c sandbox_mode="workspace-write" 生效,能在仓库内写文件;把运行中的项移出 In Progress 后,taskkill /T /F /PID <pid.txt> 结束了整棵树(runner powershell → codex.exe → code-mode-host / command-runner → powershell),Get-Process codex 归零,计划任务被删除。
  • 差异(P1-5 时):旧 worker 对没有 model 标签的项用 sol + service_tier="fast";agentd 当时只在有 model:sol 标签时加 fast。P1-6 起 codex 执行器有 tier_defaults: {gpt-6.1-sol: fast},无标签项也是 sol + fast;tier:<x> 标签改档位,tier:none 去掉档位参数。
  • win10 上 codex 的 stderr 常见 websocket 连接被拒(os error 10061,wss://chatgpt.com/...)后回退成功;只有运行失败时它才出现在失败说明的尾部,不影响分类(不匹配临时性模式)。

切换记录(2026-10-21,P1-6:oregon 改为远程启动)

  • config/agentd.yaml 的 oregon 从 transport: local 改为 warpgate-ssh(target: oregon, os: linux, launch: nohup, run_root: /home/ubuntu/.gwent-board/agentd-runs)。agentd 仍在 oregon 上跑,但启动执行器走 Warpgate 回到本机,与将来 LXC 上的路径完全相同。切换时 oregon 无在途运行;GW-7/GW-10 的 claude 线程(thread_on=[oregon, claude-code])不受影响——会话存在 oregon 的 ~/.claude,与 transport 无关。
  • win10 不变(helper 脚本 + schtasks),只是中继子进程改由共用的 call() 启动;切换后 GW-9 的 poll/collect 已经走新代码并正常结束。
  • 冒烟(经 Warpgate 远程启动):GW-10 第 1 轮 claude-code/sonnet PASS([review] ok,启动 08:08:54 → 结束 08:10:17,session fa3be0f7…);评论"请回复 [review] ok 2"并移回 Todo 后第 2 轮续同一 session(out.json 的 session_id 不变),回 [review] ok 2 转 Review。启动只用 1 次中继调用(约 12 s);72 KB 的 prompt 走分块上传(约 75 s)。
  • kill:直接经中继对 oregon 起 sleep 600 & sleep 500 的运行再 kill,两个 sleep 都被结束(进程组 + 会话),pid.txt 与 launch.pid 相同(setsid 没有再 fork)。经 Plane 的取消冒烟(运行中移到 Waiting)因 Plane 当时 502(08:14 起)未做,待补。

回滚记录(2026-10-21:调度器回到 oregon)

  • 原因:LXC agentd-01 经 tailnet 到 NAS 的路径走 DERP 中继,取 vault 身份端点(nas…/identity/agent.json)TCP 连接 30/30 超时,tick 里的中继调用不稳定。网络修好后再切回 LXC(docs/deploy-lxc.md §5)。
  • 做法: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(排查时被打印进了一次会话记录)。