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 version
  • appName and identifier – application metadata
  • platform – host operating system
  • baseUrl – 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:

  1. Validate authentication
  2. Parse and validate request body for POST routes
  3. Execute the corresponding renderer method
  4. Return structured JSON or catch and report exceptions

Lifecycle Management

The stop() method performs clean shutdown:

  1. Deletes the discovery file from disk
  2. Closes the HTTP server
  3. 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.mjs creates a token-secured HTTP bridge inside the Electron main process
  • Random loopback port allocation with discovery file emission enables reliable external connectivity
  • webContents.executeJavaScript routes 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →