How `ui-control-server.mjs` Enables Browser Automation in OpenWork
ui-control-server.mjs implements a local HTTP bridge that exposes the OpenWork desktop renderer as a secure, REST-automatable target for external scripts, plugins, and test harnesses.
OpenWork's browser automation architecture centers on a lightweight server module that runs inside the Electron main process. Located at apps/desktop/electron/ui-control-server.mjs, this file provides the foundation for headless UI testing, AI-driven workflows, and third-party integrations by translating HTTP requests into JavaScript execution within the renderer context.
The Bridge Architecture
The createUiControlServer factory function returns a lifecycle manager with start() and stop() methods. When invoked, it spins up a Node.js http server on a random loopback port and generates cryptographically secure credentials.
// From apps/desktop/electron/ui-control-server.mjs
const uiControlToken = randomBytes(32).toString("hex");
This design ensures that every automation session receives unique, unguessable credentials that expire when the server shuts down.
Authentication and Security
All endpoints require a Bearer token in the Authorization header. The authorizedUiControlRequest function performs strict string comparison against the generated token.
function authorizedUiControlRequest(request) {
const auth = request.headers.authorization ?? "";
return auth === `Bearer ${uiControlToken}`;
}
A failed authentication returns HTTP 401 immediately, preventing unauthorized access even from local processes.
Discovery Mechanism for External Processes
The server writes a JSON discovery file to openwork-ui-control.json in Electron's userData directory with the following schema:
version– protocol versionappNameandidentifier– application metadataplatform– host operating systembaseUrl–http://127.0.0.1:<random_port>token– the bearer token for authentication
The file path is also exported via the OPENWORK_UI_CONTROL_DISCOVERY environment variable, enabling child processes to locate the bridge without hardcoded paths.
// Lines 106-122 in ui-control-server.mjs
const discovery = {
version: 1,
appName,
appIdentifier,
platform: process.platform,
baseUrl,
token: uiControlToken,
};
Renderer Communication via executeJavaScript
The bridge's core capability relies on evaluateOpenworkControl, which evaluates expressions in the renderer's context through Electron's webContents.executeJavaScript:
async function evaluateOpenworkControl(expression) {
const win = await getWindow();
return win.webContents.executeJavaScript(expression, true);
}
This mechanism routes all HTTP endpoints to a global window.__openworkControl object injected into the renderer:
| HTTP Method | Endpoint | Renderer Call |
|---|---|---|
GET |
/snapshot |
window.__openworkControl.snapshot() |
GET |
/actions |
window.__openworkControl.listActions() |
GET |
/context |
window.__openworkControl.context() |
POST |
/query |
window.__openworkControl.query(payload) |
POST |
/command |
window.__openworkControl.command(payload) |
POST |
/execute |
window.__openworkControl.execute(actionId, args) |
Request Handling and Error Management
Every response includes Cache-Control: no-store to prevent stale automation state. Successful operations return { ok: true, ...payload }. Errors are captured and serialized as HTTP 500 with diagnostic details.
The route handlers (lines 140-175) follow a consistent pattern:
- Validate authentication
- Parse and validate request body for POST routes
- Execute the corresponding renderer method
- Return structured JSON or catch and report exceptions
Lifecycle Management
The stop() method performs clean shutdown:
- Deletes the discovery file from disk
- Closes the HTTP server
- Releases the bound port
This prevents socket leaks and ensures that stale discovery files never outlive their corresponding server instance.
Practical Usage Examples
Starting the Server in Your Electron App
import { createUiControlServer } from "./ui-control-server.mjs";
const uiCtrl = createUiControlServer({
appName: "OpenWork",
appIdentifier: "com.different-ai.openwork",
getWindow: async () => mainWindow,
});
await uiCtrl.start();
// Server is now accepting authenticated requests
Connecting from a Plugin or Test Harness
const discoveryPath = process.env.OPENWORK_UI_CONTROL_DISCOVERY;
const { baseUrl, token } = JSON.parse(
require("fs").readFileSync(discoveryPath, "utf8")
);
const authHeader = { Authorization: `Bearer ${token}` };
Capturing UI State
async function getSnapshot(baseUrl, authHeader) {
const res = await fetch(`${baseUrl}/snapshot`, {
headers: authHeader,
});
const { payload } = await res.json();
return payload; // { route: "/settings/general", ... }
}
Executing Registered Actions
async function executeAction(baseUrl, authHeader, actionId, args = {}) {
const res = await fetch(`${baseUrl}/execute`, {
method: "POST",
headers: { ...authHeader, "Content-Type": "application/json" },
body: JSON.stringify(args),
});
const { ok, result, error } = (await res.json()).payload;
if (!ok) throw new Error(error);
return result;
}
// Usage
await executeAction(baseUrl, authHeader, "session.create_task", {
title: "Automated task",
});
Key Implementation Files
| Path | Responsibility |
|---|---|
apps/desktop/electron/ui-control-server.mjs |
HTTP bridge, token generation, discovery file, routing |
apps/desktop/electron/main.mjs |
Factory instantiation and lifecycle coordination |
evals/specs/**/*.test.ts |
Production usage examples in test suites |
dev/evals/packages/cdp/src/app-state.ts |
Automation readiness detection via window.__openworkControl |
Summary
ui-control-server.mjscreates a token-secured HTTP bridge inside the Electron main process- Random loopback port allocation with discovery file emission enables reliable external connectivity
webContents.executeJavaScriptroutes all automation commands into the renderer without window visibility requirements- Six REST endpoints expose UI state inspection, action enumeration, and execution capabilities
- Clean lifecycle management ensures no resource leaks between automation sessions
Frequently Asked Questions
What makes ui-control-server.mjs different from standard browser automation tools?
Unlike Selenium or Playwright, which control browsers through external protocols, ui-control-server.mjs runs inside the Electron process and communicates via executeJavaScript. This eliminates the need for visible windows, CDP connections, or remote debugging ports while providing direct access to the application's internal state.
How does the token authentication work across process boundaries?
The server generates a 256-bit random token on startup and writes it to a discovery file. Child processes read this file via the OPENWORK_UI_CONTROL_DISCOVERY environment variable. Because the token changes on every launch and the server only binds to loopback addresses, external network attackers cannot access the automation API.
Can this bridge run in headless or CI environments?
Yes. The bridge operates entirely through webContents.executeJavaScript without requiring window visibility. As implemented in OpenWork's evaluation suite (evals/specs/**/*.test.ts), the automation surface functions reliably in headless CI pipelines where no display server is available.
What happens if the window.__openworkControl object is missing?
Endpoints return HTTP 500 with descriptive errors. The automation readiness check in dev/evals/packages/cdp/src/app-state.ts specifically tests for this object's presence before attempting operations, allowing callers to wait or retry until the renderer has fully initialized its control surface.
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 →