diff --git a/backend/test/backend_tests/rpc_plugins_test.clj b/backend/test/backend_tests/rpc_plugins_test.clj
index 8c57931844..8d6be84dc6 100644
--- a/backend/test/backend_tests/rpc_plugins_test.clj
+++ b/backend/test/backend_tests/rpc_plugins_test.clj
@@ -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"})
diff --git a/common/src/app/common/types/plugins.cljc b/common/src/app/common/types/plugins.cljc
index b9a1ca5330..8db72497fd 100644
--- a/common/src/app/common/types/plugins.cljc
+++ b/common/src/app/common/types/plugins.cljc
@@ -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."
diff --git a/docs/plugins/getting-started.md b/docs/plugins/getting-started.md
index ea993640a9..5f72ac4ada 100644
--- a/docs/plugins/getting-started.md
+++ b/docs/plugins/getting-started.md
@@ -160,6 +160,7 @@ Set "version": 2 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 ![]()
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): "workspace" (the default) or "global". 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 penpotMgmt object to find, create and open projects and files. Global scope adds the allow:global 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.
- allow:localstorage: 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
+- allow:global: 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 "scope": "global".
+Typical use cases: assistants or integrations that work across several files.
+
_Note: Write permissions automatically includes its corresponding read permission (e.g., content:write includes content:read) because reading is required to perform write or modification actions._
### What are plugin.ts and plugin.js files?
diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs
index f3e3817239..e4b03fb956 100644
--- a/frontend/src/app/main/data/workspace/mcp.cljs
+++ b/frontend/src/app/main/data/workspace/mcp.cljs
@@ -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))
diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs
index a6ff1b34f0..12bd45e766 100644
--- a/frontend/src/app/main/ui/workspace/plugins.cljs
+++ b/frontend/src/app/main/ui/workspace/plugins.cljs
@@ -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)}
diff --git a/frontend/src/app/plugins/register.cljs b/frontend/src/app/plugins/register.cljs
index fc709427d4..8b5bcc3bde 100644
--- a/frontend/src/app/plugins/register.cljs
+++ b/frontend/src/app/plugins/register.cljs
@@ -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
diff --git a/frontend/test/frontend_tests/plugins/register_test.cljs b/frontend/test/frontend_tests/plugins/register_test.cljs
index 55ddd1c4a6..49c387d4bc 100644
--- a/frontend/test/frontend_tests/plugins/register_test.cljs
+++ b/frontend/test/frontend_tests/plugins/register_test.cljs
@@ -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)))))
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index d71621ab02..18000c059e 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -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."
diff --git a/frontend/translations/es.po b/frontend/translations/es.po
index c1464fe980..03bac196ed 100644
--- a/frontend/translations/es.po
+++ b/frontend/translations/es.po
@@ -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."
diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md
index ab324f891c..041d252969 100644
--- a/plugins/CHANGELOG.md
+++ b/plugins/CHANGELOG.md
@@ -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
diff --git a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts
index aefc563c65..357fe9f95f 100644
--- a/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts
+++ b/plugins/libs/plugins-runtime/src/lib/models/manifest.schema.ts
@@ -20,6 +20,7 @@ export const manifestSchema = z.object({
'comment:write',
'allow:downloads',
'allow:localstorage',
+ 'allow:global',
'clipboard:read',
'clipboard:write',
]),