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 hangs or crashes the editor at startup, users can bypass it:
http://localhost:3000/?safe-mode=true
In Safe Mode the engine loads only the official core registry (base + community). All statically registered user plugins and all dynamic plugin loading are skipped, along with their shortcuts and commands. The console prints [OpenGPEX Safe Mode] so the user can identify and remove the offending plugin.
Version Gate
At registration (usePluginInit.ts), manifest.requirements.coreVersion is checked against the running engine (CORE_VERSION):
const requiredCore = rawPlugin.manifest?.requirements?.coreVersion;
if (requiredCore && !satisfiesCoreVersion(requiredCore)) {
console.warn(
`[PluginInit] Plugin "${uid}" requires core ${requiredCore}, ` +
`but current core is ${CORE_VERSION}. Plugin will not be loaded.`
);
return;
}
The gate rejects the plugin (it is never registered). Supported formats: >=x.y.z, ^x.y.z, and bare x.y.z (parsed by satisfiesCoreVersion in core/plugin/version.ts — unparseable requirements pass in lenient mode). Declare coreVersion: '>=2.0.0' for plugins that rely on v2-only APIs (the v2 AssetService, presets, WebGPU-era pixel services).
Tier 2: UI Error Boundary
Every plugin mount point is wrapped in <PluginErrorBoundary>:
- If a plugin throws during React render, only that plugin crashes
- The canvas, toolbars, and other plugins continue normally
- The crashed region shows a warning placeholder with the plugin ID
- The user can save work and disable the plugin from the Hub
Tier 3: Command Execution Isolation
All command execution and interceptors are individually try/caught in the core dispatcher:
// Interceptor isolation (per-plugin, in the core dispatcher)
try {
const scopedContext = createScopedContext(plugin.uid, plugin.initialConfig);
const isConsumed = commandInterceptor.beforeExecute(id, scopedContext);
if (isConsumed === true) return undefined as R;
} catch (error) {
console.error(`[OpenGPEX Error] Interceptor Failed in Plugin ${plugin.uid}:`, error);
}
// Command isolation
try {
return command.execute(scopedContext, payload as never) as R;
} catch (error) {
console.error(`[OpenGPEX Error] Command Execution Failed (${id}):`, error);
return undefined as R; // no crash propagation
}
Trust Levels (sourceType)
| Level | Meaning | Capabilities |
|---|---|---|
base |
Official core plugins | Full access, cannot be disabled |
community |
Verified third-party, pre-installed | Full access, user can disable |
user |
User-installed | Sandboxed metadata, user can remove |
Metadata Sandboxing (Dynamic Plugins)
When a ZIP plugin is dynamically imported, the loader forcibly rewrites and isolates its metadata:
- UID recomputed from
author + "." + id— prevents hijacking an existing plugin's namespace sourceType = 'user'— a dynamic plugin can never masquerade asbaseorcommunitygroup = 'gpex.plugins.user.'— forced into the user group for slot grouping and policy
Cloud Mode Restrictions
When running in SaaS mode (GPEX_CLOUD_MODE=true):
/api/plugins/uploadreturns 403 Forbidden (backend physical block)- The upload UI is removed entirely
- Dynamic plugin loading is disabled in addition to upload
- Pre-installed
baseandcommunityplugins function normally
This prevents untrusted code from entering the shared multi-tenant environment. Self-host deployments (GPEX_CLOUD_MODE=false) get the full upload/hot-plug workflow; uploaded plugins are persisted to the fixed data/plugins/user directory (PERSISTENT_PLUGINS_DIR in core/helpers/config.ts).
Upload Defense Layers
| Defense | Check | Result |
|---|---|---|
| Cloud Mode | GPEX_CLOUD_MODE=true |
403 Forbidden |
| Manifest | Missing/unparseable plugin.manifest/plugin.json, or missing id/author |
400 Bad Request |
| Namespace | id (lowercased) starts with base. or com.gpex.plugins.base. |
400 Bad Request |
| Zip Slip | Normalized entry path contains .. or is absolute |
400 Bad Request, extraction aborted |
| Static conflict | Same UID as a static user plugin | Allowed, returns conflictingStaticUid (static version auto-disabled) |
Resource and Data Boundaries
usePluginResourceresolves strictly inside the plugin's own directory; traversal is rejected by the serve API- Plugins read their own config via
usePluginSelfConfig()(zero-argument scope resolution) - Reading another plugin's config via
usePluginConfig(uid)is possible; mutating it directly is discouraged - Cross-plugin interaction should flow through the Command Bus (explicit, auditable) and public signals (read-only indicators)
Runtime Environment
Plugins run in the same JavaScript context as the core engine — there is no iframe or Worker sandbox for plugin UI code. Isolation is enforced by the safety net above plus convention:
- Touching DOM outside the plugin's slot → discouraged
- Importing store internals directly → discouraged
- Network access (fetch/XHR) → allowed (standard browser sandbox applies)
- File system access → browser-restricted (user prompt required)
Heavy or risky computation belongs in a plugin-owned Worker — see Plugin Workers for the fault-isolation boundary.
Best Practices for Plugin Developers
- Do not lean on the Error Boundary — wrap known-fallible operations (network, JSON parse) in your own try/catch and surface a toast.
- Clean up listeners — every
addEventListenerin auseEffectneeds a matching removal, or the boundary cannot save you from memory exhaustion. - Contain side effects — never mutate state outside your scope (
state.frames, other plugins' config, core preferences) fromonAction. - Defensive busy release — always release
setBusyin afinallyblock; bindisBusyto submit buttons to prevent double-submission during async work. - Declare
coreVersion— a correct floor lets the version gate fail fast instead of crashing on missing v2 APIs.
Next Steps
- Plugin System Overview — trust model architecture
- Packaging & Distribution — upload validation pipeline
- API Reference — scoped APIs and the asset store
Last updated: 2026-10-12