Hyperbolic Gateway

Codebase Lowdown

Hyperbolic Gateway — architecture, structure and findings Version 1.4.5 · 22 October 2026

This document explains what the code does, how the parts connect, and what to fix next. It uses short sentences and simple terms. It is a map, not a tutorial. For install and build steps, see BUILDING.md. For the tool list, see the README.


1. What this program does

Hyperbolic Gateway is a desktop application. It runs a real Chromium browser on the local machine. An AI agent controls that browser through the Model Context Protocol (MCP).

The agent does not use a remote browser. It does not use a headless browser. The browser is visible, and it keeps cookies, tabs, downloads and memory between sessions.

The program has two processes:

  1. The Electron app. It owns the window, the browser tabs, the HUD, the database and the local files. It listens on http://127.0.0.1:4111.
  2. The MCP server. Each agent client starts its own copy (bin/mcp-server.cjs). It speaks JSON-RPC over stdin/stdout and forwards each tool call to the Electron app over HTTP.

The Cyberdeck window with the HUD and the built-in manual

The program also has one optional decision path. If a decision model is configured, the agent can call decide or drive. These tools ask a model for a typed choice. The model never writes text for the page. The agent supplies text.


2. System map

flowchart LR
  Agent["Agent / MCP client<br/>(OpenCode, Claude, Cursor, ...)"]
  MCP["MCP server<br/>bin/mcp-server.cjs<br/>stdio JSON-RPC"]
  Bridge["HTTP bridge<br/>127.0.0.1:4111"]
  Chassis["BrowserChassis<br/>orchestrator"]
  Tabs["WebContentsView tabs"]
  Prim["Primitives<br/>observe · act · wait_for · extract"]
  Marks["Set-of-Marks overlay"]
  Input["CDP input + Bézier telemetry"]
  Cortex[("LanceDB Cortex")]
  Vaults[("Session / script / download vaults")]
  HUD["HUD renderer<br/>settings · recorder · decisions"]
  Web[("Live web")]

  Agent -->|stdio| MCP
  MCP -->|HTTP| Bridge
  Bridge --> Chassis
  Chassis --> Tabs
  Chassis --> Prim
  Chassis --> Marks
  Chassis --> Input
  Chassis --> Cortex
  Chassis --> Vaults
  Tabs --> Web
  Input --> Tabs
  HUD -. IPC .-> Chassis

The bridge is the only control surface. The MCP server is stateless. All state lives in the Electron app.


3. Process and window model

Part Process Job
src/main/index.ts Electron main Window, IPC, bridge start, updater, recording service
src/preload/index.ts Preload Safe window.electronAPI surface for the HUD
src/renderer/main.ts HUD renderer Controls, logs, settings, MCP client snippets
src/renderer/recording.ts HUD renderer Desktop capture and VP9/WebM encoding
WebContentsView per tab Chromium renderer The real pages the agent drives
src/mcp/bridge-server.ts Separate Node process stdio MCP server; forwards to the bridge

The HUD and the web pages are different renderers. The HUD is not a web page. A tool call cannot reach window.electronAPI. This separation is deliberate: page JavaScript (evaluate_javascript) must not be able to change settings or read local files.


4. Call path

sequenceDiagram
  participant A as Agent
  participant M as MCP server
  participant B as HTTP bridge
  participant C as BrowserChassis
  participant P as Page (WebContentsView)

  A->>M: tools/call { name: "act", arguments }
  M->>B: POST /act (JSON)
  B->>C: chassis.act(...)
  C->>P: CDP Input.dispatchMouseEvent
  P-->>C: DOM changes / events
  C-->>B: result object
  B-->>M: JSON response
  M-->>A: text content (+ image block)

Rules that the code depends on:


5. Directory map

src/
  main/            Electron main process
    index.ts       entry point, window, IPC, service wiring
    browser-chassis.ts   central orchestrator (1 865 lines)
    http-bridge.ts       HTTP routes (1 162 lines)
    primitives/          observe, act, wait_for, extract, ...
    marks/               Set-of-Marks overlay and targeting
    telemetry/           Bézier pointer, typing, fingerprints
    tabs/                multi-tab WebContentsView manager
    cortex/              LanceDB memory and embedder
    harvester/           Readability + Turndown markdown
    shield/              consent/GDPR dismissal
    hil/                 human-in-the-loop requests
    decide/              optional typed decision client + guard
    drive/               observe → decide → act loop
    recording/           timelapse capture service
    downloads/           download routing and parsing
    session/             cookie/storage vaults
    scripts/             script vault
    network/             CDP network + stream capture
    search/ crawl/ research/ prerender/ snapshots/ llm/ updates/ tray/ menu/ utils/
  mcp/             MCP server and tool catalog
    tools-catalog.ts     tool schemas, endpoints, formatters
    bridge-server.ts     stdio server, resources, dispatch
  preload/         contextBridge API
  renderer/        HUD (main.ts) and timelapse encoder (recording.ts)
  shared/          shared types and version

public/            welcome manual, assets, icons
scripts/           build, release, tests, one-off tools
docs/              architecture notes, field notes, this file
bin/               bundled MCP server (unpacked in installers)

All 46 TypeScript files are reachable from four entry points: src/main/index.ts, src/preload/index.ts, src/renderer/main.ts and src/mcp/bridge-server.ts. There are no unreachable TypeScript modules.


6. Main subsystems

6.1 BrowserChassis

src/main/browser-chassis.ts is the coordinator. It owns the active tab, the viewport bounds, the primitives object, the network capture, the harvester, the download manager and the memory writes. It is the largest file in the project. New code should not make it larger. See finding F4.

6.2 Orthogonal primitives

src/main/primitives/primitives.ts exposes the preferred tool surface:

observe · act · wait_for · expect · find · extract · collect_all · page_state · diff · export_har.

The rules are:

6.3 Set-of-Marks

src/main/marks/marks-overlay.ts adds numbered badges to interactive elements. It traverses open shadow roots and computes a safe click point. The mark table is cheap text. The image is optional.

6.4 Input and telemetry

src/main/telemetry/input-simulator.ts sends input over CDP. It uses Bézier pointer paths and Gaussian typing delays. CDP input is focus-independent: the app does not need the OS foreground. The fallback is webContents.sendInputEvent.

6.5 Memory

src/main/cortex/cortex-db.ts uses LanceDB with a vector column and a BM25 full-text index. A deterministic 128-dimension embedder keeps the install small. Visited pages, downloads, bookmarks and research results are indexed.

6.6 Decision layer (optional)

src/main/decide/decision-client.ts calls a hosted TypeSafe model or a local ONNX model. The result is a typed choice: a component and an operation. decision-guard.ts rejects unknown targets, low confidence and impossible actions. src/main/drive/driver.ts repeats observe → decide → act until it finishes, needs a human, or runs out of steps. Age gates and CAPTCHAs are human hand-offs. They are detected before any model call.

6.7 Recording

The main process owns the file and the config. The HUD renderer owns capture and encoding. Frames are captured at intervalMs and stamped 1000/fps apart. This is true timelapse. MediaRecorder cannot retime frames, so the code uses WebCodecs and webm-muxer.

6.8 Bridge and catalog

tools-catalog.ts maps each tool name to a Zod schema, an HTTP route and a formatter. http-bridge.ts serves those routes. npm run test:mcp:contract parses both files and fails if a tool points at a route that does not exist. Keep this test green.


7. Findings

Orphans and dead weight

ID Finding Evidence Recommendation
F1 Many one-off scripts are not wired into package.json. About 30 scripts/*.js files (test-tabs.js, test-shield-breaker.js, browse-reddit.js, ...) are not referenced by any npm script. Some use the old scripts/mcp-server.js path. Move them to scripts/archive/, or delete them. Keep only maintained .mjs suites.
F2 The MCP bundle exists twice. bin/mcp-server.cjs and scripts/mcp-server.js are built from the same entry and are byte-identical. Clients use bin/. Only one legacy test uses scripts/. Keep bin/mcp-server.cjs. Delete the second output, or make it a copy only when a legacy test needs it.
F3 Docs counts drift. src/mcp/README.md still says 59 advertised / 81 registered. The real values are 62 advertised (+2 optional) / 82 registered. The README badges said 1.4.2 / 59 tools. Fix the badge (done) and the sub-README. Generate counts from the catalog.
F4 Five files are too large to review safely. browser-chassis.ts 1 865 lines; tools-catalog.ts 1 615; renderer/main.ts 1 465; primitives.ts 1 350; http-bridge.ts 1 162. Split by domain. Chassis: navigation, tabs, capture, memory. Bridge: one router per tool group. HUD: settings, connect, recorder.
F5 The root contains a corrupt image file. manual_screenshot.png starts with 75 AB 5A 8A, not a PNG signature, and is gitignored. Delete it. Use docs/assets/manual.png.

Irregularities and risks

ID Finding Impact Recommendation
R1 Mutable state is split between ./data/ and app.getPath('userData'). data/ is relative to the current working directory. It can be read-only when the app is launched from / (macOS) or a protected folder. Recording config already uses userData. Move all mutable state to userData. Keep data/ only for developer runs.
R2 The bridge has no authentication token. Any local process can drive the browser and read captured data. Binding to 127.0.0.1 limits this to the same machine. Add an optional shared token (KUANG_BRIDGE_TOKEN) for shared machines.
R3 CI builds but does not run the offline test suites. build.yml runs pnpm build only. The contract test is cheap and needs no app. Add npm run test:mcp:contract to the workflow.
R4 Most tests need a running app. The primitives, decide and surface suites drive the live Cyberdeck. They cannot run on a clean runner. Add a small unit layer for pure functions (tags, config, formatting) and keep live suites for release checks.
R5 webm-muxer is deprecated upstream. It still works and is small, but it will not receive fixes. Swap to Mediabunny in one release. The dependency is isolated to src/renderer/recording.ts.
R6 The window and taskbar icon are not set. The app shows the default Electron icon in the taskbar and window switcher. Set icon on BrowserWindow and the tray to public/assets/mark-eleven.png (next release).
R7 Logo assets are heavy and duplicated. public/assets holds ~5.8 MB of PNG + JPG + SVG copies of the same three marks. The README loads all three PNGs at 30% width. Use one optimized format per use. Keep PNG only for installer icons.

Optimisations, in order of value

  1. Split the monoliths (F4). This reduces review risk more than any micro-optimisation.
  2. Move state to userData (R1). This fixes a class of macOS and packaged-app failures.
  3. Prune scripts and duplicate bundles (F1, F2). Less code to scan, smaller repository.
  4. Add the contract test to CI (R3). One minute of CI prevents a class of broken tools.
  5. Trim assets (R7). About 5 MB saved in the repository, and a faster README.
  6. Enable noUnusedLocals / noUnusedParameters. The current tsconfig disables both. Turn them on file by file, or fix the existing warnings first.
  7. Lazy-load heavy dependencies. @huggingface/transformers and the ONNX runtime dominate install size. Load them only when the local decision model or embedder is used.

8. Walkthrough: search a flight on an airline site

This example shows the normal tool order. The site is generic. Selectors are illustrative. Always prefer a mark ID from observe or find over a guessed CSS selector.

Goal: find one-way economy flights from London to New York on 14 November 2026, and return the cheapest three.

// 1. Confirm the bridge and the active tab.
{ "tool": "bridge_health" }

// 2. Open the airline. Consent banners are dismissed automatically by default.
{ "tool": "navigate_to", "arguments": { "url": "https://www.example-airline.com" } }

// 3. Read the page as compact markdown. Look for the "Book a flight" entry point.
{ "tool": "observe", "arguments": { "as": "markdown" } }

// 4. Find the link by role and name. This returns a mark ID.
{ "tool": "find", "arguments": { "role": "link", "name": "Book a flight" } }

// 5. Click the mark. `act` reports what changed: URL, revision, marks.
{ "tool": "act", "arguments": { "mark_id": 4, "action": "click" } }

// 6. Wait for the booking form, not for a fixed number of seconds.
{ "tool": "wait_for", "arguments": { "conditions": [
  { "selector": "form[data-testid=flight-search]" },
  { "network_idle": true }
] } }

// 7. Choose one-way, then type the origin. Pick the suggestion; do not just press Enter.
{ "tool": "act", "arguments": { "selector": "label[for=trip-oneway]", "action": "click" } }
{ "tool": "act", "arguments": { "selector": "#origin", "action": "type", "text": "London" } }
{ "tool": "wait_for", "arguments": { "conditions": [ { "selector": "[role=option]", "text": "London (LHR)" } ] } }
{ "tool": "act", "arguments": { "selector": "[role=option]", "action": "click" } }

// 8. Type the destination and the date, then submit.
{ "tool": "act", "arguments": { "selector": "#destination", "action": "type", "text": "New York" } }
{ "tool": "act", "arguments": { "selector": "#depart-date", "action": "type", "text": "2026-11-14" } }
{ "tool": "act", "arguments": { "selector": "button[type=submit]", "action": "click" } }

// 9. Wait for results. If the page paginates, `collect_all` harvests the list.
{ "tool": "wait_for", "arguments": { "conditions": [
  { "selector": "[data-testid=flight-card]" },
  { "count": { "selector": "[data-testid=flight-card]", "min": 1 } }
] } }

// 10. Extract structured rows, then sort by price in the agent.
{ "tool": "extract", "arguments": {
  "kind": "list",
  "selector": "[data-testid=flight-card]"
} }

Rules for this workflow:


9. Commands

Command Use
pnpm dev Development app with live reload
pnpm build Type-check, bundle the app and build the MCP server
pnpm build:mcp Rebuild only the MCP server bundle
pnpm start Run the production bundle
npm run test:mcp:contract Static check: every catalog tool maps to a bridge route
npm run test:mcp:surface Live check: advertised tools, hidden aliases, deprecations
npm run test:primitives Offline primitives (needs a running app)
npm run test:act / test:harvest / test:markdown Live browser behaviour
npm run test:decide Decision client, guard and gate tests
npm run test:focus Focus independence (foreground another app)
npm run manual:tools Regenerate the Manual tool list from the README table
npm run bench:decision Page-decision benchmark
node scripts/release-notes.mjs v1.4.5 Print a CHANGELOG section for a release
node scripts/check-release-tag.mjs v1.4.5 Verify tag and package.json agree

Release flow: push a v* tag. CI builds Windows, macOS and Linux. The publish job uploads the assets. A patch tag publishes itself; a minor or major tag stays a draft for a human. See BUILDING.md §4.


10. How to change the code safely

Add a tool

  1. Add the entry to src/mcp/tools-catalog.ts. Give it an endpoint, a Zod schema and a formatter.
  2. Add the route to src/main/http-bridge.ts.
  3. Run npm run test:mcp:contract.
  4. Run npm run test:mcp:surface with the app open.
  5. Add a row to the README tool table. Run npm run manual:tools.

Change the browser behaviour

  1. Prefer the primitives layer. Add a primitive before you add a one-off tool.
  2. Keep act honest: return evidence, and a typed error when the action did nothing.
  3. Add a live test in scripts/test-*.mjs.

Change settings

Settings are IPC-only. The HUD calls window.electronAPI. Do not add an HTTP route that writes settings. The recording tool gate depends on this rule.

Release

  1. Update package.json, src/shared/version.ts and CHANGELOG.md.
  2. Run npx tsc --noEmit, npm run build, npm run test:mcp:contract.
  3. Commit, tag vX.Y.Z, push the tag.
  4. Watch the CI run. For a minor or major tag, publish the draft after a check.

11. Glossary

Term Meaning
Bridge Local HTTP server on 127.0.0.1:4111 that accepts tool calls.
Chassis The BrowserChassis object. It coordinates tabs, primitives, memory and input.
Mark A numbered badge on an interactive element. The agent uses the number to act.
Primitive A small, composable browser operation (observe, act, wait_for, ...).
HUD The control window around the browser tab: header, telemetry, settings.
Cortex The local LanceDB memory.
HIL Human-in-the-loop: a request that stops the agent and asks a person to act.
OKF The in-app knowledge format: guide, playbooks and tool recommendations.
Timelapse A recording that stores frames at a low real-time rate and plays them at a higher rate.