Skip to content

Implement Docker support, WebUI API, and stealth extension#719

Open
aleks1k wants to merge 2 commits intobrowser-use:mainfrom
MinglesAI:openclaw-custom
Open

Implement Docker support, WebUI API, and stealth extension#719
aleks1k wants to merge 2 commits intobrowser-use:mainfrom
MinglesAI:openclaw-custom

Conversation

@aleks1k
Copy link

@aleks1k aleks1k commented Feb 28, 2026

OpenClaw custom: Docker, WebUI API, stealth extension, docs" -b "Custom build for OpenClaw: Docker/docker-compose tweaks, WebUI HTTP API (browser_api), startup scripts, stealth extension, docs (WEBUI_HTTP_API.md, CLI_AND_DOCKER_CDP.md). No browser profiles or secrets in repo.


Summary by cubic

Adds Dockerized startup browser with CDP forwarding, a small WebUI HTTP API for direct browser control, and a stealth extension to make automation more stable and easier to integrate. Also improves Docker startup reliability (CDP readiness wait, stale profile lock cleanup, optional headless) and docs so Python/CLI can connect on :9222 without exposing sensitive ports.

  • New Features

    • Docker: socat forwards 9222→9223; startup browser via supervisord with a hardened script (clears stale profile locks, auto-finds Chromium, waits for CDP; headless via STARTUP_BROWSER_HEADLESS); localhost bindings for 7788/5901/9222; volumes for data/profiles/output.
    • WebUI API: stable endpoints (goto, get_url, get_title, screenshot) plus agent actions (run_agent, stop_agent, pause_resume_agent, clear_agent). Auto-connects to BROWSER_CDP when set.
    • Stealth: bundled extension loaded via Playwright launch args to reduce detection.
    • Docs: WEBUI_HTTP_API.md and CLI_AND_DOCKER_CDP.md; README notes for using CDP from host.
    • Utilities: verify_playwright.py to sanity-check Chromium.
  • Migration

    • Set LAUNCH_BROWSER_AT_STARTUP=true and BROWSER_CDP=http://127.0.0.1:9222 to control the container’s browser; Chrome listens on 9223 internally.
    • Use PLAYWRIGHT_LAUNCH_ARGS (includes --remote-debugging-port=9223 and the stealth extension path) and BROWSER_USER_DATA=/profiles/agent-default; mount ./profiles to persist. Optionally set STARTUP_BROWSER_HEADLESS=1.
    • Call API via /gradio_api/call/<name> or gradio_client (see WEBUI_HTTP_API.md).

Written for commit 027783b. Summary will update on new commits.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 issues found across 16 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/startup_browser.py">

<violation number="1" location="scripts/startup_browser.py:33">
P2: startup_browser.py only strips the "--user-data-dir=..." form. If PLAYWRIGHT_LAUNCH_ARGS contains the two-token form ("--user-data-dir /path"), user_data_dir remains None and the script falls back to a default profile while still passing the raw --user-data-dir arguments into Chromium, ignoring the intended profile. Handle the two-token form as well to avoid silently using the wrong profile.</violation>
</file>

<file name="docs/CLI_AND_DOCKER_CDP.md">

<violation number="1" location="docs/CLI_AND_DOCKER_CDP.md:79">
P2: Documentation suggests pointing CDP to a server IP without warning that the remote debugging port is unauthenticated and should be restricted; this can lead to exposing full browser control if 9222 is reachable publicly.</violation>
</file>

<file name="stealth-extension/stealth.js">

<violation number="1" location="stealth-extension/stealth.js:1">
P2: The stealth overrides are wrapped in a bare arrow function that is never invoked, so none of the property overrides execute.</violation>

<violation number="2" location="stealth-extension/stealth.js:14">
P2: `navigator.permissions` getter returns a new object each access, so the later override of `query` does not persist and the notifications-specific behavior is never applied.</violation>

<violation number="3" location="stealth-extension/stealth.js:43">
P2: Syntax error: window.chrome object literal is not closed, so the script cannot parse.</violation>

