diff --git a/.agents/skills/build-openshell-mxc-windows/SKILL.md b/.agents/skills/build-openshell-mxc-windows/SKILL.md index b80e2330c..a989e144a 100644 --- a/.agents/skills/build-openshell-mxc-windows/SKILL.md +++ b/.agents/skills/build-openshell-mxc-windows/SKILL.md @@ -456,6 +456,12 @@ skill or design doc. ## Final Report Checklist +When syncing from the `windows` branch, consult the selective integration +checkpoint in `crates/openshell-driver-mxc/WINDOWS-INTEGRATION.md`. Preserve +the host-supervisor/Windows-boundary architecture rather than restoring legacy +relay/proxy demo wrappers. The WebSocket, OpenClaw, and inference policy fixtures +have parser/mapper tests; do not report them as live runtime scenarios. + Every substantial Windows build run should report: | Item | Required detail | diff --git a/crates/openshell-driver-mxc/README.md b/crates/openshell-driver-mxc/README.md index 1ad28b4a2..888520ba2 100644 --- a/crates/openshell-driver-mxc/README.md +++ b/crates/openshell-driver-mxc/README.md @@ -158,7 +158,10 @@ from Cargo's passed count. The E2E runner uses native release artifacts (override with `-BinaryDir`), OpenSSL on PATH for disposable Ed25519 keys, and per-run writable fixtures under `target/windows-e2e-results`. It preserves the tracked gateway TOML and isolates -CLI registration via `XDG_CONFIG_HOME`. Signing keys have an owner-only DACL, +CLI registration via disposable APPDATA, LOCALAPPDATA, and XDG directories. +Inherited gateway overrides are cleared for the run and restored exactly +(including empty versus absent entries). The caller's gateway aliases are not +modified. Signing keys have an owner-only DACL, remain outside result bundles, and are removed unless `-KeepRunning` is requested. E2E runs real MXC only: there is no mock mode, and an inherited @@ -181,3 +184,14 @@ mode, exposes a named endpoint with `openshell service expose`, and verifies two unique GET responses through gateway host-based routing. It deletes the endpoint and requires HTTP 404 afterward. Requests connect only to the gateway with the exposed URL's Host header, so no wildcard DNS setup is required. + +The `ws-agent.yaml` and `openclaw-gateway.yaml` policies preserve the Windows +branch's workload grants. `inference.yaml` is a template: replace its filesystem, +curl executable, inference host, and port placeholders before use. Authorize +curl, which owns the network socket, not its cmd launcher. These policies have +parser/mapper regression coverage; they do not add live WebSocket, OpenClaw, +or provider-backed inference scenarios to the six-scenario E2E runner. + +With `-KeepRunning`, the runner restores the caller's environment but retains +the disposable CLI/runtime directory for the live gateway and prints its path. +Remove that directory only after stopping the gateway. diff --git a/crates/openshell-driver-mxc/WINDOWS-INTEGRATION.md b/crates/openshell-driver-mxc/WINDOWS-INTEGRATION.md new file mode 100644 index 000000000..d8dd9de68 --- /dev/null +++ b/crates/openshell-driver-mxc/WINDOWS-INTEGRATION.md @@ -0,0 +1,22 @@ +# Windows integration checkpoint + +Reviewed Windows branch through +`d89e4359b810d3d7205bb7d4a706f3c773a59e1f` (PR #4252). +This is a selective port, not a branch merge or a claim of complete demo parity. + +- #4252 (`d89e4359b`): MXC 1.0.0 schema, directional networking, and UI contract + already ported in `ce2ca474a`. +- #4199 (`f6ecca206`): authorize the socket-owning curl executable in the + inference policy template and real HTTPS test, rather than its cmd launcher. +- #4192 (`ed35eb9f3`): disposable CLI state, cleared inherited gateway + overrides, exact environment restoration, and strict fresh gateway + registration in the E2E and OCSF runners. +- Restore the WebSocket and OpenClaw policy fixtures with their original + grants, updating comments to the host-supervisor/Windows-boundary architecture. + +Keep the always-present `openshell-windows-sandbox`, authenticated Sandbox +Protocol, and host isolation-backend supervisor. Do not restore the old relay +executable, driver-owned CONNECT proxy, or legacy demo/task wrappers requiring +them. Provider-backed inference, live WebSocket, OpenClaw, and OCSF demo coverage +are not implied by policy parsing or by the six core E2E scenarios. Network-fence +evidence remains the documented compatibility TODO, not a verified guarantee. diff --git a/crates/openshell-driver-mxc/examples/e2e-policies/openclaw-gateway.yaml b/crates/openshell-driver-mxc/examples/e2e-policies/openclaw-gateway.yaml new file mode 100644 index 000000000..506ffaa61 --- /dev/null +++ b/crates/openshell-driver-mxc/examples/e2e-policies/openclaw-gateway.yaml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# OpenClaw gateway policy fixture ported from windows. +# Stage Node, OpenClaw, home, temporary files, and logs under this writable path. +# Node loads user32/gdi32: explicitly permit graphical UI to disable MXC 1.0 +# win32k lockdown. Clipboard and input injection remain disabled. +# The host supervisor governs the Node outbound grant. Managed ingress uses the +# Windows sandbox boundary; neither path needs a driver-owned proxy or relay. +# Fixture validation is not live OpenClaw coverage or verified exclusive fencing. +version: 1 + +filesystem_policy: + include_workdir: false + read_only: [] + read_write: + - "C:/openshell-openclaw" + +ui: + allow_graphical_ui: true + clipboard: none + allow_input_injection: false + +network_policies: + qualification_allowed: + name: qualification-allowed + endpoints: + - host: example.com + port: 443 + protocol: tcp + binaries: + - path: "C:/openshell-openclaw/node.exe" diff --git a/crates/openshell-driver-mxc/examples/e2e-policies/ws-agent.yaml b/crates/openshell-driver-mxc/examples/e2e-policies/ws-agent.yaml new file mode 100644 index 000000000..d1c50ed8f --- /dev/null +++ b/crates/openshell-driver-mxc/examples/e2e-policies/ws-agent.yaml @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# WebSocket-agent policy fixture ported from windows. +# Stage the workload under the read-only directory; adapt the path before use. +# Managed ingress uses the always-present Windows sandbox boundary and host +# supervisor, not an optional relay binary or a driver-owned network proxy. +# No network_policies means no authorized outbound destinations. This fixture +# does not itself exercise a live WebSocket session or prove network fencing. +version: 1 + +filesystem_policy: + include_workdir: false + read_only: + - "C:/work/openshell-mxc-ws" + read_write: [] diff --git a/crates/openshell-driver-mxc/examples/inference.yaml b/crates/openshell-driver-mxc/examples/inference.yaml new file mode 100644 index 000000000..5122b7b61 --- /dev/null +++ b/crates/openshell-driver-mxc/examples/inference.yaml @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Policy template: replace the path, host, and port placeholders before use. +# Authorize the socket-owning curl executable, not its cmd.exe launcher (#4199). +# The host supervisor evaluates REST policy and injects provider credentials. +version: 1 + +filesystem_policy: + include_workdir: false + read_only: [] + read_write: + - "__OPENSHELL_DEMO_SHARE__" + +network_policies: + nvidia_inference: + name: nvidia-inference + endpoints: + - host: "__INFERENCE_HOST__" + port: __INFERENCE_PORT__ + protocol: rest + access: read-write + enforcement: enforce + binaries: + - path: '__CURL_EXE__' diff --git a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 index 4b727e8ff..61b31a563 100644 --- a/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 +++ b/crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1 @@ -76,12 +76,120 @@ if (-not $BinaryDir) { $BinaryDir = Join-Path $targetRoot "$target/release" } $savedEnv = @{} -foreach ($name in @("OPENSHELL_GATEWAY_CONFIG", "OPENSHELL_GATEWAY", "OPENSHELL_MXC_MOCK_WXC", "OPENSHELL_WXC_EXEC_PATH", "OPENSHELL_COMPUTE_DRIVER", "OPENSHELL_MXC_SHARE_DIR", "XDG_CONFIG_HOME")) { - $savedEnv[$name] = [Environment]::GetEnvironmentVariable($name, "Process") +$processEnvironment = [Environment]::GetEnvironmentVariables("Process") +foreach ($name in @("OPENSHELL_MXC_MOCK_WXC", "OPENSHELL_WXC_EXEC_PATH", "OPENSHELL_COMPUTE_DRIVER", "OPENSHELL_MXC_SHARE_DIR")) { + $exists = $processEnvironment.Contains($name) + $savedEnv[$name] = [pscustomobject]@{ + Exists = $exists + Value = if ($exists) { [string] $processEnvironment[$name] } else { $null } + } } $secretDir = Join-Path $resultsRoot "keys-$stamp-$([guid]::NewGuid().ToString('N'))" $transcriptStarted = $false +$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 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) { + $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 = @{ + 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($_) }) { + Remove-Item "Env:$name" -ErrorAction SilentlyContinue + } + } catch { + Exit-IsolatedCliEnvironment + throw + } +} + +function Exit-IsolatedCliEnvironment { + foreach ($name in $cliEnvironmentNames) { + if (-not $script:cliEnvironmentSnapshot.ContainsKey($name)) { continue } + $snapshot = $script:cliEnvironmentSnapshot[$name] + Set-ProcessEnvironmentVariableExact -Name $name -Exists $snapshot.Exists -Value $snapshot.Value + } + if ($script:cliStateRoot -and (Test-Path -LiteralPath $script:cliStateRoot)) { + if ($KeepRunning -and $gw -and -not $gw.HasExited) { + Write-Host "Retaining isolated CLI/runtime state for live gateway: $script:cliStateRoot" + return + } + # Resolve the generated target before recursive cleanup; never remove an + # arbitrary caller path or the temporary directory itself. + $cleanupPath = [IO.Path]::GetFullPath($script:cliStateRoot) + $tempPrefix = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\' + if (-not $cleanupPath.StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase) -or + (Split-Path $cleanupPath -Leaf) -notmatch '^openshell-mxc-(e2e|ocsf)-cli-[0-9]+-[0-9a-f]{32}$') { + throw "refusing unsafe CLI state cleanup: $cleanupPath" + } + Remove-Item -LiteralPath $cleanupPath -Recurse -Force -ErrorAction SilentlyContinue + } +} + function Step([string]$m) { Write-Host "`n=== $m ===" -ForegroundColor Cyan } function Info([string]$m) { Write-Host " $m" } function Ok([string]$m) { Write-Host "[OK] $m" -ForegroundColor Green } @@ -247,14 +355,13 @@ function Stop-Gw($p) { function Register-Cli { if ($script:registered) { return } - $env:OPENSHELL_GATEWAY = "" $addResult = Invoke-NativeCaptured $cli @( "gateway", "add", "http://127.0.0.1:$Port", "--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') { + if ($addResult.ExitCode -ne 0) { throw "gateway add failed (exit $($addResult.ExitCode)): $addText" } @@ -510,6 +617,7 @@ $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. @@ -541,7 +649,6 @@ try { # Capture the pristine TOML once; every scenario renders a fresh copy from this. $tomlBase = Get-Content $tomlTemplate -Raw - $env:XDG_CONFIG_HOME = Join-Path $resultDir "cli-config" $openssl = (Get-Command openssl -ErrorAction Stop).Source New-Item -ItemType Directory $secretDir | Out-Null $ownerSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value @@ -929,7 +1036,11 @@ ingress works, not that direct host ingress is blocked. } if (Test-Path $secretDir) { Remove-Item -LiteralPath $secretDir -ErrorAction SilentlyContinue } } - foreach ($name in $savedEnv.Keys) { [Environment]::SetEnvironmentVariable($name, $savedEnv[$name], "Process") } + Exit-IsolatedCliEnvironment + foreach ($name in $savedEnv.Keys) { + $snapshot = $savedEnv[$name] + Set-ProcessEnvironmentVariableExact -Name $name -Exists $snapshot.Exists -Value $snapshot.Value + } } if ($harnessError -or $failCount -gt 0) { diff --git a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 index 374ee2c04..17e90fd7c 100644 --- a/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 +++ b/crates/openshell-driver-mxc/examples/run-ocsf-audit.ps1 @@ -61,6 +61,109 @@ $resultDir = Join-Path $here "results-$stamp" New-Item -ItemType Directory -Force $resultDir | Out-Null Start-Transcript -Path (Join-Path $resultDir "transcript.txt") -Force | Out-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" +) + +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) { + $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 = @{ + 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($_) }) { + Remove-Item "Env:$name" -ErrorAction SilentlyContinue + } + } catch { + Exit-IsolatedCliEnvironment + throw + } +} + +function Exit-IsolatedCliEnvironment { + foreach ($name in $cliEnvironmentNames) { + if (-not $script:cliEnvironmentSnapshot.ContainsKey($name)) { continue } + $snapshot = $script:cliEnvironmentSnapshot[$name] + Set-ProcessEnvironmentVariableExact -Name $name -Exists $snapshot.Exists -Value $snapshot.Value + } + if ($script:cliStateRoot -and (Test-Path -LiteralPath $script:cliStateRoot)) { + if ($KeepRunning -and $gw -and -not $gw.HasExited) { + Write-Host "Retaining isolated CLI/runtime state for live gateway: $script:cliStateRoot" + return + } + # Resolve the generated target before recursive cleanup; never remove an + # arbitrary caller path or the temporary directory itself. + $cleanupPath = [IO.Path]::GetFullPath($script:cliStateRoot) + $tempPrefix = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()).TrimEnd('\') + '\' + if (-not $cleanupPath.StartsWith($tempPrefix, [StringComparison]::OrdinalIgnoreCase) -or + (Split-Path $cleanupPath -Leaf) -notmatch '^openshell-mxc-(e2e|ocsf)-cli-[0-9]+-[0-9a-f]{32}$') { + throw "refusing unsafe CLI state cleanup: $cleanupPath" + } + Remove-Item -LiteralPath $cleanupPath -Recurse -Force -ErrorAction SilentlyContinue + } +} + function Step([string]$m) { Write-Host "`n=== $m ===" -ForegroundColor Cyan } function Info([string]$m) { Write-Host " $m" } function Ok([string]$m) { Write-Host "[OK] $m" -ForegroundColor Green } @@ -94,6 +197,7 @@ $gatewayEtwSessions = @() $passed = $true try { + Enter-IsolatedCliEnvironment # 1. Validate artifacts + privilege. Step "Validate package artifacts" foreach ($f in @($gateway, $cli, $supervisor, $sandbox, $policySrc, $tomlSrc)) { @@ -238,11 +342,10 @@ try { # 9. Register CLI -> gateway. Step "Register CLI -> gateway" - $env:OPENSHELL_GATEWAY = "" - try { & $cli gateway add "http://127.0.0.1:$Port" --local --name $GatewayName 2>&1 | ForEach-Object { Info $_ } } - catch { Info "gateway add: $($_.Exception.Message) (continuing - likely already registered)" } - try { & $cli gateway select $GatewayName 2>&1 | ForEach-Object { Info $_ } } - catch { Info "gateway select: $($_.Exception.Message) (continuing)" } + & $cli gateway add "http://127.0.0.1:$Port" --local --name $GatewayName 2>&1 | ForEach-Object { Info $_ } + if ($LASTEXITCODE -ne 0) { throw "gateway add failed (exit $LASTEXITCODE)" } + & $cli gateway select $GatewayName 2>&1 | ForEach-Object { Info $_ } + if ($LASTEXITCODE -ne 0) { throw "gateway select failed (exit $LASTEXITCODE)" } Ok "selected gateway '$GatewayName'" # 10. Create N sandboxes. Each drives the Sandboxing provider -> a full OCSF @@ -284,6 +387,7 @@ finally { } # ---- summarise the OCSF audit trail -------------------------------------- + Exit-IsolatedCliEnvironment $logText = @() if (Test-Path (Join-Path $resultDir "gateway.log")) { $logText += Get-Content (Join-Path $resultDir "gateway.log") } if (Test-Path (Join-Path $resultDir "gateway.err.log")) { $logText += Get-Content (Join-Path $resultDir "gateway.err.log") } diff --git a/crates/openshell-driver-mxc/tests/windows_demo_examples.rs b/crates/openshell-driver-mxc/tests/windows_demo_examples.rs new file mode 100644 index 000000000..c4cb655f0 --- /dev/null +++ b/crates/openshell-driver-mxc/tests/windows_demo_examples.rs @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Selectively ported Windows policy and harness regressions. These are not +//! substitutes for real MXC WebSocket, `OpenClaw`, or inference runtime coverage. +#![cfg(target_os = "windows")] + +use std::path::{Path, PathBuf}; +use std::process::Command; + +use openshell_core::proto::{NetworkAccessPreset, NetworkEnforcementMode, SandboxPolicy}; +use openshell_driver_mxc::{EmbeddedPolicyMapper, MapCtx, MappedConfig, PolicyMapper}; +use openshell_policy::{parse_sandbox_policy, validate_sandbox_policy}; + +fn examples() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("examples") +} + +fn map_fixture(yaml: &str) -> (SandboxPolicy, MappedConfig) { + let policy = parse_sandbox_policy(yaml).expect("valid authored policy"); + validate_sandbox_policy(&policy).expect("valid policy semantics"); + let mapped = EmbeddedPolicyMapper + .map( + Some(&policy), + &MapCtx { + sandbox_id: "windows-policy-fixture".into(), + egress: Some("127.0.0.1:18080".parse().unwrap()), + containment: "processcontainer".into(), + }, + ) + .expect("policy maps with supervisor-owned egress"); + (policy, mapped) +} + +#[test] +fn websocket_fixture_grants_read_only_workload_without_egress() { + let yaml = std::fs::read_to_string(examples().join("e2e-policies/ws-agent.yaml")).unwrap(); + let (policy, mapped) = map_fixture(&yaml); + assert!(policy.network_policies.is_empty()); + assert_eq!(mapped.readonly_paths, [r"C:\work\openshell-mxc-ws"]); + assert!(mapped.readwrite_paths.is_empty()); + assert!(mapped.ui.unwrap().disable); +} + +#[test] +fn openclaw_fixture_preserves_explicit_ui_and_node_network_grants() { + let yaml = + std::fs::read_to_string(examples().join("e2e-policies/openclaw-gateway.yaml")).unwrap(); + let (policy, mapped) = map_fixture(&yaml); + assert_eq!(mapped.readwrite_paths, [r"C:\openshell-openclaw"]); + assert!(mapped.readonly_paths.is_empty()); + let ui = mapped.ui.unwrap(); + assert!(!ui.disable); + assert!(!ui.injection); + let config = openshell_driver_mxc::map_to_mxc( + &policy, + &openshell_driver_mxc::MxcMappingOptions::default(), + ); + assert_eq!(config.config["ui"]["clipboard"], "none"); + let rule = &policy.network_policies["qualification_allowed"]; + assert_eq!(rule.binaries[0].path, "C:/openshell-openclaw/node.exe"); + assert_eq!(rule.endpoints[0].host, "example.com"); + assert_eq!(rule.endpoints[0].ports, [443]); + assert_eq!( + mapped.trimmed_policy.unwrap().network_policies, + policy.network_policies + ); +} + +#[test] +fn inference_template_authorizes_socket_owning_curl_not_cmd() { + let yaml = std::fs::read_to_string(examples().join("inference.yaml")) + .unwrap() + .replace("__OPENSHELL_DEMO_SHARE__", "C:/work/inference") + .replace("__CURL_EXE__", r"C:\Windows\System32\curl.exe") + .replace("__INFERENCE_HOST__", "integrate.api.nvidia.com") + .replace("__INFERENCE_PORT__", "443"); + assert!(!yaml.contains("__")); + let (policy, mapped) = map_fixture(&yaml); + assert_eq!(mapped.readwrite_paths, [r"C:\work\inference"]); + let rule = &policy.network_policies["nvidia_inference"]; + assert_eq!(rule.binaries.len(), 1); + assert_eq!(rule.binaries[0].path, r"C:\Windows\System32\curl.exe"); + let endpoint = &rule.endpoints[0]; + assert_eq!(endpoint.access, NetworkAccessPreset::ReadWrite as i32); + assert_eq!(endpoint.enforcement, NetworkEnforcementMode::Enforce as i32); + assert_eq!(endpoint.protocol, "rest"); + assert_eq!( + mapped.trimmed_policy.unwrap().network_policies, + policy.network_policies + ); +} + +#[test] +fn runners_isolate_and_restore_exact_process_environment() { + // Parse the actual runners, but execute only their environment helpers: + // no gateway, executor, firewall, ETW session, or caller CLI state is touched. + let script = r#" +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest +$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 "runner parse errors: $($errors.Message -join '; ')" } +foreach ($functionName in @("Set-ProcessEnvironmentVariableExact", "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 "missing $functionName" } + 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") +$KeepRunning = $false +$gw = $null +$before = [Environment]::GetEnvironmentVariables("Process") +if (-not $before.Contains("OPENSHELL_GATEWAY") -or ([string]$before["OPENSHELL_GATEWAY"]).Length -ne 0) { + throw "empty entry was not inherited" +} +Enter-IsolatedCliEnvironment +try { + $isolated = [Environment]::GetEnvironmentVariables("Process") + foreach ($name in $cliEnvironmentNames) { + if ($name.StartsWith("OPENSHELL_")) { + if ($isolated.Contains($name)) { throw "$name not removed" } + } elseif (-not ([string]$isolated[$name]).StartsWith($cliStateRoot)) { + throw "$name not isolated" + } + } +} finally { Exit-IsolatedCliEnvironment } +$after = [Environment]::GetEnvironmentVariables("Process") +foreach ($name in $cliEnvironmentNames) { + if ($before.Contains($name) -ne $after.Contains($name) -or $before[$name] -cne $after[$name]) { + throw "environment entry $name not restored exactly" + } +} +if (Test-Path -LiteralPath $cliStateRoot) { throw "temporary state not removed" } +"#; + for runner in ["run-mxc-e2e.ps1", "run-ocsf-audit.ps1"] { + let output = Command::new("powershell.exe") + .args([ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + script, + ]) + .env("OPENSHELL_MXC_RUNNER", examples().join(runner)) + .env("OPENSHELL_GATEWAY", "") + .env("OPENSHELL_GATEWAY_ENDPOINT", "http://127.0.0.1:1") + .env_remove("OPENSHELL_GATEWAY_NAME") + .output() + .expect("launch Windows PowerShell"); + assert!( + output.status.success(), + "{runner} environment regression failed:\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } +} diff --git a/crates/openshell-driver-mxc/tests/wxc_exec_real.rs b/crates/openshell-driver-mxc/tests/wxc_exec_real.rs index 784918af3..330816a07 100644 --- a/crates/openshell-driver-mxc/tests/wxc_exec_real.rs +++ b/crates/openshell-driver-mxc/tests/wxc_exec_real.rs @@ -854,6 +854,7 @@ async fn pc_https_egress_reads_injected_ca_bundle() { let output_path_string = output_path.to_string_lossy().into_owned(); let certificate_path_string = certificate_path.to_string_lossy().into_owned(); let cmd_string = cmd.to_string_lossy().into_owned(); + let curl_string = curl.to_string_lossy().into_owned(); let script = format!( "type \"%CURL_CA_BUNDLE%\" 1>NUL && \ \"{}\" --fail --silent --show-error --cacert \"%CURL_CA_BUNDLE%\" \ @@ -861,12 +862,7 @@ async fn pc_https_egress_reads_injected_ca_bundle() { --write-out \"%{{certs}}\" 1>\"{certificate_path_string}\"", curl.display() ); - let command = vec![ - cmd_string.clone(), - "/d".to_string(), - "/c".to_string(), - script, - ]; + let command = vec![cmd_string, "/d".to_string(), "/c".to_string(), script]; let serde_json::Value::Object(driver_config) = serde_json::json!({ "command": command, "cwd": output_dir_string, @@ -894,7 +890,8 @@ async fn pc_https_egress_reads_injected_ca_bundle() { access: NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], - binaries: vec![NetworkBinary { path: cmd_string }], + // curl owns the socket; cmd only launches it (#4199). + binaries: vec![NetworkBinary { path: curl_string }], }, )]), ..Default::default()