Docs: LXC deployment as done (agentd-01), rollback to oregon, double-start guard, PAD-11 cutover record
This commit is contained in:
+59
-54
@@ -1,78 +1,83 @@
|
||||
# 在精简 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`)。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` 也都在。从零装:
|
||||
```bash
|
||||
apt-get update
|
||||
apt-get install -y python3 python3-yaml openssh-client git cron ca-certificates curl
|
||||
# win10 的 helper 脚本另需(Debian 12 默认已有):bash coreutils(base64/sha256sum) gzip libc-bin(iconv)
|
||||
adduser --disabled-password --gecos '' agentd # 普通用户运行 agentd,不用 root
|
||||
apt-get update && apt-get install -y python3 python3-yaml openssh-client git cron ca-certificates
|
||||
```
|
||||
Python ≥ 3.10(Debian 12 是 3.11)。`jsonschema` 可选,没有就用内置子集校验器。
|
||||
`jsonschema` 可选,没有就用内置子集校验器。
|
||||
|
||||
## 2. 网络
|
||||
| 去向 | 用途 |
|
||||
|---|---|
|
||||
| `nas.tail9531b.ts.net:8443`(tailnet) | Agent Vault 身份配置 `identity/agent.json`(含 vault token 与 Warpgate 主机公钥)→ LXC 必须加入 tailnet(`tailscale up`,LXC 需 `/dev/net/tun`) |
|
||||
| `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` | 所有终端的中继 |
|
||||
| Warpgate SSH `115.28.210.101:2222` | 所有终端的中继(LXC → win10 / oregon) |
|
||||
| `https://plane.kappiee.com` | Plane API |
|
||||
| `https://git.kappiee.com` | `git pull`(公开仓库,无需凭据) |
|
||||
|
||||
## 3. Agent Vault 客户端与身份
|
||||
## 3. Agent Vault 客户端
|
||||
只复制程序文件(客户端没有落盘的 token 缓存;oregon 目录里的 `agent-service-check.py` 是运维工具,不需要):
|
||||
```bash
|
||||
sudo -u agentd mkdir -p ~agentd/.local/share/server-access-client
|
||||
# 从 oregon 复制(权限 700,只给 agentd 用户):
|
||||
scp oregon:~/.local/share/server-access-client/{agent-access.py,agent-access-askpass.py} \
|
||||
agentd-01:~agentd/.local/share/server-access-client/
|
||||
chmod 700 ~agentd/.local/share/server-access-client/*
|
||||
cd ~agentd/.local/share/server-access-client && sudo -u agentd python3 -I agent-access.py \
|
||||
--config https://nas.tail9531b.ts.net:8443/identity/agent.json list # authenticated: true,能看到 win10 / oregon 等 target
|
||||
# 在 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`),不落盘;也可以换成本地私有 JSON 文件(`chmod 600`),把 `vault.config` 指过去。
|
||||
- **Warpgate known_hosts 不用配**:客户端每次用身份配置里的 `warpgate.host_key` 生成临时 known_hosts(`StrictHostKeyChecking=yes`),不读 `~/.ssh/known_hosts`。只要求有 `ssh` 可执行文件。
|
||||
- `askpass` 必须与 `agent-access.py` 同目录且可执行。
|
||||
- Warpgate 侧:该身份(`agent` 用户)要有每个终端 target 的权限(现有:`win10`、`oregon`)。
|
||||
- 身份是 tailnet 上的 URL(`--config https://…/identity/agent.json`),不落盘。
|
||||
- **Warpgate known_hosts 不用配**:客户端每次用身份配置里的 `warpgate.host_key` 生成临时 known_hosts。LXC → oregon 一次中继约 9 s。
|
||||
|
||||
## 4. 代码与配置
|
||||
```bash
|
||||
sudo -u agentd git clone https://git.kappiee.com/kappiee/plane-agentd.git ~agentd/plane-agentd
|
||||
# win10 仍用 helper 脚本(见 design §9 TODO),复制这两个:
|
||||
sudo -u agentd mkdir -p ~agentd/helpers
|
||||
scp oregon:/home/ubuntu/Documents/wjj/project/gwent-design/tools/{win10-ps.sh,win10-put.sh} agentd-01:~agentd/helpers/
|
||||
# 看板协作协议(拼进每轮 prompt,由 agentd 在本机读取):
|
||||
scp oregon:/home/ubuntu/Documents/wjj/project/gwent/design/board-protocol.md agentd-01:~agentd/board-protocol.md
|
||||
cp ~agentd/plane-agentd/config/agentd.yaml ~agentd/plane-agentd/config/agentd.local.yaml # git-ignored,优先于 agentd.yaml
|
||||
```
|
||||
`config/agentd.local.yaml` 里改:
|
||||
- `vault.client: ~/.local/share/server-access-client/agent-access.py`(`~` 在本机展开)
|
||||
- `agentd.state_dir: ~/state`(JSON 状态、日志、心跳);`agentd.helpers_dir: ~/helpers`
|
||||
- `projects.GWENT.protocol_file: ~/board-protocol.md`(**它不在终端上读,是 LXC 本机路径**;协议更新时重新复制)
|
||||
- 所有终端都是 `transport: warpgate-ssh`:
|
||||
- win10:`target: win10, os: windows, launch: schtasks`(不变)
|
||||
- oregon:`target: oregon, os: linux, launch: nohup, repo: /home/ubuntu/Documents/wjj/project/gwent, run_root: /home/ubuntu/.gwent-board/agentd-runs, bins: {claude: /home/ubuntu/.local/bin/claude}`
|
||||
- 远程 POSIX 终端的 `repo`、`run_root` 必须是绝对路径(在远程展开,`~` 不行);`bins` 写远程的绝对路径(SSH 非登录 shell 只有系统 PATH)。
|
||||
```bash
|
||||
cd ~agentd/plane-agentd && python3 -m unittest && python3 cli/agentd check-config
|
||||
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. cron
|
||||
`cron/agentd-tick.sh` 里写死了 oregon 的路径;在 LXC 上用同样的形状(单行 crontab 或复制脚本改路径):
|
||||
```
|
||||
*/2 * * * * flock -n $HOME/state/cron.lock timeout 900 python3 -I $HOME/.local/share/server-access-client/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 850 -- python3 $HOME/plane-agentd/cli/agentd tick --once >> $HOME/state/agentd-cron.log 2>&1
|
||||
```
|
||||
- token 只经 `exec --secret-env` 进 agentd 进程内存;agentd 起的中继子进程环境里去掉了所有 `token_key`(`Config.scrubbed_env()`),远程执行器在终端上由 sshd 起,本来就拿不到 LXC 的环境。
|
||||
## 5. 冒烟与切换(实际顺序)
|
||||
1. LXC 上手工跑一次(oregon cron 仍在跑,LXC 未排程;先确认 Plane 上没有 Todo 项可派,否则两个调度器会同时派单):
|
||||
```bash
|
||||
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 上的 cron**(`crontab -e` 注释掉 `agentd-tick.sh`),再在 LXC 上启用;两个调度器同时跑会重复派单。状态文件 `~/.gwent-board/agentd/agentd-state.json` 从 oregon 复制到 LXC 的 `state_dir`,在途运行(handle 里是终端上的绝对路径)照常被新调度器 poll/collect。
|
||||
- **任何时候只能有一个调度器排程**(oregon 的 crontab 里不能有 `agentd-tick.sh`)。
|
||||
|
||||
## 6. 日志与心跳
|
||||
| 路径(LXC) | 内容 |
|
||||
## 6. 日志与心跳(LXC)
|
||||
| 路径 | 内容 |
|
||||
|---|---|
|
||||
| `~/state/agentd.log` | 每 tick 的动作、错误 |
|
||||
| `~/state/heartbeat.json` | `at` 超过 10 分钟不更新 = 调度器没在跑 |
|
||||
| `~/state/agentd-state.json` | 全部工作项状态 |
|
||||
| `~/state/agentd-cron.log` | cron 输出(空闲时为空;`skip: another tick holds …` 无害) |
|
||||
| 终端上 `<run_root>/<run_id>/` | 每轮的 `prompt.md run.sh/run.ps1 pid.txt launch.pid exit.txt stderr.txt` + 执行器输出 |
|
||||
| `/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'` 能通。
|
||||
@@ -93,5 +98,5 @@ cd ~agentd/plane-agentd && python3 -m unittest && python3 cli/agentd check-confi
|
||||
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 加 `--secret-env PLANE_MAC_API_TOKEN=PLANE_MAC_API_TOKEN`。
|
||||
5. `cli/agentd check-config`,然后 `cli/agentd smoke GWENT mac`。
|
||||
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`。
|
||||
|
||||
Reference in New Issue
Block a user