隔离与约束
四种运行形态,以及它们各自真正挡住了什么
Pi 不内置沙箱。它不会在每次工具调用前征求批准,扩展、包安装器、语言服务器和其他子进程都以启动 Pi 的那个账户的权限运行。官方安全文档的第一句就是这个事实,第二句才是解法:
安全来自限制 Pi 在生成动作出错或恶意时能访问和影响哪些文件、凭据、进程和网络服务。盯住 transcript、用项目信任、审查改动,都不构成安全边界。
这句话里最关键的是「限制」的宾语。四种隔离形态挡的东西不一样,挡不住的东西也不一样。本文的目标不是让你选一个,是让你知道自己选了之后还剩什么。
16.1 四种形态与关键区别
先看官方的总表(docs/containerization.md:11-14):
这张表看着信息量很大,但真正把它拆开的是第一列。四种形态里,只有 Gondolin 的 Pi 跑在主机上。
唯一需要判断的问题
把形态按这个轴切开:
Pi 进程在边界内 ── 纯 Docker · Docker Sandboxes · OpenShell
│
└── Pi 进程在主机上 · 只有工具执行在边界内 ── Gondolin为什么这个区别是决定性的?因为 Pi 的每一个动作最终都是它的某个子进程做的事,而子进程继承 Pi 进程的权限。所以:
- Pi 在边界内,意味着模型、扩展、包安装器、语言服务器——所有东西——都已经在边界里了。你不需要逐个配置,边界是自动全覆盖的。
- Pi 在边界外,意味着只有你显式路由过去的那部分在边界里。Pi 自己、模型请求、扩展代码、以及扩展提供的其他工具,全都在边界外。
官方文档把这句话写在表格正下方(docs/containerization.md:16):
完整 Pi 进程跑在隔离环境里时,它的扩展也跑在里面。主机上的 Pi 通过 Gondolin 委托内置工具时,其他扩展工具仍然跑在主机上,除非它们也显式委托自己的工作。
安全文档的措辞更重一点(docs/security.md:17):
主机资源会受到「经由这些工具执行的动作」的保护。Pi 自身和其他扩展仍在边界之外,所以这是一种更窄形式的隔离。
「更窄」不是修辞。Gondolin 挡住的和没挡住的,可以逐条列出来(见 16.5)。
每种形态真正挡住了什么
这是本文最有用的表。它把「官方总表」翻译成「攻击面对照表」。
最后三行是全文的落点。四种形态没有一种能防提示注入改变 Pi 自身的行为——这是设计上的取舍,不是遗漏。安全文档把这一点写进了它自己的安全策略里(docs/security.md:99):预期的本地 Agent 行为、来自不可信内容的提示注入、没有内置沙箱、用户自行安装的扩展或 Skill 的行为,通常都在安全边界之外,除非报告能证明存在权限边界绕过。
换句话说:隔离解决的是「生成的动作错了会怎样」,不是「模型被说服做了什么」。
为什么隔离比盯 transcript 有效
docs/security.md:5 把这层关系点得很准:文件、注释、指令、命令输出和模型响应都能通过提示注入影响模型。项目信任控制的是启动时加载哪些项目资源,但它不让这些内容或由此产生的动作变安全。
于是就有了这个推论链:
盯 transcript 只能在你恰好在看的时候生效
审查改动 只能在改动已经落到磁盘之后生效
项目信任 只管启动期的资源加载,不管运行期的工具行为
─────────────────────────────────────────────
运行环境边界 不依赖你注意力的那一刻,从机制上生效前三种都是人的动作,第四种是机器的约束。这就是为什么官方文档把「限制可访问的资源」列为唯一真正的安全边界。
「边界」这个词具体指什么
四种形态里出现了三种不同的边界强度,值得说清楚,因为它们决定了「绕过去要付出什么」:
前两种是硬件辅助的隔离,安全性有内核和 hypervisor 支撑。第三种完全不同:它的强度取决于策略写得对不对。
这个区别在实践里很重要。容器和 micro-VM 挡的是「进程」,进程无论怎么写都在边界内。策略挡的是「行为」——一条命令只要符合策略就能跑,哪怕它正在做一件你其实不想让它做的事。
所以 OpenShell 的五类策略(文件系统、进程、网络、凭据、推理)不是「比容器更强的隔离」,而是另一种隔离:它能表达「允许访问 /workspace 但禁止写入 node_modules」这种容器很难表达的规则。两者是补充关系,不是替代关系。
一个必须先知道的例外:工作目录不是边界
docs/security.md:19 这句话很容易被误读:
工作目录控制资源发现和工具的默认位置,但它不阻止命令访问 Pi 进程可用的其他路径。
也就是说:cd 到一个干净的目录,既不会限制 read 能读什么,也不会限制 bash 能跑什么。工具拿到的是相对路径,但进程拥有的是启动它的账户的全部权限。工作目录是发现范围,不是权限范围。
在纯 Docker 里这一点体现为:-v "$PWD:/workspace" 只挂载了一个目录,但容器进程依然能访问镜像里的所有东西;反过来,你不挂载的目录它就真的看不到——这才是挂载在隔离里的真正作用。
怎么选:三个问题
四种形态可以按三个连续的问题收敛,不需要记住四张配置表。
问题 1:Pi 进程本身需不需要被保护?
│
├─ 需要(你在跑不可信的仓库,或者你装了第三方扩展)
│ └─► 纯 Docker / Docker Sandboxes / OpenShell
│ 进入问题 2
│
└─ 不需要(自用,扩展都是自己写的)
└─► Gondolin —— 保留主机界面的同时拿到文件隔离
问题 2:真实的供应商 key 能不能进边界?
│
├─ 不能(合规要求,或者 key 权限很大)
│ └─► Docker Sandboxes 或 OpenShell
│ 再问问题 3
│
└─ 能(个人 key / 短时 token / project-scoped key)
└─► 纯 Docker 就够
问题 3:要不要远程执行,或者需要细粒度行为策略?
│
├─ 要 ──► OpenShell
└─ 不要 ──► Docker Sandboxes三个问题里,问题 1 的答案几乎总是「需要」。理由在 16.1 那张表里:扩展代码、语言服务器、包安装器全都继承 Pi 进程的权限,它们跑在任何地方就在哪受到约束。既然你用了扩展(几乎所有人都用了),Gondolin 的「只保护工具」就够不着你最需要担心的那一部分。
问题 2 才是真正的分水岭。 它把纯 Docker 和另外两个分开。默认答案往往是「能」——一个范围狭窄、时效短的 key 落在本地容器里,风险可控。但如果你用的是主账号 key、或者组织有明确要求,就必须往代理那一层走。
问题 3 决定 OpenShell 何时胜出。 它的五类策略是独有的能力,远程执行也是。所以「需要策略或需要远程」直接指向 OpenShell,其余情况 Sandboxes 足够。
16.2 纯 Docker
最简单的整进程容器边界。Pi、内置工具、! 命令和扩展全部在容器里,因为它们是同一个进程。
建镜像
FROM node:24-bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends bash ca-certificates git ripgrep \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g --ignore-scripts @earendil-works/pi-coding-agent
WORKDIR /workspace
ENTRYPOINT ["pi"]有一行值得解释:ripgrep 不是可选项。 grep 工具的后端是 rg,find 的后端是 fd(见 工具与信任边界)。宿主上有这两个二进制时 Pi 直接用;没有的话它会下载到 ~/.pi/agent/bin/。在离线容器里那条下载路径走不通,所以把 ripgrep 装进镜像是最省事的做法。
--ignore-scripts 阻止 npm 在安装时跑生命周期脚本。这不是洁癖:在一个你正要用来跑 Agent 的镜像里,省掉安装期的任意代码执行。
跑起来
从你想让 Pi 访问的工作目录执行:
docker run --rm -it \
-e ANTHROPIC_API_KEY \
-v "$PWD:/workspace" \
-v pi-agent-home:/root/.pi/agent \
pi-sandbox逐段拆开:
命名卷 pi-agent-home 让容器本地的设置、凭据和会话在多次运行之间保留。这是个容易忽略但很重要的细节:不用命名卷的话,你每次 docker run 都要重新走一遍 OAuth。
关于挂载 ~/.pi/agent 的一条硬规则
官方文档的原话(docs/containerization.md:70):
除非你确实希望容器能访问你主机的 Pi 配置和凭据,否则不要挂载宿主机的
~/.pi/agent。
这句话值得展开。容器里已经有一个 /root/.pi/agent(由命名卷提供)。你在宿主机上还有一个。挂载宿主机那个,等于把所有项目的凭据、所有扩展、所有会话记录一起送进容器。
docs/containerization.md:23 把它列为隔离环境里仍然暴露的东西之一:
挂载
~/.pi/agent会把你的 Pi 凭据、设置、扩展和会话一并暴露。
所以纯 Docker 的默认姿势是:工作目录挂进去,agent 目录不要挂。凭据通过 -e 单个传入,需要保留的那部分用命名卷。
验证你确实在容器里
进 Pi 之后跑:
!pwd应该报 /workspace。/workspace 下的改动会写穿到宿主的挂载目录。
如果不接受写穿,去掉 bind mount,或者改成只读挂载(:ro)。这两条是官方给的选项。
纯 Docker 挡住了什么
三条便宜的加固
上面那张「没挡住」的表里,前两行都能用很低成本堵掉。
不给网络。 Agent 不一定需要出网——本地模型 + 只读代码分析完全可以在 --network none 下跑。这是把「工具输出可以离开这个环境」(docs/containerization.md:25)从「可能」变成「不可能」的最短路径。
只读挂载工作目录。 -v "$PWD:/workspace:ro" 让 Pi 能读不能写,需要写的地方让它输出到别处。文档给了这个选项(docs/containerization.md:80)。代价是 edit 和 write 工具会失败,所以只在「只需要分析」的任务里用。
用完即删 vs 保留状态。 --rm 让容器每次都是干净的,代价是 OAuth 要重登。镜像层面可以靠 docker commit 或者干脆把状态烘进镜像来折中。命名卷是你在「干净」和「免重登」之间唯一的中间选项——所以它默认就存在,不是可选项。
16.3 Docker Sandboxes
同样是 Pi 整个进程在边界内,区别在凭据怎么过关。
核心机制:代理替身
Docker Sandboxes 把完整的 Pi 进程放进一个托管沙箱。它的关键能力是:代理可以把真实的供应商凭据留在主机上,只在请求离开沙箱时替换进去。
Pi 进程(沙箱内)
│ 持有的是 sk-ant-oat01-{rand} 这样的占位符
↓
沙箱代理
│ 替换成真实的 ANTHROPIC_OAUTH_TOKEN
↓
api.anthropic.com沙箱里的任何东西——包括模型、扩展、任意子进程——读到的都只是那个占位符。真实令牌只在代理这个位置出现,而代理在沙箱外。
这是四种形态里唯一一个凭据默认不进入边界的方案,也是它相对纯 Docker 的全部增量价值。
配置顺序不能反
文档在标题下第一句就写死了(docs/containerization.md:86):
在创建沙箱之前配置好凭据。不要在沙箱内运行
/login,因为那会把一个真实凭据写进沙箱。
这不是建议,是操作顺序。/login 会把凭据写进沙箱内的 auth.json,那一刻起凭据就已经在边界里了,代理的替换价值全部消失。
Claude 订阅 token 的配法
要在沙箱里用 Claude Pro/Max 订阅,先生成一个 token。用 claude setup-token 在一台装了 Claude Code 的机器上生成。
如果已经配了一个 anthropic secret,先删掉——否则代理会在 bearer token 旁边再加一个 API key 头:
sbx secret rm anthropic
sbx secret set-custom \
--host api.anthropic.com \
--env ANTHROPIC_OAUTH_TOKEN \
--placeholder 'sk-ant-oat01-{rand}'sbx secret set-custom 从标准输入读真实 token。沙箱拿到的是一个形状相同但无效的 OAuth 占位符,代理只对配置的那个 host 替换它。
用 Anthropic API key 的话是 sbx secret set anthropic,不需要这层手工。
启动
sbx run --kit "docker.io/sbx/pi-kit:latest" pi对一个已存在的沙箱,非交互式跑一次:
sbx exec <sandbox-name> -- pi -p "list the failing tests"Pi kit 的文档(docker/sbx-kits-contrib 仓库)里有其他供应商的配置、排障和镜像固定方式。
和纯 Docker 怎么选
判断标准很简单:你能不能接受真实供应商 key 出现在容器里。 能,纯 Docker 就够。不接受,Sandboxes 是唯一为此设计的形态。
代价是引入了一个代理——请求路径上多一跳,而且供应商特有行为(比如某模型拒绝某个参数)可能在代理那层就变了。这是真实成本,但相比凭据落在边界内的风险,通常是划算的。
16.4 OpenShell
NVIDIA OpenShell 的定位比前两个都宽:它管文件系统、进程、网络、凭据、推理五类策略,而且沙箱可以是远程的。
Pi 在这里的角色和其他三者一样——整个进程在沙箱内。
先有网关
每个沙箱都需要一个活跃的网关:
openshell gateway add <gateway-url> --name <name>
openshell gateway select <name>网关是 OpenShell 的策略执行点。没有网关就没有沙箱——这是 OpenShell 和 Docker 最本质的架构差异。
建沙箱
openshell sandbox create --name pi-sandbox --from pi -- piPi、它的内置工具、! 命令和扩展工具全部跑在 OpenShell 边界内(docs/containerization.md:140)。
远程网关不挂载宿主目录
这是远程形态最容易踩的一步。远程网关不会把你的宿主工作目录 bind mount 进去。 文件得显式传:
openshell sandbox upload pi-sandbox ./working-folder /workspace
openshell sandbox download pi-sandbox /workspace/working-folder ./working-folder-out或者干脆在沙箱里 clone 一份。
这条限制看起来烦,但它是远程沙箱的安全基础:宿主目录在另一台机器上,物理上就没法直接挂。
凭据:推理路由
OpenShell 的凭据处理是五类策略里的一类。真正值得说的是推理路由:
OpenShell 推理路由可以让原始模型凭据留在沙箱外。配置之后,把 Pi 指向网关暴露的对应的 OpenAI 兼容或 Anthropic 兼容端点。
也就是说它的模型和 Docker Sandboxes 的代理是同一个思路:凭据留在外面,Pi 拿到的是网关地址而不是密钥。
而且因为网关自己暴露的是「OpenAI 兼容」或「Anthropic 兼容」端点,Pi 这边只需要一条 models.json 配置改 baseUrl(见 模型与供应商):
{
"providers": {
"anthropic": { "baseUrl": "https://<gateway-exposure>/..." }
}
}模型列表和认证逻辑都不变。
关于策略配置的一个澄清
OpenShell 的策略是通过 CLI 和网关管理的,Pi 文档里给出的全部是命令(openshell gateway add / gateway select / sandbox create / sandbox upload / sandbox download)。Pi 的文档里没有出现任何 OpenShell 策略配置文件。
如果旧资料里出现过一个 openshell.yaml 之类的策略文件,那是编造的。OpenShell 策略是 NVIDIA 侧的产物,写在网关那边,不在 Pi 的工作目录里。找它要去 OpenShell 官方文档。
三种「Pi 在里面」的形态怎么选
一个经验判断:先试纯 Docker。 它的隔离强度和另外两个在同一量级(内核边界),只有当凭据留在外面成为硬需求时才升级。升级的信号通常来自合规要求,而不是技术故障。
16.5 Gondolin 扩展
Gondolin 换了个思路。它不把 Pi 装进虚拟机,而是留在你主机上,用一个本地 Linux micro-VM 承接工具执行。
于是它成了四种里唯一的异类,也是最容易被误解的一个。
它到底做了什么
Gondolin 是一个 micro-VM 库(earendil-works/gondolin),Pi 官方提供了一个示例扩展。装上之后:
- Pi 进程留在主机上。
- 它把主机工作目录挂载到 VM 的
/workspace,文件改动写穿回主机。 - 它接管七个内置工具:
read、write、edit、bash、grep、find、ls。
主机 micro-VM
┌─────────────────────────┐ ┌──────────────────────────┐
│ pi 进程(Node) │ │ /workspace ← 主机工作目录 │
│ ├ 模型请求 ─────────────┼── 网络 ─┤ │
│ ├ 扩展代码(未委托的) │ │ shell: /bin/sh 或 bash │
│ └ 被接管的 7 个工具 ─────┼───────►│ read/write/edit/bash │
│ │ │ │ grep/find/ls │
│ │ │ │ │
└───────┼────────────────────┘ └──────────────────────────┘
│
└── ⚠️ process.env 复制进 VM 的每个 bash 命令注意 七个,不是八个。powershell 没有被接管——它在 Windows 上有意义,而 Gondolin 是 Linux micro-VM。
装与跑
要求 Node.js 23.6 或更新,以及通过系统包管理器装的 QEMU。
mkdir -p ~/.pi/agent/extensions
cp -R packages/coding-agent/examples/extensions/gondolin ~/.pi/agent/extensions/gondolin
cd ~/.pi/agent/extensions/gondolin
npm install --ignore-scripts然后从你想挂载的工作目录启动:
cd /path/to/working-folder
pi -e ~/.pi/agent/extensions/gondolin-e 加载的是这一个扩展,不是整个目录。
它改的不只是工具
示例扩展里还有三个容易忽略的行为:
VM 的生命周期挂在会话上。 session_start 时创建 VM,session_shutdown 时关闭。所以 VM 的存活期和你的会话一致,不会变成一个一直跑着的孤儿进程。
系统提示里的工作目录会被改写。 扩展在 before_agent_start 里把提示词中那行 Current working directory: <主机路径> 换成 Current working directory: /workspace (Gondolin VM; host workspace mounted from <主机路径>)。所以模型看到的工作目录是 VM 里的路径,不会以为自己能直接写主机路径。
! 命令也被路由进 VM。 通过扩展的 user_bash 事件接管。你在输入框里敲 !ls -la,跑的是 VM 里的 ls。
还有一个 /gondolin 命令,报告 VM ID、主机工作目录、VM 内工作目录和实际使用的 shell 路径。shell 是启动时探测的:跑一次 command -v bash,找到就用 bash,找不到退回 /bin/sh。
路径是怎么翻译的
七个被接管的工具里,有六个只是把「执行」换了个位置,剩下一个细节决定了隔离实际成不成立:路径翻译。
示例扩展里有一个函数专门干这事。它的规则是:
工具给的路径 → VM 里实际访问的路径
────────────────────────────────────────────────────────
工作目录内的相对路径 src/a.ts → /workspace/src/a.ts
工作目录内的绝对路径 <cwd>/src/a.ts → /workspace/src/a.ts
工作目录外的绝对路径 /etc/passwd → VM 自己的 /etc/passwd
空路径 → /workspace最后两行是关键。模型仍然可以请求 /etc/passwd 或 /root/.ssh/id_rsa,但请求会被送到 VM 里的那些路径——VM 里没有你的 SSH 密钥,因为那些文件从来没被挂进去。
这就是隔离真正起作用的地方:不是拒绝越界路径,而是让越界路径落到一个没有你主机文件的文件系统上。
对照宿主的行为就很清楚。docs/security.md:19 说工作目录「不阻止命令访问 Pi 进程可用的其他路径」——在纯 Docker 里,bash 跑 cat /root/.ssh/id_rsa 会读到你主机的(如果挂载了)。在 Gondolin 里,同一条命令读到的是 VM 的 /root/.ssh/id_rsa,那是个空目录。
bash 工具还多一层:cwd 也被翻译成 guest 路径,命令在 VM 里的那个目录下跑,而不是宿主的工作目录。
一个实现差异:find 和 grep 是重写的
七个工具里,六个是「把底层操作换成 vm.fs.* / vm.exec」,实现很直接。但 find 和 grep 是完全重写的——因为宿主的 fd 和 rg 是本地二进制,VM 里没有。
这带来两个可观察的行为差异。
遍历时硬编码跳过 .git 和 node_modules。 重写的遍历逻辑里就一行:if (entry === ".git" || entry === "node_modules") continue;(gondolin/index.ts:151)。这不是配置项,也没有写进 Pi 的任何文档——我在 docs/llama-cpp.md 和 docs/containerization.md 里 grep node_modules 零命中。所以它是一个你需要知道、但官方没打算让你知道的行为。
如果你依赖 find 或 grep 遍历到 node_modules 里的东西(比如查某个只存在于 node_modules/.cache 的产物),换到 Gondolin 形态后结果会不一样。
grep 把整个文件读进内存再匹配,而不是流式扫描。大文件上这会比宿主的 rg 慢。
另外 grep 的默认 limit 是 100(示例扩展里的 DEFAULT_GREP_LIMIT),和 grep 工具本身的默认值一致。
⚠️ 凭据警告:这不是凭据边界
这一节是本文最需要你记住的部分。
官方文档的原话(docs/containerization.md:157):
VM 内的命令继承主机进程环境。通过环境变量提供的供应商 key 因此在 VM 内是可见的。除非你移除敏感变量或修改扩展的环境处理,否则不要把这个模式当作凭据边界。
这句话的语气比文档里其他地方都重。原因在源码里一目了然。
bash 工具在执行命令时的环境变量来自 getShellEnv():
getShellEnv() → { ...process.env, PATH: 更新后的 }也就是说,除非显式传了别的 env,否则整个 process.env 都会交给子进程。工具确实会删掉几个 PI_* 会话变量(PI_SESSION_ID、PI_SESSION_FILE、PI_PROVIDER、PI_MODEL、PI_REASONING_LEVEL),但 ANTHROPIC_API_KEY、OPENAI_API_KEY、AWS_SECRET_ACCESS_KEY 这些一个都不会被删。
Gondolin 的 bash 实现把这份环境原样转发进 VM:
exec: async (command, cwd, { onData, signal, timeout, env }) => {
// ...
const proc = vm.exec([shellPath, "-lc", command], {
cwd: guestCwd,
env: sanitizeEnv(env), // ← 只是把每个 string 值拷一遍
// ...
});
}env: sanitizeEnv(env) 里的 env 就是 env ?? getShellEnv() 的结果,sanitizeEnv 唯一的动作是过滤出值为 string 的项——不做任何过滤意义上的脱敏。
所以完整的泄漏路径是:
宿主 shell 里的 ANTHROPIC_API_KEY
↓ 启动 pi
pi 进程的 process.env
↓ getShellEnv() 返回整个 process.env
bash 工具的 options.env
↓ sanitizeEnv() 原样保留
Gondolin 的 vm.exec({ env })
↓
VM 里的 bash 进程环境
↓
模型让 bash 跑 `env` → 密钥出现在工具输出里最后一步是最要紧的:VM 隔离的是文件系统,不是环境变量。 模型完全可以在 VM 里跑一条 env 把密钥打印出来,然后这段输出就进了 transcript。
那凭据到底被挡住了吗
这里有一个容易搞混的区分:
总表里那一格写的就是这件事:已存的 Pi 凭据留在主机上,但命令继承主机环境变量。
换句话说,Gondolin 挡住的是文件形式的凭据,挡不住环境变量形式的凭据。这不是实现疏忽——它只是「Pi 在主机上」这个前提的必然推论:Pi 进程要拿环境变量去认证,环境变量就在 Pi 的地址空间里,而 VM 里跑的命令和 Pi 是同一个用户的同一个执行上下文。
真要当凭据边界用怎么办
文档给的处方是二选一:移除敏感变量,或改扩展的环境处理。
移除变量最直接:
# 不这样起
ANTHROPIC_API_KEY=sk-ant-... pi -e ~/.pi/agent/extensions/gondolin
# 而这样起
pi -e ~/.pi/agent/extensions/gondolin然后在主机上用 auth.json(/login)而不是环境变量来认证。因为 auth.json 是文件,Pi 进程能读,但 VM 里的 bash 读不到——它只能看到挂载过去的 /workspace。
改扩展更彻底:示例扩展里 sanitizeEnv 是一个普通的函数,你可以往里加一个拒绝名单,让敏感变量不进 VM:
sanitizeEnv(env) → 复制时跳过 ANTHROPIC_*、OPENAI_*、AWS_* 等这不是官方提供的选项,是一个你可以做的改动。代价是你要自己维护这份名单,而名单漏一个就等于没做。
这是你要自己做的决定,示例代码不替你做。 我把示例扩展整个目录 grep 过一遍(BLOCKED_ENV、DENY_ENV、SENSITIVE、REDACT、allowlist 一类标识符零命中),确认 sanitizeEnv 是唯一的 env 处理点,且它只做「值是 string 就留」这一件事。默认行为就是把全部供应商 key 送进 VM。
Gondolin 挡住了什么,没挡住什么
最后两行是「为什么 Gondolin 不是完整方案」的完整答案。
什么时候选 Gondolin
它适合的场景很具体:
- 你想在本机用 micro-VM 的文件隔离强度,但不想为每次运行起一个容器;
- 你要保持完整的主机界面(IDE 集成、tmux、终端复用);
- 你能接受凭据走
auth.json而不是环境变量; - 你的扩展工具本来就不碰主机敏感资源。
最后一条是扩展开发者要自己确认的。文档在两处都写了同一句警告:审查 Gondolin 示例,在添加可能绕过 VM 边界的工具之前。
16.6 凭据与网络
前五节各自讲了各自的机制,这一节把它们放到一张图上,因为凭据和网络是隔离方案里最容易出事的两个维度——它们都穿过边界,而边界本身对它们没有内置防护。
隔离环境仍然会暴露的五样东西
docs/containerization.md:20-26 列了一份清单。这份清单的措辞是「一个被隔离的进程仍然能影响你暴露给它的资源」:
最后一行是给 Gondolin 形态的警告,也是本文 16.1 那张表最后几行的来源。
官方给的处置原则是一句话:只暴露任务需要的文件、凭据和网络目的地。 不希望写入影响主机时,用只读挂载,或者把文件拷进拷出。
凭据:四条路,风险递增
按「真实密钥离边界有多远」排序:
文档给的两条通用建议(docs/security.md:21):
尽可能把凭据放在环境之外,或者使用范围狭窄、时效短的凭据。
「范围狭窄、时效短」在 OpenAI 那类供应商上就是 project-scoped key 和短期 token;在 AWS 上就是 IRSA 或短时 STS 凭据,而不是长期 IAM key。
「放在环境之外」在纯 Docker 里做得最别扭——容器里总得有某种凭据。它在 Sandboxes 和 OpenShell 里是一等公民,在 Gondolin 里靠用 auth.json 代替环境变量也能做到。
网络:默认假设它开着
容器默认有网络。所以「跑了 Docker 就安全了」在网络维度上是错的。
docs/containerization.md:25 的措辞是「网络访问可能允许代码或工具输出离开这个环境」——注意是可能允许。你的凭据、你的源码、你 prompt 里的内容,都可能顺着出站请求走。
三条处置办法,按侵入性从低到高:
- 不需要网络就不给。
docker run --network none,或者不开端口映射。这是最彻底也最便宜的一条。 - 只给需要的出口。 OpenShell 的网络策略、代理的白名单,都属于这一类。
- 接受它,靠其他边界兜。 如果 Agent 必须联网抓文档,那就明确知道它在抓什么。
还有一个容易被忽略的出口:llama.cpp 的 /llama 命令能下载模型(模型与供应商)。这是一条通向 Hugging Face 的出站路径,也是一条往你本机模型缓存写东西的路径。它在边界内还是边界外,取决于你用哪种形态。
一个 Pi 特有的凭据陷阱:会话文件
docs/security.md:93 提醒了一件和隔离无关但同源的事:
导出或分享会话之前先审查它们。它们可能包含提示词、工具参数、命令输出、文件内容和对话过程中暴露的凭据。
会话是纯文本的。它会记录模型看到的一切,包括工具输出里出现的密钥。容器隔离不保护会话文件——会话文件在你选的地方写。
同理,auth.json 和 models.json 都应当保持私有,不要提交到版本库。
回到原点:三种「不构成边界」的做法
在结束之前,值得把 16.1 开头那句话再具体化一遍,因为它是全文的判断基准:
这三样都值得做——docs/security.md:86-93 把它们列成「降低影响或改善恢复」的最佳实践,文档没有否定它们。文档否定的只是把它们当成边界。
一句话版本:模型可能被说服做任何事,你能控制的是它做完之后能碰到什么。
上线前对照表
选完形态之后,下面这张表是实际配置时最容易漏的几项。它把全文的结论压成可勾选的形式。
三行值得单独强调。
Gondolin 的 key 那一行永远不是简单的「是」或「否」——它取决于你用 auth.json 还是环境变量。用 auth.json 是「否」,用环境变量是「是」,而示例扩展不做任何区分。这是选 Gondolin 时你必须自己做的一个决定,不是默认行为。
出站网络那一行,Gondolin 是「敞开」。 VM 继承宿主环境,通常就有网络。
会话文件那一行,Gondolin 在主机上。 会话记录了模型看到的一切,包括任何泄漏到工具输出里的凭据。它的保护级别取决于你自己的文件权限,不取决于 VM。
16.7 事实来源
完整事实表(含每条断言的推导命令)见专栏仓库的 research/pi-agent/FACTS.md。