Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/openshell-driver-mxc/examples/mxc-gateway.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
34 changes: 34 additions & 0 deletions crates/openshell-driver-mxc/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Vec<_>>()
.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::<Vec<_>>()
Expand Down Expand Up @@ -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() {
Expand Down
21 changes: 16 additions & 5 deletions crates/openshell-driver-mxc/tests/wxc_exec_real.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![
Expand Down Expand Up @@ -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!(
Expand Down
1 change: 1 addition & 0 deletions crates/openshell-supervisor-network/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result<HostProxyHandle
config.activity_tx,
ready_rx,
&upstream_proxy_args,
None,
)
.await?;

Expand Down
7 changes: 5 additions & 2 deletions crates/openshell-supervisor-network/src/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6544,6 +6544,10 @@ network_policies:
.expect("bind MCP upstream listener");
let upstream_port = upstream_listener.local_addr().unwrap().port();
let executable = std::env::current_exe().expect("current executable");
// JSON strings are valid YAML scalars and correctly escape Windows
// path separators such as `C:\\...`.
let executable_yaml =
serde_json::to_string(&executable.to_string_lossy()).expect("encode executable");
let data = format!(
r#"version: 1
network_policies:
Expand All @@ -6559,9 +6563,8 @@ network_policies:
method: tools/call
tool: echo
binaries:
- {{ path: "{executable}" }}
- {{ path: {executable_yaml} }}
"#,
executable = executable.display(),
);
let mut policy = openshell_policy::parse_sandbox_policy(&data).expect("parse MCP policy");
let endpoint = &mut policy
Expand Down
24 changes: 18 additions & 6 deletions crates/openshell-supervisor-relay/src/imp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
//! Usage: `openshell-supervisor-relay.exe <target-port>` -- `<target-port>`
//! 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
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
27 changes: 25 additions & 2 deletions tasks/scripts/windows-msvc.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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"
}
}
Expand Down Expand Up @@ -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
Expand Down
Loading