# How `ui-control-server.mjs` Enables Browser Automation in OpenWork

> Discover how ui-control-server.mjs facilitates browser automation in OpenWork. This local HTTP bridge exposes the desktop renderer for secure, REST-automatable scripts and plugins.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: internals
- Published: 2026-08-17

---

**`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.

```javascript
// 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.

```javascript
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`](https://github.com/different-ai/openwork/blob/main/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.

```javascript
// 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`:

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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`](https://github.com/different-ai/openwork/blob/main/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`](https://github.com/different-ai/openwork/blob/main/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.