✨ Ask users to allow global plugins on install

A plugin with "scope": "global" keeps running across the dashboard
and files and can find and open the user's projects and files. Any
manifest could ask for this without the user being told.

Add the allow:global permission. The plugin registry adds it to
every manifest with global scope, so the install dialog lists it,
and a plugin that becomes global in a later version shows the
permissions update dialog again. The permission is valid in the
shared schema, which the backend checks when saving, and in the
runtime manifest schema. The built-in MCP plugin declares it too.

Document the scope property and the new permission for plugin
developers.
This commit is contained in:
alonso.torres 2026-10-07 13:24:12 +02:00
parent a381761aa4
commit dfff188409
11 changed files with 73 additions and 10 deletions

View File

@ -47,6 +47,17 @@
(t/is (= [plugin-id-1] (:ids plugins)))
(t/is (= valid-plugin (get-in plugins [:data plugin-id-1]))))))
(t/deftest add-profile-plugin-accepts-global-plugins
(let [profile (th/create-profile* 1)
plugin (assoc valid-plugin
:scope "global"
:permissions #{"content:read" "allow:global"})
out (th/command! {::th/type :add-profile-plugin
::rpc/profile-id (:id profile)
:plugin plugin})]
(t/is (nil? (:error out)))
(t/is (= plugin (:result out)))))
(t/deftest add-profile-plugin-rejects-invalid-permissions
(let [profile (th/create-profile* 1)
plugin (assoc valid-plugin :permissions #{"content:read" "admin:delete"})

View File

@ -35,11 +35,12 @@
"clipboard:read" "clipboard:write"
"user:read"
"allow:downloads"
"allow:localstorage"})
"allow:localstorage"
"allow:global"})
(def schema:permissions
"Schema for plugin permissions - a set of valid permission strings."
[:set {:gen/max 11} (into [:enum] (sort valid-permissions))])
[:set {:gen/max 12} (into [:enum] (sort valid-permissions))])
(def max-plugins
"Maximum number of plugins a profile can hold."

View File

@ -160,6 +160,7 @@ Set <code class="language-js">"version": 2</code> in your
- **Code**: your plugin's file location. It needs to be compiled to JavaScript and reachable.
- **Icon**: your plugin's icon, which will be also displayed in the plugin manager modal. It'll be a <code class="language-js"><img src=""></code> tag so you can use whichever image format works better for you. **It's recommended to use a 56x56 pixel icon for the best appearance on all devices**.
- **Permissions**: your plugin's permissions, which allow access to different parts of the Penpot API.
- **Scope** (optional): <code class="language-js">"workspace"</code> (the default) or <code class="language-js">"global"</code>. A workspace plugin closes when its file closes. A global plugin keeps running while the user moves between the dashboard and files, and gets the <code class="language-js">penpotMgmt</code> object to find, create and open projects and files. Global scope adds the <code class="language-js">allow:global</code> permission, which the user must accept.
#### Types of permissions
@ -185,6 +186,9 @@ Typical use cases: downloading the full project file for backup or sharing.
- <code class="language-js">allow:localstorage</code>: Allows the access to the local storage proxy to store information. This info is only available for the plugin installation but be aware that a user can see this information in the browser.
Typical use cases: storing authentication tokens for a plugin login
- <code class="language-js">allow:global</code>: Allows the plugin to keep running while the user moves between the dashboard and files, and to find and open their projects and files. Added automatically when the manifest sets <code class="language-js">"scope": "global"</code>.
Typical use cases: assistants or integrations that work across several files.
_Note: Write permissions automatically includes its corresponding read permission (e.g., <code class="language-js">content:write</code> includes <code class="language-js">content:read</code>) because reading is required to perform write or modification actions._
### What are plugin.ts and plugin.js files?

View File

@ -42,7 +42,8 @@
#{"user:read"
"library:read" "library:write"
"comment:read" "comment:write"
"content:write" "content:read"}})
"content:write" "content:read"
"allow:global"}})
(defonce interval-sub
(atom nil))

View File

@ -331,6 +331,13 @@
[:p {:class (stl/css :permissions-list-text)}
(tr "workspace.plugins.permissions.allow-localstorage")]])
(cond
(contains? permissions "allow:global")
[:div {:class (stl/css :permissions-list-entry)}
deprecated-icon/oauth-1
[:p {:class (stl/css :permissions-list-text)}
(tr "workspace.plugins.permissions.allow-global")]])
(cond
(contains? permissions "clipboard:write")
[:div {:class (stl/css :permissions-list-entry)}

View File

@ -54,15 +54,20 @@
"Read the manifest.json defined by the plugins definition and transforms it into an
object that will be stored in the register."
[plugin-url ^js manifest]
(let [name (obj/get manifest "name")
desc (obj/get manifest "description")
code (obj/get manifest "code")
icon (obj/get manifest "icon")
vers (d/nilv (obj/get manifest "version") 1)
(let [name (obj/get manifest "name")
desc (obj/get manifest "description")
code (obj/get manifest "code")
icon (obj/get manifest "icon")
vers (d/nilv (obj/get manifest "version") 1)
scope (obj/get manifest "scope")
permissions (into #{} (obj/get manifest "permissions" []))
permissions
(cond-> permissions
;; Global plugins must be explicitly allowed by the user
(= "global" scope)
(conj "allow:global")
(contains? permissions "content:write")
(conj "content:read")
@ -102,7 +107,7 @@
{:plugin-id plugin-id
:url (str plugin-url)
:version vers
:scope (obj/get manifest "scope")
:scope scope
:name name
:description desc
:host origin

View File

@ -320,3 +320,24 @@
(t/is (= [true false] @seen) "no calls after unsubscribing")
(done'))
done))))
;; --- parse-manifest ---
(t/deftest parse-manifest-adds-allow-global-for-global-scope
(let [manifest (preg/parse-manifest
"http://localhost:4400/manifest.json"
#js {:name "Global plugin"
:code "plugin.js"
:scope "global"
:permissions #js ["content:read"]})]
(t/is (= "global" (:scope manifest)))
(t/is (= #{"content:read" "allow:global"} (:permissions manifest)))))
(t/deftest parse-manifest-keeps-workspace-plugins-without-allow-global
(let [manifest (preg/parse-manifest
"http://localhost:4400/manifest.json"
#js {:name "Workspace plugin"
:code "plugin.js"
:permissions #js ["content:read"]})]
(t/is (nil? (:scope manifest)))
(t/is (= #{"content:read"} (:permissions manifest)))))

View File

@ -8180,6 +8180,12 @@ msgstr ""
msgid "workspace.plugins.permissions.allow-download"
msgstr "Start file downloads."
#: src/app/main/ui/workspace/plugins.cljs:339
msgid "workspace.plugins.permissions.allow-global"
msgstr ""
"Keep running while you move between the dashboard and your files, and find "
"and open your projects and files."
#: src/app/main/ui/workspace/plugins.cljs:332
msgid "workspace.plugins.permissions.allow-localstorage"
msgstr "Store data in the browser."

View File

@ -8276,6 +8276,12 @@ msgstr ""
msgid "workspace.plugins.permissions.allow-download"
msgstr "Comenzar descargas de ficheros."
#: src/app/main/ui/workspace/plugins.cljs:339
msgid "workspace.plugins.permissions.allow-global"
msgstr ""
"Seguir funcionando mientras te mueves entre el panel y tus archivos, y "
"buscar y abrir tus proyectos y archivos."
#: src/app/main/ui/workspace/plugins.cljs:332
msgid "workspace.plugins.permissions.allow-localstorage"
msgstr "Guardar datos en el navegador."

View File

@ -3,7 +3,7 @@
### 🚀 Features
- **plugin-types, plugins-runtime:** Added `penpotMgmt.createProject` and `penpotMgmt.createFile` to create projects and files from the dashboard or workspace with `content:write` permission.
- **plugins-runtime, plugin-types**: Added global plugins (`scope: "global"`) that remain active across dashboard and workspace navigation until closed or logged out, with `penpotMgmt` for project and file discovery, file navigation and workspace events.
- **plugins-runtime, plugin-types**: Added global plugins (`scope: "global"`) that remain active across dashboard and workspace navigation until closed or logged out, with `penpotMgmt` for project and file discovery, file navigation and workspace events. Global scope adds the `allow:global` permission, which users must accept when installing the plugin.
- **plugin-types:** Added `paddingType` (`'simple' | 'multiple'`) to flex and grid layouts and `marginType` (`'simple' | 'multiple'`) to layout children, exposing whether the four padding/margin sides are mirrored or honoured independently.
- **plugin-types**: Added `waitForLayoutUpdate` to wait until pending layout updates have finished. It rejects when the optional timeout elapses, defaulting to 30 seconds so a wait never hangs.
- **plugin-types**: Added `waitForLayoutUpdate` to the `Shape` interface to wait until the pending layout updates of a shape and its children have finished

View File

@ -20,6 +20,7 @@ export const manifestSchema = z.object({
'comment:write',
'allow:downloads',
'allow:localstorage',
'allow:global',
'clipboard:read',
'clipboard:write',
]),