Skip to content

Headless deployments: HITL tools cannot open a visible window — working CDP-relay workaround (remote web viewer) #12

Description

@jsapede

Summary

When cortex-scout runs on a headless server (no DISPLAY, no X server), the HITL tools (hitl_web_fetch, scout_agent_profile_auth) cannot open a visible browser window. They exist in the binary (full --all-features build) but fail at runtime because there is no display to present the interactive window to a human.

This issue documents a working workaround: a small CDP relay that exposes the existing headless Chromium session (the one cortex-scout already manages via chromiumoxide) as a web viewer, giving a human operator remote interactive control (click, type, scroll, navigate) from any browser — effectively a portable HITL channel for headless deployments.

Environment

  • cortex-scout v3.3.7 built with cargo build --release --all-features on Debian 13 (trixie), x86_64
  • Headless VM: no DISPLAY, no X11/Wayland session
  • Chromium 150 system package
  • Observed behavior of hitl_web_fetch on this host: fails with user cancelled (the visible window cannot be created; no human can interact with it)

Why noVNC-style VNC is the wrong path

A classic VNC setup (Xvfb + x11vnc + websockify + noVNC) would require launching a second Chromium in non-headless mode. That means:

  • a different browser instance than the one cortex-scout is actually using → the CAPTCHA/auth wall being solved is not the one the agent is stuck on;
  • profile lock conflicts if the same --user-data-dir is reused;
  • cookies/session state divergence.

The interesting fact is that cortex-scout already runs a headless Chromium with a live CDP endpoint (chromiumoxide launches it with --remote-debugging-port=0, which binds a random localhost port). That endpoint is enough to build a faithful remote HITL channel with zero new browser instances.

The workaround: CDP relay → web viewer

Architecture:

[headless Chromium (cortex-scout)]
        │ CDP (ws://127.0.0.1:<random port>, loopback only)
        ▼
[relay: Node.js, ws@8]
   - discovers the CDP port via `ss -tlnp` (the chromium argv still says
     `--remote-debugging-port=0`, so /proc parsing does NOT give the real port)
   - connects to the page target
   - Page.startScreencast (JPEG frames)  ──► viewers
   - Input.dispatchMouseEvent / Input.dispatchKeyEvent / Page.navigate ◄── viewers
        │ HTTP :9092 + ?token= (auth)
        ▼
[browser of the human operator: static page, <canvas> + WebSocket]

Validation performed on the real system:

  • CDP discovery: ss -tlnp reliably finds the chromium listener (argv keeps --remote-debugging-port=0);
  • Page.startScreencast streams JPEG frames continuously (confirmed 3+ frames on visual change);
  • Input.dispatchMouseEvent (mouseMoved/mousePressed/mouseReleased) and Input.dispatchKeyEvent are accepted with no error;
  • Page.navigate works from the viewer;
  • the session is the same browser cortex-scout uses: cookies/profile untouched, same process PID observed from both the MCP tool and the relay;
  • scout_browser_automate and visual_scout keep working normally after a reconnect.

This turns the existing headless session into a remote "visible browser" for the duration of a wall, without touching cortex-scout's code.

Remaining gaps / not yet solved

  • Port discovery across restarts: the CDP port changes on every cortex-scout restart; the relay currently re-discovers it with ss each time a viewer connects. A stable channel (e.g. a documented fixed port or a handshake file) would be cleaner.
  • Multiple concurrent clients: Chrome tolerates several CDP clients, but concurrent viewer sessions were not stress-tested against an active scout_browser_automate run.
  • Auth/TLS: the PoC uses a static query-string token; production use should use TLS + stronger auth, and never expose the raw CDP port (full browser control) directly.
  • Walls that need a real human click with device-level trust (e.g. strict reCAPTCHA v3 scoring) may still flag CDP-dispatched input; this is inherent to any CDP-based approach.

Possible project-level improvements (nice to have)

  1. Document the headless + HITL limitation and this relay pattern in the README.
  2. Consider a CORTEX_SCOUT_CDP_PORT env var to pin --remote-debugging-port (instead of 0) for stable external attach.
  3. Consider a built-in "remote HITL" transport (e.g. serve a small viewer next to the HTTP server when CORTEX_SCOUT_REMOTE_HITL=1) so no external relay is needed.

Links

  • Prior context: issue dependancies for HITL #4 "dependancies for HITL" (build deps for the full build on Debian) — closed; this issue is about the headless runtime limitation, not the build.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions