diff --git a/.agents/skills/build-openshell-mxc-windows/SKILL.md b/.agents/skills/build-openshell-mxc-windows/SKILL.md index a7d30f63f7..c6d8c9d883 100644 --- a/.agents/skills/build-openshell-mxc-windows/SKILL.md +++ b/.agents/skills/build-openshell-mxc-windows/SKILL.md @@ -229,6 +229,14 @@ and build jobs use job-level `continue-on-error: true`; opt-in PR jobs report failures normally. Applying the label alone does not start a run: re-run all jobs in the current mirror push run, or push a new mirrored commit. The binaries are not uploaded or published. +Native x64 and ARM64 jobs run `windows:e2e:mxc:host-probe` separately, then +build release binaries and run `windows:e2e:mxc:ws-agent-mock` and +`windows:e2e:mxc:openclaw-forward-mock` as independent steps. The probe +reports an explicitly absent `wxc-exec`; the other runners use the in-process +mock to verify gateway, CLI, driver, and workload-proof wiring. A pass is not +evidence of WebSocket forwarding, OpenClaw behavior, proxy substitution, or +MXC enforcement. + The ARM64 check/build steps in this x64-host contract are cross-builds. The wrapper discovers and adds host-native LLVM and Ninja to `PATH`, requires the ARM64 compiler and Spectre-mitigated libraries, lets ARM64 crypto crates select @@ -272,6 +280,9 @@ crypto dependency builds. | `windows:test:mxc-real:x64` | Runs the serial, ignored real-`wxc-exec` integration suite natively on x64 through the MSVC wrapper. Rejects non-x64 hosts. | | `windows:test:mxc-real:arm64` | Runs the same real-`wxc-exec` suite natively on ARM64. Rejects non-ARM64 hosts. | | `windows:test:mxc-gb300:arm64` | Runs the required native ARM64 ProcessContainer subset and fails when a test skips. | +| `windows:e2e:mxc:host-probe` | Runs the shipped MXC host probe against an explicitly absent `wxc-exec`. | +| `windows:e2e:mxc:ws-agent-mock` | Runs the shipped WebSocket agent example in wiring-only mode on the native Windows host. | +| `windows:e2e:mxc:openclaw-forward-mock` | Runs the shipped OpenClaw forward example in wiring-only mode on the native Windows host. | | `windows:qualify:mxc:gb300:contract` | Validates the required/optional/unsupported/architecture-constrained GB300 matrix. | | `windows:qualify:mxc:gb300` | Runs the fail-closed GB300 ARM64 gate and validates hash-bound evidence. | | `windows:artifacts` | Reports size and SHA256 for release artifacts that exist. | diff --git a/.github/workflows/windows-msvc.yml b/.github/workflows/windows-msvc.yml index 00c41d9ca9..872f7c8588 100644 --- a/.github/workflows/windows-msvc.yml +++ b/.github/workflows/windows-msvc.yml @@ -98,6 +98,17 @@ jobs: run: mise run --skip-tools windows:lint:${{ matrix.arch }} - name: Test run: mise run --skip-tools windows:test:${{ matrix.arch }} + - name: Probe MXC host capabilities + run: mise run --skip-tools windows:e2e:mxc:host-probe + - name: Build MXC example E2E binaries + id: mxc_build + run: mise run --skip-tools windows:build:${{ matrix.arch }} + - name: Run MXC WebSocket agent example in mock mode + if: ${{ !cancelled() && steps.mxc_build.outcome == 'success' }} + run: mise run --skip-tools windows:e2e:mxc:ws-agent-mock + - name: Run MXC OpenClaw forward example in mock mode + if: ${{ !cancelled() && steps.mxc_build.outcome == 'success' }} + run: mise run --skip-tools windows:e2e:mxc:openclaw-forward-mock - name: sccache stats if: always() run: sccache --show-stats @@ -209,8 +220,17 @@ jobs: cache-targets: "true" cache-on-failure: "true" cache-bin: "false" + - name: Probe MXC host capabilities + run: mise run --skip-tools windows:e2e:mxc:host-probe - name: Build release binaries + id: mxc_build run: mise run --skip-tools windows:build:${{ matrix.arch }} + - name: Run MXC WebSocket agent example in mock mode + if: ${{ !cancelled() && steps.mxc_build.outcome == 'success' }} + run: mise run --skip-tools windows:e2e:mxc:ws-agent-mock + - name: Run MXC OpenClaw forward example in mock mode + if: ${{ !cancelled() && steps.mxc_build.outcome == 'success' }} + run: mise run --skip-tools windows:e2e:mxc:openclaw-forward-mock - name: sccache stats if: always() run: sccache --show-stats diff --git a/architecture/windows.md b/architecture/windows.md index 7a14422302..7e1715f9a8 100644 --- a/architecture/windows.md +++ b/architecture/windows.md @@ -248,6 +248,13 @@ Windows validation separates source correctness from host capability: workspace and unsupported-driver contract tests for x64 and ARM64. - Mock MXC E2E validates gateway, CLI, driver, lifecycle, and policy wiring but is not evidence of OS enforcement. +- Hosted Windows CI runs the shipped MXC host probe, WebSocket example, and + OpenClaw example as separate steps. The probe uses an explicitly absent + `wxc-exec`; the other two use the in-process mock. They check the + unavailable-backend report, sandbox lifecycle, and workload proof files. + The mock disables relay wrapping because it has no control channel. These + runs do not validate WebSocket connectivity, OpenClaw, dynamic forwarding, + proxy behavior, or MXC isolation. - Real-`wxc-exec` tests validate the installed schema and selected filesystem, UI, network, and lifecycle behavior. A probe-gated skip is useful diagnostic output, not qualification evidence. diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md index c4f1bfefdd..0f4957c208 100644 --- a/crates/openshell-driver-mxc/README.md +++ b/crates/openshell-driver-mxc/README.md @@ -244,6 +244,9 @@ is deliberately strict and fails on every required skip. | `windows:test:mxc-gb300:arm64` | Required ARM64 ProcessContainer cases from `tests/wxc_exec_real.rs`; rejects x64 and every required `SKIP` | GB300 qualification only; requires a live backend and all prerequisites | | `windows:e2e:mxc` | `examples/run-mxc-e2e.ps1` — Tier-3 scenario runner, real binary, probe-gated | Demo box / nightly; needs the gateway + CLI binaries in the script directory | | `windows:e2e:mxc:mock` | Same runner with `-Mock` — wiring-only, no real `wxc-exec` needed | Any Windows host (CI, dev machine); validates wiring and the network-reject scenario | +| `windows:e2e:mxc:host-probe` | Runs the shipped host probe with an absent `wxc-exec` | Hosted Windows x64 and ARM64; validates unavailable-backend diagnostics | +| `windows:e2e:mxc:ws-agent-mock` | Runs the shipped WebSocket agent script in mock mode | Hosted Windows x64 and ARM64; validates gateway/CLI/driver lifecycle and a workload proof, not WebSocket forwarding or MXC enforcement | +| `windows:e2e:mxc:openclaw-forward-mock` | Runs the shipped OpenClaw forward script in mock mode | Hosted Windows x64 and ARM64; validates gateway/CLI/driver lifecycle and a workload proof, not OpenClaw forwarding or MXC enforcement | | `windows:qualify:mxc:gb300` | Complete source, host, ARM64 build/test, strict MXC, policy E2E, OpenClaw, and hash-bound evidence contract | Review/release evidence on a native GB300 Windows ARM64 host | The exact required, optional, unsupported, and architecture-constrained GB300 diff --git a/crates/openshell-driver-mxc/examples/README-openclaw-forward.txt b/crates/openshell-driver-mxc/examples/README-openclaw-forward.txt index c3aa149896..09271c358d 100644 --- a/crates/openshell-driver-mxc/examples/README-openclaw-forward.txt +++ b/crates/openshell-driver-mxc/examples/README-openclaw-forward.txt @@ -120,11 +120,15 @@ FILES IN THIS PACKAGE openclaw-gateway.yaml sandbox policy (read-write grant to share_dir only -- see the comment at its top for why) run-openclaw-forward-test.ps1 the orchestrator you run + run-openclaw-forward-mock.ps1 helper used by -Mock for hosted CI wiring install-nodejs-openclaw.ps1 optional prerequisite: fetches Node.js + OpenClaw if you don't already have them README-openclaw-forward.txt this file NOTES + - -Mock runs the gateway, CLI, policy, and an in-policy proof command with + the in-process wxc shim. It needs no Node.js or OpenClaw install and does + not test the relay, WebSocket forwarding, OpenClaw, proxy behavior, or MXC. - The control plane between CLI and gateway runs with --disable-tls on loopback (that's a separate test point, T2). This test's relay traffic (host <-> sandbox) is a separate, unrelated WebSocket tunnel. diff --git a/crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml b/crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml index 8a2aa4e9b8..5f534bc48c 100644 --- a/crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml +++ b/crates/openshell-driver-mxc/examples/mxc-ws-gateway.toml @@ -32,6 +32,9 @@ # openshell-supervisor-relay kills the server directly and exits # driver also kills wxc-exec as a backstop regardless +[openshell] +version = 2 + [openshell.drivers.mxc] # Path to wxc-exec.exe. Patched at runtime by run-ws-agent-test.ps1. # Leave commented for mock-mode smoke tests (pass -Mock to the script). diff --git a/crates/openshell-driver-mxc/examples/run-openclaw-forward-mock.ps1 b/crates/openshell-driver-mxc/examples/run-openclaw-forward-mock.ps1 new file mode 100644 index 0000000000..8fe39677d9 --- /dev/null +++ b/crates/openshell-driver-mxc/examples/run-openclaw-forward-mock.ps1 @@ -0,0 +1,182 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Called by run-openclaw-forward-test.ps1 -Mock. Exercise its shipped gateway +# config and policy with a proof command; the wxc shim has no relay channel. +[CmdletBinding()] +param( + [string] $GatewayPath, + [string] $CliPath, + [Parameter(Mandatory = $true)] [string] $ShareDir, + [int] $Port = 17670, + [string] $SandboxName +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $false + +$here = $PSScriptRoot +$gateway = if ($GatewayPath) { [System.IO.Path]::GetFullPath($GatewayPath) } else { Join-Path $here "openshell-gateway.exe" } +$cli = if ($CliPath) { [System.IO.Path]::GetFullPath($CliPath) } else { Join-Path $here "openshell.exe" } +$ShareDir = [System.IO.Path]::GetFullPath($ShareDir) +if ([string]::IsNullOrWhiteSpace($SandboxName)) { $SandboxName = "oc-mock-$PID" } +$resultDir = Join-Path $here "results-openclaw-forward-mock-$PID" +$proof = Join-Path $ShareDir "mock-workload-pass.txt" +$gatewayProcess = $null +$created = $false +$passed = $false +$failure = "" +$oldConfig = $env:OPENSHELL_GATEWAY_CONFIG +$oldComputeDriver = $env:OPENSHELL_COMPUTE_DRIVER +$oldMock = $env:OPENSHELL_MXC_MOCK_WXC + +function Quote-NativeArgument([string]$value) { + if ($value.Length -gt 0 -and $value -notmatch '[\s"]') { return $value } + $quoted = New-Object System.Text.StringBuilder + [void]$quoted.Append('"') + $slashes = 0 + foreach ($ch in $value.ToCharArray()) { + if ($ch -eq '\') { $slashes++; continue } + if ($ch -eq '"') { + [void]$quoted.Append(('\' * (2 * $slashes + 1))) + [void]$quoted.Append('"') + } else { + if ($slashes -gt 0) { [void]$quoted.Append(('\' * $slashes)) } + [void]$quoted.Append($ch) + } + $slashes = 0 + } + if ($slashes -gt 0) { [void]$quoted.Append(('\' * (2 * $slashes))) } + [void]$quoted.Append('"') + return $quoted.ToString() +} + +function Invoke-Cli([string[]]$CommandArgs, [switch]$AllowFailure) { + $start = New-Object System.Diagnostics.ProcessStartInfo + $start.FileName = $cli + $start.Arguments = ((@("--gateway-endpoint", "http://127.0.0.1:$Port") + $CommandArgs | ForEach-Object { Quote-NativeArgument $_ }) -join ' ') + $start.UseShellExecute = $false + $start.CreateNoWindow = $true + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $start + if (-not $process.Start()) { throw "failed to start OpenShell CLI" } + $stdout = $process.StandardOutput.ReadToEndAsync() + $stderr = $process.StandardError.ReadToEndAsync() + $process.WaitForExit() + $output = (@($stdout.Result, $stderr.Result) | Where-Object { $_ }) -join [Environment]::NewLine + if (-not $AllowFailure -and $process.ExitCode -ne 0) { + throw "openshell $($CommandArgs -join ' ') failed (exit $($process.ExitCode)): $output" + } + return @{ ExitCode = $process.ExitCode; Output = $output } +} + +function Test-Port([int]$Candidate) { + $client = New-Object System.Net.Sockets.TcpClient + try { + $pending = $client.BeginConnect("127.0.0.1", $Candidate, $null, $null) + if (-not $pending.AsyncWaitHandle.WaitOne(250)) { return $false } + $client.EndConnect($pending) + return $true + } catch { return $false } finally { $client.Dispose() } +} + +try { + foreach ($file in @($gateway, $cli, (Join-Path $here "mxc-openclaw-gateway.toml"), + (Join-Path $here "e2e-policies\openclaw-gateway.yaml"))) { + if (-not (Test-Path -LiteralPath $file -PathType Leaf)) { throw "missing mock demo asset: $file" } + } + if (Test-Port $Port) { throw "gateway port $Port is already in use" } + New-Item -ItemType Directory -Force -Path $resultDir, $ShareDir | Out-Null + Remove-Item -LiteralPath $proof -Force -ErrorAction SilentlyContinue + + $shareFwd = $ShareDir.Replace('\', '/') + $toml = Get-Content -LiteralPath (Join-Path $here "mxc-openclaw-gateway.toml") -Raw + $mockWxc = (Join-Path $here "mock-wxc-exec.exe").Replace('\', '\\') + $toml = [regex]::Replace($toml, '(?m)^wxc_exec_path\s*=.*$', "wxc_exec_path = `"$mockWxc`"") + $toml = $toml.Replace('C:/openshell-openclaw', $shareFwd) + $toml = [regex]::Replace($toml, '(?m)^pc_relay_spawner_path\s*=.*$', 'pc_relay_spawner_path = ""') + $tomlUsed = Join-Path $resultDir "mxc-openclaw-gateway.used.toml" + [System.IO.File]::WriteAllText($tomlUsed, $toml, [System.Text.UTF8Encoding]::new($false)) + + $policy = Get-Content -LiteralPath (Join-Path $here "e2e-policies\openclaw-gateway.yaml") -Raw + $policy = $policy.Replace('C:/openshell-openclaw', $shareFwd) + $policyUsed = Join-Path $resultDir "openclaw-gateway.used.yaml" + [System.IO.File]::WriteAllText($policyUsed, $policy, [System.Text.UTF8Encoding]::new($false)) + + $env:OPENSHELL_GATEWAY_CONFIG = $tomlUsed + $env:OPENSHELL_COMPUTE_DRIVER = "mxc" + $env:OPENSHELL_MXC_MOCK_WXC = "1" + $gwLog = Join-Path $resultDir "gateway.log" + $gwErrLog = Join-Path $resultDir "gateway.err.log" + $gatewayProcess = Start-Process -FilePath $gateway -ArgumentList @( + "--disable-tls", "--db-url", "sqlite::memory:", "--port", "$Port", "--log-level", "info" + ) -WorkingDirectory $here -PassThru -WindowStyle Hidden -RedirectStandardOutput $gwLog -RedirectStandardError $gwErrLog + $deadline = (Get-Date).AddSeconds(30) + while ((Get-Date) -lt $deadline -and -not (Test-Port $Port)) { + if ($gatewayProcess.HasExited) { throw "gateway exited before listening" } + Start-Sleep -Milliseconds 250 + } + if (-not (Test-Port $Port)) { throw "gateway did not listen within 30 seconds" } + + $cmd = Join-Path $env:SystemRoot "System32\cmd.exe" + $driverConfig = @{ mxc = @{ + command = @($cmd, "/d", "/s", "/c", "echo PASS 1> `"$proof`"") + cwd = $shareFwd + } } | ConvertTo-Json -Compress -Depth 5 + $create = Invoke-Cli @("sandbox", "create", "--name", $SandboxName, + "--policy", $policyUsed, "--driver-config-json", $driverConfig, + "--no-tty", "--output", "json") -AllowFailure + $created = $true + $deadline = (Get-Date).AddSeconds(30) + $proofText = "" + while ((Get-Date) -lt $deadline) { + if (Test-Path -LiteralPath $proof) { + $proofText = Get-Content -LiteralPath $proof -Raw + if ($null -ne $proofText -and $proofText.Trim() -eq "PASS") { break } + } + Start-Sleep -Milliseconds 250 + } + if ($null -eq $proofText -or $proofText.Trim() -ne "PASS") { + throw "mock workload did not finish proof (create exit $($create.ExitCode)): $($create.Output)" + } + $get = Invoke-Cli @("sandbox", "get", $SandboxName, "--output", "json") + $expectedNamePattern = '"name"\s*:\s*"' + [regex]::Escape($SandboxName) + '"' + if ($get.Output -notmatch $expectedNamePattern) { + throw "created sandbox was not returned by the CLI" + } + $delete = Invoke-Cli @("sandbox", "delete", $SandboxName) + $created = $false + if ($delete.ExitCode -ne 0) { throw "sandbox deletion failed" } + $passed = $true + Write-Host "OpenClaw example mock wiring passed for $SandboxName" +} catch { + $failure = $_.Exception.Message + Write-Host "OpenClaw example mock wiring failed: $failure ($($_.ScriptStackTrace))" -ForegroundColor Red +} finally { + if ($created) { + try { [void](Invoke-Cli @("sandbox", "delete", $SandboxName) -AllowFailure) } catch {} + } + if ($gatewayProcess -and -not $gatewayProcess.HasExited) { + Stop-Process -Id $gatewayProcess.Id -Force -ErrorAction SilentlyContinue + try { [void]$gatewayProcess.WaitForExit(5000) } catch {} + } + $env:OPENSHELL_GATEWAY_CONFIG = $oldConfig + $env:OPENSHELL_COMPUTE_DRIVER = $oldComputeDriver + if ($null -eq $oldMock) { + Remove-Item Env:OPENSHELL_MXC_MOCK_WXC -ErrorAction SilentlyContinue + } else { + $env:OPENSHELL_MXC_MOCK_WXC = $oldMock + } + $verdict = if ($passed) { "PASS" } else { "FAIL" } + $summary = "verdict=$verdict`r`nmode=mock-wiring`r`nsandbox=$SandboxName`r`nresult=$failure`r`n" + + "Mock mode validates the shipped config, policy, gateway, CLI, driver, and proof command. " + + "It does not validate OpenClaw, WebSocket forwarding, proxy behavior, or MXC enforcement.`r`n" + [System.IO.File]::WriteAllText((Join-Path $resultDir "summary.txt"), $summary, [System.Text.UTF8Encoding]::new($false)) + Write-Host $summary + Write-Host "Results: $resultDir" +} + +if ($passed) { exit 0 } else { exit 1 } diff --git a/crates/openshell-driver-mxc/examples/run-openclaw-forward-test.ps1 b/crates/openshell-driver-mxc/examples/run-openclaw-forward-test.ps1 index 7c3a7c6e80..708483cdce 100644 --- a/crates/openshell-driver-mxc/examples/run-openclaw-forward-test.ps1 +++ b/crates/openshell-driver-mxc/examples/run-openclaw-forward-test.ps1 @@ -48,13 +48,13 @@ param( [string] $WxcExecPath = "C:\mxc-kit\bin\wxc-exec.exe", # Your existing Node.js binary. Copied (not run in place) into share_dir -- # the AppContainer cannot read paths outside it. - [Parameter(Mandatory = $true)] [string] $NodeExePath, # Root directory of your OpenClaw npm package install -- the directory that # directly contains openclaw.mjs and its own node_modules. Copied # (recursively, via robocopy) into share_dir\runtime\node_modules\openclaw. - [Parameter(Mandatory = $true)] [string] $OpenClawInstallDir, + [string] $GatewayPath, + [string] $CliPath, # Must be a DIRECT CHILD of a drive root (e.g. C:\openshell-openclaw, not # C:\work\openshell-openclaw). The staged Node invocation below uses # --preserve-symlinks-main so Node does not realpath the main module before @@ -92,7 +92,10 @@ param( # this switch was meant to test around. Left in for whoever investigates # next (a different wxc-exec build may behave differently), but don't # expect it to work today. - [switch] $UseLocalNetwork + [switch] $UseLocalNetwork, + # CI wiring mode uses the in-process wxc shim and an in-policy proof command. + # It does not launch OpenClaw or exercise forwarding or OS enforcement. + [switch] $Mock ) $ErrorActionPreference = "Stop" @@ -106,6 +109,23 @@ $OutputEncoding = [System.Text.Encoding]::UTF8 $here = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path } +if ($Mock) { + if ($Backend -ne "process_container" -or $UseLocalNetwork) { + throw "-Mock supports only the default process_container configuration" + } + if (-not $PSBoundParameters.ContainsKey("ShareDir")) { + $ShareDir = Join-Path ([System.IO.Path]::GetTempPath()) "openshell-openclaw-mock-$PID" + } + & (Join-Path $here "run-openclaw-forward-mock.ps1") ` + -GatewayPath $GatewayPath -CliPath $CliPath -ShareDir $ShareDir ` + -Port $Port -SandboxName $SandboxName + exit $LASTEXITCODE +} + +if ([string]::IsNullOrWhiteSpace($NodeExePath) -or [string]::IsNullOrWhiteSpace($OpenClawInstallDir)) { + throw "-NodeExePath and -OpenClawInstallDir are required for real MXC runs" +} + if ([string]::IsNullOrWhiteSpace($SandboxName)) { $SandboxName = "openclaw-$PID" } @@ -179,8 +199,8 @@ function Show-SandboxCreate([object]$out, [string]$name) { } } -$gateway = Join-Path $here "openshell-gateway.exe" -$cli = Join-Path $here "openshell.exe" +$gateway = if ($GatewayPath) { [System.IO.Path]::GetFullPath($GatewayPath) } else { Join-Path $here "openshell-gateway.exe" } +$cli = if ($CliPath) { [System.IO.Path]::GetFullPath($CliPath) } else { Join-Path $here "openshell.exe" } $relayExe = Join-Path $here "openshell-supervisor-relay.exe" $policy = Join-Path $here "e2e-policies\openclaw-gateway.yaml" if ($UseLocalNetwork -and $Backend -eq "isolation_session") { diff --git a/crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1 b/crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1 index 0372864928..1094ea71e0 100644 --- a/crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1 +++ b/crates/openshell-driver-mxc/examples/run-ws-agent-test.ps1 @@ -25,9 +25,9 @@ # freed. # 6. Verify port 22000 is freed within the drain timeout. # -# In -Mock mode: steps 3-4 and 6 are skipped because wxc-exec is not invoked -# and the server never starts. The test validates gateway startup, sandbox -# create, and sandbox delete only. +# In -Mock mode: the disposable policy permits a proof write and the runner +# verifies it. WebSocket connectivity, relay forwarding, and OS isolation are +# not exercised because wxc-exec is not invoked. # # PowerShell 5.1-compatible (no && / || / ternary operators). ASCII only. # @@ -49,6 +49,10 @@ param( # Path to wxc-exec.exe. Required for real runs; ignored in mock mode. [string] $WxcExecPath = "", + # Optional paths to CI-built binaries; defaults to binaries beside this script. + [string] $GatewayPath, + [string] $CliPath, + # Working directory the AppContainer can read/write. mxc-ws-agent.exe is # expected alongside this script; # the script copies it here if needed. @@ -106,6 +110,44 @@ function Ok([string]$m) { Write-Host "[OK] $m" -ForegroundColor Green } function Bad([string]$m) { Write-Host "[FAIL] $m" -ForegroundColor Red } function Warn([string]$m) { Write-Host "[WARN] $m" -ForegroundColor Yellow } +function Quote-NativeArgument([string]$value) { + if ($value.Length -gt 0 -and $value -notmatch '[\s"]') { return $value } + $quoted = New-Object System.Text.StringBuilder + [void]$quoted.Append('"') + $slashes = 0 + foreach ($ch in $value.ToCharArray()) { + if ($ch -eq '\') { $slashes++; continue } + if ($ch -eq '"') { + [void]$quoted.Append(('\' * (2 * $slashes + 1))) + [void]$quoted.Append('"') + } else { + if ($slashes -gt 0) { [void]$quoted.Append(('\' * $slashes)) } + [void]$quoted.Append($ch) + } + $slashes = 0 + } + if ($slashes -gt 0) { [void]$quoted.Append(('\' * (2 * $slashes))) } + [void]$quoted.Append('"') + return $quoted.ToString() +} + +function Invoke-Cli([string[]]$CommandArgs) { + $start = New-Object System.Diagnostics.ProcessStartInfo + $start.FileName = $cli + $start.Arguments = (($CommandArgs | ForEach-Object { Quote-NativeArgument $_ }) -join ' ') + $start.UseShellExecute = $false + $start.CreateNoWindow = $true + $start.RedirectStandardOutput = $true + $start.RedirectStandardError = $true + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $start + if (-not $process.Start()) { throw "failed to start OpenShell CLI" } + $stdout = $process.StandardOutput.ReadToEndAsync() + $stderr = $process.StandardError.ReadToEndAsync() + $process.WaitForExit() + return @{ ExitCode = $process.ExitCode; Text = ((@($stdout.Result, $stderr.Result) | Where-Object { $_ }) -join [Environment]::NewLine).Trim() } +} + # Escape backslashes for TOML basic strings. function Esc([string]$p) { return $p.Replace('\', '\\') } @@ -125,8 +167,8 @@ if ($WsPort -ne 22000) { # --- Path variables ----------------------------------------------------------- -$gateway = Join-Path $here "openshell-gateway.exe" -$cli = Join-Path $here "openshell.exe" +$gateway = if ($GatewayPath) { [System.IO.Path]::GetFullPath($GatewayPath) } else { Join-Path $here "openshell-gateway.exe" } +$cli = if ($CliPath) { [System.IO.Path]::GetFullPath($CliPath) } else { Join-Path $here "openshell.exe" } $tomlSrc = Join-Path $here "mxc-ws-gateway.toml" $toml = Join-Path $resultDir "mxc-ws-gateway.toml" $policyFile = Join-Path $here "e2e-policies\ws-agent.yaml" @@ -134,6 +176,7 @@ $policyUsed = Join-Path $resultDir "ws-agent.yaml" $agentExeSrc = Join-Path $here "mxc-ws-agent.exe" $agentExe = Join-Path $AgentDir "mxc-ws-agent.exe" +$mockProof = Join-Path $AgentDir "mock-workload-pass.txt" # openshell-supervisor-relay.exe wraps the per-sandbox command (see mxc-ws-gateway.toml's # pc_relay_spawner_path) so the driver has a control channel into the sandbox, @@ -156,7 +199,7 @@ $sandboxName = "mxc-ws-$runId" function Start-Gw { Remove-Item $gwLog, $gwErrLog -Force -ErrorAction SilentlyContinue $env:OPENSHELL_GATEWAY_CONFIG = $toml - $env:OPENSHELL_DRIVERS = "mxc" + $env:OPENSHELL_COMPUTE_DRIVER = "mxc" $env:OPENSHELL_MXC_SHARE_DIR = $AgentDir $p = Start-Process -FilePath $gateway ` -ArgumentList @("--disable-tls", "--db-url", "sqlite::memory:", "--log-level", "info", "--port", $Port) ` @@ -303,6 +346,12 @@ function Render-Toml { $t = [regex]::Replace($t, '(?m)^\s*#?\s*pc_relay_spawner_path\s*=.*$', "pc_relay_spawner_path = `"$relayExeFwd`"") + if ($Mock) { + # The wxc shim has no control-channel handshake. Run the proof command + # directly while still parsing the shipped MXC settings. + $t = [regex]::Replace($t, '(?m)^\s*pc_relay_spawner_path\s*=.*$', 'pc_relay_spawner_path = ""') + } + Set-Content $toml -Value $t -Encoding UTF8 } @@ -322,6 +371,12 @@ function Render-Policy { if ($agentDirPolicy -ne $defaultAgentDirPolicy) { $p = $p.Replace($defaultAgentDirPolicy, $agentDirPolicy) } + if ($Mock) { + # The proof is a host file; this disposable mock policy grants only + # that directory so the shim can materialize it. + $p = [regex]::Replace($p, '(?m)^ read_only:\r?\n - "[^"]+"\s*$', ' read_only: []') + $p = $p.Replace('read_write: []', "read_write: [`"$agentDirPolicy`"]") + } Set-Content $policyUsed -Value $p -Encoding UTF8 } @@ -385,7 +440,10 @@ try { Remove-Item (Join-Path $AgentDir "appcontainer-sid.txt") -Force -ErrorAction SilentlyContinue Remove-Item (Join-Path $AgentDir "outbound-probe.txt") -Force -ErrorAction SilentlyContinue Remove-Item (Join-Path $AgentDir "outbound-probe-addr.txt") -Force -ErrorAction SilentlyContinue - if (Test-Path $agentExeSrc) { + if ($Mock) { + Remove-Item -LiteralPath $mockProof -Force -ErrorAction SilentlyContinue + Info "mock workload will write $mockProof; WS server binary is not needed" + } elseif (Test-Path $agentExeSrc) { try { Copy-Item $agentExeSrc $agentExe -Force Ok "mxc-ws-agent.exe copied from release build" @@ -408,7 +466,9 @@ try { Info "mxc-ws-agent.exe already in $AgentDir (using existing)" } - if (Test-Path $relayExeSrc) { + if ($Mock) { + Info "mock mode does not launch openshell-supervisor-relay.exe" + } elseif (Test-Path $relayExeSrc) { try { Copy-Item $relayExeSrc $relayExe -Force Ok "openshell-supervisor-relay.exe copied from release build" @@ -437,17 +497,19 @@ try { } Ok "gateway port $Port is free" - $busyWs = Get-NetTCPConnection -State Listen -LocalPort $WsPort -ErrorAction SilentlyContinue - if ($busyWs) { - throw "WebSocket port $WsPort already in use (pid $($busyWs.OwningProcess)). Free it before running this test." - } - Ok "WebSocket port $WsPort is free" + if (-not $Mock) { + $busyWs = Get-NetTCPConnection -State Listen -LocalPort $WsPort -ErrorAction SilentlyContinue + if ($busyWs) { + throw "WebSocket port $WsPort already in use (pid $($busyWs.OwningProcess)). Free it before running this test." + } + Ok "WebSocket port $WsPort is free" - $busyRelay = Get-NetTCPConnection -State Listen -LocalPort $RelayPort -ErrorAction SilentlyContinue - if ($busyRelay) { - throw "relay port $RelayPort already in use (pid $($busyRelay.OwningProcess)). Free it before running this test." + $busyRelay = Get-NetTCPConnection -State Listen -LocalPort $RelayPort -ErrorAction SilentlyContinue + if ($busyRelay) { + throw "relay port $RelayPort already in use (pid $($busyRelay.OwningProcess)). Free it before running this test." + } + Ok "relay port $RelayPort is free" } - Ok "relay port $RelayPort is free" # --- Render TOML + start gateway ------------------------------------------ @@ -473,26 +535,25 @@ try { Step "Create sandbox '$sandboxName'" $createOut = $null; $createExitCode = 0 + $workloadCommand = if ($Mock) { + @((Join-Path $env:SystemRoot "System32\cmd.exe"), "/d", "/s", "/c", "echo PASS 1> `"$mockProof`"") + } else { + @((Fwd $agentExe), "server") + } $driverConfigJson = @{ mxc = @{ - command = @((Fwd $agentExe), "server") + command = $workloadCommand cwd = (Fwd $AgentDir) } } | ConvertTo-Json -Compress -Depth 4 try { - # MXC exec-in-driver has no SSH server, so any `sandbox create` invocation - # that attempts SSH will fail with connection-refused and exit non-zero. - # Use the same pattern as run-mxc-e2e.ps1: pass --no-tty with a no-op - # command so the CLI fires the SSH attempt, fails quickly (connection - # refused), and returns. Do NOT gate on exit code here. - $createOut = & $cli sandbox create ` - --name $sandboxName ` - --policy $policyUsed ` - --driver-config-json $driverConfigJson ` - --no-tty ` - -- cmd.exe /c exit 0 ` - 2>&1 - $createExitCode = $LASTEXITCODE + # ProcessStartInfo preserves JSON quoting under Windows PowerShell 5.1. + # MXC has no SSH server, so the CLI can still exit non-zero after create. + $create = Invoke-Cli @("sandbox", "create", "--name", $sandboxName, + "--policy", $policyUsed, "--driver-config-json", $driverConfigJson, + "--no-tty", "--", "cmd.exe", "/c", "exit", "0") + $createOut = $create.Text + $createExitCode = $create.ExitCode } catch { $createOut = $_.Exception.Message; $createExitCode = 1 } @@ -503,8 +564,9 @@ try { Start-Sleep -Milliseconds 500 $getOut = $null; $getExitCode = 0 try { - $getOut = & $cli sandbox get $sandboxName 2>&1 - $getExitCode = $LASTEXITCODE + $get = Invoke-Cli @("sandbox", "get", $sandboxName) + $getOut = $get.Text + $getExitCode = $get.ExitCode } catch { $getOut = $_.Exception.Message; $getExitCode = 1 } @@ -521,6 +583,20 @@ try { throw "sandbox create failed: sandbox does not exist or is not Ready after create" } + if ($Mock) { + $proofDeadline = (Get-Date).AddSeconds(15) + $proofText = "" + while ((Get-Date) -lt $proofDeadline) { + if (Test-Path -LiteralPath $mockProof) { + $proofText = Get-Content -LiteralPath $mockProof -Raw + if ($null -ne $proofText -and $proofText.Trim() -eq "PASS") { break } + } + Start-Sleep -Milliseconds 200 + } + $proofOk = $null -ne $proofText -and $proofText.Trim() -eq "PASS" + Record "mock-workload" $proofOk "in-policy proof command completed in the shared directory" + } + # --- WebSocket connectivity (real mode only) ------------------------------ if (-not $Mock) { @@ -623,8 +699,9 @@ try { Step "Delete sandbox '$sandboxName'" $deleteOut = $null; $deleteExitCode = 0 try { - $deleteOut = & $cli sandbox delete $sandboxName 2>&1 - $deleteExitCode = $LASTEXITCODE + $delete = Invoke-Cli @("sandbox", "delete", $sandboxName) + $deleteOut = $delete.Text + $deleteExitCode = $delete.ExitCode } catch { $deleteOut = $_.Exception.Message; $deleteExitCode = 1 } @@ -681,10 +758,16 @@ try { $verdict = if ($harnessError -or $failCount -gt 0) { "FAIL" } else { "PASS" } $checkLines = ($checks | ForEach-Object { " " + $_.Result + " " + $_.Check + ": " + $_.Detail }) -join "`n" - $modeStr = if ($Mock) { "MOCK (no wxc-exec, no WS connectivity)" } else { "REAL" } + $modeStr = if ($Mock) { "mock-wiring" } else { "real-mxc" } $wxcStr = if ($Mock) { "(mock)" } else { $WxcExecPath } $errStr = if ($harnessError) { "harness_error: $harnessError" } else { "" } + $agentDescription = if ($Mock) { "(not launched in mock mode)" } else { $agentExe } + $relayDescription = if ($Mock) { "(not launched in mock mode)" } else { $relayExe } + $wsDescription = if ($Mock) { "(not exercised in mock mode)" } else { "$WsPort" } + $relayPortDescription = if ($Mock) { "(not exercised in mock mode)" } else { "$RelayPort (on this host)" } + $messageDescription = if ($Mock) { "(not sent in mock mode)" } else { $WsMessage } + $forwardArtifactLine = if ($Mock) { "" } else { " forward.log / forward.err.log 'openshell forward service' stdout / stderr`n" } $summary = "OpenShell MXC WebSocket agent test`n" + "====================================`n" + "timestamp : $stamp`n" + @@ -693,13 +776,13 @@ try { "mode : $modeStr`n" + "gateway : $gateway (port $Port)`n" + "agent_dir : $AgentDir`n" + - "agent_exe : $agentExe`n" + - "relay_exe : $relayExe`n" + + "agent_exe : $agentDescription`n" + + "relay_exe : $relayDescription`n" + "policy : $policyUsed`n" + "sandbox : $sandboxName`n" + - "ws_port : $WsPort`n" + - "relay_port : $RelayPort (on this host)`n" + - "ws_message : $WsMessage`n" + + "ws_port : $wsDescription`n" + + "relay_port : $relayPortDescription`n" + + "ws_message : $messageDescription`n" + "wxc_exec : $wxcStr`n" + "totals : PASS=$passCount FAIL=$failCount`n" + "$errStr`n" + @@ -707,18 +790,24 @@ try { "`nFiles in this bundle ($resultDir):`n" + " transcript.txt full console transcript`n" + " gateway.log / gateway.err.log gateway stdout / stderr`n" + - " forward.log / forward.err.log 'openshell forward service' stdout / stderr`n" + + $forwardArtifactLine + " mxc-ws-gateway.rendered.toml exact gateway config used`n" + " ws-agent.yaml sandbox policy used`n" + "`nWhat PASS means:`n" + " gateway-start gateway bound port $Port within 30 s`n" + - " sandbox-create sandbox reached Ready after create (CLI exit may be non-zero on MXC without SSH)`n" + - " server-port-open WS server bound port $WsPort within 30 s`n" + - " ws-server-marker spawner logged its own port-ready confirmation in the gateway log`n" + - " ws-echo '$WsMessage' echoed via a dynamic 'openshell forward service' relay at 127.0.0.1:$RelayPort`n" + - " (fresh, on-demand relay for this one call -- no static bridge, nothing pre-declared)`n" + - " sandbox-delete CLI returned exit 0 for sandbox delete`n" + - " port-freed port $WsPort released within 30 s of sandbox delete`n" + " sandbox-create sandbox reached Ready after create (CLI exit may be non-zero on MXC without SSH)`n" + if ($Mock) { + $summary += " mock-workload an in-policy proof command wrote its file`n" + + " sandbox-delete CLI returned exit 0 for sandbox delete`n" + + "Mock mode does not validate WebSocket connectivity, dynamic forwarding, or MXC enforcement.`n" + } else { + $summary += " server-port-open WS server bound port $WsPort within 30 s`n" + + " ws-server-marker spawner logged its own port-ready confirmation in the gateway log`n" + + " ws-echo '$WsMessage' echoed via a dynamic 'openshell forward service' relay at 127.0.0.1:$RelayPort`n" + + " (fresh, on-demand relay for this one call -- no static bridge, nothing pre-declared)`n" + + " sandbox-delete CLI returned exit 0 for sandbox delete`n" + + " port-freed port $WsPort released within 30 s of sandbox delete`n" + } Set-Content (Join-Path $resultDir "summary.txt") -Value $summary -Encoding UTF8 $color = if ($verdict -eq "PASS") { "Green" } else { "Red" } diff --git a/tasks/scripts/windows-mxc-host-probe-e2e.ps1 b/tasks/scripts/windows-mxc-host-probe-e2e.ps1 new file mode 100644 index 0000000000..4013546d29 --- /dev/null +++ b/tasks/scripts/windows-mxc-host-probe-e2e.ps1 @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Run the shipped host probe against an explicitly absent wxc-exec. This +# verifies the hosted-runner diagnostic path, not real MXC availability. +[CmdletBinding()] +param( + [string] $ArtifactRoot, + [switch] $KeepArtifacts +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +if (-not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + throw "windows-mxc-host-probe-e2e.ps1 requires Windows" +} + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$examplesRoot = Join-Path $repoRoot "crates\openshell-driver-mxc\examples" +if (-not $ArtifactRoot) { + $ArtifactRoot = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [System.IO.Path]::GetTempPath() } +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) +$stageDir = [System.IO.Path]::GetFullPath((Join-Path $ArtifactRoot "openshell-mxc-host-probe-$PID")) +$expectedPrefix = $ArtifactRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar +if (-not $stageDir.StartsWith($expectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "staging directory is outside artifact root: $stageDir" +} +New-Item -ItemType Directory -Path $stageDir | Out-Null +$passed = $false + +try { + $probe = Join-Path $stageDir "probe-mxc-host.ps1" + Copy-Item -LiteralPath (Join-Path $examplesRoot "probe-mxc-host.ps1") -Destination $probe + $report = Join-Path $stageDir "capabilities.json" + $absentWxc = Join-Path $stageDir "absent-wxc-exec.exe" + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $probe -WxcExecPath $absentWxc -OutFile $report + if ($LASTEXITCODE -ne 0) { throw "MXC host probe failed (exit $LASTEXITCODE)" } + + $capabilities = Get-Content -LiteralPath $report -Raw | ConvertFrom-Json + if ($capabilities.wxcExec.exists -ne $false -or + $capabilities.processcontainerTrial.result -ne "absent" -or + $capabilities.isolationSessionTrial.result -ne "absent" -or + $capabilities.host.osBuildNumber -le 0) { + throw "host probe did not report the expected absent-backend diagnostic" + } + $passed = $true + Write-Host "MXC host probe CI passed" +} finally { + if ($passed -and -not $KeepArtifacts -and (Test-Path -LiteralPath $stageDir)) { + Remove-Item -LiteralPath $stageDir -Recurse -Force + } else { + Write-Host "MXC host probe artifacts: $stageDir" + } +} diff --git a/tasks/scripts/windows-mxc-openclaw-forward-mock-e2e.ps1 b/tasks/scripts/windows-mxc-openclaw-forward-mock-e2e.ps1 new file mode 100644 index 0000000000..f3c62961f8 --- /dev/null +++ b/tasks/scripts/windows-mxc-openclaw-forward-mock-e2e.ps1 @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Run the shipped OpenClaw forward example with the in-process wxc shim and +# an in-policy proof command. This does not launch OpenClaw or a relay. +[CmdletBinding()] +param( + [string] $GatewayPath, + [string] $CliPath, + [string] $ArtifactRoot, + [switch] $KeepArtifacts +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +if (-not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + throw "windows-mxc-openclaw-forward-mock-e2e.ps1 requires Windows" +} +$target = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()) { + "X64" { "x86_64-pc-windows-msvc" } + "Arm64" { "aarch64-pc-windows-msvc" } + default { throw "unsupported native Windows architecture: $($_)" } +} +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$examplesRoot = Join-Path $repoRoot "crates\openshell-driver-mxc\examples" +$targetDir = if ($env:CARGO_TARGET_DIR) { [System.IO.Path]::GetFullPath($env:CARGO_TARGET_DIR) } else { Join-Path $repoRoot "target" } +if (-not $GatewayPath) { $GatewayPath = Join-Path $targetDir "$target\release\openshell-gateway.exe" } +if (-not $CliPath) { $CliPath = Join-Path $targetDir "$target\release\openshell.exe" } +$GatewayPath = [System.IO.Path]::GetFullPath($GatewayPath) +$CliPath = [System.IO.Path]::GetFullPath($CliPath) +foreach ($binary in @($GatewayPath, $CliPath)) { + if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { throw "required release binary is missing: $binary" } +} + +if (-not $ArtifactRoot) { + $ArtifactRoot = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [System.IO.Path]::GetTempPath() } +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) +$stageDir = [System.IO.Path]::GetFullPath((Join-Path $ArtifactRoot "openshell-mxc-openclaw-forward-$target-$PID")) +$expectedPrefix = $ArtifactRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar +if (-not $stageDir.StartsWith($expectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "staging directory is outside artifact root: $stageDir" +} +New-Item -ItemType Directory -Path $stageDir | Out-Null +$passed = $false +$oldAppData = $env:APPDATA +$oldLocalAppData = $env:LOCALAPPDATA + +function Get-AvailablePort { + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + try { + $listener.Start() + return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port + } finally { $listener.Stop() } +} + +try { + $env:APPDATA = Join-Path $stageDir "appdata" + $env:LOCALAPPDATA = Join-Path $stageDir "localappdata" + New-Item -ItemType Directory -Force -Path $env:APPDATA, $env:LOCALAPPDATA | Out-Null + + New-Item -ItemType Directory -Path (Join-Path $stageDir "e2e-policies") | Out-Null + foreach ($fixture in @("run-openclaw-forward-test.ps1", "run-openclaw-forward-mock.ps1", "mxc-openclaw-gateway.toml")) { + Copy-Item -LiteralPath (Join-Path $examplesRoot $fixture) -Destination $stageDir + } + Copy-Item -LiteralPath (Join-Path $examplesRoot "e2e-policies\openclaw-gateway.yaml") -Destination (Join-Path $stageDir "e2e-policies") + + $port = Get-AvailablePort + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $stageDir "run-openclaw-forward-test.ps1") ` + -Mock -GatewayPath $GatewayPath -CliPath $CliPath -ShareDir (Join-Path $stageDir "share") -Port $port + if ($LASTEXITCODE -ne 0) { throw "MXC OpenClaw forward example failed (exit $LASTEXITCODE)" } + + $result = Get-ChildItem -LiteralPath $stageDir -Directory -Filter "results-openclaw-forward-mock-*" | + Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1 + if ($null -eq $result) { throw "OpenClaw example produced no result bundle" } + $summary = Get-Content -LiteralPath (Join-Path $result.FullName "summary.txt") -Raw + if ($summary -notmatch '(?m)^verdict=PASS\s*$' -or + $summary -notmatch '(?m)^mode=mock-wiring\s*$') { + throw "OpenClaw example did not report a mock-wiring PASS" + } + + $passed = $true + Write-Host "MXC OpenClaw forward example mock CI passed for $target" +} finally { + $env:APPDATA = $oldAppData + $env:LOCALAPPDATA = $oldLocalAppData + if ($passed -and -not $KeepArtifacts -and (Test-Path -LiteralPath $stageDir)) { + Remove-Item -LiteralPath $stageDir -Recurse -Force + } else { + Write-Host "MXC OpenClaw forward example artifacts: $stageDir" + } +} diff --git a/tasks/scripts/windows-mxc-ws-agent-mock-e2e.ps1 b/tasks/scripts/windows-mxc-ws-agent-mock-e2e.ps1 new file mode 100644 index 0000000000..8398d941bf --- /dev/null +++ b/tasks/scripts/windows-mxc-ws-agent-mock-e2e.ps1 @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Run the shipped WebSocket example with the in-process wxc shim. The proof +# command checks gateway/CLI/driver wiring; no WebSocket server is started. +[CmdletBinding()] +param( + [string] $GatewayPath, + [string] $CliPath, + [string] $ArtifactRoot, + [switch] $KeepArtifacts +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +if (-not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + throw "windows-mxc-ws-agent-mock-e2e.ps1 requires Windows" +} +$target = switch ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()) { + "X64" { "x86_64-pc-windows-msvc" } + "Arm64" { "aarch64-pc-windows-msvc" } + default { throw "unsupported native Windows architecture: $($_)" } +} +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$examplesRoot = Join-Path $repoRoot "crates\openshell-driver-mxc\examples" +$targetDir = if ($env:CARGO_TARGET_DIR) { [System.IO.Path]::GetFullPath($env:CARGO_TARGET_DIR) } else { Join-Path $repoRoot "target" } +if (-not $GatewayPath) { $GatewayPath = Join-Path $targetDir "$target\release\openshell-gateway.exe" } +if (-not $CliPath) { $CliPath = Join-Path $targetDir "$target\release\openshell.exe" } +$GatewayPath = [System.IO.Path]::GetFullPath($GatewayPath) +$CliPath = [System.IO.Path]::GetFullPath($CliPath) +foreach ($binary in @($GatewayPath, $CliPath)) { + if (-not (Test-Path -LiteralPath $binary -PathType Leaf)) { throw "required release binary is missing: $binary" } +} + +if (-not $ArtifactRoot) { + $ArtifactRoot = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [System.IO.Path]::GetTempPath() } +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) +$stageDir = [System.IO.Path]::GetFullPath((Join-Path $ArtifactRoot "openshell-mxc-ws-agent-$target-$PID")) +$expectedPrefix = $ArtifactRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar +if (-not $stageDir.StartsWith($expectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "staging directory is outside artifact root: $stageDir" +} +New-Item -ItemType Directory -Path $stageDir | Out-Null +$passed = $false +$oldAppData = $env:APPDATA +$oldLocalAppData = $env:LOCALAPPDATA + +function Get-AvailablePort { + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + try { + $listener.Start() + return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port + } finally { $listener.Stop() } +} + +try { + $env:APPDATA = Join-Path $stageDir "appdata" + $env:LOCALAPPDATA = Join-Path $stageDir "localappdata" + New-Item -ItemType Directory -Force -Path $env:APPDATA, $env:LOCALAPPDATA | Out-Null + + New-Item -ItemType Directory -Path (Join-Path $stageDir "e2e-policies") | Out-Null + foreach ($fixture in @("run-ws-agent-test.ps1", "mxc-ws-gateway.toml")) { + Copy-Item -LiteralPath (Join-Path $examplesRoot $fixture) -Destination $stageDir + } + Copy-Item -LiteralPath (Join-Path $examplesRoot "e2e-policies\ws-agent.yaml") -Destination (Join-Path $stageDir "e2e-policies") + + $port = Get-AvailablePort + & powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $stageDir "run-ws-agent-test.ps1") ` + -Mock -GatewayPath $GatewayPath -CliPath $CliPath -AgentDir (Join-Path $stageDir "agent") ` + -Port $port -GatewayName "mxc-ws-ci-$PID" + if ($LASTEXITCODE -ne 0) { throw "MXC WebSocket example failed (exit $LASTEXITCODE)" } + + $result = Get-ChildItem -LiteralPath $stageDir -Directory -Filter "results-ws-*" | + Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1 + if ($null -eq $result) { throw "WebSocket example produced no result bundle" } + $summary = Get-Content -LiteralPath (Join-Path $result.FullName "summary.txt") -Raw + if ($summary -notmatch '(?m)^verdict\s+: PASS\s*$' -or + $summary -notmatch '(?m)^mode\s+: mock-wiring\s*$' -or + $summary -notmatch 'PASS\s+mock-workload:' -or + $summary -notmatch 'PASS\s+sandbox-delete:') { + throw "WebSocket example did not report a complete mock-wiring PASS" + } + + $passed = $true + Write-Host "MXC WebSocket example mock CI passed for $target" +} finally { + $env:APPDATA = $oldAppData + $env:LOCALAPPDATA = $oldLocalAppData + if ($passed -and -not $KeepArtifacts -and (Test-Path -LiteralPath $stageDir)) { + Remove-Item -LiteralPath $stageDir -Recurse -Force + } else { + Write-Host "MXC WebSocket example artifacts: $stageDir" + } +} diff --git a/tasks/windows.toml b/tasks/windows.toml index 5b8bab4008..e16b6d366c 100644 --- a/tasks/windows.toml +++ b/tasks/windows.toml @@ -108,3 +108,18 @@ run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File crates/opensh description = "Run MXC Tier-3 e2e scenario runner in mock/wiring-only mode (no real wxc-exec required)" run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 -Mock" + +["windows:e2e:mxc:host-probe"] +description = "Run the shipped MXC host probe against an explicitly absent wxc-exec on hosted Windows" +run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-host-probe-e2e.ps1" + +["windows:e2e:mxc:ws-agent-mock"] +description = "Run the shipped MXC WebSocket agent example in hosted Windows mock mode" +run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-ws-agent-mock-e2e.ps1" + +["windows:e2e:mxc:openclaw-forward-mock"] +description = "Run the shipped MXC OpenClaw forward example in hosted Windows mock mode" +run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-openclaw-forward-mock-e2e.ps1"