From 867a7dd487d599b97d71b1ddb4bcae1560b9a3a6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Jul 2026 04:49:17 +0100 Subject: [PATCH 1/4] feat(setup): offer native Windows and WSL gateway modes --- README.md | 12 +- docs/ONBOARDING_WIZARD.md | 20 +- docs/OPERATOR_NODE_CONCEPTS.md | 8 +- docs/SETUP.md | 15 +- docs/SETUP_ENGINE_REDESIGN.md | 33 +- docs/WINDOWS_NODE_ARCHITECTURE.md | 32 +- installer.iss | 12 +- scripts/Uninstall-LocalGateway.ps1 | 581 +++++++- .../Pages/CapabilitiesPage.xaml | 6 +- .../Pages/CapabilitiesPage.xaml.cs | 9 + .../Pages/ProgressPage.xaml | 2 +- .../Pages/ProgressPage.xaml.cs | 75 +- .../Pages/WelcomePage.xaml | 33 +- .../Pages/WelcomePage.xaml.cs | 34 +- .../Pages/WizardPage.xaml.cs | 6 +- .../SetupWindow.xaml.cs | 6 + .../WizardConsoleTail.cs | 123 +- src/OpenClaw.SetupEngine/AtomicFile.cs | 21 + .../ExistingConfigDetector.cs | 32 +- src/OpenClaw.SetupEngine/GatewayCliRunner.cs | 320 +++++ .../GatewayInstallModeDetector.cs | 200 +++ src/OpenClaw.SetupEngine/GatewayLkgVersion.cs | 19 +- .../NativeGatewaySteps.cs | 839 +++++++++++ src/OpenClaw.SetupEngine/Program.cs | 43 +- src/OpenClaw.SetupEngine/SetupContext.cs | 76 + src/OpenClaw.SetupEngine/SetupPipeline.cs | 235 +++- .../SetupReviewSummary.cs | 39 + src/OpenClaw.SetupEngine/SetupSteps.cs | 921 ++++++++++-- src/OpenClaw.SetupEngine/SetupWizardRunner.cs | 6 +- src/OpenClaw.SetupEngine/default-config.json | 4 + .../Pages/SettingsPage.xaml | 8 +- .../Pages/SettingsPage.xaml.cs | 60 +- .../SetupExistingGatewayClassifier.cs | 18 +- .../Services/WslKeepAlivePolicy.cs | 7 +- .../GatewayInstallModeDetectorTests.cs | 335 +++++ .../GatewayLkgVersionTests.cs | 28 + .../NativeGatewayStepsTests.cs | 438 ++++++ .../SetupConfigTests.cs | 16 + .../SetupPipelineTests.cs | 215 ++- .../SetupStepsTests.cs | 1249 +++++++++++++++++ .../AppRefactorContractTests.cs | 50 +- .../InstallerIssAssertionTests.cs | 81 +- .../StartupSetupStateTests.cs | 63 + .../WizardConsoleTailTests.cs | 87 ++ .../WslKeepAlivePolicyTests.cs | 14 + 45 files changed, 6055 insertions(+), 376 deletions(-) create mode 100644 src/OpenClaw.SetupEngine/GatewayCliRunner.cs create mode 100644 src/OpenClaw.SetupEngine/GatewayInstallModeDetector.cs create mode 100644 src/OpenClaw.SetupEngine/NativeGatewaySteps.cs create mode 100644 tests/OpenClaw.SetupEngine.Tests/GatewayInstallModeDetectorTests.cs create mode 100644 tests/OpenClaw.SetupEngine.Tests/NativeGatewayStepsTests.cs diff --git a/README.md b/README.md index 3424a7162..e9bd381a7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ This monorepo contains the Windows hub, shared client libraries, and CLI utiliti | **OpenClaw.Chat** | Native chat model and timeline reducer | | **OpenClaw.Cli** | CLI validator for WebSocket connect/send/probe using tray settings | | **OpenClaw.WinNode.Cli** | `winnode` CLI for invoking local Windows node/MCP capabilities | -| **OpenClaw.SetupEngine** | Local gateway setup, WSL installation, and setup-code support | +| **OpenClaw.SetupEngine** | Native Windows and WSL gateway setup plus setup-code support | | **OpenClaw.SetupEngine.UI** | WinUI setup wizard pages hosted by the tray app | | **OpenClawTray.FunctionalUI** | In-repo declarative WinUI helper used by native chat and newer UI surfaces | @@ -34,7 +34,7 @@ This monorepo contains the Windows hub, shared client libraries, and CLI utiliti > **End-user installer?** Download the latest stable x64 or ARM64 installer from the [OpenClaw Windows docs](https://docs.openclaw.ai/platforms/windows), or see [docs/SETUP.md](docs/SETUP.md) for step-by-step installation (no build required). > -> **Managed WSL gateway?** Local setup creates a locked-down app-owned `OpenClawGateway` distro. See [docs/WSL_GATEWAY_ADMIN.md](docs/WSL_GATEWAY_ADMIN.md) for editing `openclaw.json` as the `openclaw` user and using root for protected-file administration. +> **Native or WSL gateway?** First-run setup recommends a native Windows gateway with no WSL dependency, while an isolated app-owned `OpenClawGateway` WSL 2 distro remains available for maximum Linux compatibility. See [docs/WSL_GATEWAY_ADMIN.md](docs/WSL_GATEWAY_ADMIN.md) for WSL administration. > > **Operator or node?** Start with [Operator and node concepts](docs/OPERATOR_NODE_CONCEPTS.md) for the beginner-facing glossary of gateway, operator, node, pairing, reapproval, and allowlisted node capabilities. @@ -162,7 +162,7 @@ Modern Windows 11-style system tray companion that connects to your local OpenCl - ⏱ **Cron Jobs** - Quick access to scheduled tasks - πŸš€ **Auto-start** - Launch with Windows - βš™οΈ **Settings** - Full configuration page -- 🎯 **First-run onboarding** β€” native WSL gateway setup with capability, permission, install, onboard, and completion screens +- 🎯 **First-run onboarding** β€” choose native Windows or isolated WSL gateway setup, then complete capability, permission, install, onboard, and completion screens #### Quick Send scope requirement @@ -421,10 +421,10 @@ Default gateway: `ws://localhost:18789` On first run, Molty launches a guided setup flow: 1. **Security notice** β€” confirms this is a trusted PC before local setup starts. -2. **Welcome** β€” choose **Install a local gateway (WSL)** or connect to an existing gateway from Connections. +2. **Welcome** β€” choose the recommended **Install on Windows (native)** path, **Install in WSL**, or connect to an existing gateway from Connections. 3. **Capabilities** β€” choose a profile, review matching Windows permission status, and see exactly what setup will install. -4. **Progress** β€” installs the app-owned `OpenClawGateway` WSL instance and keeps Live activity available but collapsed by default. -5. **Gateway installed** β€” confirms the WSL gateway is running before moving into OpenClaw onboard. +4. **Progress** β€” installs the native Windows gateway or app-owned `OpenClawGateway` WSL instance and keeps Live activity available but collapsed by default. +5. **Gateway installed** β€” confirms the selected local gateway is running before moving into OpenClaw onboard. 6. **OpenClaw onboard** β€” gateway-driven provider/model/key setup rendered as a transcript. 7. **All set** β€” summary of available features, startup preference, and Finish. diff --git a/docs/ONBOARDING_WIZARD.md b/docs/ONBOARDING_WIZARD.md index 26d6ba6d6..82befb52d 100644 --- a/docs/ONBOARDING_WIZARD.md +++ b/docs/ONBOARDING_WIZARD.md @@ -1,17 +1,17 @@ # Onboarding Wizard -The onboarding wizard installs a new app-owned local WSL gateway on Windows and then runs OpenClaw onboard. +The onboarding wizard installs a managed local gateway natively on Windows or in an app-owned WSL 2 distro, then runs OpenClaw onboard. ## Overview -On first launch, the wizard appears only when there is no usable saved gateway connection. Users with existing gateways manage connections from the tray app's Connections tab. The local WSL setup affordance in Connections is shown only when setup has not already created an app-owned WSL gateway on this device. +On first launch, the wizard appears only when there is no usable saved gateway connection. Users choose the recommended native Windows runtime, the WSL 2 runtime, or an existing gateway. Existing connections remain managed from the tray app's Connections tab. The setup flow walks users through: 1. **Security notice** β€” Device-trust warning before setup choices -2. **Welcome / Advanced** β€” Install app-owned WSL gateway or connect existing gateway from Settings +2. **Welcome / Advanced** β€” Choose native Windows, app-owned WSL, or an existing gateway 3. **Capabilities** β€” Recommended profile, inline Windows permission status, and install review -4. **Local setup progress** β€” Fresh app-owned `OpenClawGateway` WSL installation +4. **Local setup progress** β€” Native Windows or fresh app-owned `OpenClawGateway` WSL installation 5. **Gateway installed** β€” Explicit handoff from infrastructure setup to OpenClaw onboard 6. **OpenClaw onboard** β€” Gateway-driven provider/model/key configuration 7. **All set** β€” Feature summary, startup preference, and completion @@ -21,10 +21,12 @@ The setup flow no longer configures remote/manual gateways inline. The Welcome p ## Screen Details ### Welcome -Displays the OpenClaw icon, app title, and a brief description. If an app-owned local WSL gateway already exists, the primary CTA reads **Install new WSL Gateway** and confirmation warns that the current OpenClaw WSL gateway and distro will be deleted. If only an external gateway exists, the CTA remains **Set up locally** and confirmation explains that the external connection remains available in Connections. +Displays the OpenClaw icon, app title, and three explicit choices. **Install on Windows (native)** is recommended and requires no WSL. **Install in WSL** uses an isolated Ubuntu WSL 2 instance. **Connect to an existing gateway** hands off to Connections. Each local choice shows a confirmation summary before changing an existing local setup; external gateway records remain available. ### Local setup progress -Installs and connects a new app-owned `OpenClawGateway` WSL instance from a clean WSL baseline. Setup does not export from or mutate an existing user Ubuntu distro; if WSL cannot create the named app-owned distro directly, setup fails with an actionable update message. When replacing an app-owned local gateway, the removal step is shown as part of progress and can be retried on failure. +Native mode installs the pinned OpenClaw CLI through the official HTTPS PowerShell installer into an app-owned LocalAppData prefix, configures an isolated `OpenClawGateway` profile, and installs its per-user Windows Scheduled Task. Existing global OpenClaw packages, wrappers, PATH entries, and the default profile are preserved. WSL mode installs and connects a new app-owned `OpenClawGateway` instance from a clean WSL baseline. It does not export from or mutate an existing user Ubuntu distro; if WSL cannot create the named app-owned distro directly, setup fails with an actionable update message. + +Switching modes stops the previous local gateway before claiming the loopback port. Switching to native preserves the app-owned WSL distro files. Switching to WSL removes the native gateway Scheduled Task so it cannot restart and conflict, while a failed switch restores the previous native service during rollback. The managed distro is locked down and is not intended to be a normal interactive Ubuntu profile. For editing `openclaw.json` as the `openclaw` user and using root for protected-file administration, see [Managing the locked-down WSL gateway](WSL_GATEWAY_ADMIN.md). @@ -34,7 +36,7 @@ The Capabilities page applies the selected profile to both setup config and runt ### OpenClaw onboard -After OpenClaw onboard completesβ€”or when the user explicitly skips itβ€”local setup runs the pinned gateway CLI's non-interactive baseline initializer against the final runtime workspace, then writes fixed Windows-node guidance into a setup-owned managed section of that workspace's `AGENTS.md`. The section is replaced idempotently between markers, preserves user-authored `AGENTS.md` content and file permissions outside those markers, and does not modify OpenClaw source files. This helps the initial companion-app OpenClaw session know to use the Windows node / `nodes` tool for Windows desktop, files, screenshots, camera, notifications, browser proxy, and Windows command tasks. +After OpenClaw onboard completesβ€”or when the user explicitly skips itβ€”WSL setup runs the pinned gateway CLI's non-interactive baseline initializer against the final runtime workspace, then writes fixed Windows-node guidance into a setup-owned managed section of that workspace's `AGENTS.md`. The section is replaced idempotently between markers, preserves user-authored `AGENTS.md` content and file permissions outside those markers, and does not modify OpenClaw source files. Native setup skips this WSL-specific workspace injection. In both modes, the tray app registers the Windows node capabilities selected during onboarding. Renders server-defined setup steps via RPC (`wizard.start` / `wizard.next`). The gateway controls the flow β€” steps can be: - **Note** β€” informational messages @@ -88,10 +90,10 @@ Use a temp settings directory for tests that construct `SettingsManager`, or set |------|---------| | `src/OpenClaw.SetupEngine.UI/SetupWindow.xaml(.cs)` | Tray-hosted setup shell, run lock, preview routing, and page navigation | | `src/OpenClaw.SetupEngine.UI/Pages/SecurityNoticePage.xaml(.cs)` | First-run device-trust warning before setup choices | -| `src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml(.cs)` | Install-new-WSL vs connect-existing choice and existing-gateway replacement prompt | +| `src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml(.cs)` | Native Windows, WSL, or connect-existing choice and replacement prompt | | `src/OpenClaw.SetupEngine.UI/Pages/AdvancedSetupPage.xaml(.cs)` | Connect-existing handoff to Connection settings | | `src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml(.cs)` | Capability profile, inline Windows permission status, and install review | -| `src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml(.cs)` | WSL gateway install progress and gateway-installed handoff | +| `src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml(.cs)` | Mode-aware local gateway install progress and gateway-installed handoff | | `src/OpenClaw.SetupEngine.UI/Pages/WizardPage.xaml(.cs)` | OpenClaw onboard provider/model/key wizard driven by gateway `wizard.*` frames | | `src/OpenClaw.SetupEngine.UI/Pages/CompletePage.xaml(.cs)` | Success, failure, log/help, and startup preference summary | | `src/OpenClaw.SetupEngine.UI/Pages/SetupPermissionHelper.cs` | Passive Windows permission checks and inline permission rows | diff --git a/docs/OPERATOR_NODE_CONCEPTS.md b/docs/OPERATOR_NODE_CONCEPTS.md index d317c9c09..25c2329b5 100644 --- a/docs/OPERATOR_NODE_CONCEPTS.md +++ b/docs/OPERATOR_NODE_CONCEPTS.md @@ -9,6 +9,7 @@ different approval paths. | Term | Meaning | | --- | --- | | Gateway | The OpenClaw service that coordinates agents, channels, sessions, devices, and nodes. The Windows app talks to it over WebSocket. | +| Local native gateway | OpenClaw installed directly in the Windows user profile and started through a per-user Windows Scheduled Task. This is the recommended local mode and does not require WSL. | | Local WSL gateway | A dedicated `OpenClawGateway` WSL distro installed by the Windows onboarding flow. It is app-owned and locked down rather than a general-purpose Ubuntu profile. | | Operator | The user-facing control role. The tray app uses the operator connection for Quick Send, chat, diagnostics, channel controls, setup, and approving pairing requests. | | Node | The controllable Windows machine role. When Node Mode is enabled, the tray app advertises Windows capabilities such as screenshots, canvas, camera, notifications, and approved command execution. | @@ -37,10 +38,11 @@ A typical local setup uses this sequence: 5. After approval, the gateway can invoke only the node capabilities that are enabled locally and allowlisted by gateway policy. -## Local WSL Gateway Versus Existing Gateway +## Local Gateway Modes Versus Existing Gateway -The default onboarding path installs a local WSL gateway for users who do not -already have one. That gateway runs on the same Windows PC and is managed by the +The recommended onboarding path installs a native Windows gateway for users who +do not already have one. Users who prefer Linux compatibility can instead choose +the isolated WSL 2 gateway. Both run on the same PC and are managed by the OpenClaw Companion setup flow. Advanced setup is for users who already have a local, remote, or manually diff --git a/docs/SETUP.md b/docs/SETUP.md index 5ed6d17ad..a3191cc96 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -9,9 +9,9 @@ Before installing, make sure you have: - **Windows 10 (20H2 or later)** or **Windows 11** - **WebView2 Runtime** β€” pre-installed on Windows 11 and most up-to-date Windows 10 systems. If missing, download from [Microsoft Edge WebView2](https://developer.microsoft.com/microsoft-edge/webview2/). -You do **not** need a pre-existing local OpenClaw gateway before installing. On first launch, OpenClaw Companion can install a dedicated local WSL gateway for you, or you can use **Advanced setup** to connect to an existing local, remote, or manually configured gateway. See [Onboarding Wizard](ONBOARDING_WIZARD.md) for the install-new-WSL and connect-existing handoff flow. +You do **not** need a pre-existing local OpenClaw gateway before installing. On first launch, OpenClaw Companion can install OpenClaw natively on Windows (recommended), install a dedicated local WSL gateway, or connect to an existing local, remote, or manually configured gateway. See [Onboarding Wizard](ONBOARDING_WIZARD.md) for the install-new and connect-existing handoff flow. -New to the OpenClaw roles? Read [Operator and node concepts](OPERATOR_NODE_CONCEPTS.md) for a short glossary of gateway, local WSL gateway, operator, node, pairing, reapproval, and allowlisted node capabilities before starting setup. +New to the OpenClaw roles? Read [Operator and node concepts](OPERATOR_NODE_CONCEPTS.md) for a short glossary of gateway, local gateway modes, operator, node, pairing, reapproval, and allowlisted node capabilities before starting setup. ## Step-by-Step Installation @@ -46,21 +46,24 @@ After the installer finishes, OpenClaw Companion starts automatically. Look for If you don't see it, check the **hidden icons** area (the `^` arrow next to the tray). -The installer also creates a Start Menu group with shortcuts for **OpenClaw Companion**, **OpenClaw Gateway Setup**, **OpenClaw Companion Settings**, **OpenClaw Chat**, **Check for Updates**, and uninstall. The Gateway Setup shortcut launches the bundled local WSL/onboarding setup app. +The installer also creates a Start Menu group with shortcuts for **OpenClaw Companion**, **OpenClaw Gateway Setup**, **OpenClaw Companion Settings**, **OpenClaw Chat**, **Check for Updates**, and uninstall. The Gateway Setup shortcut launches the bundled native Windows/WSL onboarding app. ### 5. Onboarding Wizard -On first launch, Molty opens the onboarding wizard when there is no usable saved gateway connection. The default flow installs and configures a dedicated app-owned local WSL gateway: +On first launch, Molty opens the onboarding wizard when there is no usable saved gateway connection. The wizard offers two managed local gateway modes: + +- **Native Windows (recommended)** β€” installs the pinned OpenClaw CLI into an app-owned LocalAppData prefix with the official PowerShell installer and runs an isolated gateway profile as a per-user Windows Scheduled Task. Existing global OpenClaw installs and the default profile are preserved; no WSL dependency is required. +- **WSL 2** β€” creates a locked-down app-owned `OpenClawGateway` Ubuntu instance for maximum Linux compatibility. Requires WSL 2 and virtualization. 1. **Security notice** β€” Confirms this is a trusted PC before local setup starts. -2. **Welcome** β€” Choose **Install a local gateway (WSL)** to install the app-owned WSL gateway, or **Connect to an existing gateway** to open the tray app's Connections tab. +2. **Welcome** β€” Choose **Install on Windows (native)**, **Install in WSL**, or **Connect to an existing gateway** to open the tray app's Connections tab. For the role split behind these choices, see [Operator and node concepts](OPERATOR_NODE_CONCEPTS.md). 3. **Capabilities** β€” Choose a capability profile, review matching Windows permission status, and see exactly what setup will install before anything runs. -4. **Local setup progress** β€” Installs a fresh app-owned `OpenClawGateway` WSL instance and connects Molty to it. This does not modify an existing user Ubuntu distro. +4. **Local setup progress** β€” Installs the selected local runtime and connects Molty to it. Native mode uses Windows directly; WSL mode creates a fresh app-owned distro without modifying an existing user Ubuntu distro. 5. **Gateway installed** β€” Confirms the private gateway is running and offers **Start OpenClaw onboard**. diff --git a/docs/SETUP_ENGINE_REDESIGN.md b/docs/SETUP_ENGINE_REDESIGN.md index 0fcc7111a..bebc42377 100644 --- a/docs/SETUP_ENGINE_REDESIGN.md +++ b/docs/SETUP_ENGINE_REDESIGN.md @@ -2,13 +2,13 @@ ## Overview -The Setup Engine is a **config-driven system** for provisioning an OpenClaw WSL gateway from scratch. It consists of two setup projects plus the tray host: +The Setup Engine is a **config-driven system** for provisioning an OpenClaw gateway natively on Windows or in an isolated WSL 2 distro. It consists of two setup projects plus the tray host: -1. **`OpenClaw.SetupEngine`** β€” Headless pipeline library. Runs 19 steps sequentially with full JSONL logging, transaction journal, and rollback support. +1. **`OpenClaw.SetupEngine`** β€” Headless pipeline library. Builds the selected runtime pipeline with full JSONL logging, transaction journal, and rollback support. 2. **`OpenClaw.SetupEngine.UI`** β€” WinUI3 setup window/pages that wrap the same pipeline with a fluent wizard UI. 3. **`OpenClaw.Tray.WinUI`** β€” The only shipped WinUI executable. It hosts `SetupWindow` directly and self-restarts after successful setup. -The bundled `default-config.json` ships with the tray executable and provides secure defaults (loopback bind, WSL isolation, systemd enabled). Defaults can be overridden via config file or environment variables. +The bundled `default-config.json` ships with the tray executable and provides secure defaults (loopback bind, pinned gateway version, WSL isolation). The UI recommends native Windows; headless callers retain the explicit `InstallMode` config default. Values can be overridden via config file or environment variables. > **Status note (2026-07-06):** Current default setup includes `WindowsNodeBootstrapContextStep`, which injects Windows-node context into the WSL workspace `AGENTS.md` after onboarding. @@ -57,6 +57,8 @@ src/OpenClaw.SetupEngine/ β”œβ”€β”€ TransactionJournal.cs # Append-only JSONL journal (77 lines) β”œβ”€β”€ SetupLogger.cs # Structured JSONL logger (112 lines) β”œβ”€β”€ CommandRunner.cs # Concrete WSL/process command runner +β”œβ”€β”€ GatewayCliRunner.cs # Routes gateway CLI calls to Windows or WSL +β”œβ”€β”€ NativeGatewaySteps.cs # Windows installer and service mode-switch steps β”œβ”€β”€ RetryExecutor.cs # Exponential backoff retry β”œβ”€β”€ StubNodeCapability.cs # Minimal capability stubs for pairing └── default-config.json # THE source of truth for all config values @@ -66,23 +68,20 @@ src/OpenClaw.SetupEngine.UI/ β”œβ”€β”€ SetupWindow.xaml / .xaml.cs # 720Γ—820 window, Mica, title bar, navigation, setup events └── Pages/ β”œβ”€β”€ SecurityNoticePage.xaml / .cs # Device-trust warning - β”œβ”€β”€ WelcomePage.xaml / .cs # Install WSL gateway vs connect existing + β”œβ”€β”€ WelcomePage.xaml / .cs # Choose native Windows, WSL, or existing gateway β”œβ”€β”€ CapabilitiesPage.xaml / .cs # Profile, inline permissions, install review β”œβ”€β”€ ProgressPage.xaml / .cs # Live step rows + gateway-installed handoff β”œβ”€β”€ WizardPage.xaml / .cs # OpenClaw onboard transcript └── CompletePage.xaml / .cs # Mascot status badge, summary, startup toggle ``` -**Total engine code: ~1,882 lines across 8 files.** UI adds ~10 more files. - ---- - ## Config File (`default-config.json`) **Config is required.** Neither the headless exe nor the UI will run without one. The bundled `default-config.json` is auto-loaded from `AppContext.BaseDirectory` if no `--config` is specified. ```json { + "InstallMode": "Wsl", "DistroName": "OpenClawGateway", "GatewayPort": 18789, "BaseDistro": "Ubuntu-24.04", @@ -126,6 +125,7 @@ src/OpenClaw.SetupEngine.UI/ "Gateway": { "Bind": "loopback", "InstallUrl": null, + "WindowsInstallUrl": null, "Version": null, "HealthTimeoutSeconds": 90, "ReloadMode": "hot", @@ -166,9 +166,16 @@ src/OpenClaw.SetupEngine.UI/ --- -## Pipeline Steps (19 total) +## Pipeline Steps + +Executed sequentially. The native and WSL pipelines share configuration, pairing, verification, and onboard steps; runtime-specific provisioning is selected by `InstallMode`. + +| Runtime | Runtime-specific work | +|---|---| +| Native Windows | Official HTTPS PowerShell installer with app-owned npm prefix, native CLI routing, per-user gateway Scheduled Task, native health check | +| WSL 2 | WSL preflight, isolated distro creation/lockdown, Linux CLI install, systemd gateway service, WSL keepalive | -Executed sequentially. Each step is a small class (30–120 lines) in `SetupSteps.cs`. +The WSL pipeline includes these stages: | # | Step Class | What It Does | |---|-----------|-------------| @@ -271,13 +278,13 @@ The WinUI app is a **thin shell** β€” no business logic, just rendering pipeline **WelcomePage** - OpenClaw icon + "OpenClaw Setup" title bar -- Install app-owned WSL gateway (recommended) or connect to existing gateway -- Replacement prompt when an app-owned WSL gateway already exists +- Install natively on Windows (recommended), install an app-owned WSL gateway, or connect to an existing gateway +- Mode-aware replacement prompt for an existing local gateway **CapabilitiesPage** - Capability profile defaults to Standard - Inline Windows permission status for selected capabilities -- Install review showing WSL distro, OpenClaw CLI, local gateway service, and possible UAC +- Install review showing the selected runtime, OpenClaw CLI, local gateway service, and possible UAC **ProgressPage** - Step rows with spinning ProgressRing β†’ βœ“/βœ— badges diff --git a/docs/WINDOWS_NODE_ARCHITECTURE.md b/docs/WINDOWS_NODE_ARCHITECTURE.md index 97dfd224d..7c4201d73 100644 --- a/docs/WINDOWS_NODE_ARCHITECTURE.md +++ b/docs/WINDOWS_NODE_ARCHITECTURE.md @@ -1,10 +1,10 @@ # πŸ—οΈ Architecture: Windows Platform Strategy & Native Node Roadmap -> **πŸ“ Note**: This document was written during the initial planning phase (early 2026). Windows Node mode has since been implemented with canvas, screen, camera, system.run, and notification capabilities. The deployment scenarios, design rationale, and protocol details remain accurate reference material. The "Current State" table and roadmap checkboxes may not reflect the latest status β€” see README.md for current capabilities. +> **πŸ“ Note**: This document was written during the initial planning phase (early 2026). Windows Node mode is implemented, and onboarding now supports both a native Windows gateway (recommended) and an isolated WSL 2 gateway. The scenarios and design rationale remain historical reference material; see README.md for current capabilities. ## Summary -OpenClaw has **excellent** macOS support β€” the native menubar app runs as a full node with camera, canvas, screen capture, notifications, location, system exec, and more. Windows users today rely on **WSL2** for the gateway and get a limited experience: no native UI integration, no camera, no canvas surface, and NAT networking quirks. +OpenClaw has **excellent** macOS support β€” the native menubar app runs as a full node with camera, canvas, screen capture, notifications, location, system exec, and more. Windows now offers the same companion/node model with a native gateway path that avoids WSL networking, plus WSL 2 for users who prefer Linux compatibility. This issue proposes a comprehensive Windows platform strategy that evolves `OpenClaw.Tray.WinUI` from a gateway *client* into a **native Windows node** β€” giving the agent eyes, hands, and a voice on Windows, and eventually exploring a fully native Windows gateway. @@ -38,7 +38,7 @@ Related issues: #5 (Canvas Panel), #6 (Skills Settings UI), #7 (DEVELOPMENT.md), | `OpenClaw.Shared` | βœ… Working | Gateway WebSocket client library (.NET) | | `OpenClaw.Tray.WinUI` | βœ… Working | System tray app β€” status, Quick Send, WebChat (WebView2), toast notifications, channel control | | Windows Node | βœ… Implemented | Canvas, screen, camera, location, device info/status, system.run, notifications β€” all working via Node Mode | -| Windows Gateway | ❌ Unexplored | Gateway runs in WSL2 only | +| Windows Gateway | βœ… Supported | First-run setup installs and manages the gateway natively with a per-user Scheduled Task; WSL 2 remains selectable | ### How Scott uses it today @@ -173,9 +173,7 @@ The tray now also has a Command Center surface that combines gateway channel hea | **Setup complexity** | Low β€” `npm install -g openclaw && openclaw onboard` from PowerShell. Same as Mac. | | **UX Rating** | ⭐⭐⭐⭐⭐ True feature parity with Mac | -**The dream.** No WSL2 dependency at all. The gateway runs natively on Windows (Node.js works fine on Windows), and the tray app provides all native capabilities. This is the Mac experience, on Windows. - -**Key question:** Does the OpenClaw gateway actually *work* on Windows? It's Node.js, so *in theory* yes. But there may be Unix-specific assumptions (signals, file paths, spawning, etc.) that need auditing. See [Architectural Questions](#architectural-questions). +**Current recommended local mode.** No WSL2 dependency is required. The official PowerShell installer installs OpenClaw into a Companion-owned LocalAppData prefix without replacing the user's global CLI, the gateway uses its Windows service adapter, and the onboarding wizard configures, pairs, and verifies the tray app against it. --- @@ -497,7 +495,7 @@ Option A is cleanest but requires protocol support. Option B works today with no ### 2. Can the OpenClaw gateway run natively on Windows? -**Likely yes, with work.** +**Yes.** OpenClaw includes a Windows service adapter and the companion setup engine drives the native CLI. The concerns below are retained as the original audit checklist. The gateway is Node.js. Node.js runs natively on Windows. But: @@ -512,11 +510,11 @@ The gateway is Node.js. Node.js runs natively on Windows. But: | File watching (chokidar) | Low | Works on Windows. | | Browser automation (Playwright) | Low | Playwright supports Windows natively. | -**Recommendation:** Audit the gateway codebase for Unix assumptions. This could be a relatively tractable porting effort β€” most of the gateway is pure Node.js WebSocket/HTTP work. +The native onboarding path validates the supported gateway version and uses the official Windows installer rather than assuming every historical OpenClaw build is Windows-compatible. ### 3. What about the service lifecycle on Windows? -On macOS: launchd plist. On Linux: systemd unit. On Windows, options include: +On macOS: launchd plist. On Linux: systemd unit. On Windows, OpenClaw uses a per-user Scheduled Task with a Startup-folder fallback. Earlier options considered were: - **Windows Service** (via [node-windows](https://github.com/coreybutler/node-windows) or .NET service host) - **Task Scheduler** (run at logon) @@ -594,16 +592,16 @@ The node protocol requires a stable device identity (`device.id`) derived from a - [x] Permission prompts (camera: UnauthorizedAccessException β†’ toast; future MSIX consent) - [x] Multi-monitor support for screen capture (`screenIndex` param) -### Phase 3: Native Windows Gateway (Exploration) -**Priority: MEDIUM | Effort: High | Impact: High** +### Phase 3: Native Windows Gateway +**Status: implemented for managed onboarding** -- [ ] Audit OpenClaw gateway for Unix-specific code -- [ ] Test `openclaw gateway` on Windows (Node.js native) -- [ ] Fix platform-specific issues (signals, paths, child process spawning) -- [ ] Windows Service integration for daemon mode +- [x] Audit and support `openclaw gateway` on Windows +- [x] Test `openclaw gateway` on Windows (Node.js native) +- [x] Fix required platform-specific service lifecycle issues +- [x] Per-user Scheduled Task integration for daemon mode - [ ] Tray app: "Start/Stop/Restart Gateway" menu items (parity with Mac menubar) -- [ ] `openclaw onboard --install-daemon` for Windows (Task Scheduler or Windows Service) -- [ ] Document Windows-native gateway setup +- [x] Native Windows gateway install/configure/start through companion onboarding +- [x] Document Windows-native gateway setup ### Phase 4: Feature Parity + Polish **Priority: LOW | Effort: Medium | Impact: Medium** diff --git a/installer.iss b/installer.iss index e4a5ea4c5..69da0dd18 100644 --- a/installer.iss +++ b/installer.iss @@ -8,6 +8,8 @@ #define MyAutoStartName "OpenClawTray-Dev" #define MyStartupTaskName "OpenClaw Companion (Dev)" #define MyDistroName "OpenClawGateway-Dev" + #define MyGatewayPort "18790" + #define MyGatewayTaskName "OpenClaw Gateway (OpenClawGateway-Dev)" #define MyProtocol "openclaw-dev" #define MyOutputSuffix "-Dev" #else @@ -18,6 +20,8 @@ #define MyAutoStartName "OpenClawTray" #define MyStartupTaskName "OpenClaw Companion" #define MyDistroName "OpenClawGateway" + #define MyGatewayPort "18789" + #define MyGatewayTaskName "OpenClaw Gateway (OpenClawGateway)" #define MyProtocol "openclaw" #define MyOutputSuffix "" #endif @@ -99,7 +103,7 @@ Name: "startupicon"; Description: "Start {#MyAppName} when Windows starts"; Grou [Files] ; WinUI Tray app - include all files (WinUI needs DLLs, not single-file) Source: "{#publish}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs -; WSL gateway uninstall helper copied to {tmp} by [Code] during uninstall. +; Native/WSL gateway uninstall helper copied to {tmp} by [Code] during uninstall. Source: "scripts\Uninstall-LocalGateway.ps1"; DestDir: "{app}"; Flags: ignoreversion #if vcRedist != "" Source: "{#vcRedist}"; DestDir: "{tmp}"; DestName: "vc_redist.exe"; Flags: deleteafterinstall; AfterInstall: InstallVCRuntime @@ -189,8 +193,8 @@ begin begin LocalGatewayCleanupRequested := MsgBox( - 'Do you also want to remove the OpenClaw local WSL gateway?' + #13#10#13#10 + - 'Choose Yes to unregister the {#MyDistroName} WSL distro and remove generated local gateway state.' + #13#10 + + 'Do you also want to remove the OpenClaw local gateway?' + #13#10#13#10 + + 'Choose Yes to remove its native Windows service or {#MyDistroName} WSL distro, plus generated local gateway state.' + #13#10 + 'Choose No to leave the local gateway and generated local state on this computer.', mbConfirmation, MB_YESNO) = IDYES; @@ -236,6 +240,8 @@ begin ' -DataDirectoryName ' + AddQuotes('{#MyInstallDir}') + ' -AutoStartName ' + AddQuotes('{#MyAutoStartName}') + ' -StartupTaskName ' + AddQuotes('{#MyStartupTaskName}') + + ' -GatewayTaskName ' + AddQuotes('{#MyGatewayTaskName}') + + ' -GatewayPort {#MyGatewayPort}' + ' -DistroName ' + AddQuotes('{#MyDistroName}'); Log('Running local gateway cleanup script from {tmp}.'); diff --git a/scripts/Uninstall-LocalGateway.ps1 b/scripts/Uninstall-LocalGateway.ps1 index 8b7999a6b..cc90cf5dc 100644 --- a/scripts/Uninstall-LocalGateway.ps1 +++ b/scripts/Uninstall-LocalGateway.ps1 @@ -1,12 +1,13 @@ <# .SYNOPSIS - Removes the OpenClaw local WSL gateway during app uninstall. + Removes the OpenClaw local native Windows or WSL gateway during app uninstall. .DESCRIPTION This helper is launched by the Inno uninstaller after the user chooses to - remove the local gateway. It deliberately calls WSL directly instead of - launching OpenClaw binaries from the install directory, so the app payload is - not kept loaded while Inno removes installed files. + remove the local gateway. It removes the native gateway Scheduled Task via + the user-level OpenClaw CLI (with a direct task cleanup fallback), or directly + unregisters the app-owned WSL distro. It does not load app payload binaries + while Inno removes installed files. #> [CmdletBinding()] @@ -15,6 +16,8 @@ param( [string]$DataDirectoryName = 'OpenClawTray', [string]$AutoStartName = 'OpenClawTray', [string]$StartupTaskName = 'OpenClaw Companion', + [string]$GatewayTaskName = 'OpenClaw Gateway (OpenClawGateway)', + [int]$GatewayPort = 18789, [string]$DistroName = 'OpenClawGateway' ) @@ -24,6 +27,7 @@ $resultPath = Join-Path $AppRoot 'uninstall-gateway-result.json' $errorPath = Join-Path $AppRoot 'uninstall-gateway-error.log' $wslLogPath = Join-Path $AppRoot 'uninstall-gateway-wsl.log' $cleanupWarnings = New-Object 'System.Collections.Generic.List[string]' +$script:installedGatewayMode = 'Wsl' if ($DataDirectoryName -notmatch '^[A-Za-z0-9._-]+$') { throw "Invalid data directory name '$DataDirectoryName'." @@ -31,6 +35,9 @@ if ($DataDirectoryName -notmatch '^[A-Za-z0-9._-]+$') { if ($DistroName -notmatch '^[A-Za-z0-9._-]+$') { throw "Invalid WSL distro name '$DistroName'." } +if ($GatewayPort -le 0 -or $GatewayPort -gt 65535) { + throw "Invalid gateway port '$GatewayPort'." +} function Ensure-AppRoot { if (-not [string]::IsNullOrWhiteSpace($AppRoot) -and -not (Test-Path -LiteralPath $AppRoot)) { @@ -162,15 +169,19 @@ function Resolve-AppDataDir { try { $uri = [Uri]$Url - $host = $uri.Host.ToLowerInvariant() - return $host -eq 'localhost' -or $host -eq '127.0.0.1' -or $host -eq '::1' -or $host -eq '[::1]' + $uriHost = $uri.Host.ToLowerInvariant() + return $uriHost -eq 'localhost' -or $uriHost -eq '127.0.0.1' -or $uriHost -eq '::1' -or $uriHost -eq '[::1]' } catch { return $false } } function Test-SetupManagedLocalRecord { - param([object]$Record) + param( + [object]$Record, + [string]$InstallMode, + [string]$OwnedNativeRecordId + ) $isLocal = [bool](Get-JsonPropertyValue $Record 'isLocal') $sshTunnel = Get-JsonPropertyValue $Record 'sshTunnel' @@ -179,17 +190,29 @@ function Resolve-AppDataDir { } $setupManagedDistroName = [string](Get-JsonPropertyValue $Record 'setupManagedDistroName') - if ([string]::Equals($setupManagedDistroName, $DistroName, [StringComparison]::Ordinal)) { - return $true + $friendlyName = [string](Get-JsonPropertyValue $Record 'friendlyName') + $recordId = [string](Get-JsonPropertyValue $Record 'id') + $url = [string](Get-JsonPropertyValue $Record 'url') + $isLocalUrl = Test-LocalGatewayUrl $url + $isManagedWsl = + [string]::Equals($setupManagedDistroName, $DistroName, [StringComparison]::Ordinal) -or + ([string]::IsNullOrWhiteSpace($setupManagedDistroName) -and + [string]::Equals($friendlyName, "Local ($DistroName)", [StringComparison]::Ordinal) -and + $isLocalUrl) + $isManagedNative = + -not [string]::IsNullOrWhiteSpace($OwnedNativeRecordId) -and + [string]::Equals($recordId, $OwnedNativeRecordId, [StringComparison]::Ordinal) -and + [string]::IsNullOrWhiteSpace($setupManagedDistroName) -and + $isLocalUrl + + if ([string]::Equals($InstallMode, 'NativeWindows', [StringComparison]::OrdinalIgnoreCase)) { + return $isManagedNative } - - if (-not [string]::IsNullOrWhiteSpace($setupManagedDistroName)) { - return $false + if ([string]::Equals($InstallMode, 'Wsl', [StringComparison]::OrdinalIgnoreCase)) { + return $isManagedWsl } - $friendlyName = [string](Get-JsonPropertyValue $Record 'friendlyName') - $url = [string](Get-JsonPropertyValue $Record 'url') - return [string]::Equals($friendlyName, "Local ($DistroName)", [StringComparison]::Ordinal) -and (Test-LocalGatewayUrl $url) + return $isManagedWsl -or $isManagedNative } function Test-ExternalGatewayRecord { @@ -235,6 +258,32 @@ function Resolve-AppDataDir { } } + function Remove-OwnedDirectoryStrict { + param( + [string]$Path, + [string]$Label + ) + + if (-not (Test-Path -LiteralPath $Path)) { + Write-GatewayLog "$Label directory already absent." + return + } + + $item = Get-Item -LiteralPath $Path -Force -ErrorAction Stop + if (($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) { + throw "Refusing to recursively delete $Label reparse point '$Path'." + } + if (-not $item.PSIsContainer) { + throw "Expected $Label to be a directory: '$Path'." + } + + Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop + if (Test-Path -LiteralPath $Path) { + throw "Failed to verify removal of $Label directory '$Path'." + } + Write-GatewayLog "Deleted $Label directory." + } + function Remove-AutostartRegistryValue { $runKey = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' try { @@ -270,7 +319,11 @@ function Resolve-AppDataDir { } function Remove-SetupManagedGatewayRecords { - param([string]$DataDir) + param( + [string]$DataDir, + [string]$InstallMode, + [string]$OwnedNativeRecordId + ) $gatewaysPath = Join-Path $DataDir 'gateways.json' $registry = Read-JsonFile $gatewaysPath @@ -290,7 +343,7 @@ function Resolve-AppDataDir { $remaining = New-Object System.Collections.ArrayList $removed = New-Object System.Collections.ArrayList foreach ($record in $records) { - if (Test-SetupManagedLocalRecord $record) { + if (Test-SetupManagedLocalRecord -Record $record -InstallMode $InstallMode -OwnedNativeRecordId $OwnedNativeRecordId) { [void]$removed.Add($record) } else { [void]$remaining.Add($record) @@ -440,9 +493,442 @@ function Resolve-AppDataDir { } } + function Get-InstalledGatewayMode { + param( + [string]$DataDir, + [string]$LocalDataDir + ) + + # Written before native config/service mutation, so this marker is the + # newest setup intent after an interrupted native mode switch. + $nativeOwnershipPath = Join-Path $LocalDataDir 'native-gateway-install.json' + if (Test-Path -LiteralPath $nativeOwnershipPath -PathType Leaf) { + $nativeOwnership = Read-JsonFile $nativeOwnershipPath + if (Test-NativeOwnershipMatches -Ownership $nativeOwnership) { + return 'NativeWindows' + } + throw 'Native ownership marker belongs to a different profile or Scheduled Task; refusing destructive cleanup.' + } + + $profileOwnershipPath = Join-Path $LocalDataDir 'native-gateway-profile-owner.json' + if (Test-Path -LiteralPath $profileOwnershipPath -PathType Leaf) { + $profileOwnership = Read-JsonFile $profileOwnershipPath + if (Test-NativeOwnershipMatches -Ownership $profileOwnership) { + return 'NativeWindows' + } + throw 'Native profile ownership marker belongs to a different profile or Scheduled Task; refusing destructive cleanup.' + } + + $state = Read-JsonFile (Join-Path $LocalDataDir 'setup-state.json') + $persistedMode = [string](Get-JsonPropertyValue $state 'InstallMode') + if ([string]::Equals($persistedMode, 'NativeWindows', [StringComparison]::OrdinalIgnoreCase)) { + return 'NativeWindows' + } + if ([string]::Equals($persistedMode, 'Wsl', [StringComparison]::OrdinalIgnoreCase)) { + return 'Wsl' + } + + return 'Wsl' + } + + function Get-NativeGatewayRecordId { + param([string]$LocalDataDir) + + foreach ($fileName in @('native-gateway-install.json', 'native-gateway-profile-owner.json')) { + $ownership = Read-JsonFile (Join-Path $LocalDataDir $fileName) + if (-not (Test-NativeOwnershipMatches -Ownership $ownership)) { + continue + } + $recordId = [string](Get-JsonPropertyValue $ownership 'GatewayRecordId') + if ($recordId -match '^[A-Za-z0-9_-]{1,128}$') { + return $recordId + } + } + + return $null + } + + function Test-NativeOwnershipMatches { + param([object]$Ownership) + + if ($null -eq $Ownership) { + return $false + } + + $profileName = [string](Get-JsonPropertyValue $Ownership 'ProfileName') + $taskName = [string](Get-JsonPropertyValue $Ownership 'TaskName') + return [string]::Equals($profileName, (Get-NativeGatewayProfile), [StringComparison]::Ordinal) -and + [string]::Equals($taskName, $GatewayTaskName, [StringComparison]::Ordinal) + } + + function Resolve-NativeOpenClawCli { + $managedPrefix = Join-Path (Resolve-LocalDataDir) 'native-cli' + foreach ($name in @('openclaw.ps1', 'openclaw.cmd')) { + $managedCandidate = Join-Path $managedPrefix $name + if (Test-Path -LiteralPath $managedCandidate -PathType Leaf) { + return $managedCandidate + } + } + + foreach ($name in @('openclaw.ps1', 'openclaw.cmd')) { + $command = Get-Command $name -ErrorAction SilentlyContinue + if ($command) { + return $command.Source + } + } + + $prefixes = New-Object System.Collections.Generic.List[string] + if (-not [string]::IsNullOrWhiteSpace($env:APPDATA)) { + $prefixes.Add((Join-Path $env:APPDATA 'npm')) + } + $npm = Get-Command npm.cmd -ErrorAction SilentlyContinue + if ($npm) { + try { + $npmPrefix = (& $npm.Source config get prefix 2>$null | Select-Object -First 1).Trim() + if (-not [string]::IsNullOrWhiteSpace($npmPrefix)) { + $prefixes.Add($npmPrefix) + } + } catch { + Write-GatewayLog "Could not query npm prefix while locating the native CLI: $($_.Exception.Message)" + } + } + + foreach ($prefix in $prefixes | Select-Object -Unique) { + foreach ($name in @('openclaw.ps1', 'openclaw.cmd')) { + $candidate = Join-Path $prefix $name + if (Test-Path -LiteralPath $candidate -PathType Leaf) { + return $candidate + } + } + } + + return $null + } + + function Invoke-NativeOpenClawCli { + param( + [string]$CliPath, + [string[]]$Arguments + ) + + $managedHome = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile) + $managedProfile = Get-NativeGatewayProfile + $managedStateDir = Get-NativeGatewayStateDir + $managedSelectors = [ordered]@{ + OPENCLAW_PROFILE = $managedProfile + OPENCLAW_STATE_DIR = $managedStateDir + OPENCLAW_CONFIG_PATH = Join-Path $managedStateDir 'openclaw.json' + OPENCLAW_HOME = $managedHome + OPENCLAW_WINDOWS_TASK_NAME = $GatewayTaskName + OPENCLAW_GATEWAY_PORT = '' + OPENCLAW_GATEWAY_URL = '' + OPENCLAW_WRAPPER = '' + } + $savedSelectors = @{} + foreach ($name in $managedSelectors.Keys) { + $savedSelectors[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') + [Environment]::SetEnvironmentVariable($name, $managedSelectors[$name], 'Process') + } + + try { + $output = @(& $CliPath @Arguments 2>&1 | ForEach-Object { $_.ToString() }) + return [pscustomobject]@{ + ExitCode = [int]$LASTEXITCODE + Output = ($output -join [Environment]::NewLine) + } + } catch { + return [pscustomobject]@{ + ExitCode = -1 + Output = "Native OpenClaw CLI invocation failed: $($_.Exception.Message)" + } + } finally { + foreach ($name in $managedSelectors.Keys) { + [Environment]::SetEnvironmentVariable($name, $savedSelectors[$name], 'Process') + } + } + } + + function Get-NativeGatewayProfile { + $profile = ($DistroName -replace '[^A-Za-z0-9._-]', '-').Trim('-') + if ([string]::IsNullOrWhiteSpace($profile) -or + [string]::Equals($profile, 'default', [StringComparison]::OrdinalIgnoreCase)) { + return "companion-$GatewayPort" + } + return $profile + } + + function Get-NativeGatewayStateDir { + $userHome = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile) + $profile = Get-NativeGatewayProfile + return Join-Path $userHome ".openclaw-$profile" + } + + function Test-GatewayScheduledTask { + $schtasks = Join-Path $env:WINDIR 'System32\schtasks.exe' + if (-not (Test-Path -LiteralPath $schtasks)) { + return $false + } + + & $schtasks /Query /TN $GatewayTaskName *> $null + return $LASTEXITCODE -eq 0 + } + + function Get-NativeGatewayPort { + param( + [string]$DataDir, + [string]$LocalDataDir + ) + + $ownership = Read-JsonFile (Join-Path $LocalDataDir 'native-gateway-install.json') + $ownedPort = Get-JsonPropertyValue $ownership 'GatewayPort' + $parsedOwnedPort = 0 + if ($null -ne $ownedPort -and [int]::TryParse([string]$ownedPort, [ref]$parsedOwnedPort)) { + return $parsedOwnedPort + } + + $configPath = Join-Path (Get-NativeGatewayStateDir) 'openclaw.json' + $config = Read-JsonFile $configPath + $gateway = Get-JsonPropertyValue $config 'gateway' + $configuredPort = Get-JsonPropertyValue $gateway 'port' + $parsedPort = 0 + if ($null -ne $configuredPort -and [int]::TryParse([string]$configuredPort, [ref]$parsedPort)) { + return $parsedPort + } + + # setup-state can describe the previous mode after an interrupted switch, + # so it is only a legacy fallback behind native ownership/config state. + $state = Read-JsonFile (Join-Path $LocalDataDir 'setup-state.json') + $gatewayUrl = [string](Get-JsonPropertyValue $state 'GatewayUrl') + if (-not [string]::IsNullOrWhiteSpace($gatewayUrl)) { + try { + $uri = [Uri]$gatewayUrl + if ($uri.Port -gt 0) { + return [int]$uri.Port + } + } catch { + Write-GatewayLog "Could not parse native gateway URL '$gatewayUrl': $($_.Exception.Message)" + } + } + + return $GatewayPort + } + + function Get-NativeGatewayServiceFiles { + $safeTaskName = $GatewayTaskName -replace '[<>:"/\\|?*\x00-\x1F]', '_' + $startupDir = Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs\Startup' + $stateDir = Get-NativeGatewayStateDir + return @( + (Join-Path $startupDir "$safeTaskName.cmd"), + (Join-Path $startupDir "$safeTaskName.vbs"), + (Join-Path $stateDir 'gateway.cmd'), + (Join-Path $stateDir 'gateway.vbs') + ) + } + + function Get-NativeGatewayProcesses { + param([int]$GatewayPort) + + if (-not (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue)) { + throw 'Get-NetTCPConnection is unavailable; cannot verify native gateway shutdown.' + } + + $connections = @(Get-NetTCPConnection -LocalPort $GatewayPort -State Listen -ErrorAction SilentlyContinue) + $processes = New-Object System.Collections.ArrayList + foreach ($connection in $connections) { + $processId = [int]$connection.OwningProcess + if ($processId -le 0) { + continue + } + + $process = Get-CimInstance Win32_Process -Filter "ProcessId = $processId" -ErrorAction SilentlyContinue + $commandLine = [string](Get-JsonPropertyValue $process 'CommandLine') + if ($commandLine -match '(?i)(openclaw.*gateway|gateway.*openclaw)') { + [void]$processes.Add($process) + } + } + + return @($processes.ToArray()) + } + + function Assert-NativeGatewayRuntimeStopped { + param([int]$GatewayPort) + + for ($attempt = 1; $attempt -le 10; $attempt++) { + if (@(Get-NativeGatewayProcesses -GatewayPort $GatewayPort).Count -eq 0) { + return + } + Start-Sleep -Milliseconds 250 + } + + throw "An OpenClaw gateway is still listening on port $GatewayPort, but process ownership cannot be proven. It was left untouched." + } + + function Remove-NativeGatewayService { + param([int]$GatewayPort) + + $cliUninstallSucceeded = $false + $nativeCli = Resolve-NativeOpenClawCli + if ($nativeCli) { + $stop = Invoke-NativeOpenClawCli -CliPath $nativeCli -Arguments @('gateway', 'stop') + Write-GatewayLog "Native gateway stop exited $($stop.ExitCode): $($stop.Output)" + + $uninstall = Invoke-NativeOpenClawCli -CliPath $nativeCli -Arguments @('gateway', 'uninstall') + Write-GatewayLog "Native gateway uninstall exited $($uninstall.ExitCode): $($uninstall.Output)" + $cliUninstallSucceeded = $uninstall.ExitCode -eq 0 + } else { + Write-GatewayLog 'Native OpenClaw CLI was not found; using verified direct service cleanup.' + } + + if (Test-GatewayScheduledTask) { + $schtasks = Join-Path $env:WINDIR 'System32\schtasks.exe' + & $schtasks /End /TN $GatewayTaskName *> $null + $deleteOutput = & $schtasks /Delete /TN $GatewayTaskName /F 2>&1 + if ($LASTEXITCODE -ne 0 -and (Test-GatewayScheduledTask)) { + throw "Failed to remove native gateway task '$GatewayTaskName': $deleteOutput" + } + Write-GatewayLog "Removed native gateway task '$GatewayTaskName'." + } else { + Write-GatewayLog "Native gateway task '$GatewayTaskName' is absent." + } + + foreach ($path in @(Get-NativeGatewayServiceFiles)) { + if (Test-Path -LiteralPath $path -PathType Leaf) { + Remove-Item -LiteralPath $path -Force -ErrorAction Stop + Write-GatewayLog "Removed native gateway service file '$path'." + } + if (Test-Path -LiteralPath $path) { + throw "Failed to remove native gateway service file '$path'." + } + } + + Assert-NativeGatewayRuntimeStopped -GatewayPort $GatewayPort + if (Test-GatewayScheduledTask) { + throw "Native gateway task '$GatewayTaskName' still exists after cleanup." + } + if (-not $cliUninstallSucceeded) { + Write-GatewayLog 'Native CLI uninstall was unavailable or failed; verified direct cleanup completed.' + } + } + + function Remove-JsonPath { + param( + [object]$Root, + [string]$Path + ) + + $segments = $Path -split '\.' + $current = $Root + for ($index = 0; $index -lt $segments.Length - 1; $index++) { + $property = $current.PSObject.Properties | Where-Object { $_.Name -ieq $segments[$index] } | Select-Object -First 1 + if ($null -eq $property -or $null -eq $property.Value) { + return $false + } + $current = $property.Value + } + + $leaf = $current.PSObject.Properties | Where-Object { $_.Name -ieq $segments[-1] } | Select-Object -First 1 + if ($null -eq $leaf) { + return $false + } + + $current.PSObject.Properties.Remove($leaf.Name) + return $true + } + + function Remove-NativeGatewayConfig { + param([string]$LocalDataDir) + + $configPath = Join-Path (Get-NativeGatewayStateDir) 'openclaw.json' + $managedPaths = @() + $ownershipPath = Join-Path $LocalDataDir 'native-gateway-install.json' + if (Test-Path -LiteralPath $ownershipPath -PathType Leaf) { + $ownership = Read-JsonFile $ownershipPath + if ($null -eq $ownership) { + throw "Native gateway ownership marker exists but could not be read: $ownershipPath" + } + + $persistedPaths = Get-JsonPropertyValue $ownership 'ManagedConfigPaths' + if ($null -eq $persistedPaths) { + throw "Native gateway ownership marker has no ManagedConfigPaths: $ownershipPath" + } + + foreach ($path in @($persistedPaths)) { + $pathText = [string]$path + if ($pathText -notmatch '^[A-Za-z0-9._-]+$') { + throw "Native gateway ownership marker contains an invalid config path." + } + $managedPaths += $pathText + } + } else { + # Legacy native installs predate the ownership marker and own this fixed set. + $managedPaths = @( + 'gateway.mode', + 'gateway.port', + 'gateway.bind', + 'gateway.auth.mode', + 'gateway.auth.token', + 'gateway.reload.mode', + 'gateway.nodes.allowCommands', + 'plugins.entries.device-pair.enabled', + 'plugins.entries.device-pair.config.publicUrl' + ) + } + + $nativeCli = Resolve-NativeOpenClawCli + if ($nativeCli) { + $cliCleanupSucceeded = $true + foreach ($path in @($managedPaths | Select-Object -Unique)) { + $unset = Invoke-NativeOpenClawCli -CliPath $nativeCli -Arguments @('config', 'unset', $path) + $pathWasMissing = $unset.Output -match '(?i)(Config path not found|Nothing was changed)' + if ($unset.ExitCode -ne 0 -and -not $pathWasMissing) { + $cliCleanupSucceeded = $false + Write-GatewayLog "Native config unset failed for '$path': $($unset.Output)" + break + } + } + if ($cliCleanupSucceeded) { + Write-GatewayLog 'Removed setup-managed native gateway configuration through the OpenClaw JSON5 writer.' + return + } + } + + $profileOwnerPath = Join-Path $LocalDataDir 'native-gateway-profile-owner.json' + $profileOwnership = Read-JsonFile $profileOwnerPath + if (Test-NativeOwnershipMatches -Ownership $profileOwnership) { + # Installer uninstall deletes this whole isolated profile below. Avoid + # parsing OpenClaw's JSON5 config with PowerShell's strict JSON parser. + Write-GatewayLog 'Native config cleanup deferred to app-owned profile removal.' + return + } + + $config = Read-JsonFile $configPath + if ($null -eq $config) { + if (Test-Path -LiteralPath $configPath -PathType Leaf) { + throw "Native OpenClaw config exists but could not be read after CLI cleanup failed: $configPath" + } + Write-GatewayLog 'Native OpenClaw config was already absent.' + return + } + + $changed = $false + foreach ($path in @($managedPaths | Select-Object -Unique)) { + if (Remove-JsonPath -Root $config -Path $path) { + $changed = $true + } + } + + if ($changed) { + Write-JsonFileAtomic -Path $configPath -Value $config + Write-GatewayLog 'Removed setup-managed native gateway configuration and credential.' + } else { + Write-GatewayLog 'No setup-managed native gateway configuration remained.' + } + } + function Remove-WindowsGatewayArtifacts { $dataDir = Resolve-AppDataDir $localDataDir = Resolve-LocalDataDir + $ownedNativeRecordId = Get-NativeGatewayRecordId -LocalDataDir $localDataDir Write-GatewayLog "Cleaning Windows-side local gateway artifacts. AppData='$dataDir'; LocalData='$localDataDir'." @@ -454,7 +940,18 @@ function Resolve-AppDataDir { Remove-FileIfExists -Path (Join-Path $dataDir 'exec-policy.json') -Label 'exec-policy.json' Remove-KeepaliveMarker -LocalDataDir $localDataDir - $registryCleanup = Remove-SetupManagedGatewayRecords -DataDir $dataDir + $registryCleanup = Remove-SetupManagedGatewayRecords ` + -DataDir $dataDir ` + -InstallMode $script:installedGatewayMode ` + -OwnedNativeRecordId $ownedNativeRecordId + $profileOwnerPath = Join-Path $localDataDir 'native-gateway-profile-owner.json' + $profileOwnership = Read-JsonFile $profileOwnerPath + if (Test-NativeOwnershipMatches -Ownership $profileOwnership) { + Remove-OwnedDirectoryStrict -Path (Get-NativeGatewayStateDir) -Label 'app-owned native gateway profile' + } + Remove-OwnedDirectoryStrict -Path (Join-Path $localDataDir 'native-cli') -Label 'app-owned native CLI' + Remove-FileIfExists -Path (Join-Path $localDataDir 'native-gateway-install.json') -Label 'native gateway ownership marker' + Remove-FileIfExists -Path $profileOwnerPath -Label 'native gateway profile ownership marker' if ($registryCleanup.HasExternalGateways) { Write-GatewayLog 'External gateway records remain; preserving root device tokens.' } else { @@ -476,7 +973,7 @@ function Resolve-AppDataDir { -ExitCode 0 ` -Message $Message ` -Details ([ordered]@{ artifactWarnings = @($script:cleanupWarnings) }) - Write-Host "OpenClaw local WSL gateway removed successfully." + Write-Host "OpenClaw local gateway removed successfully." exit 0 } @@ -556,6 +1053,19 @@ function Test-DistroNotFound { $Output -match 'distribution.*not.*found' } +function Test-WslUnavailable { + param([string]$Output) + + if ([string]::IsNullOrWhiteSpace($Output)) { + return $false + } + + return $Output -match 'WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED' -or + $Output -match '0x8007019e' -or + $Output -match 'optional component is not enabled' -or + $Output -match 'Windows Subsystem for Linux has not been enabled' +} + function Test-DistroListed { param([string]$Output) @@ -568,7 +1078,9 @@ function Test-DistroListed { } function Remove-GatewayDirectory { - $gatewayDirectory = Join-Path $AppRoot "wsl\$DistroName" + param([Parameter(Mandatory = $true)][string]$LocalDataDir) + + $gatewayDirectory = Join-Path $LocalDataDir "wsl\$DistroName" if (-not (Test-Path -LiteralPath $gatewayDirectory)) { Write-GatewayLog "Gateway directory does not exist: $gatewayDirectory" @@ -603,17 +1115,40 @@ try { Ensure-AppRoot Write-GatewayLog "Starting local gateway cleanup for $DistroName." + $dataDir = Resolve-AppDataDir + $localDataDir = Resolve-LocalDataDir + $installMode = Get-InstalledGatewayMode -DataDir $dataDir -LocalDataDir $localDataDir + # Installer uninstall is destructive for every app-owned local runtime, even + # when a mode switch preserved the inactive runtime for later reuse. + $script:installedGatewayMode = 'All' + Write-GatewayLog "Detected installed gateway mode: $installMode." + if ($installMode -eq 'NativeWindows') { + $nativeGatewayPort = Get-NativeGatewayPort -DataDir $dataDir -LocalDataDir $localDataDir + Remove-NativeGatewayService -GatewayPort $nativeGatewayPort + Remove-NativeGatewayConfig -LocalDataDir $localDataDir + Write-GatewayLog 'Local native Windows gateway removed; checking for preserved app-owned WSL data.' + } + $script:WslPath = Get-WslExePath if (-not $script:WslPath) { Write-GatewayLog 'wsl.exe was not found; removing stale gateway directory if present.' - Remove-GatewayDirectory + Remove-GatewayDirectory -LocalDataDir $localDataDir Complete-GatewayCleanup -Message 'wsl.exe was not found; no registered WSL gateway can be removed.' } $listResult = Invoke-Wsl -Arguments @('--list', '--quiet') + if ($listResult.ExitCode -ne 0) { + if ($installMode -eq 'NativeWindows' -and (Test-WslUnavailable $listResult.Output)) { + Write-GatewayLog 'WSL is unavailable; no preserved app-owned distro can be registered.' + Remove-GatewayDirectory -LocalDataDir $localDataDir + Complete-GatewayCleanup -Message 'Local native Windows gateway removed.' + } + + throw "Failed to list WSL distributions: $($listResult.Output)" + } if ($listResult.ExitCode -eq 0 -and -not (Test-DistroListed $listResult.Output)) { Write-GatewayLog "WSL distro '$DistroName' is not registered; removing stale gateway directory if present." - Remove-GatewayDirectory + Remove-GatewayDirectory -LocalDataDir $localDataDir Complete-GatewayCleanup -Message "Local WSL gateway '$DistroName' was already unregistered." } @@ -638,7 +1173,7 @@ try { Write-GatewayLog "Treating missing distro '$DistroName' as already removed." } - Remove-GatewayDirectory + Remove-GatewayDirectory -LocalDataDir $localDataDir Complete-GatewayCleanup -Message "Local WSL gateway '$DistroName' removed." } catch { diff --git a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml index 79350ed36..2689b5959 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml +++ b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml @@ -11,7 +11,7 @@ - @@ -106,7 +106,7 @@ - + @@ -129,7 +129,7 @@ - + diff --git a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs index 8ca682852..176463303 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs +++ b/src/OpenClaw.SetupEngine.UI/Pages/CapabilitiesPage.xaml.cs @@ -56,6 +56,15 @@ public CapabilitiesPage() protected override void OnNavigatedTo(NavigationEventArgs e) { _config = e.Parameter as SetupConfig ?? new SetupConfig(); + SetupTitleText.Text = _config.InstallMode == GatewayInstallMode.NativeWindows + ? "Set up the Windows gateway" + : "Set up the WSL gateway"; + InstallAdminBorder.Visibility = _config.InstallMode == GatewayInstallMode.Wsl + ? Visibility.Visible + : Visibility.Collapsed; + InstallRuntimeBadgeText.Text = _config.InstallMode == GatewayInstallMode.NativeWindows + ? "No WSL" + : "~200 MB"; // The tray always registers device.info/status with Node Mode. Keep the // setup declaration and gateway allowlist aligned with that runtime contract. _config.Capabilities.Device = true; diff --git a/src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml index 787e9a4d6..5aa32b5e8 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml +++ b/src/OpenClaw.SetupEngine.UI/Pages/ProgressPage.xaml @@ -11,7 +11,7 @@ - _runCts != null && !_pipelineFinished; - // Map pipeline step IDs to display groups (N:1) - private static readonly (string GroupId, string DisplayName, string[] StepIds)[] StepGroups = - [ - ("preflight", "Check system", ["preflight-os", "preflight-wsl"]), - ("cleanup", "Removing existing gateway", ["cleanup-distro", "cleanup-gateway"]), - ("port", "Checking gateway port", ["preflight-port"]), - ("wsl-create", "Installing clean WSL gateway", ["wsl-create"]), - ("wsl-configure", "Configuring instance", ["wsl-configure", "validate-wsl-lockdown"]), - ("install-cli", "Installing OpenClaw", ["install-cli"]), - ("configure", "Preparing gateway", ["configure-gateway", "install-service"]), - ("start", "Starting gateway", ["start-gateway", "mint-token"]), - ("pairing", "Pairing device", ["pair-operator", "pair-node", "verify-e2e"]), - ("finish", "Finishing setup", ["run-wizard", "start-keepalive"]), - ]; + private (string GroupId, string DisplayName, string[] StepIds)[] _stepGroups = []; public ProgressPage() { @@ -59,12 +46,22 @@ protected override void OnNavigatedTo(NavigationEventArgs e) _config = args?.Config ?? e.Parameter as SetupConfig ?? new SetupConfig(); _dataDir = args?.DataDir ?? SetupContext.ResolveDataDir(); _localDataDir = args?.LocalDataDir ?? SetupContext.ResolveLocalDataDir(); - SubtitleText.Text = $"Creating {_config.DistroName} WSL instance"; + _stepGroups = BuildStepGroups(_config.InstallMode); + if (_config.InstallMode == GatewayInstallMode.NativeWindows) + { + HeaderTitleText.Text = "Setting up Windows gateway"; + SubtitleText.Text = "Installing OpenClaw directly on Windows"; + } + else + { + HeaderTitleText.Text = "Setting up WSL gateway"; + SubtitleText.Text = $"Creating {_config.DistroName} WSL instance"; + } BuildStepRows(); if (args?.ShowMilestoneOnly == true) { - foreach (var (groupId, _, _) in StepGroups) + foreach (var (groupId, _, _) in _stepGroups) if (_rows.TryGetValue(groupId, out var row)) row.SetStatus(StepStatus.Done); ShowGatewayInstalledMilestone(); @@ -75,7 +72,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e) { if (SetupPreview.RequestedPage == "milestone") { - foreach (var (groupId, _, _) in StepGroups) + foreach (var (groupId, _, _) in _stepGroups) if (_rows.TryGetValue(groupId, out var row)) row.SetStatus(StepStatus.Done); ShowGatewayInstalledMilestone(); @@ -89,8 +86,10 @@ protected override void OnNavigatedTo(NavigationEventArgs e) private void RenderProgressPreview() { - SubtitleText.Text = "Creating OpenClawGateway WSL instance β€” about 4 minutes left"; - var ids = StepGroups.Select(g => g.GroupId).ToArray(); + SubtitleText.Text = _config?.InstallMode == GatewayInstallMode.NativeWindows + ? "Installing OpenClaw on Windows β€” about 2 minutes left" + : "Creating OpenClawGateway WSL instance β€” about 4 minutes left"; + var ids = _stepGroups.Select(g => g.GroupId).ToArray(); for (int i = 0; i < ids.Length; i++) { var status = i < 3 ? StepStatus.Done : i == 3 ? StepStatus.Running : StepStatus.Idle; @@ -108,7 +107,7 @@ private void RenderProgressPreview() private void BuildStepRows() { - foreach (var (groupId, displayName, _) in StepGroups) + foreach (var (groupId, displayName, _) in _stepGroups) { var row = new StepRow(displayName); _rows[groupId] = row; @@ -227,10 +226,10 @@ private void OnStepProgress(object? sender, StepProgressEvent e) DispatcherQueue.TryEnqueue(() => { // Find which group this step belongs to - var groupIndex = Array.FindIndex(StepGroups, g => g.StepIds.Contains(e.StepId)); + var groupIndex = Array.FindIndex(_stepGroups, g => g.StepIds.Contains(e.StepId)); if (groupIndex < 0) return; - var group = StepGroups[groupIndex]; + var group = _stepGroups[groupIndex]; var row = _rows[group.GroupId]; if (e.Outcome == null) @@ -238,7 +237,7 @@ private void OnStepProgress(object? sender, StepProgressEvent e) // Step started β€” mark all previous groups as done if still running for (int i = 0; i < groupIndex; i++) { - var prevRow = _rows[StepGroups[i].GroupId]; + var prevRow = _rows[_stepGroups[i].GroupId]; if (prevRow.Status == StepStatus.Running) prevRow.SetStatus(StepStatus.Done); } @@ -314,10 +313,38 @@ private void Onboard_Click(object sender, RoutedEventArgs e) } private static List BuildSteps(SetupConfig config) - => SetupStepFactory.BuildDefaultSteps() + => SetupStepFactory.BuildDefaultSteps(config) .Where(step => step is not RunGatewayWizardStep) .Where(step => config.SkipWizard || step is not WindowsNodeBootstrapContextStep) .ToList(); + + private static (string GroupId, string DisplayName, string[] StepIds)[] BuildStepGroups( + GatewayInstallMode installMode) => + installMode == GatewayInstallMode.NativeWindows + ? + [ + ("preflight", "Check Windows", ["preflight-os"]), + ("prepare", "Prepare Windows gateway", ["begin-native-install", "stop-conflicting-gateways"]), + ("install-cli", "Install OpenClaw", ["install-native-cli"]), + ("cleanup", "Finish switching local gateway mode", ["native-service-cleanup", "cleanup-gateway"]), + ("port", "Check gateway port", ["preflight-port"]), + ("configure", "Prepare gateway", ["configure-gateway", "install-service"]), + ("start", "Start gateway", ["start-gateway", "mint-token"]), + ("pairing", "Pair this app", ["pair-operator", "pair-node", "verify-e2e"]), + ] + : + [ + ("preflight", "Check system", ["preflight-os", "preflight-wsl"]), + ("cleanup", "Removing existing gateway", ["stop-conflicting-gateways", "native-service-cleanup", "cleanup-distro", "cleanup-gateway"]), + ("port", "Checking gateway port", ["preflight-port"]), + ("wsl-create", "Installing clean WSL gateway", ["wsl-create"]), + ("wsl-configure", "Configuring instance", ["wsl-configure", "validate-wsl-lockdown", "sync-ca-certs"]), + ("install-cli", "Installing OpenClaw", ["install-cli"]), + ("configure", "Preparing gateway", ["configure-gateway", "install-service"]), + ("start", "Starting gateway", ["start-gateway", "mint-token"]), + ("pairing", "Pairing device", ["pair-operator", "pair-node", "verify-e2e"]), + ("finish", "Finishing setup", ["start-keepalive"]), + ]; } // ─── Step Row UI Element ─── diff --git a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml index 25f4340de..c86f0b8f5 100644 --- a/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml +++ b/src/OpenClaw.SetupEngine.UI/Pages/WelcomePage.xaml @@ -31,7 +31,7 @@ Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource AccentFillColorDefaultBrush}" BorderThickness="1" - AutomationProperties.Name="Install a local gateway (WSL), recommended"> + AutomationProperties.Name="Install on Windows natively, recommended"> @@ -41,7 +41,7 @@ + Text="The simplest path. Installs OpenClaw directly on Windows with managed startup β€” no WSL required." /> + + + + + +