The Windows Node feature allows the tray app to receive commands from the OpenClaw agent (canvas, screenshots, screen recordings, camera, location, notifications, and controlled command execution). This is experimental and must be explicitly enabled in Settings.
- Open the tray app
- Right-click → Settings
- Scroll to "ADVANCED (EXPERIMENTAL)"
- Toggle "Enable Node Mode" ON
- Click Save
For app-owned local WSL setup, after OpenClaw onboard completes or is explicitly skipped, setup runs the pinned gateway CLI's non-interactive baseline initializer against the final runtime workspace and then injects fixed Windows-node guidance into that workspace's AGENTS.md. The injected block is setup-owned and idempotently replaced between managed markers, preserving user-authored content and file permissions outside those markers and leaving OpenClaw source files unchanged.
Note on the apply script's WSL invocation. The WindowsNodeBootstrapContextStep apply and rollback scripts are piped to bash -s via stdin (RunInWslAsync(..., inputViaStdin: true)) rather than the default bash -c argv path. This is required because wsl.exe performs shell variable expansion on argv before invoking bash, which would drop user-defined $var references in the multi-line script (workspace='...' followed by mkdir -p "$workspace" becomes mkdir -p ""). See docs/WSL_EXE_ARGV_PITFALL.md for the full writeup.
The guidance helps the first companion-app OpenClaw session route Windows desktop, files, screenshots, camera, notifications, browser proxy, and Windows command tasks through the Windows node / nodes tool.
For changes touching tray UX, Settings, onboarding, chat/canvas, Command Center, Windows node capabilities, local MCP, gateway pairing/connection, permissions, or diagnostics, use .agents/skills/openclaw-proof-validation/SKILL.md.
Short version: run required tests, collect a closeout proof pass with .\run-app-local.ps1 -Isolated when UI is involved, use computer-use or developer-provided screenshots/output for the active changed UI state, prove MCP with winnode or raw JSON-RPC, prove gateway paths when available, and include current-head concrete output under ## Real behavior proof. Mid-development computer-use/MCP/rubber-duck validation is fine when explicitly requested or needed to unblock work.
Every new Windows node call must be exposed through local MCP and winnode: register the capability, update McpToolBridge.CommandDescriptions, update src/OpenClaw.WinNode.Cli/skill.md, add focused tests, and prove discovery/invocation with winnode or raw MCP JSON-RPC.
- Verify the toggle appears in Settings under "ADVANCED"
- Verify it saves and persists across app restarts
- Enable Node Mode and save
- Watch for "🔌 Node Mode Active" toast notification
- Check logs at
%LOCALAPPDATA%\OpenClawTray\openclaw-tray.logfor:[INFO] Starting Windows Node connection to ws://... [INFO] Node connected, waiting for challenge... [INFO] Registered capability: screen (2 commands) [INFO] All capabilities registered [INFO] Node status: Connected
- When the agent captures your screen, you should see "📸 Screen Captured" toast
- This is throttled to max once per 10 seconds
- Open the tray status detail or launch
openclaw://commandcenter - In Node Mode, verify the window shows gateway channel health from node
healthevents plus a synthesized local Windows node when operatornode.listis not connected - Check diagnostics for pairing approval, pending reapproval, stale health, all-stopped channels, allowlist filtering, browser control host availability for
browser.proxy, and usage-cost gaps - When only the synthesized local Windows node is available, verify its locally declared capabilities/commands are labeled unverified and are not counted as approved/effective
- For
pending-reapproval, verify effective capabilities/commands remain unchanged, pending declarations are listed separately, and the copy action emitsopenclaw nodes approve <pendingRequestId> - During a changed-command handshake, verify authoritative
pending-reapprovalreplaces the generic node-pair approval card and exposes only the node-list trust command; explicitly typed device role-upgrade, Node mode off/hidden, and failure cards remain higher priority - If the gateway omits a safe pending request ID, verify the copy action emits
openclaw nodes pending, labels it as discovery only, and does not offer reconnect-after-approval yet - Approve the request explicitly, reconnect the node, and verify the effective capability/command counts update and the pending reapproval warning clears
- Use "Copy fix" only for safe repair commands; privacy-sensitive commands remain informational unless you explicitly opt in on the gateway
These features need the gateway to send node.invoke commands:
| Command | Description | Expected Behavior |
|---|---|---|
canvas.present |
Show WebView2 window | Opens floating window with URL or HTML |
canvas.hide |
Hide canvas window | Closes the canvas window |
canvas.eval |
Execute JavaScript | Runs JS in canvas, returns result |
canvas.snapshot |
Capture canvas | Returns base64 PNG of canvas content |
canvas.a2ui.pushJSONL |
Legacy A2UI JSONL push | Routes through same renderer path as canvas.a2ui.push |
screen.snapshot |
Take screenshot | Captures screen, shows notification, returns base64 |
screen.record |
Record short screen clip | Returns MP4/base64 metadata; requires explicit gateway allowlist |
system.notify |
Show notification | Displays toast notification |
system.run |
Controlled command execution | Uses local exec approval policy. A simple unquoted gateway command can bind to an allowlisted executable and run as direct argv; shell syntax remains one-time. Prompt decisions show a Windows Allow once / Always allow / Deny dialog when Allow always is safe. |
system.run.prepare |
Pre-flight command execution | Parses and validates a system.run invocation without executing it |
system.which |
Resolve executables | Returns absolute paths for requested binaries |
camera.list |
Enumerate cameras | Returns device IDs and names |
camera.snap |
Capture photo | Returns base64 image (NV12 fallback) |
camera.clip |
Capture video clip | Returns MP4/base64 metadata |
location.get |
Get Windows location | Uses Windows location permission/settings |
device.info / device.status |
Device metadata/status | Returns host/app/locale plus battery/storage/network/uptime payloads |
browser.proxy |
Proxy browser-control host requests | Requires Browser proxy bridge enabled, a compatible browser-control host listening on gateway port + 2, and matching browser-control auth |
tts.speak |
Speak text aloud | Requires Text-to-speech playback enabled in Settings; gateway mode also requires tts.speak in gateway.nodes.allowCommands |
stt.transcribe |
Bounded microphone transcription | Requires Speech-to-text enabled in Settings; uses local Whisper.net |
stt.listen |
Voice-activity microphone transcription | Returns when the user stops speaking or timeout expires |
stt.status |
Speech-to-text readiness | Returns Whisper.net model download/readiness state |
The gateway may send the node.invoke.cancel event with
payload.invokeId matching an active node.invoke.request. The Windows node
cancels only that invocation and completes its original result with
ok: false, error: "cancelled"; unknown or already-completed IDs are ignored.
The legacy payload.requestId spelling is also accepted for compatibility.
Operation completion is the linearization point: once capability execution
returns and atomically marks the invocation complete, later cancellation is too
late and the completed result is preserved.
For local MCP, send a JSON-RPC notifications/cancelled notification with
params.requestId matching the active tools/call JSON-RPC ID. Cancellation
must stop queued camera admission, recording delays/frame waits, and active
recording cleanup rather than only abandoning the HTTP waiter.
When the node connects, it advertises these capabilities:
canvas- WebView2-based canvas windowscreen- Screen snapshot and recording via Windows.Graphics.Capturesystem- Notifications, command execution (system.run,system.run.prepare,system.which), exec approval policycamera- MediaCapture photo/video capture (frame reader fallback)location- Windows.Devices.Geolocationdevice- Host/app metadata and lightweight statusbrowser- Localbrowser.proxybridge to a browser-control host on gateway port + 2, when enabled in Settingstts- Windows speech synthesis or ElevenLabs playback, when enabled in Settingsstt- Local speech-to-text via Whisper.net, when enabled in Settings
Local MCP clients also see MCP-only app.* commands such as app.navigate, app.status, app.chat.snapshot/app.chat.send/app.chat.reset, and app.chat.queue.list/app.chat.queue.cancel. Connection diagnostics and setup tools live under app.connection.*; use app.connection.status to inspect active gateway, operator/node credential state, MCP runtime status, browser proxy caveat, pending approval commands, and recent diagnostics, and app.connection.gateways to list saved gateway records without token values. These are local testing and automation hooks registered with the tray's MCP server and are not advertised to the gateway WebSocket.
- URL Validation: Canvas blocks
file://,javascript:, localhost, private IPs, IPv6 localhost - Screen Capture Notification: User is notified when screen snapshots are captured
- Screen Recording Allowlist:
screen.recordmust be explicitly allowed by the gateway and does not leave a hidden local MP4 copy on Windows - Session Attribution: Only the optional top-level
sessionKeystamped by the Gateway onnode.invoke.requestis trusted. Older Gateways omit it, so those invokes remain unattributed; a caller-supplied nestedargs.sessionKeyis never used as a fallback. - Command Center Redaction: recent node invoke activity records command name, status, duration, node id, and privacy class only; it does not store base64 payloads, screenshots, recordings, tokens, or command arguments
- Node Mode Toggle: Must be explicitly enabled by user
- Command Validation: Only alphanumeric commands with dots/hyphens allowed
- Check the active gateway in Connection settings. Gateway records live in
%APPDATA%\OpenClawTray\gateways.json; post-pairing device tokens live under%APPDATA%\OpenClawTray\gateways\<gateway-id>\device-key-ed25519.json. - Check logs for connection errors
- If logs report that the saved device identity could not be loaded, fix access to the existing identity file or use an explicit reset/re-pair action. The tray preserves an unreadable or corrupt identity instead of replacing it automatically.
- Verify gateway is running and accessible
- If only a bootstrap token exists, finish pairing or approve the device; paired device tokens take precedence on future connects.
- Ensure Windows notifications are enabled for the app
- Check if notification settings in the app are enabled
- Setup-code / QR pairing can connect with a device token and leave
GatewayRecord.SharedGatewayTokenempty. Browser control will not declarebrowser/browser.proxyuntil a shared gateway token is saved for that gateway. - Expect Connection capability pills to say Needs gateway shared token (not "Enabled, not active yet") only while the node WebSocket session is live and the shared token is missing. Disconnected or attached-but-disconnected states should ask for reconnect, not a token paste. The pill keeps that short label; its tooltip matches Command Center remediation detail.
- Command Center, Connection pill tooltips, and
app.connection.status/app.connection.gatewaysuse the same live-session rule for the shared-token caveat. For a remote (non-loopback) gateway without an explicitBrowserControlPortor SSH browser-proxy forward - including SSH tunnels whose effective URL is127.0.0.1- that caveat also mentions the endpoint/forward requirement; the shared token alone is not enough for usable remote browser.proxy. - Enter the gateway shared token in Settings, save, and reconnect node mode. Bootstrap tokens are not the shared gateway token.
- Confirm the Browser proxy bridge toggle is enabled in Settings, then save and reconnect or re-pair if the gateway keeps an older command snapshot.
- The bridge is local-only: it calls
http://127.0.0.1:<gateway-port+2>from Windows. For a gateway onws://127.0.0.1:18789, the browser-control host must listen on127.0.0.1:18791. - In managed SSH tunnel mode, keep Browser proxy bridge enabled so the tray forwards local gateway port + 2 to remote gateway port + 2. Settings shows a selectable preview of the exact
ssh -N -L ...command. - If using a manual SSH tunnel, add both forwards, for example:
ssh -N -L 18789:127.0.0.1:18789 -L 18791:127.0.0.1:18791 <user>@<host>. If the SSH daemon is not listening on port 22, include-p <ssh-port>. If local and remote gateway ports differ, forward<local-gateway-port+2>to127.0.0.1:<remote-gateway-port+2>. - Advanced split/remote topologies can pin the browser-control listener with the active gateway record's
BrowserControlPortfield in%APPDATA%\OpenClawTray\gateways.json. This value is a local TCP port on Windows and is scoped to that gateway record. Configure it only for a trusted browser-control forward, becausebrowser.proxysends the saved shared gateway token to the selected local listener for browser-control authentication. When a gateway uses SSH, tunnel-derivedlocalPort + 2browser-control routing is used only when that gateway's managed tunnel hasIncludeBrowserProxyForwardenabled; otherwise setBrowserControlPortto a trusted manual forward. - A local SSH forward is not enough if the remote browser-control host is not running. Command Center port diagnostics should show whether the local gateway and browser-control ports are listening and which process owns them.
- If Command Center shows the browser-control port listening but
browser.proxyreturns an auth error, verify the Windows Settings gateway token matches the browser-control host token/password. QR/bootstrap pairing can connect the node without saving a shared gateway token, but browser-control auth may still require one. - A local smoke can verify the host dependency without proving gateway invoke auth: start the upstream browser-control host with a temporary no-secret config, confirm
http://127.0.0.1:<gateway-port+2>/and/tabsreturn HTTP 200, then stop the captured host process. The full parity smoke is not complete untilopenclaw nodes invoke --command browser.proxysucceeds through the active gateway.
- Check logs for
canvas.presentcommand received - Verify URL is not blocked by security validation
- If you see "Camera access blocked", enable camera access for desktop apps in Windows Privacy settings
- Packaged MSIX builds will show the system consent prompt automatically
-
Sandbox integration tests are intended for local Windows development machines and may skip when the required local sandbox prerequisites are unavailable.
-
Build the tray app before running local sandbox validation so the required sandbox helper binaries are present in the app output.
-
For MXC-related merge validation, prefer the formal script below because it sets the required gates and fails if MXC is skipped.
.\scripts\validate-mxc-e2e.ps1
-
The focused E2E below provisions a fresh WSL Gateway, starts an isolated tray instance, enables the explicit Windows UI API sandbox opt-in, sets local exec approval policy, invokes
system.runthrough the real Gatewaynode.invokepath, and verifies tray MXC diagnostics show containedmxc-direct-appcexecution for a boundhostname.exeallowlist rule, PowerShell and full-policy shell execution, and denied writes to the tray data directory. -
Run it when validating the Gateway/Windows node runtime path, not just direct MCP or shared library behavior.
-
GitHub-hosted Actions runners do not provide a working MXC/AppContainer runtime. The regular cloud E2E matrix should report these MXC proofs as skipped while still running the rest of setup-connect. Run the proof on a local MXC-enabled Windows machine. Only set
OPENCLAW_RUN_MXC_E2E=1in GitHub Actions when using an MXC-enabled self-hosted runner. -
Use
.\scripts\validate-mxc-e2e.ps1for normal local validation. It setsOPENCLAW_RUN_E2EandOPENCLAW_RUN_MXC_E2E, runs the real Gateway MXC proofs, and fails if the MXC proof skips.-AllowSkipis only for documenting a non-MXC host, not for merge validation of MXC-related work. -
When reproducing this manually against an existing Gateway, confirm
gateway.nodes.denyCommandsdoes not blocksystem.run,system.run.prepare, orsystem.which, then approve anypending-reapprovalrequest withopenclaw nodes approve <pendingRequestId>. Current gateways include these commands in the canonical Windows desktop defaults. Older or deliberately customized gateways may still need exactgateway.nodes.allowCommandsentries..\build.ps1 $env:OPENCLAW_REPO_ROOT = (Get-Location).Path $env:OPENCLAW_RUN_E2E = "1" dotnet test .\tests\OpenClaw.E2ETests\OpenClaw.E2ETests.csproj ` --no-restore ` --filter "FullyQualifiedName~RealGateway_SystemRun" ` --logger "console;verbosity=normal" ` -r win-x64
-
Expected proof markers:
- The bound-hostname proof succeeds with a local
**/hostname.exerule, logspromptAttempted=false, and reaches MXC asshell=<direct-argv>. - Gateway response contains PowerShell output
OPENCLAW_GATEWAY_SYSTEM_RUN_MXC_OKwithexitCode=0. - The denied-write proof targets a fresh file under the isolated tray data directory, returns non-zero, and leaves that file absent.
openclaw-tray.logcontains[mxc] system.run sandbox requestwithexecutor=mxc-direct-appc,contained=True,shell=<direct-argv>, anduiAllowWindows=Truefor the PowerShell proof.openclaw-tray.logcontains[mxc] system.run sandbox resultwithcontainment=mxcfor both the successful execution and the denied write.
- The bound-hostname proof succeeds with a local
-
E2E artifacts are written under
TestResults\E2E\<run-id>and skip known secret-bearing files such as gateway records and settings.
system.run + exec approvals✅ Implementedsystem.runwith PowerShell/cmd supportsystem.run.preparepre-flight commandsystem.whichcommand lookupsystem.execApprovalsallowlist flow with base-hash optimistic concurrency for remote editssystem.runenvironment override sanitizer blocks path/toolchain injection and secret-looking variables
screen.record✅ Implemented- Graphics Capture video recording (MP4/base64)
camera.clip✅ Implemented- Short webcam video capture (MediaCapture + encoding)
A2UI pushJSONL alias + device status✅ Implemented- Legacy
canvas.a2ui.pushJSONL - Safe
device.info/device.status
- Legacy
Command Center diagnostics✅ Implemented- Channel/node/usage/pairing/allowlist diagnostics and recent invoke timeline
- Packaging & consent prompts
- MSIX packaging with camera/screen capabilities for system prompts
- Test matrix & polish
- Canvas/screen/camera regression tests
- Handle timeouts/disconnects, reduce verbose logging
src/OpenClaw.Shared/WindowsNodeClient.cs- Node protocol clientsrc/OpenClaw.Shared/Capabilities/*.cs- Capability handlerssrc/OpenClaw.Tray.WinUI/Services/Connection/GatewayRegistry.cs- persistent gateway recordssrc/OpenClaw.Connection/GatewayConnectionManager.cs- public lifecycle façade, operator/state/tunnel orchestrationsrc/OpenClaw.Connection/NodeConnectionCoordinator.cs- node generation, start/recovery, connector events, and telemetrysrc/OpenClaw.Connection/BootstrapTokenLifecycle.cs- bootstrap/device-token handoff and durable clear timingsrc/OpenClaw.Connection/DevicePairApprovalCoordinator.cs- device role-upgrade approval and bounded reconnectsrc/OpenClaw.Connection/CredentialResolver.cs- device-token/shared/bootstrap credential precedencesrc/OpenClaw.Tray.WinUI/Services/NodeService.cs- Orchestrates capabilitiessrc/OpenClaw.Tray.WinUI/Services/ScreenCaptureService.cs- screen snapshotssrc/OpenClaw.Tray.WinUI/Services/ScreenRecordingService.cs- screen recordingssrc/OpenClaw.Tray.WinUI/Services/CameraCaptureService.cs- camera photo/video capturesrc/OpenClaw.Tray.WinUI/Windows/CanvasWindow.xaml- WebView2 canvas