Skip to content

13. Plugin Permissions Matrix ​

1. Goals ​

Provide a permission–capability–risk–default-policy reference table for reuse by UI copy and validation.

2. Matrix ​

PermissionRiskAllowed API / capabilityDefault policyNotes
ui.panellowOpen the plugin panelGranted at installNeeded by almost all UI plugins
ui.viewlowcontributes.views are listed in the work panel and may be openedGranted at installSame isolation as a panel window: sandboxed page, per-plugin partition, net.domains egress. Filtered by activation scope
ui.themelowcontributes.themes CSS is loaded and offered in Settings; runtime pi.themes.upsert / remove / list and pi.app.setTheme (ADR 0260)Granted at installCSS is sanitized by the host; it cannot script. Declared assets are served over the host's read-only plugin-asset: scheme. setTheme may only select a built-in preference or a currently registered plugin theme. There is no per-plugin theme count cap
ui.window.appearancelowcontributes.windowAppearance sets the native window background while one of the plugin's themes is selectedGranted at install#rrggbb / #rrggbbaa only; applied per resolved palette and back to the host default once the theme is gone. macOS keeps vibrancy
clipboard.readmediumclipboard.readText, clipboard.getHistoryConfirm on first useMay read sensitive information and retained clipboard history
clipboard.writemediumclipboard.writeTextConfirm on first usePrevents clipboard pollution
notifylowui.notify, ui.getNotificationPermission, ui.requestNotificationPermission, ui.showNativeNotificationCan be granted by defaultNative delivery is OS-controlled; avoid notification-spam abuse
fs.readmediumfs.readText / fs.stat / fs.readRange / fs.readPreview / fs.openDefault / fs.reveal / fs.glob / fs.list / fs.requestDirectory / dropped-file grantGranted at install, bounded by manifest.fs.readfs.stat and fs.readRange share the read gate; a dropped-file grant is one-file, read-only, memory-only, and gesture-bound; all file calls remain root- and deny-list-checked
fs.writehighfs.writeTextGranted at install, bounded by manifest.fs.writeScope is required; a whole-tree pattern fails validation. Out of scope asks the user
fs.deletehighfs.removeGranted at install, bounded by manifest.fs.deleteTwo tiers (own / scope), always via the OS trash, non-recursive, rate-braked (§2B)
fs.read.workspacemedium—Downgraded on load to fs.read with a whole-tree scopeLegacy name; predates scopes
fs.write.workspacehigh—Downgraded on load to fs.write with no scopeLegacy name; every write asks the user until the manifest declares scope
fs.delete.workspacehigh—Downgraded on load to fs.delete with own: trueLegacy name; only the plugin's own output goes without asking
agent.tool.registerhighRegister an agent toolConfirm at installTool execution is audited separately
agent.prompt.injecthighInject a system prompt; activates contributes.skillsDeny by default / strong confirmationEasily leads to behavior hijacking
agent.extensionhighRun contributes.agentExtensions modules inside the agent processExplicit confirmation; local imports and development plugins only in v1.1Same access as the agent's own tools; the plugin sandbox does not apply (spec 16)
provider.registerhighcontributes.providers become rows in the native provider list, owned by the plugin and refreshed from the manifest on loadExplicit confirmation; local imports and development plugins only in v1.1, matching agent.extensionThe user path refuses the row (PROVIDER_OWNED_BY_PLUGIN); credentials stay in the Host secret store under the usual provider refs; oauth declarations are not enabled yet
net.fetchhighnet.fetchDeny by defaultConfined to manifest.net.domains; an empty or malformed list means no egress (§2A)
net.websockethighpi.net.websocket.connect / send / close (host-owned sockets; at most 4 per plugin, 1 MiB frames)Deny by defaultConfined to manifest.net.domains like net.fetch; a refused host never reaches the transport, and every socket is closed when the plugin unloads, is disabled, or crashes
shell.openExternalmediumOpen external linkConfirm on first usePrevents phishing links
mcp.server.localhighSpawn a transport: "stdio" MCP server declared in the manifestDeny by defaultRuns a local executable; its tools reach the agent
mcp.server.remotehighConnect a transport: "http" MCP serverDeny by defaultSends tool arguments to a third-party endpoint; non-loopback HTTP is unencrypted
background.servicemediumStart contributes.services and keep the plugin process residentConfirm at installSupervised with backoff; visible on the Plugins page
bus.publishmediumbus.publish to declared topicsConfirm at installOther plugins can act on the message
bus.subscribemediumbus.subscribe to declared patternsConfirm at installCan observe another plugin's messages
browser.cdphighpi.browser.* against the host work-panel guestConfirm at installGuest bounds are clamped to the calling plugin view; CDP is allowlisted
desktop.controlhighpi.desktop.listOperations, pi.desktop.invoke, including the reviewed session/collaboration/* operationsConfirm at installShared with the local MCP control plane's reviewed operation catalog, except for operations marked plugin-only: the six session/collaboration/* operations reach the plugin gateway but are deliberately absent from the MCP-visible catalog and have no renderer mutation channel; collaboration spawn/send additionally require an active plugin Agent tool invocation, whose source Session/turn/invocation identity is injected by the host; panel cancellation is limited to that plugin's own deliveries; a dangerous operation needs confirm: true from the plugin and the user's answer to a host-owned native dialog that names the catalog operation; the MCP bearer token and Electron channel names are never exposed
ui.microphonemediumnavigator.mediaDevices.getUserMedia({ audio: true }) inside the plugin's isolated panelConfirm at installAudio only; camera and every other device permission stay denied; no native handle or host secret reaches the plugin
audio.capture.backgroundhighpi.audio.getInputDevices, openInput, closeInput, getCaptureState, onInputFrame / offInputFrame (registered in the plugin API and gated by this permission; the two synchronous registration helpers throw the coded refusal)Deny by defaultHost owns the device; PCM16 frames only, no device handle or MediaStream. The host has no device backend yet, so an authorized call is refused with a coded UNSUPPORTED (audited); no device is opened
audio.playback.backgroundmediumpi.audio.openOutput, writeOutput, stopOutput, closeOutput (registered in the plugin API and gated by this permission)Confirm at installHost-owned playback queue, PCM16 only. The host has no device backend yet, so an authorized call is refused with a coded UNSUPPORTED (audited); no device is opened
keyboard.globalShortcutmediumpi.keyboard.registerGlobalShortcut, unregisterGlobalShortcut, listGlobalShortcuts; contributes.globalShortcutsConfirm at installHost owns Electron globalShortcut; a shortcut only runs the plugin's own command; conflicts are refused (SHORTCUT_CONFLICT / SHORTCUT_UNAVAILABLE / INVALID_ACCELERATOR / LIMIT_EXCEEDED, max 8 per plugin); released on unload/disable/crash
models.listmediumpi.models.listConfirm at installReady provider/model rows only; no secrets
project.createhighpi.project.create and explicit projectId on session importConfirm at installCreates or reuses a durable project row without activating the workspace; imported sessions remain unbound unless the id is supplied
session.readhighpi.session.getLlmContextConfirm at installIn-flight tool session only; compaction-aware projection (D019 / D336)
session.importhighpi.session.import, pi.session.importBatchConfirm at installImports only into the calling plugin's declared session sources; bounded and rate-limited
session.read.ownmediumpi.session.list, pi.session.get, pi.session.listMessagesConfirm at installReads only sessions imported by the calling plugin; no cross-plugin access
session.update.ownmediumpi.session.renameConfirm at installRenames only the calling plugin's active imported sessions
session.delete.ownhighpi.session.deleteConfirm at installTrash/purge only the calling plugin's imported sessions; rate-limited
usage.readmediumpi.usage.listTurnsConfirm at installRead-only listing of completed-turn facts (per-turn token counters and identifiers, keyset-paginated); no message body and no write path
agent.completehighpi.agent.completeConfirm at installHost-owned one-shot; spends user quota; includeSessionContext also needs session.read
speech.adapter.registerhighpi.speech.registerAdapter / unregisterAdapterConfirm at installRegisters a speech protocol. Handles stay in the guest; HTTP plans are executed by the host with the bound provider key and must stay on that origin. Built-in protocol ids are reserved

2A. A permission is the switch; the manifest carries the range ​

Two capabilities are too coarse to be answered by a name alone, so the name says whether the plugin may act and a manifest field says how far. Both fields are enforced by the host, shown to the user next to the permissions, and validated at install time.

FieldBoundsAbsent or empty means
net.domainsEvery host-owned egress path: the panel session, pi.net.fetch, remote HTTP MCP endpointsNo egress at all, whatever net.fetch says
fs.read / fs.write / fs.deleteWhich paths that file mode may touchNo standing reach; every access falls to a confirmation

Failing closed on an absent field is what makes the two safe to omit: a manifest that says nothing grants nothing. See 04-plugin-security.md §6 and §8.1, and ADR 0088.

The two are also linked. fs.read may declare the whole tree because a read only becomes a leak when the bytes can leave, and net.domains closes that half. fs.write and fs.delete are dangerous on their own, so a whole-tree pattern (**, **/*, */**, ./*) fails manifest validation for those modes.

2B. Deletion ​

fs.delete is the one file mode whose damage is not undoable by re-running the plugin, so it carries three bounds the other modes do not:

  1. Two tiers. own: true lets a plugin remove files it wrote itself — the host keeps a write ledger in the plugin's data directory — with no scope and no prompt; a file the user has modified since drops out of the ledger. Deleting anything else needs a declared scope, and out-of-scope paths ask the user.
  2. The OS trash. Removal goes through shell.trashItem, never rm, and never recursively: a non-empty directory is refused rather than emptied. The host keeps no copy of the user's data to provide this.
  3. A rate brake. 50 deletes per rolling 60s per plugin. Past it the user is asked once with the reason given as rate rather than path, because recursive: false bounds one call and not a glob plus a loop.

3. Permission dependencies ​

  • ui.panel is required to load a panel entry
  • ui.view is required to contribute work panel views; it is independent of ui.panel, so a plugin may ship docked views without a detached window
  • agent.tool.register is required to contribute agentTools
  • When fs.write is present, it is recommended to also declare fs.read
  • manifest.fs.<mode> requires the matching fs.<mode> permission; a scope nobody can use fails validation rather than being silently ignored
  • fs.requestDirectory (the userSelected root) is gated on fs.read; writing or deleting inside the chosen directory still needs fs.write / fs.delete
  • A contribution whose permission is missing fails manifest validation (themes, mcpServers, services, bus); skills is the exception and is skipped at load time instead (see 02-plugin-manifest-schema.md §7)
  • Lifecycle and state events need no permission: workspace:changed, session:modelChanged, session:turnEnded, and plugin:settingsChanged arrive on the existing plugin event channel, and subscribing to an unknown event name does not error

3A. Plan operating-state rule ​

Every agentTools contribution is denied in Plan, regardless of this matrix's risk or default policy. agent.tool.register authorizes registration for Agent, not visibility in Plan. The host returns PLUGIN_DISABLED_IN_PLAN for a direct Plan call and records the denial. Plugin tools become eligible only after the same Agent is approved into Agent mode.

4. Permission display copy ​

English is the primary copy. The zh-CN column holds the localized example strings. A file permission is never shown alone: the declared scope is rendered beside it, so "Modify the files it lists" is followed by the list.

PermissionEnglish copyzh-CN example
fs.readRead the files it lists读取它列出的文件
clipboard.readRead the current clipboard and retained history读取当前剪贴板和保留的历史
fs.writeModify the files it lists修改它列出的文件
fs.deleteDelete the files it lists, to the trash删除它列出的文件(进回收站)
notifyShow in-app and native notifications显示应用内和系统通知
agent.tool.registerProvide executable tools to the AI Agent向 AI Agent 提供可执行工具
agent.prompt.injectAdjust agent instructions调整智能体指令
agent.extensionRun code inside the agent在 agent 内运行代码
net.fetchAccess the network访问网络
shell.openExternalOpen external links打开外部链接
ui.themeProvide a theme提供主题
ui.settingsAdd a sandboxed Settings entry in Extensions在“扩展”中添加沙盒设置项
ui.window.appearanceSet the window background设置窗口背景
mcp.server.localRun a local MCP server运行本地 MCP 服务
mcp.server.remoteReach a remote MCP server连接远端 MCP 服务
background.serviceKeep a background service running保持后台服务运行
bus.publishSend messages to other plugins向其他插件发送消息
bus.subscribeReceive messages from other plugins接收其他插件的消息
browser.cdpControl the work-panel browser控制工作面板浏览器
models.listList authenticated models列出已登录的模型
session.readRead the current conversation sent to the model读取当前发给模型的对话
session.importImport bounded session history into your declared sources导入受限的会话历史到已声明的数据源
session.read.ownRead sessions imported by this plugin读取此插件导入的会话
session.update.ownRename sessions imported by this plugin重命名此插件导入的会话
session.delete.ownTrash or purge sessions imported by this plugin将此插件导入的会话移入回收站或清除
usage.readRead usage statistics读取用量统计
agent.completeRun a one-shot completion with your models用你的模型发起一次补全
speech.adapter.registerRegister a speech adapter注册语音适配器
audio.capture.backgroundUse the microphone in the background后台使用麦克风
audio.playback.backgroundPlay audio in the background后台播放声音
keyboard.globalShortcutRegister system-wide shortcuts注册系统级快捷键
net.websocketOpen real-time connections建立实时双向连接

5. Adding permissions on upgrade ​

If new permissions appear on upgrade:

  1. Compute the diff
  2. Force user confirmation
  3. If not confirmed, cancel the upgrade or disable the new capabilities (canceling the upgrade is recommended)

6. Runtime check pseudocode ​

ts
assertPermission(pluginId, perm) {
 if (!granted(pluginId, perm)) throw ERROR_PERMISSION_DENIED
}

Every Host API entry point must assert first. A file entry point then passes three more gates, in this order — a later gate can only refuse, never widen:

ts
assertFsAccess(pluginId, mode, requestedPath, sessionId) {
 assertPermission(pluginId, `fs.${mode}`)              // declared AND granted
 full = realpathWithinRoot(root(pluginId, mode, sessionId), requestedPath)
 if (!full) throw NOT_FOUND | INVALID_ARGUMENT         // symlinks resolved first
 if (isDenied(full) || isHostReserved(full)) throw ERROR_PERMISSION_DENIED
 if (!inScope(full, declaredScope(pluginId, mode))) await confirmWithUser(...)
}

The workspace root is the invoking tool session's project, falling back to the visible workspace for a panel call (ADR 0266).

7. Acceptance ​

  1. Unauthorized API calls fail
  2. Permission copy is visible in the install UI, and a file permission shows its declared scope alongside
  3. Upgrades that add permissions prompt the user
  4. A write or delete outside the declared scope prompts, and a denial is audited as PERMISSION_DENIED
  5. .env and .git/** stay unreadable under a whole-tree read scope, and do not appear in fs.glob results either
  6. A symlink inside the root pointing outside it does not carry an access out
  7. A delete lands in the OS trash, refuses a non-empty directory, and is interrupted past 50 removals in a rolling minute
  8. A plugin declaring only the legacy fs.*.workspace names loses write and delete reach, and the Plugins page says so

Local-first · Model-agnostic · Plugin-powered. AIUO.NET