Main Process Architecture
The main process is the heart of the OpenWork desktop application. It runs in a Node.js context with full access to operating system APIs and manages all application lifecycle, IPC communication, and external process spawning.
Main Process Overview
graph TB
subgraph "Entry Point"
INDEX[index.ts<br/>Electron Bootstrap]
end
subgraph "Initialization"
SINGLE[Single Instance Lock]
CLEAN{Clean Start?}
FRESH[Fresh Install Cleanup]
READY[app.whenReady]
ICON[Dock Icon Setup]
IPC[registerIPCHandlers]
WIN[createWindow]
end
subgraph "Runtime Subsystems"
HANDLERS[IPC Handlers<br/>ipc/handlers.ts]
STORES[Storage Layer<br/>store/]
TASKMGR[TaskManager<br/>opencode/task-manager.ts]
PERM[Permission API<br/>permission-api.ts]
end
subgraph "Event Handlers"
OPENURL[open-url<br/>Protocol Handler]
QUIT[before-quit<br/>Cleanup]
ACTIVATE[activate<br/>Window Management]
CLOSED[window-all-closed<br/>Platform Behavior]
end
INDEX --> SINGLE
SINGLE --> CLEAN
CLEAN -->|Yes| FRESH
CLEAN -->|No| READY
FRESH --> READY
READY --> ICON
ICON --> IPC
IPC --> WIN
WIN --> HANDLERS
HANDLERS --> STORES
HANDLERS --> TASKMGR
HANDLERS --> PERM
INDEX -.->|app.on| OPENURL
INDEX -.->|app.on| QUIT
INDEX -.->|app.on| ACTIVATE
INDEX -.->|app.on| CLOSED
style INDEX fill:#e3f2fd
style HANDLERS fill:#f3e5f5
style STORES fill:#fff3e0
style TASKMGR fill:#e8f5e9
Entry Point Analysis
File: apps/desktop/src/main/index.ts
Application Bootstrap
sequenceDiagram
participant Env as Environment
participant Main as index.ts
participant Electron as Electron APIs
participant IPC as IPC Handlers
participant Win as BrowserWindow
Env->>Main: Process loads
Main->>Main: Check E2E flags (line 15)
Main->>Main: Check CLEAN_START env (line 21)
alt CLEAN_START=1
Main->>Electron: Clear userData directory
end
Main->>Electron: Set app.name = 'Openwork'
Main->>Main: Load .env file
Main->>Electron: app.requestSingleInstanceLock()
alt Lock acquired
Main->>Electron: Register second-instance handler
Main->>Electron: app.whenReady()
Main->>Main: checkAndCleanupFreshInstall()
Main->>Electron: Set dock icon (macOS)
Main->>IPC: registerIPCHandlers()
Main->>Win: createWindow()
else Lock failed
Main->>Electron: app.quit()
end
Key Lines:
| Lines | Function | Description |
|---|---|---|
| 15-17 | E2E Flag Detection | Sets E2E_SKIP_AUTH global flag for testing |
| 21-34 | Clean Mode | Wipes userData directory when CLEAN_START=1 |
| 37 | App Name | Sets app name before any native UI |
| 123-135 | Single Instance | Prevents multiple app instances |
| 137-174 | App Ready | Initializes handlers and creates window |
| 185-190 | Before Quit | Flushes task history and disposes TaskManager |
Single Instance Enforcement
Source: apps/desktop/src/main/index.ts lines 123-135
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
console.log('[Main] Second instance attempted; quitting');
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
}
Behavior:
- First instance acquires lock and proceeds
- Second instance quits immediately
- If first instance is minimized, it gets restored and focused
Window Creation
Source: apps/desktop/src/main/index.ts lines 70-120
graph LR
subgraph "Window Configuration"
SIZE[1280x800 default<br/>900x600 minimum]
ICON[App Icon<br/>nativeImage]
STYLE[titleBarStyle<br/>hiddenInset on macOS]
PREFS[webPreferences<br/>preload + security]
end
subgraph "Security Settings"
NO_NODE[nodeIntegration: false]
ISO[contextIsolation: true]
PRE[preload: index.cjs]
end
subgraph "Load Strategy"
DEV{isPackaged?}
VITE[Vite Dev Server<br/>Hot Reload]
PROD[Production Build<br/>file:// index.html]
end
SIZE --> PREFS
ICON --> PREFS
STYLE --> PREFS
PREFS --> NO_NODE
PREFS --> ISO
PREFS --> PRE
PREFS --> DEV
DEV -->|No| VITE
DEV -->|Yes| PROD
Security Configuration (lines 91-95):
webPreferences: {
preload: preloadPath,
nodeIntegration: false, // Critical: No Node in renderer
contextIsolation: true, // Critical: Isolated context
}
Protocol Handling
Source: apps/desktop/src/main/index.ts lines 193-202
app.setAsDefaultProtocolClient('accomplish');
app.on('open-url', (event, url) => {
event.preventDefault();
if (url.startsWith('accomplish://callback')) {
mainWindow?.webContents?.send('auth:callback', url);
}
});
Purpose: Handles OAuth callbacks and deep links
IPC Handler System
File: apps/desktop/src/main/ipc/handlers.ts
Handler Architecture
graph TB
subgraph "IPC Handler Registry"
HANDLE[handle<T, R>()<br/>Type-safe wrapper]
VALIDATE[validate()<br/>Schema validation]
SANITIZE[sanitizeString()<br/>Input cleaning]
ASSERT[assertTrustedWindow()<br/>Security check]
end
subgraph "Handler Categories"
TASK[Task Handlers<br/>start, cancel, interrupt]
SETTINGS[Settings Handlers<br/>api-keys, model, debug]
PERM[Permission Handlers<br/>respond to requests]
SESSION[Session Handlers<br/>resume conversations]
SHELL[Shell Handlers<br/>open-external]
end
subgraph "Event Forwarding"
FORWARD[forwardToRenderer()<br/>webContents.send]
BATCH[Message Batching<br/>50ms delay]
end
HANDLE --> VALIDATE
VALIDATE --> SANITIZE
SANITIZE --> ASSERT
ASSERT --> TASK
ASSERT --> SETTINGS
ASSERT --> PERM
ASSERT --> SESSION
ASSERT --> SHELL
TASK --> FORWARD
SETTINGS --> FORWARD
PERM --> FORWARD
SESSION --> FORWARD
FORWARD --> BATCH
style HANDLE fill:#e3f2fd
style VALIDATE fill:#fff3e0
style ASSERT fill:#ffebee
style BATCH fill:#e8f5e9
Handler Wrapper
Source: apps/desktop/src/main/ipc/handlers.ts lines 239-251
function handle<Args extends unknown[], ReturnType = unknown>(
channel: string,
handler: (event: IpcMainInvokeEvent, ...args: Args) => ReturnType
): void {
ipcMain.handle(channel, async (event, ...args) => {
try {
return await handler(event, ...(args as Args));
} catch (error) {
console.error(`IPC handler ${channel} failed`, error);
throw normalizeIpcError(error);
}
});
}
Features:
- Type-safe generic wrapper
- Automatic error normalization
- Async handler support
Trusted Window Assertion
Source: apps/desktop/src/main/ipc/handlers.ts lines 167-178
function assertTrustedWindow(window: BrowserWindow | null): BrowserWindow {
if (!window || window.isDestroyed()) {
throw new Error('Untrusted window');
}
const focused = BrowserWindow.getFocusedWindow();
if (BrowserWindow.getAllWindows().length > 1 &&
focused && focused.id !== window.id) {
throw new Error('IPC request must originate from the focused window');
}
return window;
}
Security: Prevents other windows from making privileged IPC calls
Input Sanitization
Source: apps/desktop/src/main/ipc/handlers.ts lines 180-192
function sanitizeString(input: unknown, field: string, maxLength = 8000): string {
if (typeof input !== 'string') {
throw new Error(`${field} must be a string`);
}
const trimmed = input.trim();
if (!trimmed) {
throw new Error(`${field} is required`);
}
if (trimmed.length > maxLength) {
throw new Error(`${field} exceeds maximum length`);
}
return trimmed;
}
Protections:
- Type checking
- Empty string rejection
- Maximum length enforcement
- Whitespace trimming
Message Batching System
Source: apps/desktop/src/main/ipc/handlers.ts lines 88-165
sequenceDiagram
participant CLI as OpenCode CLI
participant Adapter as OpenCodeAdapter
participant Batch as MessageBatcher
participant Renderer as Renderer Process
loop Multiple messages
CLI->>Adapter: NDJSON message
Adapter->>Batch: queueMessage()
Batch->>Batch: Add to pendingMessages
Batch->>Batch: Reset 50ms timer
end
Batch->>Batch: Timer fires
Batch->>Renderer: task:update:batch (all messages)
Batch->>Store: addTaskMessage() for each
Configuration:
- Batch Delay: 50ms
- Purpose: Reduce IPC overhead for rapid message streams
- Scope: Per-task batching to avoid cross-contamination
Task Start Handler
Source: apps/desktop/src/main/ipc/handlers.ts lines 264-389
stateDiagram-v2
[*] --> ValidateConfig
ValidateConfig --> InitPermAPI: First task only
InitPermAPI --> CreateCallbacks
ValidateConfig --> CreateCallbacks: Subsequent tasks
CreateCallbacks --> StartTask
StartTask --> SaveTask
SaveTask --> [*]
note right of InitPermAPI
Initialize permission API
server for file-permission MCP
end note
note right of CreateCallbacks
onMessage, onProgress,
onPermissionRequest,
onComplete, onError,
onDebug, onStatusChange
end note
Key Steps:
- Validate Task Config (line 267)
- Sanitize prompt, taskId, sessionId
- Validate allowedTools array
- Check workingDirectory path
- Initialize Permission API (lines 270-274)
- Only on first task start
- Starts HTTP server on port 9226
- Create Task-Scoped Callbacks (lines 286-371)
- Message batching for performance
- Status change notifications
- Debug logging when enabled
- Start Task via TaskManager (line 374)
- Creates isolated adapter or queues task
- Persist Initial Task (lines 376-386)
- Adds user message with prompt
- Saves to task history
Storage Layer
Store Architecture
graph TB
subgraph "electron-store Instances"
SECURE[secure-storage<br/>Encrypted API keys]
SETTINGS[app-settings<br/>Preferences]
HISTORY[task-history<br/>Task records]
end
subgraph "Security"
CRYPTO[crypto.pbkdf2Sync<br/>Key derivation]
AES[aes-256-gcm<br/>Encryption]
end
subgraph "Persistence"
DEBOUNCE[250ms debounce<br/>taskHistory]
FLUSH[flushPendingTasks()<br/>before-quit]
end
SECURE --> CRYPTO
CRYPTO --> AES
HISTORY --> DEBOUNCE
DEBOUNCE --> FLUSH
style SECURE fill:#ffebee
style CRYPTO fill:#fff3e0
style DEBOUNCE fill:#e8f5e9
App Settings Store
File: apps/desktop/src/main/store/appSettings.ts
Schema:
interface AppSettingsSchema {
debugMode: boolean;
onboardingComplete: boolean;
selectedModel: SelectedModel | null;
}
Default Model (lines 21-25):
selectedModel: {
provider: 'anthropic',
model: 'anthropic/claude-opus-4-5',
}
Task History Store
File: apps/desktop/src/main/store/taskHistory.ts
Features:
- Max History: 100 items (configurable)
- Debounced Writes: 250ms delay to reduce I/O
- Flush on Quit: Ensures no data loss on exit
Stored Task Schema (lines 7-16):
interface StoredTask {
id: string;
prompt: string;
status: TaskStatus;
messages: TaskMessage[];
sessionId?: string;
createdAt: string;
startedAt?: string;
completedAt?: string;
}
Secure Storage
File: apps/desktop/src/main/store/secureStorage.ts
Encryption Flow:
sequenceDiagram
participant Store as secureStorage
participant Crypto as Node.js crypto
participant FS as electron-store
Store->>Store: getDerivedKey()
Note over Store,Crypto: Machine-specific data + salt
Store->>Crypto: pbkdf2Sync(100000 iter)
Crypto-->>Store: 256-bit key
Store->>Store: encryptValue(apiKey)
Store->>Crypto: createCipheriv(aes-256-gcm)
Store->>Crypto: randomBytes(12) for IV
Crypto-->>Store: IV + authTag + ciphertext
Store->>FS: Store "iv:authTag:ciphertext"
Store->>Store: getApiKey(provider)
Store->>FS: Retrieve encrypted value
Store->>Crypto: createDecipheriv(aes-256-gcm)
Crypto-->>Store: Decrypted API key
Key Derivation (lines 68-94):
const machineData = [
os.platform(),
os.homedir(),
os.userInfo().username,
app.getPath('userData'),
'ai.accomplish.desktop',
].join(':');
const salt = getSalt(); // Stored once per installation
_derivedKey = crypto.pbkdf2Sync(
machineData,
salt,
100000, // iterations
32, // 256-bit key
'sha256'
);
Security Considerations:
- At Rest: AES-256-GCM encryption
- Key Source: Machine-specific (not portable)
- Salt: Generated once per installation
- Iterations: 100,000 PBKDF2 rounds
- Trade-off: Less secure than Keychain, but no prompts
Task Management Integration
TaskManager Singleton
File: apps/desktop/src/main/opencode/task-manager.ts
graph TB
subgraph "TaskManager State"
ACTIVE[activeTasks<br/>Map<taskId, ManagedTask>]
QUEUE[taskQueue<br/>QueuedTask[]]
MAX[maxConcurrentTasks = 10]
end
subgraph "Task Lifecycle"
START[startTask]
QUEUE_OP[queueTask]
EXEC[executeTask]
CLEAN[cleanupTask]
PROC[processQueue]
end
subgraph "Adapter Per Task"
ADAPTER[OpenCodeAdapter<br/>Isolated PTY]
CALLBACKS[TaskCallbacks<br/>Task-scoped events]
CLEANUP[cleanup function<br/>Event removal]
end
START --> MAX
MAX -->|< capacity| EXEC
MAX -->|at capacity| QUEUE_OP
QUEUE_OP --> QUEUE
EXEC --> ADAPTER
ADAPTER --> CALLBACKS
CALLBACKS --> CLEAN
CLEAN --> PROC
PROC --> QUEUE
style ACTIVE fill:#e3f2fd
style QUEUE fill:#fff3e0
style ADAPTER fill:#e8f5e9
Key Features:
- Parallel Execution: Up to 10 concurrent tasks
- Per-Task Isolation: Each task gets its own adapter
- Queue Management: Tasks queued when at capacity
- Auto-Cleanup: Resources freed on completion/error
Permission API Server
File: apps/desktop/src/main/permission-api.ts
sequenceDiagram
participant MCP as file-permission MCP
participant API as Permission API (HTTP)
participant UI as Renderer
participant User as User
MCP->>API: POST /permission<br/>{operation, filePath}
API->>API: Generate requestId
API->>UI: permission:request event
UI->>User: Show permission dialog
User->>UI: Allow/Deny
UI->>API: permission:respond IPC
API->>MCP: HTTP response {allowed}
Configuration:
- Port: 9226
- Timeout: 5 minutes
- Interface: localhost only
Event Flow: Task Execution
stateDiagram-v2
[*] --> TaskStart: task:start IPC
TaskStart --> Validate: Validate config
Validate --> GetTM: Get TaskManager
GetTM --> CreateCallbacks: Create task-scoped callbacks
CreateCallbacks --> TMStart: taskManager.startTask()
TMStart --> CheckCap: Check capacity
CheckCap --> Exec: Execute immediately
CheckCap --> Queue: Queue task
Exec --> CreateAdapter: new OpenCodeAdapter(taskId)
CreateAdapter --> EnsureBrowser: ensureDevBrowserServer()
EnsureBrowser --> StartCLI: adapter.startTask()
StartCLI --> SpawnPTY: pty.spawn(opencode)
SpawnPTY --> Stream: Parse NDJSON stream
Stream --> Message: Emit message events
Message --> Batch: Queue message for batching
Batch --> Forward: Send batched messages
Forward --> Renderer: IPC to renderer
Message --> Complete: step_finish
Complete --> Cleanup: Dispose adapter
Cleanup --> ProcessQueue: Start next queued task
style TaskStart fill:#e3f2fd
style Exec fill:#e8f5e9
style Queue fill:#fff3e0
style Stream fill:#f3e5f5
Summary
The main process is organized into clear subsystems:
| Subsystem | File | Responsibility |
|---|---|---|
| Bootstrap | index.ts |
Electron lifecycle, window creation |
| IPC Layer | ipc/handlers.ts |
All IPC channel handlers |
| Task Management | opencode/task-manager.ts |
Parallel task orchestration |
| CLI Integration | opencode/adapter.ts |
PTY process wrapper |
| Storage | store/*.ts |
Persistent data management |
| Permission API | permission-api.ts |
File permission requests |
Key Design Principles:
- Security First: Input validation, trusted window checks, encrypted storage
- Parallel Execution: Multiple tasks run concurrently with isolation
- Performance: Message batching, debounced writes, async operations
- Reliability: Flush on quit, cleanup handlers, error recovery