安全使用 Pi Coding Agent 的两种隔离方案

最近几个月我一直在用 Pi Coding Agent 写代码。它很极简,速度快,token 消耗也比其他 agent 低不少。代价是安全上基本没有护栏。

我搜过一圈,能直接拿来用的方案不多,于是自己折腾了一段时间,最后定下两条路:Docker 容器Linux 独立用户降权。下面是原理、配置,以及我实际用下来的一些经验。

一、风险点

Pi 官方文档里有一节专门讲 No Built-in Sandbox,原文大意是:

Pi 不包含内置沙箱。内置工具可以读文件、写文件、改文件、执行 shell 命令,使用的就是 pi 进程的权限。扩展是以相同权限运行的 TypeScript 模块。

官方还解释了为什么不做进程内沙箱:半成品的沙箱容易被当成真正的安全边界,但它照样要用宿主的 shell、文件系统和凭据,所以隔离得靠操作系统,或者容器/虚拟化这一层

顺带澄清一个容易搞混的地方:Pi 的 project trust(启动时问你要不要信任当前项目)不是沙箱,它只管防止仓库在你批准之前偷偷加载扩展或改配置。

真正要担心的是这几类:

  • 越权读取~/.ssh~/.aws、项目里的 .env、数据库密码,都可能在它一次 cat 之后进了模型上下文。
  • 越权写入和误删:改坏系统配置、删掉别的项目、覆盖家目录里的其他文件。模型经常把路径算错,容易删错东西。
  • 提示注入与供应链:第三方扩展、MCP server、skills 和 Pi 同权限运行;仓库里的 README、代码注释、构建输出都可以藏指令,官方也说了防不住。

指望”我只让 Agent 干安全的事”是不行的。只要它读到的内容不完全可控,就该默认它哪天会执行一条你没写过的命令。

二、方案一:Docker 容器隔离

1. 思路

让 Pi 整个跑在容器里,只把当前项目目录挂进去,再单独给它一份配置目录。宿主上的 ~/.ssh~/.aws 不挂载,容器里压根没有这些路径,也就谈不上读到或者修改。

2. 镜像与启动

镜像不复杂,装好 Node 和 Pi 就行。唯一的坑是别用 root 跑:建一个和宿主 UID 对齐的普通用户,否则写回宿主目录的文件全是 root 属主,后面收拾起来很烦。

FROM ubuntu:24.04

