Skip to content

04. 插件安全 ​

翻译说明: 本页是与 英文源规格 一一对应的机器辅助翻译。代码、协议字段和标识符保持原文;如翻译与英文源事实有歧义,以英文版本为准。

1. 威胁模型 ​

插件可能来自:

  • 用户自行开发
  • 同事分享
  • 未来市场中的第三方

主要风险:

  1. 恶意读写文件
  2. 恶意执行命令
  3. 窃取 API 密钥或会话内容
  4. 劫持 Agent 工具
  5. 通过用户界面进行网络钓鱼
  6. 消耗用户的模型额度,或把对话发给另一个模型(agent.complete / session.read)
  7. 在其他持久化会话中触发工作,或伪造消息发送方与来源

2. 默认拒绝原则 ​

  • 未声明的权限=不可用
  • 禁用插件 = 代码未加载
  • 未经确认的高风险行动=未执行
  • 主机 API 不在允许名单上 = 不存在
  • pi.browser.cdp 不在 CDP 白名单上的方法 = PERMISSION_DENIED(无 cookies、storage、Target 或 Fetch;无 DevTools websocket)

3. 隔离策略 ​

必须 ​

  1. 插件 UI 与宿主 UI 的 DOM 隔离
  2. 插件不能直接通过 require 加载宿主模块
  3. 密钥存储不向通用插件 API 开放。宿主代发的补全(agent.complete)在 Electron 主进程 解析凭据,从不把密钥、刷新令牌或 ModelAuth 交给插件进程。高风险的 provider.oauth 回调是一个范围严格限定的例外:它只能读取该插件自己声明的 provider 贡献项所对应的加密 OAuth 凭据
  4. 插件私有数据目录与宿主核心库分离
  5. session.getLlmContext 的会话转录只是进行中工具会话的有界投影(D336 / D019)
  6. 会话协作仅通过经过审查的 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 对源权限上限取快照,拒绝权限超出该上限的目标,在启动轮次前再次检查目标模式, 并强制执行收件箱、工作会话和自主转发跳数限制。已有目标会话保留自身的项目、模型与上下文 配置。完成回调是由宿主编写、至多发送一次的会话消息,既不能授权工具,也不能触发另一条 回调。重启恢复会保留持久化的排队投递,但不会在无人确认时重启已中断的轮次。 会话消息的来源信息在替换或重新生成转录时保持不变。

目标 ​

  1. 插件主程序在独立进程中运行
  2. 崩溃隔离
  3. 资源限制(后续: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.* 调用依次经过四项检查,后续检查不能推翻此前的拒绝:

  1. 权限 —— 必须既声明又获得授权。运行时取两者的交集,因此即使清单仍声明某项权限, 用户撤销授权后,该权限也会失效
  2. 路径包含关系 —— 对根目录和目标都执行 realpath。工作区内指向 ~/.ssh 的 符号链接会被拒绝,不会因字符串前缀匹配而通过。待创建路径通过最近的已存在祖先解析, 以区分新文件和路径越界。拒绝绝对路径和 ..;仅因路径不存在时报告 NOT_FOUND, 而不是判定为越界
  3. 拒绝列表 —— 优先于所有根目录、作用域和授权:
    • 凭证:.env*、.npmrc、.netrc、.pypirc、.git-credentials、 id_rsa* 及同类、*.pem、*.p12、*.pfx、*.keystore
    • 任意深度的目录:.git、.ssh、.aws、.gnupg、.kube、.docker
    • 宿主自身的数据目录,其中存放 provider 密钥和会话数据库
    • 根目录本身(删除操作)
  4. 已声明的作用域 —— 不在范围内时请求原生确认(§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. 安全验收 ​

  1. 没有 fs.write 权限时,写入文件失败;对 manifest.fs.write.scope 之外的写入 会请求用户确认
  2. 没有 fs.delete 权限删除文件失败;删除永不递归、落进系统回收站, 并在滚动一分钟内超过 50 次后被打断
  3. 禁用插件后,其工具不再可见
  4. 插件无法读取 API 密钥
  5. 插件面板无法调用任意宿主 IPC
  6. 插件未捕获的异常不会导致应用退出
  7. 含 @import 或远程 url() 的主题 CSS 文件被拒绝;禁用提供该主题的插件后, 应用回退到 system 主题
  8. 向未声明的主题发布消息会失败,发布者不会收到自己发布的消息
  9. 声明绝对路径 command 的 MCP 服务器会在 manifest 校验时失败;非回环的 明文 HTTP URL 只有在主机声明于 manifest.net.domains 且 UI 显示未加密连接 警告时才可接受
  10. 未声明 Plan 安全操作或 action 越界的插件工具,无论风险或授权如何,都在 Plan/Goal 中拒绝

11. 实施情况 ​

目前执行情况:

  1. PluginRuntime 中的默认拒绝权限检查,取「已声明 ∩ 已授予」的交集, 所以被撤销的权限会真的失效
  2. 插件 fs API 检查可防符号链接越界的路径包含关系,并应用无条件生效的拒绝列表。 每种文件操作的范围由 manifest.fs 限定,范围之外请求原生确认(§6)
  3. 面板窗口使用沙盒 preload 与隔离的会话分区。跨平台自定义标题栏由 preload 管理, 控件位于封闭的 Shadow DOM 中;窗口操作仅通过固定、经发送方校验的元组路由, 不会向 window.pluginBridge 添加窗口操作原语
  4. 插件仍无法访问宿主机密 / 数据库
  5. 通过市场 / 插件包安装时,必须在 UI 中明确接受权限
  6. 自动更新拒绝静默扩大权限
  7. 插件主程序在每个插件专用的 utilityProcess (ADR 0008) 中运行,环境来自 共享白名单 child-process-env.ts(PATH、工具链目录、HOME / USER / USERPROFILE,不含 provider 密钥);所有 pi.* 调用都经过宿主的白名单与权限网关, 插件崩溃仅会终止该插件
  8. 插件提供的主题 CSS 在到达渲染器前,先在主进程通过安全过滤(§3.1)
  9. 总线路由由主机拥有,并具有声明的主题和硬上限(§5.1)
  10. MCP 服务器必须声明并通过权限检查,凭据仅来自插件自身设置(§8.1)
  11. 出网被限制在 manifest.net.domains 之内,覆盖主机掌握的每一个卡点(§8.0)
  12. 插件删除操作使用系统回收站、不递归,并受速率限制(§6.1)
  13. manifest.main 和 ui.panel 在安装时被校验为相对路径,并在宿主加载 它们之前,执行与技能和主题 CSS 相同的插件目录内路径包含关系检查
  14. 来自插件的 dangerous 桌面操作,在插件自己的 confirm: true 之后, 还需要用户在宿主拥有的原生对话框中作答;对话框只显示目录文本(§8.2)
  15. ui.microphone 只授予音频采集,且仅限隔离面板 session 内(§8.2)
  16. 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>); 没有已记录授权的加载直接使用声明的权限集合
  • userSelected root 不跨重启保留,插件每个会话都得重新问一次

Fetch 重定向策略 ​

默认传输与注入的单跳传输共用宿主管理的重定向循环。error 对所有 3xx 返回 REDIRECT_DISALLOWED;manual 返回原响应,不访问目标;默认 follow 保留逐跳出网检查。 能力检测、截止时间语义与兼容性见 API 契约。 任何模式都不会扩大网络授权范围。

本地优先 · 模型可替换 · 插件可扩展。 AIUO.NET