Embedded Browser Architecture in Magnitude: BrowserWindow and Preload Scripts Explained
Magnitude's Electron renderer uses a hardened BrowserWindow with context isolation and a strictly controlled preload script to safely bridge the web-based UI to native Node.js capabilities.
Magnitude is an open-source testing framework for AI agents, and its desktop application embeds a Chromium-based browser using Electron. The embedded browser architecture centers on two core components: the BrowserWindow instance created in the main process and a preload script that exposes a minimal, typed RPC interface to the renderer. This design follows Electron's security best practices while enabling the Magnitude SDK to power the UI.
BrowserWindow Configuration in the Main Process
The main process instantiates the embedded browser in desktop/src/electron-rpc.ts. This file handles window creation, IPC registration, and the secure loading of the Magnitude web client.
Security-First Window Options
The BrowserWindow constructor explicitly disables dangerous defaults:
// desktop/src/electron-rpc.ts (architectural pattern)
const win = new BrowserWindow({
width: 1024,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // isolated preload entry
contextIsolation: true, // enforces context separation
nodeIntegration: false, // denies direct Node access
sandbox: true, // enables Chromium sandbox
},
});
win.loadURL('app://./index.html');
contextIsolation: true— Forces the preload script to run in an isolated JavaScript world, preventing prototype pollution attacks from the web content.nodeIntegration: false— Ensures the renderer cannot accessrequire()or Node.js APIs.sandbox: true— Applies OS-level process sandboxing to the renderer.
The app:// protocol is registered by the main process to serve bundled assets without exposing the local filesystem directly.
Preload Script Architecture
The preload script at desktop/preload.ts (compiled to preload.js by Vite) is the sole communication channel between the Magnitude UI and the Electron main process.
Exposed API Surface
Using contextBridge, the preload exposes a single window.magnitude object with strictly typed methods:
// desktop/preload.ts (excerpt)
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('magnitude', {
invoke: (channel: string, ...args: any[]) =>
ipcRenderer.invoke(channel, ...args),
on: (channel: string, listener: (...args: any[]) => void) =>
ipcRenderer.on(channel, (_event, ...args) => listener(...args)),
send: (channel: string, ...args: any[]) =>
ipcRenderer.send(channel, ...args),
});
No other globals are exposed. The UI must route all native operations through these three methods.
Type Safety and Validation
The preload does not perform business logic. Instead, it forwards serialized messages that are validated against Effect-TS schemas on both sides:
- Outbound: The SDK in
packages/sdk/src/rpc.tsdefines RPC contracts as Effect schemas. - Inbound: The main process unwraps effects and returns serialized results.
This preserves type safety across the process boundary without exposing schema definitions to the renderer.
RPC Flow: From UI to SDK
The embedded browser architecture implements a four-stage request lifecycle:
- UI Invocation — Renderer code calls
window.magnitude.invoke('agent.getStatus', payload). - IPC Forwarding — Preload forwards to
ipcRenderer.invoke, which serializes across the process boundary. - Main Handling —
desktop/src/electron-rpc.tsreceives the channel, executes the corresponding SDK effect frompackages/sdk/src/rpc.ts, and awaits resolution. - Response Return — Result serializes back through
ipcMain.handle, resolves in the preload, and returns to the UI as a Promise.
// Main process handler registration (desktop/src/electron-rpc.ts pattern)
ipcMain.handle('agent.getStatus', async (_event, params) => {
const effect = agentRpc.getStatus(params); // Effect-TS effect
return await Effect.runPromise(effect); // executes in main process
});
// UI consumption (renderer context)
async function checkStatus() {
const status = await window.magnitude.invoke('agent.getStatus');
// status is typed via Effect-TS schema inference
}
Build and Bundling Configuration
The Vite configuration at desktop/electron.vite.config.ts produces distinct build targets for the preload and renderer:
| Target | Entry | Output | Build Options |
|---|---|---|---|
| Preload | desktop/preload.ts |
preload.js |
target: 'node', format: 'cjs', no minification of IPC channels |
| Renderer | desktop/src/main.tsx |
index.html + assets |
Standard web build, imports from client-common and sdk |
This separation prevents the large UI bundle from bloating the preload script, keeping the isolated context lightweight and inspectable.
Key Source Files
desktop/src/electron-rpc.ts—BrowserWindowcreation and IPC handler registrationdesktop/preload.ts— Isolated preload script exposingwindow.magnitudedesktop/electron.vite.config.ts— Dual-target Vite configurationpackages/sdk/src/rpc.ts— SDK RPC definitions and Effect-TS schemaspackages/client-common/src/effect-query.ts— Client-side query layer callingwindow.magnitude.invoke
Summary
- BrowserWindow in
desktop/src/electron-rpc.tscreates a sandboxed, context-isolated renderer with no direct Node access. - Preload script in
desktop/preload.tsexposes only three IPC methods viacontextBridge, forming a minimal attack surface. - Effect-TS schemas in
packages/sdk/src/rpc.tsenforce type safety across the process boundary without exposing types to the renderer. - Vite dual build separates preload and renderer bundles for optimal loading and security auditing.
Frequently Asked Questions
Why does Magnitude disable nodeIntegration in the BrowserWindow?
Disabling nodeIntegration prevents untrusted web content from accessing Node.js APIs like fs or child_process. Magnitude's UI renders user-provided test configurations and agent outputs, so this hardening is essential. All native operations flow through the typed window.magnitude bridge instead.
How does the preload script maintain type safety without TypeScript in the renderer?
The preload exposes untyped JavaScript methods, but the Effect-TS schemas in packages/sdk/src/rpc.ts validate every argument and return value. The client-common package wraps the raw window.magnitude.invoke calls with generated TypeScript clients, so developers get full autocomplete and compile-time checking without exposing schema code to the isolated renderer.
What is the app:// protocol used for?
The app:// protocol serves the bundled web assets (HTML, JS, CSS) through a custom handler registered in the main process. This avoids file:// protocol limitations and security restrictions while keeping assets offline. The protocol is registered before the BrowserWindow loads its URL.
Can the preload script be modified at runtime?
No. The preload script is loaded from a bundled file (preload.js) whose path is resolved at startup via path.join(__dirname, 'preload.js'). In packaged builds, this file resides in an ASAR archive. contextIsolation further prevents the renderer from modifying or replacing the exposed window.magnitude object after creation.
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 →