From dfff188409799865d6b05151d43e8c55af813c31 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 7 Oct 2026 13:24:12 +0200 Subject: [PATCH] :sparkles: 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. --- .../test/backend_tests/rpc_plugins_test.clj | 11 ++++++++++ common/src/app/common/types/plugins.cljc | 5 +++-- docs/plugins/getting-started.md | 4 ++++ frontend/src/app/main/data/workspace/mcp.cljs | 3 ++- .../src/app/main/ui/workspace/plugins.cljs | 7 +++++++ frontend/src/app/plugins/register.cljs | 17 +++++++++------ .../frontend_tests/plugins/register_test.cljs | 21 +++++++++++++++++++ frontend/translations/en.po | 6 ++++++ frontend/translations/es.po | 6 ++++++ plugins/CHANGELOG.md | 2 +- .../src/lib/models/manifest.schema.ts | 1 + 11 files changed, 73 insertions(+), 10 deletions(-) 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', ]),