(This guide is adapted for PLUGIN 2.0; the legacy guide is deprecated and can be traced in the GitHub repository.)
中文 | English
Applicable Version: GETSSH Plugin Runtime. This document reflects the current implementation and replaces legacy VM-sandbox SDK documentation.
The GETSSH Plugin SDK is not an npm package that needs to be installed. The host injects controlled APIs when plugins launch: backend code receives context via activate(context), and plugin web pages communicate with the host via window.GETSSH.
1. Choose Your Plugin Architecture
| Architecture | Manifest | Execution Environment | Ideal Use Cases |
|---|---|---|---|
| Pure UI Plugin | getssh.type: "sandbox" | iframe sandbox="allow-scripts" | Panels, dashboards, read-only utilities |
| Pure Backend Plugin | Omit getssh.type | Isolated OS sandbox process | Storage, SSH extensions, network requests, native dialogs |
| UI + Backend Plugin | Omit getssh.type, provide main, renderer, and HTML | UI iframe + isolated backend process | Visual interfaces requiring controlled host capabilities |
Do not specify "type": "hybrid". Version 3 only recognizes "sandbox"; plugins with backend logic should omit getssh.type.
Pure UI plugins remain fully operational in Safe Mode. Whether backend plugins launch depends on the user-configured backend execution policy:
| Mode | Backend Behavior |
|---|---|
safe | Does not execute any plugin backend code |
normal | Each plugin executes in an isolated OS sandbox process |
strict | Currently shares the same OS-level sandbox boundary as normal; reserved for policy evolution |
developer | Plugins enter the Electron main process directly, treated as fully trusted code |
Isolated processes leverage macOS Seatbelt, Linux bubblewrap, and Windows AppContainer launcher. Plugins cannot directly read private host files, write to host directories, access raw networks, spawn child processes, or instantiate Workers; when host capabilities are required, call the context APIs documented below.
2. Manifest: package.json
Every plugin root directory must include a package.json. Below is a complete example of a UI + Backend plugin:
{
"name": "hello-getssh",
"version": "1.0.0",
"displayName": "Hello GETSSH",
"description": "GETSSH plugin example",
"author": "Your Name",
"main": "main.js",
"renderer": "renderer.js",
"getssh": {
"pluginId": "com.example.hello-getssh",
"capabilities": ["lifecycle", "storage:default"]
}
}
Field Reference
| Field | Required | Current Semantics |
|---|---|---|
name | Yes | Unique v3 runtime identity, installation directory name, backend RPC binding namespace, and getssh-plugin:// URL host |
version | Yes | Plugin version; semantic versioning recommended |
main | Yes | HTML path for pure UI plugins; CommonJS JavaScript entry point for backend plugins |
displayName | Recommended | Display name in user interface |
description | Recommended | Short plugin summary |
author | Recommended | Author information |
renderer | Required for UI | Bootstrapping script executed in hidden UI sandbox to register sidebar buttons and panels |
getssh.type | Required for UI | Currently the only valid value is "sandbox"; when present, GETSSH completely skips backend loading |
getssh.capabilities | Required for backend | Array of declared backend capabilities; must include "lifecycle" |
getssh.name | Optional | Display name override with higher precedence than displayName |
getssh.pluginId | Optional | Compatibility and marketplace metadata; currently not used for runtime identity binding |
The runtime strictly uses name as identity. Do not use getssh.pluginId for panel URL construction, RPC invocation, or storage access. For public plugins, restrict name to lowercase alphanumeric characters and hyphens (e.g., server-health) to avoid case sensitivity discrepancies across Windows, Linux, macOS, and URL hostnames.
Supported Capabilities
| Capability | Effect |
|---|---|
lifecycle | Mandatory for all backend plugins, affirming implementation of deactivate() |
storage:default | Explicit declaration of default storage tier; identical to 5 MiB default quota |
storage:extended | Elevates plugin KV storage quota to 500 MiB |
storage:unlimited | Removes plugin KV storage quota limit |
ssh:read | Subscribes to terminal output of designated SSH sessions |
ssh:write | Writes commands to designated SSH sessions; requires explicit user approval upon first use |
host:clipboard | Reads and writes system clipboard; reading triggers a user notification |
net:fetch | Accesses public HTTP/HTTPS endpoints via GETSSH's SSRF-guarded gateway |
Declaring capabilities unlocks corresponding host APIs without granting arbitrary Electron, Node.js, or OS-level privileges.
3. Three Supported Directory Structures
3.1 Pure UI Plugin
hello-ui/
├── package.json
├── renderer.js
├── index.html
└── ui.js
{
"name": "hello-ui",
"version": "1.0.0",
"displayName": "Hello UI",
"description": "Pure UI plugin",
"main": "index.html",
"renderer": "renderer.js",
"getssh": {
"type": "sandbox"
}
}
main points to HTML, but is not executed as a Node.js file. Third-party UI plugins must supply renderer.js to register panel entries on startup.
3.2 Pure Backend Plugin
hello-backend/
├── package.json
└── main.js
{
"name": "hello-backend",
"version": "1.0.0",
"displayName": "Hello Backend",
"description": "Pure backend plugin",
"main": "main.js",
"getssh": {
"capabilities": ["lifecycle", "storage:default"]
}
}
3.3 UI + Backend Plugin
hello-getssh/
├── package.json
├── main.js
├── renderer.js
├── index.html
└── ui.js
This configuration uses the complete Manifest from Section 2. renderer.js handles UI registration, index.html serves as the visible web view, and main.js exposes backend RPC handlers.
4. Build a UI + Backend Plugin in 5 Minutes
renderer.js: UI Registration Entry
renderer.js executes inside a hidden sandboxed iframe strictly to register UI endpoints. Do not make backend RPC calls here.
const panelId = 'hello-getssh.main';
const actionId = 'open-hello-panel';
window.GETSSH.registerPanel(
panelId,
'Hello GETSSH',
'getssh-plugin://hello-getssh/index.html'
);
window.__sidebarHandlers[actionId] = () => {
window.GETSSH.openPanel(panelId);
};
window.GETSSH.registerSidebarAction(
actionId,
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="currentColor"/></svg>',
'Open Hello Plugin'
);
The hostname in getssh-plugin://hello-getssh/ must match name in package.json exactly. Bundled HTML, JavaScript, CSS, JSON, and standard image assets load via the same protocol:
<link rel="stylesheet" href="./style.css">
<script src="./ui.js"></script>
main.js: Registering Backend Capabilities
Backend entry points must be CommonJS modules exporting both activate and deactivate.
let unsubscribe = null;
module.exports = {
async activate(context) {
// All backend plugins currently must register 1-256 settings fields.
context.ui.registerSettings([
{
id: 'greeting',
type: 'string',
label: 'Greeting Message',
description: 'Text displayed inside the plugin panel',
default: 'Hello from GETSSH'
}
]);
context.rpc.registerMethod('greet', async (payload) => {
const configured = await context.storage.get('greeting');
const greeting = configured ?? 'Hello from GETSSH';
return { message: `${greeting}, ${payload?.name || 'developer'}!` };
});
context.ui.registerTerminalContextMenu(
'remember-selection',
'Save Selected Text',
async ({ selectionText }) => {
await context.storage.set('lastSelection', selectionText);
context.host.notify('Hello GETSSH', 'Saved terminal selection');
}
);
},
async deactivate() {
unsubscribe?.();
unsubscribe = null;
}
};
index.html & ui.js: Calling the Backend
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello GETSSH</title>
</head>
<body>
<input id="name" value="GETSSH">
<button id="run">Call Backend</button>
<pre id="output"></pre>
<script src="./ui.js"></script>
</body>
</html>
const output = document.querySelector('#output');
document.querySelector('#run').addEventListener('click', async () => {
try {
const result = await window.GETSSH.invokeBackend('greet', {
name: document.querySelector('#name').value
});
output.textContent = result.message;
} catch (error) {
output.textContent = error instanceof Error ? error.message : String(error);
}
});
window.GETSSH.onBackendMessage((payload) => {
console.log('backend event:', payload);
});
window.GETSSH.onThemeChange((theme) => {
document.documentElement.dataset.theme = theme;
});
If the backend is in safe mode, failed to launch, or did not register the requested method, invokeBackend() rejects. The frontend must always catch exceptions.
5. Backend Lifecycle Rules
interface PluginModule {
activate(context: MainContextAPI): void | Promise<void>;
deactivate(): void | Promise<void>;
}
activate()timeout: maximum 8 seconds.- The backend must register RPCs, terminal context menus, SFTP menus, and settings schemas prior to resolving
activate(). The isolation runtime takes a single snapshot upon activation completion; subsequent registrations will not sync to the host. - The backend must call
context.ui.registerSettings()with at least one field. If the plugin requires no configurations, supply a simple boolean toggle such asenabled. deactivate()should terminate timers, streams, event listeners, and subscriptions. The host allows approximately 2 seconds for shutdown before forcefully killing the plugin process.- Forceful termination by the Security Center can kill processes immediately; do not rely solely on
deactivate()for data integrity. - Use
console.log/info/warn/errorfor logging. Standard output of isolated processes is an internal IPC channel; do not overrideprocess.stdoutor write raw protocol data to it.
The backend entry point is loaded via require(). Ensure output is CommonJS; if writing in TypeScript, ESM, or utilizing bundlers, compile to CommonJS target.
6. Backend API Reference: context
Boundary data types referenced in signatures:
type StructuredData =
| null
| string
| boolean
| number
| StructuredData[]
| { [key: string]: StructuredData };
6.1 Notifications & System Encryption
interface MainContextAPI {
showNotification(title: string, body: string): void;
safeStorageEncrypt(text: string): Promise<string>;
host: {
notify(
title: string,
body: string,
type?: 'info' | 'warning' | 'error'
): void;
};
}
showNotification() is a legacy alias; modern plugins should invoke host.notify(). type expresses semantic intent; actual display styling is governed by the host operating system.
safeStorageEncrypt() encrypts strings using Electron's OS-backed safe storage, returning a base64 ciphertext. It is asynchronous and must be awaited. The SDK does not currently expose a decryption API to plugins; do not use it as a general-purpose plugin credential vault.
6.2 Plugin KV Storage
interface PluginStorageAPI {
get(key: string): Promise<StructuredData | undefined>;
set(key: string, value: StructuredData): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
}
Storage keys are partitioned by Manifest name. Keys can be up to 256 characters. Default quota is 5 MiB, storage:extended allows 500 MiB, and storage:unlimited is unmetered.
Store JSON-serializable structures only: null, string, boolean, finite numbers, arrays, and plain objects. Avoid passing functions, Symbols, BigInts, Dates, Maps, Sets, Buffers, class instances, circular structures, or undefined.
await context.storage.set('cache', { updatedAt: Date.now(), hosts: 3 });
const cache = await context.storage.get('cache');
await context.storage.delete('cache');
6.3 Bidirectional RPC
interface PluginRpcAPI {
registerMethod(
method: string,
handler: (payload: StructuredData | undefined) =>
StructuredData | undefined | Promise<StructuredData | undefined>
): void;
sendToFrontend(payload: StructuredData): void;
}
RPC method names permit alphanumeric characters, ., _, :, -, length 1-128, and forbid __proto__, prototype, or constructor.
context.rpc.registerMethod('server:list', async ({ group }) => {
return { group, items: await context.storage.get(`group:${group}`) ?? [] };
});
context.rpc.sendToFrontend({ type: 'sync-complete', count: 12 });
Frontend caller:
const result = await window.GETSSH.invokeBackend('server:list', { group: 'prod' });
window.GETSSH.onBackendMessage((event) => {
if (event.type === 'sync-complete') console.log(event.count);
});
RPC payload and return values must be structured data under 2 MiB with a maximum nesting depth of 24 levels. Timeout is 15 seconds. Paginate large datasets rather than embedding raw file buffers into single RPC calls.
6.4 Settings Schema
interface PluginSettingsField {
id: string;
type: 'string' | 'number' | 'boolean' | 'password';
label: string;
description?: string;
default?: StructuredData;
}
interface PluginSettingsAPI {
registerSettings(fields: PluginSettingsField[]): void;
}
Rules:
- Each backend plugin must register between 1 and 256 fields.
idfollows safe identifier rules and must be unique.labellength: 1-256 characters;descriptionmax: 4096 characters.- When saved by the user, GETSSH stores values under field
idin the plugin KV store and reloads the plugin. - Retrieve settings via
context.storage.get(field.id). passwordonly controls UI input masking; it does not provide separate cryptographic encryption guarantees.
context.ui.registerSettings([
{ id: 'enabled', type: 'boolean', label: 'Enable Plugin', default: true },
{ id: 'endpoint', type: 'string', label: 'API Endpoint', default: 'https://api.example.com' },
{ id: 'interval', type: 'number', label: 'Refresh Interval (s)', default: 30 },
{ id: 'token', type: 'password', label: 'Access Token' }
]);
const enabled = (await context.storage.get('enabled')) ?? true;
6.5 Terminal & SFTP Context Menus
interface PluginContextMenuAPI {
registerTerminalContextMenu(
actionId: string,
label: string,
handler: (data: {
sessionId: string;
selectionText: string;
}) => unknown | Promise<unknown>
): void;
registerSFTPContextMenu(
actionId: string,
label: string,
handler: (data: {
sessionId: string;
currentPath: string;
selectedFiles: string[];
}) => unknown | Promise<unknown>
): void;
}
actionId adheres to safe identifier conventions with label length 1-256. Maximum 128 registrations per menu category.
context.ui.registerSFTPContextMenu(
'copy-remote-path',
'Copy Remote Path',
async ({ currentPath }) => {
await context.host.clipboard.writeText(currentPath);
}
);
The snippet above also requires the host:clipboard capability.
6.6 SSH Data Streams & Command Execution
interface PluginSshAPI {
onData(
sessionId: string,
callback: (chunk: string) => void
): () => void;
write(sessionId: string, command: string): Promise<void>;
}
ssh:readunlocksonData().ssh:writeunlockswrite().- UI panels cannot access raw session IDs. The backend extracts
sessionIdfrom terminal or SFTP menu invocation payloads. onData()returns an unsubscribe function that must be invoked when done and cleaned up duringdeactivate().write()accepts up to ~1 MiB per call. First write prompts user confirmation (deny, allow once, allow for this session).
let stopReading = null;
context.ui.registerTerminalContextMenu(
'watch-output',
'Monitor Session Output',
({ sessionId }) => {
stopReading?.();
stopReading = context.ssh.onData(sessionId, (chunk) => {
console.log('SSH output:', chunk);
});
}
);
Only call these methods when capabilities are declared in the Manifest.
6.7 Clipboard & Native Dialogs
interface PluginHostAPI {
clipboard: {
writeText(text: string): Promise<void>;
readText(): Promise<string>;
};
showMessageBox(options: {
type?: 'none' | 'info' | 'warning' | 'error' | 'question';
buttons?: string[];
defaultId?: number;
cancelId?: number;
title?: string;
message: string;
detail?: string;
checkboxLabel?: string;
}): Promise<{ response: number; checkboxChecked: boolean }>;
showOpenDialog(options: {
title?: string;
defaultPath?: string;
filters?: Array<{ name: string; extensions: string[] }>;
properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'>;
}): Promise<{ canceled: boolean; filePaths: string[] }>;
showSaveDialog(options: {
title?: string;
defaultPath?: string;
filters?: Array<{ name: string; extensions: string[] }>;
}): Promise<{ canceled: boolean; filePath?: string }>;
}
Clipboard methods require host:clipboard. Native dialogs do not require additional capabilities.
File dialogs return selected paths only. In normal/strict modes, this path does not automatically confer filesystem read/write privileges; isolated plugins cannot open files via raw fs. Use controlled file APIs when available.
6.8 Outbound Network Requests: context.net.fetch
Manifest must declare net:fetch:
const response = await context.net.fetch('https://api.example.com/v1/status', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ source: 'getssh' }),
redirect: 'follow'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
Gateway constraints:
- Only
http:andhttps:schemes allowed (URL max: 8192 characters). - Embedded credentials in URLs are blocked.
- Requests resolving to loopback, private RFC1918, link-local, reserved, or multicast IP addresses are rejected.
- Maximum 5 redirects followed; cross-origin redirects strip
authorizationandcookieheaders. - Request timeout: 30 seconds; maximum response body: 1 MiB.
- Maximum 128 headers; connection-level headers (
host,content-length,connection,transfer-encoding) are host-managed. - Supported options:
method,headers,body,redirect. Transmit strings for widest cross-mode compatibility. - Direct raw sockets,
AbortSignal,FormData,Blob, or streaming request bodies are not supported.
Violating network security rules throws SecurityError, triggering the Security Center and terminating the offending plugin. Standard network errors reject with NetworkError.
7. Frontend API Reference: window.GETSSH
Plugin web pages must be loaded by GETSSH via getssh-plugin://<name>/<file>. When opened directly in browsers or from local disk, window.GETSSH is undefined.
API Availability
| API | renderer.js Startup Sandbox | Visible HTML Panel |
|---|---|---|
registerSidebarAction | Yes | Yes |
registerPanel / openPanel | Yes | Yes |
showNotification | Yes | Yes |
getLocale / onThemeChange | Yes | Yes |
invokeBackend / onBackendMessage | No | Yes |
registerSidebarAction
interface RendererContextAPI {
registerSidebarAction(id: string, svgIcon: string, label: string): void;
}
SVG strings are sanitized. Pass handler functions by attaching to window.__sidebarHandlers:
window.__sidebarHandlers.open = () => window.GETSSH.openPanel('server-dashboard.main');
window.GETSSH.registerSidebarAction('open', svg, 'Open Dashboard');
registerPanel & openPanel
interface RendererContextAPI {
registerPanel(panelId: string, title: string, renderUrl: string): void;
openPanel(panelId: string): void;
}
Always use internal plugin protocol URLs:
window.GETSSH.registerPanel(
'server-dashboard.main',
'Server Dashboard',
'getssh-plugin://server-dashboard/index.html'
);
Register before opening. Format IDs as <plugin-name>.<panel-name> to maintain global uniqueness.
showNotification
interface RendererContextAPI {
showNotification(title: string, body: string): void;
}
Renders via browser Notification API; requires system permission.
getLocale & onThemeChange
interface RendererContextAPI {
getLocale(): string;
onThemeChange(
callback: (theme: 'dark' | 'light' | 'system') => void
): void;
}
getLocale() provides current language snapshot. onThemeChange() notifies of theme transitions.
invokeBackend & onBackendMessage
interface RendererContextAPI {
invokeBackend(
method: string,
payload?: StructuredData
): Promise<StructuredData | undefined>;
onBackendMessage(callback: (payload: StructuredData) => void): void;
}
invokeBackend() automatically routes to the backend sharing the same name as the panel URL. Cross-plugin invocation is barred.
onBackendMessage() receives payloads dispatched via backend context.rpc.sendToFrontend().
Host System Telemetry Events
Plugin panels receive regular periodic telemetry messages:
window.addEventListener('message', (event) => {
if (event.source !== window.parent) return;
if (event.data?.type !== 'sysmon:data') return;
const { cpus, mem, net } = event.data.payload;
// cpus.overall: number; cpus.cores: number[]
// mem.total / mem.used / mem.free: bytes
// net.rx / net.tx: bytes since the latest refresh
});
Verify event.source and type before processing host telemetry.
8. Data, Rate, and Registration Limits
| Metric | Current Limit |
|---|---|
| Single IPC protocol message | 2 MiB |
| Structured data depth | 24 levels |
| RPC / host call timeout | 15 seconds |
| Activation timeout | 8 seconds |
| Registered RPC methods | 128 |
| Terminal context menu items | 128 |
| SFTP context menu items | 128 |
| Settings schema fields | 1-256 |
| Isolated plugin calls to host | 250 calls/sec |
| IPC protocol message throughput | 500 msgs/sec, 16 MiB/sec |
| Backend logging throughput | 64 KiB per execution |
These thresholds protect client stability. Throttle high-frequency polling and batch events accordingly.
9. Node.js Execution Rules in Isolated Mode
While backend plugins run as Node.js processes loading local CommonJS modules, OS sandboxing defines strict privilege boundaries:
- Plugin installation directory is read-only.
- A randomized private temporary HOME is allocated per run and wiped upon termination.
- Host user directories, GETSSH
userData, system temp directories, and external volumes cannot be directly read or written. - Raw network sockets are disabled; use
context.net.fetch(). - Spawning child processes, Workers, POSIX signals, debuggers, and native addons are disallowed.
- Environment variables inherit a sanitized whitelist only.
Bundle business logic into pure JavaScript and channel all host operations through context. Do not rely on developer mode loopholes (fs, child_process, raw Electron APIs).
Linux Note: Normal and strict modes require bwrap (bubblewrap) installed on the system. If absent, GETSSH refuses to launch backend plugins (pure UI plugins remain operational).
10. Dependencies, Packaging, and Installation
GETSSH does not execute npm install upon installation. Bundles must include all required runtime JavaScript, CSS, and asset dependencies. We recommend compiling backend code into a single CommonJS bundle.
ZIP archives support flat or single top-level directory layouts:
hello-getssh.zip
├── package.json
├── main.js
├── renderer.js
├── index.html
└── ui.js
Or nested inside one directory:
hello-getssh.zip
└── hello-getssh/
├── package.json
└── ...
macOS / Linux packaging command:
cd hello-getssh
zip -r ../hello-getssh.zip . -x '*.DS_Store'
Windows PowerShell packaging command:
Compress-Archive -Path .\hello-getssh\* -DestinationPath .\hello-getssh.zip -Force
Navigate to Settings → Plugins, drag the ZIP archive into the dropzone, inspect requested permissions, and confirm installation.
Internal TypeScript definitions are maintained at apps/getssh-client/src/types/plugin.d.ts. Third-party plugins should treat this guide's signatures as normative.
11. Migrating from Legacy SDK
| Legacy Convention | v3 Convention |
|---|---|
Backend executes in main process vm.Script | Normal/strict modes execute in isolated OS sandbox processes |
getssh.type: "hybrid" | Omit getssh.type; pair renderer with backend main |
getssh.pluginId acts as runtime identity | name acts as runtime identity; pluginId is metadata |
safeStorageEncrypt() returns synchronously | Asynchronous: await context.safeStorageEncrypt() |
ssh.onData() unmanaged | Returns an unsubscribe function to be stored and invoked |
ssh.write() synchronous | Asynchronous: await context.ssh.write() |
pluginRpcInvoke() | Frontend calls window.GETSSH.invokeBackend() |
onPluginRpcMessage() | Frontend registers window.GETSSH.onBackendMessage() |
registerSettingsSchema() | Backend calls context.ui.registerSettings() |
registerUIExtension() | Use registerTerminalContextMenu() or registerSFTPContextMenu() |
onSSHSessionConnect | Not exposed; obtain session ID from context menu invocation |
| Normal mode accesses arbitrary files/network | Controlled access via context.storage, context.net, context.ssh, context.host |
12. Common Errors
Node.js plugins must declare ... lifecycle
Manifest missing "lifecycle" capability. Add it to capabilities array and export deactivate().
Backend plugins must export activate() and deactivate()
Backend is not CommonJS, exports invalid identifiers, or main points to wrong file. Verify bundled production artifact.
Backend plugins must call context.ui.registerSettings()
Call registerSettings() before activate() completes, providing at least one field.
Plugin '<name>' is not running
Common causes: Safe Mode disabled backend execution, activation failure, missing system isolation tools (bwrap), or URL hostname mismatching Manifest name.
Method '<method>' not found
Ensure backend calls context.rpc.registerMethod() before activate() finishes and verify method identifier casing.
window.GETSSH is undefined
Page must be loaded via getssh-plugin://<name>/.... Direct filesystem or standard browser tabs do not inject the SDK bridge.
Plugin Terminated Following SecurityError
Plugin crossed a zero-trust boundary (e.g. net.fetch resolving to intranet/loopback address). Check endpoint configuration and reload.
Data Rejected by RPC or Storage
Ensure values are JSON-serializable without circular references, functions, BigInts, or binary Buffers, and stay within the 2 MiB payload limit.
13. Pre-Release Checklist
nameuses stable lowercase identifiers matching allgetssh-plugin://<name>/...URLs.- Pure UI plugins declare
getssh.type: "sandbox"; backend plugins omittype. - Backend compiles to CommonJS and exports both
activateanddeactivate. - Backend capabilities include
lifecyclewith remaining capabilities strictly minimized. activate()completes within 8 seconds with all registrations finalized prior to resolution.deactivate()safely clears timers, listeners, and subscriptions.- Frontend handles
invokeBackend()rejections gracefully. - All cross-boundary IPC data is JSON-serializable within size and rate boundaries.
- No reliance on developer mode loopholes (
child_process,fs, raw Electron). - ZIP archive contains all required production dependencies.
- Verified in normal/strict modes across macOS, Windows, and Linux.