Skip to content

feat: Rust runtime adapter + OC-FE Tauri frontend#1

Open
RedWoodOG wants to merge 2 commits intomainfrom
rust-runtime-adapter
Open

feat: Rust runtime adapter + OC-FE Tauri frontend#1
RedWoodOG wants to merge 2 commits intomainfrom
rust-runtime-adapter

Conversation

@RedWoodOG
Copy link
Copy Markdown
Owner

@RedWoodOG RedWoodOG commented Apr 3, 2026

Summary

  • Rust runtime adapter: Toggleable backend that connects the desktop app to openclaw-agent-rs instead of the stock Node.js gateway. Activated via OPENCLAW_RUNTIME_BACKEND=rust env var.
  • Port separation: Rust runtime runs on port 18800 (WS) + 18890 (HTTP), stock OpenClaw keeps 18789 untouched.
  • CSP fix: Inline script execution allowed so the dashboard Connect button works inside Electron.
  • Session seeding: Supports HTTP/WS port split when connecting to the Rust runtime.

New files

  • src/gateway-rust-adapter.ts — Health checks via HTTP, process spawn for start, shutdown RPC for stop, dashboard URL construction
  • src/config.ts — Added RuntimeBackend type, Rust binary/config/port/token fields

What this enables

The desktop app can now manage the Rust runtime (openclaw-agent-rs) lifecycle — start, stop, health check — and load its UI, all on separate ports from stock OpenClaw.

OC-FE (Tauri + React frontend) was also built in this session and connects to the Rust runtime on :18800 with auto-connect (no login screen). That code lives in the OC-FE repo.

Test plan

  • Run desktop with no env vars → stock behavior unchanged
  • Set OPENCLAW_RUNTIME_BACKEND=rust with Rust runtime on :18800 → connects via adapter
  • Health check, start, stop, shutdown RPC all functional
  • Stock OpenClaw on :18789 completely unaffected

Note

High Risk
High risk because the app strips security headers (CSP/X-Frame-Options) to embed the dashboard and adds logic to spawn/stop local gateway runtimes via CLI/RPC, which can affect local security posture and runtime stability.

Overview
Introduces a new openclaw-desktop Electron app for Windows that launches the OpenClaw dashboard in a frameless window, seeds local/session storage for auto-connect, and falls back to a local renderer/status.html recovery/control page when the dashboard cannot be loaded.

Adds gateway lifecycle management via src/gateway.ts (health/status checks, tokenized dashboard URL retrieval, start/stop/restart with verification) plus an optional OPENCLAW_RUNTIME_BACKEND=rust path (src/gateway-rust-adapter.ts) that manages a Rust runtime through an HTTP control plane, direct process spawning, and shutdown RPC.

Includes tray + single-instance behavior (src/tray.ts, src/main.ts), a secure IPC bridge (src/preload.ts), configurable runtime paths/ports via env (src/config.ts), and packaging/build setup (package.json, tsconfig.json, README).

Written by Cursor Bugbot for commit 0e9f512. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Introduced OpenClaw Desktop for Windows: frameless single-instance app with system tray, start/stop/restart gateway controls, runtime auto-detection, health polling, and in-app dashboard with session seeding.
    • Added a runtime status dashboard with diagnostics, recovery actions, and one-click dashboard launch.
  • Documentation

    • Added comprehensive setup, quick-start, build and installer instructions, and environment configuration notes.

- Add RuntimeBackend toggle (stock vs rust) in config.ts
- New gateway-rust-adapter.ts for direct Rust runtime communication
  (health checks, process spawn, shutdown RPC, dashboard URL)
- Gateway routing: each export delegates to Rust adapter when
  backend='rust', stock path completely unchanged
- Fix CSP to allow inline scripts in dashboard (unsafe-inline)
- Session seeding supports HTTP/WS port split for Rust runtime
- Rust runtime on port 18800, stock OpenClaw keeps 18789
- Env vars: OPENCLAW_RUNTIME_BACKEND, OPENCLAW_RS_BINARY_PATH,
  OPENCLAW_RS_CONFIG, OPENCLAW_RS_HTTP_PORT, OPENCLAW_RS_GATEWAY_TOKEN

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

Adds OpenClaw Desktop: a new Windows Electron app that detects and manages a local OpenClaw gateway (stock CLI or Rust backend), performs health checks, starts/stops/restarts the runtime, retrieves dashboard tokens, provides a recovery/status UI, and integrates tray, single-instance, and IPC preload functionality.

Changes

Cohort / File(s) Summary
Docs & Packaging
openclaw-desktop/README.md, openclaw-desktop/package.json, openclaw-desktop/tsconfig.json
New README with setup/build/dev instructions; npm scripts and electron-builder config; TypeScript build config targeting ES2022 with strict checks.
Renderer UI
openclaw-desktop/renderer/status.html
New status/recovery page showing gateway state, recorded CLI action previews, and buttons to refresh/start/restart/stop gateway or request a dashboard session; calls window.openclawDesktop.*.
Configuration
openclaw-desktop/src/config.ts
Typed desktop config and environment-aware defaults (backend, CLI/rust paths, ports, timeouts); helpers to build control/health/status URLs.
Gateway Core
openclaw-desktop/src/gateway.ts
Unified gateway types and control surface: health checks, CLI command execution, parsing results, polling for state transitions, and dashboard URL extraction (delegates to Rust adapter for rust backend).
Rust Backend Adapter
openclaw-desktop/src/gateway-rust-adapter.ts
Rust-specific adapter: spawn/track detached Rust process, HTTP health checks with timeouts, wait/poll helpers, graceful shutdown via RPC, SIGTERM fallback, and dashboard URL/token construction.
Electron Main & Window Orchestration
openclaw-desktop/src/main.ts
Electron entry: single-instance enforcement, BrowserWindow management, external URL handling, response header relaxation (CSP/x-frame-options) for embedded dashboard, storage seeding with token/endpoints, injection of frameless controls, tray wiring, and IPC handlers for gateway control and window actions.
Preload IPC Bridge
openclaw-desktop/src/preload.ts
Context-isolated API exposed as window.openclawDesktop providing typed gateway control calls and window actions via ipcRenderer.
Tray Integration
openclaw-desktop/src/tray.ts
Tray creation utility that resolves icon paths, constructs context menu (Open Dashboard, Refresh Gateway, Quit), and binds double-click to open dashboard.

Sequence Diagram(s)

sequenceDiagram
    participant App as Main Process
    participant Gateway as Gateway Module
    participant RustAdapter as Rust Adapter
    participant CLI as Gateway CLI
    participant HTTP as Gateway HTTP
    participant Renderer as Renderer / Dashboard
    participant Recovery as Recovery Page

    App->>Gateway: checkGatewayHealth()
    activate Gateway
    alt backend == rust
        Gateway->>RustAdapter: checkGatewayHealth()
        RustAdapter->>HTTP: GET /health (with timeout)
        HTTP-->>RustAdapter: 200 / error
        RustAdapter-->>Gateway: GatewayState
    else backend == stock
        Gateway->>CLI: exec status/health commands
        CLI-->>Gateway: command result
        Gateway->>HTTP: optional GET /health
        HTTP-->>Gateway: 200 / error
    end
    Gateway-->>App: GatewayState
    deactivate Gateway

    alt state != healthy
        App->>Gateway: startGatewayIntegrationPoint()
        activate Gateway
        alt backend == rust
            Gateway->>RustAdapter: startGatewayIntegrationPoint()
            RustAdapter->>RustAdapter: spawn detached process (maybe with --config)
            RustAdapter->>HTTP: poll /health until healthy or timeout
            RustAdapter-->>Gateway: GatewayLaunchResult
        else
            Gateway->>CLI: exec start command
            CLI-->>Gateway: result
            Gateway->>HTTP: poll /health until healthy or timeout
            Gateway-->>App: GatewayLaunchResult
        end
        deactivate Gateway
    end

    alt healthy
        App->>Gateway: getDashboardLaunchUrl()
        Gateway->>CLI: exec dashboard command (or RustAdapter for token)
        CLI-->>Gateway: tokenized URL / token
        Gateway-->>App: dashboard URL
        App->>Renderer: loadURL(dashboard URL)
        Renderer-->>App: rendered
    else not healthy
        App->>Recovery: load status.html
        Recovery-->>App: displayed
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

