Security Architecture
OpenWork implements defense-in-depth security across multiple layers. The application handles sensitive API keys, executes arbitrary code via AI agents, and manages file permissions while maintaining a secure user environment.
Security Overview
graph TB
subgraph "Application Boundaries"
RENDERER[Renderer Process<br/>Untrusted Context]
PRELOAD[Preload Script<br/>Isolated Bridge]
MAIN[Main Process<br/>Trusted Context]
EXTERNAL[External Processes<br/>CLI, MCP Servers]
end
subgraph "Security Measures"
ISO[Context Isolation]
NO_NODE[nodeIntegration: false]
VALID[Input Validation]
SANITIZE[String Sanitization]
TRUST[Trusted Window Check]
ENCRYPT[AES-256-GCM Storage]
PERM[File Permission Gates]
HTTP[Local HTTP Only]
end
RENDERER --> ISO
RENDERER --> NO_NODE
RENDERER --> PRELOAD
PRELOAD --> VALID
PRELOAD --> SANITIZE
PRELOAD --> MAIN
MAIN --> TRUST
MAIN --> ENCRYPT
MAIN --> PERM
MAIN --> EXTERNAL
EXTERNAL --> HTTP
style RENDERER fill:#ffebee
style PRELOAD fill:#fff3e0
style MAIN fill:#e8f5e9
Process Isolation
Security Boundaries
sequenceDiagram
participant UI as Renderer UI
participant Pre as Preload Script
participant Main as Main Process
participant OS as Operating System
Note over UI: Web Context Only
UI->>Pre: window.accomplish.method()
Note over Pre: Isolated Context
Pre->>Pre: Validate and sanitize
Pre->>Main: ipcRenderer.invoke
Note over Main: Trusted Context
Main->>Main: Verify trusted window
Main->>Main: Execute with full Node.js access
Main->>OS: File system, processes, crypto
Renderer Security Configuration
Source: apps/desktop/src/main/index.ts lines 91-95
webPreferences: {
preload: preloadPath,
nodeIntegration: false, // Critical: No Node.js in renderer
contextIsolation: true, // Critical: Isolated context
}
Security Impact:
| Setting | Value | Protection |
|---|---|---|
| nodeIntegration | false |
Renderer cannot access Node.js APIs directly |
| contextIsolation | true |
Preload script is isolated from renderer global scope |
| sandbox | true (default) |
Renderer runs in chromium sandbox |
contextBridge API
Source: apps/desktop/src/preload/index.ts lines 139-139
contextBridge.exposeInMainWorld('accomplish', accomplishAPI);
Security Properties:
- One-way: Only exposes specific functions to renderer
- Typed: Full TypeScript type safety
- Limited: No direct access to
ipcRenderer, only wrapped methods
IPC Security
Input Validation Chain
graph TB
subgraph "Validation Layers"
L1[Layer 1: Type Check<br/>Zod schemas]
L2[Layer 2: String Sanitization<br/>sanitizeString]
L3[Layer 3: Length Limits<br/>Max 8000 chars]
L4[Layer 4: Trusted Window<br/>Focus check]
L5[Layer 5: Handler Logic<br/>Business validation]
end
subgraph "Data Flow"
INPUT[Raw Input]
VALIDATED[Validated Data]
HANDLER[Handler Execution]
end
INPUT --> L1
L1 --> L2
L2 --> L3
L3 --> L4
L4 --> L5
L5 --> VALIDATED
VALIDATED --> HANDLER
style INPUT fill:#ffebee
style VALIDATED fill:#e8f5e9
style HANDLER fill:#e3f2fd
String 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 (string only)
- Empty string rejection
- Maximum length enforcement
- Whitespace trimming
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;
}
Threat Mitigated: Prevents other windows (including potential malicious windows) from making privileged IPC calls.
API Key Storage
Security Architecture
graph TB
subgraph "Storage Options Trade-off"
KEYCHAIN[keytar<br/>OS Keychain]
CUSTOM[Custom Encryption<br/>electron-store]
end
subgraph "Keychain Issues"
PROMPT[macOS prompts<br/>User friction]
PERM[Permission dialogs<br/>On every access]
end
subgraph "Custom Solution"
DERIVE[Key Derivation<br/>Machine-bound]
SALT[Random Salt<br/>Per install]
ITER[100k Iterations<br/>PBKDF2]
AES[AES-256-GCM<br/>Encryption]
end
KEYCHAIN -.->|Rejected due to UX| PROMPT
KEYCHAIN -.->|Rejected due to UX| PERM
CUSTOM --> DERIVE
DERIVE --> SALT
SALT --> ITER
ITER --> AES
style KEYCHAIN fill:#fff3e0
style CUSTOM fill:#e8f5e9
Key Derivation
Source: apps/desktop/src/main/store/secureStorage.ts lines 68-94
function getDerivedKey(): Buffer {
if (_derivedKey) {
return _derivedKey;
}
// Combine machine-specific values
const machineData = [
os.platform(),
os.homedir(),
os.userInfo().username,
app.getPath('userData'),
'ai.accomplish.desktop',
].join(':');
const salt = getSalt();
// Use PBKDF2 to derive a 256-bit key
_derivedKey = crypto.pbkdf2Sync(
machineData,
salt,
100000, // iterations
32, // key length (256 bits)
'sha256'
);
return _derivedKey;
}
Security Properties:
| Property | Value | Security Benefit |
|---|---|---|
| Algorithm | PBKDF2-SHA256 | Proven key derivation function |
| Iterations | 100,000 | Slows brute force attacks |
| Key Length | 256 bits | AES-256 compatible |
| Salt | Random, per-install | Prevents rainbow table attacks |
| Machine Data | Platform + home + user + app | Keys bound to machine |
Encryption Process
Source: apps/desktop/src/main/store/secureStorage.ts lines 100-113
sequenceDiagram
participant API as API Layer
participant Crypto as Node.js crypto
participant Store as electron-store
API->>API: storeApiKey(provider, key)
API->>Crypto: getDerivedKey()
Crypto->>Crypto: pbkdf2Sync(machineData, salt)
Crypto-->>API: 256-bit key
API->>Crypto: createCipheriv(aes-256-gcm, key, iv)
API->>Crypto: randomBytes(12) for IV
Crypto->>Crypto: Encrypt with GCM mode
Crypto-->>API: ciphertext + authTag
API->>API: Format "iv:authTag:ciphertext"
API->>Store: Store encrypted value
Note over Store: At rest encryption
Decryption Process
Source: apps/desktop/src/main/store/secureStorage.ts lines 118-143
sequenceDiagram
participant API as API Layer
participant Store as electron-store
participant Crypto as Node.js crypto
API->>Store: getApiKey(provider)
Store-->>API: Encrypted value
API->>API: Split "iv:authTag:ciphertext"
API->>Crypto: getDerivedKey()
Crypto-->>API: 256-bit key
API->>Crypto: createDecipheriv(aes-256-gcm, key, iv)
API->>Crypto: setAuthTag(authTag)
Crypto->>Crypto: Verify and decrypt
alt Valid
Crypto-->>API: Decrypted API key
API-->>API: Return key
else Invalid (wrong key, tampering)
Crypto-->>API: Error
API-->>API: Return null
end
Security Considerations
Strengths:
- AES-256-GCM provides authenticated encryption
- Keys encrypted at rest
- Machine-bound derivation prevents portability
- No macOS Keychain prompts
Limitations (acknowledged in comments):
- Key derivation could be reverse-engineered
- Less secure than OS Keychain
- Suitable for rotatable API keys (not passwords)
File Permission System
Permission Flow
sequenceDiagram
participant AI as AI Agent
participant MCP as file-permission MCP
participant API as Permission API (HTTP)
participant Main as Main Process
participant UI as Renderer UI
participant User as User
AI->>MCP: request_file_permission({operation, path})
MCP->>API: POST /permission
API->>API: Generate requestId
API->>Main: mainWindow.webContents.send
Main->>UI: permission:request event
UI->>User: Show permission dialog
User->>UI: Allow/Deny
UI->>Main: permission:respond IPC
Main->>API: resolvePermission(requestId, allowed)
API->>MCP: HTTP response {allowed}
MCP->>AI: Return result
Source: apps/desktop/src/main/permission-api.ts
Permission API Server
Configuration:
- Port: 9226
- Interface: localhost only (127.0.0.1)
- Timeout: 5 minutes per request
Security Measures:
- Localhost Only: Not exposed to network
- CORS Headers: Local requests only
- Request Validation: Operation type, file path validation
- Task Scoping: Links permission to active task ID
- Timeout: Prevents indefinite blocking
File Operation Types
Source: apps/desktop/src/main/permission-api.ts lines 114-119
const validOperations = ['create', 'delete', 'rename', 'move', 'modify', 'overwrite'];
if (!validOperations.includes(data.operation)) {
res.writeHead(400, { 'Content-Type': 'application/json'});
res.end(JSON.stringify({ error: `Invalid operation` }));
return;
}
Supported Operations:
| Operation | Description | Risk Level |
|---|---|---|
| create | Create new file | Medium |
| delete | Delete file/folder | High |
| rename | Rename file | Medium |
| move | Move file | Medium |
| modify | Change file content | Medium |
| overwrite | Replace file entirely | High |
URL and External Link Security
Shell Open Handler
Source: apps/desktop/src/main/ipc/handlers.ts lines 934-945
handle('shell:open-external', async (_event: IpcMainInvokeEvent, url: string) => {
try {
const parsed = new URL(url);
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error('Only http and https URLs are allowed');
}
await shell.openExternal(url);
} catch (error) {
console.error('Failed to open external URL:', error);
throw error;
}
});
Protections:
- Protocol validation (http/https only)
- URL parsing validation
- Error handling and logging
Blocked Protocols:
file://(local file access)javascript:(code execution)data:(potential XSS)- Custom protocols (except
accomplish://)
PTY Security
Process Spawning Security
Source: apps/desktop/src/main/opencode/adapter.ts
Security Considerations:
- Working Directory: Uses temp directory to avoid TCC prompts
const safeCwd = config.workingDirectory || app.getPath('temp'); - Shell Selection: Avoids user shell in packaged apps
// In packaged macOS apps, use /bin/sh to avoid loading user configs if (app.isPackaged && process.platform === 'darwin') { return '/bin/sh'; } - Environment Control: Clean environment with only necessary variables
const env: NodeJS.ProcessEnv = { ...process.env }; // Add only what's needed env.ANTHROPIC_API_KEY = apiKey; env.PATH = bundledPath + PATH_DELIMITER + env.PATH;
Command Quoting
Source: Lines 143-162
const fullCommand = [command, ...allArgs].map(arg => {
if (process.platform === 'win32') {
if (arg.includes(' ') || arg.includes('"')) {
return `"${arg.replace(/"/g, '\\"')}"`;
}
return arg;
} else {
if (arg.includes("'") || arg.includes(' ') || arg.includes('"')) {
return `'${arg.replace(/'/g, "'\\''")}'`;
}
return arg;
}
}).join(' ');
Purpose: Prevent command injection through argument injection
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();
}
});
}
Security Benefits:
- Prevents race conditions
- Single point of control for resources
- Prevents multiple instances from interfering
Clean Start Data Wipe
Source: apps/desktop/src/main/index.ts lines 19-34
if (process.env.CLEAN_START === '1') {
const userDataPath = app.getPath('userData');
console.log('[Clean Mode] Clearing userData directory:', userDataPath);
try {
if (fs.existsSync(userDataPath)) {
fs.rmSync(userDataPath, { recursive: true, force: true });
console.log('[Clean Mode] Successfully cleared userData');
}
} catch (err) {
console.error('[Clean Mode] Failed to clear userData:', err);
}
}
Security Use Case: Wipe all data if compromise suspected
Threat Model
Mitigated Threats
| Threat | Mitigation | Component |
|---|---|---|
| Renderer Code Injection | Context isolation, no nodeIntegration | Preload |
| IPC Message Injection | Input validation, sanitization | IPC handlers |
| API Key Theft | AES-256-GCM encryption at rest | secureStorage |
| Unauthorized File Access | Permission gates for all operations | Permission API |
| Command Injection | Shell quoting, PTY isolation | OpenCode adapter |
| URL Injection | Protocol validation (http/https only) | Shell handler |
| Window Hijacking | Trusted window check | IPC handlers |
| Process Overflow | Message batching, max length limits | IPC, StreamParser |
Accepted Risks
| Risk | Justification | Mitigation |
|---|---|---|
| API Key in Memory | Required for CLI execution | Cleared on process exit |
| Custom Encryption | Keychain UX unacceptable | Documented limitations |
| Local HTTP Server | Needed for MCP communication | localhost only |
| PTY Process | Required for interactive CLI | Isolated, temp directory |
Security Best Practices
1. Defense in Depth
graph TB
L1[Layer 1: Renderer Sandbox]
L2[Layer 2: Context Isolation]
L3[Layer 3: Input Validation]
L4[Layer 4: Business Logic Checks]
L5[Layer 5: OS Permissions]
L1 --> L2 --> L3 --> L4 --> L5
2. Principle of Least Privilege
- Renderer has NO direct Node.js access
- Preload has limited Node.js APIs
- Main process validates all operations
- File operations require explicit user consent
3. Fail Securely
// Default to deny if validation fails
function sanitizeString(input: unknown, field: string): string {
if (typeof input !== 'string') {
throw new Error(`${field} must be a string`);
}
// ...
}
4. Audit Logging
All security-relevant operations logged:
- IPC handler invocations
- Permission requests
- API key access
- Process spawns
- Errors and failures
Compliance Considerations
Data Storage
- API Keys: Encrypted at rest with AES-256-GCM
- Task History: Stored locally, user-controlled
- Settings: Stored in electron-store (plain text)
- No Telemetry: All external logging removed
User Privacy
- Local-First: All data stored locally
- No Cloud Sync: No account system
- User Control: User can wipe data via CLEAN_START
- Transparent: Open source codebase
Summary
OpenWork’s security architecture provides:
| Layer | Protection | Implementation |
|---|---|---|
| Process Isolation | Renderer sandboxing | contextBridge, no nodeIntegration |
| IPC Security | Input validation | Sanitization, length limits, trusted window |
| Data Protection | API key encryption | AES-256-GCM, machine-bound keys |
| File Access | Permission gates | User approval required for all operations |
| Process Security | PTY isolation | Temp directory, shell selection |
| Network Security | URL validation | http/https only, localhost APIs |
Key Design Principles:
- Security First: Validate all inputs, check window focus
- Defense in Depth: Multiple security layers
- User Control: Explicit permission for sensitive operations
- Fail Securely: Default to deny on validation failure
- Transparency: Documented limitations and trade-offs