Files
plane-agentd/docs/deploy-lxc.md
T

14 KiB
Raw Blame History

在精简 LXC 上部署 agentd(design §9 阶段 3)

调度器跑在一台不装任何 agent CLI 的 Debian 12 LXC(agentd-01)上;所有终端(win10、oregon、mac…)都经 Warpgate SSH 远程启动(transport: warpgate-ssh)。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 '…'。

1. 系统包

LXC 交付时已有 python3(3.11)、python3-yaml、openssh-client、git、cron(active);win10 helper 需要的 iconv gzip base64 sha256sum 也都在。从零装:

apt-get update && apt-get install -y python3 python3-yaml openssh-client git cron ca-certificates

jsonschema 可选,没有就用内置子集校验器。

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)
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 是运维工具,不需要):

# 在 oregon 上:两个文件 base64 后经中继写到 LXC(一次 run 调用,sha256 与 oregon 一致)
/root/.local/share/server-access-client/agent-access.py          # 700 root
/root/.local/share/server-access-client/agent-access-askpass.py  # 700 root,必须与 agent-access.py 同目录且可执行
# 在 LXC 上验证:
cd /root/.local/share/server-access-client
python3 -I agent-access.py --config https://nas.tail9531b.ts.net:8443/identity/agent.json status   # "authenticated": true
python3 -I agent-access.py --config https://nas.tail9531b.ts.net:8443/identity/agent.json list     # 含 win10、oregon、agentd-01
  • 身份是 tailnet 上的 URL(--config https://…/identity/agent.json),不落盘。
  • Warpgate known_hosts 不用配:客户端每次用身份配置里的 warpgate.host_key 生成临时 known_hosts。LXC → oregon 一次中继约 9 s。

4. 代码与配置

cd /opt/plane-agentd && git pull          # 仓库已 clone 在 /opt/plane-agentd(https://git.kappiee.com/kappiee/plane-agentd.git)
mkdir -p /var/lib/plane-agentd /var/log/plane-agentd && chmod 700 /var/lib/plane-agentd
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 终端。
  • 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)。
  • 相对路径(state_dir/log_dir/helpers_dir/protocol_file)都按仓库根解析;远程终端的 repo/run_root/bins 必须是远程绝对路径。

5. 冒烟与切换(实际顺序)

  1. LXC 上手工跑一次(oregon cron 仍在跑,LXC 未排程;先确认 Plane 上没有 Todo 项可派,否则两个调度器会同时派单):
    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 \
      --timeout 250 -- python3 /opt/plane-agentd/cli/agentd tick --once      # 空闲:无输出,heartbeat errors=[]
    
  2. oregon:备份 crontab(~/.gwent-board/crontab.backup-20261021-pad11),删掉 agentd-tick.sh 那一行;flock ~/.gwent-board/agentd.lock true 等正在跑的 tick 结束。
  3. 状态:~/.gwent-board/agentd/agentd-state.json(oregon)→ /var/lib/plane-agentd/agentd-state.json(LXC,600;base64 经中继写入,比对 sha256)。在途运行的 handle 是终端上的绝对路径,新调度器照常 poll/collect。
  4. LXC 上 root 的 crontab(只有这一行):
    */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。 注意:经中继装 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)。
  • token 只经 exec --secret-env 进 agentd 进程内存;agentd 起的中继子进程环境里去掉了所有 token_key(Config.scrubbed_env())。
  • --secret-env 的完整列表:cli/agentd check-config 最后一行。
  • 任何时候只能有一个调度器排程(oregon 的 crontab 里不能有 agentd-tick.sh)。

6. 日志与心跳(LXC)

路径 内容
/var/log/plane-agentd/agentd.log 每 tick 的动作、错误
/var/log/plane-agentd/cron.log cron 输出(空闲时为空;skip: another tick holds … 无害)
/var/lib/plane-agentd/heartbeat.json at 超过 10 分钟不更新 = 调度器没在跑;down = 本 tick 中继不通而跳过的终端;launching = 启动未确认的运行
/var/lib/plane-agentd/agentd-state.json 全部工作项状态
/var/lib/plane-agentd/agentd-state.json.lxc-smoke 切换前手工冒烟 tick 产生的空状态(可删)
终端上 <run_root>/<run_id>/ 每轮的 prompt.md run.sh/run.ps1 pid.txt launch.pid exit.txt stderr.txt + 执行器输出;重新启动的运行在 <run_id>-r2/

7. 加一个终端(例:mac)

  1. Warpgate 加 SSH target(如 mac),给 agent 身份授权;agent-access.py … run --target mac --timeout 30 -- bash -c 'echo ok; uname' 能通。
  2. 在终端上装好 agent CLI 并登录(Claude Code / Codex),clone 工作仓库;记下二进制绝对路径。
  3. 配置:
    mac:
      transport: warpgate-ssh
      target: mac
      os: darwin            # linux | darwin;darwin 用 perl POSIX::setsid 脱离会话(无 setsid 命令)
      launch: nohup
      repo: /Users/me/src/gwent
      run_root: /Users/me/.agentd-runs
      bins: {claude: /Users/me/.local/bin/claude}
      plane_user: mac
      token_key: PLANE_MAC_API_TOKEN
      concurrency: 1
      executors: [claude-code]
    
    远程 shell 是目标账户的登录 shell(bash/zsh 均可,agentd 发的是 bash -c '<script>');终端需有 bash base64 nohup pgrep pkill(Linux 另需 setsid,macOS 需 perl)。
  4. Plane:建用户 mac,token 存 Vault 键 PLANE_MAC_API_TOKEN;项目 routing.by_assignee 加 mac: mac,default_executor 加 mac: claude-code;cron/agentd-tick-lxc.sh 加 --secret-env PLANE_MAC_API_TOKEN=PLANE_MAC_API_TOKEN。
  5. 提交、推送、LXC git pull;cli/agentd check-config,然后 cli/agentd smoke GWENT mac --watch。

8. Webhook 接收端(PAD-7)

  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 webhook-consumer outbox-poller automation-consumer beat-worker(先 --dry-run,确认只重建这 6 个;商业版的 v2 投递走 webhook-consumer/worker,创建时校验在 api;不要用 prime-cli,它要 TTY。compose 里 WEBHOOK_ALLOWED_HOSTS: ${WEB_URL} 不带 env 文件时为空,prime-cli 起的容器也是空值,属现状)。状态(2026-10-22):已应用(备份 *.bak-pad7-20261022-011453,6 个容器 WEBHOOK_ALLOWED_IPS=10.77.10.249/32),webhook c039c471-e78a-49c9-b009-5cf77a389ad6 已建并启用(v2;签名与接收端一致,见 design §10),接收端 active/enabled。纯 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):
    cd /opt/plane-agentd && flock /var/lib/plane-agentd/cron.lock git pull
    cp systemd/plane-agentd-webhook.service /etc/systemd/system/ && systemctl daemon-reload
    systemctl enable --now plane-agentd-webhook
    curl -s http://10.77.10.249:8790/healthz       # {"ok": true, ...}
    tail /var/log/plane-agentd/webhook.log           # listening on http://10.77.10.249:8790/plane (tenants: GWENT)
    
    单元文件复用 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 恢复。