🐰
I nibbled code and seeded state,
A tray, a window — oh how great!
Health checks hop, the dashboard springs,
Rust and CLI both earn their wings,
OpenClaw Desktop, ready to skate!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title mentions Rust runtime adapter and Tauri frontend, but the changeset only implements Electron-based desktop changes and a Rust adapter integration, with no Tauri or OC-FE frontend code present in the files. Update title to accurately reflect the actual changeset: e.g., 'feat: Add Rust runtime adapter and Electron desktop app' to match the implemented files and architecture shown.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rust-runtime-adapter

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b8584e991

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const httpControlUrl = `http://127.0.0.1:${desktopConfig.gateway.httpControlPort}/rpc`;

// Try the HTTP control port first (preferred for Rust runtime).
for (const rpcUrl of [httpControlUrl, url]) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restrict Rust RPC calls to Rust control endpoint

Do not fall back to desktopConfig.gateway.baseUrl when sending Rust control RPCs. In Rust mode this fallback can target the stock gateway port (default 127.0.0.1:18789), so a failed call to the Rust control plane can end up posting gateway.shutdown to the stock runtime instead. This breaks the advertised port/runtime separation and can stop the wrong service when Rust is unhealthy or missing.

Useful? React with 👍 / 👎.

Comment on lines +246 to +250
rustProcess = spawn(binaryPath, args, {
cwd: desktopConfig.gateway.workingDirectory,
detached: true,
stdio: 'ignore',
windowsHide: true,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Handle child process spawn errors before unref

spawn(...) errors are emitted asynchronously on the child process, so the surrounding try/catch will not catch cases like invalid cwd or execution failures. Because no error listener is attached, those failures become unhandled 'error' events and can crash the Electron main process during startup attempts. Add an error handler and convert it into a failed launch result.

Useful? React with 👍 / 👎.

Comment thread openclaw-desktop/src/main.ts
Comment thread openclaw-desktop/src/gateway-rust-adapter.ts
Comment thread openclaw-desktop/src/main.ts Outdated
… clean dead code

- postRpc: Only target Rust HTTP control port, never fall back to stock
  gateway (prevents shutting down the wrong runtime)
- spawn: Add error handler on child process before unref
- tryLoadDashboard: Wire session seeding so dashboard auto-connects
- Remove unused: withGatewayDetail, detectDashboardAuthIssue,
  clearDashboardOriginState, dashboardAuthIssuePattern

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (2)
openclaw-desktop/src/gateway.ts (1)

351-362: Consider adding early return for stop when already stopped.

The function returns early for start when already healthy but doesn't do the same for stop when already stopped. While the CLI stop command is likely idempotent, adding a similar check would be consistent with the Rust adapter's stopGatewayIntegrationPoint behavior (which returns early at lines 295-303).

♻️ Suggested improvement for consistency
   if (action === 'start' && existingState.healthy) {
     return {
       attempted: false,
       action: 'noop',
       ok: true,
       message: 'Gateway already appears healthy.',
       state: existingState,
     };
   }
+
+  if (action === 'stop' && !existingState.healthy && existingState.kind === 'stopped') {
+    return {
+      attempted: false,
+      action: 'noop',
+      ok: true,
+      message: 'Gateway already stopped.',
+      state: existingState,
+    };
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/gateway.ts` around lines 351 - 362, runGatewayControl
currently returns early when action === 'start' and the gateway is healthy but
lacks a symmetrical early return for action === 'stop' when the gateway is
already stopped; update runGatewayControl to call checkGatewayHealth(), detect
when action === 'stop' and existingState.healthy is false (or an explicit
stopped flag on existingState), and return a GatewayLaunchResult with attempted:
false, action: 'noop', ok: true, a clear message like 'Gateway already
stopped.', and state: existingState to mirror the start case and match
stopGatewayIntegrationPoint behavior.
openclaw-desktop/src/gateway-rust-adapter.ts (1)

21-48: Consider extracting shared utilities.

delay, normalizeError, and fetchJson are duplicated from gateway.ts. Consider extracting these to a shared utility module (e.g., utils.ts) to reduce duplication and ensure consistent behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/gateway-rust-adapter.ts` around lines 21 - 48, The
functions delay, normalizeError, and fetchJson are duplicated; extract them into
a shared utils module (e.g., create utils.ts) that exports delay,
normalizeError, and fetchJson with the same signatures and behavior (preserve
AbortController/timeout logic in fetchJson and types). Replace the local
definitions in gateway-rust-adapter.ts (and gateway.ts) with imports from the
new utils module and remove the duplicated implementations so both files import
and use utils.delay, utils.normalizeError, and utils.fetchJson.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openclaw-desktop/package.json`:
- Line 23: The "verify" npm script in package.json currently runs "node
scripts/verify.mjs" which resolves relative to openclaw-desktop and triggers
MODULE_NOT_FOUND; update the "verify" script entry to point to the
repository-root verifier (e.g., change the value for the "verify" script from
"node scripts/verify.mjs" to "node ../scripts/verify.mjs") so npm runs the
correct script from the repo root.

In `@openclaw-desktop/README.md`:
- Around line 116-125: The README's Configuration table is missing the new Rust
backend env vars used in openclaw-desktop/src/config.ts; update the table to
include OPENCLAW_RUNTIME_BACKEND, OPENCLAW_RS_BINARY_PATH, OPENCLAW_RS_CONFIG,
OPENCLAW_RS_HTTP_PORT, and OPENCLAW_RS_GATEWAY_TOKEN with sensible defaults and
short descriptions so users can discover and configure the Rust adapter;
reference the variables exactly as named and mirror any default values or notes
from config.ts (e.g., backend selection, binary path, config file, HTTP port,
gateway auth token) to keep docs in sync with the code.

In `@openclaw-desktop/renderer/status.html`:
- Around line 99-112: Update the recovery UI copy to be backend-aware by
switching the text in the elements with IDs summary, start, restart, stop, and
open based on the active backend mode; detect the backend mode via the existing
renderer-to-main bridge (e.g., a preload-exposed flag or RPC like
window.api.getBackendMode()/invoke("backendMode")) and, when mode === "rust" or
a no-login path is reported, replace the stock CLI/service strings ("Start
gateway service", "Request fresh dashboard session", and the summary paragraph)
with Rust-specific text (e.g., "Spawn gateway process", "Request dashboard
session (no-login mode)", and a matching summary) and adjust restart/stop labels
to process-aware wording; ensure this logic runs on page load and also when the
user clicks the refresh button so the UI always reflects the current backend
mode.

In `@openclaw-desktop/src/config.ts`:
- Around line 53-72: The gateway.baseUrl and dashboard.url defaults currently
always use the stock port 18789; when resolvedBackend === 'rust' update the
default values to use the Rust gateway port (use rustHttpControlPort if
Number.isFinite(rustHttpControlPort) else 18890) so the UI targets the Rust
WS/dashboard by default. Concretely, change how gateway.baseUrl and
dashboard.url are computed in the config block: if resolvedBackend === 'rust'
construct a default URL that points to 127.0.0.1:{rustPort} (and include
trailing slash for dashboard.url as before), otherwise keep the existing 18789
defaults; reference the existing symbols gateway.baseUrl, dashboard.url,
resolvedBackend and rustHttpControlPort to locate and modify the logic.

In `@openclaw-desktop/src/gateway-rust-adapter.ts`:
- Around line 314-321: The fallback SIGTERM against rustProcessPid can hit a
stale PID; before calling process.kill(rustProcessPid, 'SIGTERM') add a safety
check that the PID is still the expected Rust process: first test existence with
process.kill(rustProcessPid, 0) inside a try, and then verify the process
identity (e.g., read /proc/<pid>/cmdline or run ps to compare the command/name)
to ensure it matches the Rust adapter binary or expected args; only send SIGTERM
if both checks pass. Implement this check as a helper (e.g., isSameProcess(pid,
expectedCmd)) and use it in the block where rustProcessPid is used to decide
whether to kill.
- Around line 18-19: The module-level rustProcess/rustProcessPid tracking is
racy because startGatewayIntegrationPoint can be invoked concurrently; add an
in-flight guard (e.g. a module-scoped boolean like isStarting or a simple mutex)
and use it inside startGatewayIntegrationPoint to serialize starts: immediately
return the existing rustProcess/rustProcessPid if a process is already running,
and if not, check and set isStarting before performing the health-check and
spawn; ensure you clear isStarting in all exit paths (success, error, or early
return) and only assign rustProcess/rustProcessPid after the child is
successfully spawned so duplicate processes are not created or left orphaned.

In `@openclaw-desktop/src/main.ts`:
- Around line 250-298: tryLoadDashboard currently only loads the URL and injects
the titlebar, but never runs the session-seeding/auth-retry helpers defined
above it, so the Rust gateway token and split HTTP/WS settings never reach the
dashboard; modify tryLoadDashboard to await the existing session-seeding and
auth-retry helper functions (the helper functions you defined above
tryLoadDashboard) before calling await window.loadURL(dashboardUrl), and ensure
their results (e.g., OPENCLAW_RS_GATEWAY_TOKEN and HTTP/WS settings) are passed
into the renderer (for example by adding them to the dashboard URL/hash or by
using window.webContents.executeJavaScript to set window-level variables) so the
dashboard boots with the correct Rust-mode configuration, then proceed with the
current CSS/JS injection as before.
- Around line 236-237: The code is logging a session-bearing URL from
getDashboardLaunchUrl(), leaking tokens; change the logging to redact sensitive
parts: compute dashboardUrl via getDashboardLaunchUrl() ??
desktopConfig.dashboard.url, but before calling log.info('Loading dashboard',
...) sanitize the URL returned by getDashboardLaunchUrl() (or if it contains a
query string or known token param name) by stripping query parameters or
replacing token/session params with "<REDACTED>" and only log the safe
components (hostname/path) or the fallback desktopConfig.dashboard.url; update
the log call that uses dashboardUrl to log the sanitized value instead
(referencing getDashboardLaunchUrl(), dashboardUrl and log.info).
- Around line 48-83: The onHeadersReceived handler
(window.webContents.session.webRequest.onHeadersReceived) is too broad and
strips X-Frame-Options/CSP for all responses and resource types, and
setWindowOpenHandler only covers window.open; add a will-navigate listener to
block or validate navigations against a trusted dashboard origin allowlist and
prevent main-frame navigations to non-allowed origins, and change the
onHeadersReceived logic to only mutate headers when details.resourceType ===
'mainFrame' and the response origin matches the trusted dashboard origin; also
sanitize the dashboard URL when logging from getDashboardLaunchUrl() to remove
any fragment/token before writing to logs.

In `@openclaw-desktop/src/preload.ts`:
- Around line 4-16: The preload currently exposes the full api object
(contextBridge.exposeInMainWorld('openclawDesktop', api)) including sensitive
methods startGateway/stopGateway/restartGateway/showDashboard to any loaded
page; restrict those by splitting or gating the bridge: expose a minimal safe
API (window control methods like minimizeWindow/maximizeWindow/closeWindow) to
all origins, and only expose or enable
startGateway/stopGateway/restartGateway/showDashboard for trusted local origins
by checking the renderer origin inside preload (e.g., evaluate location.origin
or a validated origin whitelist) before adding those functions to the exposed
object or by exposing a separate interface for trusted content; update the api
construction and the call to contextBridge.exposeInMainWorld accordingly so
untrusted remote dashboard cannot call gateway control functions.

---

Nitpick comments:
In `@openclaw-desktop/src/gateway-rust-adapter.ts`:
- Around line 21-48: The functions delay, normalizeError, and fetchJson are
duplicated; extract them into a shared utils module (e.g., create utils.ts) that
exports delay, normalizeError, and fetchJson with the same signatures and
behavior (preserve AbortController/timeout logic in fetchJson and types).
Replace the local definitions in gateway-rust-adapter.ts (and gateway.ts) with
imports from the new utils module and remove the duplicated implementations so
both files import and use utils.delay, utils.normalizeError, and
utils.fetchJson.

In `@openclaw-desktop/src/gateway.ts`:
- Around line 351-362: runGatewayControl currently returns early when action ===
'start' and the gateway is healthy but lacks a symmetrical early return for
action === 'stop' when the gateway is already stopped; update runGatewayControl
to call checkGatewayHealth(), detect when action === 'stop' and
existingState.healthy is false (or an explicit stopped flag on existingState),
and return a GatewayLaunchResult with attempted: false, action: 'noop', ok:
true, a clear message like 'Gateway already stopped.', and state: existingState
to mirror the start case and match stopGatewayIntegrationPoint behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d2b4b80-94bb-4d67-a536-2f356d4e4c1f

📥 Commits

Reviewing files that changed from the base of the PR and between a2e471f and 5b8584e.

⛔ Files ignored due to path filters (1)
  • openclaw-desktop/renderer/trayTemplate.png is excluded by !**/*.png
📒 Files selected for processing (10)
  • openclaw-desktop/README.md
  • openclaw-desktop/package.json
  • openclaw-desktop/renderer/status.html
  • openclaw-desktop/src/config.ts
  • openclaw-desktop/src/gateway-rust-adapter.ts
  • openclaw-desktop/src/gateway.ts
  • openclaw-desktop/src/main.ts
  • openclaw-desktop/src/preload.ts
  • openclaw-desktop/src/tray.ts
  • openclaw-desktop/tsconfig.json

"dev": "npm run build && electron .",
"start": "electron .",
"typecheck": "tsc --noEmit -p tsconfig.json",
"verify": "node scripts/verify.mjs",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

npm run verify points at the wrong script location.

npm resolves node scripts/verify.mjs relative to openclaw-desktop/package.json, but the provided verifier lives at the repo root. From this package directory the current script will fail with MODULE_NOT_FOUND.

One possible fix
-    "verify": "node scripts/verify.mjs",
+    "verify": "node ../scripts/verify.mjs",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"verify": "node scripts/verify.mjs",
"verify": "node ../scripts/verify.mjs",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/package.json` at line 23, The "verify" npm script in
package.json currently runs "node scripts/verify.mjs" which resolves relative to
openclaw-desktop and triggers MODULE_NOT_FOUND; update the "verify" script entry
to point to the repository-root verifier (e.g., change the value for the
"verify" script from "node scripts/verify.mjs" to "node ../scripts/verify.mjs")
so npm runs the correct script from the repo root.

Comment on lines +116 to +125
## Configuration

All settings have sensible defaults. Override with environment variables if needed:

| Variable | Default | Description |
|---|---|---|
| `OPENCLAW_GATEWAY_URL` | `http://127.0.0.1:18789` | Gateway base URL |
| `OPENCLAW_CLI_PATH` | `%APPDATA%\npm\openclaw.cmd` | Path to the OpenClaw CLI |
| `OPENCLAW_WORKDIR` | `%USERPROFILE%\.openclaw` | OpenClaw working directory |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document the new Rust backend environment variables.

openclaw-desktop/src/config.ts now reads OPENCLAW_RUNTIME_BACKEND, OPENCLAW_RS_BINARY_PATH, OPENCLAW_RS_CONFIG, OPENCLAW_RS_HTTP_PORT, and OPENCLAW_RS_GATEWAY_TOKEN, but this table still only documents the stock gateway settings. That makes the new adapter hard to discover and configure from the package docs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/README.md` around lines 116 - 125, The README's
Configuration table is missing the new Rust backend env vars used in
openclaw-desktop/src/config.ts; update the table to include
OPENCLAW_RUNTIME_BACKEND, OPENCLAW_RS_BINARY_PATH, OPENCLAW_RS_CONFIG,
OPENCLAW_RS_HTTP_PORT, and OPENCLAW_RS_GATEWAY_TOKEN with sensible defaults and
short descriptions so users can discover and configure the Rust adapter;
reference the variables exactly as named and mirror any default values or notes
from config.ts (e.g., backend selection, binary path, config file, HTTP port,
gateway auth token) to keep docs in sync with the code.

Comment on lines +99 to +112
This shell now attaches to the real local OpenClaw runtime pattern on this machine: a Windows service-managed
gateway controlled through the OpenClaw CLI. On startup it will try to recover by starting the gateway if needed,
then request a fresh tokenized dashboard URL. If that still fails, this page stays visible as a truthful recovery
and control surface instead of pretending the runtime is simply off.
</p>
<p id="summary">Checking gateway state…</p>
</div>

<div class="actions">
<button class="primary" id="refresh">Refresh status</button>
<button class="secondary" id="start">Start gateway service</button>
<button class="secondary" id="restart">Restart gateway service</button>
<button class="danger" id="stop">Stop gateway service</button>
<button class="ghost" id="open">Request fresh dashboard session</button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Make the recovery copy backend-aware.

These strings hardcode the stock CLI/service flow, but this PR also adds a Rust backend with process spawn, shutdown RPC, and optional no-login behavior. In Rust mode the page will still tell users to “start gateway service” and request a “fresh tokenized dashboard session,” which points them at the wrong remediation path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/renderer/status.html` around lines 99 - 112, Update the
recovery UI copy to be backend-aware by switching the text in the elements with
IDs summary, start, restart, stop, and open based on the active backend mode;
detect the backend mode via the existing renderer-to-main bridge (e.g., a
preload-exposed flag or RPC like
window.api.getBackendMode()/invoke("backendMode")) and, when mode === "rust" or
a no-login path is reported, replace the stock CLI/service strings ("Start
gateway service", "Request fresh dashboard session", and the summary paragraph)
with Rust-specific text (e.g., "Spawn gateway process", "Request dashboard
session (no-login mode)", and a matching summary) and adjust restart/stop labels
to process-aware wording; ensure this logic runs on page load and also when the
user clicks the refresh button so the UI always reflects the current backend
mode.

Comment on lines +53 to +72
gateway: {
backend: resolvedBackend === 'rust' ? 'rust' : 'stock',
baseUrl: process.env.OPENCLAW_GATEWAY_URL ?? 'http://127.0.0.1:18789',
healthPath: process.env.OPENCLAW_GATEWAY_HEALTH_PATH ?? '/health',
statusPath: process.env.OPENCLAW_GATEWAY_STATUS_PATH ?? '/status',
launchCommand: process.env.OPENCLAW_CLI_PATH ?? path.join(npmBinDir, 'openclaw.cmd'),
workingDirectory: process.env.OPENCLAW_WORKDIR ?? path.join(userHome, '.openclaw'),
healthTimeoutMs: 2500,
commandTimeoutMs: 12000,
startTimeoutMs: 20000,
stopTimeoutMs: 15000,
pollIntervalMs: 1000,
rustBinaryPath: process.env.OPENCLAW_RS_BINARY_PATH ?? path.join(userHome, '.openclaw', 'bin', 'openclaw-rs.exe'),
rustConfigPath: process.env.OPENCLAW_RS_CONFIG ?? path.join(userHome, '.openclaw', 'openclaw-rs.toml'),
httpControlPort: Number.isFinite(rustHttpControlPort) ? rustHttpControlPort : 18890,
rustToken: process.env.OPENCLAW_RS_GATEWAY_TOKEN,
},
dashboard: {
url: process.env.OPENCLAW_DASHBOARD_URL ?? 'http://127.0.0.1:18789/',
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Rust mode still defaults the frontend to the stock gateway port.

When OPENCLAW_RUNTIME_BACKEND=rust is the only override, baseUrl and the fallback dashboard URL still default to 18789. openclaw-desktop/src/main.ts Line 118 derives the Rust WebSocket endpoint from desktopConfig.gateway.baseUrl, so the UI keeps targeting the stock gateway unless the user manually overrides it. This needs a Rust-specific WS/dashboard default instead of reusing the stock base URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/config.ts` around lines 53 - 72, The gateway.baseUrl and
dashboard.url defaults currently always use the stock port 18789; when
resolvedBackend === 'rust' update the default values to use the Rust gateway
port (use rustHttpControlPort if Number.isFinite(rustHttpControlPort) else
18890) so the UI targets the Rust WS/dashboard by default. Concretely, change
how gateway.baseUrl and dashboard.url are computed in the config block: if
resolvedBackend === 'rust' construct a default URL that points to
127.0.0.1:{rustPort} (and include trailing slash for dashboard.url as before),
otherwise keep the existing 18789 defaults; reference the existing symbols
gateway.baseUrl, dashboard.url, resolvedBackend and rustHttpControlPort to
locate and modify the logic.

Comment on lines +18 to +19
let rustProcess: ChildProcess | null = null;
let rustProcessPid: number | null = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition risk with module-level process state.

These module-level variables track a single spawned process, but startGatewayIntegrationPoint lacks synchronization. As noted in the IPC handler context (main.ts:396-405), rapid successive calls can both pass the health check before either detects a spawned process, resulting in:

  1. Multiple processes spawned
  2. Only the last one tracked in rustProcess/rustProcessPid
  3. Orphan processes that won't be properly managed or cleaned up

Consider adding a mutex/lock or a "starting" state flag to serialize concurrent start attempts.

🔒 Proposed fix using a simple in-flight guard
 let rustProcess: ChildProcess | null = null;
 let rustProcessPid: number | null = null;
+let startInProgress = false;

Then in startGatewayIntegrationPoint:

 export async function startGatewayIntegrationPoint(): Promise<GatewayLaunchResult> {
+  if (startInProgress) {
+    return {
+      attempted: false,
+      action: 'noop',
+      ok: false,
+      message: 'Rust runtime start already in progress.',
+      state: await checkGatewayHealth(),
+    };
+  }
+
   const existingState = await checkGatewayHealth();
 
   if (existingState.healthy) {
     return {
       attempted: false,
       action: 'noop',
       ok: true,
       message: 'Rust runtime already appears healthy.',
       state: existingState,
     };
   }
+
+  startInProgress = true;
+  try {
     // ... rest of start logic ...
+  } finally {
+    startInProgress = false;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/gateway-rust-adapter.ts` around lines 18 - 19, The
module-level rustProcess/rustProcessPid tracking is racy because
startGatewayIntegrationPoint can be invoked concurrently; add an in-flight guard
(e.g. a module-scoped boolean like isStarting or a simple mutex) and use it
inside startGatewayIntegrationPoint to serialize starts: immediately return the
existing rustProcess/rustProcessPid if a process is already running, and if not,
check and set isStarting before performing the health-check and spawn; ensure
you clear isStarting in all exit paths (success, error, or early return) and
only assign rustProcess/rustProcessPid after the child is successfully spawned
so duplicate processes are not created or left orphaned.

Comment on lines +314 to +321
// Fall back to killing the tracked process.
if (rustProcessPid) {
try {
process.kill(rustProcessPid, 'SIGTERM');
} catch {
// Process may already be gone.
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor edge case: Stale PID could target wrong process.

If the Rust process exited without triggering the exit handler (e.g., if unref() disconnected event delivery and the process crashed), rustProcessPid might become stale. If the OS reassigned that PID to another process, process.kill(rustProcessPid, 'SIGTERM') could terminate an unrelated process.

This is a narrow window and unlikely in practice, but consider checking if the process is still alive or using a more robust process management approach if this becomes a concern.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/gateway-rust-adapter.ts` around lines 314 - 321, The
fallback SIGTERM against rustProcessPid can hit a stale PID; before calling
process.kill(rustProcessPid, 'SIGTERM') add a safety check that the PID is still
the expected Rust process: first test existence with
process.kill(rustProcessPid, 0) inside a try, and then verify the process
identity (e.g., read /proc/<pid>/cmdline or run ps to compare the command/name)
to ensure it matches the Rust adapter binary or expected args; only send SIGTERM
if both checks pass. Implement this check as a helper (e.g., isSameProcess(pid,
expectedCmd)) and use it in the block where rustProcessPid is used to decide
whether to kill.

Comment on lines +48 to +83
webPreferences: {
preload: preloadPath,
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
},
});

window.webContents.setWindowOpenHandler(({ url }) => {
void shell.openExternal(url);
return { action: 'deny' };
});

// The gateway sends X-Frame-Options: DENY and CSP frame-ancestors 'none',
// which prevent the page from rendering inside Electron's BrowserWindow.
// Strip these headers so the dashboard can load in the desktop app.
window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
const headers = { ...details.responseHeaders };
delete headers['x-frame-options'];
delete headers['X-Frame-Options'];

// Relax CSP so the dashboard can run inside Electron.
// Remove frame-ancestors (blocks embedding) and loosen script-src
// (blocks inline event handlers like the Connect button's onclick).
for (const key of Object.keys(headers)) {
if (key.toLowerCase() === 'content-security-policy') {
headers[key] = headers[key]!.map((policy) =>
policy
.replace(/frame-ancestors\s+[^;]+;?\s*/gi, '')
.replace(/script-src\s+[^;]+/gi, "script-src 'self' 'unsafe-inline' 'unsafe-eval'"),
);
}
}

callback({ responseHeaders: headers });
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "main.ts" | grep -E "openclaw-desktop|src"

Repository: RedWoodOG/openclaw-desktop

Length of output: 114


🏁 Script executed:

git ls-files | grep -E "openclaw-desktop.*main\.ts"

Repository: RedWoodOG/openclaw-desktop

Length of output: 98


🏁 Script executed:

wc -l openclaw-desktop/src/main.ts

Repository: RedWoodOG/openclaw-desktop

Length of output: 102


🏁 Script executed:

cat -n openclaw-desktop/src/main.ts

Repository: RedWoodOG/openclaw-desktop

Length of output: 15735


🏁 Script executed:

rg "getDashboardLaunchUrl" openclaw-desktop/src

Repository: RedWoodOG/openclaw-desktop

Length of output: 540


🏁 Script executed:

rg "will-navigate" openclaw-desktop/src

Repository: RedWoodOG/openclaw-desktop

Length of output: 52


🏁 Script executed:

rg "resourceType" openclaw-desktop/src

Repository: RedWoodOG/openclaw-desktop

Length of output: 52


🌐 Web query:

Electron webRequest onHeadersReceived details parameter resourceType

💡 Result:

In Electron's webRequest.onHeadersReceived listener, the details parameter includes a resourceType property. This is a string indicating the type of resource being requested. Possible values: mainFrame, subFrame, stylesheet, script, image, font, object, xhr, ping, cspReport, media, webSocket, or other. This is documented in the official Electron WebRequest API documentation.

Citations:


🏁 Script executed:

cat -n openclaw-desktop/src/gateway.ts | head -100

Repository: RedWoodOG/openclaw-desktop

Length of output: 3391


🏁 Script executed:

grep -A 20 "export async function getDashboardLaunchUrl" openclaw-desktop/src/gateway.ts

Repository: RedWoodOG/openclaw-desktop

Length of output: 767


🏁 Script executed:

grep -A 20 "export async function getDashboardLaunchUrl" openclaw-desktop/src/gateway-rust-adapter.ts

Repository: RedWoodOG/openclaw-desktop

Length of output: 378


Block main-frame navigations and scope the header rewrite to the trusted origin.

setWindowOpenHandler only covers popup creation via window.open(). A normal link click or location.href = ... can replace the page with content from a different origin. The session-wide onHeadersReceived callback then strips X-Frame-Options and rewrites CSP for that new origin too, turning an arbitrary navigation into privileged content. The handler lacks both origin filtering and resourceType checks.

Additionally, the dashboard URL logged at line 195 may contain a token in the URL hash (e.g., #token=... returned by getDashboardLaunchUrl()), leaking it to logs.

Add a will-navigate handler with an allowlist, and restrict the header rewrite to the trusted dashboard origin and main-frame requests only using the resourceType property in the onHeadersReceived details.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/main.ts` around lines 48 - 83, The onHeadersReceived
handler (window.webContents.session.webRequest.onHeadersReceived) is too broad
and strips X-Frame-Options/CSP for all responses and resource types, and
setWindowOpenHandler only covers window.open; add a will-navigate listener to
block or validate navigations against a trusted dashboard origin allowlist and
prevent main-frame navigations to non-allowed origins, and change the
onHeadersReceived logic to only mutate headers when details.resourceType ===
'mainFrame' and the response origin matches the trusted dashboard origin; also
sanitize the dashboard URL when logging from getDashboardLaunchUrl() to remove
any fragment/token before writing to logs.

Comment on lines +236 to +237
const dashboardUrl = (await getDashboardLaunchUrl()) ?? desktopConfig.dashboard.url;
log.info('Loading dashboard', { dashboardUrl });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't log the tokenized dashboard URL.

getDashboardLaunchUrl() returns a session-bearing URL, and this writes it verbatim to electron-log. That leaks the dashboard token into local log files.

Redact the sensitive parts before logging
   const dashboardUrl = (await getDashboardLaunchUrl()) ?? desktopConfig.dashboard.url;
-  log.info('Loading dashboard', { dashboardUrl });
+  const safeDashboardUrl = new URL(dashboardUrl);
+  safeDashboardUrl.search = '';
+  safeDashboardUrl.hash = '';
+  log.info('Loading dashboard', { dashboardUrl: safeDashboardUrl.toString() });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const dashboardUrl = (await getDashboardLaunchUrl()) ?? desktopConfig.dashboard.url;
log.info('Loading dashboard', { dashboardUrl });
const dashboardUrl = (await getDashboardLaunchUrl()) ?? desktopConfig.dashboard.url;
const safeDashboardUrl = new URL(dashboardUrl);
safeDashboardUrl.search = '';
safeDashboardUrl.hash = '';
log.info('Loading dashboard', { dashboardUrl: safeDashboardUrl.toString() });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/main.ts` around lines 236 - 237, The code is logging a
session-bearing URL from getDashboardLaunchUrl(), leaking tokens; change the
logging to redact sensitive parts: compute dashboardUrl via
getDashboardLaunchUrl() ?? desktopConfig.dashboard.url, but before calling
log.info('Loading dashboard', ...) sanitize the URL returned by
getDashboardLaunchUrl() (or if it contains a query string or known token param
name) by stripping query parameters or replacing token/session params with
"<REDACTED>" and only log the safe components (hostname/path) or the fallback
desktopConfig.dashboard.url; update the log call that uses dashboardUrl to log
the sanitized value instead (referencing getDashboardLaunchUrl(), dashboardUrl
and log.info).

Comment thread openclaw-desktop/src/main.ts
Comment on lines +4 to +16
const api = {
getGatewayState: (): Promise<GatewayState> => ipcRenderer.invoke('gateway:get-state'),
refreshGatewayState: (): Promise<GatewayState> => ipcRenderer.invoke('gateway:refresh-state'),
startGateway: (): Promise<GatewayLaunchResult> => ipcRenderer.invoke('gateway:start'),
stopGateway: (): Promise<GatewayLaunchResult> => ipcRenderer.invoke('gateway:stop'),
restartGateway: (): Promise<GatewayLaunchResult> => ipcRenderer.invoke('gateway:restart'),
showDashboard: (): Promise<void> => ipcRenderer.invoke('window:show-dashboard'),
minimizeWindow: (): void => { ipcRenderer.send('window:minimize'); },
maximizeWindow: (): void => { ipcRenderer.send('window:maximize'); },
closeWindow: (): void => { ipcRenderer.send('window:close'); },
};

contextBridge.exposeInMainWorld('openclawDesktop', api);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd openclaw-desktop && find . -name "main.ts" -o -name "preload.ts" | head -20

Repository: RedWoodOG/openclaw-desktop

Length of output: 100


🏁 Script executed:

cat -n openclaw-desktop/src/main.ts | sed -n '1,80p'

Repository: RedWoodOG/openclaw-desktop

Length of output: 3345


🏁 Script executed:

rg -n "loadURL|preload" openclaw-desktop/src/main.ts -A 2 -B 2

Repository: RedWoodOG/openclaw-desktop

Length of output: 685


🏁 Script executed:

cd openclaw-desktop && grep -n "getDashboardLaunchUrl" src/main.ts -A 5 -B 5

Repository: RedWoodOG/openclaw-desktop

Length of output: 817


🏁 Script executed:

cd openclaw-desktop && cat -n src/main.ts | sed -n '200,250p'

Repository: RedWoodOG/openclaw-desktop

Length of output: 2587


🏁 Script executed:

cd openclaw-desktop && cat -n src/gateway.ts | head -50

Repository: RedWoodOG/openclaw-desktop

Length of output: 1620


🏁 Script executed:

cd openclaw-desktop && cat -n src/preload.ts

Repository: RedWoodOG/openclaw-desktop

Length of output: 1248


Limit the preload bridge to trusted/local content.

The preload at line 46 of main.ts is attached to the same BrowserWindow that loads the remote dashboard via loadURL() at line 210. This single bridge exposes startGateway, stopGateway, restartGateway, and showDashboard to any code in that window without origin validation. The fact that lines 61–80 actively strip X-Frame-Options and relax CSP to allow 'unsafe-inline' and 'unsafe-eval' confirms the dashboard is untrusted remote content. Any XSS in the dashboard gains full access to gateway control and window manipulation. Split the bridge or gate it by origin so the remote dashboard only receives the minimal window-control methods it needs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/preload.ts` around lines 4 - 16, The preload currently
exposes the full api object (contextBridge.exposeInMainWorld('openclawDesktop',
api)) including sensitive methods
startGateway/stopGateway/restartGateway/showDashboard to any loaded page;
restrict those by splitting or gating the bridge: expose a minimal safe API
(window control methods like minimizeWindow/maximizeWindow/closeWindow) to all
origins, and only expose or enable
startGateway/stopGateway/restartGateway/showDashboard for trusted local origins
by checking the renderer origin inside preload (e.g., evaluate location.origin
or a validated origin whitelist) before adding those functions to the exposed
object or by exposing a separate interface for trusted content; update the api
construction and the call to contextBridge.exposeInMainWorld accordingly so
untrusted remote dashboard cannot call gateway control functions.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

if (desktopConfig.gateway.backend === 'rust') {
// Rust runtime splits HTTP (control plane) and WebSocket across different ports.
const httpControlUrl = `http://127.0.0.1:${desktopConfig.gateway.httpControlPort}`;
const wsUrl = desktopConfig.gateway.baseUrl.replace(/^http/, 'ws');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rust backend WebSocket URL defaults to wrong port

High Severity

When OPENCLAW_RUNTIME_BACKEND=rust is set, buildDashboardSessionSeed derives the WebSocket URL from desktopConfig.gateway.baseUrl, which defaults to http://127.0.0.1:18789 (the stock port). This produces ws://127.0.0.1:18789 — but per the PR description, the Rust runtime's WebSocket port is 18800. The config has httpControlPort for the Rust HTTP port (18890) but lacks a dedicated Rust WS port field, so the dashboard session seed sends WebSocket traffic to the stock gateway instead of the Rust runtime.

Additional Locations (1)
Fix in Cursor Fix in Web

} finally {
clearTimeout(timeout);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicated utility functions across gateway modules

Low Severity

delay, normalizeError, and fetchJson are identically implemented in both gateway.ts and gateway-rust-adapter.ts. Since the adapter already imports from gateway.ts (for types), these utilities could live in a shared module to avoid the maintenance risk of fixing a bug in one copy but not the other.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (4)
openclaw-desktop/src/main.ts (3)

194-195: ⚠️ Potential issue | 🟠 Major

Redact the dashboard token before logging.

getDashboardLaunchUrl() can return /#token=..., and this logs the full value to electron-log. That leaks a reusable session token into local log files.

Possible fix
 const dashboardUrl = (await getDashboardLaunchUrl()) ?? desktopConfig.dashboard.url;
-  log.info('Loading dashboard', { dashboardUrl });
+  const safeDashboardUrl = new URL(dashboardUrl);
+  safeDashboardUrl.search = '';
+  safeDashboardUrl.hash = '';
+  log.info('Loading dashboard', { dashboardUrl: safeDashboardUrl.toString() });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/main.ts` around lines 194 - 195, The code logs the full
dashboardUrl which may include a fragment token (e.g., `#token`=...), leaking
secrets; update the logic around
getDashboardLaunchUrl()/desktopConfig.dashboard.url so that before calling
log.info('Loading dashboard', { dashboardUrl }) you sanitize dashboardUrl by
stripping or redacting the fragment token—detect a "#token=" fragment in the
dashboardUrl variable and replace the token value with a placeholder like
"#token=[REDACTED]" (or remove the fragment entirely) so the logged dashboardUrl
contains no reusable token.

208-219: ⚠️ Potential issue | 🟠 Major

Seed storage before the first dashboard boot.

loadURL() resolves after the page has loaded, so the SPA has already started with the freshly cleared storage before seedDashboardSession() runs. The initial login/connection choice will still use empty/default values; at best the Rust settings take effect only after a manual reload. Seed at document start/preload, or reload once after writing the values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/main.ts` around lines 208 - 219, The SPA is loading
before storage is seeded so the initial auth state is incorrect; move seeding to
occur before the page starts by calling buildDashboardSessionSeed(...) and await
seedDashboardSession(window, seed) before invoking window.loadURL(dashboardUrl),
or alternatively perform the seeding in the renderer preload script (so
seedDashboardSession runs at document start) or trigger a programmatic reload
(window.reload()) immediately after seeding to ensure the SPA picks up the
values; update the sequence around loadURL, buildDashboardSessionSeed and
seedDashboardSession accordingly.

53-80: ⚠️ Potential issue | 🔴 Critical

Scope the header rewrite to the trusted dashboard origin.

This session-wide onHeadersReceived hook still strips X-Frame-Options and weakens CSP for every response, while setWindowOpenHandler() only covers popups. A main-frame navigation to any other origin will inherit the same downgrade. Restrict the rewrite to the expected dashboard origin plus details.resourceType === 'mainFrame', and block other main-frame navigations with will-navigate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/main.ts` around lines 53 - 80, The session-wide
onHeadersReceived handler is too broad; update the
window.webContents.session.webRequest.onHeadersReceived callback to only modify
headers when details.resourceType === 'mainFrame' and details.url matches the
trusted dashboard origin (match the exact origin string used for the dashboard),
leaving all other responses untouched, and ensure you still return callback({
responseHeaders: details.responseHeaders }) for non-matching requests;
additionally register a window.webContents.on('will-navigate', ...) handler that
prevents/blocks navigations to any main-frame origin other than the trusted
dashboard by calling event.preventDefault() (and optionally opening external
links with shell.openExternal) to keep untrusted main-frame navigations from
inheriting the relaxed CSP.
openclaw-desktop/src/gateway-rust-adapter.ts (1)

18-19: ⚠️ Potential issue | 🟠 Major

Serialize concurrent start requests.

This path still has a TOCTOU window between checkGatewayHealth() and spawn(). Two rapid start/restart invokes can each observe “down”, launch detached children, and leave only the last PID tracked, which makes stop/restart target the wrong process.

Also applies to: 213-266

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/gateway-rust-adapter.ts` around lines 18 - 19, There is
a TOCTOU race between checkGatewayHealth() and spawn() where concurrent
start/restart calls can each see the gateway as down and spawn detached children
while only the last PID is stored in rustProcess/rustProcessPid; serialize
start/restart by adding a start-in-progress mutex/lock or single Promise (e.g.,
startLock or startPromise) that each start/restart routine must await before
performing checkGatewayHealth() and spawn(), and ensure when a child is spawned
you set rustProcess and rustProcessPid only while still holding the lock (or
verify the spawned child belongs to this invocation) so stray children aren’t
clobbered; apply the same serialization to the other start/restart code path
where the same checkGatewayHealth()/spawn() sequence is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openclaw-desktop/src/gateway-rust-adapter.ts`:
- Around line 131-153: The code assumes any non-undefined healthPayload is
healthy; instead validate the Rust health shape (e.g. healthPayload.ok === true
or healthPayload.result?.ok === true) before returning healthy. After
fetchJson/getGatewayHealthUrl and normalizeError, check the payload shape; if it
doesn't meet the expected shape, set healthError (or wrap it with
normalizeError) and do not enter the healthy branch — only call
parseRustHealthPayload and summarizeRustHealthDetail when the payload passes the
ok/result.ok check so proxy/error pages or {ok:false} responses are treated as
unhealthy.
- Around line 245-295: Create a one-shot promise (e.g., bootFailurePromise) that
listens to rustProcess.on('error') and rustProcess.on('exit') and settles
immediately with a failure (reject or resolve with an error state) when the
process fails or exits before healthy; ensure the handlers still clear
rustProcess/rustProcessPid and normalizeError the failure to include in the
result. Then use Promise.race to race bootFailurePromise against
waitForHealthy(desktopConfig.gateway.startTimeoutMs) so startup returns fast on
real boot failures; if bootFailurePromise wins return the same shape as the
existing failure responses (attempted/action/ok/message/state using
checkGatewayHealth()).

In `@openclaw-desktop/src/main.ts`:
- Around line 104-115: The Rust branch currently builds gatewayWsUrl from
desktopConfig.gateway.baseUrl (via wsUrl) which defaults to the stock HTTP port,
causing connections to hit the wrong runtime; update the logic that returns
gatewayWsUrl in the rust branch to use the dedicated Rust websocket port (e.g.
desktopConfig.gateway.rustWsPort or a rust-specific ws URL) instead of
baseUrl—construct wsUrl from the rust ws port (or replace the port in baseUrl
with desktopConfig.gateway.rustWsPort) so gatewayWsUrl points to the Rust
runtime’s WS port; keep httpControlUrl and token logic unchanged.

---

Duplicate comments:
In `@openclaw-desktop/src/gateway-rust-adapter.ts`:
- Around line 18-19: There is a TOCTOU race between checkGatewayHealth() and
spawn() where concurrent start/restart calls can each see the gateway as down
and spawn detached children while only the last PID is stored in
rustProcess/rustProcessPid; serialize start/restart by adding a
start-in-progress mutex/lock or single Promise (e.g., startLock or startPromise)
that each start/restart routine must await before performing
checkGatewayHealth() and spawn(), and ensure when a child is spawned you set
rustProcess and rustProcessPid only while still holding the lock (or verify the
spawned child belongs to this invocation) so stray children aren’t clobbered;
apply the same serialization to the other start/restart code path where the same
checkGatewayHealth()/spawn() sequence is used.

In `@openclaw-desktop/src/main.ts`:
- Around line 194-195: The code logs the full dashboardUrl which may include a
fragment token (e.g., `#token`=...), leaking secrets; update the logic around
getDashboardLaunchUrl()/desktopConfig.dashboard.url so that before calling
log.info('Loading dashboard', { dashboardUrl }) you sanitize dashboardUrl by
stripping or redacting the fragment token—detect a "#token=" fragment in the
dashboardUrl variable and replace the token value with a placeholder like
"#token=[REDACTED]" (or remove the fragment entirely) so the logged dashboardUrl
contains no reusable token.
- Around line 208-219: The SPA is loading before storage is seeded so the
initial auth state is incorrect; move seeding to occur before the page starts by
calling buildDashboardSessionSeed(...) and await seedDashboardSession(window,
seed) before invoking window.loadURL(dashboardUrl), or alternatively perform the
seeding in the renderer preload script (so seedDashboardSession runs at document
start) or trigger a programmatic reload (window.reload()) immediately after
seeding to ensure the SPA picks up the values; update the sequence around
loadURL, buildDashboardSessionSeed and seedDashboardSession accordingly.
- Around line 53-80: The session-wide onHeadersReceived handler is too broad;
update the window.webContents.session.webRequest.onHeadersReceived callback to
only modify headers when details.resourceType === 'mainFrame' and details.url
matches the trusted dashboard origin (match the exact origin string used for the
dashboard), leaving all other responses untouched, and ensure you still return
callback({ responseHeaders: details.responseHeaders }) for non-matching
requests; additionally register a window.webContents.on('will-navigate', ...)
handler that prevents/blocks navigations to any main-frame origin other than the
trusted dashboard by calling event.preventDefault() (and optionally opening
external links with shell.openExternal) to keep untrusted main-frame navigations
from inheriting the relaxed CSP.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f5e85cbe-d1c7-48c6-8dad-b02ba4d22b21

📥 Commits

Reviewing files that changed from the base of the PR and between 5b8584e and 0e9f512.

📒 Files selected for processing (2)
  • openclaw-desktop/src/gateway-rust-adapter.ts
  • openclaw-desktop/src/main.ts

Comment on lines +131 to +153
try {
healthPayload = await fetchJson(getGatewayHealthUrl(), desktopConfig.gateway.healthTimeoutMs);
} catch (error) {
healthError = normalizeError(error);
}

if (healthPayload !== undefined) {
const facts = parseRustHealthPayload(healthPayload);
return {
healthy: true,
kind: 'healthy',
url: desktopConfig.gateway.baseUrl,
checkedAt,
detail: summarizeRustHealthDetail(healthPayload, facts),
payload: healthPayload,
facts,
checks: {
httpHealth: 'ok',
cliStatus: 'skipped',
cliHealth: 'skipped',
},
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't treat any defined /health payload as healthy.

fetchJson() returns plain text for non-JSON 200s, and this branch promotes any non-undefined payload to healthy: true. A proxy/error page or { ok: false } response on the control port will suppress startup and send the app into a dead dashboard session. Validate the expected Rust ok/result.ok shape before returning a healthy state.

Possible fix
+function isHealthyRustPayload(payload: unknown): boolean {
+  if (!payload || typeof payload !== 'object') return false;
+  const root = payload as Record<string, unknown>;
+  if (root.ok !== true) return false;
+  if (typeof root.result === 'object' && root.result) {
+    const result = root.result as Record<string, unknown>;
+    return !('ok' in result) || result.ok === true;
+  }
+  return true;
+}
+
 export async function checkGatewayHealth(): Promise<GatewayState> {
   const checkedAt = new Date().toISOString();
@@
-  if (healthPayload !== undefined) {
+  if (healthPayload !== undefined && isHealthyRustPayload(healthPayload)) {
     const facts = parseRustHealthPayload(healthPayload);
     return {
       healthy: true,
@@
     };
   }
+
+  if (healthPayload !== undefined) {
+    healthError = 'Rust runtime health endpoint returned an unexpected payload.';
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
healthPayload = await fetchJson(getGatewayHealthUrl(), desktopConfig.gateway.healthTimeoutMs);
} catch (error) {
healthError = normalizeError(error);
}
if (healthPayload !== undefined) {
const facts = parseRustHealthPayload(healthPayload);
return {
healthy: true,
kind: 'healthy',
url: desktopConfig.gateway.baseUrl,
checkedAt,
detail: summarizeRustHealthDetail(healthPayload, facts),
payload: healthPayload,
facts,
checks: {
httpHealth: 'ok',
cliStatus: 'skipped',
cliHealth: 'skipped',
},
};
}
function isHealthyRustPayload(payload: unknown): boolean {
if (!payload || typeof payload !== 'object') return false;
const root = payload as Record<string, unknown>;
if (root.ok !== true) return false;
if (typeof root.result === 'object' && root.result) {
const result = root.result as Record<string, unknown>;
return !('ok' in result) || result.ok === true;
}
return true;
}
export async function checkGatewayHealth(): Promise<GatewayState> {
const checkedAt = new Date().toISOString();
let healthPayload: unknown;
let healthError: string | null = null;
try {
healthPayload = await fetchJson(getGatewayHealthUrl(), desktopConfig.gateway.healthTimeoutMs);
} catch (error) {
healthError = normalizeError(error);
}
if (healthPayload !== undefined && isHealthyRustPayload(healthPayload)) {
const facts = parseRustHealthPayload(healthPayload);
return {
healthy: true,
kind: 'healthy',
url: desktopConfig.gateway.baseUrl,
checkedAt,
detail: summarizeRustHealthDetail(healthPayload, facts),
payload: healthPayload,
facts,
checks: {
httpHealth: 'ok',
cliStatus: 'skipped',
cliHealth: 'skipped',
},
};
}
if (healthPayload !== undefined) {
healthError = 'Rust runtime health endpoint returned an unexpected payload.';
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/gateway-rust-adapter.ts` around lines 131 - 153, The
code assumes any non-undefined healthPayload is healthy; instead validate the
Rust health shape (e.g. healthPayload.ok === true or healthPayload.result?.ok
=== true) before returning healthy. After fetchJson/getGatewayHealthUrl and
normalizeError, check the payload shape; if it doesn't meet the expected shape,
set healthError (or wrap it with normalizeError) and do not enter the healthy
branch — only call parseRustHealthPayload and summarizeRustHealthDetail when the
payload passes the ok/result.ok check so proxy/error pages or {ok:false}
responses are treated as unhealthy.

Comment on lines +245 to +295
rustProcess = spawn(binaryPath, args, {
cwd: desktopConfig.gateway.workingDirectory,
detached: true,
stdio: 'ignore',
windowsHide: true,
});

rustProcessPid = rustProcess.pid ?? null;

rustProcess.on('error', (error) => {
log.error('Rust runtime process error', { error: normalizeError(error), pid: rustProcessPid });
rustProcess = null;
rustProcessPid = null;
});

rustProcess.on('exit', (code) => {
log.info('Rust runtime process exited', { code, pid: rustProcessPid });
rustProcess = null;
rustProcessPid = null;
});

rustProcess.unref();
} catch (error) {
return {
attempted: true,
action: 'start',
ok: false,
message: `Failed to spawn Rust runtime: ${normalizeError(error)}`,
state: await checkGatewayHealth(),
};
}

const settledState = await waitForHealthy(desktopConfig.gateway.startTimeoutMs);
if (settledState) {
return {
attempted: true,
action: 'start',
ok: true,
message: 'Rust runtime started and responding to health checks.',
state: settledState,
};
}

const fallbackState = await checkGatewayHealth();
return {
attempted: true,
action: 'start',
ok: false,
message: `Rust runtime spawned but did not become healthy within timeout. Current state: ${fallbackState.kind}.`,
state: fallbackState,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f gateway-rust-adapter.ts

Repository: RedWoodOG/openclaw-desktop

Length of output: 114


🏁 Script executed:

cat -n openclaw-desktop/src/gateway-rust-adapter.ts | head -300

Repository: RedWoodOG/openclaw-desktop

Length of output: 10902


Race waitForHealthy() against a boot-failure detector to fail fast on startup errors.

The 'error' and 'exit' handlers (lines 254–264) only log and clear module state; they don't interrupt waitForHealthy() on line 277, which still polls for the full startTimeoutMs. If the binary is not executable, the cwd is invalid, or the process crashes on boot, the caller waits the entire timeout before receiving a generic timeout error instead of the real startup failure. Create a one-shot promise that settles when the process errors or exits early, and race it against waitForHealthy() to return that error immediately.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/gateway-rust-adapter.ts` around lines 245 - 295, Create
a one-shot promise (e.g., bootFailurePromise) that listens to
rustProcess.on('error') and rustProcess.on('exit') and settles immediately with
a failure (reject or resolve with an error state) when the process fails or
exits before healthy; ensure the handlers still clear rustProcess/rustProcessPid
and normalizeError the failure to include in the result. Then use Promise.race
to race bootFailurePromise against
waitForHealthy(desktopConfig.gateway.startTimeoutMs) so startup returns fast on
real boot failures; if bootFailurePromise wins return the same shape as the
existing failure responses (attempted/action/ok/message/state using
checkGatewayHealth()).

Comment on lines +104 to +115
if (desktopConfig.gateway.backend === 'rust') {
// Rust runtime splits HTTP (control plane) and WebSocket across different ports.
const httpControlUrl = `http://127.0.0.1:${desktopConfig.gateway.httpControlPort}`;
const wsUrl = desktopConfig.gateway.baseUrl.replace(/^http/, 'ws');
const rustToken = token ?? desktopConfig.gateway.rustToken;

return {
dashboardUrl,
gatewayHttpUrl: httpControlUrl,
gatewayWsUrl: wsUrl,
token: rustToken,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Rust mode is still seeded with the stock WebSocket port by default.

In the Rust branch, gatewayWsUrl is derived from desktopConfig.gateway.baseUrl. Per openclaw-desktop/src/config.ts:44-73, baseUrl still defaults to http://127.0.0.1:18789, so setting only OPENCLAW_RUNTIME_BACKEND=rust seeds ws://127.0.0.1:18789 instead of the Rust runtime’s :18800. That breaks the advertised backend switch and can connect the dashboard to the wrong runtime. Use a dedicated Rust WS port/config here instead of reusing baseUrl.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openclaw-desktop/src/main.ts` around lines 104 - 115, The Rust branch
currently builds gatewayWsUrl from desktopConfig.gateway.baseUrl (via wsUrl)
which defaults to the stock HTTP port, causing connections to hit the wrong
runtime; update the logic that returns gatewayWsUrl in the rust branch to use
the dedicated Rust websocket port (e.g. desktopConfig.gateway.rustWsPort or a
rust-specific ws URL) instead of baseUrl—construct wsUrl from the rust ws port
(or replace the port in baseUrl with desktopConfig.gateway.rustWsPort) so
gatewayWsUrl points to the Rust runtime’s WS port; keep httpControlUrl and token
logic unchanged.

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.

1 participant