fix(mxc): pass native Windows build and HTTPS qualification

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
Drew Newberry
2026-10-13 08:24:13 -07:00
parent 48afd81fae
commit 924e060077
62 changed files with 1551 additions and 637 deletions
@@ -34,6 +34,14 @@ mechanics belong to the sandbox or isolation backend. Keep the optional Unix
SSH access adapter separate from portable session orchestration.
Shared Sandbox Protocol audit validation defaults to strict Linux evidence;
concrete platform validators must be selected by the implementing backend.
MXC control connects outward from the boundary to a generation-scoped host
loopback listener, matching the Windows relay's connection direction. Keep
the reverse connector in the MXC isolation backend and share it across policy
discovery and attachment. Shared transport hooks must retain pinned TLS peer
verification and supervisor JWT authentication; do not change host firewall
rules to repair host-to-AppContainer control connectivity.
The dedicated Windows sandbox binary links the Windows-only `openshell-mxc-boundary`
library for process operations, containment confirmation, and loopback
forwarding. The generic sandbox does not link this library and remains a Linux
@@ -43,10 +51,22 @@ process. Include boundary-library regression tests in native workspace tests;
their real process/socket checks are not qualification of MXC enforcement.
Keep MXC audit schemas and their validators in the Windows boundary library.
Register its `MxcRuntimeBackend` under `openshell-mxc` at supervisor composition;
it reuses the shared authenticated Sandbox Protocol, whose default Linux
Register its `MxcRuntimeBackend` under `openshell-mxc` at supervisor composition.
Use the private `BackendSetup` / `PreparedBackend` extension for launch decoding,
policy discovery, and client construction. Shared startup must not decode the
MXC descriptor or allocate its connector. Retain the same reverse-TCP connector
in prepared launch state through discovery, attachment, and reconnects.
It reuses the shared authenticated Sandbox Protocol, whose default Linux
backend remains separate. Shared protocol code
must not interpret MXC evidence. Do not reintroduce driver-owned proxy startup
must not interpret MXC evidence.
Keep proxy provisioning in MXC-owned host-launch and one-use bootstrap
envelopes. Private Windows backend setup supplies concrete listener options to
shared supervisor networking. Do not add proxy hooks to `BoundBoundary`, proxy
fields to shared Sandbox Protocol descriptors, or placeholders to other drivers.
Proxy startup remains after authenticated attachment and boundary confirmation.
Do not reintroduce driver-owned proxy startup
or gateway create-time credential snapshots. Gateway JSONL audit output
is portable and an explicit operator opt-in, independent of the selected driver.
Keep platform directory conventions in shared path utilities and ETW capture
@@ -60,11 +80,12 @@ provider-publication generations. Do not order opaque credential revisions
numerically. MXC rejects unsupported provider file delivery before launch or
environment replacement; it must not acknowledge files it did not install.
Preserve main's explicit outer-fence evidence contract. The current MXC mapper
allows broad host loopback and has no verified live-revocation evidence, so
configuration flags cannot establish the required guarantees. Keep confirmation
fail-closed until native qualification proves each property; a supported PSEC
host alone does not close these implementation gaps.
Preserve main's shared outer-fence evidence contract. At the user's explicit
request, MXC temporarily asserts the required guarantees for Windows-branch
parity testing. These MXC-only assertions are stubs, not verified enforcement:
broad host loopback and unverified live revocation remain known gaps. Keep the
TODOs and unverified audit mechanism visible; never report passing E2E as proof
that these gaps are closed. A supported PSEC host alone does not close them.
## Current Repository Shape
@@ -260,7 +281,7 @@ jobs in the current mirror push run, or push a new mirrored commit. The binaries
The ARM64 check/build steps in this x64-host contract are cross-builds. The
wrapper discovers and adds host-native LLVM and Ninja to `PATH`, requires the
ARM64 compiler and Spectre-mitigated libraries, lets ARM64 crypto crates select
`clang-cl`, and downloads the official prebuilt ARM64 Z3 static library.
`clang-cl`, and downloads the official prebuilt ARM64 Z3 package.
On ARM64 hosts, validate the native ARM64 check, build, and test path. The
wrapper rejects test targets that do not match the host architecture, so x64
@@ -337,6 +358,7 @@ without the capability reject explicit UI without implementing UI-specific
handling. Keep native UI validation and mapping in MXC. Omitted UI settings deny
graphical UI, clipboard access, and input injection. UI changes require
recreating the sandbox.
Windows must continue to reject unsupported compute drivers clearly.
The gateway's `compute-driver-mxc` feature independently links and registers
@@ -389,6 +411,7 @@ Verify the SSH host key beforehand. Never send the private SSH key or bundle
the harness signing-key directories. `-SkipBuild` is an explicit stale-artifact
opt-in, not evidence that artifacts match current sources. See the MXC driver
README for the command and remote dependency contract.
For MXC validation, also run `windows:e2e:mxc`
after the native release build. The harness uses .NET port discovery, disposable
TOML and CLI registration, and owner-only Ed25519 keys generated by OpenSSL on
@@ -397,8 +420,9 @@ bundles and are deleted unless `-KeepRunning` is explicitly requested.
E2E has no mock mode and rejects `OPENSHELL_MXC_MOCK_WXC=1`. Unit-test mocks
are not E2E coverage. Every selected scenario must pass for a successful exit;
any skip reports `INCOMPLETE` and exits non-zero. Never bypass audit to make
tests pass. Real runtime scenarios require native ProcessContainer PSEC egress
any skip reports `INCOMPLETE` and exits non-zero. Do not broaden the temporary
MXC network-assertion waiver to other audit or authentication checks. Real
runtime scenarios require native ProcessContainer PSEC egress
filtering and ingress host-loopback support. `wxc-exec --probe` is authoritative,
not the Windows build number. All-skipped real runs are not passes. Separate
test-internal `SKIP` messages from Cargo's passed count in the real integration
@@ -419,6 +443,17 @@ exposure, two exact GET responses through gateway routing, and HTTP 404 after
endpoint deletion. This is distinct from `openshell forward service`; neither
scenario tests file upload or exclusive ingress enforcement.
The real integration HTTPS test runs the same authenticated gateway harness with
the opt-in `https-ca` scenario, not an unauthenticated standalone driver. Build
native release binaries first, or select them with `OPENSHELL_MXC_TEST_BINARY_DIR`.
It requires Internet access to example.com and Windows curl >= 8.3 to explicitly
expand the injected CA bundle into `--cacert` (Schannel ignores that environment
variable by default). Require both a nonempty response and the OpenShell proxy
CA issuer; never replace these assertions with an insecure TLS request. This
fixture permits unavailable CRL information using best-effort Schannel revocation
for the ephemeral private CA; this is not revocation qualification. This
scenario is not part of the default six offline-capable E2E scenarios.
When reporting `windows:ci`, distinguish these categories:
- Passed tests from the full x64 workspace test log.
@@ -110,6 +110,7 @@ top-level workspace targets for check/test:
--exclude openshell-driver-podman
--exclude openshell-driver-vault
--exclude openshell-driver-vm
--exclude openshell-sandbox
--exclude openshell-vfio
```
Generated
+2 -1
View File
@@ -4689,6 +4689,8 @@ dependencies = [
"rustls-pemfile",
"serde",
"serde_json",
"sha2 0.10.9",
"tempfile",
"tokio",
"tokio-rustls",
"tokio-stream",
@@ -4851,7 +4853,6 @@ dependencies = [
"tonic",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]
@@ -46,7 +46,6 @@ struct ComputeDriverInfoView {
struct ComputeDriverCapabilitiesView {
driver_name: String,
driver_version: String,
supports_ui_policy: bool,
}
#[derive(Debug, Clone)]
@@ -418,7 +417,6 @@ pub async fn gateway_info(
capabilities: ComputeDriverCapabilitiesView {
driver_name: capabilities.driver_name,
driver_version: capabilities.driver_version,
supports_ui_policy: capabilities.supports_ui_policy,
},
}
})
@@ -516,15 +514,6 @@ fn print_compute_driver_info(drivers: &[ComputeDriverInfoView]) {
"Driver version:".dimmed(),
driver.capabilities.driver_version
);
println!(
" {} {}",
"UI policy:".dimmed(),
if driver.capabilities.supports_ui_policy {
"supported"
} else {
"unsupported"
}
);
}
}
@@ -543,7 +532,6 @@ fn gateway_info_to_json(view: &GatewayInfoView) -> serde_json::Value {
"capabilities": {
"driver_name": &driver.capabilities.driver_name,
"driver_version": &driver.capabilities.driver_version,
"supports_ui_policy": driver.capabilities.supports_ui_policy,
},
}))
.collect::<Vec<_>>(),
@@ -1917,7 +1905,6 @@ mod tests {
capabilities: ComputeDriverCapabilitiesView {
driver_name: "podman".to_string(),
driver_version: "0.0.75".to_string(),
supports_ui_policy: false,
},
}],
extensions: vec![ExtensionInfoView {
@@ -1946,10 +1933,6 @@ mod tests {
json["compute_drivers"][0]["capabilities"]["driver_version"],
"0.0.75"
);
assert_eq!(
json["compute_drivers"][0]["capabilities"]["supports_ui_policy"],
false
);
}
#[test]
+2 -59
View File
@@ -1676,16 +1676,8 @@ enum SandboxCommands {
names: Vec<String>,
/// Delete all sandboxes.
#[arg(long, conflicts_with_all = ["names", "expected_id", "expected_resource_version"])]
#[arg(long, conflicts_with = "names")]
all: bool,
/// Delete only if the current sandbox has this exact immutable ID.
#[arg(long = "expected-id", value_name = "ID")]
expected_id: Option<String>,
/// Delete only if the current sandbox has this resource version.
#[arg(long = "expected-resource-version", value_name = "VERSION")]
expected_resource_version: Option<u64>,
},
/// Stop a sandbox while preserving its workspace.
@@ -3576,18 +3568,11 @@ async fn run_async() -> Result<()> {
)
.await?;
}
SandboxCommands::Delete {
names,
all,
expected_id,
expected_resource_version,
} => {
SandboxCommands::Delete { names, all } => {
run::sandbox_delete(
endpoint,
&names,
all,
expected_id.as_deref(),
expected_resource_version,
&cli.workspace,
&tls,
&ctx.name,
@@ -5513,48 +5498,6 @@ mod tests {
));
}
#[test]
fn sandbox_delete_accepts_identity_preconditions() {
let cli = Cli::try_parse_from([
"openshell",
"sandbox",
"delete",
"demo",
"--expected-id",
"sb-123",
"--expected-resource-version",
"17",
])
.expect("identity-guarded sandbox delete should parse");
assert!(matches!(
cli.command,
Some(Commands::Sandbox {
command: Some(SandboxCommands::Delete {
ref names,
all: false,
expected_id: Some(ref expected_id),
expected_resource_version: Some(17),
})
}) if names == &["demo"] && expected_id == "sb-123"
));
}
#[test]
fn sandbox_delete_all_rejects_identity_preconditions() {
assert!(
Cli::try_parse_from([
"openshell",
"sandbox",
"delete",
"--all",
"--expected-id",
"sb-123",
])
.is_err()
);
}
#[test]
fn sandbox_list_accepts_output_json() {
let cli = Cli::try_parse_from(["openshell", "sandbox", "list", "-o", "json"])
+1 -24
View File
@@ -412,9 +412,7 @@ async fn finalize_sandbox_create_session(
}
let names = [sandbox_name.to_string()];
if let Err(err) =
sandbox_delete(server, &names, false, None, None, workspace, tls, gateway).await
{
if let Err(err) = sandbox_delete(server, &names, false, workspace, tls, gateway).await {
if let Ok(exit_code) = session_result.as_ref() {
return Err(miette::miette!(
"sandbox command exited with status {exit_code}, but ephemeral cleanup failed: {err}"
@@ -3881,35 +3879,14 @@ fn labels_display(labels: &HashMap<String, String>) -> String {
}
/// Delete a sandbox by name, or all sandboxes when `all` is true.
#[allow(clippy::too_many_arguments)] // user-facing CLI command with explicit identity guards
pub async fn sandbox_delete(
server: &str,
names: &[String],
all: bool,
expected_id: Option<&str>,
expected_resource_version: Option<u64>,
workspace: &str,
tls: &TlsOptions,
gateway: &str,
) -> Result<()> {
if (expected_id.is_some() || expected_resource_version.is_some()) && (all || names.len() != 1) {
return Err(miette!(
"--expected-id and --expected-resource-version require exactly one sandbox name"
));
}
if expected_id.is_some_and(str::is_empty) {
return Err(miette!("--expected-id must not be empty"));
}
if expected_resource_version.is_some() && expected_id.is_none() {
return Err(miette!(
"--expected-resource-version requires --expected-id"
));
}
if expected_resource_version == Some(0) {
return Err(miette!(
"--expected-resource-version must be greater than zero"
));
}
let mut client = grpc_client(server, tls).await?;
let names_to_delete: Vec<String> = if all {
@@ -64,7 +64,6 @@ struct SandboxState {
/// so a catalog lookup failure can be told apart from an empty catalog.
fail_list_provider_profiles: Arc<AtomicBool>,
deleted_names: Arc<Mutex<Vec<Vec<String>>>>,
delete_requests: Arc<Mutex<Vec<DeleteSandboxRequest>>>,
create_requests: Arc<Mutex<Vec<CreateSandboxRequest>>>,
expose_service_requests: Arc<Mutex<Vec<openshell_core::proto::ExposeServiceRequest>>>,
fail_delete_sandbox_message: Arc<Mutex<Option<String>>>,
@@ -390,11 +389,6 @@ impl OpenShell for TestOpenShell {
request: tonic::Request<DeleteSandboxRequest>,
) -> Result<Response<DeleteSandboxResponse>, Status> {
let request = request.into_inner();
self.state
.delete_requests
.lock()
.await
.push(request.clone());
self.state
.deleted_names
.lock()
@@ -1600,10 +1594,6 @@ async fn deleted_names(server: &TestServer) -> Vec<Vec<String>> {
server.openshell.state.deleted_names.lock().await.clone()
}
async fn delete_requests(server: &TestServer) -> Vec<DeleteSandboxRequest> {
server.openshell.state.delete_requests.lock().await.clone()
}
async fn create_requests(server: &TestServer) -> Vec<CreateSandboxRequest> {
server.openshell.state.create_requests.lock().await.clone()
}
@@ -1713,8 +1703,6 @@ async fn sandbox_delete_continues_after_entry_failure() {
&server.endpoint,
&["failing-sandbox".to_string(), "later-sandbox".to_string()],
false,
None,
None,
"default",
&tls,
"openshell",
+13
View File
@@ -58,6 +58,19 @@ configuration validation live in `openshell_core::middleware`. Policy admission
and the supervisor runtime consume the same contract without introducing a
dependency from the policy crate to the supervisor implementation.
## Extension Capabilities
Extensions advertise namespaced string capabilities through `PeerMetadata`.
Use shared constants in `extension_protocol` and pass optional capabilities to
`extension_metadata`; do not add per-driver booleans for policy features.
Negotiation validates and retains the extension's supported capability list.
`POLICY_UI_V1` (`openshell.policy.ui.v1`) declares complete startup UI-policy
support. The gateway checks the effective policy before driver validation or
creation and rejects any explicit UI section when the capability is absent.
Supporting drivers validate and enforce the concrete settings. Omitted UI
preserves existing driver behavior; changing UI requires sandbox recreation.
## Protobuf Struct Conversion
Use `openshell_core::proto_struct` when crossing between `serde_json` values and
@@ -19,6 +19,10 @@ pub const PROTOCOL_MINOR: u32 = 0;
/// configured signer before accepting sandbox creation.
pub const COMPUTE_LAUNCH_AUTHENTICATION: &str = "openshell.compute.launch-authentication";
/// Compute drivers advertising this capability enforce the complete startup UI
/// policy contract. Absence means explicit UI policy must be rejected.
pub const POLICY_UI_V1: &str = "openshell.policy.ui.v1";
const MAX_IMPLEMENTATION_NAME_BYTES: usize = 128;
const MAX_IMPLEMENTATION_VERSION_BYTES: usize = 128;
const MAX_CAPABILITY_BYTES: usize = 128;
@@ -418,6 +422,36 @@ mod tests {
);
}
#[test]
fn ui_policy_capability_uses_existing_optional_negotiation() {
let gateway = gateway_metadata(ExtensionFamily::Compute);
let extension = extension_metadata(
ExtensionFamily::Compute,
"openshell/test-ui-driver",
"test",
[POLICY_UI_V1.to_string()],
);
let result = negotiate(
ExtensionFamily::Compute,
"test-ui-driver",
&gateway,
Some(extension),
)
.unwrap();
assert!(
result
.supported_capabilities
.iter()
.any(|value| value == POLICY_UI_V1)
);
assert!(
!result
.required_capabilities
.iter()
.any(|value| value == POLICY_UI_V1)
);
}
#[test]
fn missing_metadata_and_major_skew_are_actionable() {
let (gateway, mut extension) = compatible();
@@ -132,7 +132,6 @@ impl DockerBoundarySpec {
},
tls: self.supervisor_tls,
host_gateway_ip: self.host_gateway_ip,
direct_proxy: None,
resource_claims,
outer_fence,
},
@@ -289,7 +289,6 @@ impl KubernetesSandboxRuntimeBoundarySpec {
},
tls: self.supervisor_tls,
host_gateway_ip: self.host_gateway_ip,
direct_proxy: None,
resource_claims,
outer_fence,
},
+62 -9
View File
@@ -31,6 +31,10 @@ The resolved create-time `SandboxPolicy` is carried by
`DriverSandboxSpec.policy`. The driver provisions a one-shot ProcessContainer,
launches the Windows boundary and separate host supervisor, and monitors both.
The boundary launches the workload only after authenticated confirmation.
The driver defaults its paired runtimes to `info` logging so native workload
launches remain observable. An explicit sandbox log-level setting takes
precedence. The boundary emits structured process activity and the Windows
branch's `MXC agent launched` acknowledgement only after native spawn succeeds.
The driver resolves its gateway endpoint and TLS server-name defaults from
generic gateway inputs. The host supervisor authenticates its gateway session
@@ -43,15 +47,25 @@ embed the supervisor, and the sandbox does not link the compute driver.
Dynamic forwarding uses the shared protocol stream, not a dedicated relay
executable or a reverse connection to a new gateway listener.
The boundary establishes its control TCP connection outward from MXC to a
generation-scoped loopback listener in the host isolation backend, matching the
Windows branch relay's connection direction. The shared transport connector
extension preserves pinned boundary TLS identity and supervisor JWT checks;
the driver neither brokers protocol requests nor runs supervision. Discovery
and attachment reuse the backend-owned listener, including reconnects. No host
firewall rule changes are required for this control path.
## Enforcement boundaries
The current port is not runtime-qualified against main's outer-fence contract.
The current port temporarily asserts main's required outer-fence guarantees
to permit Windows-branch compatibility testing. This is an unconditional
MXC-only stub, not verified enforcement evidence or production qualification.
Broad host-loopback access does not establish `NoUnmanagedEgressPath`, and
live access revocation has no verified native evidence. The driver therefore
does not publish those guarantees, and main's confirmation gate prevents
workload activation. Configuration intent must not be promoted to enforcement
proof. A qualified PSEC host is necessary but does not by itself close these
implementation gaps.
live access revocation has no verified native evidence. The provisioning TODO
tracks replacing these assertions with real enforcement. Audit metadata labels
egress as `mxc-windows-parity-unverified-egress-stub`. AppContainer identity,
authenticated transport, and generation checks remain required. Do not expose
sensitive host-local services to workloads using this compatibility path.
- MXC supplies the default-deny filesystem fence, AppContainer token, UI
policy, and loopback-only network fence.
@@ -73,6 +87,13 @@ implementation gaps.
## Configuration
MXC does not implement external-resource label admission. Do not configure
`resource_admission` under its driver table; unsupported fields are rejected.
Host filesystem grants rely on workload policy and a trusted gateway operator,
not resource labels. Caller-supplied command configuration still requires
`allow_driver_config = true`. Native isolation, authenticated transport, and
outer-fence confirmation remain required.
The packaged `openshell-supervisor.exe` and `openshell-windows-sandbox.exe` default to
siblings of `openshell-gateway.exe`. Override their paths for development
builds.
@@ -113,9 +134,11 @@ openshell sandbox create --name mxc-demo --policy policy.yaml `
--driver-config-json $config --no-tty
```
The command is required. The working directory is required because it contains
the generation-scoped bootstrap staging directory. Environment belongs in
`--env` or `--env-from`, not gateway configuration.
The command and working directory are required. The working directory contains
the generation-scoped bootstrap staging directory. When no generic canonical
command is supplied, the driver passes `mxc.command` to the supervisor rather
than starting a scratch workload. An explicit generic canonical command retains
precedence. Environment belongs in `--env` or `--env-from`, not gateway configuration.
When gateway TLS is enabled, configure the gateway-owned `guest_tls_ca`.
The host supervisor verifies the gateway certificate with that CA and uses
@@ -133,6 +156,24 @@ not required; driver-owned guest TLS fields are rejected.
| ETW/OCSF audit | Optional Windows Sandboxing ETW consumer |
| Gateway restart recovery | Not yet supported; live MXC generations remain in-memory |
UI controls are startup settings in the sandbox policy:
```yaml
ui:
allow_graphical_ui: true
clipboard: read
allow_input_injection: false
```
Omitted UI settings deny all three capabilities. Clipboard accepts `none`,
`read`, `write`, or `all`, from the workload's perspective. Clipboard and input
injection require graphical UI. Unknown fields/values and unsupported
containment targets are rejected before provisioning. MXC advertises
`openshell.policy.ui.v1` through existing extension metadata. The gateway rejects
explicit UI policy (including `{}`) for drivers missing this capability; those
drivers need no UI-specific handling. UI policy itself does not require the
`allow_driver_config` opt-in. Recreate the sandbox to change these settings.
## Validation
Windows release build tasks stage the pinned, target-matching `libz3.dll` beside
@@ -183,6 +224,7 @@ coverage remain nonzero exits. Missing host-loopback evidence is not a pass,
and a qualified host does not close the outer-fence implementation gaps above.
### Local Windows host
Run the Windows build lane on a native Windows MSVC host:
```powershell
@@ -219,6 +261,17 @@ to execute successfully. Unit-test mocks are separate from E2E coverage.
`network-policy` checks admission and workload execution, not real network
enforcement. All-skipped real runs report `INCOMPLETE`, not `PASS`.
The real-MXC HTTPS test uses the authenticated E2E gateway/supervisor harness.
Build native release binaries first; `OPENSHELL_MXC_TEST_BINARY_DIR` optionally
selects another binary directory. Its opt-in `-Scenario https-ca` requires
Internet access to `https://example.com` and Windows curl 8.3 or newer. Curl
explicitly loads the boundary-injected CA bundle and records the peer certificate;
success requires a nonempty response and the OpenShell proxy CA issuer. It does
not install a host trust root or disable CA/hostname verification. The fixture
uses best-effort Schannel revocation because the ephemeral private CA has no CRL
distribution point; it does not qualify revocation enforcement. The default six E2E
scenarios remain independent of Internet access.
The `forwarding` scenario starts a TCP listener inside MXC, then verifies two
nonce-bound request/reply exchanges through `openshell forward service`, the
gateway, supervisor, and Windows boundary. Both listener ports are dynamically
@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
version: 1
filesystem_policy:
include_workdir: false
read_only: []
read_write:
- "C:/work/openshell-mxc-e2e"
network_policies:
https_example:
name: https-example
endpoints:
- host: example.com
port: 443
protocol: rest
enforcement: enforce
access: read-only
binaries:
- path: '__CURL_EXE__'
@@ -11,8 +11,11 @@ version = 2
# and environment variables through the standard sandbox --env option.
[openshell.drivers.mxc]
# Path to wxc-exec.exe. Required for live runs. Leave the default for mock-mode
# smoke tests (set OPENSHELL_MXC_MOCK_WXC=1 instead).
# Explicit administrator opt-in for this disposable development gateway.
# MXC workload command and cwd are supplied through per-sandbox driver config.
allow_driver_config = true
# Path to wxc-exec.exe. Required for real runtime tests; mocks are not E2E.
wxc_exec_path = "C:\\mxc\\wxc-exec.exe"
# The RFC 0012 MXC runtime requires ProcessContainer.
@@ -36,6 +36,7 @@
# network-policy - supervisor-owned network policy permits admission.
# forwarding - two TCP request/reply exchanges through OpenShell ingress.
# service-forwarding - named HTTP service routing and endpoint deletion.
# https-ca - opt-in outbound HTTPS using the injected proxy CA.
[CmdletBinding()]
param(
@@ -752,6 +753,19 @@ try {
}
)
# External connectivity is deliberately opt-in; the default six scenarios
# remain runnable without Internet access. Use the same authenticated
# gateway launch path as every other workload, not a bare driver fixture.
if ($Scenario -eq 'https-ca') {
$allScenarios += @{
Name = 'https-ca'; PolicyFile = Join-Path $policyDir 'https-ca.yaml'
SandboxId = 'ca'; Backends = 'process_container'; Kind = 'https'
PosTarget = Join-Path $DemoDir 'example.html'
CertificateTarget = Join-Path $DemoDir 'peer-certificate.txt'
ErrorTarget = Join-Path $DemoDir 'curl-stderr.txt'
Description = 'curl trusts the injected CA and observes the OpenShell HTTPS proxy certificate'
}
}
if ($Scenario) {
# Wrap in @() so an exact single match stays an array: without it a lone
# match is a bare hashtable, its .Count is unreliable on PS 5.1, and
@@ -800,6 +814,10 @@ try {
$defaultDemoDir.Replace('\', '/'),
$DemoDir.Replace('\', '/')
)
$curlExe = Join-Path $env:SYSTEMROOT 'System32/curl.exe'
# Preserve the exact Windows executable path used in the launch;
# network binary identities are compared as strings, not FS paths.
$policyText = $policyText.Replace('__CURL_EXE__', $curlExe.Replace("'", "''"))
Set-Content -Path $policyUsed -Value $policyText -Encoding UTF8
# Per-scenario gateway logs land in the bundle under their own names.
@@ -820,6 +838,22 @@ try {
} else {
$command = @($cmdExe, "/c", "echo denied 1> $denied")
}
} elseif ($sc.Kind -eq 'https') {
if (-not (Test-Path -LiteralPath $curlExe)) { throw "Missing Windows curl: $curlExe" }
Remove-Item -LiteralPath $sc.PosTarget, $sc.CertificateTarget, $sc.ErrorTarget -Force -ErrorAction SilentlyContinue
# Launch curl directly so the policy identity is the socket
# owner. Schannel ignores CURL_CA_BUNDLE by default, so curl
# imports and expands it itself (curl >= 8.3). No shell child,
# host trust-store installation, or insecure TLS override.
# The ephemeral private CA has no CRL distribution point.
# Best-effort revocation tolerates that absence, while keeping
# certificate/hostname validation and known-revoked rejection.
$command = @($curlExe, '--disable', '--fail', '--silent', '--show-error', '--max-time', '45',
'--ssl-revoke-best-effort',
'--variable', '%CURL_CA_BUNDLE', '--expand-cacert', '{{CURL_CA_BUNDLE}}',
'--stderr', $sc.ErrorTarget,
'--output', $sc.PosTarget, '--write-out', "%output{$($sc.CertificateTarget)}%{certs}\nOpenShell curl exit: %{exitcode}\n",
'https://example.com/')
} elseif ($sc.Kind -in @("forwarding", "service-forwarding")) {
Remove-Item -LiteralPath $sc.ReadyFile -Force -ErrorAction SilentlyContinue
$forwardNonce = [guid]::NewGuid().ToString('N')
@@ -879,7 +913,29 @@ try {
$gwText = (Get-Content $gwLog, $gwErrLog -Raw -ErrorAction SilentlyContinue) -join "`n"
# Evaluate.
if ($sc.Kind -eq "positive") {
if ($sc.Kind -eq 'https') {
try {
if ($createExitCode -ne 0) { throw "sandbox create failed: $createOutStr" }
if (-not (Wait-File $sc.CertificateTarget 60)) { throw 'HTTPS certificate absent' }
$certificate = Get-Content -LiteralPath $sc.CertificateTarget -Raw
Copy-Item -LiteralPath $sc.CertificateTarget -Destination (Join-Path $resultDir 'https-peer-certificate.txt')
if (Test-Path -LiteralPath $sc.ErrorTarget) {
Copy-Item -LiteralPath $sc.ErrorTarget -Destination (Join-Path $resultDir 'https-curl-stderr.txt')
}
if ($certificate -notmatch '(?m)^OpenShell curl exit: 0\r?$') {
$curlError = Get-Content -LiteralPath $sc.ErrorTarget -Raw -ErrorAction SilentlyContinue
throw "curl did not complete successfully: $curlError"
}
if ($certificate -notmatch 'OpenShell Sandbox CA') { throw 'HTTPS peer was not issued by the OpenShell proxy CA' }
if (-not (Wait-File $sc.PosTarget 10)) { throw 'HTTPS response absent' }
if ((Get-Item -LiteralPath $sc.PosTarget).Length -eq 0) { throw 'HTTPS response empty' }
Ok 'https-ca: response and OpenShell proxy certificate verified'
$results += [pscustomobject]@{ Scenario = $sc.Name; Result = 'PASS'; Reason = 'real HTTPS through injected proxy CA' }
} catch {
Bad "https-ca: $($_.Exception.Message)"
$results += [pscustomobject]@{ Scenario = $sc.Name; Result = 'FAIL'; Reason = $_.Exception.Message }
}
} elseif ($sc.Kind -eq "positive") {
$present = Wait-File $sc.PosTarget 30
if ($present -and $createExitCode -eq 0) {
Ok "$($sc.Name): in-policy write produced artifact"
+137 -34
View File
@@ -16,7 +16,7 @@ use futures::Stream;
use openshell_core::gpu::{driver_gpu_requirements, effective_driver_gpu_count};
use openshell_core::proto::SandboxPolicy;
use openshell_core::proto::compute::v1::{
DriverCondition, DriverPlatformEvent, DriverSandbox, DriverSandboxStatus,
DriverCondition, DriverPlatformEvent, DriverSandbox, DriverSandboxSpec, DriverSandboxStatus,
GetCapabilitiesResponse, WatchSandboxesDeletedEvent, WatchSandboxesEvent,
WatchSandboxesPlatformEvent, WatchSandboxesSandboxEvent, watch_sandboxes_event,
};
@@ -67,10 +67,8 @@ impl MxcBackend {
#[serde(default, deny_unknown_fields)]
#[allow(clippy::struct_excessive_bools)] // independent operator options in the existing TOML schema
pub struct MxcComputeConfig {
/// Permit caller-supplied driver JSON. Does not waive resource admission.
/// Permit caller-supplied driver JSON. Does not waive workload policy.
pub allow_driver_config: bool,
/// Operator-owned external attachment approval policy.
pub resource_admission: openshell_core::resource_admission::ResourceAdmissionConfig,
/// Path to `wxc-exec.exe`. Required for live runs.
pub wxc_exec_path: String,
pub supervisor_binary_path: String,
@@ -104,8 +102,6 @@ impl Default for MxcComputeConfig {
Self {
wxc_exec_path: "wxc-exec.exe".into(),
allow_driver_config: false,
resource_admission:
openshell_core::resource_admission::ResourceAdmissionConfig::default(),
supervisor_binary_path: sibling_binary("openshell-supervisor.exe"),
sandbox_binary_path: sibling_binary("openshell-windows-sandbox.exe"),
state_dir,
@@ -122,6 +118,18 @@ impl Default for MxcComputeConfig {
}
impl MxcComputeConfig {
/// MXC does not implement external-resource label admission. Keep the
/// independent caller configuration gate in the gateway/driver contract.
pub fn admission_policy(&self) -> openshell_core::resource_admission::DriverAdmissionConfig {
openshell_core::resource_admission::DriverAdmissionConfig {
allow_driver_config: self.allow_driver_config,
resource_admission: openshell_core::resource_admission::ResourceAdmissionConfig {
enabled: false,
required_labels: std::collections::BTreeMap::new(),
},
}
}
/// Validate static MXC configuration without requiring launch credentials.
///
/// Keep executable lookup independent of the gateway's PATH, as in the
@@ -262,16 +270,13 @@ impl MxcComputeBackend {
pub fn capabilities(&self) -> GetCapabilitiesResponse {
GetCapabilitiesResponse {
resource_admission_policy: openshell_core::resource_admission::DriverAdmissionConfig {
allow_driver_config: self.config.allow_driver_config,
resource_admission: self.config.resource_admission.clone(),
}
.acknowledgement(),
resource_admission_policy: self.config.admission_policy().acknowledgement(),
extension: Some(openshell_core::extension_protocol::extension_metadata(
openshell_core::extension_protocol::ExtensionFamily::Compute,
"openshell/mxc",
openshell_core::VERSION,
[],
(self.config.backend == MxcBackend::ProcessContainer)
.then(|| openshell_core::extension_protocol::POLICY_UI_V1.to_string()),
)),
driver_name: DRIVER_NAME.to_string(),
driver_version: DRIVER_VERSION.to_string(),
@@ -282,23 +287,14 @@ impl MxcComputeBackend {
resource_capabilities: None,
rootfs_tar_staging_dir: String::new(),
rootfs_tar_max_bytes: 0,
supports_ui_policy: true,
}
}
pub fn validate_sandbox_create(&self, sandbox: &DriverSandbox) -> Result<(), tonic::Status> {
self.config
.resource_admission
.validate()
.map_err(tonic::Status::failed_precondition)?;
openshell_core::resource_admission::check_sandbox_driver_config(
self.config.allow_driver_config,
sandbox,
)?;
// Host filesystem grants still have no authoritative label resolver.
self.config
.resource_admission
.reject_unlabelable("MXC host filesystem grants")?;
if !self.invoker.is_mock() {
self.config
.validate_configuration()
@@ -712,11 +708,8 @@ async fn run_lifecycle_inner(
let descriptor_path = context.host_state_dir.join(HOST_RUNTIME_DESCRIPTOR_FILE);
std::fs::write(
&descriptor_path,
provisioning
.runtime_descriptor
.backend_descriptor()
.map_err(|error| error.to_string())?
.payload,
serde_json::to_vec(&provisioning.runtime_descriptor)
.map_err(|error| format!("encode MXC launch data: {error}"))?,
)
.map_err(|error| format!("write MXC runtime descriptor: {error}"))?;
// The supervisor owns the proxy listener. Release only that reservation
@@ -730,7 +723,7 @@ async fn run_lifecycle_inner(
"--bootstrap".to_string(),
boundary_config_path.display().to_string(),
"--log-level".to_string(),
openshell_core::driver_utils::sandbox_log_level(&context.sandbox, "warn"),
openshell_core::driver_utils::sandbox_log_level(&context.sandbox, "info"),
]);
let mut filesystem = MxcFilesystem {
readwrite_paths: mapped.readwrite_paths,
@@ -834,6 +827,8 @@ fn spawn_supervisor(
.arg(descriptor_path)
.arg("--auth-bundle-file")
.arg(auth_bundle_path)
.arg("--workdir")
.arg(&context.sandbox_config.cwd)
.env(
openshell_core::sandbox_env::ADMITTED_ISOLATION_BACKEND,
openshell_mxc_boundary::BACKEND_NAME,
@@ -850,7 +845,7 @@ fn spawn_supervisor(
.env(openshell_core::sandbox_env::SANDBOX, &context.sandbox.name)
.env(
openshell_core::sandbox_env::LOG_LEVEL,
openshell_core::driver_utils::sandbox_log_level(&context.sandbox, "warn"),
openshell_core::driver_utils::sandbox_log_level(&context.sandbox, "info"),
);
if let Some(ca) = &context.gateway.tls {
command.env(openshell_core::sandbox_env::TLS_CA, ca);
@@ -884,6 +879,7 @@ fn mxc_main_process_config(
}
main
}
enum RuntimePairExit {
Shutdown,
Boundary(std::process::ExitStatus),
@@ -1339,18 +1335,15 @@ mod tests {
fn host_grants_config() -> MxcComputeConfig {
MxcComputeConfig {
allow_driver_config: true,
resource_admission: openshell_core::resource_admission::ResourceAdmissionConfig {
enabled: false,
..Default::default()
},
..Default::default()
}
}
#[tokio::test]
async fn admission_rejects_host_grants_even_with_driver_config_enabled() {
async fn driver_config_gate_remains_required_without_resource_admission() {
let (mut backend, sandbox, _dir) = preflight_fixture();
backend.config.resource_admission.enabled = true;
assert!(backend.validate_sandbox_create(&sandbox).is_ok());
backend.config.allow_driver_config = false;
let error = backend.validate_sandbox_create(&sandbox).unwrap_err();
assert_eq!(error.code(), tonic::Code::FailedPrecondition);
assert!(backend.create_sandbox(&sandbox).await.is_err());
@@ -1358,6 +1351,24 @@ mod tests {
assert!(!backend.config.state_dir.exists());
}
#[test]
fn resource_admission_is_unsupported_and_acknowledged() {
let config = MxcComputeConfig::default();
let policy = config.admission_policy();
assert!(!policy.resource_admission.enabled);
assert!(policy.resource_admission.required_labels.is_empty());
assert!(!policy.allow_driver_config);
policy
.verify_acknowledgement(&policy.acknowledgement())
.unwrap();
assert!(
serde_json::from_value::<MxcComputeConfig>(serde_json::json!({
"resource_admission": { "enabled": true }
}))
.is_err()
);
}
#[test]
fn static_configuration_requires_an_absolute_wxc_path() {
for path in ["", "wxc-exec.exe", "relative\\wxc-exec.exe"] {
@@ -1426,6 +1437,98 @@ mod tests {
assert!(!backend.config.state_dir.exists());
}
fn with_ui_policy(
mut sandbox: DriverSandbox,
ui: openshell_core::proto::UiPolicy,
) -> DriverSandbox {
sandbox.spec.as_mut().unwrap().policy.as_mut().unwrap().ui = Some(ui);
sandbox
}
#[test]
fn ui_policy_reaches_mapper_and_capability_is_advertised() {
use openshell_core::proto::{UiClipboardAccess, UiPolicy};
let (backend, sandbox, _dir) = preflight_fixture();
assert!(
backend
.capabilities()
.extension
.unwrap()
.supported_capabilities
.iter()
.any(|capability| capability == openshell_core::extension_protocol::POLICY_UI_V1)
);
for clipboard in [
UiClipboardAccess::None,
UiClipboardAccess::Read,
UiClipboardAccess::Write,
UiClipboardAccess::All,
] {
let sandbox = with_ui_policy(
sandbox.clone(),
UiPolicy {
allow_graphical_ui: true,
clipboard: clipboard as i32,
allow_input_injection: true,
},
);
backend
.validate_sandbox_create(&sandbox)
.expect("valid UI policy");
let mapped = backend
.map_sandbox_policy(
&sandbox.id,
sandbox.spec.as_ref().unwrap().policy.as_ref(),
"127.0.0.1:3128".parse().unwrap(),
)
.unwrap();
let ui = mapped.ui.unwrap();
assert!(!ui.disable);
assert!(ui.injection);
assert!(!backend.config.state_dir.exists());
}
}
#[test]
fn ui_policy_rejects_invalid_settings_before_provisioning() {
use openshell_core::proto::{UiClipboardAccess, UiPolicy};
let (backend, sandbox, _dir) = preflight_fixture();
for ui in [
UiPolicy {
clipboard: 999,
..Default::default()
},
UiPolicy {
clipboard: UiClipboardAccess::Read as i32,
..Default::default()
},
UiPolicy {
allow_input_injection: true,
..Default::default()
},
] {
let sandbox = with_ui_policy(sandbox.clone(), ui);
let error = backend.validate_sandbox_create(&sandbox).unwrap_err();
assert_eq!(error.code(), tonic::Code::InvalidArgument);
assert!(!backend.config.state_dir.exists());
}
}
#[test]
fn isolation_session_does_not_advertise_ui_policy() {
let (mut backend, _sandbox, _dir) = preflight_fixture();
backend.config.backend = MxcBackend::IsolationSession;
assert!(
!backend
.capabilities()
.extension
.unwrap()
.supported_capabilities
.iter()
.any(|capability| capability == openshell_core::extension_protocol::POLICY_UI_V1)
);
}
#[tokio::test]
async fn provisioning_rejects_missing_authentication_before_side_effects() {
let (backend, sandbox, _dir) = preflight_fixture();
+83 -50
View File
@@ -8,9 +8,9 @@ use std::net::SocketAddr;
use std::path::PathBuf;
use openshell_isolation_interface::contract::{
BackendError, BinaryIdentity, DirectProxyConfiguration, ExecutableIdentity,
OuterFenceGuarantees, ResolvedWorkloadIdentity,
BackendError, OuterFenceGuarantee, OuterFenceGuarantees, ResolvedWorkloadIdentity,
};
use openshell_mxc_boundary::launch::{MxcBoundaryConfig, MxcLaunchDescriptor, MxcProxyConfig};
use openshell_sandbox_backend::boundary_protocol::{
BoundaryConfig, BoundaryListener, GatewayVerificationKey, SandboxRuntimeDescriptor,
SandboxTlsClientConfig, SandboxTlsServerConfig, SandboxTransport,
@@ -20,6 +20,7 @@ use serde::Serialize;
#[derive(Serialize)]
#[allow(clippy::struct_excessive_bools)] // separately serialized enforcement properties, not state flags
struct MxcOuterFenceEvidence<'a> {
compatibility_assertion: &'a str,
generation: &'a str,
containment: &'a str,
default_deny_filesystem: bool,
@@ -47,8 +48,8 @@ pub struct MxcBoundarySpec {
}
pub struct MxcBoundaryProvisioning {
pub boundary_config: BoundaryConfig,
pub runtime_descriptor: SandboxRuntimeDescriptor,
pub boundary_config: MxcBoundaryConfig,
pub runtime_descriptor: MxcLaunchDescriptor,
}
impl MxcBoundarySpec {
@@ -74,6 +75,10 @@ impl MxcBoundarySpec {
resource_digest,
)?;
let resource_claims = BTreeMap::from([
(
"mxc.control_transport".to_string(),
"reverse_tcp".to_string(),
),
("mxc.generation".to_string(), self.generation.clone()),
(
"mxc.appcontainer_profile".to_string(),
@@ -81,6 +86,7 @@ impl MxcBoundarySpec {
),
]);
let evidence = serde_json::to_vec(&MxcOuterFenceEvidence {
compatibility_assertion: "windows-parity-unverified-egress-and-revocation",
generation: &self.generation,
containment: "process_container",
default_deny_filesystem: true,
@@ -91,60 +97,87 @@ impl MxcBoundarySpec {
.map_err(|error| {
BackendError::Descriptor(format!("encode MXC outer-fence evidence: {error}"))
})?;
// Configuration intent is not enforcement evidence. In particular,
// allowing all host loopback ports does not prove NoUnmanagedEgressPath,
// and this launch path has no verified live-revocation mechanism.
// Keep main's confirmation gate closed until native evidence can
// establish every required guarantee.
let outer_fence =
OuterFenceGuarantees::from_enforcement_evidence(&self.generation, [], &evidence)?;
let direct_proxy = DirectProxyConfiguration {
let outer_fence = windows_parity_outer_fence(&self.generation, &evidence)?;
let proxy = MxcProxyConfig {
bind_addr: self.proxy_addr,
authorization: self.proxy_authorization,
binary_identity: BinaryIdentity {
executable: ExecutableIdentity {
path: self.workload_binary,
digest: None,
},
ancestors: Vec::new(),
cmdline_paths: Vec::new(),
},
workload_binary: self.workload_binary,
};
proxy.validate()?;
Ok(MxcBoundaryProvisioning {
boundary_config: BoundaryConfig {
boundary_id: self.boundary_id.clone(),
generation: self.generation.clone(),
session_id: self.session_id,
session_rotation: self.session_rotation,
auth_epoch: self.auth_epoch,
gateway_id: self.gateway_id,
verification_keys: self.verification_keys,
listener: BoundaryListener::TlsTcp {
address: self.control_addr,
tls: self.sandbox_tls,
boundary_config: MxcBoundaryConfig {
proxy_url: self.proxy_url,
runtime: BoundaryConfig {
boundary_id: self.boundary_id.clone(),
generation: self.generation.clone(),
session_id: self.session_id,
session_rotation: self.session_rotation,
auth_epoch: self.auth_epoch,
gateway_id: self.gateway_id,
verification_keys: self.verification_keys,
listener: BoundaryListener::TlsTcp {
address: self.control_addr,
tls: self.sandbox_tls,
},
resource_claims: resource_claims.clone(),
resource_claim_files: BTreeMap::new(),
workload_identity: workload_identity.clone(),
outer_fence: outer_fence.clone(),
child_env: self.child_env,
},
resource_claims: resource_claims.clone(),
resource_claim_files: BTreeMap::new(),
workload_identity: workload_identity.clone(),
outer_fence: outer_fence.clone(),
direct_proxy_url: Some(self.proxy_url),
child_env: self.child_env,
},
runtime_descriptor: SandboxRuntimeDescriptor {
boundary_id: self.boundary_id,
generation: self.generation,
session_id: self.session_id,
workload_identity,
transport: SandboxTransport::Tcp {
authority: self.control_addr.to_string(),
addresses: vec![self.control_addr],
runtime_descriptor: MxcLaunchDescriptor {
proxy,
runtime: SandboxRuntimeDescriptor {
boundary_id: self.boundary_id,
generation: self.generation,
session_id: self.session_id,
workload_identity,
transport: SandboxTransport::Tcp {
authority: self.control_addr.to_string(),
addresses: vec![self.control_addr],
},
tls: self.supervisor_tls,
host_gateway_ip: Some(self.proxy_addr.ip()),
resource_claims,
outer_fence,
},
tls: self.supervisor_tls,
host_gateway_ip: Some(self.proxy_addr.ip()),
direct_proxy: Some(direct_proxy),
resource_claims,
outer_fence,
},
})
}
}
fn windows_parity_outer_fence(
generation: &str,
evidence: &[u8],
) -> Result<OuterFenceGuarantees, BackendError> {
// TODO(mxc-network-fence): Remove this temporary Windows-branch parity stub
// once supervisor-only egress and live revocation are implemented. Broad
// 127.0.0.1 access permits bypassing the proxy through other host services;
// these assertions are NOT verified enforcement evidence. Keep this waiver
// confined to MXC rather than weakening the shared confirmation contract.
OuterFenceGuarantees::from_enforcement_evidence(
generation,
[
OuterFenceGuarantee::DefaultDenyEgress,
OuterFenceGuarantee::NoUnmanagedEgressPath,
OuterFenceGuarantee::RevocationVerified,
OuterFenceGuarantee::ControllerLossFailsClosed,
],
evidence,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn windows_parity_stub_preserves_generation_binding() {
let fence = windows_parity_outer_fence("generation-a", b"compatibility-stub").unwrap();
assert!(fence.validate("generation-a").is_ok());
assert!(fence.validate("generation-b").is_err());
assert!(windows_parity_outer_fence("", b"compatibility-stub").is_err());
assert!(windows_parity_outer_fence("generation-a", b"").is_err());
}
}
@@ -42,6 +42,30 @@ fn websocket_fixture_grants_read_only_workload_without_egress() {
assert!(mapped.ui.unwrap().disable);
}
#[test]
fn https_fixture_authorizes_direct_curl_with_read_only_https() {
let yaml = std::fs::read_to_string(examples().join("e2e-policies/https-ca.yaml"))
.unwrap()
.replace("__CURL_EXE__", r"C:\Windows\System32\curl.exe");
let (policy, mapped) = map_fixture(&yaml);
let rule = &policy.network_policies["https_example"];
assert_eq!(rule.binaries[0].path, r"C:\Windows\System32\curl.exe");
assert_eq!(rule.endpoints[0].host, "example.com");
assert_eq!(rule.endpoints[0].ports, [443]);
assert_eq!(
rule.endpoints[0].access,
NetworkAccessPreset::ReadOnly as i32
);
assert_eq!(
rule.endpoints[0].enforcement,
NetworkEnforcementMode::Enforce as i32
);
assert_eq!(
mapped.trimmed_policy.unwrap().network_policies,
policy.network_policies
);
}
#[test]
fn openclaw_fixture_preserves_explicit_ui_and_node_network_grants() {
let yaml =
@@ -178,11 +202,16 @@ $errors = $null
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
$env:OPENSHELL_BUILD_SCRIPT, [ref]$tokens, [ref]$errors)
if ($errors.Count) { throw "build script parse errors: $($errors.Message -join '; ')" }
$function = $ast.Find({ param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq 'Stage-Z3Runtime'
}, $true)
if ($null -eq $function) { throw 'missing staging function' }
Invoke-Expression $function.Extent.Text
foreach ($name in @('Get-Sha256', 'Stage-Z3Runtime')) {
$function = $ast.Find({ param($node)
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name
}, $true)
if ($null -eq $function) { throw "missing function: $name" }
Invoke-Expression $function.Extent.Text
}
# Mise's Windows PowerShell may not expose this module cmdlet. Packaging must
# retain its checksum checks without depending on it.
function Get-FileHash { throw 'Get-FileHash must not be used' }
$TargetDir = $env:OPENSHELL_TEST_ROOT
$PrebuiltZ3Version = '5.1.0'
$env:Z3_LIBRARY_PATH_OVERRIDE = $null
@@ -197,11 +226,11 @@ $bytes[68] = 0x64; $bytes[69] = 0xAA
[IO.File]::WriteAllBytes($source, $bytes)
Stage-Z3Runtime 'aarch64-pc-windows-msvc'
$destination = Join-Path $release 'libz3.dll'
if ((Get-FileHash $destination).Hash -ne (Get-FileHash $source).Hash) { throw 'copy mismatch' }
if ((Get-Sha256 $destination) -ne (Get-Sha256 $source)) { throw 'copy mismatch' }
# A stale adjacent DLL must be overwritten from the pinned cache.
[IO.File]::WriteAllBytes($destination, (New-Object byte[] 128))
Stage-Z3Runtime 'aarch64-pc-windows-msvc'
if ((Get-FileHash $destination).Hash -ne (Get-FileHash $source).Hash) { throw 'stale DLL retained' }
if ((Get-Sha256 $destination) -ne (Get-Sha256 $source)) { throw 'stale DLL retained' }
$bytes[68] = 0x64; $bytes[69] = 0x86
[IO.File]::WriteAllBytes($source, $bytes)
$rejected = $false
@@ -31,15 +31,9 @@
#![cfg(target_os = "windows")]
use base64::Engine as _;
use openshell_core::proto::compute::v1::{DriverSandbox, DriverSandboxSpec, DriverSandboxTemplate};
use openshell_core::proto::{
FilesystemPolicy, NetworkAccessPreset, NetworkBinary, NetworkEndpoint, NetworkEnforcementMode,
NetworkPolicyRule, NetworkTlsMode, SandboxPolicy,
};
use openshell_driver_mxc::{MxcComputeBackend, MxcComputeConfig};
use openshell_core::proto::{FilesystemPolicy, NetworkEndpoint, NetworkPolicyRule, SandboxPolicy};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
// ── Path resolution ──────────────────────────────────────────────────────────
@@ -819,12 +813,11 @@ fn pc_oneshot_in_policy_write_succeeds() {
);
}
/// Run an HTTPS request through the real driver and `ProcessContainer`. The
/// workload explicitly reads the injected bundle before curl uses it, proving
/// that the driver's internal TLS share is reachable from the `AppContainer`.
#[tokio::test]
/// Run HTTPS through a real gateway, authenticated host supervisor, and MXC.
/// Curl consumes the injected bundle and records the proxy's peer certificate.
#[test]
#[ignore = "requires real wxc-exec and outbound HTTPS"]
async fn pc_https_egress_reads_injected_ca_bundle() {
fn pc_https_egress_reads_injected_ca_bundle() {
let Some(wxc) = wxc_path() else {
eprintln!("SKIP: wxc-exec not found");
return;
@@ -840,7 +833,6 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
}
let system_root = std::env::var("SYSTEMROOT").expect("SYSTEMROOT must be set on Windows");
let cmd = PathBuf::from(&system_root).join("System32").join("cmd.exe");
let curl = PathBuf::from(system_root).join("System32").join("curl.exe");
if !curl.exists() {
eprintln!("SKIP: Windows curl.exe not found at {}", curl.display());
@@ -850,104 +842,38 @@ async fn pc_https_egress_reads_injected_ca_bundle() {
let output_dir = tempfile::tempdir().expect("HTTPS output directory");
let output_path = output_dir.path().join("example.html");
let certificate_path = output_dir.path().join("peer-certificate.txt");
let output_dir_string = output_dir.path().to_string_lossy().into_owned();
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%\" \
https://example.com/ --output \"{output_path_string}\" \
--write-out \"%{{certs}}\" 1>\"{certificate_path_string}\"",
curl.display()
);
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,
}) else {
unreachable!();
let repo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let target_root =
std::env::var_os("CARGO_TARGET_DIR").map_or_else(|| repo.join("target"), PathBuf::from);
let target = if cfg!(target_arch = "aarch64") {
"aarch64-pc-windows-msvc"
} else {
"x86_64-pc-windows-msvc"
};
let policy = SandboxPolicy {
version: 1,
filesystem: Some(FilesystemPolicy {
include_workdir: false,
read_only: Vec::new(),
read_write: vec![output_dir_string],
}),
network_policies: std::collections::HashMap::from([(
"https_example".to_string(),
NetworkPolicyRule {
name: "https-example".to_string(),
endpoints: vec![NetworkEndpoint {
host: "example.com".to_string(),
ports: vec![443],
protocol: "rest".to_string(),
tls: NetworkTlsMode::Unspecified as i32,
enforcement: NetworkEnforcementMode::Enforce as i32,
access: NetworkAccessPreset::ReadOnly as i32,
..Default::default()
}],
// curl owns the socket; cmd only launches it (#4199).
binaries: vec![NetworkBinary { path: curl_string }],
},
)]),
..Default::default()
};
let sandbox = DriverSandbox {
id: "pc-https-ca".to_string(),
name: "pc-https-ca".to_string(),
spec: Some(DriverSandboxSpec {
template: Some(DriverSandboxTemplate {
driver_config: Some(
openshell_core::proto_struct::json_object_to_struct(driver_config)
.expect("driver config"),
),
..Default::default()
}),
policy: Some(policy),
..Default::default()
}),
..Default::default()
};
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
let config = MxcComputeConfig {
wxc_exec_path: wxc.to_string_lossy().into_owned(),
// This fixture intentionally submits a caller-selected curl command.
allow_driver_config: true,
..Default::default()
};
let backend = MxcComputeBackend::new(openshell_core::config::DEFAULT_GATEWAY_NAME, config);
backend
.create_sandbox(&sandbox)
.await
.expect("real HTTPS sandbox create accepted");
let mut terminal_condition = None;
for _ in 0..600 {
if let Some(observed) = backend.get_sandbox("pc-https-ca").await
&& let Some(condition) = observed
.status
.and_then(|status| status.conditions.into_iter().find(|c| c.r#type == "Ready"))
&& matches!(
condition.reason.as_str(),
"AgentCompleted" | "ExecFailed" | "ProvisionFailed"
)
{
terminal_condition = Some(condition);
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
let condition = terminal_condition.expect("HTTPS sandbox should reach a terminal condition");
assert_eq!(
condition.reason, "AgentCompleted",
"HTTPS workload failed: {}",
condition.message
let binaries = std::env::var_os("OPENSHELL_MXC_TEST_BINARY_DIR")
.map_or_else(|| target_root.join(target).join("release"), PathBuf::from);
let port = std::net::TcpListener::bind("127.0.0.1:0")
.expect("allocate HTTPS fixture gateway port")
.local_addr()
.unwrap()
.port();
let result = Command::new("powershell.exe")
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"])
.arg(repo.join("crates/openshell-driver-mxc/examples/run-mxc-e2e.ps1"))
.args(["-Scenario", "https-ca", "-Port", &port.to_string()])
.arg("-WxcExecPath")
.arg(wxc)
.arg("-BinaryDir")
.arg(binaries)
.arg("-DemoDir")
.arg(output_dir.path())
.output()
.expect("run authenticated HTTPS E2E harness");
assert!(
result.status.success(),
"HTTPS E2E failed (build native release binaries first):\n{}\n{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
assert!(output_path.exists(), "curl should write the HTTPS response");
assert!(
@@ -133,7 +133,6 @@ impl VmBoundarySpec {
// reserved host aliases terminate at its loopback address
// after crossing the authenticated boundary channel.
host_gateway_ip: Some(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
direct_proxy: None,
resource_claims,
outer_fence,
},
+8
View File
@@ -153,6 +153,14 @@ struct MxcFactory;
#[cfg(all(target_os = "windows", feature = "compute-driver-mxc"))]
#[async_trait::async_trait]
impl openshell_server::ComputeDriverFactory for MxcFactory {
fn admission_policy(
&self,
context: openshell_server::ComputeDriverConfigContext<'_>,
) -> openshell_core::Result<openshell_core::resource_admission::DriverAdmissionConfig> {
let config: openshell_driver_mxc::MxcComputeConfig = context.driver_config()?;
Ok(config.admission_policy())
}
fn supports_config_preflight(&self) -> bool {
true
}
@@ -370,15 +370,6 @@ pub trait BoundBoundary: Send {
/// Retained by the supervisor before consuming `Bound`.
fn network_mediation_source(&self) -> Arc<dyn NetworkMediationSource>;
/// Driver-provisioned direct proxy listener for backends whose outer
/// fence can route workload traffic to a host listener but cannot stage
/// individual socket opens. The supervisor owns this listener and its
/// policy evaluation; the generation-scoped authorization prevents other
/// local processes from entering the sandbox's policy context.
fn direct_proxy_configuration(&self) -> Option<DirectProxyConfiguration> {
None
}
/// Trusted host-side dial target for the well-known host-gateway aliases.
///
/// Backends return this when the mediation service runs outside the
@@ -395,31 +386,6 @@ pub trait BoundBoundary: Send {
async fn confirm(self: Box<Self>) -> Result<ConfirmedBoundary, BackendError>;
}
/// Authenticated host listener used by an isolation backend's explicit-proxy
/// path.
///
/// This is control-plane material and must be delivered through the protected
/// runtime descriptor, never command-line arguments or logs.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DirectProxyConfiguration {
pub bind_addr: SocketAddr,
/// Exact HTTP `Proxy-Authorization` value required from this generation.
pub authorization: String,
/// Driver-resolved identity applied to direct-listener requests.
pub binary_identity: BinaryIdentity,
}
impl fmt::Debug for DirectProxyConfiguration {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("DirectProxyConfiguration")
.field("bind_addr", &self.bind_addr)
.field("authorization", &"<redacted>")
.field("binary_identity", &self.binary_identity)
.finish()
}
}
/// Backend-neutral guarantees established by the component that owns the outer
/// network fence.
///
@@ -512,6 +478,7 @@ pub struct EnforcedProperty {
pub enforced: bool,
pub mechanism: String,
}
impl EnforcedProperty {
#[must_use]
pub fn new(enforced: bool, mechanism: impl Into<String>) -> Self {
@@ -383,6 +383,7 @@ fn complete_outer_fence(generation: &str, evidence: &[u8]) -> OuterFenceGuarante
)
.unwrap()
}
fn confirmation() -> BoundaryConfirmation {
BoundaryConfirmation {
generation: "generation-1".to_string(),
+5 -1
View File
@@ -32,9 +32,13 @@ rustls-pemfile = { workspace = true }
tokio-rustls = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
windows = { workspace = true }
windows = { workspace = true, features = ["Win32_Security_Authorization"] }
[target.'cfg(target_os = "windows")'.dev-dependencies]
tempfile = "3"
[lints]
workspace = true
+12 -3
View File
@@ -42,6 +42,7 @@ payload; the authenticated workload proxy URL stays in the MXC bootstrap.
Windows supervisor composition turns these into ordinary shared CONNECT
listener options. Shared isolation traits and Sandbox Protocol descriptors
contain no MXC proxy fields, and other compute drivers need no placeholders.
MXC adopts main's authenticated image-policy discovery and ordered provider
environment publication. Command-based MXC has no rootfs image policy to
discover. Publication generations prevent stale refreshes from replacing newer
@@ -68,9 +69,17 @@ still requires native ProcessContainer networking and permits all ports on
`127.0.0.1`; it is not isolation from unrelated host-loopback services.
Main requires explicit proof of no unmanaged egress path and verified
revocation. The current driver publishes no established outer-fence guarantees
because its configuration alone does not prove those properties. Confirmation
therefore remains fail-closed, including on an otherwise qualified PSEC host.
revocation. For Windows-branch parity testing, the MXC driver temporarily
asserts those guarantees without establishing them. Network audit assertions
are likewise compatibility stubs, labeled as unverified in mechanism metadata.
This does not restrict access to unrelated host-loopback services or establish
live revocation. TODOs in provisioning and confirmation track removing the
stubs; shared backend validation and native AppContainer checks remain intact.
The boundary measures its AppContainer SID from the current Windows process
token for audit identity. It rejects uncontained processes; a configured
container/profile name is not a substitute for that native token measurement.
This measurement alone does not establish network-fence guarantees.
Unit tests exercise real Windows process launch, output retention, and a real
ephemeral TCP forwarding target. These tests do not qualify MXC enforcement;
+4 -3
View File
@@ -7,10 +7,11 @@ use openshell_isolation_interface::contract::{BackendError, BoundaryProperties,
use openshell_sandbox_backend::audit::BoundaryAuditValidator;
use serde::{Deserialize, Serialize};
/// Windows `ProcessContainer` evidence measured by the MXC boundary.
/// Windows `ProcessContainer` audit reported by the MXC boundary.
///
/// MXC supplies the outer filesystem and network fence; the in-container
/// sandbox supplies authenticated lifecycle and process I/O.
/// sandbox supplies authenticated lifecycle and process I/O. Network assertions
/// currently include a temporary Windows-parity stub, not measured exclusivity.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[allow(
@@ -59,7 +60,7 @@ impl MxcSandboxAuditEvidence {
),
egress_interception: EnforcedProperty::new(
self.default_deny_egress && self.loopback_proxy_only,
"mxc-wfp-loopback-proxy-fence",
"mxc-windows-parity-unverified-egress-stub",
),
request_attribution: EnforcedProperty::new(
self.generation_scoped_attribution,
+164 -2
View File
@@ -8,8 +8,88 @@ use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use openshell_isolation_interface::contract::{
BackendError, BoundBoundary, IsolationBackend, SandboxContext, VerifiedBackendDescriptor,
BackendError, BoundBoundary, BoundaryDuplexStream, IsolationBackend, SandboxContext,
VerifiedBackendDescriptor,
};
use openshell_sandbox_backend::{
BoundaryTransportConnector,
boundary_protocol::{SandboxRuntimeDescriptor, SandboxTransport},
};
/// The Windows relay establishes control outward from MXC; preserve that
/// direction without embedding supervision or policy in the compute driver.
#[derive(Debug, Default)]
pub struct MxcReverseTcpConnector {
listener: tokio::sync::Mutex<Option<Arc<tokio::net::TcpListener>>>,
}
#[async_trait]
impl BoundaryTransportConnector for MxcReverseTcpConnector {
async fn connect(
&self,
descriptor: &SandboxRuntimeDescriptor,
) -> Result<BoundaryDuplexStream, BackendError> {
if descriptor
.resource_claims
.get("mxc.control_transport")
.map(String::as_str)
!= Some("reverse_tcp")
{
return Err(BackendError::Descriptor(
"MXC reverse control provisioning is required".into(),
));
}
let SandboxTransport::Tcp { addresses, .. } = &descriptor.transport else {
return Err(BackendError::Descriptor(
"MXC reverse control requires TCP".into(),
));
};
let [address] = addresses.as_slice() else {
return Err(BackendError::Descriptor(
"MXC reverse control requires one address".into(),
));
};
if !address.ip().is_loopback() || address.port() == 0 {
return Err(BackendError::Descriptor(
"MXC reverse control requires a concrete loopback endpoint".into(),
));
}
let listener = {
let mut current = self.listener.lock().await;
if let Some(listener) = current.as_ref() {
if listener
.local_addr()
.map_err(|e| BackendError::Unavailable(e.to_string()))?
!= *address
{
return Err(BackendError::Descriptor(
"MXC reverse control endpoint changed".into(),
));
}
listener.clone()
} else {
let listener =
Arc::new(tokio::net::TcpListener::bind(address).await.map_err(|e| {
BackendError::Unavailable(format!("bind MXC reverse control: {e}"))
})?);
*current = Some(listener.clone());
listener
}
};
let (stream, _) =
tokio::time::timeout(std::time::Duration::from_secs(30), listener.accept())
.await
.map_err(|_| {
BackendError::Unavailable("MXC reverse control accept timed out".into())
})?
.map_err(|e| {
BackendError::Unavailable(format!("accept MXC reverse control: {e}"))
})?;
openshell_core::net::set_tcp_nodelay_best_effort(&stream);
// TLS peer verification and JWT authentication remain shared and mandatory.
Ok(Box::new(stream))
}
}
/// The host-side MXC isolation backend. The compute driver owns provisioning;
/// this backend owns attachment and interpretation of Windows confirmation.
@@ -25,6 +105,7 @@ impl MxcRuntimeBackend {
ca_file_paths: Arc<Mutex<Option<(PathBuf, PathBuf)>>>,
provider_credentials: openshell_core::provider_credentials::ProviderCredentialState,
sandbox_bearer: openshell_core::jwt::SessionBearerTokenSlot,
connector: Arc<dyn BoundaryTransportConnector>,
) -> Self {
Self {
transport: openshell_sandbox_backend::OpenShellRuntimeBackend::new(
@@ -32,7 +113,8 @@ impl MxcRuntimeBackend {
provider_credentials,
sandbox_bearer,
)
.with_audit_validator(Arc::new(crate::audit::MxcBoundaryAuditValidator)),
.with_audit_validator(Arc::new(crate::audit::MxcBoundaryAuditValidator))
.with_transport_connector(connector),
}
}
}
@@ -74,6 +156,7 @@ mod tests {
std::collections::HashMap::default(),
),
openshell_core::jwt::SessionBearerTokenSlot::empty(),
Arc::new(MxcReverseTcpConnector::default()),
))
}
@@ -113,4 +196,83 @@ mod tests {
Err(BackendError::Descriptor(_))
));
}
#[tokio::test]
async fn reverse_connector_reuses_listener_and_rejects_endpoint_changes() {
use openshell_isolation_interface::contract::{
OuterFenceGuarantees, ResolvedWorkloadIdentity,
};
use openshell_sandbox_backend::boundary_protocol::SandboxTlsClientConfig;
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
let reservation = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let address = reservation.local_addr().unwrap();
drop(reservation);
let descriptor = SandboxRuntimeDescriptor {
boundary_id: "test".into(),
generation: "test".into(),
session_id: "550e8400-e29b-41d4-a716-446655440000".parse().unwrap(),
workload_identity: ResolvedWorkloadIdentity::new(
1,
1,
Vec::new(),
"test".into(),
"test".into(),
)
.unwrap(),
transport: SandboxTransport::Tcp {
authority: address.to_string(),
addresses: vec![address],
},
tls: SandboxTlsClientConfig {
server_name: "test".into(),
trust_anchor_pem: "test".into(),
},
host_gateway_ip: None,
resource_claims: std::collections::BTreeMap::from([(
"mxc.control_transport".into(),
"reverse_tcp".into(),
)]),
outer_fence: OuterFenceGuarantees::from_enforcement_evidence("test", [], b"test")
.unwrap(),
};
let connector = Arc::new(MxcReverseTcpConnector::default());
for _ in 0..2 {
let host_connector = connector.clone();
let host_descriptor = descriptor.clone();
let host =
tokio::spawn(
async move { host_connector.connect(&host_descriptor).await.unwrap() },
);
let mut peer = tokio::time::timeout(std::time::Duration::from_secs(3), async {
loop {
if let Ok(stream) = tokio::net::TcpStream::connect(address).await {
break stream;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
})
.await
.unwrap();
let mut stream = host.await.unwrap();
peer.write_all(b"hello").await.unwrap();
let mut received = [0; 5];
stream.read_exact(&mut received).await.unwrap();
assert_eq!(&received, b"hello");
}
let mut other = descriptor.clone();
let SandboxTransport::Tcp { addresses, .. } = &mut other.transport else {
unreachable!()
};
addresses[0] = "127.0.0.1:1".parse().unwrap();
assert!(matches!(
connector.connect(&other).await,
Err(BackendError::Descriptor(_))
));
other.resource_claims.clear();
assert!(matches!(
connector.connect(&other).await,
Err(BackendError::Descriptor(_))
));
}
}
@@ -0,0 +1,141 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Measured Windows process containment identity; never an authored profile name.
use windows::Win32::Foundation::{CloseHandle, HANDLE, HLOCAL, LocalFree};
use windows::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows::Win32::Security::{
GetLengthSid, GetTokenInformation, IsValidSid, TOKEN_APPCONTAINER_INFORMATION, TOKEN_QUERY,
TokenAppContainerSid, TokenIsAppContainer,
};
use windows::Win32::System::Threading::{
GetCurrentProcessId, OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION,
};
struct OwnedHandle(HANDLE);
#[allow(unsafe_code)]
impl Drop for OwnedHandle {
fn drop(&mut self) {
// SAFETY: this guard owns a real handle acquired by a successful Win32
// call, and closes it exactly once. It never wraps pseudo handles.
let _ = unsafe { CloseHandle(self.0) };
}
}
#[allow(unsafe_code)]
pub fn current_appcontainer_sid() -> Result<String, String> {
// SAFETY: GetCurrentProcessId has no pointer or lifetime preconditions.
process_appcontainer_sid(unsafe { GetCurrentProcessId() })
}
#[allow(unsafe_code)]
fn process_appcontainer_sid(process_id: u32) -> Result<String, String> {
// SAFETY: owned handles are closed by guards on every exit path. Query
// buffers are correctly sized/aligned and remain alive while SID pointers
// are used. The SID extent is checked before conversion.
unsafe {
let process = OwnedHandle(
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, process_id)
.map_err(|error| format!("open MXC process identity: {error}"))?,
);
let mut token = HANDLE::default();
OpenProcessToken(process.0, TOKEN_QUERY, &raw mut token)
.map_err(|error| format!("open MXC process token: {error}"))?;
let token = OwnedHandle(token);
let mut contained = 0_u32;
let mut returned = 0_u32;
GetTokenInformation(
token.0,
TokenIsAppContainer,
Some(std::ptr::from_mut(&mut contained).cast()),
4,
&raw mut returned,
)
.map_err(|error| format!("query MXC AppContainer token: {error}"))?;
if returned != 4 || contained != 1 {
return Err("MXC boundary process is not an AppContainer".to_string());
}
// A SID has at most 15 subauthorities. This aligned buffer is larger
// than TOKEN_APPCONTAINER_INFORMATION plus the largest Windows SID.
let mut buffer = [0_usize; 32];
let length = u32::try_from(size_of_val(&buffer)).expect("fixed token buffer fits u32");
GetTokenInformation(
token.0,
TokenAppContainerSid,
Some(buffer.as_mut_ptr().cast()),
length,
&raw mut returned,
)
.map_err(|error| format!("query MXC AppContainer SID: {error}"))?;
if returned < u32::try_from(size_of::<TOKEN_APPCONTAINER_INFORMATION>()).unwrap()
|| returned > length
{
return Err("MXC AppContainer SID buffer length is invalid".to_string());
}
let info = &*buffer.as_ptr().cast::<TOKEN_APPCONTAINER_INFORMATION>();
let start = buffer.as_ptr() as usize;
let end = start + returned as usize;
let sid = info.TokenAppContainer.0 as usize;
// Check the fixed SID header before reading its subauthority count.
if sid < start || sid > end.saturating_sub(8) {
return Err("MXC AppContainer SID pointer is invalid".to_string());
}
let count = *info.TokenAppContainer.0.cast::<u8>().add(1);
let sid_length = 8 + usize::from(count) * 4;
if count > 15
|| sid_length > end - sid
|| !IsValidSid(info.TokenAppContainer).as_bool()
|| GetLengthSid(info.TokenAppContainer) as usize != sid_length
{
return Err("MXC AppContainer SID is invalid".to_string());
}
let mut text = windows::core::PWSTR::null();
ConvertSidToStringSidW(info.TokenAppContainer, &raw mut text)
.map_err(|error| format!("format MXC AppContainer SID: {error}"))?;
let result = text.to_string();
let _ = LocalFree(Some(HLOCAL(text.0.cast())));
let sid = result.map_err(|error| format!("decode MXC AppContainer SID: {error}"))?;
if !sid.starts_with("S-1-15-2-") {
return Err("MXC token SID is not an AppContainer identity".to_string());
}
Ok(sid)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_uncontained_host_process_identity() {
assert!(current_appcontainer_sid().is_err());
}
#[test]
fn rejects_invalid_process_identity() {
assert!(process_appcontainer_sid(0).is_err());
}
#[test]
#[ignore = "requires execution inside a real native MXC ProcessContainer"]
fn measures_native_appcontainer_identity() {
let sid = current_appcontainer_sid().expect("native AppContainer SID must be measurable");
assert!(sid.starts_with("S-1-15-2-"));
}
#[test]
#[ignore = "requires native MXC with all host loopback and egress denied"]
fn native_same_container_loopback() {
current_appcontainer_sid().expect("must run inside native MXC");
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let client =
std::net::TcpStream::connect_timeout(&address, std::time::Duration::from_secs(3))
.expect("same-container loopback must work without host-loopback permission");
let (_server, _) = listener.accept().unwrap();
drop(client);
}
}
+177
View File
@@ -0,0 +1,177 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! MXC-owned host launch data, separate from the shared Sandbox Protocol.
use openshell_isolation_interface::contract::{
BackendError, BinaryIdentity, ExecutableIdentity, Sha256Digest,
};
use openshell_sandbox_backend::boundary_protocol::{BoundaryConfig, SandboxRuntimeDescriptor};
use serde::{Deserialize, Serialize};
use sha2::{Digest as _, Sha256};
use std::{fmt, net::SocketAddr, path::PathBuf};
/// Protected, generation-bound launch envelope decoded by Windows composition.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MxcLaunchDescriptor {
pub runtime: SandboxRuntimeDescriptor,
pub proxy: MxcProxyConfig,
}
/// One-use Windows bootstrap; proxy credentials are never in shared protocol data.
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MxcBoundaryConfig {
pub runtime: BoundaryConfig,
pub proxy_url: String,
}
impl MxcBoundaryConfig {
pub fn encode(&self) -> Result<Vec<u8>, BackendError> {
serde_json::to_vec(self)
.map_err(|e| BackendError::Descriptor(format!("encode MXC bootstrap: {e}")))
}
}
impl fmt::Debug for MxcBoundaryConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MxcBoundaryConfig")
.field("runtime", &self.runtime)
.field("proxy_url", &"<redacted>")
.finish()
}
}
/// Only MXC needs to provision an authenticated host CONNECT listener.
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MxcProxyConfig {
pub bind_addr: SocketAddr,
pub authorization: String,
pub workload_binary: PathBuf,
}
impl fmt::Debug for MxcProxyConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MxcProxyConfig")
.field("bind_addr", &self.bind_addr)
.field("authorization", &"<redacted>")
.field("workload_binary", &self.workload_binary)
.finish()
}
}
impl MxcLaunchDescriptor {
pub fn decode(payload: &[u8]) -> Result<Self, BackendError> {
let descriptor: Self = serde_json::from_slice(payload)
.map_err(|e| BackendError::Descriptor(format!("decode MXC launch data: {e}")))?;
descriptor.proxy.validate()?;
Ok(descriptor)
}
}
impl MxcProxyConfig {
/// Supply actual digest evidence for the configured main workload. This
/// retains MXC's existing fixed-main attribution; it is not per-connection
/// socket-owner or descendant identity discovery.
pub fn binary_identity(&self) -> Result<BinaryIdentity, BackendError> {
self.validate()?;
let bytes = std::fs::read(&self.workload_binary).map_err(|error| {
BackendError::Descriptor(format!("read MXC workload identity: {error}"))
})?;
let digest: Sha256Digest =
format!("{:x}", Sha256::digest(&bytes))
.parse()
.map_err(|error| {
BackendError::Descriptor(format!("hash MXC workload identity: {error}"))
})?;
Ok(BinaryIdentity {
executable: ExecutableIdentity {
path: self.workload_binary.clone(),
digest: Some(digest),
},
ancestors: Vec::new(),
cmdline_paths: Vec::new(),
})
}
pub fn validate(&self) -> Result<(), BackendError> {
if !self.bind_addr.ip().is_loopback()
|| self.bind_addr.port() == 0
|| self.authorization.trim().is_empty()
|| self.authorization.contains(['\r', '\n'])
|| !self.workload_binary.is_absolute()
{
return Err(BackendError::Descriptor(
"MXC proxy requires a loopback listener, single-line authorization, and an absolute workload binary".into(),
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn proxy() -> MxcProxyConfig {
MxcProxyConfig {
bind_addr: "127.0.0.1:3128".parse().unwrap(),
authorization: "Basic generation-secret".into(),
workload_binary: "C:/Windows/System32/cmd.exe".into(),
}
}
#[test]
fn proxy_configuration_is_validated_by_mxc() {
assert!(proxy().validate().is_ok());
for address in ["0.0.0.0:3128", "192.0.2.1:3128", "127.0.0.1:0"] {
let mut config = proxy();
config.bind_addr = address.parse().unwrap();
assert!(config.validate().is_err());
}
for authorization in ["", " ", "Basic token\r\nInjected: header"] {
let mut config = proxy();
config.authorization = authorization.into();
assert!(config.validate().is_err());
}
let mut config = proxy();
config.workload_binary = "relative.exe".into();
assert!(config.validate().is_err());
}
#[test]
fn proxy_identity_hashes_workload_and_rejects_unreadable_evidence() {
let directory = tempfile::tempdir().unwrap();
let executable = directory.path().join("workload.exe");
let mut config = proxy();
config.workload_binary = executable.clone();
assert!(config.binary_identity().is_err());
std::fs::write(&executable, b"first workload").unwrap();
let first = config.binary_identity().unwrap();
assert_eq!(first.executable.path, executable);
assert_eq!(
first.executable.digest,
Some(
format!("{:x}", Sha256::digest(b"first workload"))
.parse()
.unwrap()
)
);
std::fs::write(&executable, b"replacement workload").unwrap();
assert_ne!(
first.executable.digest,
config.binary_identity().unwrap().executable.digest
);
}
#[test]
fn launch_credentials_are_redacted_and_unknown_fields_rejected() {
assert!(!format!("{:?}", proxy()).contains("generation-secret"));
let mut value = serde_json::to_value(proxy()).unwrap();
value["unexpected"] = serde_json::json!(true);
assert!(serde_json::from_value::<MxcProxyConfig>(value).is_err());
assert!(MxcLaunchDescriptor::decode(b"{}").is_err());
}
}
+4
View File
@@ -12,6 +12,10 @@
pub mod audit;
#[cfg(target_os = "windows")]
pub mod backend;
#[cfg(target_os = "windows")]
mod identity;
#[cfg(target_os = "windows")]
pub mod launch;
/// Exact admission name for the MXC isolation backend; never a Linux fallback.
pub const BACKEND_NAME: &str = "openshell-mxc";
+57 -58
View File
@@ -9,7 +9,6 @@
use std::collections::{HashMap, HashSet, VecDeque};
use std::io;
use std::mem::size_of_val;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
@@ -51,7 +50,7 @@ const OUTPUT_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
pub fn run_boundary(config_path: &Path) -> Result<(), String> {
let bytes = std::fs::read(config_path)
.map_err(|error| format!("read boundary config {}: {error}", config_path.display()))?;
let config: BoundaryConfig = serde_json::from_slice(&bytes)
let config: crate::launch::MxcBoundaryConfig = serde_json::from_slice(&bytes)
.map_err(|error| format!("decode boundary config {}: {error}", config_path.display()))?;
validate_config(&config)?;
let runtime_directory = config_path
@@ -67,7 +66,7 @@ pub fn run_boundary(config_path: &Path) -> Result<(), String> {
.build()
.map_err(|error| format!("create Windows boundary runtime: {error}"))?;
runtime.block_on(async move {
let (address, tls) = match &config.listener {
let (address, tls) = match &config.runtime.listener {
BoundaryListener::TlsTcp { address, tls } => (*address, tls.clone()),
BoundaryListener::Unix { .. } | BoundaryListener::Vsock { .. } => {
return Err("MXC requires a TLS TCP boundary listener".to_string());
@@ -77,14 +76,21 @@ pub fn run_boundary(config_path: &Path) -> Result<(), String> {
let boundary = Arc::new(BoundaryRuntime::new(config, runtime_directory)?);
tracing::info!(%address, "MXC Sandbox Protocol reverse control ready");
loop {
let (stream, _) = listener
.accept()
.await
.map_err(|error| format!("accept MXC boundary connection: {error}"))?;
openshell_core::net::set_tcp_nodelay_best_effort(&stream);
// Match the windows branch relay: MXC dials the host, avoiding
// host-to-AppContainer inbound authorization requirements. TLS
// roles do not change: this boundary still proves its pinned identity.
let stream =
match openshell_core::net::connect_tcp_nodelay_best_effort(&[address]).await {
Ok(stream) => stream,
Err(error) => {
tracing::debug!(%error, "MXC reverse control host not ready");
tokio::time::sleep(Duration::from_millis(200)).await;
continue;
}
};
let acceptor = tokio_rustls::TlsAcceptor::from(tls.clone());
let boundary = boundary.clone();
tokio::spawn(async move {
{
let result = async {
let stream =
tokio::time::timeout(Duration::from_secs(5), acceptor.accept(stream))
@@ -99,12 +105,22 @@ pub fn run_boundary(config_path: &Path) -> Result<(), String> {
if let Err(error) = result {
tracing::debug!(%error, "MXC boundary connection ended");
}
});
}
tokio::time::sleep(Duration::from_millis(200)).await;
}
})
}
fn validate_config(config: &BoundaryConfig) -> Result<(), String> {
fn validate_config(launch: &crate::launch::MxcBoundaryConfig) -> Result<(), String> {
let config = &launch.runtime;
if config
.resource_claims
.get("mxc.control_transport")
.map(String::as_str)
!= Some("reverse_tcp")
{
return Err("MXC boundary requires reverse TCP control provisioning".to_string());
}
if config.boundary_id.trim().is_empty()
|| config.generation.trim().is_empty()
|| config.gateway_id.trim().is_empty()
@@ -119,6 +135,7 @@ fn validate_config(config: &BoundaryConfig) -> Result<(), String> {
match &config.listener {
BoundaryListener::TlsTcp { address, tls }
if address.port() != 0
&& address.ip().is_loopback()
&& tls.certificate_chain_path.is_absolute()
&& tls.private_key_path.is_absolute() => {}
BoundaryListener::TlsTcp { .. } => {
@@ -128,10 +145,8 @@ fn validate_config(config: &BoundaryConfig) -> Result<(), String> {
return Err("MXC boundary supports only TLS TCP transport".to_string());
}
}
let Some(proxy_url) = config.direct_proxy_url.as_deref() else {
return Err("MXC boundary requires a generation-scoped direct proxy".to_string());
};
let url = proxy_url
let url = launch
.proxy_url
.parse::<url::Url>()
.map_err(|error| format!("validate MXC direct proxy URL: {error}"))?;
if url.scheme() != "http"
@@ -625,17 +640,15 @@ impl BoundaryRuntime {
}
fn confirmation(&self) -> Result<BoundaryConfirmation, String> {
let appcontainer_sid = crate::identity::current_appcontainer_sid()?;
let evidence = MxcSandboxAuditEvidence {
process_container: current_process_is_appcontainer()?,
appcontainer_profile: self
.config
.resource_claims
.get("mxc.appcontainer_profile")
.cloned()
.unwrap_or_else(|| self.config.generation.clone()),
process_container: !appcontainer_sid.is_empty(),
appcontainer_profile: appcontainer_sid,
default_deny_filesystem: true,
// TODO(mxc-network-fence): Windows parity assertions, not measured
// exclusive mediation. MXC still allows all host 127.0.0.1 ports.
default_deny_egress: true,
loopback_proxy_only: self.config.direct_proxy_url.is_some(),
loopback_proxy_only: !self.proxy_url.is_empty(),
authenticated_control: true,
generation_scoped_attribution: true,
};
@@ -959,41 +972,6 @@ impl BoundaryRuntime {
}
}
#[allow(unsafe_code)]
fn current_process_is_appcontainer() -> Result<bool, String> {
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Security::{GetTokenInformation, TOKEN_QUERY, TokenIsAppContainer};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
let mut is_appcontainer = 0_u32;
let buffer_size = u32::try_from(size_of_val(&is_appcontainer))
.map_err(|error| format!("size MXC AppContainer token buffer: {error}"))?;
// SAFETY: the token handle is initialized by OpenProcessToken, queried into
// a correctly sized u32 buffer, and closed on every path after acquisition.
unsafe {
let mut token = HANDLE::default();
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &raw mut token)
.map_err(|error| format!("open MXC sandbox process token: {error}"))?;
let mut returned = 0_u32;
let query = GetTokenInformation(
token,
TokenIsAppContainer,
Some(std::ptr::from_mut(&mut is_appcontainer).cast()),
buffer_size,
&raw mut returned,
);
let close = CloseHandle(token);
query.map_err(|error| format!("query MXC sandbox AppContainer token: {error}"))?;
close.map_err(|error| format!("close MXC sandbox process token: {error}"))?;
if returned != buffer_size {
return Err(format!(
"query MXC sandbox AppContainer token returned {returned} bytes"
));
}
Ok(is_appcontainer != 0)
}
}
async fn serve_one(
mut stream: tokio::io::DuplexStream,
runtime: Arc<BoundaryRuntime>,
@@ -1306,6 +1284,27 @@ impl ManagedProcess {
let mut child = command
.spawn()
.map_err(|error| format!("spawn MXC workload executable {program:?}: {error}"))?;
if matches!(kind, ProcessKindWire::Main) {
openshell_ocsf::ocsf_emit!(
openshell_ocsf::ProcessActivityBuilder::new(openshell_ocsf::ctx::ctx())
.activity(openshell_ocsf::ActivityId::Open)
.action(openshell_ocsf::ActionId::Allowed)
.disposition(openshell_ocsf::DispositionId::Allowed)
.severity(openshell_ocsf::SeverityId::Informational)
.status(openshell_ocsf::StatusId::Success)
.launch_type(openshell_ocsf::LaunchTypeId::Spawn)
.process(openshell_ocsf::Process::new(
&program,
i64::from(child.id().unwrap_or_default()),
))
.message("MXC agent launched")
.build()
);
// Preserve the windows branch's launch acknowledgement for existing
// operators and E2E tooling. Process OCSF shorthand omits the message;
// emit this acknowledgement only after the native spawn succeeds.
tracing::info!(process_id = %id, "MXC agent launched");
}
let stdin = child.stdin.take();
let stdout = child
.stdout
@@ -21,14 +21,36 @@ fn shared_runtime_keeps_platform_audit_interpretation_in_backend_extension() {
let runtime = include_str!("../../openshell-sandbox-backend/src/runtime.rs");
assert!(runtime.contains("self.audit_validator.validate(&confirmation.backend_audit)?"));
assert!(runtime.contains("confirmation.properties != properties"));
let composition = include_str!("../../openshell-supervisor/src/isolation_backends.rs");
let composition = include_str!("../../openshell-supervisor/src/backend_setup/mxc.rs");
assert!(composition.contains("MxcRuntimeBackend::new"));
assert!(composition.contains("impl BackendSetup for MxcBackendSetup"));
assert!(composition.contains("impl PreparedBackend for MxcLaunch"));
let supervisor = include_str!("../../openshell-supervisor/src/lib.rs");
assert!(!supervisor.contains("serde_json::from_slice(&backend_descriptor.payload)"));
assert!(!supervisor.contains("OpenShellRuntimeBackend::discover_policy"));
assert!(!supervisor.contains("MxcReverseTcpConnector"));
let backend = include_str!("../src/backend.rs");
assert!(backend.contains("impl IsolationBackend for MxcRuntimeBackend"));
assert!(backend.contains("MxcBoundaryAuditValidator"));
assert!(backend.contains("self.transport.attach(descriptor, sandbox).await"));
}
#[test]
fn proxy_launch_data_does_not_extend_shared_isolation_contracts() {
let contract = include_str!("../../openshell-isolation-interface/src/contract.rs");
let protocol = include_str!("../../openshell-sandbox-backend/src/boundary_protocol.rs");
assert!(!contract.contains("DirectProxyConfiguration"));
assert!(!contract.contains("direct_proxy_configuration"));
assert!(!protocol.contains("direct_proxy"));
for driver in [
include_str!("../../openshell-driver-docker/src/isolation.rs"),
include_str!("../../openshell-driver-kubernetes/src/isolation.rs"),
include_str!("../../openshell-driver-vm/src/isolation/mod.rs"),
] {
assert!(!driver.contains("direct_proxy"));
}
}
#[test]
fn only_dedicated_sandbox_delegates_to_the_windows_boundary_library() {
let manifest = include_str!("../../openshell-sandbox/Cargo.toml");
@@ -24,6 +24,7 @@ the execution outcome unknown.
Exec envelopes without an expiration time are rejected. Update the supervisor
and sandbox runtime together when deploying this protocol change.
## Boundary audit validation
`OpenShellRuntimeBackend` implements the authenticated host side of the shared
@@ -59,6 +59,7 @@ pub const BOUNDARY_CONNECTION_WINDOW_BYTES: u32 = BOUNDARY_MAX_CONCURRENT_STREAM
+ BOUNDARY_CONNECTION_WINDOW_RESERVE_BYTES;
// HTTP/2 caps any flow-control window at 2^31 - 1.
const _: () = assert!(BOUNDARY_CONNECTION_WINDOW_BYTES <= i32::MAX as u32);
/// Capability masks measured from `/proc/<pid>/status` by the `OpenShell`
/// co-located runtime.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
@@ -439,12 +440,6 @@ pub struct SandboxRuntimeDescriptor {
/// network supervisor cannot use the boundary's resolver view.
#[serde(default)]
pub host_gateway_ip: Option<std::net::IpAddr>,
/// Optional generation-scoped explicit proxy owned by the host
/// supervisor. Backends set this only when their outer fence routes the
/// workload to this listener and the boundary cannot provide staged
/// socket mediation.
#[serde(default)]
pub direct_proxy: Option<openshell_isolation_interface::contract::DirectProxyConfiguration>,
/// Driver-specific immutable resource coordinates bound at attach (for
/// example pod UID, VM generation, or container ID).
#[serde(default)]
@@ -463,7 +458,6 @@ impl fmt::Debug for SandboxRuntimeDescriptor {
.field("transport", &self.transport)
.field("tls", &self.tls)
.field("host_gateway_ip", &self.host_gateway_ip)
.field("direct_proxy", &self.direct_proxy)
.field("resource_claims", &self.resource_claims)
.field("outer_fence", &self.outer_fence)
.finish()
@@ -518,13 +512,8 @@ pub struct BoundaryConfig {
pub resource_claim_files: std::collections::BTreeMap<String, PathBuf>,
/// Exact identity already applied by the runtime to the sandbox process.
pub workload_identity: openshell_isolation_interface::contract::ResolvedWorkloadIdentity,
/// Backend-neutral projection of the validated outer fence.
/// Backend-neutral projection of the validated outer network fence.
pub outer_fence: OuterFenceGuarantees,
/// Authenticated proxy URL injected into workload children. It is staged
/// only in this protected one-use configuration and is never inherited by
/// the trusted sandbox process itself.
#[serde(default)]
pub direct_proxy_url: Option<String>,
/// Driver-resolved environment exposed only to workload processes.
#[serde(default)]
pub child_env: std::collections::HashMap<String, String>,
@@ -553,10 +542,6 @@ impl fmt::Debug for BoundaryConfig {
.field("resource_claim_files", &self.resource_claim_files)
.field("workload_identity", &self.workload_identity)
.field("outer_fence", &self.outer_fence)
.field(
"direct_proxy_url",
&self.direct_proxy_url.as_ref().map(|_| "<redacted>"),
)
.field("child_env_keys", &self.child_env.keys().collect::<Vec<_>>())
.finish()
}
@@ -1537,6 +1522,7 @@ mod tests {
assert!(audit.validate().is_err());
assert!(!audit.properties().egress_interception.enforced);
}
#[test]
fn binary_identity_wire_rejects_ambiguous_or_invalid_shapes() {
for encoded in [
@@ -130,6 +130,7 @@ impl OpenShellRuntimeBackend {
self.connector = Some(connector);
self
}
/// Select the backend implementation that validates opaque audit evidence.
#[must_use]
pub fn with_audit_validator(
@@ -158,7 +159,6 @@ impl IsolationBackend for OpenShellRuntimeBackend {
})?;
validate_runtime_descriptor(&runtime_descriptor, &sandbox)?;
let host_gateway_ip = runtime_descriptor.host_gateway_ip;
let direct_proxy = runtime_descriptor.direct_proxy.clone();
let resource_claims = runtime_descriptor.resource_claims.clone();
let generation = runtime_descriptor.generation.clone();
let session_id = runtime_descriptor.session_id;
@@ -189,7 +189,6 @@ impl IsolationBackend for OpenShellRuntimeBackend {
sandbox_id: sandbox.sandbox_id,
mediation: Arc::new(RemoteNetworkMediation { client }),
host_gateway_ip,
direct_proxy,
ca_file_paths: self.ca_file_paths.clone(),
provider_credentials: self.provider_credentials.clone(),
identity: sandbox.identity,
@@ -259,18 +258,6 @@ fn validate_runtime_descriptor(
}
}
validate_client_tls(&runtime_descriptor.tls)?;
if let Some(proxy) = &runtime_descriptor.direct_proxy
&& (!proxy.bind_addr.ip().is_loopback()
|| proxy.bind_addr.port() == 0
|| proxy.authorization.trim().is_empty()
|| proxy.authorization.contains(['\r', '\n'])
|| !proxy.binary_identity.executable.path.is_absolute())
{
return Err(BackendError::Descriptor(
"direct proxy requires a loopback listener, a single-line authorization value, and an absolute binary identity"
.to_string(),
));
}
Ok(())
}
@@ -348,7 +335,6 @@ struct RemoteBound {
sandbox_id: String,
mediation: Arc<RemoteNetworkMediation>,
host_gateway_ip: Option<std::net::IpAddr>,
direct_proxy: Option<openshell_isolation_interface::contract::DirectProxyConfiguration>,
ca_file_paths: Arc<std::sync::Mutex<Option<(PathBuf, PathBuf)>>>,
provider_credentials: openshell_core::provider_credentials::ProviderCredentialState,
identity: openshell_isolation_interface::contract::ResolvedWorkloadIdentity,
@@ -368,12 +354,6 @@ impl BoundBoundary for RemoteBound {
self.host_gateway_ip
}
fn direct_proxy_configuration(
&self,
) -> Option<openshell_isolation_interface::contract::DirectProxyConfiguration> {
self.direct_proxy.clone()
}
async fn confirm(self: Box<Self>) -> Result<ConfirmedBoundary, BackendError> {
let response = self.client.call_idempotent(Request::Confirm).await?;
let Response::Confirmed { confirmation } = response else {
@@ -2827,7 +2807,6 @@ mod tests {
},
tls,
host_gateway_ip: None,
direct_proxy: None,
resource_claims: std::collections::BTreeMap::new(),
outer_fence: test_outer_fence(),
}
@@ -3091,7 +3070,6 @@ mod tests {
},
tls: certificate.client_tls.clone(),
host_gateway_ip: Some(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
direct_proxy: None,
resource_claims: std::collections::BTreeMap::new(),
outer_fence: test_outer_fence(),
};
@@ -3112,7 +3090,6 @@ mod tests {
},
tls: test_certificate().client_tls,
host_gateway_ip: None,
direct_proxy: None,
resource_claims: std::collections::BTreeMap::new(),
outer_fence: test_outer_fence(),
};
@@ -3135,7 +3112,6 @@ mod tests {
},
tls: test_certificate().client_tls,
host_gateway_ip: None,
direct_proxy: None,
resource_claims: std::collections::BTreeMap::new(),
outer_fence: test_outer_fence(),
};
@@ -3158,7 +3134,6 @@ mod tests {
},
tls: test_certificate().client_tls,
host_gateway_ip: None,
direct_proxy: None,
resource_claims: std::collections::BTreeMap::new(),
outer_fence: test_outer_fence(),
};
@@ -3467,7 +3442,6 @@ mod tests {
},
tls: certificate.client_tls,
host_gateway_ip: None,
direct_proxy: None,
resource_claims: std::collections::BTreeMap::new(),
outer_fence: test_outer_fence(),
},
@@ -3661,7 +3635,6 @@ mod tests {
},
tls: certificate.client_tls,
host_gateway_ip: None,
direct_proxy: None,
resource_claims: std::collections::BTreeMap::new(),
outer_fence: test_outer_fence(),
},
-1
View File
@@ -61,7 +61,6 @@ tokio-rustls = { workspace = true }
base64 = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
url = { workspace = true }
# Logging
tracing = { workspace = true }
+38 -16
View File
@@ -352,9 +352,6 @@ pub struct ComputeDriverInfoSnapshot {
pub rootfs_tar_staging_dir: String,
/// Maximum rootfs tar file size in bytes.
pub rootfs_tar_max_bytes: u64,
/// Whether this configured driver instance completely enforces the portable
/// UI policy contract.
pub supports_ui_policy: bool,
}
// Startup recovery owns persisted desired state before normal observations
@@ -785,7 +782,6 @@ impl ComputeRuntime {
resource_capabilities: capabilities.resource_capabilities,
rootfs_tar_staging_dir: capabilities.rootfs_tar_staging_dir,
rootfs_tar_max_bytes: capabilities.rootfs_tar_max_bytes,
supports_ui_policy: capabilities.supports_ui_policy,
};
let default_image = capabilities.default_image;
let rootfs_tar_staging = Arc::new(rootfs_tar::RootfsTarStagingRegistry::new(
@@ -1145,8 +1141,14 @@ impl ComputeRuntime {
.or_else(|| sandbox.spec.as_ref().and_then(|spec| spec.policy.as_ref()))
.and_then(|policy| policy.ui.as_ref())
.is_some();
if has_explicit_ui && !self.driver_info.supports_ui_policy {
return Err(Status::invalid_argument(format!(
let supports_ui = self
.driver_info
.negotiated_extension
.supported_capabilities
.iter()
.any(|capability| capability == openshell_core::extension_protocol::POLICY_UI_V1);
if has_explicit_ui && !supports_ui {
return Err(Status::failed_precondition(format!(
"compute driver '{}' does not support the complete UI policy contract; remove the explicit ui section or select a supporting driver/backend",
self.driver_info.name
)));
@@ -7428,7 +7430,6 @@ impl ComputeDriver for NoopTestDriver {
"test",
[],
)),
supports_ui_policy: false,
},
))
}
@@ -7606,7 +7607,6 @@ pub fn new_test_runtime_with_driver(
resource_capabilities: None,
rootfs_tar_staging_dir: String::new(),
rootfs_tar_max_bytes: 0,
supports_ui_policy: false,
},
telemetry_compute_driver: TelemetryComputeDriver::custom(),
driver_process: None,
@@ -7631,7 +7631,6 @@ pub fn new_test_runtime_with_driver(
mod tests {
use super::*;
use futures::stream;
use openshell_core::proto::SandboxPolicy as PublicSandboxPolicy;
use openshell_core::proto::compute::v1::{
CreateSandboxResponse, DeleteSandboxResponse, GetCapabilitiesResponse, GetSandboxRequest,
GetSandboxResponse, StartSandboxResponse, StopSandboxRequest, StopSandboxResponse,
@@ -8048,7 +8047,6 @@ mod tests {
}
metadata
}),
supports_ui_policy: false,
}))
}
@@ -8452,7 +8450,6 @@ mod tests {
"test",
[],
)),
supports_ui_policy: false,
}))
}
@@ -8714,7 +8711,6 @@ mod tests {
resource_capabilities: None,
rootfs_tar_staging_dir: String::new(),
rootfs_tar_max_bytes: 0,
supports_ui_policy: false,
},
telemetry_compute_driver: TelemetryComputeDriver::custom(),
driver_process: None,
@@ -8757,7 +8753,7 @@ mod tests {
.await
.expect_err("an unsupported driver must reject explicit UI policy");
assert_eq!(error.code(), Code::InvalidArgument);
assert_eq!(error.code(), Code::FailedPrecondition);
assert!(error.message().contains("complete UI policy contract"));
assert_eq!(
driver.validate_create_calls.load(Ordering::Relaxed),
@@ -8776,7 +8772,7 @@ mod tests {
.await
.expect_err("an internal caller must not bypass UI capability validation");
assert_eq!(error.code(), Code::InvalidArgument);
assert_eq!(error.code(), Code::FailedPrecondition);
assert_eq!(
driver.create_calls.load(Ordering::Relaxed),
0,
@@ -8803,7 +8799,7 @@ mod tests {
.await
.expect_err("an unsupported driver must reject the effective UI policy");
assert_eq!(error.code(), Code::InvalidArgument);
assert_eq!(error.code(), Code::FailedPrecondition);
assert_eq!(driver.validate_create_calls.load(Ordering::Relaxed), 0);
}
@@ -8811,7 +8807,11 @@ mod tests {
async fn explicit_ui_policy_reaches_driver_when_capability_is_complete() {
let driver = Arc::new(TestDriver::default());
let mut runtime = test_runtime(driver.clone()).await;
runtime.driver_info.supports_ui_policy = true;
runtime
.driver_info
.negotiated_extension
.supported_capabilities
.push(openshell_core::extension_protocol::POLICY_UI_V1.to_string());
runtime
.validate_sandbox_create(&sandbox_with_explicit_ui("sb-ui-supported"))
@@ -8821,6 +8821,27 @@ mod tests {
assert_eq!(driver.validate_create_calls.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn effective_ui_policy_rejects_before_unsupported_driver_create() {
let driver = Arc::new(TestDriver::default());
let runtime = test_runtime(driver.clone()).await;
let sandbox = sandbox_record(
"sb-effective-ui-create",
"ui-policy",
SandboxPhase::Provisioning,
);
let runtime_inputs = SandboxCreateRuntimeInputs::new(PublicSandboxPolicy {
ui: Some(UiPolicy::default()),
..Default::default()
});
let error = runtime
.create_sandbox_with_runtime_inputs(sandbox, None, false, runtime_inputs)
.await
.expect_err("effective UI must be checked on internal creation");
assert_eq!(error.code(), Code::FailedPrecondition);
assert_eq!(driver.create_calls.load(Ordering::Relaxed), 0);
}
#[tokio::test]
async fn absent_ui_policy_preserves_unsupported_driver_behavior() {
let driver = Arc::new(TestDriver::default());
@@ -8834,6 +8855,7 @@ mod tests {
assert_eq!(driver.validate_create_calls.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn create_runtime_inputs_reach_driver_without_persisting_effective_policy_or_secrets() {
let driver = Arc::new(TestDriver::default());
-1
View File
@@ -288,7 +288,6 @@ impl OpenShell for OpenShellService {
.resource_capabilities
.as_ref()
.map(|resources| public_resource_capabilities(*resources)),
supports_ui_policy: driver.supports_ui_policy,
}),
})
.collect();
@@ -8695,18 +8695,6 @@ mod tests {
assert!(inputs.launch_authentication.is_none());
}
#[tokio::test]
async fn create_runtime_inputs_preserve_base_policy_without_credential_sink() {
let state = test_server_state().await;
let policy = openshell_policy::restrictive_default_policy();
let sandbox = test_sandbox("create-policy", "create-policy", policy.clone(), Vec::new());
let inputs = resolve_sandbox_create_runtime_inputs(state.as_ref(), &sandbox)
.await
.expect("driver-independent effective policy");
assert_eq!(inputs.effective_policy, Some(policy));
assert!(inputs.launch_authentication.is_none());
}
#[tokio::test]
async fn create_runtime_inputs_apply_global_policy_and_preserve_startup_ui() {
let state = test_server_state().await;
@@ -161,7 +161,6 @@ impl FakeComputeDriver {
gateway_manages_lifecycle: false,
supports_sandbox_authentication: false,
driver_reports_runtime_readiness: false,
supports_ui_policy: false,
resource_capabilities: None,
rootfs_tar_staging_dir: String::new(),
rootfs_tar_max_bytes: 0,
@@ -758,7 +758,6 @@ mod tests {
));
}
#[cfg(unix)]
#[tokio::test]
async fn signaling_without_boundary_is_rejected() {
let session = MainSession::inert();
+11 -2
View File
@@ -4,7 +4,9 @@ The supervisor loads and reconciles policy, maintains provider credentials, appl
## Backend startup
The public `run_sandbox` entry point selects the OpenShell Sandbox Protocol backend and collects its startup inputs into a private `SandboxRunConfig`. Shared startup receives that config and the trusted backend setup separately. The `backend_setup` module owns that backend's launch-data decoder, workload policy discovery, and client construction. Descriptor contents cannot select an implementation.
The public `run_sandbox` entry point selects the platform backend and collects its startup inputs into a private `SandboxRunConfig`. Shared startup receives that config and the trusted backend setup separately. The `backend_setup` module owns the selected backend's launch-data decoder, workload policy discovery, and client construction. Descriptor contents cannot select an implementation.
Windows composition selects MXC; other platforms retain the standard OpenShell Sandbox Protocol backend. The MXC setup retains one reverse-TCP connector through discovery, attachment, and reconnects, and builds the host-side isolation backend with its Windows audit validator. Provisioning remains in the compute driver; supervision remains in a separate host process.
Shared startup checks the admitted backend name before passing the opaque payload to its decoder. It then compares the decoded sandbox, session, and runtime generation with the trusted launch inputs before installing credentials or discovering workload policy. A mismatch stops startup.
@@ -14,7 +16,14 @@ The supervisor admits policy and prepares credentials before constructing and at
The client receives the supervisor's live provider state, bearer-token slot, and CA-path slot. Provider refresh, token rotation, and later CA publication must remain visible through those shared handles. Startup does not create independent copies of their current values.
The setup interface stays private to the supervisor. It adds no runtime backend registration, endpoint configuration, or public factory API. The public `run_sandbox` signature and standard backend selection remain unchanged.
The setup interface stays private to the supervisor. It adds no runtime backend registration, endpoint configuration, or public factory API. The public `run_sandbox` signature remains unchanged.
MXC proxy settings are decoded by Windows backend setup from an MXC-owned
launch envelope. The private startup result carries the transport payload and
concrete CONNECT listener options separately. Shared networking consumes those
options after attachment and confirmation; the host supervisor still owns policy
evaluation, live credentials, and listener lifetime. Neither `BoundBoundary` nor
the shared Sandbox Protocol descriptor exposes a proxy-configuration hook.
The private startup result separates the backend transport payload from optional
authenticated CONNECT listener settings. Shared networking starts the listener
@@ -16,6 +16,21 @@ use openshell_isolation_interface::contract::{
ResolvedWorkloadIdentity, SandboxContext, SandboxPolicy,
};
#[cfg(target_os = "windows")]
mod mxc;
/// Trusted binary composition chooses the platform implementation, never the payload.
pub fn platform_setup() -> &'static dyn BackendSetup {
#[cfg(target_os = "windows")]
{
&mxc::MxcBackendSetup
}
#[cfg(not(target_os = "windows"))]
{
&OpenShellBackendSetup
}
}
/// Coordinates decoded by the selected trusted backend. Shared startup checks
/// these against admission before credentials or discovery reach that backend.
pub struct LaunchIdentity {
@@ -201,8 +216,10 @@ impl SelectedBackend {
/// The standard binary selects the `OpenShell` Sandbox Protocol. Its wire schema
/// and concrete client stay here rather than in the shared startup sequence.
#[cfg(any(test, not(target_os = "windows")))]
pub struct OpenShellBackendSetup;
#[cfg(any(test, not(target_os = "windows")))]
impl BackendSetup for OpenShellBackendSetup {
fn backend_name(&self) -> &str {
openshell_sandbox_backend::BACKEND_NAME
@@ -233,8 +250,10 @@ impl BackendSetup for OpenShellBackendSetup {
}
}
#[cfg(any(test, not(target_os = "windows")))]
struct OpenShellLaunch(openshell_sandbox_backend::boundary_protocol::SandboxRuntimeDescriptor);
#[cfg(any(test, not(target_os = "windows")))]
#[tonic::async_trait]
impl PreparedBackend for OpenShellLaunch {
async fn discover_policy(
@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Trusted Windows composition of the MXC isolation backend.
use super::{BackendServices, BackendSetup, BuiltBackend, LaunchIdentity, PreparedBackend};
use openshell_core::jwt::SessionBearerTokenSlot;
use openshell_isolation_interface::contract::BackendError;
use openshell_mxc_boundary::backend::{MxcReverseTcpConnector, MxcRuntimeBackend};
use openshell_mxc_boundary::launch::MxcLaunchDescriptor;
use openshell_supervisor_network::run::ProxyListenerConfig;
use std::sync::Arc;
pub(super) struct MxcBackendSetup;
impl BackendSetup for MxcBackendSetup {
fn backend_name(&self) -> &str {
openshell_mxc_boundary::BACKEND_NAME
}
fn decode(
&self,
payload: &[u8],
) -> Result<(LaunchIdentity, Box<dyn PreparedBackend>), BackendError> {
let descriptor = MxcLaunchDescriptor::decode(payload)?;
let identity = LaunchIdentity {
sandbox_id: descriptor.runtime.boundary_id.clone(),
generation: descriptor.runtime.generation.clone(),
session_id: descriptor.runtime.session_id,
workload_identity: descriptor.runtime.workload_identity.clone(),
vm_policy_identity: None,
};
Ok((
identity,
Box::new(MxcLaunch {
descriptor,
connector: Arc::new(MxcReverseTcpConnector::default()),
}),
))
}
}
struct MxcLaunch {
descriptor: MxcLaunchDescriptor,
// Retain the same listener across discovery, attachment, and reconnects.
connector: Arc<MxcReverseTcpConnector>,
}
#[tonic::async_trait]
impl PreparedBackend for MxcLaunch {
async fn discover_policy(
&self,
bearer: SessionBearerTokenSlot,
) -> Result<(Option<String>, bool), BackendError> {
openshell_sandbox_backend::OpenShellRuntimeBackend::discover_policy_with_connector(
self.descriptor.runtime.clone(),
bearer,
Some(self.connector.clone()),
)
.await
}
fn build(self: Box<Self>, services: BackendServices) -> Result<BuiltBackend, BackendError> {
let payload = self.descriptor.runtime.backend_descriptor()?.payload;
let proxy = self.descriptor.proxy;
let binary_identity = proxy.binary_identity()?;
Ok(BuiltBackend {
payload,
proxy_listener: Some(ProxyListenerConfig {
bind_addr: proxy.bind_addr,
authorization: proxy.authorization.into(),
binary_identity,
}),
backend: Arc::new(MxcRuntimeBackend::new(
services.ca_file_paths,
services.provider_credentials,
services.sandbox_bearer,
self.connector,
)),
})
}
}
@@ -626,6 +626,98 @@ fn standard_setup_decodes_native_descriptor_and_preserves_vm_identity() {
}
}
#[cfg(target_os = "windows")]
#[test]
fn mxc_setup_preserves_admitted_identity_and_constructs_only_mxc() {
use openshell_sandbox_backend::boundary_protocol::{
SandboxRuntimeDescriptor, SandboxTlsClientConfig, SandboxTransport,
};
let setup = TestSetup::new();
let descriptor = SandboxRuntimeDescriptor {
boundary_id: setup.sandbox_id.clone(),
generation: setup.auth.runtime_generation.to_string(),
session_id: setup.auth.session_id,
workload_identity: identity(),
transport: SandboxTransport::Tcp {
authority: "unused-test-runtime".into(),
addresses: vec!["127.0.0.1:1".parse().unwrap()],
},
tls: SandboxTlsClientConfig {
server_name: "unused-test-runtime".into(),
trust_anchor_pem: String::new(),
},
host_gateway_ip: None,
resource_claims: BTreeMap::from([("mxc.control_transport".into(), "reverse_tcp".into())]),
outer_fence: OuterFenceGuarantees::from_enforcement_evidence(
"generation-1",
[],
b"test-evidence",
)
.unwrap(),
};
let launch = openshell_mxc_boundary::launch::MxcLaunchDescriptor {
runtime: descriptor,
proxy: openshell_mxc_boundary::launch::MxcProxyConfig {
bind_addr: "127.0.0.1:3128".parse().unwrap(),
authorization: "Basic test-generation-token".into(),
workload_binary: "C:/Windows/System32/cmd.exe".into(),
},
};
let descriptor = BackendDescriptor {
backend_name: openshell_mxc_boundary::BACKEND_NAME.into(),
payload: serde_json::to_vec(&launch).unwrap(),
};
assert_eq!(
platform_setup().backend_name(),
openshell_mxc_boundary::BACKEND_NAME
);
assert!(mxc::MxcBackendSetup.decode(b"{}").is_err());
let selected = SelectedBackend::select(
&mxc::MxcBackendSetup,
descriptor.clone(),
Some(openshell_mxc_boundary::BACKEND_NAME),
Some(&setup.sandbox_id),
&setup.auth,
)
.unwrap();
assert!(selected.vm_policy_identity().is_none());
assert_eq!(selected.identity.workload_identity, identity());
let built = selected.prepared.build(setup.services()).unwrap();
assert_eq!(
built.backend.backend_name(),
openshell_mxc_boundary::BACKEND_NAME
);
let transport: SandboxRuntimeDescriptor = serde_json::from_slice(&built.payload).unwrap();
assert_eq!(transport, launch.runtime);
assert!(
!String::from_utf8(built.payload)
.unwrap()
.contains("test-generation-token")
);
let proxy = built.proxy_listener.unwrap();
assert_eq!(proxy.bind_addr, launch.proxy.bind_addr);
assert_eq!(proxy.authorization.as_ref(), launch.proxy.authorization);
assert_eq!(
proxy.binary_identity.executable.path,
launch.proxy.workload_binary
);
// Selecting a Windows implementation does not waive main's identity checks.
let mut foreign_auth = setup.auth.clone();
foreign_auth.session_id = SandboxSessionId::new();
assert!(
SelectedBackend::select(
&mxc::MxcBackendSetup,
descriptor,
Some(openshell_mxc_boundary::BACKEND_NAME),
Some(&setup.sandbox_id),
&foreign_auth,
)
.is_err()
);
}
/// Auth slots and shutdown signals are process-wide. Run the real startup in a
/// child so parallel tests cannot replace credentials or consume its SIGTERM.
#[cfg(unix)]
@@ -1,29 +0,0 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Binary composition of platform isolation-backend implementations.
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use openshell_isolation_interface::contract::IsolationBackend;
pub fn sandbox_backend(
ca_file_paths: Arc<Mutex<Option<(PathBuf, PathBuf)>>>,
provider_credentials: openshell_core::provider_credentials::ProviderCredentialState,
sandbox_bearer: openshell_core::jwt::SessionBearerTokenSlot,
) -> Arc<dyn IsolationBackend> {
#[cfg(not(target_os = "windows"))]
let backend = openshell_sandbox_backend::OpenShellRuntimeBackend::new(
ca_file_paths,
provider_credentials,
sandbox_bearer,
);
#[cfg(target_os = "windows")]
let backend = openshell_mxc_boundary::backend::MxcRuntimeBackend::new(
ca_file_paths,
provider_credentials,
sandbox_bearer,
);
Arc::new(backend)
}
+2 -2
View File
@@ -21,7 +21,6 @@ mod activity_aggregator;
mod backend_setup;
mod denial_aggregator;
mod endpoint_status;
mod isolation_backends;
mod mechanistic_mapper;
mod provider_readiness;
@@ -311,6 +310,7 @@ fn prepare_control_readiness_path(_path: &std::path::Path) -> Result<()> {
"Unix readiness sockets are unsupported on this host"
))
}
impl Drop for ControlReadiness {
fn drop(&mut self) {
self.task.abort();
@@ -670,7 +670,7 @@ pub async fn run_sandbox(
) -> Result<i32> {
// Shared startup retains policy and networking state; box it to keep callers' futures small.
Box::pin(run_sandbox_with_backend(
&backend_setup::OpenShellBackendSetup,
backend_setup::platform_setup(),
SandboxRunConfig {
command,
workdir,
+25 -3
View File
@@ -646,7 +646,7 @@ enabled = true
"openshell.ai/sandbox-attachable-workspace" = "${workspace}"
```
Use the same fields under the selected Docker, Podman, VM, MXC, or external
Use the same fields under the selected Docker, Podman, VM, or external
driver table. Omitted admission settings use the labels above. A supplied map
replaces the defaults, and every entry must match. An explicitly empty map is
invalid while enforcement is enabled. `${workspace}` is the entire label value
@@ -702,8 +702,10 @@ The exception does not approve unrelated host mounts or non-GPU devices.
Raw host bind mounts and supplemental image mounts have no trusted label
resolver and are rejected while admission is enabled. Publisher image labels
cannot grant operator approval. Docker's existing unsupported image-mount
restriction remains. MXC host filesystem grants likewise require disabling
admission until a resolver exists. MXC's command config and VM upload workflows
restriction remains. MXC does not implement external-resource label admission
and rejects `resource_admission` configuration rather than implying that labels
are checked. Its host filesystem grants rely on workload policy and a trusted
gateway operator. MXC's command config and VM upload workflows
encoded in driver JSON require explicit driver-config opt-in.
`allow_driver_config` applies to nonempty caller config, including saved
@@ -1197,6 +1199,10 @@ The MXC driver is Windows-only and opt-in. It links into the gateway and invokes
The dedicated `openshell-windows-sandbox.exe` uses a Windows boundary library for process operations and forwarding. It replaces the generic sandbox binary in the Windows runtime pair; the supervisor remains a separate host process, not part of the compute driver. No separate supervisor-relay executable is required. Set `wxc_exec_path` to an absolute path; configuration preflight rejects an omitted or relative path. Existing MXC configurations that explicitly select `openshell-sandbox.exe` must update `sandbox_binary_path` to the Windows executable.
The Windows sandbox initiates its authenticated control connection to a generation-scoped loopback listener in the host isolation backend. This follows the Windows relay's connection direction without changing host firewall rules. TLS identity verification and supervisor bearer authentication remain required.
This development branch temporarily asserts MXC outer-fence guarantees for Windows-branch compatibility testing. These assertions do not establish supervisor-only egress or verified live revocation. Workloads can bypass the proxy to reach other host-local services. Do not use this compatibility path as a production-qualified security boundary; passing E2E does not close these known gaps.
```toml
[openshell]
version = 2
@@ -1235,6 +1241,22 @@ The MXC driver requires MXC 1.0.0 and emits its stable directional network schem
Supply the workload command and working directory through `sandbox create --driver-config-json`, for example `{"mxc":{"command":["C:\\Windows\\System32\\cmd.exe","/d","/c","echo hello"],"cwd":"C:\\work"}}`. Both are required. Supply workload environment through `sandbox create --env` or `--env-from`; it is not part of gateway configuration.
Configure UI controls in the sandbox policy, not the `mxc` driver JSON:
```yaml
ui:
allow_graphical_ui: true
clipboard: read
allow_input_injection: false
```
Omitted fields deny access. Clipboard accepts `none`, `read`, `write`, or `all`,
from the sandbox's perspective. Clipboard and input injection require graphical
UI; invalid settings fail before provisioning. Delete and recreate the sandbox
to change UI settings. MXC advertises `openshell.policy.ui.v1` in its existing
extension capabilities. The gateway rejects explicit UI policy, including
`ui: {}`, when the selected driver does not advertise that capability.
The driver assigns distinct Sandbox Protocol and proxy listeners plus fresh credentials to every generation. The host proxy rejects missing, invalid, duplicate, or cross-sandbox proxy credentials before forwarding. MXC denies direct Internet egress and permits only the `127.0.0.1/32` route required by the authenticated transport and proxy. That loopback exception does not isolate unrelated host services. The current explicit-proxy path attributes descendant traffic to the admitted main workload binary rather than resolving each Windows socket owner. Treat the gateway host as trusted and avoid policies that rely on different network rights for child executables.
Attached provider credentials use the ordinary live supervisor refresh path. Static values stay in the host supervisor's endpoint-bound resolver and are injected only for matching requests. Dynamic token grants remain request-time operations in the host proxy.
+3 -2
View File
@@ -20,7 +20,7 @@ Policies](/how-it-works/policies/manage-policies).
## What a Policy Controls
A policy file sets `version: 1` and has up to five other top-level sections.
A policy file sets `version: 1` and has up to six other top-level sections.
Each section controls a different part of the sandbox and takes effect at a
specific time:
@@ -29,10 +29,11 @@ specific time:
| `filesystem_policy` | Paths that sandbox processes can read, or read and write. | Landlock LSM in the kernel. | At sandbox startup. |
| `landlock` | Whether the sandbox still starts, without your filesystem rules, if OpenShell cannot apply them. | Sandbox runtime. | At sandbox startup. |
| `process` | User and group that sandbox processes run as. | Docker and Podman, when they create the sandbox. | At sandbox creation. |
| `ui` | Graphical UI, clipboard, and synthetic input. | A compute driver advertising complete UI-policy support. | At sandbox creation. |
| `network_policies` | Destinations each binary can reach, and the requests it can send. | Sandbox network proxy. | While the sandbox runs. |
| `network_middlewares` | Additional inspection, transformation, or blocking of traffic that network rules allow. | Sandbox network proxy. | While the sandbox runs. |
Filesystem, Landlock, and process settings are fixed once the sandbox starts.
Filesystem, Landlock, process, and UI settings are fixed once the sandbox starts.
Network rules and middleware can change while it runs, as described in [How
Changes Take
Effect](/how-it-works/policies/manage-policies#how-changes-take-effect).
+28
View File
@@ -19,6 +19,7 @@ version: 1
filesystem_policy: { ... }
landlock: { ... }
process: { ... }
ui: { ... }
network_policies: { ... }
network_middlewares: { ... }
```
@@ -29,6 +30,7 @@ network_middlewares: { ... }
| `filesystem_policy` | object | No | Startup | Paths the workload can read and write. |
| `landlock` | object | No | Startup | Landlock enforcement behavior. |
| `process` | object | No | Startup | User and group that the workload runs as. |
| `ui` | object | No | Startup | Graphical UI, clipboard, and input injection on a supporting runtime. |
| `network_policies` | map | No | Live | Which binaries can reach which network endpoints. |
| `network_middlewares` | map | No | Live | Middleware applied to allowed traffic. |
@@ -100,6 +102,32 @@ process:
run_as_group: "1500"
```
## UI
UI controls require the selected compute driver to advertise
`openshell.policy.ui.v1` in its extension capabilities. Unsupported drivers
reject any explicit UI section, including
`ui: {}`, before provisioning. Omit the section to preserve their existing
behavior.
| Field | Type | Default | Description |
|---|---|---|---|
| `allow_graphical_ui` | bool | `false` | Allows a graphical UI surface. |
| `clipboard` | string | `none` | `none`, `read`, `write`, or `all`, from the sandbox's perspective. |
| `allow_input_injection` | bool | `false` | Allows synthetic input. |
Clipboard access and input injection require `allow_graphical_ui: true`.
Unknown fields and values are rejected. UI settings are startup-only; recreate
the sandbox to change them. Global policies cannot set UI and preserve each
sandbox's startup UI settings.
```yaml showLineNumbers={false}
ui:
allow_graphical_ui: true
clipboard: read
allow_input_injection: false
```
## Network Policies
A map of named rules. The key is the rule's name. Each rule allows every listed
-19
View File
@@ -260,11 +260,6 @@ openshell sandbox create --from registry.example.com/agents/my-agent:1.0
Refer to [Default Policy](/how-it-works/policies/default-policy), [Run Your First Agent](/about/run-your-first-agent), and the [bring-your-own-container example](https://github.com/NVIDIA/OpenShell/tree/main/examples/bring-your-own-container).
Reusable image and resource settings belong in sandbox templates; see
[Templates](/how-it-works/sandboxes/templates) for the current SDK workflow.
The pre-0.1.0 `base` image alias and CLI template commands do not apply to
this release; pass an explicit image reference with `--from`.
## Connect to a Sandbox
Attach to the canonical main process in a running sandbox:
@@ -972,20 +967,6 @@ When a multi-sandbox delete fails for one entry, the CLI reports that sandbox's
failure and continues with the remaining names. The command exits with an error
after it attempts every requested deletion if any entry failed.
Automation that already observed a sandbox's immutable identity can prevent a
same-name replacement from being deleted:
```shell
openshell sandbox delete my-sandbox \
--expected-id <sandbox-id> \
--expected-resource-version <resource-version>
```
Identity preconditions require exactly one sandbox name, and a resource version
may be supplied only with the immutable ID. The gateway returns an `ABORTED`
error without changing the sandbox or calling its compute driver when either
value no longer matches.
## Sandbox Lifecycle
Every sandbox moves through a defined set of phases:
-4
View File
@@ -108,10 +108,6 @@ message GetCapabilitiesResponse {
// Gateways require an exact policy match before activating the driver.
// Empty denotes a legacy driver and requires explicit admission opt-out.
string resource_admission_policy = 13;
// Whether this configured driver instance completely enforces the current
// portable SandboxPolicy.ui contract. Partial support must report false so
// the gateway rejects every explicit UI section before provisioning.
bool supports_ui_policy = 14;
}
message AuthenticateSandboxRequest {
-4
View File
@@ -971,10 +971,6 @@ message ComputeDriverCapabilities {
// Static portable resource request forms reported by the driver.
ResourceCapabilities resource_capabilities = 3;
// Whether the configured driver instance completely enforces the portable
// SandboxPolicy.ui contract.
bool supports_ui_policy = 4;
}
// Static portable resource request forms reported by a compute driver.
+11 -2
View File
@@ -80,7 +80,13 @@ probe, as runtime readiness.
### Outer fence and confirmation
MXC receives the mapped filesystem, UI, and network constraints before
`openshell-sandbox` starts. The boundary consumes and deletes its one-use
workload startup. UI remains part of `SandboxPolicy`; the gateway requires the
selected driver's existing extension metadata to advertise
`openshell.policy.ui.v1` for any explicit UI section. Unsupported drivers need
no UI-specific handling. MXC validates and maps the native UI settings before
provisioning; omitted capabilities deny and changes require recreation.
The boundary consumes and deletes its one-use
configuration and TLS private key before releasing workload code. It confirms
the ProcessContainer generation, resource claims, filesystem fence, egress
fence, authenticated control transport, and controller-loss behavior through
@@ -95,7 +101,10 @@ descriptors are stored beneath an owner-only Windows DACL.
MXC denies direct Internet egress and permits the loopback route used by the
Sandbox Protocol and explicit proxy. The host supervisor owns a distinct proxy
listener and random authorization value for every sandbox generation.
`openshell-sandbox` injects the proxy URL and public CA paths only into workload
These launch settings stay in MXC-owned envelopes. Private Windows supervisor
composition supplies listener options to shared networking without extending
`BoundBoundary` or the shared Sandbox Protocol descriptor. The dedicated
`openshell-windows-sandbox` injects the proxy URL and public CA paths only into workload
children. The supervisor retains private CA keys and provider secrets, applies
network policy, and refreshes provider state through the ordinary session.
@@ -83,7 +83,6 @@ func ComputeDriverInfoFromProto(d *pb.ComputeDriverInfo) types.ComputeDriverInfo
if caps := d.GetCapabilities(); caps != nil {
result.DriverName = caps.GetDriverName()
result.DriverVersion = caps.GetDriverVersion()
result.SupportsUIPolicy = caps.GetSupportsUiPolicy()
}
return result
}
@@ -20,9 +20,8 @@ func TestGatewayInfoFromProto(t *testing.T) {
{
Name: "k8s",
Capabilities: &pb.ComputeDriverCapabilities{
DriverName: "kubernetes",
DriverVersion: "2.1.0",
SupportsUiPolicy: true,
DriverName: "kubernetes",
DriverVersion: "2.1.0",
},
},
{
@@ -55,14 +54,12 @@ func TestGatewayInfoFromProto(t *testing.T) {
assert.Equal(t, "k8s", info.ComputeDrivers[0].Name)
assert.Equal(t, "kubernetes", info.ComputeDrivers[0].DriverName)
assert.Equal(t, "2.1.0", info.ComputeDrivers[0].DriverVersion)
assert.True(t, info.ComputeDrivers[0].SupportsUIPolicy)
assert.Equal(t, "docker", info.ComputeDrivers[1].Name)
assert.Equal(t, "docker-engine", info.ComputeDrivers[1].DriverName)
require.Len(t, info.Extensions, 1)
assert.Equal(t, v1.ExtensionKindComputeDriver, info.Extensions[0].Kind)
assert.Equal(t, "openshell/kubernetes", info.Extensions[0].ImplementationName)
assert.Equal(t, uint32(1), info.Extensions[0].ProtocolMajor)
assert.False(t, info.ComputeDrivers[1].SupportsUIPolicy)
}
func TestGatewayInfoFromProto_NoDrivers(t *testing.T) {
@@ -133,7 +130,6 @@ func TestComputeDriverInfoFromProto_NilCapabilities(t *testing.T) {
assert.Equal(t, "bare-metal", info.Name)
assert.Empty(t, info.DriverName)
assert.Empty(t, info.DriverVersion)
assert.False(t, info.SupportsUIPolicy)
}
func TestCurrentUserFromProto(t *testing.T) {
+3 -4
View File
@@ -54,10 +54,9 @@ type ExtensionInfo struct {
// ComputeDriverInfo describes a compute backend available on the gateway.
type ComputeDriverInfo struct {
Name string
DriverName string
DriverVersion string
SupportsUIPolicy bool
Name string
DriverName string
DriverVersion string
}
// CurrentUser holds the authenticated caller's identity.
+6 -17
View File
@@ -1993,11 +1993,8 @@ type ComputeDriverCapabilities struct {
DriverVersion string `protobuf:"bytes,2,opt,name=driver_version,json=driverVersion,proto3" json:"driver_version,omitempty"`
// Static portable resource request forms reported by the driver.
ResourceCapabilities *ResourceCapabilities `protobuf:"bytes,3,opt,name=resource_capabilities,json=resourceCapabilities,proto3" json:"resource_capabilities,omitempty"`
// Whether the configured driver instance completely enforces the portable
// SandboxPolicy.ui contract.
SupportsUiPolicy bool `protobuf:"varint,4,opt,name=supports_ui_policy,json=supportsUiPolicy,proto3" json:"supports_ui_policy,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ComputeDriverCapabilities) Reset() {
@@ -2051,13 +2048,6 @@ func (x *ComputeDriverCapabilities) GetResourceCapabilities() *ResourceCapabilit
return nil
}
func (x *ComputeDriverCapabilities) GetSupportsUiPolicy() bool {
if x != nil {
return x.SupportsUiPolicy
}
return false
}
// Static portable resource request forms reported by a compute driver.
// An omitted domain means the driver does not report that domain.
type ResourceCapabilities struct {
@@ -11703,8 +11693,8 @@ type UpdateConfigRequest struct {
// The new policy to apply.
//
// Sandbox scope (`global=false`):
// - only network_policies may differ from create-time
// policy; static fields must match version 1.
// - only network_policies may differ from create-time
// policy; static fields must match version 1.
//
// Global scope (`global=true`):
// - applies to all sandboxes in full (no merge).
@@ -18284,13 +18274,12 @@ const file_openshell_proto_rawDesc = "" +
"\x15required_capabilities\x18\b \x03(\tR\x14requiredCapabilities\"t\n" +
"\x11ComputeDriverInfo\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12K\n" +
"\fcapabilities\x18\x02 \x01(\v2'.openshell.v1.ComputeDriverCapabilitiesR\fcapabilities\"\xea\x01\n" +
"\fcapabilities\x18\x02 \x01(\v2'.openshell.v1.ComputeDriverCapabilitiesR\fcapabilities\"\xbc\x01\n" +
"\x19ComputeDriverCapabilities\x12\x1f\n" +
"\vdriver_name\x18\x01 \x01(\tR\n" +
"driverName\x12%\n" +
"\x0edriver_version\x18\x02 \x01(\tR\rdriverVersion\x12W\n" +
"\x15resource_capabilities\x18\x03 \x01(\v2\".openshell.v1.ResourceCapabilitiesR\x14resourceCapabilities\x12,\n" +
"\x12supports_ui_policy\x18\x04 \x01(\bR\x10supportsUiPolicy\"\xca\x01\n" +
"\x15resource_capabilities\x18\x03 \x01(\v2\".openshell.v1.ResourceCapabilitiesR\x14resourceCapabilities\"\xca\x01\n" +
"\x14ResourceCapabilities\x127\n" +
"\x03cpu\x18\x01 \x01(\v2%.openshell.v1.CpuResourceCapabilitiesR\x03cpu\x12@\n" +
"\x06memory\x18\x02 \x01(\v2(.openshell.v1.MemoryResourceCapabilitiesR\x06memory\x127\n" +
+4 -5
View File
@@ -288,11 +288,10 @@ ui:
```
Treat clipboard direction from the sandbox's perspective. Warn that UI is a
static sandbox-creation control. It is currently enforceable only by the MXC
driver's OpenShell `process_container` backend, which emits MXC's
`processcontainer` containment value and advertises complete support. MXC
`isolation_session` and non-Windows drivers advertise no support, so the gateway
rejects any explicit UI section, including `{}`, before provisioning. Omit the
static sandbox-creation control. It requires the selected compute driver to
advertise `openshell.policy.ui.v1` through existing extension metadata. A driver
without that capability rejects any explicit UI section, including `{}`, before
provisioning. Omit the
section rather than emitting deny-only UI for those drivers; omission preserves
their existing behavior.
+7 -15
View File
@@ -480,11 +480,6 @@ openshell logs my-sandbox --since 5m
openshell sandbox delete my-sandbox
openshell sandbox delete sandbox-1 sandbox-2 sandbox-3 # Multiple at once
openshell sandbox delete --all
# Fail closed if the observed sandbox was replaced or changed
openshell sandbox delete my-sandbox \
--expected-id <sandbox-id> \
--expected-resource-version <resource-version>
```
`deletion accepted` means cleanup is still pending. Inspect the sandbox until
@@ -492,10 +487,6 @@ it disappears before assuming completion. An already-absent sandbox succeeds;
missing workspaces and authorization failures remain errors. Do not blindly
retry by name if another process might have recreated that name.
Identity preconditions are valid only for one named sandbox, and a resource
version requires the immutable ID. A mismatch returns `ABORTED` before
OpenShell mutates gateway state or calls the compute driver.
### Stop and start sandboxes
Use stop to halt compute while retaining the sandbox and its persistent
@@ -529,6 +520,12 @@ This is the most important multi-step workflow. It enables a tight feedback cycl
**Key concept**: Policies have static fields (immutable after activation: `filesystem_policy`, `landlock`, `process`, `ui`) and two dynamic fields: `network_policies` and `network_middlewares`. Both dynamic fields can be updated without recreating the sandbox when the selected compute driver supports live policy updates. Drivers without the standard supervisor fetch revisions through the sandbox configuration API and report whether they loaded them.
Explicit `ui` requires the selected driver to advertise
`openshell.policy.ui.v1` in its existing supported extension capabilities,
visible through `openshell gateway info`. Unsupported drivers reject even
`ui: {}` before provisioning. Omit UI unless requested. UI changes require
sandbox recreation.
If startup reports `ConfigurationInvalid`, inspect `openshell sandbox get` and
repair the complete policy or provider set through the gateway. The workload
has not started on its first activation, so static fields can also be replaced
@@ -543,11 +540,6 @@ wait timeout is separate from this gateway deadline. Follow the
published [policy repair guidance](https://docs.nvidia.com/openshell/latest/how-it-works/policies/overview)
and confirm current replacement/detach syntax with installed CLI help.
UI capabilities require a driver/backend advertising complete UI-policy support.
MXC's `process_container` backend enforces them at provisioning; changing UI
capabilities after activation requires recreating the sandbox. Backends without
UI-policy support reject an explicit UI section before provisioning.
An endpoint with omitted `protocol` retains explicit-proxy behavior. Explicit
`protocol: tcp` requests policy DNS and transparent TCP and currently requires
the Docker or Podman runtime; unsupported runtimes reject the policy before starting the
@@ -619,7 +611,7 @@ Edit `current-policy.yaml` to allow the blocked actions. **For policy content au
- Binary matching patterns
- Ordered `network_middlewares`, host selection, HTTP request/response and WebSocket bindings, and `fail_open` or `fail_closed` behavior
`network_policies` and `network_middlewares` can be modified at runtime when the selected compute driver supports live policy updates. Use `--wait` to verify that the active runtime loaded the revision; do not infer enforcement from the gateway accepting the update. MXC rejects live policy replacement and merge updates; delete and recreate an MXC sandbox instead. If `filesystem_policy`, `landlock`, `process`, or `ui` need changes, the sandbox must be recreated. Built-in middleware such as `openshell/regex` needs no gateway registration. An operator-run middleware must already be registered under `[[openshell.supervisor.middleware]]`; changing that static registration requires a gateway restart.
`network_policies` and `network_middlewares` can be modified at runtime when the selected compute driver supports live policy updates. Use `--wait` to verify that the active runtime loaded the revision; do not infer enforcement from the gateway accepting the update. MXC rejects live policy replacement and merge updates; delete and recreate an MXC sandbox instead. If `filesystem_policy`, `landlock`, or `process` need changes, the sandbox must be recreated. Built-in middleware such as `openshell/regex` needs no gateway registration. An operator-run middleware must already be registered under `[[openshell.supervisor.middleware]]`; changing that static registration requires a gateway restart.
Middleware can inspect HTTP requests, HTTP responses, or client WebSocket text
messages when the implementation advertises the matching binding. The built-in
+79
View File
@@ -0,0 +1,79 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Pure helper regressions. This does not replace real remote E2E coverage.
$ErrorActionPreference = 'Stop'
$count = 0
foreach ($name in @('test-mxc-remote.ps1', 'run-mxc-remote.ps1')) {
$tokens = $null
$errors = $null
$ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $PSScriptRoot $name), [ref]$tokens, [ref]$errors)
if ($errors.Count) { throw ($errors | Out-String) }
# Load pure functions only; do not start builds or SSH sessions.
foreach ($function in $ast.FindAll({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] }, $true)) {
if ($function.Name -in @('Quote-PS', 'Find-Z3Runtime', 'Read-CacheHashes', 'Get-PeMachine')) { Invoke-Expression $function.Extent.Text }
}
$count++
}
if ((Quote-PS "C:\Users\O'Brien\file") -ne "'C:\Users\O''Brien\file'") { throw 'PowerShell literal quoting regression.' }
$count++
$hashA = 'a' * 64
$hashB = 'b' * 64
$hashes = @(Read-CacheHashes ('["' + $hashA + '","' + $hashB + '"]'))
if ($hashes.Count -ne 2 -or $hashes -notcontains $hashA -or $hashes -notcontains $hashB) { throw 'JSON cache array enumeration regression.' }
$count++
if (@(Read-CacheHashes '[]').Count -ne 0) { throw 'Empty cache response regression.' }
$count++
if (@(Read-CacheHashes ('["' + $hashA + '"]')).Count -ne 1) { throw 'Single cache hash regression.' }
$count++
function Pe-Bytes([int] $Machine) {
$bytes = New-Object byte[] 128
$bytes[0] = 0x4d; $bytes[1] = 0x5a; $bytes[0x3c] = 0x40
$bytes[0x40] = 0x50; $bytes[0x41] = 0x45
$bytes[0x44] = $Machine -band 255; $bytes[0x45] = ($Machine -shr 8) -band 255
return ,$bytes
}
$fixture = [IO.Path]::GetTempFileName()
try {
if ((Find-Z3Runtime '' '' $fixture) -ne $fixture) { throw 'Explicit Z3 DLL path regression.' }
$count++
$rejected = $false
try { Find-Z3Runtime ([IO.Path]::GetTempPath()) ('missing-' + [guid]::NewGuid().ToString('N')) '' | Out-Null } catch { $rejected = $true }
if (-not $rejected) { throw 'Missing Z3 DLL accepted.' }
$count++
foreach ($machine in @(0xaa64, 0x8664)) {
[IO.File]::WriteAllBytes($fixture, (Pe-Bytes $machine))
if ((Get-PeMachine $fixture) -ne $machine) { throw 'PE architecture regression.' }
$count++
}
[IO.File]::WriteAllBytes($fixture, [byte[]]@(1, 2, 3, 4))
$rejected = $false
try { Get-PeMachine $fixture | Out-Null } catch { $rejected = $true }
if (-not $rejected) { throw 'Invalid executable accepted.' }
$count++
# Reader cleanup must permit rewriting/deleting even after rejection.
} finally { Remove-Item -LiteralPath $fixture }
$cacheFixture = Join-Path ([IO.Path]::GetTempPath()) ('openshell-z3-test-' + [guid]::NewGuid().ToString('N'))
$target = 'aarch64-pc-windows-msvc'
$first = Join-Path $cacheFixture "$target/release/build/z3-sys-first/out/z3-5.1.0/bin/libz3.dll"
$second = Join-Path $cacheFixture "$target/release/build/z3-sys-second/out/z3-5.1.0/bin/libz3.dll"
try {
New-Item -ItemType Directory -Path (Split-Path $first), (Split-Path $second) -Force | Out-Null
[IO.File]::WriteAllBytes($first, (Pe-Bytes 0xaa64))
if ((Find-Z3Runtime $cacheFixture $target '') -ne $first) { throw 'Target-specific Z3 discovery regression.' }
$count++
Copy-Item -LiteralPath $first -Destination $second
if (-not (Find-Z3Runtime $cacheFixture $target '')) { throw 'Identical cached Z3 runtimes rejected.' }
$count++
[IO.File]::WriteAllBytes($second, (Pe-Bytes 0x8664))
$rejected = $false
try { Find-Z3Runtime $cacheFixture $target '' | Out-Null } catch { $rejected = $true }
if (-not $rejected) { throw 'Ambiguous Z3 runtimes accepted.' }
$count++
} finally {
# Only tear down the explicitly created fixture paths, not a recursive root.
foreach ($path in @($first, $second)) { if (Test-Path $path) { Remove-Item -LiteralPath $path } }
Get-ChildItem -LiteralPath $cacheFixture -Directory -Recurse | Sort-Object { $_.FullName.Length } -Descending | ForEach-Object { Remove-Item -LiteralPath $_.FullName }
Remove-Item -LiteralPath $cacheFixture
}
Write-Host "$count remote-script regression checks passed. No runtime qualification claimed."
+3 -3
View File
@@ -50,7 +50,7 @@ if (-not [int]::TryParse($BuildJobsValue, [ref] $WindowsBuildJobs) -or $WindowsB
}
$WindowsCargoMutex = [System.Threading.Mutex]::new($false, "Local\OpenShellWindowsMsvcCargo")
$UnsupportedDriverPackageExcludes = "--exclude openshell-driver-docker --exclude openshell-driver-kubernetes --exclude openshell-driver-kubernetes-secrets --exclude openshell-driver-podman --exclude openshell-driver-vault --exclude openshell-driver-vm --exclude openshell-vfio"
$UnsupportedDriverPackageExcludes = "--exclude openshell-driver-docker --exclude openshell-driver-kubernetes --exclude openshell-driver-kubernetes-secrets --exclude openshell-driver-podman --exclude openshell-driver-vault --exclude openshell-driver-vm --exclude openshell-sandbox --exclude openshell-vfio"
$WindowsClippyPackageExcludes = $UnsupportedDriverPackageExcludes
$WindowsClippyLintArgs = "-D warnings -A dead-code -A unused-imports -A clippy::unused-async"
$PrebuiltZ3WorkspaceFeatures = "--features openshell-prover/prebuilt-z3"
@@ -528,7 +528,7 @@ function Stage-Z3Runtime([string] $RustTarget) {
$candidates = @(Get-ChildItem -Path (Join-Path $release "build/z3-sys-*/out/z3-$PrebuiltZ3Version/bin/libz3.dll") -File -ErrorAction SilentlyContinue)
}
if ($candidates.Count -eq 0) { throw "Matching libz3.dll not found for $RustTarget." }
$hashes = @($candidates | ForEach-Object { (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash } | Select-Object -Unique)
$hashes = @($candidates | ForEach-Object { Get-Sha256 $_.FullName } | Select-Object -Unique)
if ($hashes.Count -ne 1) { throw "Ambiguous Z3 runtimes for $RustTarget; use a clean target cache or Z3_LIBRARY_PATH_OVERRIDE." }
$source = $candidates[0].FullName
$reader = [IO.BinaryReader]::new([IO.File]::OpenRead($source))
@@ -546,7 +546,7 @@ function Stage-Z3Runtime([string] $RustTarget) {
if ([IO.Path]::GetFullPath($source) -ne [IO.Path]::GetFullPath($destination)) {
Copy-Item -LiteralPath $source -Destination $destination -Force
}
if ((Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash -ne $hashes[0]) { throw 'Staged Z3 DLL hash mismatch.' }
if ((Get-Sha256 $destination) -ne $hashes[0]) { throw 'Staged Z3 DLL hash mismatch.' }
Write-Host "==> Staged $destination (SHA256=$($hashes[0]))"
}