diff --git a/package-lock.json b/package-lock.json index b64336f5..89138e7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,20 +8,20 @@ "name": "openclaw-windows-node-mxc", "version": "0.0.0", "dependencies": { - "@microsoft/mxc-sdk": "^0.1.8" + "@microsoft/mxc-sdk": "^0.6.1" } }, "node_modules/@microsoft/mxc-sdk": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@microsoft/mxc-sdk/-/mxc-sdk-0.1.8.tgz", - "integrity": "sha512-sjywLhMc/eAnBxauw5Fj+7tXJtvoFKpUjD6++g44vPVauy4wJzWHlw8NmIwIuEWlkkyIXEijdTa+hCU+AqtkDQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@microsoft/mxc-sdk/-/mxc-sdk-0.6.1.tgz", + "integrity": "sha512-jpbJU/xfF4qLWcNMplDTUX/q13m2A6vYao1QN3lkZaQlzsRce95H+iU0Qu0wlweJZ2gx6eY1PRQU+/bnQki/dw==", "license": "MIT", "dependencies": { "node-pty": "^1.2.0-beta.12", "semver": "^7.7.4" }, "engines": { - "node": ">=16.0.0" + "node": ">=18.0.0" } }, "node_modules/node-addon-api": { diff --git a/package.json b/package.json index 7646d733..f15e5a60 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/OpenClaw.SetupEngine/CommandRunner.cs b/src/OpenClaw.SetupEngine/CommandRunner.cs index b3818a35..e42545f0 100644 --- a/src/OpenClaw.SetupEngine/CommandRunner.cs +++ b/src/OpenClaw.SetupEngine/CommandRunner.cs @@ -24,7 +24,8 @@ Task RunInWslAsync( TimeSpan timeout, IReadOnlyDictionary? environment = null, CancellationToken ct = default, - string? user = null); + string? user = null, + string? stdinInput = null); } public sealed class CommandRunner : ICommandRunner @@ -145,7 +146,8 @@ public Task RunInWslAsync( TimeSpan timeout, IReadOnlyDictionary? 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", ""); @@ -171,7 +173,7 @@ public Task 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) diff --git a/src/OpenClaw.SetupEngine/SetupPipeline.cs b/src/OpenClaw.SetupEngine/SetupPipeline.cs index b33a132e..45c37216 100644 --- a/src/OpenClaw.SetupEngine/SetupPipeline.cs +++ b/src/OpenClaw.SetupEngine/SetupPipeline.cs @@ -50,6 +50,7 @@ public static List BuildDefaultSteps() new CreateWslInstanceStep(), new ConfigureWslInstanceStep(), new ValidateWslLockdownStep(), + new ImportWindowsCaCertsStep(), new InstallCliStep(), new ConfigureGatewayStep(), new InstallGatewayServiceStep(), diff --git a/src/OpenClaw.SetupEngine/SetupSteps.cs b/src/OpenClaw.SetupEngine/SetupSteps.cs index 458da89e..1121fb58 100644 --- a/src/OpenClaw.SetupEngine/SetupSteps.cs +++ b/src/OpenClaw.SetupEngine/SetupSteps.cs @@ -1082,6 +1082,85 @@ private static void ValidateConfValue(Dictionary +/// 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). +/// +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 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 // ═══════════════════════════════════════════════════════════════════ diff --git a/tests/OpenClaw.SetupEngine.Tests/SetupPipelineTests.cs b/tests/OpenClaw.SetupEngine.Tests/SetupPipelineTests.cs index 0e4debd0..3d28caa4 100644 --- a/tests/OpenClaw.SetupEngine.Tests/SetupPipelineTests.cs +++ b/tests/OpenClaw.SetupEngine.Tests/SetupPipelineTests.cs @@ -60,14 +60,20 @@ public void BuildDefaultSteps_IncludesCurrentSetupFlow() { var steps = SetupStepFactory.BuildDefaultSteps(); - Assert.Equal(18, steps.Count); + Assert.Equal(19, steps.Count); Assert.IsType(steps[0]); Assert.IsType(steps[1]); Assert.IsType(steps[2]); Assert.IsType(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(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] diff --git a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs index e6405949..715962a6 100644 --- a/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs +++ b/tests/OpenClaw.SetupEngine.Tests/SetupStepsTests.cs @@ -1214,7 +1214,8 @@ public Task RunInWslAsync( TimeSpan timeout, IReadOnlyDictionary? 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.");