From 7e2807c778b7a6ca435a6b5f3a591e187cc3abd5 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 28 Sep 2026 09:44:05 -0700 Subject: [PATCH 1/3] ci(windows): exercise MXC Ollama demo with mock API Signed-off-by: Prekshi Vyas --- .github/workflows/windows-msvc.yml | 6 + architecture/windows.md | 4 + .../examples/run-ollama-test.ps1 | 26 ++- .../tests/demo_examples.rs | 5 + tasks/scripts/windows-mxc-ollama-e2e.ps1 | 192 ++++++++++++++++++ tasks/scripts/windows-mxc-ollama-stub.ps1 | 88 ++++++++ tasks/windows.toml | 10 + 7 files changed, 328 insertions(+), 3 deletions(-) create mode 100644 tasks/scripts/windows-mxc-ollama-e2e.ps1 create mode 100644 tasks/scripts/windows-mxc-ollama-stub.ps1 diff --git a/.github/workflows/windows-msvc.yml b/.github/workflows/windows-msvc.yml index 00c41d9ca9..fdb3e50609 100644 --- a/.github/workflows/windows-msvc.yml +++ b/.github/workflows/windows-msvc.yml @@ -98,6 +98,10 @@ jobs: run: mise run --skip-tools windows:lint:${{ matrix.arch }} - name: Test run: mise run --skip-tools windows:test:${{ matrix.arch }} + - name: Build MXC example E2E binaries + run: mise run --skip-tools windows:build:${{ matrix.arch }} + - name: Run shipped MXC Ollama example with mock API + run: mise run --skip-tools windows:e2e:mxc:ollama-mock:${{ matrix.arch }} - name: sccache stats if: always() run: sccache --show-stats @@ -211,6 +215,8 @@ jobs: cache-bin: "false" - name: Build release binaries run: mise run --skip-tools windows:build:${{ matrix.arch }} + - name: Run shipped MXC Ollama example with mock API + run: mise run --skip-tools windows:e2e:mxc:ollama-mock:${{ matrix.arch }} - name: sccache stats if: always() run: sccache --show-stats diff --git a/architecture/windows.md b/architecture/windows.md index 7a14422302..8c097a6090 100644 --- a/architecture/windows.md +++ b/architecture/windows.md @@ -248,6 +248,10 @@ 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 Ollama demo against that mock and a local + Ollama-compatible API. This proves the complete demo script and response path + without a model or credential, but it does not prove MXC isolation or network + enforcement. - 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/examples/run-ollama-test.ps1 b/crates/openshell-driver-mxc/examples/run-ollama-test.ps1 index 59c5bbcbb6..1dbfb77161 100644 --- a/crates/openshell-driver-mxc/examples/run-ollama-test.ps1 +++ b/crates/openshell-driver-mxc/examples/run-ollama-test.ps1 @@ -2,6 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 # Hello World local-inference demo for OpenShell on MXC. PowerShell 5.1 compatible. +# -Mock runs the workload on the host through the in-process wxc shim. It is for +# CI wiring coverage only and does not provide MXC or AppContainer isolation. [CmdletBinding()] param( @@ -15,6 +17,7 @@ param( [string] $Prompt = "Say hello in exactly five words.", [ValidateRange(0, 65535)] [int] $Port = 0, [string] $SandboxName, + [switch] $Mock, [switch] $KeepArtifacts ) @@ -135,11 +138,18 @@ $success = $false $failure = $null $oldGatewayConfig = $env:OPENSHELL_GATEWAY_CONFIG $oldComputeDriver = $env:OPENSHELL_COMPUTE_DRIVER +$oldMockWxc = $env:OPENSHELL_MXC_MOCK_WXC try { $gateway = Resolve-Executable $GatewayPath "openshell-gateway.exe" "" $cli = Resolve-Executable $CliPath "openshell.exe" "" - $wxc = Resolve-Executable $WxcExecPath "wxc-exec.exe" "OPENSHELL_WXC_EXEC_PATH" + if ($Mock) { + # The gateway still validates that wxc_exec_path is absolute. The + # in-process mock never launches this placeholder. + $wxc = Join-Path $here "mock-wxc-exec.exe" + } else { + $wxc = Resolve-Executable $WxcExecPath "wxc-exec.exe" "OPENSHELL_WXC_EXEC_PATH" + } foreach ($fixture in @("mxc-ollama.toml", "ollama.yaml")) { if (-not (Test-Path -LiteralPath (Join-Path $here $fixture) -PathType Leaf)) { throw "required demo fixture '$fixture' is missing beside the runner" @@ -163,7 +173,7 @@ try { Info "gateway: $gateway" Info "CLI: $cli" - Info "wxc-exec: $wxc" + Info "wxc-exec: $(if ($Mock) { 'in-process mock (no MXC isolation)' } else { $wxc })" Info "share: $ShareDir" Info "Ollama: http://${OllamaHost}:$OllamaPort" @@ -226,6 +236,11 @@ try { $gwErrLog = Join-Path $resultDir "gateway.err.log" $env:OPENSHELL_GATEWAY_CONFIG = $tomlUsed $env:OPENSHELL_COMPUTE_DRIVER = "mxc" + if ($Mock) { + $env:OPENSHELL_MXC_MOCK_WXC = "1" + } else { + Remove-Item Env:OPENSHELL_MXC_MOCK_WXC -ErrorAction SilentlyContinue + } $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)) { @@ -269,13 +284,18 @@ try { } $env:OPENSHELL_GATEWAY_CONFIG = $oldGatewayConfig $env:OPENSHELL_COMPUTE_DRIVER = $oldComputeDriver + if ([string]::IsNullOrWhiteSpace($oldMockWxc)) { + Remove-Item Env:OPENSHELL_MXC_MOCK_WXC -ErrorAction SilentlyContinue + } else { + $env:OPENSHELL_MXC_MOCK_WXC = $oldMockWxc + } if (-not $KeepArtifacts -and $createdShare -and $ShareDir -and (Test-Path -LiteralPath $ShareDir)) { Remove-Item -LiteralPath $ShareDir -Recurse -Force -ErrorAction SilentlyContinue } } $verdict = if ($success) { "PASS" } else { "FAIL" } -$summary = "verdict=$verdict`r`nbase=local-ollama`r`nsandbox=$SandboxName`r`ngateway=$endpoint`r`nbackend=process_container`r`nresult=$failure`r`n" +$summary = "verdict=$verdict`r`nbase=local-ollama`r`nmode=$(if ($Mock) { 'mock-wiring' } else { 'real-mxc' })`r`nsandbox=$SandboxName`r`ngateway=$endpoint`r`nbackend=process_container`r`nresult=$failure`r`n" Write-Utf8 (Join-Path $resultDir "summary.txt") $summary Write-Host "`n$summary" Write-Host "Results: $resultDir" diff --git a/crates/openshell-driver-mxc/tests/demo_examples.rs b/crates/openshell-driver-mxc/tests/demo_examples.rs index 11e72e688d..04c1d90e59 100644 --- a/crates/openshell-driver-mxc/tests/demo_examples.rs +++ b/crates/openshell-driver-mxc/tests/demo_examples.rs @@ -156,6 +156,11 @@ fn shipped_runners_supply_sandbox_scoped_workload_configuration() { assert!(!cloud.contains("pass -ApiKey")); assert!(cloud.contains("nvidia/nemotron-3.5-lightning-30b-a3b")); assert!(!cloud.contains("nvidia/nvidia-nemotron-nano-9b-v2")); + + let local = read_example("run-ollama-test.ps1"); + assert!(local.contains("[switch] $Mock")); + assert!(local.contains("in-process wxc shim")); + assert!(local.contains("does not provide MXC or AppContainer isolation")); } #[cfg(target_os = "windows")] diff --git a/tasks/scripts/windows-mxc-ollama-e2e.ps1 b/tasks/scripts/windows-mxc-ollama-e2e.ps1 new file mode 100644 index 0000000000..3d0ab11043 --- /dev/null +++ b/tasks/scripts/windows-mxc-ollama-e2e.ps1 @@ -0,0 +1,192 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Runs the shipped MXC Ollama demo through the real gateway and CLI against the +# in-process wxc mock and a local Ollama-compatible HTTP stub. This proves demo +# wiring only; it is not evidence of MXC or AppContainer enforcement. + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateSet("x86_64-pc-windows-msvc", "aarch64-pc-windows-msvc")] + [string] $Target, + + [string] $GatewayPath, + [string] $CliPath, + [string] $ArtifactRoot, + [switch] $KeepArtifacts +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" +$PSNativeCommandUseErrorActionPreference = $false + +if (-not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + throw "windows-mxc-ollama-e2e.ps1 requires Windows." +} + +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$ExamplesRoot = Join-Path $RepoRoot "crates\openshell-driver-mxc\examples" +$TargetDir = if ([string]::IsNullOrWhiteSpace($env:CARGO_TARGET_DIR)) { + Join-Path $RepoRoot "target" +} else { + [System.IO.Path]::GetFullPath($env:CARGO_TARGET_DIR) +} + +if ([string]::IsNullOrWhiteSpace($GatewayPath)) { + $GatewayPath = Join-Path $TargetDir "$Target\release\openshell-gateway.exe" +} +if ([string]::IsNullOrWhiteSpace($CliPath)) { + $CliPath = Join-Path $TargetDir "$Target\release\openshell.exe" +} +$GatewayPath = [System.IO.Path]::GetFullPath($GatewayPath) +$CliPath = [System.IO.Path]::GetFullPath($CliPath) + +foreach ($artifact in @($GatewayPath, $CliPath)) { + if (-not (Test-Path -LiteralPath $artifact -PathType Leaf)) { + throw "required release artifact is missing: $artifact" + } +} + +if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { + $ArtifactRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) { + [System.IO.Path]::GetTempPath() + } else { + $env:RUNNER_TEMP + } +} +$ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) +$StageDir = [System.IO.Path]::GetFullPath((Join-Path $ArtifactRoot "openshell-mxc-ollama-e2e-$Target")) +$expectedPrefix = $ArtifactRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar +if (-not $StageDir.StartsWith($expectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "refusing to use staging path outside artifact root: $StageDir" +} +if (Test-Path -LiteralPath $StageDir) { + Remove-Item -LiteralPath $StageDir -Recurse -Force +} +New-Item -ItemType Directory -Path $StageDir | Out-Null + +foreach ($fixture in @("run-ollama-test.ps1", "mxc-ollama.toml", "ollama.yaml")) { + Copy-Item -LiteralPath (Join-Path $ExamplesRoot $fixture) -Destination $StageDir +} + +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() + } +} + +function Test-Listener([int] $Port) { + $client = [System.Net.Sockets.TcpClient]::new() + try { + $pending = $client.BeginConnect("127.0.0.1", $Port, $null, $null) + if (-not $pending.AsyncWaitHandle.WaitOne(250)) { return $false } + $client.EndConnect($pending) + return $true + } catch { + return $false + } finally { + $client.Dispose() + } +} + +$ApiPort = Get-AvailablePort +$RequestLog = Join-Path $StageDir "mock-ollama-requests.log" +$ServerReady = Join-Path $StageDir "mock-ollama.ready" +$ServerOutLog = Join-Path $StageDir "mock-ollama.out.log" +$ServerErrLog = Join-Path $StageDir "mock-ollama.err.log" +$ServerScript = Join-Path $PSScriptRoot "windows-mxc-ollama-stub.ps1" +$serverProcess = $null +$passed = $false +$oldAppData = $env:APPDATA +$oldLocalAppData = $env:LOCALAPPDATA + +try { + $serverProcess = Start-Process -FilePath "powershell.exe" -ArgumentList @( + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "-File", "`"$ServerScript`"", + "-Port", "$ApiPort", + "-RequestLog", "`"$RequestLog`"", + "-ReadyPath", "`"$ServerReady`"" + ) -PassThru -WindowStyle Hidden -RedirectStandardOutput $ServerOutLog -RedirectStandardError $ServerErrLog + + $deadline = (Get-Date).AddSeconds(15) + while ((Get-Date) -lt $deadline -and (-not (Test-Path -LiteralPath $ServerReady) -or -not (Test-Listener $ApiPort))) { + if ($serverProcess.HasExited) { + $details = (Get-Content $ServerOutLog, $ServerErrLog -ErrorAction SilentlyContinue) -join [Environment]::NewLine + throw "mock Ollama server stopped before listening: $details" + } + Start-Sleep -Milliseconds 200 + } + if (-not (Test-Listener $ApiPort)) { + throw "mock Ollama server did not listen on port $ApiPort within 15 seconds" + } + + $env:APPDATA = Join-Path $StageDir "appdata" + $env:LOCALAPPDATA = Join-Path $StageDir "localappdata" + New-Item -ItemType Directory -Force -Path $env:APPDATA, $env:LOCALAPPDATA | Out-Null + + $runner = Join-Path $StageDir "run-ollama-test.ps1" + $share = Join-Path $StageDir "share" + $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $runner ` + -Mock ` + -GatewayPath $GatewayPath ` + -CliPath $CliPath ` + -ShareDir $share ` + -OllamaPort $ApiPort ` + -Model "openshell-ci-mock" ` + -Prompt "Return the CI mock response." ` + -KeepArtifacts 2>&1 + $exitCode = $LASTEXITCODE + $output | ForEach-Object { Write-Host $_ } + if ($exitCode -ne 0) { + throw "shipped Ollama demo failed in mock mode (exit $exitCode)" + } + + $resultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-ollama-*" | + Sort-Object LastWriteTimeUtc -Descending | + Select-Object -First 1 + if ($null -eq $resultDir) { throw "Ollama demo did not produce a results directory" } + + $summary = Get-Content -LiteralPath (Join-Path $resultDir.FullName "summary.txt") -Raw + if ($summary -notmatch '(?m)^verdict=PASS\s*$' -or $summary -notmatch '(?m)^mode=mock-wiring\s*$') { + throw "Ollama demo summary did not report a mock-wiring PASS: $summary" + } + $response = Get-Content -LiteralPath (Join-Path $resultDir.FullName "ollama-response.json") -Raw | ConvertFrom-Json + if ($response.response -ne "Hello from the CI mock.") { + throw "Ollama demo did not retain the expected mock completion" + } + + $requests = Get-Content -LiteralPath $RequestLog -Raw + if ($requests -notmatch 'GET /api/tags ' -or $requests -notmatch 'POST /api/generate ') { + throw "mock Ollama server did not observe both demo API calls: $requests" + } + + $passed = $true + Write-Host "MXC Ollama example mock E2E passed for $Target" +} catch { + Write-Host "MXC Ollama example mock E2E failed: $($_.Exception.Message)" -ForegroundColor Red + Get-ChildItem -LiteralPath $StageDir -File -Recurse -Include "*.log", "summary.txt" -ErrorAction SilentlyContinue | + ForEach-Object { + Write-Host "--- $($_.FullName) ---" + Get-Content -LiteralPath $_.FullName -ErrorAction SilentlyContinue | ForEach-Object { Write-Host $_ } + } + throw +} finally { + $env:APPDATA = $oldAppData + $env:LOCALAPPDATA = $oldLocalAppData + if ($serverProcess -and -not $serverProcess.HasExited) { + Stop-Process -Id $serverProcess.Id -Force -ErrorAction SilentlyContinue + try { [void] $serverProcess.WaitForExit(5000) } catch {} + } + if ($passed -and -not $KeepArtifacts -and (Test-Path -LiteralPath $StageDir)) { + Remove-Item -LiteralPath $StageDir -Recurse -Force + } else { + Write-Host "MXC Ollama example artifacts: $StageDir" + } +} diff --git a/tasks/scripts/windows-mxc-ollama-stub.ps1 b/tasks/scripts/windows-mxc-ollama-stub.ps1 new file mode 100644 index 0000000000..309777a6e7 --- /dev/null +++ b/tasks/scripts/windows-mxc-ollama-stub.ps1 @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateRange(1, 65535)] + [int] $Port, + + [Parameter(Mandatory = $true)] + [string] $RequestLog, + + [Parameter(Mandatory = $true)] + [string] $ReadyPath +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +$utf8 = [System.Text.UTF8Encoding]::new($false) +$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port) +$listener.Start() +[System.IO.File]::WriteAllText($ReadyPath, "$Port`r`n", $utf8) + +try { + while ($true) { + $client = $listener.AcceptTcpClient() + try { + $stream = $client.GetStream() + $reader = [System.IO.StreamReader]::new( + $stream, + [System.Text.Encoding]::ASCII, + $false, + 1024, + $true + ) + $requestLine = $reader.ReadLine() + if ([string]::IsNullOrWhiteSpace($requestLine)) { continue } + + $contentLength = 0 + while ($true) { + $line = $reader.ReadLine() + if ([string]::IsNullOrEmpty($line)) { break } + if ($line -match '^Content-Length:\s*(\d+)\s*$') { + $contentLength = [int] $Matches[1] + } + } + if ($contentLength -gt 0) { + $buffer = New-Object char[] $contentLength + $read = 0 + while ($read -lt $contentLength) { + $count = $reader.Read($buffer, $read, $contentLength - $read) + if ($count -le 0) { break } + $read += $count + } + } + + [System.IO.File]::AppendAllText($RequestLog, "$requestLine`r`n", $utf8) + $parts = $requestLine.Split(' ') + $path = if ($parts.Count -ge 2) { $parts[1] } else { "/" } + switch ($path) { + "/api/tags" { + $status = "200 OK" + $body = '{"models":[{"name":"openshell-ci-mock"}]}' + } + "/api/generate" { + $status = "200 OK" + $body = '{"model":"openshell-ci-mock","response":"Hello from the CI mock.","done":true}' + } + default { + $status = "404 Not Found" + $body = '{"error":"not found"}' + } + } + + $bodyBytes = $utf8.GetBytes($body) + $headers = "HTTP/1.1 $status`r`nContent-Type: application/json`r`nContent-Length: $($bodyBytes.Length)`r`nConnection: close`r`n`r`n" + $headerBytes = [System.Text.Encoding]::ASCII.GetBytes($headers) + $stream.Write($headerBytes, 0, $headerBytes.Length) + $stream.Write($bodyBytes, 0, $bodyBytes.Length) + $stream.Flush() + } finally { + $client.Dispose() + } + } +} finally { + $listener.Stop() +} diff --git a/tasks/windows.toml b/tasks/windows.toml index 5b8bab4008..6b1bd4d8a5 100644 --- a/tasks/windows.toml +++ b/tasks/windows.toml @@ -108,3 +108,13 @@ 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:ollama-mock:x64"] +description = "Run the shipped MXC Ollama demo against the in-process wxc mock and a local API stub on Windows x64" +run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-ollama-e2e.ps1 -Target x86_64-pc-windows-msvc" + +["windows:e2e:mxc:ollama-mock:arm64"] +description = "Run the shipped MXC Ollama demo against the in-process wxc mock and a local API stub on Windows ARM64" +run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-ollama-e2e.ps1 -Target aarch64-pc-windows-msvc" From 442d6f0fe4abca0680e1520e2c682cdb5947de48 Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 28 Sep 2026 16:26:26 -0700 Subject: [PATCH 2/3] ci(windows): cover both MXC inference demos Signed-off-by: Prekshi Vyas --- .../build-openshell-mxc-windows/SKILL.md | 39 +++-- .github/workflows/windows-msvc.yml | 8 +- architecture/windows.md | 9 +- .../examples/inference.yaml | 4 +- .../examples/run-inference-test.ps1 | 57 ++++++- crates/openshell-driver-mxc/src/driver.rs | 4 +- crates/openshell-driver-mxc/src/mxc.rs | 36 ++++- .../tests/demo_examples.rs | 10 ++ ...=> windows-mxc-inference-examples-e2e.ps1} | 140 +++++++++++++----- ...tub.ps1 => windows-mxc-inference-stub.ps1} | 30 +++- tasks/windows.toml | 12 +- 11 files changed, 275 insertions(+), 74 deletions(-) rename tasks/scripts/{windows-mxc-ollama-e2e.ps1 => windows-mxc-inference-examples-e2e.ps1} (50%) rename tasks/scripts/{windows-mxc-ollama-stub.ps1 => windows-mxc-inference-stub.ps1} (71%) diff --git a/.agents/skills/build-openshell-mxc-windows/SKILL.md b/.agents/skills/build-openshell-mxc-windows/SKILL.md index a7d30f63f7..18e69d943d 100644 --- a/.agents/skills/build-openshell-mxc-windows/SKILL.md +++ b/.agents/skills/build-openshell-mxc-windows/SKILL.md @@ -1,6 +1,6 @@ --- name: build-openshell-mxc-windows -description: Maintain and validate OpenShell's build-only Windows MSVC lane for x64 and ARM64. Use when working on Windows compilation, `windows:*` mise tasks, unsupported Windows compute-driver contracts, or Windows build reports. This skill does not implement Docker, Kubernetes, Podman, VM, MXC driver, policy translation, MSI, service, or supervisor runtime support on Windows. +description: Maintain and validate OpenShell's Windows MSVC lane for x64 and ARM64. Use when working on Windows compilation, `windows:*` mise tasks, unsupported Windows compute-driver contracts, MXC example wiring checks, or Windows build reports. This skill does not implement Docker, Kubernetes, Podman, VM, MXC driver, policy translation, MSI, service, or supervisor runtime support on Windows. metadata: internal: true --- @@ -12,8 +12,8 @@ OpenShell repository. The Windows lane is already present in `main`; do not treat this skill as a first-time porting recipe unless the user explicitly asks for a new fork or a from-scratch bring-up. -The lane is build-only. It validates that OpenShell can compile and test on -Windows MSVC for the supported deliverables: +The lane validates that OpenShell can compile and test on Windows MSVC for the +supported deliverables: - `openshell-gateway.exe` - `openshell.exe` @@ -51,6 +51,8 @@ In scope: `openshell`. - Running workspace tests on a native x64 or ARM64 host. - Running focused unsupported-driver contract tests. +- Running the shipped Ollama and cloud-inference MXC examples against the + in-process `wxc` mock and a local API stub. - Reporting test counts, skipped/gated areas, warnings, artifacts, and logs. - Keeping Linux and macOS build paths unchanged. - Keeping unsupported Windows compute drivers explicit and testable. @@ -158,8 +160,15 @@ mise run --skip-tools windows:build:x64 mise run --skip-tools windows:build:arm64 mise run --skip-tools windows:test:x64 mise run --skip-tools windows:test:unsupported:x64 +mise run --skip-tools windows:e2e:mxc:inference-mock:x64 ``` +Use the `arm64` form of the inference task on a native ARM64 host. It exercises +the real gateway, CLI, and shipped PowerShell runners with an in-process `wxc` +mock and local HTTP responses. It proves example wiring, request execution, and +sandbox-scoped credential propagation; it does not prove MXC, AppContainer, +filesystem, or network enforcement. + The two `windows:test:mxc-real:*` tasks are host-specific and mutually exclusive on a single host (each rejects the other architecture -- see the table below): run `windows:test:mxc-real:x64` on an x64 host, or @@ -220,14 +229,17 @@ order: The GitHub Actions jobs layer architecture-specific `Swatinem/rust-cache` entries for Cargo registry and dependency target artifacts with sccache's GHA backend for cacheable Rust compiler outputs. Failed runs also save their usable -dependency artifacts. Pull-request mirrors labeled `test:windows` run Clippy for the -Windows-supported workspace and e2e crates plus Rust tests. Pushes to `main` and -manual dispatches run the same lint and test commands in a cache-seed job, -followed by a dependent release-binary build job. The seed and PR jobs use the -same cache namespaces. Merge queues do not run this workflow. Main/manual seed -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. +dependency artifacts. Pull-request mirrors labeled `test:windows` run Clippy +for the Windows-supported workspace and e2e crates plus Rust tests, build +release binaries, and run both shipped MXC inference examples through the mock +task. Pushes to `main` and manual dispatches run the same lint and test commands +in a cache-seed job, followed by a dependent release-binary build and +mock-example job. The seed and PR jobs use the same cache namespaces. Merge +queues do not run this workflow. Main/manual seed 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. 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 @@ -272,6 +284,8 @@ 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:inference-mock:x64` | Runs the shipped Ollama and cloud-inference demos on native x64 through the real gateway and CLI, in-process `wxc` mock, and local API stub. This is wiring evidence, not MXC enforcement evidence. | +| `windows:e2e:mxc:inference-mock:arm64` | Runs the same mock-wiring checks on native ARM64 with ARM64 release binaries. | | `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. | @@ -324,6 +338,8 @@ When reporting `windows:ci`, distinguish these categories: - Passed tests from the full ARM64 workspace test log when run on a native ARM64 host. - The focused unsupported-contract re-run. +- The architecture-matched MXC inference-example mock task and its explicit + wiring-only limitation. - Explicit Cargo ignored tests, usually ignored doc examples. - Tests hidden by `#[cfg(not(target_os = "windows"))]`; these often appear as `running 0 tests`, not as ignored tests. @@ -385,6 +401,7 @@ Every substantial Windows build run should report: | ARM64 check/build | Pass/fail/skipped and log path. | | Native tests | Passed/failed/ignored/filtered counts and log path for the host architecture. | | Unsupported contracts | Which focused tests ran and their result. | +| MXC example mock E2E | Architecture, result, artifact directory on failure, and the wiring-only limitation. | | Artifacts | Binary paths, size, and SHA256 when available. | | Skips | Explicitly explain tests not run for a non-native architecture, unsupported driver package exclusions, and Windows cfg-gated tests. | | Follow-ups | Only concrete follow-ups tied to failures or requested scope. | diff --git a/.github/workflows/windows-msvc.yml b/.github/workflows/windows-msvc.yml index fdb3e50609..1b7964eb20 100644 --- a/.github/workflows/windows-msvc.yml +++ b/.github/workflows/windows-msvc.yml @@ -100,8 +100,8 @@ jobs: run: mise run --skip-tools windows:test:${{ matrix.arch }} - name: Build MXC example E2E binaries run: mise run --skip-tools windows:build:${{ matrix.arch }} - - name: Run shipped MXC Ollama example with mock API - run: mise run --skip-tools windows:e2e:mxc:ollama-mock:${{ matrix.arch }} + - name: Run shipped MXC inference examples with mock API + run: mise run --skip-tools windows:e2e:mxc:inference-mock:${{ matrix.arch }} - name: sccache stats if: always() run: sccache --show-stats @@ -215,8 +215,8 @@ jobs: cache-bin: "false" - name: Build release binaries run: mise run --skip-tools windows:build:${{ matrix.arch }} - - name: Run shipped MXC Ollama example with mock API - run: mise run --skip-tools windows:e2e:mxc:ollama-mock:${{ matrix.arch }} + - name: Run shipped MXC inference examples with mock API + run: mise run --skip-tools windows:e2e:mxc:inference-mock:${{ matrix.arch }} - name: sccache stats if: always() run: sccache --show-stats diff --git a/architecture/windows.md b/architecture/windows.md index 8c097a6090..abf5043543 100644 --- a/architecture/windows.md +++ b/architecture/windows.md @@ -248,10 +248,11 @@ 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 Ollama demo against that mock and a local - Ollama-compatible API. This proves the complete demo script and response path - without a model or credential, but it does not prove MXC isolation or network - enforcement. +- Hosted Windows CI runs the shipped Ollama and cloud-inference demos against + that mock and a local compatible API. This proves both complete demo scripts, + sandbox-scoped credential propagation, and response paths without a model or + external credential, but it does not prove MXC isolation, proxy substitution, + or network enforcement. - 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/examples/inference.yaml b/crates/openshell-driver-mxc/examples/inference.yaml index e20f4020b8..820321c0be 100644 --- a/crates/openshell-driver-mxc/examples/inference.yaml +++ b/crates/openshell-driver-mxc/examples/inference.yaml @@ -14,8 +14,8 @@ network_policies: nvidia_inference: name: nvidia-inference endpoints: - - host: integrate.api.nvidia.com - port: 443 + - host: "__INFERENCE_HOST__" + port: __INFERENCE_PORT__ protocol: rest # Chat completions use POST. access: read-write diff --git a/crates/openshell-driver-mxc/examples/run-inference-test.ps1 b/crates/openshell-driver-mxc/examples/run-inference-test.ps1 index 1dc08f5a5f..9b2a39e329 100644 --- a/crates/openshell-driver-mxc/examples/run-inference-test.ps1 +++ b/crates/openshell-driver-mxc/examples/run-inference-test.ps1 @@ -2,6 +2,8 @@ # SPDX-License-Identifier: Apache-2.0 # Cloud inference (T1) demo for OpenShell on MXC. PowerShell 5.1 compatible. +# -Mock runs the workload on the host through the in-process wxc shim. It is for +# CI wiring coverage only and does not provide MXC or AppContainer isolation. [CmdletBinding()] param( @@ -11,8 +13,10 @@ param( [string] $ShareDir, [string] $Model = "nvidia/nemotron-3.5-lightning-30b-a3b", [string] $Prompt = "Say hello in exactly five words.", + [string] $ApiUrl = "https://integrate.api.nvidia.com/v1/chat/completions", [ValidateRange(0, 65535)] [int] $Port = 0, [string] $SandboxName, + [switch] $Mock, [switch] $KeepArtifacts ) @@ -133,6 +137,7 @@ $success = $false $failure = $null $oldGatewayConfig = $env:OPENSHELL_GATEWAY_CONFIG $oldComputeDriver = $env:OPENSHELL_COMPUTE_DRIVER +$oldMockWxc = $env:OPENSHELL_MXC_MOCK_WXC $oldApiKey = $env:NV_API_KEY $apiKey = $env:NV_API_KEY @@ -142,12 +147,29 @@ try { } $gateway = Resolve-Executable $GatewayPath "openshell-gateway.exe" "" $cli = Resolve-Executable $CliPath "openshell.exe" "" - $wxc = Resolve-Executable $WxcExecPath "wxc-exec.exe" "OPENSHELL_WXC_EXEC_PATH" + if ($Mock) { + # The gateway still validates that wxc_exec_path is absolute. The + # in-process mock never launches this placeholder. + $wxc = Join-Path $here "mock-wxc-exec.exe" + } else { + $wxc = Resolve-Executable $WxcExecPath "wxc-exec.exe" "OPENSHELL_WXC_EXEC_PATH" + } foreach ($fixture in @("mxc-inference.toml", "inference.yaml")) { if (-not (Test-Path -LiteralPath (Join-Path $here $fixture) -PathType Leaf)) { throw "required demo fixture '$fixture' is missing beside the runner" } } + if ($ApiUrl.IndexOfAny([char[]]@('"', '%', '!', '&', '|', '<', '>', '^', [char] 13, [char] 10)) -ge 0) { + throw "ApiUrl contains a character that cannot be rendered safely into the sandbox command" + } + try { + $apiUri = [System.Uri] $ApiUrl + } catch { + throw "ApiUrl is not a valid absolute URI: $ApiUrl" + } + if (-not $apiUri.IsAbsoluteUri -or $apiUri.Scheme -notin @("http", "https")) { + throw "ApiUrl must be an absolute HTTP or HTTPS URI" + } if ($Port -eq 0) { $Port = Get-AvailablePort } $endpoint = "http://127.0.0.1:$Port" if ([string]::IsNullOrWhiteSpace($SandboxName)) { $SandboxName = "inference-$PID" } @@ -163,8 +185,9 @@ try { Info "gateway: $gateway" Info "CLI: $cli" - Info "wxc-exec: $wxc" + Info "wxc-exec: $(if ($Mock) { 'in-process mock (no MXC isolation)' } else { $wxc })" Info "share: $ShareDir" + Info "inference API: $ApiUrl" Info "NV_API_KEY: present (value redacted)" $cmdExe = Join-Path $env:SystemRoot "System32\cmd.exe" @@ -187,6 +210,8 @@ try { $policyText = [System.IO.File]::ReadAllText((Join-Path $here "inference.yaml")) $policyText = $policyText.Replace("__OPENSHELL_DEMO_SHARE__", $sharePolicy) $policyText = $policyText.Replace("__CMD_EXE__", $cmdExe) + $policyText = $policyText.Replace("__INFERENCE_HOST__", $apiUri.DnsSafeHost) + $policyText = $policyText.Replace("__INFERENCE_PORT__", [string] $apiUri.Port) Write-Utf8 $policyUsed $policyText $requestPath = Join-Path $ShareDir "inference-request.json" @@ -198,11 +223,21 @@ try { Write-Utf8 $requestPath $requestJson $probePath = Join-Path $ShareDir "inference-probe.cmd" - $probeLines = @( - "@echo off", + $tlsArgs = if ($apiUri.Scheme -eq "https") { # Inbox curl uses Schannel and does not honor CURL_CA_BUNDLE by itself. # Point --cacert at the public bundle staged by the governed proxy. - "`"$curlExe`" --silent --show-error --fail-with-body --ssl-no-revoke --cacert `"%CURL_CA_BUNDLE%`" --max-time 120 -D `"$headersPath`" -H `"Authorization: Bearer %NV_API_KEY%`" -H `"Content-Type: application/json`" --data-binary `"@$requestPath`" -o `"$responsePath`" `"https://integrate.api.nvidia.com/v1/chat/completions`" 2> `"$errorPath`" || exit /b 31", + '--ssl-no-revoke --cacert "%CURL_CA_BUNDLE%"' + } else { + "" + } + $proxyBypassArgs = if ($apiUri.IsLoopback) { + "--noproxy `"$($apiUri.DnsSafeHost)`"" + } else { + "" + } + $probeLines = @( + "@echo off", + "`"$curlExe`" $proxyBypassArgs --silent --show-error --fail-with-body $tlsArgs --max-time 120 -D `"$headersPath`" -H `"Authorization: Bearer %NV_API_KEY%`" -H `"Content-Type: application/json`" --data-binary `"@$requestPath`" -o `"$responsePath`" `"$ApiUrl`" 2> `"$errorPath`" || exit /b 31", "`"$findStrExe`" /C:`"choices`" `"$responsePath`" >nul || exit /b 32", "echo PASS> `"$donePath`"" ) @@ -214,6 +249,11 @@ try { $gwErrLog = Join-Path $resultDir "gateway.err.log" $env:OPENSHELL_GATEWAY_CONFIG = $tomlUsed $env:OPENSHELL_COMPUTE_DRIVER = "mxc" + if ($Mock) { + $env:OPENSHELL_MXC_MOCK_WXC = "1" + } else { + Remove-Item Env:OPENSHELL_MXC_MOCK_WXC -ErrorAction SilentlyContinue + } # The credential belongs to sandbox creation, not gateway configuration. Remove-Item Env:NV_API_KEY -ErrorAction SilentlyContinue try { @@ -280,6 +320,11 @@ try { } $env:OPENSHELL_GATEWAY_CONFIG = $oldGatewayConfig $env:OPENSHELL_COMPUTE_DRIVER = $oldComputeDriver + if ([string]::IsNullOrWhiteSpace($oldMockWxc)) { + Remove-Item Env:OPENSHELL_MXC_MOCK_WXC -ErrorAction SilentlyContinue + } else { + $env:OPENSHELL_MXC_MOCK_WXC = $oldMockWxc + } if ([string]::IsNullOrWhiteSpace($oldApiKey)) { Remove-Item Env:NV_API_KEY -ErrorAction SilentlyContinue } else { $env:NV_API_KEY = $oldApiKey } if (-not $KeepArtifacts -and $createdShare -and $ShareDir -and (Test-Path -LiteralPath $ShareDir)) { Remove-Item -LiteralPath $ShareDir -Recurse -Force -ErrorAction SilentlyContinue @@ -287,7 +332,7 @@ try { } $verdict = if ($success) { "PASS" } else { "FAIL" } -$summary = "verdict=$verdict`r`nbase=cloud-inference`r`nsandbox=$SandboxName`r`ngateway=$endpoint`r`nbackend=process_container`r`nresult=$failure`r`n" +$summary = "verdict=$verdict`r`nbase=cloud-inference`r`nmode=$(if ($Mock) { 'mock-wiring' } else { 'real-mxc' })`r`nsandbox=$SandboxName`r`ngateway=$endpoint`r`nbackend=process_container`r`nresult=$failure`r`n" Write-Utf8 (Join-Path $resultDir "summary.txt") $summary Write-Host "`n$summary" Write-Host "Results: $resultDir" diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs index cccb77bd47..bf9ffe10e2 100644 --- a/crates/openshell-driver-mxc/src/driver.rs +++ b/crates/openshell-driver-mxc/src/driver.rs @@ -151,7 +151,7 @@ async fn wait_for_target_listener(port: u16) -> std::io::Result<()> { /// /// - `IsolationSession`: persistent, attachable session /// (provision → start → exec → stop → deprovision). Does not support -/// OpenShell filesystem-policy grants; backend defaults determine visibility. +/// `OpenShell` filesystem-policy grants; backend defaults determine visibility. /// - `ProcessContainer` (default): one-shot `AppContainer`. Genuinely default-deny: a /// write to any ungranted path is denied by the OS. No persistent session. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] @@ -2847,6 +2847,8 @@ mod lifecycle_tests { .replace("__OPENSHELL_DEMO_SHARE__", share) .replace("__OLLAMA_HOST__", "127.0.0.1") .replace("__OLLAMA_PORT__", "11434") + .replace("__INFERENCE_HOST__", "integrate.api.nvidia.com") + .replace("__INFERENCE_PORT__", "443") .replace("__CMD_EXE__", r"C:\Windows\System32\cmd.exe"); parse_sandbox_policy(&rendered).expect("parse rendered shipped demo policy") } diff --git a/crates/openshell-driver-mxc/src/mxc.rs b/crates/openshell-driver-mxc/src/mxc.rs index e0e1598b1f..6f68dc5e3d 100644 --- a/crates/openshell-driver-mxc/src/mxc.rs +++ b/crates/openshell-driver-mxc/src/mxc.rs @@ -660,7 +660,15 @@ impl WxcExecInvoker { let cmd_norm = mock_normalize(&process.command_line); let in_policy = grants.iter().any(|g| !g.is_empty() && cmd_norm.contains(g)); - let mut cmd = Command::new("cmd"); + let command_shell = std::env::var_os("COMSPEC") + .unwrap_or_else(|| std::ffi::OsString::from(r"C:\Windows\System32\cmd.exe")); + let mut cmd = Command::new(command_shell); + cmd.env_clear(); + for entry in &process.env { + if let Some((key, value)) = entry.split_once('=') { + cmd.env(key, value); + } + } cmd.stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) @@ -975,6 +983,32 @@ mod tests { assert!(config.get("ui").is_none()); } + #[tokio::test] + async fn mock_exec_uses_only_the_mxc_process_environment() { + const KEY: &str = "OPENSHELL_MXC_MOCK_ENV_TEST"; + let workdir = tempfile::tempdir().expect("temporary workdir"); + let output = workdir.path().join("mock-env.txt"); + let process = MxcProcess { + command_line: format!("echo %{KEY}%,%SystemRoot% 1> \"{}\"", output.display()), + cwd: workdir.path().to_string_lossy().into_owned(), + env: vec![format!("{KEY}=process-value")], + timeout: 0, + }; + + let mut child = WxcExecInvoker::mock_spawn_with_grants( + &process, + &[mock_normalize(&workdir.path().to_string_lossy())], + ) + .expect("mock process should launch"); + let status = child.wait().await.expect("mock process should finish"); + + assert!(status.success()); + assert_eq!( + std::fs::read_to_string(output).expect("mock output").trim(), + "process-value,%SystemRoot%" + ); + } + #[test] fn oneshot_config_json_emits_typed_ui_policy() { let filesystem = MxcFilesystem::default(); diff --git a/crates/openshell-driver-mxc/tests/demo_examples.rs b/crates/openshell-driver-mxc/tests/demo_examples.rs index 04c1d90e59..2b9ab52362 100644 --- a/crates/openshell-driver-mxc/tests/demo_examples.rs +++ b/crates/openshell-driver-mxc/tests/demo_examples.rs @@ -102,6 +102,8 @@ fn shipped_inference_policies_are_narrow_and_valid_after_rendering() { .replace("__OPENSHELL_DEMO_SHARE__", share) .replace("__OLLAMA_HOST__", "127.0.0.1") .replace("__OLLAMA_PORT__", "11434") + .replace("__INFERENCE_HOST__", "integrate.api.nvidia.com") + .replace("__INFERENCE_PORT__", "443") .replace("__CMD_EXE__", r"C:\Windows\System32\cmd.exe"); let policy = parse_sandbox_policy(&rendered) .unwrap_or_else(|error| panic!("failed to parse rendered {name}: {error}")); @@ -161,6 +163,14 @@ fn shipped_runners_supply_sandbox_scoped_workload_configuration() { assert!(local.contains("[switch] $Mock")); assert!(local.contains("in-process wxc shim")); assert!(local.contains("does not provide MXC or AppContainer isolation")); + + let cloud = read_example("run-inference-test.ps1"); + assert!(cloud.contains("[switch] $Mock")); + assert!(cloud.contains("[string] $ApiUrl")); + assert!(cloud.contains("$apiUri.IsLoopback")); + assert!(cloud.contains("--noproxy")); + assert!(cloud.contains("in-process wxc shim")); + assert!(cloud.contains("does not provide MXC or AppContainer isolation")); } #[cfg(target_os = "windows")] diff --git a/tasks/scripts/windows-mxc-ollama-e2e.ps1 b/tasks/scripts/windows-mxc-inference-examples-e2e.ps1 similarity index 50% rename from tasks/scripts/windows-mxc-ollama-e2e.ps1 rename to tasks/scripts/windows-mxc-inference-examples-e2e.ps1 index 3d0ab11043..f0f3f95b78 100644 --- a/tasks/scripts/windows-mxc-ollama-e2e.ps1 +++ b/tasks/scripts/windows-mxc-inference-examples-e2e.ps1 @@ -1,8 +1,8 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# Runs the shipped MXC Ollama demo through the real gateway and CLI against the -# in-process wxc mock and a local Ollama-compatible HTTP stub. This proves demo +# Runs both shipped MXC inference demos through the real gateway and CLI +# against the in-process wxc mock and a local HTTP API stub. This proves demo # wiring only; it is not evidence of MXC or AppContainer enforcement. [CmdletBinding()] @@ -22,7 +22,7 @@ $ErrorActionPreference = "Stop" $PSNativeCommandUseErrorActionPreference = $false if (-not [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { - throw "windows-mxc-ollama-e2e.ps1 requires Windows." + throw "windows-mxc-inference-examples-e2e.ps1 requires Windows." } $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path @@ -56,7 +56,7 @@ if ([string]::IsNullOrWhiteSpace($ArtifactRoot)) { } } $ArtifactRoot = [System.IO.Path]::GetFullPath($ArtifactRoot) -$StageDir = [System.IO.Path]::GetFullPath((Join-Path $ArtifactRoot "openshell-mxc-ollama-e2e-$Target")) +$StageDir = [System.IO.Path]::GetFullPath((Join-Path $ArtifactRoot "openshell-mxc-inference-examples-e2e-$Target")) $expectedPrefix = $ArtifactRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar if (-not $StageDir.StartsWith($expectedPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { throw "refusing to use staging path outside artifact root: $StageDir" @@ -66,7 +66,14 @@ if (Test-Path -LiteralPath $StageDir) { } New-Item -ItemType Directory -Path $StageDir | Out-Null -foreach ($fixture in @("run-ollama-test.ps1", "mxc-ollama.toml", "ollama.yaml")) { +foreach ($fixture in @( + "run-ollama-test.ps1", + "mxc-ollama.toml", + "ollama.yaml", + "run-inference-test.ps1", + "mxc-inference.toml", + "inference.yaml" +)) { Copy-Item -LiteralPath (Join-Path $ExamplesRoot $fixture) -Destination $StageDir } @@ -95,15 +102,17 @@ function Test-Listener([int] $Port) { } $ApiPort = Get-AvailablePort -$RequestLog = Join-Path $StageDir "mock-ollama-requests.log" -$ServerReady = Join-Path $StageDir "mock-ollama.ready" -$ServerOutLog = Join-Path $StageDir "mock-ollama.out.log" -$ServerErrLog = Join-Path $StageDir "mock-ollama.err.log" -$ServerScript = Join-Path $PSScriptRoot "windows-mxc-ollama-stub.ps1" +$MockToken = "openshell-ci-mock-token" +$RequestLog = Join-Path $StageDir "mock-inference-requests.log" +$ServerReady = Join-Path $StageDir "mock-inference.ready" +$ServerOutLog = Join-Path $StageDir "mock-inference.out.log" +$ServerErrLog = Join-Path $StageDir "mock-inference.err.log" +$ServerScript = Join-Path $PSScriptRoot "windows-mxc-inference-stub.ps1" $serverProcess = $null $passed = $false $oldAppData = $env:APPDATA $oldLocalAppData = $env:LOCALAPPDATA +$oldNvApiKey = $env:NV_API_KEY try { $serverProcess = Start-Process -FilePath "powershell.exe" -ArgumentList @( @@ -112,65 +121,117 @@ try { "-File", "`"$ServerScript`"", "-Port", "$ApiPort", "-RequestLog", "`"$RequestLog`"", - "-ReadyPath", "`"$ServerReady`"" + "-ReadyPath", "`"$ServerReady`"", + "-ExpectedBearerToken", $MockToken ) -PassThru -WindowStyle Hidden -RedirectStandardOutput $ServerOutLog -RedirectStandardError $ServerErrLog $deadline = (Get-Date).AddSeconds(15) while ((Get-Date) -lt $deadline -and (-not (Test-Path -LiteralPath $ServerReady) -or -not (Test-Listener $ApiPort))) { if ($serverProcess.HasExited) { $details = (Get-Content $ServerOutLog, $ServerErrLog -ErrorAction SilentlyContinue) -join [Environment]::NewLine - throw "mock Ollama server stopped before listening: $details" + throw "mock inference server stopped before listening: $details" } Start-Sleep -Milliseconds 200 } if (-not (Test-Listener $ApiPort)) { - throw "mock Ollama server did not listen on port $ApiPort within 15 seconds" + throw "mock inference server did not listen on port $ApiPort within 15 seconds" } $env:APPDATA = Join-Path $StageDir "appdata" $env:LOCALAPPDATA = Join-Path $StageDir "localappdata" New-Item -ItemType Directory -Force -Path $env:APPDATA, $env:LOCALAPPDATA | Out-Null - $runner = Join-Path $StageDir "run-ollama-test.ps1" - $share = Join-Path $StageDir "share" - $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $runner ` - -Mock ` - -GatewayPath $GatewayPath ` - -CliPath $CliPath ` - -ShareDir $share ` - -OllamaPort $ApiPort ` - -Model "openshell-ci-mock" ` - -Prompt "Return the CI mock response." ` - -KeepArtifacts 2>&1 + $ollamaRunner = Join-Path $StageDir "run-ollama-test.ps1" + $ollamaShare = Join-Path $StageDir "ollama-share" + $ollamaArgs = @( + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "-File", $ollamaRunner, + "-Mock", + "-GatewayPath", $GatewayPath, + "-CliPath", $CliPath, + "-ShareDir", $ollamaShare, + "-OllamaPort", "$ApiPort", + "-Model", "openshell-ci-mock", + "-Prompt", "Return the CI mock response.", + "-KeepArtifacts" + ) + $output = & powershell.exe @ollamaArgs 2>&1 $exitCode = $LASTEXITCODE $output | ForEach-Object { Write-Host $_ } if ($exitCode -ne 0) { throw "shipped Ollama demo failed in mock mode (exit $exitCode)" } - $resultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-ollama-*" | + $ollamaResultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-ollama-*" | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1 - if ($null -eq $resultDir) { throw "Ollama demo did not produce a results directory" } + if ($null -eq $ollamaResultDir) { throw "Ollama demo did not produce a results directory" } - $summary = Get-Content -LiteralPath (Join-Path $resultDir.FullName "summary.txt") -Raw - if ($summary -notmatch '(?m)^verdict=PASS\s*$' -or $summary -notmatch '(?m)^mode=mock-wiring\s*$') { - throw "Ollama demo summary did not report a mock-wiring PASS: $summary" + $ollamaSummary = Get-Content -LiteralPath (Join-Path $ollamaResultDir.FullName "summary.txt") -Raw + if ($ollamaSummary -notmatch '(?m)^verdict=PASS\s*$' -or $ollamaSummary -notmatch '(?m)^mode=mock-wiring\s*$') { + throw "Ollama demo summary did not report a mock-wiring PASS: $ollamaSummary" } - $response = Get-Content -LiteralPath (Join-Path $resultDir.FullName "ollama-response.json") -Raw | ConvertFrom-Json - if ($response.response -ne "Hello from the CI mock.") { + $ollamaResponse = Get-Content -LiteralPath (Join-Path $ollamaResultDir.FullName "ollama-response.json") -Raw | ConvertFrom-Json + if ($ollamaResponse.response -ne "Hello from the CI mock.") { throw "Ollama demo did not retain the expected mock completion" } - $requests = Get-Content -LiteralPath $RequestLog -Raw - if ($requests -notmatch 'GET /api/tags ' -or $requests -notmatch 'POST /api/generate ') { - throw "mock Ollama server did not observe both demo API calls: $requests" + $env:NV_API_KEY = $MockToken + $cloudRunner = Join-Path $StageDir "run-inference-test.ps1" + $cloudShare = Join-Path $StageDir "cloud-share" + $cloudApiUrl = "http://127.0.0.1:$ApiPort/v1/chat/completions" + $cloudArgs = @( + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "-File", $cloudRunner, + "-Mock", + "-GatewayPath", $GatewayPath, + "-CliPath", $CliPath, + "-ShareDir", $cloudShare, + "-ApiUrl", $cloudApiUrl, + "-Model", "openshell-ci-mock", + "-Prompt", "Return the CI mock response.", + "-KeepArtifacts" + ) + $output = & powershell.exe @cloudArgs 2>&1 + $exitCode = $LASTEXITCODE + $output | ForEach-Object { Write-Host $_ } + if ($exitCode -ne 0) { + throw "shipped cloud-inference demo failed in mock mode (exit $exitCode)" + } + + $cloudResultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-inference-*" | + Sort-Object LastWriteTimeUtc -Descending | + Select-Object -First 1 + if ($null -eq $cloudResultDir) { throw "cloud-inference demo did not produce a results directory" } + + $cloudSummary = Get-Content -LiteralPath (Join-Path $cloudResultDir.FullName "summary.txt") -Raw + if ($cloudSummary -notmatch '(?m)^verdict=PASS\s*$' -or $cloudSummary -notmatch '(?m)^mode=mock-wiring\s*$') { + throw "cloud-inference demo summary did not report a mock-wiring PASS: $cloudSummary" + } + $cloudResponse = Get-Content -LiteralPath (Join-Path $cloudResultDir.FullName "inference-response.json") -Raw | ConvertFrom-Json + if ($cloudResponse.choices[0].message.content -ne "Hello from the CI mock.") { + throw "cloud-inference demo did not retain the expected mock completion" + } + + $requestLines = @([System.IO.File]::ReadAllLines($RequestLog)) + $expectedRequests = @( + @{ Pattern = '^GET /api/tags HTTP/\S+ authorization=absent$'; Count = 2; Description = 'host prerequisite and sandbox Ollama tag requests' }, + @{ Pattern = '^POST /api/generate HTTP/\S+ authorization=absent$'; Count = 1; Description = 'sandbox Ollama generation request' }, + @{ Pattern = '^POST /v1/chat/completions HTTP/\S+ authorization=synthetic$'; Count = 1; Description = 'sandbox cloud-inference request with the synthetic CI credential' } + ) + foreach ($expected in $expectedRequests) { + $actualCount = @($requestLines | Where-Object { $_ -match $expected.Pattern }).Count + if ($actualCount -ne $expected.Count) { + throw "mock inference server observed $actualCount $($expected.Description); expected $($expected.Count): $($requestLines -join '; ')" + } } $passed = $true - Write-Host "MXC Ollama example mock E2E passed for $Target" + Write-Host "MXC inference examples mock E2E passed for $Target" } catch { - Write-Host "MXC Ollama example mock E2E failed: $($_.Exception.Message)" -ForegroundColor Red + Write-Host "MXC inference examples mock E2E failed: $($_.Exception.Message)" -ForegroundColor Red Get-ChildItem -LiteralPath $StageDir -File -Recurse -Include "*.log", "summary.txt" -ErrorAction SilentlyContinue | ForEach-Object { Write-Host "--- $($_.FullName) ---" @@ -180,6 +241,11 @@ try { } finally { $env:APPDATA = $oldAppData $env:LOCALAPPDATA = $oldLocalAppData + if ([string]::IsNullOrWhiteSpace($oldNvApiKey)) { + Remove-Item Env:NV_API_KEY -ErrorAction SilentlyContinue + } else { + $env:NV_API_KEY = $oldNvApiKey + } if ($serverProcess -and -not $serverProcess.HasExited) { Stop-Process -Id $serverProcess.Id -Force -ErrorAction SilentlyContinue try { [void] $serverProcess.WaitForExit(5000) } catch {} @@ -187,6 +253,6 @@ try { if ($passed -and -not $KeepArtifacts -and (Test-Path -LiteralPath $StageDir)) { Remove-Item -LiteralPath $StageDir -Recurse -Force } else { - Write-Host "MXC Ollama example artifacts: $StageDir" + Write-Host "MXC inference example artifacts: $StageDir" } } diff --git a/tasks/scripts/windows-mxc-ollama-stub.ps1 b/tasks/scripts/windows-mxc-inference-stub.ps1 similarity index 71% rename from tasks/scripts/windows-mxc-ollama-stub.ps1 rename to tasks/scripts/windows-mxc-inference-stub.ps1 index 309777a6e7..a62d881e9b 100644 --- a/tasks/scripts/windows-mxc-ollama-stub.ps1 +++ b/tasks/scripts/windows-mxc-inference-stub.ps1 @@ -11,7 +11,10 @@ param( [string] $RequestLog, [Parameter(Mandatory = $true)] - [string] $ReadyPath + [string] $ReadyPath, + + [Parameter(Mandatory = $true)] + [string] $ExpectedBearerToken ) Set-StrictMode -Version Latest @@ -38,11 +41,14 @@ try { if ([string]::IsNullOrWhiteSpace($requestLine)) { continue } $contentLength = 0 + $authorization = "" while ($true) { $line = $reader.ReadLine() if ([string]::IsNullOrEmpty($line)) { break } if ($line -match '^Content-Length:\s*(\d+)\s*$') { $contentLength = [int] $Matches[1] + } elseif ($line -match '^Authorization:\s*(.+)\s*$') { + $authorization = $Matches[1] } } if ($contentLength -gt 0) { @@ -55,9 +61,20 @@ try { } } - [System.IO.File]::AppendAllText($RequestLog, "$requestLine`r`n", $utf8) $parts = $requestLine.Split(' ') $path = if ($parts.Count -ge 2) { $parts[1] } else { "/" } + $authorizationStatus = if ([string]::IsNullOrWhiteSpace($authorization)) { + "absent" + } elseif ($authorization -ceq "Bearer $ExpectedBearerToken") { + "synthetic" + } else { + "mismatch" + } + [System.IO.File]::AppendAllText( + $RequestLog, + "$requestLine authorization=$authorizationStatus`r`n", + $utf8 + ) switch ($path) { "/api/tags" { $status = "200 OK" @@ -67,6 +84,15 @@ try { $status = "200 OK" $body = '{"model":"openshell-ci-mock","response":"Hello from the CI mock.","done":true}' } + "/v1/chat/completions" { + if ($authorizationStatus -eq "synthetic") { + $status = "200 OK" + $body = '{"choices":[{"message":{"role":"assistant","content":"Hello from the CI mock."}}]}' + } else { + $status = "401 Unauthorized" + $body = '{"error":{"message":"invalid mock credential"}}' + } + } default { $status = "404 Not Found" $body = '{"error":"not found"}' diff --git a/tasks/windows.toml b/tasks/windows.toml index 6b1bd4d8a5..c533dd53a9 100644 --- a/tasks/windows.toml +++ b/tasks/windows.toml @@ -109,12 +109,12 @@ description = "Run MXC Tier-3 e2e scenario runner in mock/wiring-only mode (no r 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:ollama-mock:x64"] -description = "Run the shipped MXC Ollama demo against the in-process wxc mock and a local API stub on Windows x64" +["windows:e2e:mxc:inference-mock:x64"] +description = "Run the shipped MXC inference demos against the in-process wxc mock and a local API stub on Windows x64" run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" -run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-ollama-e2e.ps1 -Target x86_64-pc-windows-msvc" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-inference-examples-e2e.ps1 -Target x86_64-pc-windows-msvc" -["windows:e2e:mxc:ollama-mock:arm64"] -description = "Run the shipped MXC Ollama demo against the in-process wxc mock and a local API stub on Windows ARM64" +["windows:e2e:mxc:inference-mock:arm64"] +description = "Run the shipped MXC inference demos against the in-process wxc mock and a local API stub on Windows ARM64" run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1" -run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-ollama-e2e.ps1 -Target aarch64-pc-windows-msvc" +run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-mxc-inference-examples-e2e.ps1 -Target aarch64-pc-windows-msvc" From 80d9f8698aa3027969c387023ac73bf3879fd7cf Mon Sep 17 00:00:00 2001 From: Prekshi Vyas Date: Mon, 28 Sep 2026 19:13:49 -0700 Subject: [PATCH 3/3] fix(mxc): preserve executable extension resolution Signed-off-by: Prekshi Vyas --- crates/openshell-driver-mxc/README.md | 12 ++++++------ crates/openshell-driver-mxc/src/driver.rs | 14 ++++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md index c4f1bfefdd..7e88e24f79 100644 --- a/crates/openshell-driver-mxc/README.md +++ b/crates/openshell-driver-mxc/README.md @@ -59,7 +59,7 @@ pc_relay_spawner_path = "" pc_relay_target_port = 0 # processContainer only: env-inheritance tier for the launched process # (safest first): default is a minimal Windows CreateProcessW bootstrap set -# (SYSTEMROOT/WINDIR/PATH/COMSPEC/LOCALAPPDATA); pc_minimal_env starts from an +# (SYSTEMROOT/WINDIR/PATH/PATHEXT/COMSPEC/LOCALAPPDATA); pc_minimal_env starts from an # EMPTY env for runtimes that need a fully curated per-sandbox environment. pc_minimal_env = false # processContainer only: compatibility fallback for unrestricted outbound TCP. @@ -203,11 +203,11 @@ environment variable, so workloads using it must pass `--cacert %CURL_CA_BUNDLE%` explicitly. Clients that honor the injected trust variables consume the same per-sandbox bundle directly. -The driver seeds only `SYSTEMROOT`, `WINDIR`, `PATH`, `COMSPEC`, and -`LOCALAPPDATA` from the gateway host before applying sandbox and TLS overrides, -so required Windows bootstrap values remain available without exposing the -gateway's full environment unless the gateway explicitly opts into another -environment mode. +The driver seeds only `SYSTEMROOT`, `WINDIR`, `PATH`, `PATHEXT`, `COMSPEC`, and +`LOCALAPPDATA` from the gateway host before applying sandbox and TLS overrides. +These values provide Windows process startup and command-resolution behavior +without exposing the gateway's full environment unless the gateway explicitly +opts into another environment mode. When governed egress is disabled, any network rule fails closed during sandbox creation. diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs index bf9ffe10e2..4621dcf919 100644 --- a/crates/openshell-driver-mxc/src/driver.rs +++ b/crates/openshell-driver-mxc/src/driver.rs @@ -211,8 +211,8 @@ pub struct MxcComputeConfig { /// passed to the process. /// Use for agents like Node.js that fail with `STATUS_DLL_INIT_FAILED` /// when unrecognised host env vars are present; the caller is then - /// responsible for supplying `SYSTEMROOT`/`WINDIR`/`PATH`/`COMSPEC`/ - /// `LOCALAPPDATA` through `sandbox create --env/--env-from` if needed + /// responsible for supplying `SYSTEMROOT`/`WINDIR`/`PATH`/`PATHEXT`/ + /// `COMSPEC`/`LOCALAPPDATA` through `sandbox create --env/--env-from` if needed /// (`CreateProcessW` itself won't succeed without `LOCALAPPDATA` at /// least -- see `MINIMAL_WINDOWS_BOOTSTRAP_ENV`). /// @@ -665,8 +665,14 @@ fn allocate_sandbox_proxy_addr( /// are secrets, so resolving them from the gateway host is safe; this is /// the per-sandbox environment layers on top of. See `pc_minimal_env` on /// `MxcComputeConfig` for the explicit empty-baseline option. -const MINIMAL_WINDOWS_BOOTSTRAP_ENV: [&str; 5] = - ["SYSTEMROOT", "WINDIR", "PATH", "COMSPEC", "LOCALAPPDATA"]; +const MINIMAL_WINDOWS_BOOTSTRAP_ENV: [&str; 6] = [ + "SYSTEMROOT", + "WINDIR", + "PATH", + "PATHEXT", + "COMSPEC", + "LOCALAPPDATA", +]; const TLS_ENV_KEYS: [&str; 6] = [ "NODE_EXTRA_CA_CERTS",