最近几个月我一直在用 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. 几个坑
- 别挂
docker.sock。 很多人为了让容器里也能跑 docker,把 /var/run/docker.sock 挂进去。这等于把宿主的 root 权限交出去,隔离白做。
- 别直接挂宿主的
~/.pi。 那里有你所有的会话、设置和凭据。需要哪个凭据,就往 ~/pi-data/pi 里放哪个。当然了如果你使用docker,可以直接删除宿主机的Pi。
- git 同理。要让 Agent push 代码,就单独生成一把只对单个仓库有效的 deploy key,别把能开所有仓库的私钥塞进去,出事后的损失不是一个量级。
- 构建时记得用
.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. 三个细节
- 项目不能放在家目录下。Ubuntu 家目录默认
750 甚至 700,other 没有任何权限,pi-agent 连门都进不去。共享工作区得放在双方都能进的地方,比如 /srv/dev。
- 新建文件的归属会让 git 卡住。
pi-agent 建出来的文件,属主和属组都是它自己;默认的 umask 022 又让文件是组只读。结果你既不是属主、也不在它的组里,改都改不动,git 提交自然失败。解决办法是两件事一起做:给共享目录加 setgid 位,让新建文件自动继承 dev 组;再把 pi-agent 的 umask 改成 002,让新文件组内可写,这两件缺一不可。
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 上如果嫌麻烦,降权用户也够用,但它会随着时间,开发环境会变乱。两者你可以自行尝试选择。
五、参考资料
- Pi 官方安全文档(No Built-in Sandbox)
- Pi 官方容器化文档
- Pi 项目主页