How Modly's Extension System Architecture Works: Model vs Process Extensions Explained
Modly's extension system uses a unified AnyExtension union type that distinguishes between ModelExtension (AI generation) and ProcessExtension (custom executable scripts) through a type discriminator field, with each executing through fundamentally different paths—model extensions via HTTP backend calls and process extensions via local Electron IPC.
Modly is an open-source generative AI platform that provides a flexible plugin architecture through its extension system. Understanding how model extensions differ from process extensions is essential for developers building custom capabilities. Both types share common metadata fields but diverge sharply in execution semantics, security boundaries, and runtime behavior.
The Core Type Architecture
Modly defines its extension types in src/shared/types/electron.d.ts, establishing a discriminated union pattern that TypeScript consumers use throughout the application.
ModelExtension Structure
A model extension wraps a generative AI model—such as Stable Diffusion for images or LLMs for text—that receives inputs and produces new generated assets.
Key fields include:
type: 'model'— the discriminator constantnodes— UI node definitions the model exposes for workflow compositiontrusted,builtin,source,localPath— metadata and provenance fields- Optional
manifestErrorflags for validation failures
// src/shared/types/electron.d.ts#L29-L45
interface ModelExtension {
type: 'model';
id: string;
name: string;
nodes: ExtensionNode[];
trusted: boolean;
builtin: boolean;
source: ExtensionSource;
localPath: string;
manifestError?: string;
paramsSchema?: JSONSchema;
}
ProcessExtension Structure
A process extension wraps an arbitrary executable—typically a Python script or compiled binary—that transforms existing assets rather than generating new ones from scratch.
Key fields include:
type: 'process'— the discriminator constantentry— filesystem path to the executable script- Same metadata fields as model extensions for consistency
// src/shared/types/electron.d.ts#L63-L80
interface ProcessExtension {
type: 'process';
id: string;
name: string;
entry: string;
trusted: boolean;
builtin: boolean;
source: ExtensionSource;
localPath: string;
manifestError?: string;
paramsSchema?: JSONSchema;
}
Both types unite under AnyExtension = ModelExtension | ProcessExtension, enabling polymorphic handling in stores and UI components while preserving type-specific behavior at execution time.
Extension Discovery and State Management
The extensionsStore.ts module maintains separate reactive arrays for each extension type after loading from the Electron preload API.
Loading and Filtering
When extensionsStore.loadExtensions() invokes window.electron.extensions.list(), it immediately partitions the unified list by type:
// src/shared/stores/extensionsStore.ts#L49-L53
const list = await window.electron.extensions.list();
set({
modelExtensions: list.filter((e) => e.type === 'model'),
processExtensions: list.filter((e) => e.type === 'process'),
});
This separation persists throughout the store's lifecycle, allowing UI components to render appropriate pickers and configuration panels for each category.
Installation Routing
The generic installExtension helper directs newly installed extensions to the correct array based on runtime type inspection:
// src/shared/stores/extensionsStore.ts#L28-L35
const installExtension = (ext: AnyExtension) => {
set((state) => {
if (ext.type === 'model') {
return { modelExtensions: [...state.modelExtensions, ext] };
} else {
return { processExtensions: [...state.processExtensions, ext] };
}
});
};
Execution Pathways: Where the Architectures Diverge
The workflowRunStore.ts module contains the critical branching logic in its executeExtensionNode function. Despite both extension types appearing as nodes in the same workflow graph, their execution implementations share no code path.
Model Extension Execution: Backend Generation Jobs
When a workflow node references a model extension (ext.type === 'model'), the client constructs a multipart HTTP POST to the Modly backend's generation API:
// src/areas/workflows/workflowRunStore.ts#L40-L88
if (ext?.type === 'model') {
const fd = new FormData();
fd.append('image', blob, fname);
fd.append('model_id', node.data.extensionId ?? '');
fd.append('params', JSON.stringify(effectiveParams));
const { data } = await client.post('/generate/from-image', fd);
const jobId = data.job_id;
// Poll until completion
const status = await pollJobStatus(jobId);
updateNodeOutput(nodeId, status.output_url);
}
This path characteristics:
- Stateful: returns a
jobIdfor asynchronous polling - Server-side execution: the model runs on GPU-equipped backend infrastructure
- Network-transported assets: images and parameters serialize over HTTP
- Latency-tolerant: assumes multi-second generation times
Process Extension Execution: Local IPC Invocation
When a node references a process extension (ext.type === 'process'), execution remains entirely client-side through Electron's IPC bridge:
// src/areas/workflows/workflowRunStore.ts#L26-L33
if (ext?.type === 'process') {
const result = await window.electron.extensions.runProcess(
extId,
{ filePath: nodeInputPath, text: nodeInputText, texts: nodeInputTexts, nodeId: nid },
liveParams,
);
if (!result.success) throw new Error(result.error);
updateNodeOutput(nodeId, result.result.filePath ?? result.result.text);
}
This path characteristics:
- Synchronous from client's perspective: returns immediate result or error
- Local execution: spawns Python process via
src/electron/main/ipc-handlers.ts#L1398-L1408 - Filesystem-local: operates on paths accessible to the Electron main process
- Lower latency: suitable for preprocessing, postprocessing, and utility transforms
Security and Trust Implications
Both extension types respect Modly's trust system, but the attack surface differs materially.
| Concern | Model Extension | Process Extension |
|---|---|---|
| Code execution venue | Remote backend (containerized) | Local user machine |
| Filesystem access | None (API-mediated) | Full access to Electron's permissions |
| Network capabilities | Isolated backend network | Inherits Electron main process reach |
| Supply chain risk | Model weights tampering | Script injection, dependency confusion |
The trusted and builtin boolean flags in both extension types gate automatic execution. Untrusted extensions require explicit user confirmation before workflow nodes activate.
UI Integration and Parameter Schemas
Despite execution differences, both extension types expose paramsSchema (JSON Schema) for workflow node configuration. The UI renders parameter editors generically, though model extensions typically expose generation-oriented controls (seed, steps, CFG scale) while process extensions expose transformation parameters (thresholds, format options, boolean flags).
Neither extension type appears visually distinct to end users in node pickers—discrimination happens at the data layer through the type field, maintaining conceptual simplicity while enabling powerful specialization.
Summary
- Modly's
AnyExtensionunion type unifies model and process extensions under common metadata while preserving execution-specific fields - Model extensions (type:
'model') execute remotely via HTTP/generate/*endpoints, return job IDs, and run AI inference on backend infrastructure - Process extensions (type:
'process') execute locally viawindow.electron.extensions.runProcess, return immediate results, and spawn Python scripts through Electron IPC - The
extensionsStoremaintains separate filtered arrays after loading fromwindow.electron.extensions.list(), routing installations appropriately - Workflow execution branches at
executeExtensionNode—no shared runtime path between the two extension categories - Both types respect trust flags and expose JSON Schema parameters, but process extensions carry elevated local security privileges
Frequently Asked Questions
Can a single extension be both a model and a process extension?
No. The type discriminator field in electron.d.ts ('model' | 'process') forces mutual exclusivity. An extension package must declare one execution mode. If you need both generation and transformation capabilities, ship two extensions or implement the transformation as a post-processing step within a model extension's node graph.
How does Modly prevent malicious process extensions from harming my system?
Process extensions inherit Electron main process privileges, creating inherent risk. Modly mitigates this through: (1) mandatory trusted flag verification—untrusted extensions require explicit user activation; (2) builtin extensions from verified publishers execute by default; (3) source attribution tracking via the source field for auditability. However, process extensions fundamentally require more trust than model extensions due to local execution.
Why don't model extensions run locally like process extensions?
Generative AI models typically require GPU acceleration and substantial memory unavailable in standard Electron contexts. By routing model execution to a backend service, Modly: enables hardware flexibility (cloud GPUs, local server variants), centralizes model weight management, and isolates computationally intensive inference from UI responsiveness. Process extensions handle lighter-weight CPU-bound transformations suitable for client hardware.
Can I convert between extension types after installation?
No conversion mechanism exists. The type field is immutable after extension loading. To change execution modes, modify the extension's manifest and reinstall. The extensionsStore re-filters on every loadExtensions() call, so type changes reflect immediately upon reload if you manually edit extension metadata—though this breaks the extension contract and will likely cause runtime failures.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →