1635 words
8 minutes
mini-agent 多租户后 pi RPC 起不来的排障复盘

mini-agent 多租户后 pi RPC 起不来的排障复盘#

mini-agent 是我的个人 AI 助手守护进程。Cloud 版(ECS 云主机)走上多租户后,/task/query/chat 这些命令统一走 pi RPC 子进程,子进程外面套着两层沙箱:env 白名单(防止 CLAUDE_API_KEYTELEGRAM_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 这个目录根本不存在。而 PiRpcClientcreate_subprocess_exec(self._pi_path, ...) 直接 exec 配置里的路径,没有 PATH 兜底,所以 spawn 就失败。

为什么会被覆盖?ECS 的 config.yamlskip-worktree 的(云端专属配置,不让 git pull 覆盖)。但多租户部署那天 skip-worktree 一度丢失,在丢失的窗口里 git pull 把 tracked 的本机 config 覆盖到了云端,pi_path 被重置回本机默认值。之后虽然重新设了 skip-worktree,但覆盖已经发生。

修复:在 ECS 的 .env 里加一行。.env 是 gitignored,git pull 永远碰不到它,正好堵住这个复发根因:

Terminal window
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_for
base = Path("data/workspaces") / str(user_id) # ← 相对路径!

_workspace_for 返回的是相对路径 data/workspaces/1。这个相对路径被同时用作两处:

  1. bwrap 的 --bind data/workspaces/1 data/workspaces/1目标(dest)
  2. 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.2
ldd /bin/true # /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2

bwrap 对相对的 bind 目标会误解析,把它当成名字空间根目录上的一层,等于把 /lib64/ld-linux-x86-64.so.2 这个动态链接器挡在了名字空间之外。于是任何动态链接的二进制都起不来——不只是 pi,连最小的 /bin/true 都不能 exec。这就是为什么错误信息跟 pi 无关、却总在 pi 这一步暴露。

我用二分法定位到罪魁:

Terminal window
# 只加 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 base

commit 9ed51f2

三个差点误导的”测试假象”#

这次排查最花时间的地方,是我自己造出来的假象,值得单独记下来:

  1. cwd="." 会复现相对目标 bug。 我最初直接测 PiRpcClient 时传了 cwd=".",结果 _build_bwrap_argv 生成 --bind . .——相对目标,同样砸碎名字空间。所以”直接测也崩”一度让我以为 bug 在沙箱包装上,其实是我传参传错了。所有直接测试必须传绝对 cwd。

  2. ssh shell 的 PATH 和 systemd 服务的 PATH 不一样。 ssh 登录 shell 的 PATH 把系统 /usr/bin/node(v20)排前面;而 systemd 服务的 PATH 把 pi-node 的 v22 bin 排前面。pi 的 cli.js shebang 是 #!/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 --version0.83.0)。所以”真机上 pi 是坏的”也曾经是假象——服务里用的是 v22。

  1. 启动日志里的 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 pullskip-worktree 的防护是”靠 flag 存在”,flag 一旦丢失就裸奔。把云端特有变量(PI_PATH)挪到 gitignored 的 .env,从根上免疫 git 同步。
  • 代码层面的:bwrap 的绑定目标必须是绝对路径,这是它的硬约束。相对路径的 --bind 不是”报错”而是”静默砸掉整个名字空间”,所以最隐蔽——它不报 pi 相关的错,却让所有动态链接二进制都起不来。

调试时最贵的不是 bug 本身,而是我自己造成的假象。凡是”直接测也崩、服务里却可能正常”的结论,先检查测试参数(cwd 绝对/相对、PATH 顺序、请求字段)是否和生产环境一致,再下结论。

相关文章:《mini-agent ECS 沙箱排障复盘》(resolv.conf DNS 那一次)、《mini-agent 多租户隔离工程复盘》

mini-agent 多租户后 pi RPC 起不来的排障复盘
https://sgjki547.top/posts/2026-08-03-mini-agent-多租户-pi-rpc-排障复盘/
Author
SGJki
Published at
2026-08-03
License
CC BY-NC-SA 4.0