Launch OpenGPEX

OpenGPEX 1.x documentation (legacy editor).

v2 Docs

Security & Isolation

OpenGPEX enforces a layered security model to protect users from malicious or buggy plugins while preserving the flexibility of the plugin system.


Three-Tier Safety Net

Tier 1: Safe Mode (URL Escape Hatch)

If a plugin causes the editor to crash on startup, users can bypass it:

http://localhost:3000/?safe-mode=true

In Safe Mode:

  • All official core plugins (base + community) still load — Safe Mode excludes every user plugin
  • Both static user plugins and dynamically uploaded ZIP plugins are skipped entirely (usePluginInit never registers them), so their commands and shortcuts are absent
  • Console outputs ⚠️ [OpenGPEX Safe Mode] warning
  • User can then disable/remove the problematic plugin

Tier 2: UI Error Boundary

Every plugin component is wrapped in <PluginErrorBoundary>:

  • If a plugin throws during React render, only that plugin crashes
  • The rest of the editor (canvas, toolbars, other plugins) continues normally
  • The crashed area shows a warning icon with the plugin ID
  • User can save work and disable the plugin from GPEX Hub

Tier 3: Command Execution Isolation

All command execution is wrapped in try/catch within the core dispatcher:

// In useEditorStore.ts — executeCommand
try {
  return command.execute(scopedContext, payload);
} catch (error) {
  console.error(`[OpenGPEX Error] Command Execution Failed (${id}):`, error);
  return undefined; // Prevents crash propagation
}

Interceptors (beforeExecute) are also independently try/caught.


Trust Levels (sourceType)

Level Meaning Capabilities
base Official core plugins Full access, cannot be disabled
community Verified third-party Full access, user can disable
user User-installed Sandboxed metadata, user can remove

Cloud Mode Restrictions

When running in GPEX-Cloud SaaS mode (NEXT_PUBLIC_GPEX_CLOUD_MODE=true, or server-side GPEX_CLOUD_MODE=true — see core/helpers/config.ts):

  • ❌ Plugin upload API (/api/plugins/upload) returns 403 Forbidden
  • ❌ Dynamic plugin loading is skipped entirely (usePluginInit never calls loadUserPlugins() in cloud mode)
  • ❌ Upload UI button is physically hidden (IS_CLOUD_MODE gate in GPEX Hub panel)
  • ✅ Pre-installed base and community plugins function normally
  • ✅ This prevents untrusted code from executing in the shared cloud environment

Upload Defenses (Server Side)

The /api/plugins/upload route applies layered validation before anything touches disk:

Defense Behavior
Manifest validation ZIP must contain plugin.manifest (or plugin.json) parseable as JSON, with both id and author — otherwise 400
Namespace blocklist id starting with base. or com.gpex.plugins.base. is rejected (400)
Zip Slip check Every entry is normalized; paths containing .. or absolute paths are rejected, and the final write path is re-validated to stay inside the plugins directory
Flat/nested extraction ZIPs without a shared root folder are extracted into a sandbox folder derived from the plugin's local ID
Static conflict detection If an uploaded plugin collides with a statically registered user plugin UID, the upload succeeds and the conflicting static version is auto-disabled

Uploaded plugins are persisted under PERSISTENT_PLUGINS_DIR (a compile-time constant, data/plugins/user, in core/helpers/config.ts).


Namespace Protection

Rule Enforcement
base.* namespace reserved Packager refuses to build; upload API rejects
Dynamic plugins get forced sourceType: 'user' Loader overwrites any attempts to impersonate base
UID computed from author + id Prevents two plugins from claiming the same namespace

Metadata Sandboxing (Dynamic Plugins)

When a ZIP plugin is dynamically loaded, the system forces:

  1. UID override: Computed fresh from manifest (prevents hijacking existing plugins)
  2. sourceType = 'user': Cannot masquerade as base or community
  3. group = 'gpex.plugins.user.': Forced into user namespace group

Plugin Configuration Access

  • Plugins access their own config via usePluginSelfConfig() (zero-argument)
  • Reading another plugin's config is possible via usePluginConfig(uid) but modifying it directly is discouraged
  • Cross-plugin interaction should flow through:
    • Command Bus — Explicit, auditable actions
    • Public Signals — Read-only state indicators for consumers

Runtime Environment

Plugins run in the same JavaScript context as the core engine (no iframe or Worker sandboxing). Isolation is enforced by convention:

  • Accessing DOM outside the plugin's slot → discouraged
  • Importing store internals directly → discouraged
  • Network access (fetch/XHR) → allowed (standard browser sandbox applies)
  • Local file system access → browser-restricted (requires user prompt)

Best Practices for Plugin Developers

  1. Don't abuse Error Boundaries — Handle known failure cases (network, JSON parse) with your own try/catch and show toast notifications
  2. Clean up event listeners — Always remove window.addEventListener in useEffect cleanup functions
  3. Isolate side effects — Don't modify state outside your plugin's scope (state.frames, other plugins' config)
  4. Use try...finally for async commands — Always call ctx.scoped.setBusy(false) in the finally block

Next Steps


Last updated: 2026-07-30