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
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
"private": true,
"description": "MXC sandbox dependency used by the OpenClaw tray build to copy wxc-exec.exe into the app output.",
"dependencies": {
"@microsoft/mxc-sdk": "^0.1.8"
"@microsoft/mxc-sdk": "^0.6.1"
}
}
8 changes: 5 additions & 3 deletions src/OpenClaw.SetupEngine/CommandRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ Task<CommandResult> RunInWslAsync(
TimeSpan timeout,
IReadOnlyDictionary<string, string>? environment = null,
CancellationToken ct = default,
string? user = null);
string? user = null,
string? stdinInput = null);
}

public sealed class CommandRunner : ICommandRunner
Expand Down Expand Up @@ -145,7 +146,8 @@ public Task<CommandResult> RunInWslAsync(
TimeSpan timeout,
IReadOnlyDictionary<string, string>? environment = null,
CancellationToken ct = default,
string? user = null)
string? user = null,
string? stdinInput = null)
{
// Strip Windows \r to avoid bash "$'\r': command not found" errors
command = command.Replace("\r", "");
Expand All @@ -171,7 +173,7 @@ public Task<CommandResult> RunInWslAsync(
: wslEnvKeys;
}

return RunAsync("wsl.exe", args.ToArray(), timeout, env, ct: ct);
return RunAsync("wsl.exe", args.ToArray(), timeout, env, stdinInput: stdinInput, ct: ct);
}

private static void TryKill(Process process)
Expand Down
1 change: 1 addition & 0 deletions src/OpenClaw.SetupEngine/SetupPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public static List<SetupStep> BuildDefaultSteps()
new CreateWslInstanceStep(),
new ConfigureWslInstanceStep(),
new ValidateWslLockdownStep(),
new ImportWindowsCaCertsStep(),
new InstallCliStep(),
new ConfigureGatewayStep(),
new InstallGatewayServiceStep(),
Expand Down
79 changes: 79 additions & 0 deletions src/OpenClaw.SetupEngine/SetupSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,85 @@ private static void ValidateConfValue(Dictionary<string, Dictionary<string, stri
}
}

// ═══════════════════════════════════════════════════════════════════
// WINDOWS CA CERTIFICATE TRUST STEP
// ═══════════════════════════════════════════════════════════════════

/// <summary>
/// Imports trusted root CA certificates from the Windows certificate store into
/// the WSL distro. This is necessary in corporate environments where HTTPS traffic
/// is inspected by a proxy that uses a self-signed or enterprise-issued root CA,
/// causing curl to fail with exit code 60 (SSL certificate problem: self-signed
/// certificate in certificate chain).
/// </summary>
public sealed class ImportWindowsCaCertsStep : SetupStep
{
public override string Id => "import-windows-ca-certs";
public override string DisplayName => "Import Windows trusted CA certificates";

public override async Task<StepResult> ExecuteAsync(SetupContext ctx, CancellationToken ct)
{
var distro = ctx.DistroName!;

string pemBundle;
try
{
pemBundle = BuildWindowsCaPemBundle();
}
catch (Exception ex)
{
ctx.Logger.Warn($"Could not read Windows certificate store: {ex.Message} — skipping CA import");
return StepResult.Ok("Skipped: could not read Windows certificate store");
}

if (string.IsNullOrWhiteSpace(pemBundle))
{
ctx.Logger.Warn("Windows root CA store returned no certificates — skipping CA import");
return StepResult.Ok("Skipped: no certificates in Windows root store");
}

// Stream the PEM bundle via stdin to avoid exceeding Windows process argument
// size limits that would occur if the full bundle were embedded in the bash -c argument.
const string script = """
set -e
mkdir -p /usr/local/share/ca-certificates
cat > /usr/local/share/ca-certificates/windows-root-ca.crt
update-ca-certificates
echo CA_IMPORT_OK
""";

var result = await ctx.Commands.RunInWslAsync(distro, script, TimeSpan.FromSeconds(60), ct: ct, user: "root", stdinInput: pemBundle);

if (result.ExitCode != 0 || !result.Stdout.Contains("CA_IMPORT_OK", StringComparison.Ordinal))
{
ctx.Logger.Warn($"CA certificate import failed (exit {result.ExitCode}): {result.Stderr.Trim()} — proceeding anyway");
return StepResult.Ok("CA import encountered errors; setup will continue but network calls may fail in corporate environments");
}

ctx.Logger.Info("Windows root CA certificates imported into WSL distro");
return StepResult.Ok("Imported Windows root CA certificates into WSL distro");
}

private static string BuildWindowsCaPemBundle()
{
var sb = new System.Text.StringBuilder();
using var store = new System.Security.Cryptography.X509Certificates.X509Store(
System.Security.Cryptography.X509Certificates.StoreName.Root,
System.Security.Cryptography.X509Certificates.StoreLocation.LocalMachine);
store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadOnly);

foreach (var cert in store.Certificates)
{
sb.AppendLine("-----BEGIN CERTIFICATE-----");
sb.AppendLine(Convert.ToBase64String(cert.RawData, Base64FormattingOptions.InsertLineBreaks));
sb.AppendLine("-----END CERTIFICATE-----");
}

return sb.ToString();
}
}


// ═══════════════════════════════════════════════════════════════════
// GATEWAY INSTALL STEPS
// ═══════════════════════════════════════════════════════════════════
Expand Down
8 changes: 7 additions & 1 deletion tests/OpenClaw.SetupEngine.Tests/SetupPipelineTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,20 @@ public void BuildDefaultSteps_IncludesCurrentSetupFlow()
{
var steps = SetupStepFactory.BuildDefaultSteps();

Assert.Equal(18, steps.Count);
Assert.Equal(19, steps.Count);
Assert.IsType<PreflightOsStep>(steps[0]);
Assert.IsType<PreflightWslStep>(steps[1]);
Assert.IsType<CleanupStaleDistroStep>(steps[2]);
Assert.IsType<CleanupStaleGatewayStep>(steps[3]);
Assert.Contains(steps, s => s is ValidateWslLockdownStep);
Assert.Contains(steps, s => s is ImportWindowsCaCertsStep);
Assert.Contains(steps, s => s is InstallCliStep);
Assert.Contains(steps, s => s is RunGatewayWizardStep);
Assert.IsType<StartKeepaliveStep>(steps[^1]);
// ImportWindowsCaCertsStep must come immediately before InstallCliStep
var caCertsIdx = steps.FindIndex(s => s is ImportWindowsCaCertsStep);
var installCliIdx = steps.FindIndex(s => s is InstallCliStep);
Assert.Equal(installCliIdx - 1, caCertsIdx);
}

[Fact]
Expand Down
3 changes: 2 additions & 1 deletion tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1214,7 +1214,8 @@ public Task<CommandResult> RunInWslAsync(
TimeSpan timeout,
IReadOnlyDictionary<string, string>? environment = null,
CancellationToken ct = default,
string? user = null)
string? user = null,
string? stdinInput = null)
{
if (runInWsl == null)
throw new NotSupportedException("RunInWslAsync is not expected in these tests.");
Expand Down