Permissions & trust model
Permissions make a plugin's intended access visible before it runs.
| Permission | Declares that the plugin… | Capability detail |
|---|---|---|
asset.read | reads selected assets (required for asset placeholders) | filesystem.read contains selected_asset |
library.read | reads the library folder (required for {library.path}) | filesystem.read contains library_root |
filesystem.read | reads other files the user points it at | filesystem.read contains user_selected |
filesystem.write | creates or modifies files | filesystem.write lists where |
network | makes network connections or asks the UI bridge to fetch remote images | network.destinations lists every origin, purpose, and uploaded data category |
process.launch | starts other programs | process.launch lists the targets; browser.origins lists sites for default_browser |
The network permission is not theoretical: network-backed plugins run today, and the UI bridge can fetch HTTPS raster previews from declared origins. Declare it, and list the origins, whenever your entry or interface depends on remote data.
Internet access is deny-by-default
Declaring a destination does not grant it. For every declared origin the user holds one of three decisions, made in a prompt that shows the plugin name, publisher, the exact origin, its declared purpose, and the data categories it uploads:
- Deny — the request fails with
network_permission_denied. - Allow once — exactly one request proceeds; the next one asks again.
- Always allow — requests to that origin proceed until the grant is reset.
Grants are bound to the plugin's approval hash. Re-approval (after any edit or update) drops them and every origin returns to ask. Users can also return every origin to ask from the row menu in Plugin Center → Installed (Reset internet access), and the local API exposes the same state through GET /api/plugins/<id>/permissions, POST …/permissions/decision, and POST …/permissions/revoke. Requests to an origin that is not declared fail with network_destination_not_declared and never prompt.
Consented requests go through Oruva's broker: HTTPS only, the DNS answer is validated as a public address and pinned for the TLS connection, at most three redirects each re-validated under the same rules, methods limited to GET, HEAD, POST, and PUT, request and response bodies capped at 8 MiB, and 20 seconds per request. Ambient proxies and credentials are stripped, and the plugin never sees Oruva's backend token.
browser.origins uses the same prompt with a separate decision per site (purpose Open this declared website in the default browser). The UI bridge's oruva-plugin:open-url message is the only way a plugin interface can open a website.
Honesty note, which we also publish to users: Plugin API v1 validates and displays these declarations, refuses undeclared placeholder access, and gates the UI bridge's network and browser requests behind the prompts above — but it does not yet enforce an operating-system sandbox around the plugin process. An approved plugin runs with the current user's operating-system authority, and the broker mediates only what goes through the bridge; a Python entry that opens its own sockets is constrained by its declarations as a promise the user approved, not by a kernel boundary. The allow-scripts iframe sandbox protects the app UI; it does not sandbox the Python script or executable. Declare what you actually do. Catalog and package signatures protect what you install; they do not sandbox what runs.