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:
- Only
baseplugins load — allcommunityanduserplugins are forcibly excluded - Related shortcuts and commands are also isolated
- 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(context, payload);
} catch (error) {
console.error(`[OpenGPEX Error] Command 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 (GPEX_CLOUD_MODE=true):
- ❌ Plugin upload API (
/api/plugins/upload) returns 403 Forbidden - ❌ Upload UI button is physically hidden
- ✅ Pre-installed
baseandcommunityplugins function normally - ✅ This prevents untrusted code from executing in the shared cloud environment
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:
- UID override: Computed fresh from manifest (prevents hijacking existing plugins)
- sourceType = 'user': Cannot masquerade as
baseorcommunity - 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
- Don't abuse Error Boundaries — Handle known failure cases (network, JSON parse) with your own try/catch and show toast notifications
- Clean up event listeners — Always remove
window.addEventListenerin useEffect cleanup functions - Isolate side effects — Don't modify state outside your plugin's scope (
state.frames, other plugins' config) - Use
try...finallyfor async commands — Always callctx.scoped.setBusy(false)in the finally block
Next Steps
- Plugin System Overview — Trust model architecture
- Plugin Packaging — Distribution requirements
- Plugin API Reference — Complete hook documentation
Last updated: 2026-07-30