# Node 22 + 基础工具
RUN apt-get update && apt-get install -y --no-install-recommends \
      ca-certificates curl git ripgrep build-essential \
 && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
 && apt-get install -y --no-install-recommends nodejs \
 && rm -rf /var/lib/apt/lists/*

# 安装 pi
ARG PI_VERSION=0.85.1
RUN npm install -g @earendil-works/pi-coding-agent@${PI_VERSION}

# 非 root 用户,UID 与宿主对齐,避免挂载目录出现 root 属主文件
ARG HOST_UID=1000
RUN useradd -m -u ${HOST_UID} -s /bin/bash dev
USER dev
WORKDIR /home/dev/workspace
ENTRYPOINT ["pi"]

构建完之后,配置和凭据也单独放一份,不要挂宿主的 ~/.pi

docker build --build-arg HOST_UID=$(id -u) -t pi-sandbox .

# 专用的 Pi 配置/凭据目录,不要直接用宿主的 ~/.pi
mkdir -p ~/pi-data/pi

cd ~/code/my-project
docker run --rm -it \
  -v "$PWD:/home/dev/workspace" -w /home/dev/workspace \
  -v "$HOME/pi-data/pi:/home/dev/.pi" \
  pi-sandbox

--rm 表示容器退出即删。容器里临时装的东西会丢,项目代码和 ~/pi-data 是持久保存的。

3. 几个坑

  1. 别挂 docker.sock 很多人为了让容器里也能跑 docker,把 /var/run/docker.sock 挂进去。这等于把宿主的 root 权限交出去,隔离白做。
  2. 别直接挂宿主的 ~/.pi 那里有你所有的会话、设置和凭据。需要哪个凭据,就往 ~/pi-data/pi 里放哪个。当然了如果你使用docker,可以直接删除宿主机的Pi。
  3. git 同理。要让 Agent push 代码,就单独生成一把只对单个仓库有效的 deploy key,别把能开所有仓库的私钥塞进去,出事后的损失不是一个量级。
  4. 构建时记得用 .dockerignore 把数据目录排除掉。否则含密钥的文件会被打进镜像层,docker history 一翻就出来了。

4. pi-docker:完整实现

上面这个最小版本只能算跑通。真正用起来还有一堆琐事:在任意目录一条命令启动、同时开几个项目、dev server 的端口映射、容器用完自动回收……我把这些塞进了一个小工具 pi-docker,顺带把日常最常用的几条命令也封了进去。用法:

cd 任意项目目录
pi-docker        # 启动 Pi 会话(一次性容器,退出即焚)
pi-shell         # 进容器交互 shell,用于试装软件
pi-up / pi-exec  # 常驻容器,可在单独终端跑 dev server
pi-doctor        # 自检路径、凭据、镜像

容器里还跑了个看门狗:没有活跃会话、空闲超过 15 分钟,就自动停掉并删除,免得用久了容器越积越多。

pi-docker 下载地址见:pi-docker.zip

三、方案二:Ubuntu 独立用户降权

如果你的开发机就是 Linux,又不想为每个项目都开容器,还有另一种更轻的做法:不换环境,只换身份,让 Pi 以一个专用的低权限用户运行。

1. 工作原理

Linux 判断一个进程能不能读某个文件,看的是启动它的用户身份(UID)。Pi 以 pi-agent 的身份跑,而你的 ~/.ssh 属主是你自己,内核就会在它试图打开文件的时候直接拒绝。这就是全部的安全来源。

2. 三个细节

  1. 项目不能放在家目录下。Ubuntu 家目录默认 750 甚至 700,other 没有任何权限,pi-agent 连门都进不去。共享工作区得放在双方都能进的地方,比如 /srv/dev
  2. 新建文件的归属会让 git 卡住。pi-agent 建出来的文件,属主和属组都是它自己;默认的 umask 022 又让文件是组只读。结果你既不是属主、也不在它的组里,改都改不动,git 提交自然失败。解决办法是两件事一起做:给共享目录加 setgid 位,让新建文件自动继承 dev 组;再把 pi-agentumask 改成 002,让新文件组内可写,这两件缺一不可。
  3. sudo 会重置 PATH。 它默认把 PATH 换成 secure_path,可以把pi装在系统级目录,或是按我下面的单独设置PATH目录。

3. 配置脚本

这套配置我整理成了脚本,直接跑就行(把 WORKSPACE 换成你自己的共享目录):

#!/usr/bin/env bash
set -euo pipefail

AGENT_USER="pi-agent"
SHARED_GROUP="dev"
WORKSPACE="/srv/dev"

# 1. 建组、建用户(锁定密码,禁止登录)
sudo groupadd -f "$SHARED_GROUP"
if ! id "$AGENT_USER" &>/dev/null; then
  sudo useradd -m -s /bin/bash -g "$SHARED_GROUP" "$AGENT_USER"
  sudo passwd -l "$AGENT_USER"
fi
sudo usermod -aG "$SHARED_GROUP" "$USER"
sudo usermod -aG "$SHARED_GROUP" "$AGENT_USER"

# 2. 共享工作区:双方可读写,setgid 让新文件继承组
sudo mkdir -p "$WORKSPACE"
sudo chgrp -R "$SHARED_GROUP" "$WORKSPACE"
sudo chmod -R g+rwX "$WORKSPACE"
sudo chmod g+s "$WORKSPACE"

# 3. umask 002:新文件组内可写(与 setgid 缺一不可)
grep -q '^umask 002' "/home/$AGENT_USER/.bashrc" \
  || echo 'umask 002' | sudo tee -a "/home/$AGENT_USER/.bashrc"

# 4. 继承 git 身份
sudo tee "/home/$AGENT_USER/.gitconfig" >/dev/null <<EOF
[user]
  name = $(git config user.name 2>/dev/null || echo 'Your Name')
  email = $(git config user.email 2>/dev/null || echo 'you@example.com')
EOF

# 5. 专用 SSH key,去 GitHub 配成单仓库 deploy key
if [ ! -f "/home/$AGENT_USER/.ssh/id_ed25519" ]; then
  sudo -u "$AGENT_USER" ssh-keygen -t ed25519 -N '' -f "/home/$AGENT_USER/.ssh/id_ed25519"
  sudo -u "$AGENT_USER" cat "/home/$AGENT_USER/.ssh/id_ed25519.pub"
fi

4. 无感 wrapper

每次手写 sudo -u pi-agent 实在太烦,我索性写了个 pi-safe 代替 pi,它会顺手把当前目录纳入共享组,用起来和原来几乎没区别:

#!/usr/bin/env bash
set -euo pipefail

AGENT_USER="pi-agent"
AGENT_HOME="/home/pi-agent"
PI_BIN="$AGENT_HOME/.local/share/pi-node/node-v22.23.2-linux-x64/bin/pi"
SHARED_GROUP="dev"
BREW="/home/linuxbrew/.linuxbrew"

p="$(pwd)"
sudo chgrp -R "$SHARED_GROUP" "$p" 2>/dev/null || true
sudo chmod -R g+rwX "$p" 2>/dev/null || true
sudo chmod g+s "$p"

# 单独设置PATH,ENV变量,如果有其他的类似JAVA_HOME,可以自行添加
exec sudo -u "$AGENT_USER" env \
  HOME="$AGENT_HOME" \
  PATH="$AGENT_HOME/.local/share/pi-node/node-v22.23.2-linux-x64/bin:$BREW/opt/python@3.12/libexec/bin:$BREW/bin:$BREW/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
  HOMEBREW_PREFIX="$BREW" \
  HOMEBREW_CELLAR="$BREW/Cellar" \
  HOMEBREW_REPOSITORY="$BREW/Homebrew" \
  bash -c 'umask 002; exec "$0" "$@"' "$PI_BIN" "$@"
chmod +x ~/bin/pi-safe
cd /srv/dev/my-project
pi-safe 

四、两种方案对比与选择

两种方案我都用了不短时间,下面是我自己的体感:

维度 Docker 容器 Linux 降权用户
隔离强度
文件权限摩擦 低(UID 对齐后基本无感) 中(需 setgid + umask + 共享组)
环境一致性 强(镜像锁死工具链) 弱(用宿主工具链)
资源开销 几乎为零
跨平台 macOS / Windows / Linux 仅 Linux
内核漏洞逃逸 有风险(与宿主共享内核) 不适用
本地服务 / 端口 需要端口映射 原生支持
对宿主的副作用 几乎无 Agent 装的包会留在系统里

我日常主力是 Docker 模式,隔离干净,代价是每次 Pi 升级都得重新构建镜像,或是安装软件也要重构镜像。这个成本不小,但有AI也很方便。Linux 上如果嫌麻烦,降权用户也够用,但它会随着时间,开发环境会变乱。两者你可以自行尝试选择。

五、参考资料

  1. Pi 官方安全文档(No Built-in Sandbox)
  2. Pi 官方容器化文档
  3. Pi 项目主页

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《ITechLib》

留言:

(lesstile enabled - surround code blocks with ---)