From d8ecfb1ba0f3ad08bbb151e275eccf2b0475dee3 Mon Sep 17 00:00:00 2001 From: nv-vankit Date: Mon, 5 Oct 2026 01:40:25 -0700 Subject: [PATCH 1/4] fix(mxc): refresh stale demo gateway aliases (NVBug 6870039) Signed-off-by: nv-vankit --- .../examples/run-mxc-e2e.ps1 | 42 +++++++++++++++-- .../examples/run-ocsf-audit.ps1 | 45 ++++++++++++++++--- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 index 060b7c6f79..96b21ec788 100644 --- a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 +++ b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 @@ -144,6 +144,7 @@ function Invoke-NativeCaptured([string]$filePath, [string[]]$argumentList) { return @{ ExitCode = $process.ExitCode Output = @($stdout.Result, $stderr.Result) | Where-Object { $_ } + StdOut = $stdout.Result } } @@ -233,15 +234,48 @@ function Stop-Gw($p) { function Register-Cli { if ($script:registered) { return } - $env:OPENSHELL_GATEWAY = "" + Remove-Item Env:OPENSHELL_GATEWAY -ErrorAction SilentlyContinue + $expectedEndpoint = "http://127.0.0.1:$Port" $addResult = Invoke-NativeCaptured $cli @( - "gateway", "add", "http://127.0.0.1:$Port", "--local", "--name", $GatewayName + "gateway", "add", $expectedEndpoint, "--local", "--name", $GatewayName ) $addText = ($addResult.Output -join "`n") if ($addText) { $addResult.Output | ForEach-Object { Info $_ } } - if ($addResult.ExitCode -ne 0 -and $addText -notmatch '(?i)already exists') { - throw "gateway add failed (exit $($addResult.ExitCode)): $addText" + if ($addResult.ExitCode -ne 0) { + $listResult = Invoke-NativeCaptured $cli @("gateway", "list", "-o", "json") + if ($listResult.ExitCode -ne 0) { + throw "gateway add failed (exit $($addResult.ExitCode)): $addText; gateway list also failed: $($listResult.Output -join "`n")" + } + try { + $gateways = $listResult.StdOut | ConvertFrom-Json + } catch { + throw "gateway add failed (exit $($addResult.ExitCode)): $addText; could not parse gateway list JSON: $($_.Exception.Message)" + } + $existing = $gateways | Where-Object { $_.name -eq $GatewayName } | Select-Object -First 1 + if ($null -eq $existing) { + throw "gateway add failed (exit $($addResult.ExitCode)): $addText" + } + + $existingEndpoint = ([string]$existing.endpoint).TrimEnd('/') + $normalizedExpected = $expectedEndpoint.TrimEnd('/') + if ($existingEndpoint -ne $normalizedExpected) { + Info "'$GatewayName' points at '$existingEndpoint' instead of '$normalizedExpected'; replacing the stale registration" + $removeResult = Invoke-NativeCaptured $cli @("gateway", "remove", $GatewayName) + if ($removeResult.Output) { $removeResult.Output | ForEach-Object { Info $_ } } + if ($removeResult.ExitCode -ne 0) { + throw "failed to remove stale gateway '$GatewayName' (exit $($removeResult.ExitCode)): $($removeResult.Output -join "`n")" + } + $addResult = Invoke-NativeCaptured $cli @( + "gateway", "add", $expectedEndpoint, "--local", "--name", $GatewayName + ) + if ($addResult.Output) { $addResult.Output | ForEach-Object { Info $_ } } + if ($addResult.ExitCode -ne 0) { + throw "gateway add failed after removing stale registration (exit $($addResult.ExitCode)): $($addResult.Output -join "`n")" + } + } else { + Info "'$GatewayName' already points at '$normalizedExpected'; reusing it" + } } $selectResult = Invoke-NativeCaptured $cli @("gateway", "select", $GatewayName) diff --git a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 index c410b1df8e..e0d06a5947 100644 --- a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 +++ b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 @@ -129,7 +129,11 @@ function Invoke-Cli([string[]]$CommandArgs, [switch]$AllowFailure) { if (-not $AllowFailure -and $process.ExitCode -ne 0) { throw "openshell $($CommandArgs -join ' ') failed (exit $($process.ExitCode)): $text" } - return @{ ExitCode = $process.ExitCode; Text = $text } + return @{ + ExitCode = $process.ExitCode + Text = $text + StdOut = $stdout.Result + } } function Resolve-Artifact([string]$explicit, [string]$leaf) { @@ -319,11 +323,42 @@ try { # 9. Register CLI -> gateway. Step "Register CLI -> gateway" - $env:OPENSHELL_GATEWAY = "" - $gatewayAdd = Invoke-Cli @("gateway", "add", "http://127.0.0.1:$Port", "--local", "--name", $GatewayName) -AllowFailure + Remove-Item Env:OPENSHELL_GATEWAY -ErrorAction SilentlyContinue + $expectedEndpoint = "http://127.0.0.1:$Port" + $gatewayAdd = Invoke-Cli @("gateway", "add", $expectedEndpoint, "--local", "--name", $GatewayName) -AllowFailure if ($gatewayAdd.Text) { Info $gatewayAdd.Text } - if ($gatewayAdd.ExitCode -ne 0 -and $gatewayAdd.Text -notmatch '(?i)already exists') { - throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text)" + if ($gatewayAdd.ExitCode -ne 0) { + $gatewayList = Invoke-Cli @("gateway", "list", "-o", "json") -AllowFailure + if ($gatewayList.ExitCode -ne 0) { + throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text); gateway list also failed: $($gatewayList.Text)" + } + try { + $gateways = $gatewayList.StdOut | ConvertFrom-Json + } catch { + throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text); could not parse gateway list JSON: $($_.Exception.Message)" + } + $existing = $gateways | Where-Object { $_.name -eq $GatewayName } | Select-Object -First 1 + if ($null -eq $existing) { + throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text)" + } + + $existingEndpoint = ([string]$existing.endpoint).TrimEnd('/') + $normalizedExpected = $expectedEndpoint.TrimEnd('/') + if ($existingEndpoint -ne $normalizedExpected) { + Info "'$GatewayName' points at '$existingEndpoint' instead of '$normalizedExpected'; replacing the stale registration" + $gatewayRemove = Invoke-Cli @("gateway", "remove", $GatewayName) -AllowFailure + if ($gatewayRemove.Text) { Info $gatewayRemove.Text } + if ($gatewayRemove.ExitCode -ne 0) { + throw "failed to remove stale gateway '$GatewayName' (exit $($gatewayRemove.ExitCode)): $($gatewayRemove.Text)" + } + $gatewayAdd = Invoke-Cli @("gateway", "add", $expectedEndpoint, "--local", "--name", $GatewayName) -AllowFailure + if ($gatewayAdd.Text) { Info $gatewayAdd.Text } + if ($gatewayAdd.ExitCode -ne 0) { + throw "gateway registration failed after removing stale registration (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text)" + } + } else { + Info "'$GatewayName' already points at '$normalizedExpected'; reusing it" + } } $gatewaySelect = Invoke-Cli @("gateway", "select", $GatewayName) if ($gatewaySelect.Text) { Info $gatewaySelect.Text } From d4f11c40b219b3aadaac7bf0fc48c214b135a14e Mon Sep 17 00:00:00 2001 From: nv-vankit Date: Mon, 5 Oct 2026 23:47:40 -0700 Subject: [PATCH 2/4] fix(mxc): isolate demo CLI gateway state Signed-off-by: nv-vankit --- .../examples/run-mxc-e2e.ps1 | 89 +++++++++++-------- .../examples/run-ocsf-audit.ps1 | 87 +++++++++++------- .../tests/demo_examples.rs | 17 ++++ .../tests/windows_e2e_harness.rs | 20 +++++ tasks/scripts/windows-mxc-aggregate-e2e.ps1 | 26 +++++- tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 | 26 +++++- 6 files changed, 195 insertions(+), 70 deletions(-) diff --git a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 index 96b21ec788..633d8b1a2d 100644 --- a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 +++ b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 @@ -144,7 +144,6 @@ function Invoke-NativeCaptured([string]$filePath, [string[]]$argumentList) { return @{ ExitCode = $process.ExitCode Output = @($stdout.Result, $stderr.Result) | Where-Object { $_ } - StdOut = $stdout.Result } } @@ -177,9 +176,58 @@ $script:registered = $false $tomlBase = $null $gwLog = $null $gwErrLog = $null +$cliStateRoot = $null +$cliEnvironmentSnapshot = @{} +$cliEnvironmentNames = @( + "APPDATA", + "LOCALAPPDATA", + "XDG_CONFIG_HOME", + "XDG_STATE_HOME", + "XDG_DATA_HOME", + "OPENSHELL_GATEWAY", + "OPENSHELL_GATEWAY_ENDPOINT", + "OPENSHELL_GATEWAY_INSECURE", + "OPENSHELL_GATEWAY_CONFIG", + "OPENSHELL_GATEWAY_NAME" +) # --- Helpers ------------------------------------------------------------------ +function Enter-IsolatedCliEnvironment { + foreach ($name in $cliEnvironmentNames) { + $script:cliEnvironmentSnapshot[$name] = [Environment]::GetEnvironmentVariable($name, "Process") + } + $script:cliStateRoot = Join-Path ([IO.Path]::GetTempPath()) "openshell-mxc-e2e-cli-$PID-$([Guid]::NewGuid().ToString('N'))" + $isolatedPaths = @{ + APPDATA = Join-Path $script:cliStateRoot "appdata" + LOCALAPPDATA = Join-Path $script:cliStateRoot "localappdata" + XDG_CONFIG_HOME = Join-Path $script:cliStateRoot "xdg-config" + XDG_STATE_HOME = Join-Path $script:cliStateRoot "xdg-state" + XDG_DATA_HOME = Join-Path $script:cliStateRoot "xdg-data" + } + try { + New-Item -ItemType Directory -Force -Path @($isolatedPaths.Values) | Out-Null + foreach ($entry in $isolatedPaths.GetEnumerator()) { + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, "Process") + } + foreach ($name in $cliEnvironmentNames | Where-Object { -not $isolatedPaths.ContainsKey($_) }) { + [Environment]::SetEnvironmentVariable($name, $null, "Process") + } + } catch { + Exit-IsolatedCliEnvironment + throw + } +} + +function Exit-IsolatedCliEnvironment { + foreach ($name in $cliEnvironmentNames) { + [Environment]::SetEnvironmentVariable($name, $script:cliEnvironmentSnapshot[$name], "Process") + } + if ($script:cliStateRoot -and (Test-Path -LiteralPath $script:cliStateRoot)) { + Remove-Item -LiteralPath $script:cliStateRoot -Recurse -Force -ErrorAction SilentlyContinue + } +} + # Render host-runtime settings from the pristine base. Sandbox workload # settings are create-time driver config, not gateway-wide TOML. function Render-Toml { @@ -234,7 +282,6 @@ function Stop-Gw($p) { function Register-Cli { if ($script:registered) { return } - Remove-Item Env:OPENSHELL_GATEWAY -ErrorAction SilentlyContinue $expectedEndpoint = "http://127.0.0.1:$Port" $addResult = Invoke-NativeCaptured $cli @( @@ -243,39 +290,7 @@ function Register-Cli { $addText = ($addResult.Output -join "`n") if ($addText) { $addResult.Output | ForEach-Object { Info $_ } } if ($addResult.ExitCode -ne 0) { - $listResult = Invoke-NativeCaptured $cli @("gateway", "list", "-o", "json") - if ($listResult.ExitCode -ne 0) { - throw "gateway add failed (exit $($addResult.ExitCode)): $addText; gateway list also failed: $($listResult.Output -join "`n")" - } - try { - $gateways = $listResult.StdOut | ConvertFrom-Json - } catch { - throw "gateway add failed (exit $($addResult.ExitCode)): $addText; could not parse gateway list JSON: $($_.Exception.Message)" - } - $existing = $gateways | Where-Object { $_.name -eq $GatewayName } | Select-Object -First 1 - if ($null -eq $existing) { - throw "gateway add failed (exit $($addResult.ExitCode)): $addText" - } - - $existingEndpoint = ([string]$existing.endpoint).TrimEnd('/') - $normalizedExpected = $expectedEndpoint.TrimEnd('/') - if ($existingEndpoint -ne $normalizedExpected) { - Info "'$GatewayName' points at '$existingEndpoint' instead of '$normalizedExpected'; replacing the stale registration" - $removeResult = Invoke-NativeCaptured $cli @("gateway", "remove", $GatewayName) - if ($removeResult.Output) { $removeResult.Output | ForEach-Object { Info $_ } } - if ($removeResult.ExitCode -ne 0) { - throw "failed to remove stale gateway '$GatewayName' (exit $($removeResult.ExitCode)): $($removeResult.Output -join "`n")" - } - $addResult = Invoke-NativeCaptured $cli @( - "gateway", "add", $expectedEndpoint, "--local", "--name", $GatewayName - ) - if ($addResult.Output) { $addResult.Output | ForEach-Object { Info $_ } } - if ($addResult.ExitCode -ne 0) { - throw "gateway add failed after removing stale registration (exit $($addResult.ExitCode)): $($addResult.Output -join "`n")" - } - } else { - Info "'$GatewayName' already points at '$normalizedExpected'; reusing it" - } + throw "gateway add failed (exit $($addResult.ExitCode)): $addText" } $selectResult = Invoke-NativeCaptured $cli @("gateway", "select", $GatewayName) @@ -413,6 +428,8 @@ $backendProbe = @{ Live = $false; Reason = "not probed" } $runId = Get-Date -Format 'MMddHHmmss' try { + Enter-IsolatedCliEnvironment + # Start the transcript inside the guarded region so a Start-Transcript failure # is caught and the results bundle is still produced. Pre-flight runs # immediately below, so the transcript still captures the whole run. @@ -792,6 +809,8 @@ catch { Bad "harness error: $harnessError" } finally { + Exit-IsolatedCliEnvironment + # --- Summary + results bundle --------------------------------------------- Step "Summary" $results | Format-Table -AutoSize diff --git a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 index e0d06a5947..94e0e75e85 100644 --- a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 +++ b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 @@ -132,7 +132,6 @@ function Invoke-Cli([string[]]$CommandArgs, [switch]$AllowFailure) { return @{ ExitCode = $process.ExitCode Text = $text - StdOut = $stdout.Result } } @@ -156,6 +155,56 @@ function Get-MxcEtwSessions { } } +$cliStateRoot = $null +$cliEnvironmentSnapshot = @{} +$cliEnvironmentNames = @( + "APPDATA", + "LOCALAPPDATA", + "XDG_CONFIG_HOME", + "XDG_STATE_HOME", + "XDG_DATA_HOME", + "OPENSHELL_GATEWAY", + "OPENSHELL_GATEWAY_ENDPOINT", + "OPENSHELL_GATEWAY_INSECURE", + "OPENSHELL_GATEWAY_CONFIG", + "OPENSHELL_GATEWAY_NAME" +) + +function Enter-IsolatedCliEnvironment { + foreach ($name in $cliEnvironmentNames) { + $script:cliEnvironmentSnapshot[$name] = [Environment]::GetEnvironmentVariable($name, "Process") + } + $script:cliStateRoot = Join-Path ([IO.Path]::GetTempPath()) "openshell-mxc-ocsf-cli-$PID-$([Guid]::NewGuid().ToString('N'))" + $isolatedPaths = @{ + APPDATA = Join-Path $script:cliStateRoot "appdata" + LOCALAPPDATA = Join-Path $script:cliStateRoot "localappdata" + XDG_CONFIG_HOME = Join-Path $script:cliStateRoot "xdg-config" + XDG_STATE_HOME = Join-Path $script:cliStateRoot "xdg-state" + XDG_DATA_HOME = Join-Path $script:cliStateRoot "xdg-data" + } + try { + New-Item -ItemType Directory -Force -Path @($isolatedPaths.Values) | Out-Null + foreach ($entry in $isolatedPaths.GetEnumerator()) { + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, "Process") + } + foreach ($name in $cliEnvironmentNames | Where-Object { -not $isolatedPaths.ContainsKey($_) }) { + [Environment]::SetEnvironmentVariable($name, $null, "Process") + } + } catch { + Exit-IsolatedCliEnvironment + throw + } +} + +function Exit-IsolatedCliEnvironment { + foreach ($name in $cliEnvironmentNames) { + [Environment]::SetEnvironmentVariable($name, $script:cliEnvironmentSnapshot[$name], "Process") + } + if ($script:cliStateRoot -and (Test-Path -LiteralPath $script:cliStateRoot)) { + Remove-Item -LiteralPath $script:cliStateRoot -Recurse -Force -ErrorAction SilentlyContinue + } +} + $gateway = Resolve-Artifact $GatewayPath "openshell-gateway.exe" $cli = Resolve-Artifact $CliPath "openshell.exe" $policySrc = Join-Path $here "ocsf-audit.yaml" @@ -171,6 +220,8 @@ $proxyOn = -not $NoProxy $oldMockWxc = $env:OPENSHELL_MXC_MOCK_WXC try { + Enter-IsolatedCliEnvironment + # 1. Validate artifacts + privilege. Step "Validate package artifacts" foreach ($f in @($gateway, $cli, $policySrc, $tomlSrc)) { @@ -323,42 +374,11 @@ try { # 9. Register CLI -> gateway. Step "Register CLI -> gateway" - Remove-Item Env:OPENSHELL_GATEWAY -ErrorAction SilentlyContinue $expectedEndpoint = "http://127.0.0.1:$Port" $gatewayAdd = Invoke-Cli @("gateway", "add", $expectedEndpoint, "--local", "--name", $GatewayName) -AllowFailure if ($gatewayAdd.Text) { Info $gatewayAdd.Text } if ($gatewayAdd.ExitCode -ne 0) { - $gatewayList = Invoke-Cli @("gateway", "list", "-o", "json") -AllowFailure - if ($gatewayList.ExitCode -ne 0) { - throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text); gateway list also failed: $($gatewayList.Text)" - } - try { - $gateways = $gatewayList.StdOut | ConvertFrom-Json - } catch { - throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text); could not parse gateway list JSON: $($_.Exception.Message)" - } - $existing = $gateways | Where-Object { $_.name -eq $GatewayName } | Select-Object -First 1 - if ($null -eq $existing) { - throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text)" - } - - $existingEndpoint = ([string]$existing.endpoint).TrimEnd('/') - $normalizedExpected = $expectedEndpoint.TrimEnd('/') - if ($existingEndpoint -ne $normalizedExpected) { - Info "'$GatewayName' points at '$existingEndpoint' instead of '$normalizedExpected'; replacing the stale registration" - $gatewayRemove = Invoke-Cli @("gateway", "remove", $GatewayName) -AllowFailure - if ($gatewayRemove.Text) { Info $gatewayRemove.Text } - if ($gatewayRemove.ExitCode -ne 0) { - throw "failed to remove stale gateway '$GatewayName' (exit $($gatewayRemove.ExitCode)): $($gatewayRemove.Text)" - } - $gatewayAdd = Invoke-Cli @("gateway", "add", $expectedEndpoint, "--local", "--name", $GatewayName) -AllowFailure - if ($gatewayAdd.Text) { Info $gatewayAdd.Text } - if ($gatewayAdd.ExitCode -ne 0) { - throw "gateway registration failed after removing stale registration (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text)" - } - } else { - Info "'$GatewayName' already points at '$normalizedExpected'; reusing it" - } + throw "gateway registration failed (exit $($gatewayAdd.ExitCode)): $($gatewayAdd.Text)" } $gatewaySelect = Invoke-Cli @("gateway", "select", $GatewayName) if ($gatewaySelect.Text) { Info $gatewaySelect.Text } @@ -417,6 +437,7 @@ finally { } else { $env:OPENSHELL_MXC_MOCK_WXC = $oldMockWxc } + Exit-IsolatedCliEnvironment # ---- summarise the OCSF audit trail -------------------------------------- $logText = @() diff --git a/crates/openshell-driver-mxc/tests/demo_examples.rs b/crates/openshell-driver-mxc/tests/demo_examples.rs index 5fb7f33d69..f902fb7dbf 100644 --- a/crates/openshell-driver-mxc/tests/demo_examples.rs +++ b/crates/openshell-driver-mxc/tests/demo_examples.rs @@ -137,6 +137,23 @@ fn shipped_aggregate_e2e_assets_support_mock_wiring_validation() { assert!(runner.contains("not evidence of native MXC or OS enforcement")); } +#[test] +fn shipped_runners_isolate_cli_state_and_gateway_overrides() { + for name in ["run-mxc-e2e.ps1", "run-ocsf-audit.ps1"] { + let runner = read_example(name); + for required in [ + "Enter-IsolatedCliEnvironment", + "Exit-IsolatedCliEnvironment", + "APPDATA", + "LOCALAPPDATA", + "OPENSHELL_GATEWAY", + "OPENSHELL_GATEWAY_ENDPOINT", + ] { + assert!(runner.contains(required), "{name} is missing {required}"); + } + } +} + #[test] fn shipped_audit_and_websocket_configs_use_current_schema() { for name in ["mxc-ocsf-audit.toml", "mxc-ws-gateway.toml"] { diff --git a/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs b/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs index e9c4852bfe..41d756b4b6 100644 --- a/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs +++ b/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs @@ -81,6 +81,26 @@ foreach ($name in $names) { ); } +#[test] +fn aggregate_and_ocsf_harnesses_preserve_caller_gateway_state() { + let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); + for name in [ + "windows-mxc-aggregate-e2e.ps1", + "windows-mxc-ocsf-audit-e2e.ps1", + ] { + let source = std::fs::read_to_string(repo_root.join("tasks/scripts").join(name)) + .unwrap_or_else(|error| panic!("failed to read {name}: {error}")); + for required in [ + "$sentinelEndpoint", + "OPENSHELL_GATEWAY_ENDPOINT", + "gateway list -o json", + "$sentinel.active", + ] { + assert!(source.contains(required), "{name} is missing {required}"); + } + } +} + #[test] fn aggregate_harness_exercises_a_staging_path_with_spaces() { let harness = Path::new(env!("CARGO_MANIFEST_DIR")) diff --git a/tasks/scripts/windows-mxc-aggregate-e2e.ps1 b/tasks/scripts/windows-mxc-aggregate-e2e.ps1 index 6ffaa6d37b..b872b237f2 100644 --- a/tasks/scripts/windows-mxc-aggregate-e2e.ps1 +++ b/tasks/scripts/windows-mxc-aggregate-e2e.ps1 @@ -97,19 +97,43 @@ try { $runner = Join-Path $StageDir "run-mxc-e2e.ps1" $demoDir = Join-Path $StageDir "demo" $port = Get-AvailablePort + $gatewayName = "openshell-mxc-aggregate-ci" + $sentinelEndpoint = "http://127.0.0.1:9" + $inheritedEndpoint = "http://127.0.0.1:1" + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $sentinelAdd = & $CliPath gateway add $sentinelEndpoint --local --name $gatewayName 2>&1 + $sentinelAddExitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($sentinelAddExitCode -ne 0) { + throw "failed to seed sentinel gateway '$gatewayName': $($sentinelAdd -join [Environment]::NewLine)" + } + $env:OPENSHELL_GATEWAY_ENDPOINT = $inheritedEndpoint $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $runner ` -Mock ` -GatewayPath $GatewayPath ` -CliPath $CliPath ` -DemoDir $demoDir ` -Port $port ` - -GatewayName "openshell-mxc-aggregate-ci" 2>&1 + -GatewayName $gatewayName 2>&1 $exitCode = $LASTEXITCODE $output | ForEach-Object { Write-Host $_ } if ($exitCode -ne 0) { throw "shipped aggregate MXC example failed in mock mode (exit $exitCode)" } + $gateways = & $CliPath gateway list -o json | ConvertFrom-Json + $sentinel = $gateways | Where-Object { $_.name -eq $gatewayName } | Select-Object -First 1 + if ($null -eq $sentinel -or $sentinel.endpoint -ne $sentinelEndpoint -or -not $sentinel.active) { + throw "aggregate runner changed the caller's sentinel gateway registration or active selection" + } + if ($env:OPENSHELL_GATEWAY_ENDPOINT -ne $inheritedEndpoint) { + throw "aggregate runner changed the caller's OPENSHELL_GATEWAY_ENDPOINT" + } + $resultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-e2e-*" | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1 diff --git a/tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 b/tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 index e94681fd1a..2e6eddb5b8 100644 --- a/tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 +++ b/tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 @@ -96,6 +96,21 @@ try { $runner = Join-Path $StageDir "run-ocsf-audit.ps1" $share = Join-Path $StageDir "share" $port = Get-AvailablePort + $gatewayName = "openshell-mxc-ocsf-ci" + $sentinelEndpoint = "http://127.0.0.1:9" + $inheritedEndpoint = "http://127.0.0.1:1" + $previousErrorActionPreference = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $sentinelAdd = & $CliPath gateway add $sentinelEndpoint --local --name $gatewayName 2>&1 + $sentinelAddExitCode = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorActionPreference + } + if ($sentinelAddExitCode -ne 0) { + throw "failed to seed sentinel gateway '$gatewayName': $($sentinelAdd -join [Environment]::NewLine)" + } + $env:OPENSHELL_GATEWAY_ENDPOINT = $inheritedEndpoint $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $runner ` -Mock ` -GatewayPath $GatewayPath ` @@ -103,13 +118,22 @@ try { -ShareDir $share ` -SandboxCount 1 ` -Port $port ` - -GatewayName "openshell-mxc-ocsf-ci" 2>&1 + -GatewayName $gatewayName 2>&1 $exitCode = $LASTEXITCODE $output | ForEach-Object { Write-Host $_ } if ($exitCode -ne 0) { throw "shipped OCSF audit example failed in mock mode (exit $exitCode)" } + $gateways = & $CliPath gateway list -o json | ConvertFrom-Json + $sentinel = $gateways | Where-Object { $_.name -eq $gatewayName } | Select-Object -First 1 + if ($null -eq $sentinel -or $sentinel.endpoint -ne $sentinelEndpoint -or -not $sentinel.active) { + throw "OCSF runner changed the caller's sentinel gateway registration or active selection" + } + if ($env:OPENSHELL_GATEWAY_ENDPOINT -ne $inheritedEndpoint) { + throw "OCSF runner changed the caller's OPENSHELL_GATEWAY_ENDPOINT" + } + $resultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-*" | Sort-Object LastWriteTimeUtc -Descending | Select-Object -First 1 From c573b22282226baaf65c851ba7af6a003f5a7a9c Mon Sep 17 00:00:00 2001 From: nv-vankit Date: Tue, 6 Oct 2026 22:25:14 -0700 Subject: [PATCH 3/4] fix(mxc): remove empty gateway overrides Signed-off-by: nv-vankit --- .../examples/run-mxc-e2e.ps1 | 9 ++- .../examples/run-ocsf-audit.ps1 | 9 ++- .../tests/demo_examples.rs | 1 + .../tests/windows_e2e_harness.rs | 71 +++++++++++++++++++ tasks/scripts/windows-mxc-aggregate-e2e.ps1 | 42 +++++------ tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 | 44 ++++++------ 6 files changed, 131 insertions(+), 45 deletions(-) diff --git a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 index 633d8b1a2d..7153f530be 100644 --- a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 +++ b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 @@ -211,7 +211,7 @@ function Enter-IsolatedCliEnvironment { [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, "Process") } foreach ($name in $cliEnvironmentNames | Where-Object { -not $isolatedPaths.ContainsKey($_) }) { - [Environment]::SetEnvironmentVariable($name, $null, "Process") + Remove-Item "Env:$name" -ErrorAction SilentlyContinue } } catch { Exit-IsolatedCliEnvironment @@ -221,7 +221,12 @@ function Enter-IsolatedCliEnvironment { function Exit-IsolatedCliEnvironment { foreach ($name in $cliEnvironmentNames) { - [Environment]::SetEnvironmentVariable($name, $script:cliEnvironmentSnapshot[$name], "Process") + $value = $script:cliEnvironmentSnapshot[$name] + if ($null -eq $value) { + Remove-Item "Env:$name" -ErrorAction SilentlyContinue + } else { + [Environment]::SetEnvironmentVariable($name, $value, "Process") + } } if ($script:cliStateRoot -and (Test-Path -LiteralPath $script:cliStateRoot)) { Remove-Item -LiteralPath $script:cliStateRoot -Recurse -Force -ErrorAction SilentlyContinue diff --git a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 index 94e0e75e85..438f36d9bf 100644 --- a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 +++ b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 @@ -188,7 +188,7 @@ function Enter-IsolatedCliEnvironment { [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, "Process") } foreach ($name in $cliEnvironmentNames | Where-Object { -not $isolatedPaths.ContainsKey($_) }) { - [Environment]::SetEnvironmentVariable($name, $null, "Process") + Remove-Item "Env:$name" -ErrorAction SilentlyContinue } } catch { Exit-IsolatedCliEnvironment @@ -198,7 +198,12 @@ function Enter-IsolatedCliEnvironment { function Exit-IsolatedCliEnvironment { foreach ($name in $cliEnvironmentNames) { - [Environment]::SetEnvironmentVariable($name, $script:cliEnvironmentSnapshot[$name], "Process") + $value = $script:cliEnvironmentSnapshot[$name] + if ($null -eq $value) { + Remove-Item "Env:$name" -ErrorAction SilentlyContinue + } else { + [Environment]::SetEnvironmentVariable($name, $value, "Process") + } } if ($script:cliStateRoot -and (Test-Path -LiteralPath $script:cliStateRoot)) { Remove-Item -LiteralPath $script:cliStateRoot -Recurse -Force -ErrorAction SilentlyContinue diff --git a/crates/openshell-driver-mxc/tests/demo_examples.rs b/crates/openshell-driver-mxc/tests/demo_examples.rs index f902fb7dbf..e473be781a 100644 --- a/crates/openshell-driver-mxc/tests/demo_examples.rs +++ b/crates/openshell-driver-mxc/tests/demo_examples.rs @@ -148,6 +148,7 @@ fn shipped_runners_isolate_cli_state_and_gateway_overrides() { "LOCALAPPDATA", "OPENSHELL_GATEWAY", "OPENSHELL_GATEWAY_ENDPOINT", + "Remove-Item \"Env:$name\"", ] { assert!(runner.contains(required), "{name} is missing {required}"); } diff --git a/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs b/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs index 41d756b4b6..66d72573d0 100644 --- a/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs +++ b/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs @@ -81,6 +81,76 @@ foreach ($name in $names) { ); } +#[test] +fn shipped_runners_remove_empty_overrides_in_both_powershell_versions() { + let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples"); + let script = r#" +$ErrorActionPreference = "Stop" +$tokens = $null +$errors = $null +$ast = [System.Management.Automation.Language.Parser]::ParseFile( + $env:OPENSHELL_MXC_RUNNER, + [ref]$tokens, + [ref]$errors +) +if ($errors.Count -gt 0) { throw ($errors.Message -join "; ") } +foreach ($functionName in @("Enter-IsolatedCliEnvironment", "Exit-IsolatedCliEnvironment")) { + $function = $ast.Find({ + param($node) + $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and + $node.Name -eq $functionName + }, $true) + if ($null -eq $function) { throw "$functionName was not found" } + Invoke-Expression $function.Extent.Text +} + +$cliStateRoot = $null +$cliEnvironmentSnapshot = @{} +$cliEnvironmentNames = @( + "APPDATA", "LOCALAPPDATA", "XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_DATA_HOME", + "OPENSHELL_GATEWAY", "OPENSHELL_GATEWAY_ENDPOINT", "OPENSHELL_GATEWAY_INSECURE", + "OPENSHELL_GATEWAY_CONFIG", "OPENSHELL_GATEWAY_NAME" +) +$env:OPENSHELL_GATEWAY_ENDPOINT = "http://127.0.0.1:1" +Remove-Item Env:OPENSHELL_GATEWAY -ErrorAction SilentlyContinue + +Enter-IsolatedCliEnvironment +try { + if (Test-Path Env:OPENSHELL_GATEWAY_ENDPOINT) { + throw "OPENSHELL_GATEWAY_ENDPOINT was not removed" + } + if (Test-Path Env:OPENSHELL_GATEWAY) { + throw "OPENSHELL_GATEWAY was not removed" + } +} finally { + Exit-IsolatedCliEnvironment +} + +if ($env:OPENSHELL_GATEWAY_ENDPOINT -ne "http://127.0.0.1:1") { + throw "OPENSHELL_GATEWAY_ENDPOINT was not restored" +} +if (Test-Path Env:OPENSHELL_GATEWAY) { + throw "null OPENSHELL_GATEWAY snapshot was restored as an empty variable" +} +"#; + + for runner in ["run-mxc-e2e.ps1", "run-ocsf-audit.ps1"] { + for shell in ["powershell.exe", "pwsh.exe"] { + let output = std::process::Command::new(shell) + .args(["-NoProfile", "-Command", script]) + .env("OPENSHELL_MXC_RUNNER", examples.join(runner)) + .output() + .unwrap_or_else(|error| panic!("failed to launch {shell}: {error}")); + assert!( + output.status.success(), + "{runner} environment isolation failed under {shell}:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + } +} + #[test] fn aggregate_and_ocsf_harnesses_preserve_caller_gateway_state() { let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../.."); @@ -95,6 +165,7 @@ fn aggregate_and_ocsf_harnesses_preserve_caller_gateway_state() { "OPENSHELL_GATEWAY_ENDPOINT", "gateway list -o json", "$sentinel.active", + "pwsh.exe", ] { assert!(source.contains(required), "{name} is missing {required}"); } diff --git a/tasks/scripts/windows-mxc-aggregate-e2e.ps1 b/tasks/scripts/windows-mxc-aggregate-e2e.ps1 index b872b237f2..46da26bbf1 100644 --- a/tasks/scripts/windows-mxc-aggregate-e2e.ps1 +++ b/tasks/scripts/windows-mxc-aggregate-e2e.ps1 @@ -96,7 +96,6 @@ try { $runner = Join-Path $StageDir "run-mxc-e2e.ps1" $demoDir = Join-Path $StageDir "demo" - $port = Get-AvailablePort $gatewayName = "openshell-mxc-aggregate-ci" $sentinelEndpoint = "http://127.0.0.1:9" $inheritedEndpoint = "http://127.0.0.1:1" @@ -112,26 +111,29 @@ try { throw "failed to seed sentinel gateway '$gatewayName': $($sentinelAdd -join [Environment]::NewLine)" } $env:OPENSHELL_GATEWAY_ENDPOINT = $inheritedEndpoint - $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $runner ` - -Mock ` - -GatewayPath $GatewayPath ` - -CliPath $CliPath ` - -DemoDir $demoDir ` - -Port $port ` - -GatewayName $gatewayName 2>&1 - $exitCode = $LASTEXITCODE - $output | ForEach-Object { Write-Host $_ } - if ($exitCode -ne 0) { - throw "shipped aggregate MXC example failed in mock mode (exit $exitCode)" - } + foreach ($powerShell in @("powershell.exe", "pwsh.exe")) { + $port = Get-AvailablePort + $output = & $powerShell -NoProfile -ExecutionPolicy Bypass -File $runner ` + -Mock ` + -GatewayPath $GatewayPath ` + -CliPath $CliPath ` + -DemoDir $demoDir ` + -Port $port ` + -GatewayName $gatewayName 2>&1 + $exitCode = $LASTEXITCODE + $output | ForEach-Object { Write-Host $_ } + if ($exitCode -ne 0) { + throw "shipped aggregate MXC example failed under $powerShell in mock mode (exit $exitCode)" + } - $gateways = & $CliPath gateway list -o json | ConvertFrom-Json - $sentinel = $gateways | Where-Object { $_.name -eq $gatewayName } | Select-Object -First 1 - if ($null -eq $sentinel -or $sentinel.endpoint -ne $sentinelEndpoint -or -not $sentinel.active) { - throw "aggregate runner changed the caller's sentinel gateway registration or active selection" - } - if ($env:OPENSHELL_GATEWAY_ENDPOINT -ne $inheritedEndpoint) { - throw "aggregate runner changed the caller's OPENSHELL_GATEWAY_ENDPOINT" + $gateways = & $CliPath gateway list -o json | ConvertFrom-Json + $sentinel = $gateways | Where-Object { $_.name -eq $gatewayName } | Select-Object -First 1 + if ($null -eq $sentinel -or $sentinel.endpoint -ne $sentinelEndpoint -or -not $sentinel.active) { + throw "aggregate runner changed the caller's sentinel gateway registration or active selection under $powerShell" + } + if ($env:OPENSHELL_GATEWAY_ENDPOINT -ne $inheritedEndpoint) { + throw "aggregate runner changed the caller's OPENSHELL_GATEWAY_ENDPOINT under $powerShell" + } } $resultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-e2e-*" | diff --git a/tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 b/tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 index 2e6eddb5b8..fc3f4f8cf8 100644 --- a/tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 +++ b/tasks/scripts/windows-mxc-ocsf-audit-e2e.ps1 @@ -95,7 +95,6 @@ try { $runner = Join-Path $StageDir "run-ocsf-audit.ps1" $share = Join-Path $StageDir "share" - $port = Get-AvailablePort $gatewayName = "openshell-mxc-ocsf-ci" $sentinelEndpoint = "http://127.0.0.1:9" $inheritedEndpoint = "http://127.0.0.1:1" @@ -111,27 +110,30 @@ try { throw "failed to seed sentinel gateway '$gatewayName': $($sentinelAdd -join [Environment]::NewLine)" } $env:OPENSHELL_GATEWAY_ENDPOINT = $inheritedEndpoint - $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $runner ` - -Mock ` - -GatewayPath $GatewayPath ` - -CliPath $CliPath ` - -ShareDir $share ` - -SandboxCount 1 ` - -Port $port ` - -GatewayName $gatewayName 2>&1 - $exitCode = $LASTEXITCODE - $output | ForEach-Object { Write-Host $_ } - if ($exitCode -ne 0) { - throw "shipped OCSF audit example failed in mock mode (exit $exitCode)" - } + foreach ($powerShell in @("powershell.exe", "pwsh.exe")) { + $port = Get-AvailablePort + $output = & $powerShell -NoProfile -ExecutionPolicy Bypass -File $runner ` + -Mock ` + -GatewayPath $GatewayPath ` + -CliPath $CliPath ` + -ShareDir $share ` + -SandboxCount 1 ` + -Port $port ` + -GatewayName $gatewayName 2>&1 + $exitCode = $LASTEXITCODE + $output | ForEach-Object { Write-Host $_ } + if ($exitCode -ne 0) { + throw "shipped OCSF audit example failed under $powerShell in mock mode (exit $exitCode)" + } - $gateways = & $CliPath gateway list -o json | ConvertFrom-Json - $sentinel = $gateways | Where-Object { $_.name -eq $gatewayName } | Select-Object -First 1 - if ($null -eq $sentinel -or $sentinel.endpoint -ne $sentinelEndpoint -or -not $sentinel.active) { - throw "OCSF runner changed the caller's sentinel gateway registration or active selection" - } - if ($env:OPENSHELL_GATEWAY_ENDPOINT -ne $inheritedEndpoint) { - throw "OCSF runner changed the caller's OPENSHELL_GATEWAY_ENDPOINT" + $gateways = & $CliPath gateway list -o json | ConvertFrom-Json + $sentinel = $gateways | Where-Object { $_.name -eq $gatewayName } | Select-Object -First 1 + if ($null -eq $sentinel -or $sentinel.endpoint -ne $sentinelEndpoint -or -not $sentinel.active) { + throw "OCSF runner changed the caller's sentinel gateway registration or active selection under $powerShell" + } + if ($env:OPENSHELL_GATEWAY_ENDPOINT -ne $inheritedEndpoint) { + throw "OCSF runner changed the caller's OPENSHELL_GATEWAY_ENDPOINT under $powerShell" + } } $resultDir = Get-ChildItem -LiteralPath $StageDir -Directory -Filter "results-*" | From 13ace6fc690fc886fd7ed944cdcf8aa6f61e7001 Mon Sep 17 00:00:00 2001 From: Shailendra Singh Date: Wed, 7 Oct 2026 11:58:05 -0700 Subject: [PATCH 4/4] fix(mxc): preserve exact runner environment state Signed-off-by: Shailendra Singh --- .../examples/run-mxc-e2e.ps1 | 61 +++++++-- .../examples/run-ocsf-audit.ps1 | 57 +++++++- .../tests/demo_examples.rs | 1 + .../tests/windows_e2e_harness.rs | 123 ++++++++++++------ tasks/scripts/windows-mxc-e2e-environment.ps1 | 80 +++++++++--- 5 files changed, 247 insertions(+), 75 deletions(-) diff --git a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 index 7153f530be..f8853beeb6 100644 --- a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 +++ b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 @@ -193,9 +193,55 @@ $cliEnvironmentNames = @( # --- Helpers ------------------------------------------------------------------ +function Set-ProcessEnvironmentVariableExact { + param( + [Parameter(Mandatory = $true)] + [string] $Name, + [Parameter(Mandatory = $true)] + [bool] $Exists, + [AllowNull()] + [string] $Value + ) + + if (-not $Exists) { + Remove-Item "Env:$Name" -ErrorAction SilentlyContinue + return + } + + if ($Value.Length -eq 0) { + # Windows PowerShell 5.1 maps an empty value passed through + # Environment.SetEnvironmentVariable to deletion. Call Win32 directly + # so an inherited empty entry remains distinguishable from absence. + if (-not ("OpenShellMxcProcessEnvironmentNative" -as [type])) { + Add-Type -TypeDefinition @' +using System.Runtime.InteropServices; + +public static class OpenShellMxcProcessEnvironmentNative +{ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetEnvironmentVariable(string name, string value); +} +'@ + } + if (-not [OpenShellMxcProcessEnvironmentNative]::SetEnvironmentVariable($Name, [string]::Empty)) { + $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "failed to restore empty process environment variable '$Name' (Win32 error $errorCode)" + } + return + } + + [Environment]::SetEnvironmentVariable($Name, $Value, "Process") +} + function Enter-IsolatedCliEnvironment { + $processEnvironment = [Environment]::GetEnvironmentVariables("Process") foreach ($name in $cliEnvironmentNames) { - $script:cliEnvironmentSnapshot[$name] = [Environment]::GetEnvironmentVariable($name, "Process") + $exists = $processEnvironment.Contains($name) + $script:cliEnvironmentSnapshot[$name] = [pscustomobject]@{ + Exists = $exists + Value = if ($exists) { [string] $processEnvironment[$name] } else { $null } + } } $script:cliStateRoot = Join-Path ([IO.Path]::GetTempPath()) "openshell-mxc-e2e-cli-$PID-$([Guid]::NewGuid().ToString('N'))" $isolatedPaths = @{ @@ -221,12 +267,8 @@ function Enter-IsolatedCliEnvironment { function Exit-IsolatedCliEnvironment { foreach ($name in $cliEnvironmentNames) { - $value = $script:cliEnvironmentSnapshot[$name] - if ($null -eq $value) { - Remove-Item "Env:$name" -ErrorAction SilentlyContinue - } else { - [Environment]::SetEnvironmentVariable($name, $value, "Process") - } + $snapshot = $script:cliEnvironmentSnapshot[$name] + Set-ProcessEnvironmentVariableExact -Name $name -Exists $snapshot.Exists -Value $snapshot.Value } if ($script:cliStateRoot -and (Test-Path -LiteralPath $script:cliStateRoot)) { Remove-Item -LiteralPath $script:cliStateRoot -Recurse -Force -ErrorAction SilentlyContinue @@ -806,7 +848,10 @@ try { } } finally { if ($gw -and -not $KeepRunning) { Stop-Gw $gw } - if ($KeepRunning -and $gw) { Info "gateway pid $($gw.Id) left running (-KeepRunning)" } + if ($KeepRunning -and $gw) { + Info "gateway pid $($gw.Id) left running (-KeepRunning)" + Info "CLI inspection endpoint: `$env:OPENSHELL_GATEWAY_ENDPOINT='http://127.0.0.1:$Port'" + } } } catch { diff --git a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 index 438f36d9bf..80dbe43de2 100644 --- a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 +++ b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 @@ -170,9 +170,55 @@ $cliEnvironmentNames = @( "OPENSHELL_GATEWAY_NAME" ) +function Set-ProcessEnvironmentVariableExact { + param( + [Parameter(Mandatory = $true)] + [string] $Name, + [Parameter(Mandatory = $true)] + [bool] $Exists, + [AllowNull()] + [string] $Value + ) + + if (-not $Exists) { + Remove-Item "Env:$Name" -ErrorAction SilentlyContinue + return + } + + if ($Value.Length -eq 0) { + # Windows PowerShell 5.1 maps an empty value passed through + # Environment.SetEnvironmentVariable to deletion. Call Win32 directly + # so an inherited empty entry remains distinguishable from absence. + if (-not ("OpenShellMxcProcessEnvironmentNative" -as [type])) { + Add-Type -TypeDefinition @' +using System.Runtime.InteropServices; + +public static class OpenShellMxcProcessEnvironmentNative +{ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetEnvironmentVariable(string name, string value); +} +'@ + } + if (-not [OpenShellMxcProcessEnvironmentNative]::SetEnvironmentVariable($Name, [string]::Empty)) { + $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "failed to restore empty process environment variable '$Name' (Win32 error $errorCode)" + } + return + } + + [Environment]::SetEnvironmentVariable($Name, $Value, "Process") +} + function Enter-IsolatedCliEnvironment { + $processEnvironment = [Environment]::GetEnvironmentVariables("Process") foreach ($name in $cliEnvironmentNames) { - $script:cliEnvironmentSnapshot[$name] = [Environment]::GetEnvironmentVariable($name, "Process") + $exists = $processEnvironment.Contains($name) + $script:cliEnvironmentSnapshot[$name] = [pscustomobject]@{ + Exists = $exists + Value = if ($exists) { [string] $processEnvironment[$name] } else { $null } + } } $script:cliStateRoot = Join-Path ([IO.Path]::GetTempPath()) "openshell-mxc-ocsf-cli-$PID-$([Guid]::NewGuid().ToString('N'))" $isolatedPaths = @{ @@ -198,12 +244,8 @@ function Enter-IsolatedCliEnvironment { function Exit-IsolatedCliEnvironment { foreach ($name in $cliEnvironmentNames) { - $value = $script:cliEnvironmentSnapshot[$name] - if ($null -eq $value) { - Remove-Item "Env:$name" -ErrorAction SilentlyContinue - } else { - [Environment]::SetEnvironmentVariable($name, $value, "Process") - } + $snapshot = $script:cliEnvironmentSnapshot[$name] + Set-ProcessEnvironmentVariableExact -Name $name -Exists $snapshot.Exists -Value $snapshot.Value } if ($script:cliStateRoot -and (Test-Path -LiteralPath $script:cliStateRoot)) { Remove-Item -LiteralPath $script:cliStateRoot -Recurse -Force -ErrorAction SilentlyContinue @@ -424,6 +466,7 @@ finally { # Stop the gateway FIRST so it releases its log + JSONL file handles. if ($KeepRunning -and $gw -and -not $gw.HasExited) { Info "leaving gateway pid $($gw.Id) running (-KeepRunning); stop it with: Stop-Process -Id $($gw.Id) -Force" + Info "CLI inspection endpoint: `$env:OPENSHELL_GATEWAY_ENDPOINT='http://127.0.0.1:$Port'" } elseif ($gw -and -not $gw.HasExited) { Step "Cleanup" Stop-Process -Id $gw.Id -Force -ErrorAction SilentlyContinue diff --git a/crates/openshell-driver-mxc/tests/demo_examples.rs b/crates/openshell-driver-mxc/tests/demo_examples.rs index e473be781a..5cb8852657 100644 --- a/crates/openshell-driver-mxc/tests/demo_examples.rs +++ b/crates/openshell-driver-mxc/tests/demo_examples.rs @@ -149,6 +149,7 @@ fn shipped_runners_isolate_cli_state_and_gateway_overrides() { "OPENSHELL_GATEWAY", "OPENSHELL_GATEWAY_ENDPOINT", "Remove-Item \"Env:$name\"", + "CLI inspection endpoint:", ] { assert!(runner.contains(required), "{name} is missing {required}"); } diff --git a/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs b/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs index 66d72573d0..f360aee15c 100644 --- a/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs +++ b/crates/openshell-driver-mxc/tests/windows_e2e_harness.rs @@ -26,20 +26,25 @@ fn task_harnesses_isolate_and_restore_gateway_configuration() { let script = r#" $ErrorActionPreference = "Stop" . $env:OPENSHELL_MXC_ENVIRONMENT_HELPER -$names = @( - "APPDATA", - "LOCALAPPDATA", - "XDG_CONFIG_HOME", - "XDG_STATE_HOME", - "XDG_DATA_HOME", - "OPENSHELL_GATEWAY", - "OPENSHELL_GATEWAY_ENDPOINT", - "OPENSHELL_GATEWAY_INSECURE", - "OPENSHELL_GATEWAY_CONFIG", - "OPENSHELL_GATEWAY_NAME" -) -foreach ($name in $names) { - [System.Environment]::SetEnvironmentVariable($name, "caller-$name", "Process") +$nonEmptyValues = @{ + APPDATA = "caller-APPDATA" + LOCALAPPDATA = "caller-LOCALAPPDATA" + XDG_CONFIG_HOME = "caller-XDG_CONFIG_HOME" + XDG_STATE_HOME = "caller-XDG_STATE_HOME" + XDG_DATA_HOME = "caller-XDG_DATA_HOME" + OPENSHELL_GATEWAY_ENDPOINT = "http://127.0.0.1:1" + OPENSHELL_GATEWAY_INSECURE = "caller-OPENSHELL_GATEWAY_INSECURE" + OPENSHELL_GATEWAY_CONFIG = "caller-OPENSHELL_GATEWAY_CONFIG" +} +foreach ($entry in $nonEmptyValues.GetEnumerator()) { + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, "Process") +} +$before = [Environment]::GetEnvironmentVariables("Process") +if (-not $before.Contains("OPENSHELL_GATEWAY") -or ([string] $before["OPENSHELL_GATEWAY"]).Length -ne 0) { + throw "OPENSHELL_GATEWAY did not start as an empty entry" +} +if ($before.Contains("OPENSHELL_GATEWAY_NAME")) { + throw "OPENSHELL_GATEWAY_NAME did not start absent" } $snapshot = Push-OpenShellMxcE2eEnvironment -StageDir $env:OPENSHELL_MXC_ENVIRONMENT_STAGE @@ -50,8 +55,9 @@ try { throw "$name escaped the staging directory: $value" } } + $isolatedEnvironment = [Environment]::GetEnvironmentVariables("Process") foreach ($name in @("OPENSHELL_GATEWAY", "OPENSHELL_GATEWAY_ENDPOINT", "OPENSHELL_GATEWAY_INSECURE", "OPENSHELL_GATEWAY_CONFIG", "OPENSHELL_GATEWAY_NAME")) { - if ($null -ne [System.Environment]::GetEnvironmentVariable($name, "Process")) { + if ($isolatedEnvironment.Contains($name)) { throw "$name was inherited by the isolated harness" } } @@ -59,26 +65,43 @@ try { Pop-OpenShellMxcE2eEnvironment -Snapshot $snapshot } -foreach ($name in $names) { - $value = [System.Environment]::GetEnvironmentVariable($name, "Process") - if ($value -ne "caller-$name") { - throw "$name was not restored: $value" +foreach ($entry in $nonEmptyValues.GetEnumerator()) { + $value = [Environment]::GetEnvironmentVariable($entry.Key, "Process") + if ($value -ne $entry.Value) { + throw "$($entry.Key) was not restored: $value" } } +$restored = [Environment]::GetEnvironmentVariables("Process") +if (-not $restored.Contains("OPENSHELL_GATEWAY") -or ([string] $restored["OPENSHELL_GATEWAY"]).Length -ne 0) { + throw "empty OPENSHELL_GATEWAY was not restored exactly" +} +if ($restored.Contains("OPENSHELL_GATEWAY_NAME")) { + throw "absent OPENSHELL_GATEWAY_NAME was restored" +} "#; - let output = std::process::Command::new("powershell.exe") - .args(["-NoProfile", "-Command", script]) - .env("OPENSHELL_MXC_ENVIRONMENT_HELPER", helper) - .env("OPENSHELL_MXC_ENVIRONMENT_STAGE", stage) - .output() - .expect("failed to launch Windows PowerShell"); - assert!( - output.status.success(), - "environment isolation check failed:\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); + for shell in ["powershell.exe", "pwsh.exe"] { + let output = std::process::Command::new(shell) + .args([ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + script, + ]) + .env("OPENSHELL_MXC_ENVIRONMENT_HELPER", &helper) + .env("OPENSHELL_MXC_ENVIRONMENT_STAGE", &stage) + .env("OPENSHELL_GATEWAY", "") + .env_remove("OPENSHELL_GATEWAY_NAME") + .output() + .unwrap_or_else(|error| panic!("failed to launch {shell}: {error}")); + assert!( + output.status.success(), + "environment isolation check failed under {shell}:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } } #[test] @@ -94,7 +117,7 @@ $ast = [System.Management.Automation.Language.Parser]::ParseFile( [ref]$errors ) if ($errors.Count -gt 0) { throw ($errors.Message -join "; ") } -foreach ($functionName in @("Enter-IsolatedCliEnvironment", "Exit-IsolatedCliEnvironment")) { +foreach ($functionName in @("Set-ProcessEnvironmentVariableExact", "Enter-IsolatedCliEnvironment", "Exit-IsolatedCliEnvironment")) { $function = $ast.Find({ param($node) $node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and @@ -112,15 +135,21 @@ $cliEnvironmentNames = @( "OPENSHELL_GATEWAY_CONFIG", "OPENSHELL_GATEWAY_NAME" ) $env:OPENSHELL_GATEWAY_ENDPOINT = "http://127.0.0.1:1" -Remove-Item Env:OPENSHELL_GATEWAY -ErrorAction SilentlyContinue +$before = [Environment]::GetEnvironmentVariables("Process") +if (-not $before.Contains("OPENSHELL_GATEWAY") -or ([string] $before["OPENSHELL_GATEWAY"]).Length -ne 0) { + throw "OPENSHELL_GATEWAY did not start as an empty entry" +} +if ($before.Contains("OPENSHELL_GATEWAY_NAME")) { + throw "OPENSHELL_GATEWAY_NAME did not start absent" +} Enter-IsolatedCliEnvironment try { - if (Test-Path Env:OPENSHELL_GATEWAY_ENDPOINT) { - throw "OPENSHELL_GATEWAY_ENDPOINT was not removed" - } - if (Test-Path Env:OPENSHELL_GATEWAY) { - throw "OPENSHELL_GATEWAY was not removed" + $isolatedEnvironment = [Environment]::GetEnvironmentVariables("Process") + foreach ($name in @("OPENSHELL_GATEWAY", "OPENSHELL_GATEWAY_ENDPOINT", "OPENSHELL_GATEWAY_NAME")) { + if ($isolatedEnvironment.Contains($name)) { + throw "$name was not removed" + } } } finally { Exit-IsolatedCliEnvironment @@ -129,16 +158,28 @@ try { if ($env:OPENSHELL_GATEWAY_ENDPOINT -ne "http://127.0.0.1:1") { throw "OPENSHELL_GATEWAY_ENDPOINT was not restored" } -if (Test-Path Env:OPENSHELL_GATEWAY) { - throw "null OPENSHELL_GATEWAY snapshot was restored as an empty variable" +$restored = [Environment]::GetEnvironmentVariables("Process") +if (-not $restored.Contains("OPENSHELL_GATEWAY") -or ([string] $restored["OPENSHELL_GATEWAY"]).Length -ne 0) { + throw "empty OPENSHELL_GATEWAY snapshot was not restored exactly" +} +if ($restored.Contains("OPENSHELL_GATEWAY_NAME")) { + throw "absent OPENSHELL_GATEWAY_NAME snapshot was restored" } "#; for runner in ["run-mxc-e2e.ps1", "run-ocsf-audit.ps1"] { for shell in ["powershell.exe", "pwsh.exe"] { let output = std::process::Command::new(shell) - .args(["-NoProfile", "-Command", script]) + .args([ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + script, + ]) .env("OPENSHELL_MXC_RUNNER", examples.join(runner)) + .env("OPENSHELL_GATEWAY", "") + .env_remove("OPENSHELL_GATEWAY_NAME") .output() .unwrap_or_else(|error| panic!("failed to launch {shell}: {error}")); assert!( diff --git a/tasks/scripts/windows-mxc-e2e-environment.ps1 b/tasks/scripts/windows-mxc-e2e-environment.ps1 index e4d3b2eb44..50068ce77e 100644 --- a/tasks/scripts/windows-mxc-e2e-environment.ps1 +++ b/tasks/scripts/windows-mxc-e2e-environment.ps1 @@ -1,6 +1,48 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +function Set-ProcessEnvironmentVariableExact { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] $Name, + [Parameter(Mandatory = $true)] + [bool] $Exists, + [AllowNull()] + [string] $Value + ) + + if (-not $Exists) { + Remove-Item "Env:$Name" -ErrorAction SilentlyContinue + return + } + + if ($Value.Length -eq 0) { + # Windows PowerShell 5.1 maps an empty value passed through + # Environment.SetEnvironmentVariable to deletion. Call Win32 directly + # so an inherited empty entry remains distinguishable from absence. + if (-not ("OpenShellMxcProcessEnvironmentNative" -as [type])) { + Add-Type -TypeDefinition @' +using System.Runtime.InteropServices; + +public static class OpenShellMxcProcessEnvironmentNative +{ + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetEnvironmentVariable(string name, string value); +} +'@ + } + if (-not [OpenShellMxcProcessEnvironmentNative]::SetEnvironmentVariable($Name, [string]::Empty)) { + $errorCode = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + throw "failed to restore empty process environment variable '$Name' (Win32 error $errorCode)" + } + return + } + + [Environment]::SetEnvironmentVariable($Name, $Value, "Process") +} + function Push-OpenShellMxcE2eEnvironment { [CmdletBinding()] param( @@ -29,29 +71,30 @@ function Push-OpenShellMxcE2eEnvironment { $isolatedValues.XDG_DATA_HOME ) | Out-Null + $processEnvironment = [Environment]::GetEnvironmentVariables("Process") $savedValues = @{} foreach ($name in $isolatedValues.Keys) { - $savedValues[$name] = [System.Environment]::GetEnvironmentVariable( - $name, - [System.EnvironmentVariableTarget]::Process - ) + $exists = $processEnvironment.Contains($name) + $savedValues[$name] = [pscustomobject]@{ + Exists = $exists + Value = if ($exists) { [string] $processEnvironment[$name] } else { $null } + } } try { foreach ($entry in $isolatedValues.GetEnumerator()) { - [System.Environment]::SetEnvironmentVariable( - $entry.Key, - $entry.Value, - [System.EnvironmentVariableTarget]::Process - ) + if ($null -eq $entry.Value) { + Remove-Item "Env:$($entry.Key)" -ErrorAction SilentlyContinue + } else { + [Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, "Process") + } } } catch { foreach ($entry in $savedValues.GetEnumerator()) { - [System.Environment]::SetEnvironmentVariable( - $entry.Key, - $entry.Value, - [System.EnvironmentVariableTarget]::Process - ) + Set-ProcessEnvironmentVariableExact ` + -Name $entry.Key ` + -Exists $entry.Value.Exists ` + -Value $entry.Value.Value } throw } @@ -67,10 +110,9 @@ function Pop-OpenShellMxcE2eEnvironment { ) foreach ($entry in $Snapshot.Values.GetEnumerator()) { - [System.Environment]::SetEnvironmentVariable( - $entry.Key, - $entry.Value, - [System.EnvironmentVariableTarget]::Process - ) + Set-ProcessEnvironmentVariableExact ` + -Name $entry.Key ` + -Exists $entry.Value.Exists ` + -Value $entry.Value.Value } }