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
15 changes: 15 additions & 0 deletions docs/SETUP_ENGINE_REDESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ The Setup Engine is a **config-driven system** for provisioning an OpenClaw WSL

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.

WSL wizard completion restores `gateway.reload.mode` before explicitly restarting
the Gateway. With Gateway 2026.9.6, restoring hybrid reload can initiate a systemd
restart for accumulated wizard changes before the explicit restart records its
owner intent. During that gap the live-owner lease is absent, so the CLI correctly
refuses to signal a process. `SetupWizardRunner` recognizes only that exact
serving-owner refusal, waits for verified managed endpoint ownership using the
existing bounded provenance probe, and retries the normal CLI restart once.
The probe allows up to 30 one-second retry delays, plus probe duration, for
`NoListener` and `UnknownListener` tagged `ListenerSnapshotChanged`. Other
unknown/conflicting listeners, other restart errors, and a repeated refusal still
fail setup. Listener provenance does not prove owner-lease or coordinator
readiness; the retried CLI command retains those guards. Restart-intent recording
contention is a separate failure and is not retried here. There is no direct
systemd restart fallback or ownership bypass.

> **Status note (2026-07-06):** Current default setup includes `WindowsNodeBootstrapContextStep`, which injects Windows-node context into the WSL workspace `AGENTS.md` after onboarding.

---
Expand Down
18 changes: 18 additions & 0 deletions src/OpenClaw.SetupEngine/SetupWizardRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public sealed class SetupWizardRunner
TimeSpan.FromMilliseconds(500);
internal const string StartupMigrationLeaseDiagnostic =
"OpenClaw startup migrations are already running for this state directory;";
internal const string RestartServingOwnerDiagnostic =
"GATEWAY_RESTART_PREPARATION_REFUSED: Cannot verify a live serving Gateway owner for the selected service. Gateway was not signaled.";
private static readonly Regex s_normalizeKeyRegex = new("[^a-z0-9]+", RegexOptions.Compiled);

// Progress steps can repeat while background work runs; keep bounded caps
Expand Down Expand Up @@ -614,6 +616,22 @@ internal async Task<StepResult> RestoreReloadModeAsync()
await StartGatewayStep.RestartAndWaitForHealthAsync(
_ctx,
CancellationToken.None);
if (!restartResult.IsSuccess &&
restartResult.Message?.Contains(RestartServingOwnerDiagnostic, StringComparison.Ordinal) == true)
{
// Restoring hybrid reload can initiate a supervisor restart before the CLI
// records its intent. Never bypass that CLI ownership gate or adopt a listener.
_ctx.Logger.Warn(
"Gateway restart owner was unavailable after restoring reload. Rechecking managed ownership before one restart retry.");
var retryOwnership = await VerifyExpectedManagedGatewayAsync(
"before retrying gateway restart");
if (!retryOwnership.IsSuccess)
return retryOwnership;

restartResult = await StartGatewayStep.RestartAndWaitForHealthAsync(
_ctx,
CancellationToken.None);
}
if (!restartResult.IsSuccess)
{
return StepResult.Fail(
Expand Down
89 changes: 89 additions & 0 deletions tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4266,6 +4266,95 @@ public async Task SetupWizard_RestoreReloadModeRestartsAndVerifiesGateway()
AssertReloadRestorationCompleted(commands);
}

[Fact]
public async Task SetupWizard_RestartOwnerGapReverifiesOwnershipBeforeOneRetry()
{
var restarts = 0;
var inspections = 0;
var commands = new FakeCommandRunner(
_ => Ok(),
(_, command, _) => command switch
{
var value when value.Contains("config set gateway.reload.mode") => Ok(),
var value when value.Contains("openclaw gateway restart") => Restart(),
var value when value.Contains("curl -s") => Ok("200"),
_ => Fail($"Unexpected command: {command}"),
});
var ctx = CreateContext(commands: commands);
ctx.DistroName = "test-distro";
ctx.EndpointProvenanceProbe = (_, _) =>
{
inspections++;
return Task.FromResult(new GatewayEndpointProvenance(
inspections == 1
? GatewayEndpointProvenanceKind.NoListener
: GatewayEndpointProvenanceKind.ExpectedManagedGateway,
ctx.Config.GatewayPort));
};

var result = await new SetupWizardRunner(ctx).RestoreReloadModeAsync();

Assert.True(result.IsSuccess, result.Message);
Assert.Equal(2, restarts);
Assert.Equal(3, inspections);
Assert.DoesNotContain(commands.WslCalls, call => call.Command.Contains("systemctl"));
return;

CommandResult Restart()
{
if (++restarts == 1)
return Fail(SetupWizardRunner.RestartServingOwnerDiagnostic);
Assert.Equal(2, inspections);
return Ok();
}
}

[Theory]
[InlineData(GatewayEndpointProvenanceKind.UnknownListener)]
[InlineData(GatewayEndpointProvenanceKind.ConflictingOpenClawGateway)]
public async Task SetupWizard_RestartOwnerGapDoesNotRetryAnUntrustedListener(
GatewayEndpointProvenanceKind kind)
{
var commands = new FakeCommandRunner(
_ => Ok(),
(_, command, _) => command.Contains("config set gateway.reload.mode")
? Ok()
: Fail(SetupWizardRunner.RestartServingOwnerDiagnostic));
var ctx = CreateContext(commands: commands);
ctx.DistroName = "test-distro";
ctx.EndpointProvenanceProbe = (_, _) => Task.FromResult(
new GatewayEndpointProvenance(kind, ctx.Config.GatewayPort));

var result = await new SetupWizardRunner(ctx).RestoreReloadModeAsync();

Assert.False(result.IsSuccess);
Assert.Contains("ownership verification failed", result.Message);
Assert.Single(commands.WslCalls, call => call.Command.Contains("openclaw gateway restart"));
}

[Theory]
[InlineData(SetupWizardRunner.RestartServingOwnerDiagnostic, 2)]
[InlineData("GATEWAY_RESTART_PREPARATION_REFUSED: Cannot verify the selected service command.", 1)]
[InlineData("StateDatabaseCoordinatorContentionError: another OpenClaw process owns state-lifecycle. GATEWAY_RESTART_PREPARATION_REFUSED: Cannot record restart intent for the serving Gateway. Gateway was not signaled.", 1)]
[InlineData("Unrelated restart failure", 1)]
public async Task SetupWizard_RestartRetryRemainsBoundedAndSpecific(string error, int expectedRestarts)
{
var commands = new FakeCommandRunner(
_ => Ok(),
(_, command, _) => command.Contains("config set gateway.reload.mode") ? Ok() : Fail(error));
var ctx = CreateContext(commands: commands);
ctx.DistroName = "test-distro";
TrustManagedEndpoint(ctx);

var result = await new SetupWizardRunner(ctx).RestoreReloadModeAsync();

Assert.False(result.IsSuccess);
Assert.Contains(error, result.Message);
Assert.Equal(expectedRestarts,
commands.WslCalls.Count(call => call.Command.Contains("openclaw gateway restart")));
Assert.DoesNotContain(commands.WslCalls, call => call.Command.Contains("systemctl"));
}

[Fact]
public async Task SetupWizard_RestoreReloadModeRetriesExactStartupMigrationLeaseContention()
{
Expand Down
Loading