10 Commits

Author SHA1 Message Date
alonso.torres
cf1fbb4d7c 🐛 Close plugins whose load is cancelled before they run
Unloading or replacing a plugin while its code was still being
fetched could leave a late instance running, or block reopening a
plugin that closed during startup.

Each load now carries an AbortController. Unloading, a newer load
of the same plugin or logout aborts it, and the plugin manager
closes itself on abort, also while it fetches its code.
createPlugin returns nothing for a plugin closed before its
sandbox exists, and failed loads clear their pending state so the
plugin can be loaded again.
2026-10-07 14:58:32 +02:00
Dominik Jain
036f59266b 🐛 Discard cancelled plugin loads after unloading
Cause:
- Disabling MCP during script loading could leave a late plugin instance
  attached to a stopped controller, blocking connection after re-enabling.

Fix:
- Track pending loads and discard instances cancelled by unload.
- Prevent duplicate global loads and preserve newer replacement instances.
- Clear pending state after failures so a subsequent load can retry.

Validation:
- Pass 84 runtime tests, the runtime build, lint and formatting checks.
- Reproduce the race with a delayed script in the browser, then verify
  re-enabling restores the session ID, themed indicator and MCP execution.

AI-assisted-by: gpt-6
Signed-off-by: Dominik Jain <dominik.jain@oraios-ai.de>
2026-10-07 14:58:32 +02:00
Dominik Jain
be17fabc6a 🎉 Introduce global plugins and move MCP to application scope
Global plugin lifecycle:
- Add optional manifest scope "global" for plugins that remain active
  across dashboard and workspace navigation within one browser tab.
  Existing plugins retain workspace scope by default.
- Preserve global sandboxes when a workspace ends or another plugin
  loads, including when that other plugin fails to initialize.
- Avoid duplicate instances of an already-running global plugin.
- Close global plugins on explicit closure or logout, with idempotent
  cleanup of listeners, timers and UI resources. Global scope does not
  imply automatic startup or persistence across a browser reload.

Plugin API and navigation:
- Keep a permanently live penpot facade whose getters follow the current
  workspace, while user, theme and UI facilities remain available on
  the dashboard.
- Expose penpotMgmt to global plugins with workspace metadata,
  none/loading/ready status and workspacechange subscriptions.
- Add awaitable openFile(fileId, {teamId}) navigation in the current tab,
  defaulting to the current team and resolving only once the requested
  file and its active page are ready.
- Reject invalid identifiers, failed or superseded navigation and waits
  exceeding 30 seconds; release navigation watchers on completion.
- Guard workspace-bound operations while no workspace is ready and
  return null from root, currentFile and currentPage in that state.
- Require content:read for the initial management API. Callers must
  reacquire file, page and shape references after changing files.

MCP lifecycle and connection controls:
- Make MCP a global plugin and initialize its integrated controller per
  authenticated application session instead of per workspace.
- Load the enabled integration with a valid token on the dashboard as
  well as in a file, retaining its existing hidden-iframe implementation.
- Keep enabling and connecting separate: an enabled plugin starts idle,
  and each tab connects explicitly through dashboard or workspace
  controls.
- Preserve the iframe, WebSocket, heartbeat and reconnect machinery
  during navigation; disconnect on request and close on disable/logout.

MCP sessions and execution:
- Derive the short MCP session ID solely from currentUser.sessionId,
  excluding the file ID. Retain the SHA-256-based, 50-bit Base32 encoding
  to produce a stable, copyable 10-character identifier.
- Allow sessions without an open file and update workspace metadata over
  the existing connection without replacing its routing identity.
- Refresh context after the connection handshake so initialization does
  not leave stale file metadata.
- Expose penpotMgmt to execute_code, preserve execution storage across
  navigation and skip layout waits when no workspace is ready.
- Update API types, tool guidance, documentation and the changelog.

