mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-23 06:30:28 +08:00
test(mxc): sync Windows policies and isolated CLI harness fixes
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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: []
|
||||
@@ -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__'
|
||||
@@ -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) {
|
||||
|
||||
@@ -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") }
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user