04. 插件安全
翻译说明: 本页是与 英文源规格 一一对应的机器辅助翻译。代码、协议字段和标识符保持原文;如翻译与英文源事实有歧义,以英文版本为准。
1. 威胁模型
插件可能来自:
- 用户自行开发
- 同事分享
- 未来市场中的第三方
主要风险:
- 恶意读写文件
- 恶意执行命令
- 窃取 API 密钥或会话内容
- 劫持 Agent 工具
- 通过用户界面进行网络钓鱼
- 消耗用户的模型额度,或把对话发给另一个模型(
agent.complete/session.read) - 在其他持久化会话中触发工作,或伪造消息发送方与来源
2. 默认拒绝原则
- 未声明的权限=不可用
- 禁用插件 = 代码未加载
- 未经确认的高风险行动=未执行
- 主机 API 不在允许名单上 = 不存在
pi.browser.cdp不在 CDP 白名单上的方法 =PERMISSION_DENIED(无 cookies、storage、Target 或 Fetch;无 DevTools websocket)
3. 隔离策略
必须
- 插件 UI 与宿主 UI 的 DOM 隔离
- 插件不能直接通过 require 加载宿主模块
- 密钥存储不向通用插件 API 开放。宿主代发的补全(
agent.complete)在 Electron 主进程 解析凭据,从不把密钥、刷新令牌或ModelAuth交给插件进程。高风险的provider.oauth回调是一个范围严格限定的例外:它只能读取该插件自己声明的 provider 贡献项所对应的加密 OAuth 凭据 - 插件私有数据目录与宿主核心库分离
session.getLlmContext的会话转录只是进行中工具会话的有界投影(D336 / D019)- 会话协作仅通过经过审查的
desktop.control操作目录提供。代理层从当前活动的 Agent 工具调用中确定源插件、Session ID、turn ID 和 invocation ID,插件载荷不能指定 这些身份。Host-core 负责目标 Session ID、投递台账、权限上限、轮次绑定、回调、 取消操作和转录来源信息。
剪贴板历史由 Electron 主进程持有,仅保存在内存中,不会写入插件数据目录或主机 数据库。主机只记录明确的剪贴板写入和用户主动在 Composer 中粘贴的内容,不会在后台 轮询系统剪贴板。插件只能通过 clipboard.read 权限读取历史,该权限也用于 readText;每次 getHistory 调用都会 记录审计及返回条目数。应用退出时历史会清空,保留上限也限制了隐私暴露范围。
由宿主渲染的场景主题设置页面
contributes.scenicThemes 仅包含数据。宿主在扩展页渲染卡片前,会校验两项权限均已授予、 主题属于同一插件、预览资源已声明,以及只使用取值受限的 --nexus-backdrop-blur 变量。 插件不能提供设置页的 HTML、CSS、JavaScript、选择器、DOM、任意操作或直接的渲染器 IPC。 透明画布、布局、焦点、原生控件、标题栏、“应用”操作,以及生命周期变化时回退到“常规”页, 均由宿主管理。
3.2 会话协作边界
用户授予 desktop.control 后,Session Orchestrator 可创建数量受限的工作会话、向现有 Agent 会话发送消息、查看范围受限的状态 / 结果投影,以及取消工作。此能力不直接授予插件 session.create、agent.prompt、宿主 RPC、SQLite、转录文件或 MCP 令牌访问权。 发送或创建工作会话的调用必须位于该插件正在执行的 Agent 工具调用中;服务、面板或普通 插件代码在缺少此上下文时发起调用,会被拒绝。插件面板可作为用户的显式控制入口, 请求取消该插件自己的投递,但取消操作不能新建投递或更改投递目标。
Host-core 对源权限上限取快照,拒绝权限超出该上限的目标,在启动轮次前再次检查目标模式, 并强制执行收件箱、工作会话和自主转发跳数限制。已有目标会话保留自身的项目、模型与上下文 配置。完成回调是由宿主编写、至多发送一次的会话消息,既不能授权工具,也不能触发另一条 回调。重启恢复会保留持久化的排队投递,但不会在无人确认时重启已中断的轮次。 会话消息的来源信息在替换或重新生成转录时保持不变。
目标
- 插件主程序在独立进程中运行
- 崩溃隔离
- 资源限制(后续:CPU / 内存 / 超时)
3.1 插件提供的主题 CSS
主题贡献(ui.theme)在主进程通过安全过滤后,将 CSS 发送到宿主渲染器。 另一个高风险权限 renderer.extension 会在宿主渲染器中执行受信任插件的 JavaScript,并不提供沙箱:
- 只检查浏览器实际生效的 CSS:先按等长空白遮蔽注释体与字符串字面量 (每个被遮蔽字符对应一个空格,偏移仍指向原文),
url(...)参数按原样保留 并按目标判定。因此仅在注释或字符串里提到被禁关键字的样式表会被接受 - 拒绝:
@import、既非data:URI 也非已声明主题资源的url()目标、 解析器无法解析的url(、javascript:、expression(和标记序列 (<style、</style、<!--);空样式表也会被拒绝 - 每个文件上限为 256 KiB,不限制每插件主题数量
- 主题可声明
assets,使用白名单图片/字体扩展名:插件包内相对路径(在插件根目录内解析,拒绝 路径穿越和node_modules)或绝对路径;所有资源总量上限为 4MB。命中的url()会被改写为plugin-asset://<pluginId>/<path>,由宿主处理器提供;该处理器 只按已加载插件自己登记的清单解析,只读、带nosniff,并在插件卸载时一并撤销。pi.themes.upsert也可在运行时登记同类路径。未登记的引用仍会被拒绝, 原始路径不会到达渲染器 contributes.windowAppearance(背景为#rrggbb/#rrggbbaa,cornerRadius为 0..24 DIP 的整数)需要ui.window.appearance,且只在该插件的某个主题被选中时生效。 切换到其他主题即恢复宿主背景,因为外观由实时主题目录推导,不会被单独记住。 macOS 保持原生圆角;启用 vibrancy 的半透明窗口忽略该颜色,关闭“半透明窗口” (Translucent window)后,颜色才应用于不透明窗口。Linux 保持原生圆角行为。 Windows 默认请求 12 DIP 圆角;在 Windows 11(build 22000+)上,半径 0 请求直角, 所有正值使用同一种系统圆角偏好,实际半径由操作系统决定。带 alpha 的背景会与解析后的 内置主题颜色合成,确保顶层窗口不透明。更早的 Windows 版本保留请求的 DIP 半径- CSS 在加载时从磁盘读取,通过 IPC 整体传递。渲染器将其注入单个专用
<style>元素, 插在应用自身样式表之后,因此它可以覆盖设计令牌,但不能注入标记。 后写的声明只有在选择器特异度相同时才胜出;应使用:root[data-theme="light"]或:root[data-theme="dark"]匹配基础调色板的 选择器,单独的:root特异度更低 - 选中的主题以设置值
plugin:<pluginId>:<themeId>保存;提供主题的插件被禁用或卸载后, 该设置回退到system
CSS 不能执行脚本,却仍可能误导用户:主题是塑造用户所见内容的第三方代码, 因此必须显式声明权限,且授权可以撤销。
4. 权限授予用户体验
安装 / 加载时显示:
- 权限列表
- 风险描述
- 开发者信息
- 源路径
用户操作:
- 接受并启用
- 取消
首次使用高风险的 API 可能会再次确认。
5. 数据隔离
插件可以访问:
- 他们自己的设置
- 他们自己的数据路径
插件无法访问:
- 其他插件的数据
- 宿主机密的通用访问接口;
provider.oauth仅允许回调访问自己声明的 provider 凭据 - 主机完整会话数据库的任何通用接口。存在受控 API:
pi.session的.own授权(仅本插件导入的会话)或session.*.all授权(所有活动会话,安装时批准并逐次审计)。除此之外没有别的会话访问路径,也没有 SQLite 句柄或记录文件访问
5.1 插件间消息总线
总线是两个插件之间的唯一通道,其能力范围受到刻意限制:
- 双方都在清单中声明消息范围:
bus.publish列出具体主题,bus.subscribe列出匹配模式。 即使已授予权限,代理层仍拒绝未声明的消息操作 - 路由完全由宿主管理;订阅者无法得知其他订阅者,发布者也不会收到自己发布的消息
- 消息仅携带
topic、from、payload和主机分配的at - 上限:每条载荷 64KB,每个插件 16 个订阅,每个滚动 10 秒窗口最多发布 100 次。 超限调用以
LIMIT_EXCEEDED/RATE_LIMITED失败,并连同主题记入审计 - 载荷是数据,不是能力:接收消息不会赋予订阅者原本不具备的权限
应将主题视为应用内的公共信息:任何声明了匹配模式且获得 bus.subscribe 权限的插件 都能看到消息。不要通过总线传递机密。
Provider OAuth 凭据边界
provider.oauth 是独立于 provider.register 和 net.fetch 的高风险授权。它允许 onProviderOAuth 为同一插件声明的 provider 处理登录和刷新。宿主会在其密钥存储中加密每个凭据,绝不会把刷新令牌发送给渲染器或 Agent Runtime。插件回调可以读取该凭据,因为它实现了该 provider 的 OAuth 协议;但它不能读取其他 provider 的密钥,也不能调用通用密钥 API。通过宿主网络 API 发出的请求仍然需要 net.fetch 和 manifest.net.domains。插件入口代码不是操作系统沙盒,可以使用原始 Node API,因此获得此授权的插件必须是用户信任的代码。当登录被取消、插件卸载或宿主调用超时时,回调会收到中止信号。退出登录会清除凭据,并保留清单拥有的 provider 行。
“添加服务”的 provider 目录也由宿主渲染。它只读取已加载且获得 provider.register 权限的 插件清单元数据,仅展示尚未配置、且声明了端点的 API-key provider,并将类别与 provider 名称 作为文本渲染。可选描述仅在悬停或键盘聚焦时以纯文本提示显示。选择器不会执行插件代码, 也不会返回凭据。保存密钥时沿用宿主现有的 provider 密钥存储路径;宿主仅在用户明确保存后 发现端点模型,用户通过该 provider 发送请求时,对应 provider 服务才会收到密钥。
6. 路径安全
fs.read / fs.write / fs.delete 决定插件能否操作文件;manifest.fs 限定可操作哪些文件(参见 02-plugin-manifest-schema.md §5.2 与 ADR 0088)。每次 pi.fs.* 调用依次经过四项检查,后续检查不能推翻此前的拒绝:
- 权限 —— 必须既声明又获得授权。运行时取两者的交集,因此即使清单仍声明某项权限, 用户撤销授权后,该权限也会失效
- 路径包含关系 —— 对根目录和目标都执行
realpath。工作区内指向~/.ssh的 符号链接会被拒绝,不会因字符串前缀匹配而通过。待创建路径通过最近的已存在祖先解析, 以区分新文件和路径越界。拒绝绝对路径和..;仅因路径不存在时报告NOT_FOUND, 而不是判定为越界 - 拒绝列表 —— 优先于所有根目录、作用域和授权:
- 凭证:
.env*、.npmrc、.netrc、.pypirc、.git-credentials、id_rsa*及同类、*.pem、*.p12、*.pfx、*.keystore - 任意深度的目录:
.git、.ssh、.aws、.gnupg、.kube、.docker - 宿主自身的数据目录,其中存放 provider 密钥和会话数据库
- 根目录本身(删除操作)
- 凭证:
- 已声明的作用域 —— 不在范围内时请求原生确认(§6.2)
pi.fs.glob 遵守相同规则:获取名称也属于读取,因此被拒绝的路径和保留目录树 不会出现在结果里,匹配结果会按读取范围过滤,而 node_modules / .git / .venv / __pycache__ 永不遍历。
6.1 删除
删除是唯一无法通过重新运行插件来恢复的文件操作,因此受到四重约束:
- 两级授权。 使用
own: true时,宿主在插件数据目录维护写入台账(路径及 mtime), 允许插件删除自身写入的文件,无需声明作用域或弹窗。若文件的 mtime 晚于记录值, 表明用户后来修改过,该文件便不再视为插件所有。删除其他内容必须声明scope。 - 系统回收站。 删除通过
shell.trashItem,不使用rm。即使某项检查判断有误, 用户仍能从回收站恢复文件。宿主不会为此复制用户数据。 - 永不递归。 非空目录直接拒绝,而不是清空。
- 速率限制。 每个插件在滚动 60 秒窗口内最多删除 50 次,因为
recursive: false只能约束单次调用,无法限制先glob再循环删除的行为。超限后请求一次用户确认, 并明确说明原因是速率,而不是路径。
6.2 运行时同意
清单未覆盖的访问会显示原生 dialog.showMessageBox: 拒绝 / 允许一次 / 本会话允许。会话授权覆盖所在目录, 仅保存在内存中,随进程结束失效,不会持久化。速率限制提示不提供会话授权选项。 宿主未提供授权确认服务时必须拒绝,无法询问用户时不能默认同意。 拒绝和授权都会记入审计。
6.3 用户选定的 root
pi.fs.requestDirectory() 打开原生目录选择器。用户已明确选择目录,因此插件在返回的 目录内无需清单作用域,但路径包含关系检查和拒绝列表仍然生效。句柄仅保存在内存中, 随进程结束失效。因此,用户可以选择任意目录,但插件不会获得跨进程存续的访问权; 这与浏览器 File System Access API 的授权模型相同。
6.4 拖放文件授权
沙盒化的插件面板可以通过宿主 preload 的 getDroppedFilePath 将用户拖放的 File 解析为本地路径。preload 会在页面代码运行前把该路径报告给面板宿主。fs.registerDropped(path) 会消费其中一个短生命周期、绑定发送者的报告,并返回仅存在于内存中的 grantId。该授权只覆盖那一个规范化的常规文件,用于 fs.stat 和 fs.readRange;它不会改变 manifest.fs,不会授予目录,也不允许写入、打开、显示或删除。授权随插件进程结束而失效,且永不持久化。受保护路径、凭据、符号链接替换和拒绝列表在注册时以及之后每次读取时仍然生效。
7. Agent 安全性
- 插件工具名使用已确定的强制前缀
plugin_<pluginIdSafe>_<toolName>(D015),通过命名空间避免冲突 - 工具执行超时
- 用户可以一键禁用工具
- 提示注入 API 默认情况下是高风险的,需要显式许可
7.1 提供给 Agent 的技能和 MCP 工具
这两类能力都允许插件改变 Agent 的知识或可执行操作,因此在提供给模型前都受到限制:
技能(agent.prompt.inject)—— 系统提示词只携带技能目录(id、名称、最多 240 字符的 单行描述),正文由内置 Skill 工具按需读取。每个插件最多提供 32 个技能, 每份文档最多 128KB。没有权限时会跳过技能:清单仍能通过校验,但技能不会进入提示词。
MCP 工具(mcp.server.local / mcp.server.remote)—— 发现的工具与手写插件工具 注册在同一 plugin_* 命名空间下,因而沿用工具超时、审计记录和按插件禁用的开关。 它们始终以 risk: "medium" 注册:schema 和描述来自第三方服务器,宿主不能信任服务器 自行声明的风险等级。服务器目录会完整注册,工具数量只受 §8.1 的协议限制约束; 每个插件最多接入 8 个服务器。
插件工具默认在 Plan/Goal 中隐藏并拒绝。运行时工具注册可通过非空 planSafeActions 显式加入,只有 args.action 位于该列表时才允许执行。有 schema action enum 时会校验取值。 清单描述符应与运行时注册保持一致,真正执行的工具由运行时注册决定。 risk: "low"、会话授权和 auto 不能代替该声明。宿主策略与插件执行器都会校验, 执行器对越界 action 返回 PERMISSION_DENIED。该列表是作者的只读承诺,不是副作用沙盒(ADR 0211)。
8. 网络和外部链接
- 默认情况下不授予
net.fetch openExternal应确认。主机解析 URL,只打开http:、https:和mailto:(D330 / ADR 0168);其他 scheme 以INVALID_ARGUMENT失败,不会到达shell.openExternal。fs.openDefault、fs.openInBrowser和fs.reveal是独立能力: 都只接受已通过插件fs.read策略检查的现有文件。Browser 操作仅接受 HTML 文件, 并在工作面板中打开新的资源标签页,不会授予 Browser CDP 访问权。这些 API 用于显式的 文件查看操作,不能打开任意 URL。宿主代理的文件操作也接受已注册项目根目录内的绝对路径;fs.reveal仅请求系统文件管理器选中该文件。- 禁止插件静默下载和执行二进制文件(MVP 中根本没有这样做)
8.0 出网白名单
一个权限没法表达「读得宽但什么都漏不出去」,所以范围放在清单里: net.domains 是每个插件唯一的一份主机名白名单,主机掌握的每一条出网路径 都听它的。
- 面板 session。
sandbox: true去掉的是 Node,不是网络,所以面板以前 是一个完整的浏览器,而且从不经过net.fetch。现在该 session 跑一个webRequest过滤器,拒绝所有设备权限(唯一例外是获得ui.microphone的插件 发起的纯音频麦克风请求,见 §8.2),并禁止window.open—— 否则它会造出一个 不受过滤的窗口。该 session 是每个插件一个的持久化分区 (persist:pi-plugin-<pluginId>),由插件的面板窗口和停靠视图共用,两者应用同一个 过滤器。file:、data:、blob:、devtools:、chrome-extension:和只读的 主题资源 scheme 不属于网络请求,会直接放行 pi.net.fetch。 检查白名单并手工跟随重定向,因为一个被允许的主机 30x 跳到未声明的主机上,就会把请求带出去。宿主共享的逐跳循环同时负责默认 传输和注入的单跳传输。redirect: "error"遇到 3xx 直接拒绝、不访问目标;manual返回原始响应。默认仍是follow,每一跳都重新检查。能力探测和错误码 见 API 契约- 远程 MCP 端点。 同样听这份列表,而不是只看它自己的权限。HTTP 端点可以 位于可信局域网,但明文 HTTP 不加密,配置或插件权限审查时会明确提示。 MCP 客户端手动跟随重定向,最多允许五次 HTTP(S) 跳转,并在每一跳之前重新 检查白名单。
pi.net.websocket。 目标主机不在manifest.net.domains里的ws://或wss://会在出网卡点处被拒绝,传输根本不会被要求去打开任何东西;而且持有 套接字的是宿主、不是插件,所以插件卸载、被禁用或崩溃时,宿主会关闭它仍 持有的每个套接字。套接字按插件设上限(4 个),入站和出站帧都封顶 1 MiB, 超大帧会关闭连接而不是被缓冲,超过 4 MiB 的发送队列会被拒绝而不是继续 增长。帧只会送达持有它的那个插件。
列表缺失、为空或非法就完全不放行出网,而裸 * 无法通过 validateManifest(), 免得有人靠声明绕出去:pi-plugin check 会报错,宿主也会拒绝加载该插件。 host-core 安装器不读取 net.domains,所以这样的包能装上,但随后加载失败。正是这一点让一个宽松的 fs.read 范围变得可以接受(§6)。
仍然敞着、单独跟踪的:agent.prompt.inject(技能文本可以让一个有 shell 能力的 Agent 替它搬运)、shell.openExternal、bus.publish 转给一个有网络 能力的插件,以及插件进程里的原生 fetch —— 最后一项需要 ADR 0008 D009 的沙箱化插件运行时。pi.net.fetch 并不会缩小这些缺口:宿主只负责套用白名单、 手工跟随重定向并审计这次调用,它从不重试、不限流、也不重新发起请求 —— 上游的 429 会带着服务器给出的 Retry-After 原样到达插件,插件怎么处理是插件自己 的策略。
8.1 MCP 服务器出网与凭据
MCP 服务器是 net.fetch 之外的另一条出网路径,因此必须通过可审查的声明配置, 不能任由代码动态决定:插件不能打开清单中未声明的连接。
transport: "stdio"启动本地可执行文件(mcp.server.local)。command必须是裸 PATH 名称或插件相对路径;绝对路径在验证时被拒绝。 子进程拿到最小环境——声明的env条目、共享白名单(child-process-env.ts), 以及npx/uvx需要的工具链键(PATHEXT、ComSpec、FNM_DIR等)。 Unix 上 PATH 是 login-shell PATH(D600)。裸npx/uvx会解析到真实二进制: 官方 Windows Node 走node.exe+npx-cli.js,其余.cmd经cmd.exe以引号字面参数启动(D624)。provider key 和其它宿主状态仍然不会穿越。transport: "http"到达远程端点 (mcp.server.remote)。url可以使用http或https;非回环 HTTP 不加密,只应在可信网络中使用。插件端点还 必须被manifest.net.domains覆盖。工具参数会离开机器,这就是为什么权限 文案必须明确说明。env和headers的值仅通过{ "setting": "<key>" }从插件自身设置解析。 宿主环境不会直接透传。清单中出现明文机密应视为审查问题,而不是受支持的用法(D018)。- 连接预算:
initialize必须在 10 秒内完成,每次tools/call上限为 100 秒, 每行 stdio 消息上限为 4MB。远程 HTTP 请求采用对应操作的预算;握手成功后, 后续工具调用不受 10 秒握手预算限制。tools/list会遍历到最后一页,同时应用 §8.1 的 单服务器限制:最多 2048 个工具、100 页、整轮遍历 30 秒,且游标不得重复或格式错误。 违反任一限制时拒绝整个服务器,不会只接收目录前半部分,因为 MCP 工具通过ToolSearch之后的按需条目提供给模型,而不是始终存在的工具列表。 服务器按需连接,并在插件卸载或禁用时关闭。 停止调用方会话会取消该会话正在执行的 MCP 请求,并向服务器发送notifications/cancelled;共享连接及其他会话的调用保持有效。请求超时后同样会发送取消通知;initialize请求从不取消。
8.2 桌面控制与设备访问
desktop.control 把本地 MCP 控制平面暴露的那份已审查操作目录(ADR 0203 / D370)交给插件:项目、会话、Agent 和工作区操作,每一项都标记为 read、 write 或 dangerous。plugin-only 例外涵盖六个 session/collaboration/* 操作:它们可以经由插件网关调用,却被刻意排除在 MCP 可见目录之外,因为它们 需要已认证的插件调用上下文,且渲染器没有任何变更通道。插件看到的是 id、描述和 风险等级,绝不会看到 Electron 通道名或 MCP bearer token;每一次调用都经过与 MCP 调用相同的 IPC 校验、生命周期检查、完成事件和审计条目。
dangerous 操作由用户拍板,而不是由调用者。控制器的 confirm: true 只是 插件的知会(MCP 也是同样处理,D372)。在此之后,宿主弹出一个原生对话框, 点名目录中的操作 id、目录描述和一段有界的参数预览,并刻意不显示任何由插件 或其背后模型撰写的文本,因此一份被提示注入的转录本无法把 session/delete 重新包装成无害的东西。Escape 和关闭对话框都视为拒绝。没有对话框服务的 无头宿主会直接拒绝所有 dangerous 操作。
ui.microphone 只在插件的隔离面板 session 内允许 media 权限,且仅限音频。 摄像头和其他所有设备权限仍被拒绝,插件也拿不到任何原生句柄:采集始终由 页面持有。
audio.capture.background 和 audio.playback.background 控制音频 API 的调用权限: 十个 pi.audio.* 方法已存在于插件宿主进程中,且各自仍要求对应权限,但当前分支没有设备 后端。因此,即使已获授权,调用仍会返回 UNSUPPORTED 并记入审计 (audio.<method>、ok: false),不会打开任何设备。两个同步注册辅助函数 onInputFrame / offInputFrame 也会抛出相同错误码,不会注册无法触发的处理器。 宿主服务落地后,设备将由宿主持有:插件只 交换 PCM16 帧,永远拿不到 MediaStream、设备句柄、操作系统设备路径或 Node 流,每个插件只允许一条输入流;禁用、卸载、崩溃或撤销权限会停止采集并丢弃 已排队的播放,而不会留下孤立的设备或计时器。
keyboard.globalShortcut 已实现,并且始终留在宿主的注册模型之内。宿主持有 Electron 的 globalShortcut;插件永远拿不到键盘钩子、before-input-event、 原始输入设备或按键事件流,因此无法通过这些 API 记录用户输入。插件只能把快捷键组合 映射到自己已注册的一条命令;被操作系统保留、被 PI-Desktop 当前占用(插件启动器默认为 Alt+Space,窗口显示 / 隐藏默认为 Alt+Shift+W; 用户改绑后即可释放给插件)或已被另一个插件持有的快捷键组合会被拒绝,不会被抢占。 注册根据具体原因返回 SHORTCUT_CONFLICT、SHORTCUT_UNAVAILABLE 或 INVALID_ACCELERATOR;LIMIT_EXCEEDED 则表示超过每个插件最多 8 条的数量上限。 一次触发只运行那一条命令。注册、注销和触发的审计会记录插件 id 与结果 —— 注册和触发还会记录加速键与命令 —— 绝不记录用户输入。每条条目都在禁用、 卸载和崩溃时释放。
四种能力均采用默认拒绝:未声明或未授予权限时,调用在触达任何设备、快捷键 或套接字之前就会被拒绝并记入审计。
9. 审计和应急响应
用户应该能够:
- 查看插件权限
- 查看插件错误日志
- 一键禁用
- 一键卸载
主机应该能够:
- 出现异常时自动禁用插件
- 保证主应用程序可以启动
10. 安全验收
- 没有
fs.write权限时,写入文件失败;对manifest.fs.write.scope之外的写入 会请求用户确认 - 没有
fs.delete权限删除文件失败;删除永不递归、落进系统回收站, 并在滚动一分钟内超过 50 次后被打断 - 禁用插件后,其工具不再可见
- 插件无法读取 API 密钥
- 插件面板无法调用任意宿主 IPC
- 插件未捕获的异常不会导致应用退出
- 含
@import或远程url()的主题 CSS 文件被拒绝;禁用提供该主题的插件后, 应用回退到system主题 - 向未声明的主题发布消息会失败,发布者不会收到自己发布的消息
- 声明绝对路径
command的 MCP 服务器会在 manifest 校验时失败;非回环的 明文 HTTP URL 只有在主机声明于manifest.net.domains且 UI 显示未加密连接 警告时才可接受 - 未声明 Plan 安全操作或 action 越界的插件工具,无论风险或授权如何,都在 Plan/Goal 中拒绝
11. 实施情况
目前执行情况:
PluginRuntime中的默认拒绝权限检查,取「已声明 ∩ 已授予」的交集, 所以被撤销的权限会真的失效- 插件 fs API 检查可防符号链接越界的路径包含关系,并应用无条件生效的拒绝列表。 每种文件操作的范围由
manifest.fs限定,范围之外请求原生确认(§6) - 面板窗口使用沙盒 preload 与隔离的会话分区。跨平台自定义标题栏由 preload 管理, 控件位于封闭的 Shadow DOM 中;窗口操作仅通过固定、经发送方校验的元组路由, 不会向
window.pluginBridge添加窗口操作原语 - 插件仍无法访问宿主机密 / 数据库
- 通过市场 / 插件包安装时,必须在 UI 中明确接受权限
- 自动更新拒绝静默扩大权限
- 插件主程序在每个插件专用的
utilityProcess(ADR 0008) 中运行,环境来自 共享白名单child-process-env.ts(PATH、工具链目录、HOME/USER/USERPROFILE,不含 provider 密钥);所有pi.*调用都经过宿主的白名单与权限网关, 插件崩溃仅会终止该插件 - 插件提供的主题 CSS 在到达渲染器前,先在主进程通过安全过滤(§3.1)
- 总线路由由主机拥有,并具有声明的主题和硬上限(§5.1)
- MCP 服务器必须声明并通过权限检查,凭据仅来自插件自身设置(§8.1)
- 出网被限制在
manifest.net.domains之内,覆盖主机掌握的每一个卡点(§8.0) - 插件删除操作使用系统回收站、不递归,并受速率限制(§6.1)
manifest.main和ui.panel在安装时被校验为相对路径,并在宿主加载 它们之前,执行与技能和主题 CSS 相同的插件目录内路径包含关系检查- 来自插件的
dangerous桌面操作,在插件自己的confirm: true之后, 还需要用户在宿主拥有的原生对话框中作答;对话框只显示目录文本(§8.2) ui.microphone只授予音频采集,且仅限隔离面板 session 内(§8.2)keyboard.globalShortcut由宿主持有:注册表拒绝被操作系统保留、宿主 自己占用或属于其他插件的加速键,快捷键只能运行持有插件自己的命令, 每条条目都与插件的命令和工具走同一条清理路径(§8.2)
audio.capture.background 和 audio.playback.background 已声明并且存在于 插件 API 中:这些方法由这两个权限把关,获得授权的调用会以带错误码的 UNSUPPORTED 拒绝并记入审计,因为当前宿主还没有设备后端,所以没有任何东西 能到达设备。 net.websocket 已实现:连接由宿主持有、经过白名单检查、有界,并随插件一起 释放(§8.0)。
尚未强制执行:
- 插件进程内的能力沙箱:Node 内置模块仍然可用,因此
fs.*权限约束的是插件 API, 并不约束整个进程。这是当前主要缺口:§6 与 §8.0 的限制适用于使用规定 API 的插件, 无法约束绕过 API 的插件(ADR 0008 D009) - CPU/内存限制
- 签名验证(仅对包进行 sha256 检查)
- 声明的清单权限整体授予。安装审核没有逐项开关,授权缺少任何一项声明权限时 安装器会拒绝(
PLUGIN_PERMISSION_DENIED: missing grant for <permission>); 没有已记录授权的加载直接使用声明的权限集合 userSelectedroot 不跨重启保留,插件每个会话都得重新问一次
Fetch 重定向策略
默认传输与注入的单跳传输共用宿主管理的重定向循环。error 对所有 3xx 返回 REDIRECT_DISALLOWED;manual 返回原响应,不访问目标;默认 follow 保留逐跳出网检查。 能力检测、截止时间语义与兼容性见 API 契约。 任何模式都不会扩大网络授权范围。