源文件:
src/main/pi-security.ts,src/main/sandbox-manager.ts最后更新:2026-08-10
- 不牺牲能力:安全检查不应阉割 Agent 能力。safe 操作自动放行,needs_confirmation 操作弹窗让用户决定,只有 risky 操作才主动阻止。
- 纵深防御:三层安全模型,每层独立工作,内层不可被外层绕过。
- 最小权限:默认拒绝未知操作,明确列入白名单的才放行。
- 参考标准:Claude Code auto mode(分类器+升级)、LobsterAI(权限门控+沙箱)。
Layer 1: 工具风险分类器(每次 beforeToolCall)
→ safe: 自动放行,零延迟
→ risky: 阻止并返回原因,Agent 自动换方案
→ needs_confirmation: 升级到 Layer 2
Layer 2: 用户确认(Renderer UI;桌面 IPC / 会话 SSE + HTTP 回传)
→ 显示工具名、参数摘要、风险原因
→ 用户点「允许」或「拒绝」
→ 各通道按自身超时和执行生命周期 fail closed;无可用通道即拒绝
Layer 3: 硬性边界(不可绕过,即使 Layer 1/2 放行也会拦截)
→ 敏感路径黑名单
→ 系统目录禁写
→ 符号链接追踪(防止通过 symlink 绕过路径检查)
- 只读内置工具:
read_screen,read_page_content,recall_memory,load_skill,web_search,capture_screenshot - 用户交互工具:
ask_user - 自有数据写入:
save_memory(写入~/.openpipal/目录) - 文件读取:
read_file(路径在允许目录内时) - MCP 工具名以
get_,list_,query_,search_,resolve,fetch_开头
- 所有命令执行:
execute_command,bash,shell(非危险模式的) generate_document(创建本地文件)write_file(路径在允许目录内时)- MCP 工具名以
create_,add_,edit_,modify_,update_,set_开头 - MCP 工具名以
delete_,remove_,drop_,stop_,end_开头 - 文件写入操作(路径在允许范围内)
- 破坏性但可逆的命令:
rm -rf、git reset --hard、git clean -fdx、git push --force、find -delete(编码工作里的日常操作——回滚失败改动、重装依赖、强推自己的分支。硬拒它们等于让 agent 在需要 回滚时走投无路;git push --force-with-lease是安全替代,不在此列,直接放行) - 所有未分类的工具
- 匹配不可逆命令:
sudo、mkfs、dd if=、curl|sh、chmod 777,以及命令位置上的 shelleval/exec(只在行首或;&&|||(之后匹配——否则npm run test:eval、npm exec tsc会被误伤) - 路径指向敏感目录(见下方黑名单)
- 路径指向系统目录(
/etc,/System,/usr等) - 路径不在允许的工作目录内
以下路径即使在用户主目录内也绝对拒绝访问:
| 路径 | 说明 |
|---|---|
~/.ssh/ |
SSH 密钥 |
~/.aws/ |
AWS 凭证 |
~/.gnupg/ |
GPG 密钥 |
~/.config/gcloud/ |
Google Cloud 凭证 |
~/.docker/ |
Docker 配置和凭证 |
~/.kube/ |
Kubernetes 配置 |
~/.npmrc |
npm 凭证 |
~/.netrc |
网络凭证 |
~/.git-credentials、~/.config/git/credentials |
git credential.helper=store 的明文凭证 |
~/.config/gh、~/.config/hub |
GitHub CLI 的 oauth token |
~/.gitconfig.local |
常见的「把 token 塞进 url.insteadOf」私有配置 |
~/.bash_history, ~/.zsh_history |
命令历史 |
~/.env, ~/.credentials |
通用凭证文件 |
~/.password-store/ |
密码管理器 |
| 路径 | 说明 |
|---|---|
~/.openpipal/ |
OpenPipal 数据目录 |
~/Documents/ |
用户文档 |
~/Desktop/ |
桌面 |
~/Downloads/ |
下载目录 |
/tmp/, os.tmpdir() |
临时目录 |
工作目录里的 AGENTS.md / CLAUDE.md 会被原文读进系统提示词(main/agent-runtime/project-context.ts)。
这条路径是直接 readFileSync,不过工具层的读取黑名单,所以边界由它自己守:
| 边界 | 拦的是什么 |
|---|---|
只走 [仓库根 .. 工作目录] |
没有 .git 就只看工作目录本层。爬到 ~ 或 / 会把用户其它项目、甚至全局私人规矩拽进本次对话 |
工作目录在 ~/.openpipal/** 时整段不注入 |
助手自己的草稿区里的 md 是它自己的产物,注入等于自问自答 |
软链解析(realpathSync.native)后必须仍在仓库内 |
AGENTS.md -> ~/.ssh/id_rsa 会把凭证原文送进系统提示词、随每一轮请求发给模型服务商 |
| 单文件 1MB 上限、整段 8k token 上限 | 一份失控的文档不该吃掉整个上下文窗口 |
注入的正文里明确声明它们是项目配置而不是用户指令:无权放宽安全边界、无权替用户授权。
第三方仓库里的 AGENTS.md 属于不可信内容,出现"忽略先前指令"这类文本时按可疑内容处理。
回归:tests/unit/project-context-injection.test.ts
MCP 工具通过 Pi Agent 的 beforeToolCall hook 统一经过分类器检查。分类规则:
- 按名称前缀推断操作类型(见上方分类规则)
- 参数中的文件路径同样受 Layer 3 硬性边界约束
- 所有 MCP 工具对所有角色开放(角色白名单不过滤 MCP 工具——这是设计决策,因为 MCP 工具已经有安全分类器保护)
源文件:
src/main/hooks/、src/main/agent-runtime/pi-core-tool-adapter.ts、src/main/evolver-agent.ts
用户对助手说一句"以后读成绩表先把学生名字遮掉",助手把这句话交给后台写手(Evolver 的 set-rule
技能)写成一个 TypeScript 文件。文件放在哪决定它管谁(位置即范围):
- 全局规则:
~/.openpipal/plugins/local-rules/hooks/*.ts,每个对话都装;任何插件也可以在自己的hooks/里带规则 - 某个 Pal 里定的:
~/.openpipal/agents/<id>/hooks/*.ts,只在跑这个 Pal 时装、跟着它走
它们是用户自己机器上、自己目录里的代码,信任级别与用户装的插件、MCP 服务器相同;在主进程里 执行,没有沙箱,能改工具入参、拦下调用、补改工具结果、追加系统提示。边界:
- 规则在授权器之前跑,授权器审的是改过之后的最终参数:改出来的参数先按工具 schema 复验, 再走同一套分类器 / 确认 / 路径边界。规则不能放宽任何一层,也不能推翻宿主的 terminate。
- 规则自己要用工具(
ctx.callTool)走同一道授权,与模型直接调用无差别。 - 规则拦下调用写审计:
RESULT=hook_blocked,带原因。 - 规则出错一律放行(fail-open)并在对话里报告——规则是便利层,不是安全层;安全层始终是 分类器与沙箱。
- 写手的边界只有那个
hooks/目录:Evolver 只有文件工具、没有 shell、没有权限弹窗,assignedRoot就是规则文件所在的hooks/(不是 Pal 的整个目录——那里还有 agent.md / memory / skills,一条规则 不该有改人设的权限);在 Pal 里写要带workspaceId,否则租户边界把agents/下的一切当别人的。 - 写完当场加载、结论只来自加载器:工具刚写完的文件若落在上面两种位置,注册表立刻编译加载, 成功 / 失败(带原因)以胶囊显示给用户、同一句话回给模型。探针的范围与本会话装规则的范围一致: 写进别的 Pal 目录的文件对本会话不生效,不会得到"从下一轮开始执行"的结论。
- 模型每轮看到规则清单(系统提示末尾的
<rules>块:生效 / 没生效带原因 / 后台正在写), 清单来自本会话刚装的那份;只给事实,不给"该怎么答"的指令。 - 规则文件里
require直接抛错——这是防误用(规则该是纯逻辑),不是隔离:vm.compileFunction在主进程上下文里跑,process等全局照样可达。别把这条当安全边界;安全边界是"文件来自用户自己的 目录"这一前提。每次调用带独立超时。 - 关闭:文件改名加
.off(规则页的开关就是这样做的),或环境变量OPENPIPAL_DISABLE_HOOKS=1整体停用。
回归:tests/unit/pi-core-hook-composition.test.ts、tests/unit/hook-chain.test.ts、
tests/unit/hook-registry.test.ts(范围与野目录)、tests/unit/rule-writer.test.ts(写手根目录)
- 分类器判定
needs_confirmation createDesktopPermissionHandler通过agent:permission-request把请求发给 Renderer 的确认 UI;会话内联请求使用permission:inline-request- Renderer 分别通过
agent:permission-response或permission:inline-response回传用户决定 - 用户明确允许后才执行;拒绝、超时、执行已中止或窗口不可用时均拒绝
- 只有用户明确选择会话级授权时才记录对应工具/站点许可
- 分类器判定
needs_confirmation - 活跃且归属于扩展的聊天 SSE 流收到权限请求;请求不会回落到无关的 桌面窗口
- 已认证的浏览器客户端通过
POST /api/permission回传requestId、执行/会话标识和允许/拒绝决定 - 无活动流、标识不匹配、请求已过期或解析器不可用时 fail closed;HTTP 返回明确失败,工具不执行
- 无持久会话的 HTTP turn 只能批准本次操作,不能创建持久的会话级授权
- 旧的
autoApproveHandler已废除,浏览器入口不会自动批准
- 普通桌面确认:
agent:permission-request/agent:permission-response - 桌面会话内联确认:
permission:inline-request/permission:inline-response - 扩展会话确认:聊天 SSE 权限事件 /
POST /api/permission - Preload API:
onPermissionRequest(),respondPermission(),onPermissionRequestInline(),respondPermissionInline()
每次升级 Agent 能力时,必须检查以下项目:
- 新增的工具是否已在分类器中注册(safe/risky/needs_confirmation)?
- 新增的 MCP 服务是否有破坏性操作?其工具名前缀是否被正确分类?
- 新增的文件操作是否受路径边界约束?
- 新增的网络操作是否有数据泄露风险?
- 是否需要更新敏感路径黑名单?
- 是否需要更新允许的工作目录?
- 新增的 hook 事件或
ctx能力,是否仍在授权器之前完成、授权器看到的是最终参数?
源文件:
src/main/sandbox-manager.ts依赖:@anthropic-ai/sandbox-runtime
OpenPipal 集成了 Anthropic 的 Sandbox Runtime (SRT),在 macOS 上通过 Seatbelt (sandbox-exec) 实现 进程级别 的文件系统和网络隔离。
应用启动 → initSandbox()
↓
平台支持? ──否──→ 降级到三层安全模型
↓是
SRT.initialize(config)
↓
沙箱就绪 ──→ bash 工具通过 SRT.wrapWithSandbox() 包装命令
| 操作 | 无沙箱 | 有沙箱 |
|---|---|---|
| bash(普通命令) | risky(无沙箱时 shell 整条禁用) | safe(OS 已保护) |
| bash(不可逆,如 sudo/mkfs) | risky | risky(Layer 3 不可绕过) |
| bash(可逆破坏性,如 rm -rf) | risky(shell 已禁用) | needs_confirmation |
| 文件写入(允许路径内) | needs_confirmation | safe(沙箱限制写入范围) |
| 敏感路径访问 | risky | risky(Layer 3 不可绕过) |
bash / execute_code(bash) / execute_code(python|js) 共用 assessDestructiveCommand():
- 此前只有 bash 通道过危险命令表,python 里
subprocess.run(["git","reset","--hard"])是静默放行的。 堵一扇门开一扇窗,模型会绕(2026-07-26 实案:rm被拦 → 改用shutil.rmtree删了上个会话的产物)。 - 非 shell 语言额外扫一遍「摊平引号与逗号」后的文本,让参数数组写法也能被同一套规则看见。
该摊平只用于 needs_confirmation 档——多问一次的代价远小于漏过;risky 档仍只看原文,
避免字符串里提一句
sudo就把整段代码硬拒。
| 规则 | 路径 |
|---|---|
| denyRead | ~/.ssh, ~/.aws, ~/.gnupg, ~/.config/gcloud, ~/.docker, ~/.kube, ~/.npmrc, ~/.netrc, ~/.password-store, ~/.credentials |
| allowWrite | .(工作目录), ~/Documents, ~/Desktop, ~/Downloads, ~/.openpipal, /tmp |
| denyWrite | .env, .git/hooks |
默认白名单分两组:模型服务(api.anthropic.com 等,不通就没法对话)与取包/取代码
(registry.npmjs.org、registry.yarnpkg.com、github.com、codeload.github.com、
objects.githubusercontent.com、raw.githubusercontent.com、pypi.org、files.pythonhosted.org)。
第二组是编码场景的最低要求——实测缺了它,curl https://github.com 与
git ls-remote https://github.com/... 都被代理挡成 CONNECT tunnel failed, response 403。
两个环境变量语义不同,别混(逗号分隔):
| 变量 | 语义 | 用在什么时候 |
|---|---|---|
OPENPIPAL_ALLOWED_DOMAINS |
整表替换 | 想收窄:只放行你列出的这几个域 |
OPENPIPAL_EXTRA_ALLOWED_DOMAINS |
在默认表上追加 | 想加一个公司私有 registry |
分成两个而不是把前者改成追加:改语义会让"用它收窄"的人静默失去收窄效果(安全方向的回归); 而只有前者时,想加一个域就得把整张默认表重抄一遍,抄漏一个 AI API 域就是整个应用连不上模型。
SSH 走不通,而且补不了:SRT 只代理 HTTP/HTTPS,沙箱里 DNS 也不放行,
ssh git@github.com 卡在 Could not resolve hostname。git 只能走 HTTPS。
沙箱化的 bash 命令执行前,自动移除以下敏感环境变量:
- 显式列表:
OPENAI_API_KEY,ANTHROPIC_API_KEY,DEEPSEEK_API_KEY,GOOGLE_API_KEY,TAVILY_API_KEY,AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,GITHUB_TOKEN,GH_TOKEN,NPM_TOKEN - 模式匹配:所有以
_KEY,_SECRET,_TOKEN,_PASSWORD,_CREDENTIAL结尾的变量
如果沙箱在以下情况下不可用,应用自动降级到现有三层安全模型:
- macOS / Linux 之外的平台(Windows,见下一节)
- Seatbelt 工具缺失
- 初始化异常
两种"没沙箱"在分类器里是分开判的:macOS / Linux 本该有沙箱却没起来是故障,bash / execute_code 整条禁掉(fail-closed);Windows 根本没有可用的 OS 沙箱,走下面这套。
@anthropic-ai/sandbox-runtime 只有 Seatbelt(macOS)与 bubblewrap(Linux)两个后端,Windows 上没有等价物。Windows 版因此不带 OS 沙箱,边界就是用户自己的账号权限,应用层这样补:
- 每条命令交给用户裁决:
bash(Git Bash)/powershell/execute_code一律needs_confirmation,确认卡理由写明"本平台没有系统沙箱,这条命令会以你的账号权限直接执行"。"本次会话允许"只记住完全相同的那一条命令;"完全允许"档吃掉普通命令的确认,但破坏性命令(rm -rf / Remove-Item -Recurse / git reset --hard …)与用户目录/整盘遍历仍然每次问。 - 凭据路径文本闸:命令或代码里写明了凭据位置(
~/.ssh、~/.aws、%APPDATA%\GitHub CLI、.env、~/.openpipal/config.json等)直接拒绝,不给"点允许"的机会。它认不全(变量拼接、子解释器),所以是逐条确认之上的加一道,不是替代。.env.example这类模板、~/.openpipal/workspace与skills不在其列。 - 路径表按平台取:系统目录禁写(
%SystemRoot%、Program Files、ProgramData)、工作根禁区(盘根、C:\Users、AppData)、凭据目录(点目录那一套加%APPDATA%下的 gh / gcloud / gnupg / PowerShell 历史)都有 Windows 版本;比较时折叠大小写。 - PowerShell 与 bash 同一张危险命令表:Format-Volume / diskpart /
irm … | iex/ Invoke-Expression / RunAs / reg delete HKLM / Set-MpPreference -Disable 硬拒;Remove-Item -Recurse|-Force、rd /s /q、del /s /q、管道删除、HKCU 注册表删除、清空回收站需确认。
这是最低防线,不是沙箱的替代品:不可信仓库的 postinstall、提示词注入让 agent 跑的命令,在这里能挡住的只有"文本上认得出"的那部分。需要隔离的 Windows 用户,后续路线是 WSL2 里复用 bubblewrap 后端。
已知代价:
GITHUB_TOKEN/GH_TOKEN/NPM_TOKEN也在剥离之列(*_TOKEN通配), 因此沙箱内的git push、私有源安装无法用环境变量凭证认证;SSH 又因为不被代理而走不通。 读侧不受影响:公开仓库的 clone/fetch/ls-remote、npm/pip 安装都实测可用。 是否为编码场景放行 git 凭证是一次显式的信任取舍,目前保持剥离,未开口子。
所有工具调用自动记录到 ~/.openpipal/audit.log:
[2026-03-31T10:00:00.000Z] TOOL=bash ARGS={"command":"ls -la"} RESULT=safe SANDBOX=true
[2026-03-31T10:00:01.000Z] TOOL=write ARGS={"path":"./test.ts"} RESULT=safe SANDBOX=true
[2026-09-07T10:00:02.000Z] TOOL=read ARGS={"path":"./grades.xlsx"} RESULT=hook_blocked REASON=被规则「读成绩表前先遮名字」拦下 SANDBOX=true
日志使用非阻塞写入(appendFile),不影响工具执行性能。
- 废除
autoApproveHandler:浏览器扩展模式不再自动批准所有 needs_confirmation 请求 - 默认拒绝:如果没有可用且匹配当前执行的 Renderer/SSE 审批通道,所有 needs_confirmation 请求默认拒绝
- 桌面、内联和扩展响应最终都进入受执行/会话标识约束的权限解析器
- 审批 UI 完善:补齐键盘/焦点、超时提示和可审计的会话级授权管理
- 用户可配置白名单/黑名单:在设置中添加路径白名单编辑器
- 审计日志 UI:在设置页面展示工具调用历史和安全事件