How PI-Desktop Plugin Permissions and Isolation Work: A Complete Technical Guide
PI-Desktop runs every plugin in a sandboxed Node process and enforces a manifest-driven permission model that validates file system, network, and UI access at runtime through a Rust-based permission gateway.
The PI-Desktop plugin system (vastsa/PI-Desktop) combines process-level isolation with declarative security to prevent untrusted code from accessing host resources. Unlike simple allow-list approaches, the platform uses a closed-by-default architecture where every capability—from reading files to opening UI panels—must be explicitly declared in manifest.json and validated by the Rust host core before execution.
Architectural Isolation Mechanisms
PI-Desktop employs a multi-layer isolation strategy that separates plugin logic, UI rendering, and host privileges into distinct trust boundaries.
Process Isolation for Plugin Entry Code
When a plugin loads, the PluginManager (located in plugins/registry) spawns the plugin’s entry code in a dedicated Node process via apps/desktop/electron/main/plugin-runtime.mjs. This process has no direct access to the host’s SQLite database or system APIs. Instead, all calls to pi.* host APIs route through a permission gateway that intercepts requests and checks them against the plugin’s declared permissions.
Sandboxed UI Rendering
Panels and views run inside sandboxed Electron windows with Node integration disabled (apps/desktop/electron/main/plugin-panel-host.ts). The UI context cannot access Node.js APIs or Electron internals directly. Communication with the plugin backend occurs exclusively through the window.pluginBridge channel, which filters all messages through the same permission checks applied to the main process.
Host Core Authority
The Rust crate crates/host-core maintains ultimate control over sensitive operations. It owns the SQLite database, file system access, and network stack. Before delegating any request to the plugin process, the host core validates file system scopes, network allow-lists, and high-risk capabilities like shell execution or clipboard access.
Declaring PI-Desktop Plugin Permissions in manifest.json
Every plugin must declare its required capabilities in a static manifest.json file. The system evaluates these declarations using closed-by-default semantics—an absent or empty permission list grants zero access.
Permission Structure
{
"permissions": [
"ui.panel",
"fs.read",
"net.fetch"
],
"fs": {
"read": { "scope": ["docs/**", "*.md"] }
},
"net": {
"domains": ["api.example.com", "*.githubusercontent.com"]
}
}
The permissions array enumerates high-level capabilities such as ui.panel, fs.read, or agent.tool.register. For permissions supporting range constraints (like fs.* or net.*), the manifest must include corresponding scope or domain lists. The host rejects any request outside these declared boundaries, including path traversal attempts (..) or symlinks escaping the root directory.
The canonical reference for available permissions lives in spec/07-plugins/13-plugin-permissions-matrix.md, which maps every host API to its required permission string and risk level.
Runtime Permission Enforcement
When a plugin invokes a host API—such as pi.fs.readText—the system executes a three-step validation pipeline implemented in crates/host-core/src/plugins/permissions.rs:
- Lookup: The host maps the API call to its required permission identifier (e.g.,
pi.fs.readTextrequiresfs.read). - Verification: The system checks that the permission appears in the plugin’s manifest and has been granted by the user through an explicit prompt (options: allow once, always, or deny).
- Scope Validation: For scoped permissions, the host validates that the requested file path matches the
manifest.fs.read.scopeglobs or that the network hostname appears inmanifest.net.domains.
If any check fails, the host immediately throws a PERMISSION_DENIED error that propagates back to the plugin process. Plugins can catch these errors to implement fallback behavior or user notifications.
Isolation Guarantees and Threat Mitigation
The PI-Desktop plugin system addresses specific attack vectors through targeted isolation techniques:
| Threat Vector | Isolation Technique |
|---|---|
| File System Access | Path validation against declared globs; rejection of traversal sequences and restricted paths (.env*, .git/**). |
| Network Egress | net.fetch restricted to manifest.net.domains; panel-side requests inherit the same allow-list. |
| Clipboard and Shell | Explicit opt-in required (clipboard.read, shell.openExternal); APIs remain unavailable without declaration. |
| Inter-Plugin Messaging | Message bus (bus.publish, bus.subscribe) scopes by topics, but payloads are visible to any subscriber—sensitive data should never traverse this channel. |
| Agent Extensions | agent.extension runs unsandboxed inside the Agent process and requires high-risk confirmation due to potential system access. |
| Panel UI Escape | Node.js disabled in renderer process; only window.pluginBridge exposed to prevent arbitrary IPC exploitation. |
All enforcement occurs within the Rust host core before delegation, ensuring that even a compromised Node process cannot bypass scope restrictions.
Permission Lifecycle and User Consent
The permission system maintains state across the plugin lifecycle through distinct phases:
- Declaration: Static definition in
manifest.jsonat install time. - User Grant: Interactive prompts appear on first use; users may grant once, always, or deny future requests.
- Hot-Reload: Development mode changes to permissions require a full plugin reload and trigger re-prompting for user consent.
- Revocation: Disabling or uninstalling a plugin automatically invalidates all granted permissions and purges associated state from the host core.
Troubleshooting Common Permission Errors
Developers frequently encounter specific PERMISSION_DENIED scenarios that indicate manifest misconfigurations:
| Symptom | Root Cause | Resolution |
|---|---|---|
PERMISSION_DENIED on pi.fs.readText |
Missing fs.read permission or path outside declared scope |
Add "fs.read" to the permissions array and expand fs.read.scope globs to include the target path. |
| Panel fails to render | Absent ui.panel permission or invalid entry point |
Declare "ui.panel" and verify that manifest.ui.panel references a valid HTML file. |
| Network request blocked | net.fetch granted but hostname not listed |
Append the target domain (e.g., "api.example.com") or a wildcard pattern to manifest.net.domains. |
| Agent tool invisible | Missing agent.tool.register or registration failure |
Include "agent.tool.register" in permissions and ensure the tool registers during the onLoad lifecycle hook. |
Summary
- PI-Desktop isolates plugins using separate Node processes for entry code and sandboxed Electron windows for UI rendering.
- The manifest-driven permission model requires explicit declaration of all capabilities in
manifest.json, evaluated under closed-by-default semantics. - Runtime enforcement occurs in
crates/host-core/src/plugins/permissions.rs, validating user grants and scope constraints before executing sensitive operations. - File system, network, and system API access are strictly bounded by declared scopes, with hard-coded deny-lists preventing access to sensitive paths.
- Users retain control through interactive grant prompts that support temporary or permanent authorization, with automatic revocation on plugin removal.
Frequently Asked Questions
How does PI-Desktop prevent plugins from accessing files outside their declared scope?
The host core in crates/host-core/src/plugins/permissions.rs sanitizes every file path request against the fs.read.scope or fs.write.scope globs defined in manifest.json. It explicitly rejects path traversal sequences (..), resolves symlinks to ensure they remain within the allowed root, and maintains a hard-coded deny-list protecting files like .env and .git/**. If the path fails validation, the host returns a PERMISSION_DENIED error before the filesystem operation executes.
Can a plugin access the network if the user grants net.fetch but omits domains from the manifest?
No. The PI-Desktop plugin system uses closed-by-default semantics for network permissions. Even with net.fetch listed in the permissions array, the host checks the manifest.net.domains list during every pi.net.fetch call. An empty or missing domains array results in an immediate PERMISSION_DENIED error, preventing any egress communication.
What is the difference between the plugin Node process and the panel sandbox?
The plugin Node process (spawned by plugin-runtime.mjs) executes the plugin’s main logic with access to Node.js APIs but isolated from the host core via IPC. The panel sandbox (created by plugin-panel-host.ts) renders HTML/CSS in an Electron window with Node integration disabled, preventing access to require or filesystem APIs. The panel communicates with its parent plugin only through the window.pluginBridge channel, which inherits the same permission constraints enforced by the Rust host core.
Why are agent extensions considered high-risk permissions?
Agent extensions (declared via agent.extension) execute inside the Agent process without sandboxing, granting them the same privileges as the host application. Because this bypasses the Node process isolation and Electron sandbox used by standard plugins, malicious code could potentially access unrestricted system resources. Consequently, PI-Desktop treats agent.extension as a high-risk permission requiring explicit user confirmation beyond standard capability grants.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →