Skip to content
Merged
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
35 changes: 32 additions & 3 deletions src/OpenClaw.SetupEngine/SetupSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1224,7 +1224,14 @@ public sealed class ConfigureGatewayStep : SetupStep
{
internal const string DevicePairPublicUrlKey = "plugins.entries.device-pair.config.publicUrl";
internal const string DevicePairEnabledKey = "plugins.entries.device-pair.enabled";
internal static readonly TimeSpan GatewayConfigurationTimeout = TimeSpan.FromSeconds(120);
// Each `openclaw config set` emitted below spawns the Node CLI fresh inside WSL; on a
// newly created distro with a cold cache that is ~4-5s apiece. Budget the step by how
// many config commands we actually emit -- BuildConfigCommands grows with the
// device-pair keys and every Gateway.ExtraConfig entry -- with a floor so the minimal
// path keeps generous headroom. A fixed cap silently regresses as the list grows.
internal static readonly TimeSpan ConfigBaseBudget = TimeSpan.FromSeconds(45);
internal static readonly TimeSpan PerConfigCommandBudget = TimeSpan.FromSeconds(15);
internal static readonly TimeSpan MinConfigurationTimeout = TimeSpan.FromSeconds(180);

public override string Id => "configure-gateway";
public override string DisplayName => "Configure gateway";
Expand Down Expand Up @@ -1277,13 +1284,14 @@ public override async Task<StepResult> ExecuteAsync(SetupContext ctx, Cancellati
echo "GATEWAY_CONFIGURED"
""";

var result = await ctx.Commands.RunInWslAsync(distro, script, GatewayConfigurationTimeout, env, ct);
var timeout = ComputeConfigurationTimeout(configCommands);
var result = await ctx.Commands.RunInWslAsync(distro, script, timeout, env, ct);

if (result.ExitCode != 0 || !result.Stdout.Contains("GATEWAY_CONFIGURED"))
{
if (result.TimedOut)
return StepResult.Fail(
$"Gateway configuration timed out after {GatewayConfigurationTimeout.TotalSeconds:0}s while running openclaw config inside WSL.");
$"Gateway configuration timed out after {timeout.TotalSeconds:0}s while running openclaw config inside WSL.");

return StepResult.Fail($"Gateway configuration failed (exit {result.ExitCode}): {result.Stderr}");
}
Expand Down Expand Up @@ -1343,6 +1351,27 @@ openclaw config set gateway.nodes.allowCommands {escapedAllowedCommands}
return configCommands;
}

// Budget = base + per-command, floored. Scales the WSL timeout with the number of
// `openclaw config set` invocations the step emits so it cannot silently regress as
// BuildConfigCommands grows.
internal static TimeSpan ComputeConfigurationTimeout(string configCommands)
{
var budget = ConfigBaseBudget + PerConfigCommandBudget * CountConfigSetCommands(configCommands);
return budget > MinConfigurationTimeout ? budget : MinConfigurationTimeout;
}

private static int CountConfigSetCommands(string configCommands)
{
var count = 0;
foreach (var line in configCommands.Split('\n'))
{
if (line.Contains("openclaw config set", StringComparison.Ordinal))
count++;
}

return count;
}

internal static string? GetDefaultDevicePairPublicUrl(GatewayConfig gw, int port) =>
gw.Bind == "loopback" ? $"http://127.0.0.1:{port}" : null;

Expand Down
50 changes: 48 additions & 2 deletions tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,10 @@ public async Task ConfigureGateway_UsesExtendedTimeoutForWslConfig()

Assert.True(result.IsSuccess);
var wslCall = Assert.Single(commands.WslCalls);
Assert.Equal(ConfigureGatewayStep.GatewayConfigurationTimeout, wslCall.Timeout);
Assert.Equal(
ConfigureGatewayStep.ComputeConfigurationTimeout(wslCall.Command),
wslCall.Timeout);
Assert.True(wslCall.Timeout >= ConfigureGatewayStep.MinConfigurationTimeout);
}

[Fact]
Expand All @@ -950,10 +953,53 @@ public async Task ConfigureGateway_ReturnsTimeoutSpecificFailure()

Assert.Equal(StepOutcome.Failed, result.Outcome);
var message = Assert.IsType<string>(result.Message);
Assert.Contains("Gateway configuration timed out after 120s", message);
Assert.Contains("Gateway configuration timed out after", message);
Assert.DoesNotContain("exit -1", message);
}

[Fact]
public void ComputeConfigurationTimeout_ScalesWithConfigCommandCount()
{
// Each `openclaw config set` pays a cold Node start inside WSL. As more keys are
// configured the budget must grow, otherwise the step silently regresses toward a
// timeout (the failure mode the fixed 120s cap only partially closed).
var fewCommands = ConfigureGatewayStep.BuildConfigCommands(
new GatewayConfig { Bind = "lan" },
18789,
"'[]'");
var manyCommands = ConfigureGatewayStep.BuildConfigCommands(
new GatewayConfig
{
Bind = "loopback",
ExtraConfig = new Dictionary<string, string>
{
["gateway.extra.one"] = "1",
["gateway.extra.two"] = "2",
["gateway.extra.three"] = "3",
["gateway.extra.four"] = "4",
},
},
18789,
"'[]'");

var fewTimeout = ConfigureGatewayStep.ComputeConfigurationTimeout(fewCommands);
var manyTimeout = ConfigureGatewayStep.ComputeConfigurationTimeout(manyCommands);

Assert.True(
manyTimeout > fewTimeout,
$"Timeout should grow with config command count; few={fewTimeout}, many={manyTimeout}");
}

[Fact]
public void ComputeConfigurationTimeout_NeverBelowFloor()
{
// A minimal config set must still receive the safety floor, never base + one.
var timeout = ConfigureGatewayStep.ComputeConfigurationTimeout(
"openclaw config set gateway.mode local");

Assert.True(timeout >= ConfigureGatewayStep.MinConfigurationTimeout);
}

[Theory]
[InlineData("""{"bootstrapToken":"boot-token"}""", "boot-token", "bootstrapToken")]
[InlineData("""{"setupCode":"setup-code"}""", "setup-code", "setupCode")]
Expand Down