# 在精简 LXC 上部署 agentd(design §9 阶段 3) 目标:调度器跑在一台**不装任何 agent CLI** 的 Debian 12 LXC(`agentd-01`)上;所有终端(win10、oregon、mac…)都经 Warpgate SSH 远程启动(`transport: warpgate-ssh`)。LXC 只做调度、回帖、通知。 ## 1. 系统包 ```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 ``` Python ≥ 3.10(Debian 12 是 3.11)。`jsonschema` 可选,没有就用内置子集校验器。 ## 2. 网络 | 去向 | 用途 | |---|---| | `nas.tail9531b.ts.net:8443`(tailnet) | Agent Vault 身份配置 `identity/agent.json`(含 vault token 与 Warpgate 主机公钥)→ LXC 必须加入 tailnet(`tailscale up`,LXC 需 `/dev/net/tun`) | | Vault(身份配置里的地址) | 取 Plane token、Warpgate 密码 | | Warpgate SSH `115.28.210.101:2222` | 所有终端的中继 | | `https://plane.kappiee.com` | Plane API | ## 3. Agent Vault 客户端与身份 ```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 ``` - 身份是 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`)。 ## 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 ``` ## 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 的环境。 - `--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。 ## 6. 日志与心跳 | 路径(LXC) | 内容 | |---|---| | `~/state/agentd.log` | 每 tick 的动作、错误 | | `~/state/heartbeat.json` | `at` 超过 10 分钟不更新 = 调度器没在跑 | | `~/state/agentd-state.json` | 全部工作项状态 | | `~/state/agentd-cron.log` | cron 输出(空闲时为空;`skip: another tick holds …` 无害) | | 终端上 `//` | 每轮的 `prompt.md run.sh/run.ps1 pid.txt launch.pid exit.txt stderr.txt` + 执行器输出 | ## 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. 配置: ```yaml 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 '