diff --git a/crates/openshell-driver-mxc/examples/mxc-gateway.toml b/crates/openshell-driver-mxc/examples/mxc-gateway.toml index 4fbd2c41f5..601b66bec6 100644 --- a/crates/openshell-driver-mxc/examples/mxc-gateway.toml +++ b/crates/openshell-driver-mxc/examples/mxc-gateway.toml @@ -7,6 +7,9 @@ # Supply command and cwd through --driver-config-json when creating a sandbox, # and environment variables through the standard sandbox --env option. +[openshell] +version = 2 + [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). diff --git a/crates/openshell-driver-mxc/src/driver.rs b/crates/openshell-driver-mxc/src/driver.rs index ce728cbeec..072b1debf1 100644 --- a/crates/openshell-driver-mxc/src/driver.rs +++ b/crates/openshell-driver-mxc/src/driver.rs @@ -660,6 +660,31 @@ fn append_tls_readonly_grant( } } fn encode_windows_command_line(args: &[String]) -> String { + if args + .first() + .and_then(|executable| executable.rsplit(['\\', '/']).next()) + .is_some_and(|executable| { + executable.eq_ignore_ascii_case("cmd") || executable.eq_ignore_ascii_case("cmd.exe") + }) + && let Some(command_index) = args + .iter() + .position(|arg| arg.eq_ignore_ascii_case("/c") || arg.eq_ignore_ascii_case("/k")) + { + let mut encoded = args[..=command_index] + .iter() + .map(|arg| quote_windows_argument(arg)) + .collect::>() + .join(" "); + if command_index + 1 < args.len() { + encoded.push(' '); + // cmd.exe parses the command tail with its own grammar. Escaping + // embedded quotes as C argv would leave literal backslashes in + // paths and redirections (for example `\"C:\\work file\"`). + encoded.push_str(&args[command_index + 1..].join(" ")); + } + return encoded; + } + args.iter() .map(|arg| quote_windows_argument(arg)) .collect::>() @@ -2919,6 +2944,15 @@ mod lifecycle_tests { quote_windows_argument("trailing slash\\ "), r#""trailing slash\ ""# ); + assert_eq!( + encode_windows_command_line(&[ + r"C:\Windows\System32\CMD.EXE".into(), + "/d".into(), + "/c".into(), + r#"echo hello > "C:\work dir\output.txt""#.into(), + ]), + r#"C:\Windows\System32\CMD.EXE /d /c echo hello > "C:\work dir\output.txt""# + ); } #[tokio::test] async fn positive_in_policy_write_reaches_ready_and_materializes_file() { diff --git a/crates/openshell-driver-mxc/tests/wxc_exec_real.rs b/crates/openshell-driver-mxc/tests/wxc_exec_real.rs index c7bb11d86b..36b6edb804 100644 --- a/crates/openshell-driver-mxc/tests/wxc_exec_real.rs +++ b/crates/openshell-driver-mxc/tests/wxc_exec_real.rs @@ -832,15 +832,24 @@ 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 diagnostic_path = output_dir.path().join("https-diagnostic.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 diagnostic_path_string = diagnostic_path.to_string_lossy().into_owned(); let cmd_string = cmd.to_string_lossy().into_owned(); + // Schannel's revocation lookup targets are intentionally outside this + // test's example.com-only policy. Disable that network lookup while still + // requiring curl to validate the proxy-issued certificate against the + // injected CA bundle. let script = format!( - "type \"%CURL_CA_BUNDLE%\" 1>NUL && \ - \"{}\" --fail --silent --show-error --cacert \"%CURL_CA_BUNDLE%\" \ + "echo CURL_CA_BUNDLE=%CURL_CA_BUNDLE% 1>\"{diagnostic_path_string}\" && \ + type \"%CURL_CA_BUNDLE%\" 1>NUL 2>>\"{diagnostic_path_string}\" && \ + \"{}\" --fail --silent --show-error --ssl-no-revoke \ + --cacert \"%CURL_CA_BUNDLE%\" \ https://example.com/ --output \"{output_path_string}\" \ - --write-out \"%{{certs}}\" 1>\"{certificate_path_string}\"", + --write-out \"%{{certs}}\" 1>\"{certificate_path_string}\" \ + 2>>\"{diagnostic_path_string}\"", curl.display() ); let command = vec![ @@ -932,10 +941,12 @@ async fn pc_https_egress_reads_injected_ca_bundle() { } let condition = terminal_condition.expect("HTTPS sandbox should reach a terminal condition"); + let diagnostic = std::fs::read_to_string(diagnostic_path) + .unwrap_or_else(|error| format!("failed to read HTTPS diagnostic: {error}")); assert_eq!( condition.reason, "AgentCompleted", - "HTTPS workload failed: {}", - condition.message + "HTTPS workload failed: {}; diagnostic: {diagnostic}", + condition.message, ); assert!(output_path.exists(), "curl should write the HTTPS response"); assert!( diff --git a/crates/openshell-supervisor-network/src/host.rs b/crates/openshell-supervisor-network/src/host.rs index 9d6f5cfffa..eb09634772 100644 --- a/crates/openshell-supervisor-network/src/host.rs +++ b/crates/openshell-supervisor-network/src/host.rs @@ -231,6 +231,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result` -- `` //! is the TCP port the launched command is expected to bind (an early //! liveness check, `wait_for_port_ready`: if the target never binds it -//! within 60s, this process exits with an error instead of sitting around -//! with a target that will never work). This binary uses no `share_dir` files +//! within the bounded five-minute cold-start budget, this process exits with +//! an error instead of sitting around with a target that will never work). +//! This binary uses no `share_dir` files //! at all -- command/env and shutdown both travel over the control channel. //! //! Shutdown: driver sends a `"shutdown"` request over the control channel @@ -164,7 +165,7 @@ pub async fn run() -> anyhow::Result<()> { // budget after a caller was told shutdown succeeded. let mut shutdown_rx = shutdown_rx; tokio::select! { - result = wait_for_port_ready(&mut child, port, Duration::from_mins(1)) => { + result = wait_for_port_ready(&mut child, port, PORT_READY_PER_TRY_TIMEOUT) => { result?; } _ = &mut shutdown_rx => { @@ -715,8 +716,11 @@ where /// Number of full-budget tries `wait_for_port_ready` makes -- each try gets /// its own complete `per_try_timeout` window, not a slice of it. Worst case -/// total wait is `max_tries * per_try_timeout` (3 * 60s = 180s today). -const PORT_READY_MAX_TRIES: u32 = 3; +/// total wait is `max_tries * per_try_timeout` (5 * 60s = 300s today). Keep +/// this aligned with the MXC driver's 310-second target-ready timeout so a +/// cold process remains actively probed for the full advertised budget. +const PORT_READY_MAX_TRIES: u32 = 5; +const PORT_READY_PER_TRY_TIMEOUT: Duration = Duration::from_mins(1); /// Poll for the target port accepting TCP connections, bailing out early /// (rather than waiting out all tries) if the child process exits first — a @@ -1013,12 +1017,20 @@ async fn run_lifecycle(mut child: tokio::process::Child, shutdown_rx: oneshot::R #[cfg(test)] mod forward_connect_tests { - use super::connect_forward_target; + use super::{PORT_READY_MAX_TRIES, PORT_READY_PER_TRY_TIMEOUT, connect_forward_target}; use std::io::{Error, ErrorKind}; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; + #[test] + fn port_ready_budget_covers_five_minute_cold_start_contract() { + assert_eq!( + PORT_READY_PER_TRY_TIMEOUT * PORT_READY_MAX_TRIES, + Duration::from_mins(5), + ); + } + #[tokio::test(start_paused = true)] async fn forward_connect_returns_success_without_retry_delay() { let start = tokio::time::Instant::now(); diff --git a/tasks/scripts/windows-msvc.ps1 b/tasks/scripts/windows-msvc.ps1 index e76507032d..92569c57fd 100644 --- a/tasks/scripts/windows-msvc.ps1 +++ b/tasks/scripts/windows-msvc.ps1 @@ -518,6 +518,29 @@ function Invoke-Build([string] $RustTarget) { -RustTarget $RustTarget ` -CargoArgs "cargo build --release --target $RustTarget --bin openshell-gateway --bin openshell --bin openshell-supervisor-relay $Z3GatewayFeatures" ` -LogName "build-$RustTarget-release.log" + + $z3Runtime = if ([string]::IsNullOrWhiteSpace($env:Z3_LIBRARY_PATH_OVERRIDE)) { + $buildRoot = Join-Path $TargetDir "$RustTarget\release\build" + $candidates = @( + Get-ChildItem -Path $buildRoot -Filter "libz3.dll" -File -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*\z3-$PrebuiltZ3Version\bin\libz3.dll" } | + Sort-Object LastWriteTimeUtc -Descending + ) + if ($candidates.Count -eq 0) { + throw "The prebuilt Z3 runtime was not found under: $buildRoot" + } + $candidates[0].FullName + } else { + $path = Join-Path $env:Z3_LIBRARY_PATH_OVERRIDE "libz3.dll" + if (-not (Test-Path $path -PathType Leaf)) { + throw "Z3_LIBRARY_PATH_OVERRIDE is set but libz3.dll was not found at: $path" + } + $path + } + + $z3RuntimeDestination = Join-Path $TargetDir "$RustTarget\release\libz3.dll" + Copy-Item -LiteralPath $z3Runtime -Destination $z3RuntimeDestination -Force + Write-Host "==> Staged Z3 runtime: $z3RuntimeDestination" } function Invoke-Test([string] $RustTarget) { @@ -555,7 +578,7 @@ function Invoke-UnsupportedContractTests([string] $RustTarget) { $variant = if ($features) { $features.Replace(",", "-") } else { "protocol-only" } Invoke-VsCargo ` -RustTarget $RustTarget ` - -CargoArgs "cargo test -p openshell-gateway --lib --target $RustTarget --no-default-features $featureArgs $Z3ServerFeatures" ` + -CargoArgs "cargo test -p openshell-gateway --lib --target $RustTarget --no-default-features $featureArgs $Z3GatewayFeatures" ` -LogName "test-$RustTarget-selective-$variant.log" } } @@ -585,7 +608,7 @@ function Get-Sha256([string] $Path) { function Show-Artifacts([string[]] $RustTargets) { $rows = @() foreach ($rustTarget in $RustTargets) { - foreach ($binary in @("openshell-gateway.exe", "openshell.exe", "openshell-supervisor-relay.exe")) { + foreach ($binary in @("openshell-gateway.exe", "openshell.exe", "openshell-supervisor-relay.exe", "libz3.dll")) { $path = Join-Path $TargetDir "$rustTarget\release\$binary" if (-not (Test-Path $path)) { continue