Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ONBOARDING_WIZARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The setup flow no longer configures remote/manual gateways inline. The Welcome p
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.

### 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.
Installs and connects a new app-owned `OpenClawGateway` WSL instance from a clean WSL baseline. If the WSL platform is missing or its optional component is not initialized, setup requests administrator approval to install it, re-inspects readiness, and reports when a Windows restart is required. 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.

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).

Expand Down
31 changes: 28 additions & 3 deletions src/OpenClaw.SetupEngine/PreflightWslStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ public static async Task<WslViabilityResult> InspectAsync(
}

var combined = $"{status.Stdout}\n{status.Stderr}";
if (LooksPlatformInstallRequired(status))
{
return new(
WslViabilityKind.Installable,
"WSL is not initialized yet.",
"Setup can request administrator approval to initialize and verify it before continuing.");
}

if (WslInstallSupport.TryGetEnvironmentIssue(combined, out var message))
{
logger.Warn($"WSL environment issue detected: {NormalizeWslOutput(combined).Trim()}");
Expand Down Expand Up @@ -147,10 +155,20 @@ internal static bool LooksUnavailable(CommandResult result)
var text = NormalizeWslOutput($"{result.Stdout}\n{result.Stderr}");
return text.Contains("aka.ms/wslinstall", StringComparison.OrdinalIgnoreCase)
|| text.Contains("Windows Subsystem for Linux has no installed distributions", StringComparison.OrdinalIgnoreCase)
|| LooksPlatformInstallRequired(text)
|| text.Contains("not recognized", StringComparison.OrdinalIgnoreCase)
|| text.Contains("not installed", StringComparison.OrdinalIgnoreCase);
}

private static bool LooksPlatformInstallRequired(CommandResult result) =>
LooksPlatformInstallRequired(NormalizeWslOutput($"{result.Stdout}\n{result.Stderr}"));

private static bool LooksPlatformInstallRequired(string text) =>
text.Contains("WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED", StringComparison.OrdinalIgnoreCase)
|| text.Contains("0x8007019e", StringComparison.OrdinalIgnoreCase)
|| text.Contains("requires the Windows Subsystem for Linux Optional Component", StringComparison.OrdinalIgnoreCase)
|| text.Contains("Optional components needed to run WSL are not installed", StringComparison.OrdinalIgnoreCase);

private static bool LooksTooOldForVersionCommand(CommandResult result)
{
var text = NormalizeWslOutput($"{result.Stdout}\n{result.Stderr}");
Expand Down Expand Up @@ -294,8 +312,15 @@ public override async Task<StepResult> ExecuteAsync(SetupContext ctx, Cancellati

viability = await WslViabilityInspector.InspectAsync(ctx.Commands, ctx.Logger, ct);
ctx.WslViability = viability;
return viability.Kind == WslViabilityKind.Ready
? StepResult.Ok("WSL platform installed and verified.")
: StepResult.Terminal(viability.Description);
if (viability.Kind == WslViabilityKind.Ready)
return StepResult.Ok("WSL platform installed and verified.");
if (viability.Kind == WslViabilityKind.Installable)
{
return StepResult.Terminal(
"WSL platform installation completed, but Windows must be restarted before WSL is ready. " +
"Reboot Windows, then run setup again.");
}

return StepResult.Terminal(viability.Description);
}
}
165 changes: 163 additions & 2 deletions tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1276,6 +1276,149 @@ public async Task EnsureWslPlatform_InstallsOnlyAfterReadOnlyPreflight()
Assert.Equal(3, commands.Calls.Count);
}

[Fact]
public async Task EnsureWslPlatform_LeavesReadyWslUnchanged()
{
var installCalls = 0;
var commands = new FakeCommandRunner(args => args switch
{
["--version"] => Ok("WSL version: 2.7.3.0\n"),
["--status"] => Ok("Default Version: 2\n"),
_ => Fail($"unexpected args: {string.Join(' ', args)}"),
});
var ctx = CreateContext(commands: commands);
var step = new EnsureWslPlatformStep((_, _) =>
{
installCalls++;
return Task.FromResult(StepResult.Ok("initialized"));
});

var result = await step.ExecuteAsync(ctx, CancellationToken.None);

Assert.Equal(StepOutcome.Success, result.Outcome);
Assert.Equal("WSL platform is ready.", result.Message);
Assert.Equal(0, installCalls);
Assert.Equal(WslViabilityKind.Ready, ctx.WslViability?.Kind);
Assert.Equal(2, commands.Calls.Count);
}

[Fact]
public async Task PreflightWsl_UninitializedPlatformIsInstallable()
{
var commands = new FakeCommandRunner(args => args switch
{
["--version"] => Ok("WSL version: 2.7.3.0\n"),
["--status"] => new CommandResult(
1,
"",
"This application requires the Windows Subsystem for Linux Optional Component.\n" +
"Install it by running: wsl.exe --install --no-distribution\n" +
"Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED",
TimeSpan.Zero,
TimedOut: false),
_ => Fail($"unexpected args: {string.Join(' ', args)}"),
});
var ctx = CreateContext(commands: commands);

var result = await new PreflightWslStep().ExecuteAsync(ctx, CancellationToken.None);

Assert.Equal(StepOutcome.Success, result.Outcome);
Assert.Equal(WslViabilityKind.Installable, ctx.WslViability?.Kind);
Assert.Contains("not initialized", result.Message);
Assert.DoesNotContain(commands.Calls, call => call.Arguments.Contains("--install"));
}

[Fact]
public async Task EnsureWslPlatform_InitializesPlatformAndReinspectsReadiness()
{
var initialized = false;
var installCalls = 0;
var commands = new FakeCommandRunner(args => args switch
{
["--version"] => Ok("WSL version: 2.7.3.0\n"),
["--status"] when !initialized => new CommandResult(
1,
"",
"Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED",
TimeSpan.Zero,
TimedOut: false),
["--status"] => Ok("Default Version: 2\n"),
_ => Fail($"unexpected args: {string.Join(' ', args)}"),
});
var ctx = CreateContext(commands: commands);
var step = new EnsureWslPlatformStep((_, _) =>
{
installCalls++;
initialized = true;
return Task.FromResult(StepResult.Ok("initialized"));
});

var result = await step.ExecuteAsync(ctx, CancellationToken.None);

Assert.Equal(StepOutcome.Success, result.Outcome);
Assert.Equal("WSL platform installed and verified.", result.Message);
Assert.Equal(1, installCalls);
Assert.Equal(WslViabilityKind.Ready, ctx.WslViability?.Kind);
Assert.Equal(4, commands.Calls.Count);
}

[Fact]
public async Task EnsureWslPlatform_RequiresRestartWhenInitializationIsStillPending()
{
var installCalls = 0;
var commands = new FakeCommandRunner(args => args switch
{
["--version"] => Ok("WSL version: 2.7.3.0\n"),
["--status"] => new CommandResult(
1,
"",
"Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED",
TimeSpan.Zero,
TimedOut: false),
_ => Fail($"unexpected args: {string.Join(' ', args)}"),
});
var ctx = CreateContext(commands: commands);
var step = new EnsureWslPlatformStep((_, _) =>
{
installCalls++;
return Task.FromResult(StepResult.Ok("initialized"));
});

var result = await step.ExecuteAsync(ctx, CancellationToken.None);

Assert.Equal(StepOutcome.FailedTerminal, result.Outcome);
Assert.Contains("restarted", result.Message, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Reboot Windows", result.Message);
Assert.Equal(1, installCalls);
Assert.Equal(WslViabilityKind.Installable, ctx.WslViability?.Kind);
Assert.Equal(4, commands.Calls.Count);
}

[Fact]
public async Task EnsureWslPlatform_PropagatesElevationCancellationWithoutReinspection()
{
var commands = new FakeCommandRunner(args => args switch
{
["--version"] => Ok("WSL version: 2.7.3.0\n"),
["--status"] => new CommandResult(
1,
"",
"Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED",
TimeSpan.Zero,
TimedOut: false),
_ => Fail($"unexpected args: {string.Join(' ', args)}"),
});
var ctx = CreateContext(commands: commands);
var step = new EnsureWslPlatformStep((_, _) =>
Task.FromResult(StepResult.Fail("WSL platform install was cancelled at the elevation prompt.")));

var result = await step.ExecuteAsync(ctx, CancellationToken.None);

Assert.Equal(StepOutcome.Failed, result.Outcome);
Assert.Contains("elevation prompt", result.Message);
Assert.Equal(2, commands.Calls.Count);
}

[Fact]
public async Task PreflightWsl_UnclassifiedStatusFailureFailsClosed()
{
Expand Down Expand Up @@ -1855,10 +1998,16 @@ public async Task PreflightWsl_FailsTerminalWhenVirtualizationDisabledInFirmware
if (args is ["--version"])
return Ok("WSL version: 2.7.3.0\n");
if (args is ["--status"])
return Ok(
{
return new CommandResult(
1,
"",
"WSL2 is unable to start since virtualization is not enabled on this machine. "
+ "Please ensure the 'Virtual Machine Platform' optional component is enabled "
+ "and virtualization is turned on in your computer's firmware settings.");
+ "and virtualization is turned on in your computer's firmware settings.",
TimeSpan.Zero,
TimedOut: false);
}
return Ok();
});
var ctx = CreateContext(commands: commands);
Expand Down Expand Up @@ -2970,6 +3119,18 @@ public void ExistingConfigDetector_TreatsUnavailableWslAsNoDistro()
Assert.False(ExistingConfigDetector.InterpretDistroList(result, "OpenClawGateway"));
}

[Theory]
[InlineData("Error code: Wsl/WSL_E_WSL_OPTIONAL_COMPONENT_REQUIRED")]
[InlineData("This application requires the Windows Subsystem for Linux Optional Component.")]
[InlineData("Optional components needed to run WSL are not installed.")]
[InlineData("Error: 0x8007019e")]
public void ExistingConfigDetector_TreatsUninitializedWslAsNoDistro(string error)
{
var result = new CommandResult(1, "", error, TimeSpan.Zero, TimedOut: false);

Assert.False(ExistingConfigDetector.InterpretDistroList(result, "OpenClawGateway"));
}

[Theory]
[InlineData(true, 1, "")]
[InlineData(false, 1, "unexpected failure")]
Expand Down
Loading