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:

  1. Validate Task Config (line 267)
    • Sanitize prompt, taskId, sessionId
    • Validate allowedTools array
    • Check workingDirectory path
  2. Initialize Permission API (lines 270-274)
    • Only on first task start
    • Starts HTTP server on port 9226
  3. Create Task-Scoped Callbacks (lines 286-371)
    • Message batching for performance
    • Status change notifications
    • Debug logging when enabled
  4. Start Task via TaskManager (line 374)
    • Creates isolated adapter or queues task
  5. 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:

  1. Parallel Execution: Up to 10 concurrent tasks
  2. Per-Task Isolation: Each task gets its own adapter
  3. Queue Management: Tasks queued when at capacity
  4. 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:

  1. Security First: Input validation, trusted window checks, encrypted storage
  2. Parallel Execution: Multiple tasks run concurrently with isolation
  3. Performance: Message batching, debounced writes, async operations
  4. Reliability: Flush on quit, cleanup handlers, error recovery

Back to top

OpenWork Documentation - Community documentation for accomplish-ai/openwork