Validation:
- Cover authenticated startup, workspace survival, logout cleanup,
  readiness, navigation failure, duplicate loads and session routing.
- Verify that short IDs remain stable across files and reconnects and
  that disconnect cancels pending asynchronous ID initialization.
- Pass 16 focused frontend tests, 73 runtime tests, 30 MCP plugin tests,
  builds, TypeScript checks and frontend/runtime lint.
- Pass all 73 MCP server tests with --test-force-exit; the normal runner
  still stays alive after assertions finish.
- Verify a live dashboard-to-file-to-dashboard round trip without a
  document or iframe reload, preserving facade identity and storage.

AI-assisted-by: gpt-6
2026-10-07 14:58:32 +02:00
breken
b41445d092
🐛 Keep background plugins registered when another plugin opens (#11929)
Loading a plugin closed every non-background plugin and then emptied the
runtime registry, dropping background plugins (allowBackground, such as
the MCP plugin) that were left running. The registry is what routes a
plugin iframe's postMessage traffic to its plugin and what ɵunloadPlugin
searches, so after the user opened any other plugin the MCP plugin never
received task requests from its UI (MCP tool calls timed out) and could
no longer be unloaded.

Keep background plugins in the registry and drop only the plugins that
were closed.

AI-assisted-by: claude-opus-5.5

Signed-off-by: breken-ai <312387581+breken-ai@users.noreply.github.com>
Co-authored-by: breken-ai <312387581+breken-ai@users.noreply.github.com>
2026-10-05 20:17:04 +02:00
makesomethingshit
efa2518fe8
🐛 Fix deep-harden of host plugin context on load (#11521)
* 🐛 Fix deep-harden of host plugin context on load

ses.harden(context) in loadPlugin deep-freezes every host-owned object
and function reachable through the context. The host keeps needing to
modify those across page navigation (listener wrappers, proxies), so a
later property augmentation (e.g. assigning toString) throws
'TypeError: Cannot assign to read only property toString' and kills the
MCP session (penpot/penpot#11001).

Pass the host context through untouched; sandbox isolation stays at the
compartment boundary (hardened sandbox-owned globals + ses.safeReturn).

Regression test: load-plugin-context.spec.ts (no ses mock).

AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Add real SES bootstrap to host-context regression test

The previous load-plugin-context.spec.ts had no SES bootstrap, so it
failed on the original code with 'ReferenceError: harden is not defined'
instead of the intended freeze assertion, and passed on the fixed code
merely by avoiding ses.harden.

Now the spec bootstraps real SES (repairIntrinsics + hardenIntrinsics),
adds a control test proving real ses.harden deep-freezes host-owned
functions (Object.isFrozen === true, later toString assignment throws
TypeError - the #11001 crash signature), and keeps the regression test
asserting loadPlugin leaves host functions unfrozen and patchable.

AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Add production-order hardening contrast evidence

Proves the initialization-ordering hazard behind #11001 (cf. #8636):
in production, index.ts runs repairIntrinsics only at module load while
hardenIntrinsics runs later in createSandbox. The original loadPlugin
called ses.harden(context) between those steps, freezing the shared
Function.prototype with plain data properties so later override taming
is skipped and any subsequent fn.toString assignment throws TypeError.

Kept in a separate spec file so the full SES bootstrap in
load-plugin-context.spec.ts cannot mask the ordering effect.

AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Apply approved lint fix and CHANGELOG entry

Restores the two approved deliverables missing from the previous push:
the prefer-rest-params fix in load-plugin-harden-order.spec.ts
(replacing the deprecated arguments usage) and the plugins-runtime
CHANGELOG entry for the host-context harden fix (#11001).

AI-assisted-by: muse-spark-1.3

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Remove deep-hardening of host plugin context on load

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>

* 🐛 Align CHANGELOG and context comment with reviewed evidence

AI-assisted-by: multica-agent
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>

* 🔥 Remove SES semantic tests from plugin regression coverage

Drop the tests that only verify SES library semantics rather than Penpot
application behavior:

- Delete load-plugin-harden-order.spec.ts (pure SES initialization-order
  evidence, never calls loadPlugin).
- Remove the ses.harden control test and its SES bootstrap setup from
  load-plugin-context.spec.ts.
- Remove the #8636 hardening-order contrast test and the now-unused ses
  import from load-plugin-real-path.spec.ts.

Keep the application-level regression coverage: the real loadPlugin
initialization path, permission enforcement, host-context isolation and
safeReturn protection. No production code changes.

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
AI-assisted-by: Omen Alpha

---------

Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-08 12:32:45 +02:00
Andrey Antukh
7e1139b906
🐛 Validate origin and route messages to sender in plugin postMessage channel (#10970)
* 🐛 Fix plugin postMessage channel allowing cross-plugin message injection

The global postMessage listener was broadcasting incoming messages to all
loaded plugins without validating the origin or routing to the correct
sender. This allowed any plugin (or any iframe from any origin) to inject
messages into other plugins.

- Added origin validation — messages from origins other than
  window.location.origin are rejected.
- Added sender-based routing — a message is only delivered to the plugin
  whose iframe contentWindow matches event.source.
- Exposed iframeWindow getters in PluginManager, PluginModalElement, and
  createPlugin so the runtime can compare event.source against the correct
  iframe reference.
- Updated documentation examples to include origin validation and
  recommend window.location.origin over '*' for postMessage targetOrigin.

AI-assisted-by: qwen3.7-plus

* 🐛 Fix plugin origin check breaking cross-origin plugin messaging

The origin check added in the previous commit compared event.origin
against window.location.origin (Penpot own origin). Since plugins
are cross-origin by design (hosted on the plugin author domain),
this check rejected every legitimate message from every real plugin.

The event.source-based sender routing (matching iframeWindow identity)
is the correct and sufficient security mechanism - it cannot be forged
cross-origin, so the redundant origin check was removed.

- Removed event.origin check from load-plugin.ts message listener
- Updated tests to use realistic plugin origins (localhost:4202/4203)
  and to verify rejection based on source identity, not origin
- Fixed documentation examples: use event.source for receiving
  validation and '*' for postMessage targetOrigin

AI-assisted-by: mimo-v2.5-pro
2026-09-07 09:31:35 +02:00
Andrey Antukh
f7e1bcf87f
🐛 Handle plugin errors gracefully without crashing the UI (#8810)
* 🐛 Handle plugin errors gracefully without crashing the UI

Plugin errors (like 'Set is not a constructor') were propagating to the
global error handler and showing the exception page. This fix:

- Uses a WeakMap to track plugin errors (works in SES hardened environment)
- Wraps setTimeout/setInterval handlers to mark errors and re-throw them
- Frontend global handler checks isPluginError and logs to console

Plugin errors are now logged to console with 'Plugin Error' prefix but
don't crash the main application or show the exception page.

Signed-off-by: AI Agent <agent@penpot.app>

* ✨ Improved handling of plugin errors on initialization

* ✨ Fix test and linter

---------

Signed-off-by: AI Agent <agent@penpot.app>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-04-01 11:37:27 +02:00
Alonso Torres
c32a336c50
🎉 Add MCP plugin embedded execution (#8368)
* ✨ Add core changes for mcp server

* ✨ Changes to plugins-runtime to add mcp extensions

* ✨ Changes to MCP plugin

* ✨ Changes post-review and ci fixes
2026-02-17 09:18:46 +01:00
Juanfran
96b682aa12 ♻️ Remove Nx and rely on pnpm monorepo features 2026-02-10 08:29:24 +01:00
Andrey Antukh
ec1af4ad96 🎉 Import penpot-plugins repository
As commit 819a549e4928d2b1fa98e52bee82d59aec0f70d8
2025-12-30 14:56:15 +01:00