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
3 changes: 2 additions & 1 deletion src/OpenClaw.SetupEngine/ConfigureGatewayStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ public override async Task<StepResult> ExecuteAsync(SetupContext ctx, Cancellati
""";

var timeout = ComputeConfigurationTimeout(configCommands);
var result = await ctx.Commands.RunInWslAsync(distro, script, timeout, env, ct);
// PATH prefix references $PATH. Pipe the script so wsl.exe cannot expand it on argv.
var result = await ctx.Commands.RunInWslAsync(distro, script, timeout, env, ct, inputViaStdin: true);

if (result.ExitCode != 0 || !result.Stdout.Contains("GATEWAY_CONFIGURED"))
{
Expand Down
4 changes: 2 additions & 2 deletions src/OpenClaw.SetupEngine/InstallGatewayServiceStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public override async Task<StepResult> ExecuteAsync(SetupContext ctx, Cancellati
var distro = ctx.DistroName!;

var result = await ctx.Commands.RunInWslAsync(
distro, $"{ctx.WslPathPrefix} && openclaw gateway install --force", TimeSpan.FromSeconds(60), ct: ct);
distro, $"{ctx.WslPathPrefix} && openclaw gateway install --force", TimeSpan.FromSeconds(60), ct: ct, inputViaStdin: true);

if (result.ExitCode != 0)
return StepResult.Fail($"Service install failed (exit {result.ExitCode}): {result.Stderr}");
Expand All @@ -30,6 +30,6 @@ public override async Task<StepResult> ExecuteAsync(SetupContext ctx, Cancellati

public override async Task RollbackAsync(SetupContext ctx, CancellationToken ct)
{
await ctx.Commands.RunInWslAsync(ctx.DistroName!, $"{ctx.WslPathPrefix} && openclaw gateway uninstall", TimeSpan.FromSeconds(30), ct: ct);
await ctx.Commands.RunInWslAsync(ctx.DistroName!, $"{ctx.WslPathPrefix} && openclaw gateway uninstall", TimeSpan.FromSeconds(30), ct: ct, inputViaStdin: true);
}
}
2 changes: 1 addition & 1 deletion src/OpenClaw.SetupEngine/MintBootstrapTokenStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public override async Task<StepResult> ExecuteAsync(SetupContext ctx, Cancellati
};

var mint = await ctx.Commands.RunInWslAsync(
distro, $"{ctx.WslPathPrefix} && openclaw qr --json", TimeSpan.FromSeconds(30), env, ct);
distro, $"{ctx.WslPathPrefix} && openclaw qr --json", TimeSpan.FromSeconds(30), env, ct, inputViaStdin: true);

if (mint.ExitCode == 0 && !string.IsNullOrWhiteSpace(mint.Stdout))
{
Expand Down
4 changes: 2 additions & 2 deletions src/OpenClaw.SetupEngine/PairNodeStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ internal static async Task<StepResult> AutoApproveNodePairing(SetupContext ctx,
var pending = await ctx.Commands.RunInWslAsync(
distro,
$"""{ctx.WslPathPrefix} && openclaw nodes list --json""",
TimeSpan.FromSeconds(30), env, ct);
TimeSpan.FromSeconds(30), env, ct, inputViaStdin: true);

ctx.Logger.Info($"Node pending list: exit={pending.ExitCode}");

Expand Down Expand Up @@ -323,7 +323,7 @@ internal static async Task<StepResult> AutoApproveNodePairing(SetupContext ctx,
var approve = await ctx.Commands.RunInWslAsync(
distro,
$"""{ctx.WslPathPrefix} && {ApprovalRequestHelper.ApprovalCommand(approvalKind)}""",
TimeSpan.FromSeconds(30), approvalEnv, ct);
TimeSpan.FromSeconds(30), approvalEnv, ct, inputViaStdin: true);

ctx.Logger.Info($"Node approve result: exit={approve.ExitCode}");

Expand Down
4 changes: 2 additions & 2 deletions src/OpenClaw.SetupEngine/PairOperatorStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ internal static async Task<StepResult> AutoApprovePairing(SetupContext ctx, stri
var preview = await ctx.Commands.RunInWslAsync(
distro,
$"""{ctx.WslPathPrefix} && openclaw devices approve --latest --json""",
TimeSpan.FromSeconds(30), env, ct);
TimeSpan.FromSeconds(30), env, ct, inputViaStdin: true);

ctx.Logger.Info($"Approve preview: exit={preview.ExitCode}");

Expand All @@ -373,7 +373,7 @@ internal static async Task<StepResult> AutoApprovePairing(SetupContext ctx, stri
var approve = await ctx.Commands.RunInWslAsync(
distro,
$"""{ctx.WslPathPrefix} && {ApprovalRequestHelper.ApprovalCommand(ApprovalRequestKind.Device)}""",
TimeSpan.FromSeconds(30), approvalEnv, ct);
TimeSpan.FromSeconds(30), approvalEnv, ct, inputViaStdin: true);

ctx.Logger.Info($"Approve result: exit={approve.ExitCode}");

Expand Down
5 changes: 4 additions & 1 deletion src/OpenClaw.SetupEngine/SetupWizardRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,14 @@ internal async Task<StepResult> SuspendReloadModeAsync()
{
try
{
// PATH prefix references $PATH. Pipe the script so wsl.exe cannot expand it on argv.
var result = await _ctx.Commands.RunInWslAsync(
_ctx.DistroName!,
$"{_ctx.WslPathPrefix} && openclaw config set gateway.reload.mode off",
TimeSpan.FromSeconds(15),
// This bounded handoff must finish so we know whether restoration is required.
ct: CancellationToken.None);
ct: CancellationToken.None,
inputViaStdin: true);
if (result.ExitCode != 0)
{
return StepResult.Fail(
Expand Down Expand Up @@ -671,6 +673,7 @@ private async Task<CommandResult> RunReloadModeRestorationCommandAsync(string re
if (lastResult is not null && remaining < MinimumReloadRestorationCommandTimeout)
return lastResult;

// PATH prefix references $PATH. Pipe the script so wsl.exe cannot expand it on argv.
var result = await _ctx.Commands.RunInWslAsync(
_ctx.DistroName!,
command,
Expand Down
5 changes: 3 additions & 2 deletions src/OpenClaw.SetupEngine/StartGatewayStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ private static async Task<StepResult> StartOrRestartAndWaitForHealthAsync(
}

var start = await ctx.Commands.RunInWslAsync(
distro, $"{pathCmd} && openclaw gateway {action}", TimeSpan.FromSeconds(30), ct: ct);
distro, $"{pathCmd} && openclaw gateway {action}", TimeSpan.FromSeconds(30), ct: ct, inputViaStdin: true);

if (start.ExitCode != 0)
{
Expand All @@ -113,7 +113,8 @@ await ctx.Commands.RunInWslAsync(
distro,
$"{pathCmd} && openclaw gateway {action}",
TimeSpan.FromSeconds(30),
ct: ct);
ct: ct,
inputViaStdin: true);
if (start.ExitCode != 0)
return StepResult.Fail($"Gateway {action} failed after reset: {start.Stderr}");
}
Expand Down
3 changes: 2 additions & 1 deletion src/OpenClaw.SetupEngine/TailscaleSetupSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,8 @@ public override async Task<StepResult> ExecuteAsync(SetupContext ctx, Cancellati
ctx.DistroName!,
$"{ctx.WslPathPrefix} && openclaw config set {ConfigureGatewayStep.DevicePairPublicUrlKey} {WslShellQuoting.QuotePosixSingleQuote(devicePairPublicUrl)} && openclaw config set {ConfigureGatewayStep.DevicePairEnabledKey} true",
TimeSpan.FromSeconds(45),
ct: ct);
ct: ct,
inputViaStdin: true);
if (configurePairUrl.ExitCode != 0)
return StepResult.Fail($"Could not configure the Tailscale device-pair URL: {configurePairUrl.Stderr}");

Expand Down
10 changes: 5 additions & 5 deletions src/OpenClaw.SetupEngine/VerifyEndToEndStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public override async Task<StepResult> ExecuteAsync(SetupContext ctx, Cancellati
// Verify gateway is still healthy
var distro = ctx.DistroName!;
var status = await ctx.Commands.RunInWslAsync(
distro, $"{ctx.WslPathPrefix} && openclaw gateway status --json", TimeSpan.FromSeconds(15), ct: ct);
distro, $"{ctx.WslPathPrefix} && openclaw gateway status --json", TimeSpan.FromSeconds(15), ct: ct, inputViaStdin: true);

if (status.ExitCode != 0 || !status.Stdout.Contains("running", StringComparison.OrdinalIgnoreCase))
return StepResult.Fail("Gateway is not running");
Expand Down Expand Up @@ -99,7 +99,7 @@ internal static async Task<StepResult> DrainPendingDeviceApprovalsAsync(SetupCon
var preview = await ctx.Commands.RunInWslAsync(
distro,
$"""{pathPrefix} && openclaw devices approve --latest --json""",
TimeSpan.FromSeconds(15), env, ct);
TimeSpan.FromSeconds(15), env, ct, inputViaStdin: true);

if (preview.Stdout.Contains("No pending", StringComparison.OrdinalIgnoreCase) ||
preview.Stderr.Contains("No pending", StringComparison.OrdinalIgnoreCase))
Expand All @@ -115,7 +115,7 @@ internal static async Task<StepResult> DrainPendingDeviceApprovalsAsync(SetupCon
var approve = await ctx.Commands.RunInWslAsync(
distro,
$"""{pathPrefix} && {ApprovalRequestHelper.ApprovalCommand(ApprovalRequestKind.Device)}""",
TimeSpan.FromSeconds(15), approvalEnv, ct);
TimeSpan.FromSeconds(15), approvalEnv, ct, inputViaStdin: true);

if (approve.ExitCode != 0)
return StepResult.Fail($"Device approval drain failed for {parsed.RequestId} (exit {approve.ExitCode}): {approve.Stdout.Trim()} {approve.Stderr.Trim()}".Trim());
Expand Down Expand Up @@ -165,7 +165,7 @@ private static async Task<StepResult> DrainPendingApprovalsAsync(SetupContext ct
var nodeList = await ctx.Commands.RunInWslAsync(
distro,
$"""{pathPrefix} && openclaw nodes list --json""",
TimeSpan.FromSeconds(15), env, ct);
TimeSpan.FromSeconds(15), env, ct, inputViaStdin: true);

var parsed = ApprovalRequestHelper.TryReadPendingRequestIds(nodeList.Stdout.Trim());
if (!parsed.Success)
Expand All @@ -186,7 +186,7 @@ private static async Task<StepResult> DrainPendingApprovalsAsync(SetupContext ct
var approve = await ctx.Commands.RunInWslAsync(
distro,
$"""{pathPrefix} && {ApprovalRequestHelper.ApprovalCommand(ApprovalRequestKind.Node)}""",
TimeSpan.FromSeconds(15), approvalEnv, ct);
TimeSpan.FromSeconds(15), approvalEnv, ct, inputViaStdin: true);

if (approve.ExitCode != 0)
return StepResult.Fail($"Node approval drain failed for {requestId} (exit {approve.ExitCode}): {approve.Stdout.Trim()} {approve.Stderr.Trim()}".Trim());
Expand Down
56 changes: 56 additions & 0 deletions tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5305,6 +5305,62 @@ public void RedactTokens_RedactsEmbeddedTokenOnly()
// lives in KeepaliveProcessManager, not StartKeepaliveStep (see setup-keepalive-process-manager
// in docs/ARCHITECTURE.md).

[Fact]
public async Task WslPathPrefixScripts_UseStdinSoWslExeDoesNotExpandPath()
{
var latestApprovals = 0;
var commands = new FakeCommandRunner(
_ => Ok(),
(_, command, _) =>
{
if (command.Contains("openclaw qr --json", StringComparison.Ordinal))
return Ok("""{"bootstrapToken":"boot-token"}""");
if (command.Contains("devices approve --latest", StringComparison.Ordinal))
{
latestApprovals++;
return latestApprovals == 1
? Ok("""{"selected":{"requestId":"device-req-1"}}""")
: Ok("No pending device approvals");
}
if (command.Contains("nodes list --json", StringComparison.Ordinal))
return Ok("""{"pending":[{"requestId":"node-req-1"}]}""");
if (command.Contains("GATEWAY_CONFIGURED", StringComparison.Ordinal))
return Ok("GATEWAY_CONFIGURED");
if (command.Contains("curl -s", StringComparison.Ordinal))
return Ok("200");
return Ok("""{"requestId":"device-req-1"}""");
});
var ctx = CreateContext(commands: commands);
ctx.DistroName = "test-distro";
ctx.SharedGatewayToken = "shared-token";
ctx.Config.Gateway.ReloadMode = "hybrid";

Assert.True((await new InstallGatewayServiceStep().ExecuteAsync(ctx, CancellationToken.None)).IsSuccess);
await new InstallGatewayServiceStep().RollbackAsync(ctx, CancellationToken.None);
Assert.True((await new MintBootstrapTokenStep().ExecuteAsync(ctx, CancellationToken.None)).IsSuccess, "mint");
Assert.True((await PairOperatorStep.AutoApprovePairing(ctx, CancellationToken.None)).IsSuccess, "operator approve");
Assert.True((await PairNodeStep.AutoApproveNodePairing(ctx, requestId: null, CancellationToken.None)).IsSuccess, "node approve");
Assert.True((await StartGatewayStep.RestartAndWaitForHealthAsync(ctx, CancellationToken.None)).IsSuccess, "restart");
Assert.True((await VerifyEndToEndStep.DrainPendingDeviceApprovalsAsync(ctx, CancellationToken.None)).IsSuccess, "drain");
Assert.True((await new ConfigureGatewayStep().ExecuteAsync(ctx, CancellationToken.None)).IsSuccess, "configure");
TrustManagedEndpoint(ctx);
Assert.True((await new SetupWizardRunner(ctx).SuspendReloadModeAsync()).IsSuccess, "suspend reload");
Assert.True((await new SetupWizardRunner(ctx).RestoreReloadModeAsync()).IsSuccess, "restore reload");

var pathScripts = commands.WslCalls.Where(call => call.Command.Contains("$PATH", StringComparison.Ordinal)).ToList();
Assert.Contains(pathScripts, call => call.Command.Contains("gateway install --force", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("gateway uninstall", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("openclaw qr --json", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("devices approve", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("nodes list --json", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("nodes approve", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("gateway restart", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("GATEWAY_CONFIGURED", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("gateway.reload.mode off", StringComparison.Ordinal));
Assert.Contains(pathScripts, call => call.Command.Contains("gateway.reload.mode 'hybrid'", StringComparison.Ordinal));
Assert.All(pathScripts, call => Assert.True(call.InputViaStdin, call.Command));
}

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