<violation number="4" location="stealth-extension/stealth.js:49">
P2: Using HTMLNavigator.prototype will throw ReferenceError in standard browsers; the Navigator interface is the correct prototype for userAgent overrides.</violation>
</file>

<file name="stealth-extension/manifest.json">

<violation number="1" location="stealth-extension/manifest.json:14">
P2: manifest.json is missing the closing `}` for the top-level object, making the JSON invalid and causing the extension to fail to load.</violation>
</file>

<file name="src/webui/browser_api.py">

<violation number="1" location="src/webui/browser_api.py:137">
P2: api_screenshot promises a base64 image but can return a filesystem path when "path" is present, breaking the API contract and potentially leaking local paths to consumers expecting base64 data.</violation>
</file>

<file name="docker-compose.yml">

<violation number="1" location="docker-compose.yml:68">
P2: noVNC is exposed on 0.0.0.0 while the default VNC_PASSWORD is empty, which results in a blank VNC password being generated and allows unauthenticated access to the VNC/noVNC UI from any host.</violation>
</file>

Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

If the host is not the same machine as the server (e.g. you’re on a laptop and the container runs on a server), replace `127.0.0.1` with the server’s IP or hostname:

```python
CDP_URL = "http://YOUR_SERVER_IP:9222"
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Documentation suggests pointing CDP to a server IP without warning that the remote debugging port is unauthenticated and should be restricted; this can lead to exposing full browser control if 9222 is reachable publicly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/CLI_AND_DOCKER_CDP.md, line 79:

<comment>Documentation suggests pointing CDP to a server IP without warning that the remote debugging port is unauthenticated and should be restricted; this can lead to exposing full browser control if 9222 is reachable publicly.</comment>

<file context>
@@ -0,0 +1,133 @@
+If the host is not the same machine as the server (e.g. you’re on a laptop and the container runs on a server), replace `127.0.0.1` with the server’s IP or hostname:
+
+```python
+CDP_URL = "http://YOUR_SERVER_IP:9222"
+```
+
</file context>
Fix with Cubic

get: () => [1, 2, 3, 4, 5],
});

Object.defineProperty(navigator, 'permissions', {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: navigator.permissions getter returns a new object each access, so the later override of query does not persist and the notifications-specific behavior is never applied.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At stealth-extension/stealth.js, line 14:

<comment>`navigator.permissions` getter returns a new object each access, so the later override of `query` does not persist and the notifications-specific behavior is never applied.</comment>

<file context>
@@ -0,0 +1,51 @@
+        get: () => [1, 2, 3, 4, 5],
+    });
+
+    Object.defineProperty(navigator, 'permissions', {
+        get: () => ({
+            query: () => Promise.resolve({ state: 'granted' }),
</file context>
Fix with Cubic

get: () => window.chrome,
});

Object.defineProperty(HTMLNavigator.prototype, 'userAgent', {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Using HTMLNavigator.prototype will throw ReferenceError in standard browsers; the Navigator interface is the correct prototype for userAgent overrides.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At stealth-extension/stealth.js, line 49:

<comment>Using HTMLNavigator.prototype will throw ReferenceError in standard browsers; the Navigator interface is the correct prototype for userAgent overrides.</comment>

<file context>
@@ -0,0 +1,51 @@
+        get: () => window.chrome,
+    });
+
+    Object.defineProperty(HTMLNavigator.prototype, 'userAgent', {
+        get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+    });
</file context>
Fix with Cubic

@@ -0,0 +1,51 @@
() => {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The stealth overrides are wrapped in a bare arrow function that is never invoked, so none of the property overrides execute.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At stealth-extension/stealth.js, line 1:

<comment>The stealth overrides are wrapped in a bare arrow function that is never invoked, so none of the property overrides execute.</comment>

<file context>
@@ -0,0 +1,51 @@
+() => {
+    Object.defineProperty(navigator, 'webdriver', {
+        get: () => false,
</file context>
Fix with Cubic

canary: false,
loadTimes: () => {},
csi: () => {},
;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Syntax error: window.chrome object literal is not closed, so the script cannot parse.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At stealth-extension/stealth.js, line 43:

<comment>Syntax error: window.chrome object literal is not closed, so the script cannot parse.</comment>

<file context>
@@ -0,0 +1,51 @@
+        canary: false,
+        loadTimes: () => {},
+        csi: () => {},
+    ;
+
+    Object.defineProperty(navigator, 'chrome', {
</file context>
Fix with Cubic

"permissions": [
"webNavigation",
"tabs"
]
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: manifest.json is missing the closing } for the top-level object, making the JSON invalid and causing the extension to fail to load.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At stealth-extension/manifest.json, line 14:

<comment>manifest.json is missing the closing `}` for the top-level object, making the JSON invalid and causing the extension to fail to load.</comment>

<file context>
@@ -0,0 +1,14 @@
+  "permissions": [
+    "webNavigation",
+    "tabs"
+  ]
</file context>
Fix with Cubic

if isinstance(out, dict) and "base64" in out:
return out["base64"]
if hasattr(out, "get"):
return out.get("base64") or out.get("path")
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: api_screenshot promises a base64 image but can return a filesystem path when "path" is present, breaking the API contract and potentially leaking local paths to consumers expecting base64 data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/webui/browser_api.py, line 137:

<comment>api_screenshot promises a base64 image but can return a filesystem path when "path" is present, breaking the API contract and potentially leaking local paths to consumers expecting base64 data.</comment>

<file context>
@@ -0,0 +1,165 @@
+            if isinstance(out, dict) and "base64" in out:
+                return out["base64"]
+            if hasattr(out, "get"):
+                return out.get("base64") or out.get("path")
+        return None
+    except Exception as e:
</file context>
Fix with Cubic


# VNC Settings
- VNC_PASSWORD=${VNC_PASSWORD:-youvncpassword}
- VNC_PASSWORD=${VNC_PASSWORD:-}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: noVNC is exposed on 0.0.0.0 while the default VNC_PASSWORD is empty, which results in a blank VNC password being generated and allows unauthenticated access to the VNC/noVNC UI from any host.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docker-compose.yml, line 68:

<comment>noVNC is exposed on 0.0.0.0 while the default VNC_PASSWORD is empty, which results in a blank VNC password being generated and allows unauthenticated access to the VNC/noVNC UI from any host.</comment>

<file context>
@@ -43,30 +44,35 @@ services:
 
       # VNC Settings
-      - VNC_PASSWORD=${VNC_PASSWORD:-youvncpassword}
+      - VNC_PASSWORD=${VNC_PASSWORD:-}
 
     volumes:
</file context>
Fix with Cubic

…docker-compose.yml for clarity, improved browser launch script to handle stale profile locks, find Chromium executable, and wait for CDP port readiness. Added support for headless mode and ensured proper environment variable handling.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/startup_browser.py">

<violation number="1" location="scripts/startup_browser.py:104">
P2: SIGTERM/SIGINT only terminates the child process; the parent keeps sleeping and polling for the port, delaying shutdown up to the full startup timeout. Consider exiting immediately or setting a shutdown flag checked by the wait loop.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

)

def on_sigterm(*_args: object) -> None:
proc.terminate()
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: SIGTERM/SIGINT only terminates the child process; the parent keeps sleeping and polling for the port, delaying shutdown up to the full startup timeout. Consider exiting immediately or setting a shutdown flag checked by the wait loop.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/startup_browser.py, line 104:

<comment>SIGTERM/SIGINT only terminates the child process; the parent keeps sleeping and polling for the port, delaying shutdown up to the full startup timeout. Consider exiting immediately or setting a shutdown flag checked by the wait loop.</comment>

<file context>
@@ -1,66 +1,123 @@
+    )
+
+    def on_sigterm(*_args: object) -> None:
+        proc.terminate()
+
+    signal.signal(signal.SIGTERM, on_sigterm)
</file context>
Suggested change
proc.terminate()
proc.terminate()
raise SystemExit(0)
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants