mini-agent 多租户后 pi RPC 起不来的排障复盘
mini-agent 是我的个人 AI 助手守护进程。Cloud 版(ECS 云主机)走上多租户后,/task、/query、/chat 这些命令统一走 pi RPC 子进程,子进程外面套着两层沙箱:env 白名单(防止 CLAUDE_API_KEY、TELEGRAM_BOT_TOKEN 等凭据泄漏进去)+ bubblewrap 文件系统隔离。多租户更新部署完后,用户反馈云端服务”pi RPC 又起不来了”。这次排查最后发现不止一个根因,而且中间踩了不少”测试假象”的坑。本文按时间线保留完整的技术探索过程。
现象:一次静默失败
先看服务状态:
systemctl --user status mini-agent # active,进程活着服务在跑,但 /task 返回 HTTP 444,且是空响应体。这个 444 是 silent_errors 模式的产物——channels/web.py 里 _unauthorized 和 _bad_request 在静默模式下都返回 444 而不是 JSON 错误体,让 nginx 能无痕关闭连接。所以”444”本身并不能告诉我们失败在哪一层,这是第一层迷雾。
更关键的是:web 接口的请求体字段是 text,不是 message。 我一开始用 {"message": "..."} 去测,返回的一直是 444(_bad_request),我误以为是鉴权失败,绕了一大圈。换成 {"text": "..."} 之后,才终于看到真正的 pi 错误:
{"ok": false, "error": "Pi RPC process exited unexpectedly"}到这里才确认:鉴权是过的,走到 pi 执行了,但 pi 子进程一启动就崩。
第一层根因:config 被 git pull 覆盖
直接看 ECS 的 config.yaml:
pi_path: ${PI_PATH:-/home/worker/.local/bin/pi} # ← 默认值是本机路径ECS 用户是 deploy-pro,/home/worker 这个目录根本不存在。而 PiRpcClient 用 create_subprocess_exec(self._pi_path, ...) 直接 exec 配置里的路径,没有 PATH 兜底,所以 spawn 就失败。
为什么会被覆盖?ECS 的 config.yaml 是 skip-worktree 的(云端专属配置,不让 git pull 覆盖)。但多租户部署那天 skip-worktree 一度丢失,在丢失的窗口里 git pull 把 tracked 的本机 config 覆盖到了云端,pi_path 被重置回本机默认值。之后虽然重新设了 skip-worktree,但覆盖已经发生。
修复:在 ECS 的 .env 里加一行。.env 是 gitignored,git pull 永远碰不到它,正好堵住这个复发根因:
PI_PATH=/home/deploy-pro/.local/share/pi-node/node-v22.23.1-linux-x64/bin/pi这里有个值得注意的细节:真正的 pi 二进制其实是个符号链接
bin/pi -> ../lib/node_modules/@earendil-works/pi-coding-agent/dist/cli.js它的 shebang 是 #!/usr/bin/env node。这意味着 pi 到底跑在哪个 node 上,取决于 PATH 里 node 解析到谁——这为后面埋了一个坑。
第二层根因:相对路径破坏 bwrap 的整个文件系统
修好 pi_path、重启服务后,/task 依然报 “Pi RPC process exited unexpectedly”。我直接复现了 tenant 模式(多租户下每个用户有自己的 workspace)的调用:
# code_task.py:_workspace_forbase = Path("data/workspaces") / str(user_id) # ← 相对路径!_workspace_for 返回的是相对路径 data/workspaces/1。这个相对路径被同时用作两处:
- bwrap 的
--bind data/workspaces/1 data/workspaces/1的目标(dest) - pi 子进程的
cwd
我为了定位,直接在 shell 里跑 bwrap 探针,结果发现一个惊人的现象:连 /bin/true 都 exec 不了,报 bwrap: execvp /bin/true: No such file or directory。而 _probe_bwrap 里用最小绑定集却是能跑通的。
问题出在 /bin/true 是动态链接的,interpreter 是 /lib64/ld-linux-x86-64.so.2:
file /bin/true # ELF ... dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2ldd /bin/true # /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2bwrap 对相对的 bind 目标会误解析,把它当成名字空间根目录上的一层,等于把 /lib64/ld-linux-x86-64.so.2 这个动态链接器挡在了名字空间之外。于是任何动态链接的二进制都起不来——不只是 pi,连最小的 /bin/true 都不能 exec。这就是为什么错误信息跟 pi 无关、却总在 pi 这一步暴露。
我用二分法定位到罪魁:
# 只加 cwd 绑定,/bin/true 就崩;加 pi 的各个子目录 ro-bind 都没事+pi-bin: rc=0 # 正常+dist: rc=0 # 正常+5-subdirs: rc=0 # 正常+cwd: rc=1 # bwrap: execvp /bin/true: No such file or directory修复是一行:把 workspace 先 resolve 成绝对路径,再 mkdir、返回。这跟 agent.py:359 对共享 pi_workspace 的处理方式保持一致:
base = (Path("data/workspaces") / str(user_id)).resolve()base.mkdir(parents=True, exist_ok=True)return basecommit 9ed51f2。
三个差点误导的”测试假象”
这次排查最花时间的地方,是我自己造出来的假象,值得单独记下来:
-
cwd="."会复现相对目标 bug。 我最初直接测PiRpcClient时传了cwd=".",结果_build_bwrap_argv生成--bind . .——相对目标,同样砸碎名字空间。所以”直接测也崩”一度让我以为 bug 在沙箱包装上,其实是我传参传错了。所有直接测试必须传绝对 cwd。 -
ssh shell 的 PATH 和 systemd 服务的 PATH 不一样。 ssh 登录 shell 的 PATH 把系统
/usr/bin/node(v20)排前面;而 systemd 服务的 PATH 把pi-node的 v22 bin 排前面。pi 的cli.jsshebang 是#!/usr/bin/env node,所以在 ssh 里直接跑 pi 会撞上 undici 的版本不兼容:
TypeError: webidl.util.markAsUncloneable is not a function at new CacheStorage (.../undici/lib/web/cache/cachestorage.js:20)而 v22 下跑得好好的(pi --version → 0.83.0)。所以”真机上 pi 是坏的”也曾经是假象——服务里用的是 v22。
- 启动日志里的
ro_paths不列 pi 路径,是正常的。agent.py打印的Pi sandbox config: ro_paths=[...]只包含 config 里的额外项 + HOME 自动推导的路径(.ssh、.gitconfig、blog 仓库)。真正给 pi 用的 ro-bind(bin/、node_modules/等)是在_build_bwrap_argv里运行时单独算的,不打印。所以”日志里看不到 pi 路径”容易被误读成”pi 没挂载”,其实挂载是好的。
验证
修复后,ECS 上端到端跑通:
POST /task → HTTP 200 {"ok": true, "response": "PI_OK"}result-2026-08-03.log → [task] u1 reply with exactly: PI_OK本地 uv run pytest 全量 408 通过,本地服务也重启加载了新代码。
复盘
这次两个根因都来自”多租户更新”:
- config 层面的:
git pull对skip-worktree的防护是”靠 flag 存在”,flag 一旦丢失就裸奔。把云端特有变量(PI_PATH)挪到 gitignored 的.env,从根上免疫 git 同步。 - 代码层面的:bwrap 的绑定目标必须是绝对路径,这是它的硬约束。相对路径的
--bind不是”报错”而是”静默砸掉整个名字空间”,所以最隐蔽——它不报 pi 相关的错,却让所有动态链接二进制都起不来。
调试时最贵的不是 bug 本身,而是我自己造成的假象。凡是”直接测也崩、服务里却可能正常”的结论,先检查测试参数(cwd 绝对/相对、PATH 顺序、请求字段)是否和生产环境一致,再下结论。
相关文章:《mini-agent ECS 沙箱排障复盘》(resolv.conf DNS 那一次)、《mini-agent 多租户隔离工程复盘》。