diff --git a/crates/openshell-driver-mxc/WINDOWS-INTEGRATION.md b/crates/openshell-driver-mxc/WINDOWS-INTEGRATION.md index d8dd9de68..4c1eca114 100644 --- a/crates/openshell-driver-mxc/WINDOWS-INTEGRATION.md +++ b/crates/openshell-driver-mxc/WINDOWS-INTEGRATION.md @@ -13,6 +13,9 @@ This is a selective port, not a branch merge or a claim of complete demo parity. registration in the E2E and OCSF runners. - Restore the WebSocket and OpenClaw policy fixtures with their original grants, updating comments to the host-supervisor/Windows-boundary architecture. +- Adaptation found by real E2E: store injected proxy CA files under the already + protected/granted boundary runtime directory. Ambient host temporary paths + become inaccessible when CLI APPDATA/LOCALAPPDATA is isolated. Keep the always-present `openshell-windows-sandbox`, authenticated Sandbox Protocol, and host isolation-backend supervisor. Do not restore the old relay diff --git a/crates/openshell-mxc-boundary/README.md b/crates/openshell-mxc-boundary/README.md index 58835da0e..07a4c34b8 100644 --- a/crates/openshell-mxc-boundary/README.md +++ b/crates/openshell-mxc-boundary/README.md @@ -75,3 +75,8 @@ therefore remains fail-closed, including on an otherwise qualified PSEC host. Unit tests exercise real Windows process launch, output retention, and a real ephemeral TCP forwarding target. These tests do not qualify MXC enforcement; that requires the real-MXC integration and E2E lanes on a supported host. + +The bootstrap path must be absolute. Injected proxy CA files are stored beside +the consumed bootstrap, under the generation's protected boundary-state +directory already granted by the driver. They do not use ambient host +APPDATA/TEMP paths, which may be isolated or inaccessible inside MXC. diff --git a/crates/openshell-mxc-boundary/src/runtime.rs b/crates/openshell-mxc-boundary/src/runtime.rs index 19f781e7b..cd9fc5cd7 100644 --- a/crates/openshell-mxc-boundary/src/runtime.rs +++ b/crates/openshell-mxc-boundary/src/runtime.rs @@ -54,6 +54,11 @@ pub fn run_boundary(config_path: &Path) -> Result<(), String> { let config: BoundaryConfig = serde_json::from_slice(&bytes) .map_err(|error| format!("decode boundary config {}: {error}", config_path.display()))?; validate_config(&config)?; + let runtime_directory = config_path + .parent() + .filter(|path| path.is_absolute()) + .ok_or_else(|| "MXC bootstrap requires an absolute runtime directory".to_string())? + .to_path_buf(); std::fs::remove_file(config_path) .map_err(|error| format!("consume boundary config {}: {error}", config_path.display()))?; @@ -69,11 +74,8 @@ pub fn run_boundary(config_path: &Path) -> Result<(), String> { } }; let tls = Arc::new(load_tls_server_config(&tls)?); - let listener = tokio::net::TcpListener::bind(address) - .await - .map_err(|error| format!("bind MXC boundary listener at {address}: {error}"))?; - let boundary = Arc::new(BoundaryRuntime::new(config)?); - tracing::info!(%address, "MXC Sandbox Protocol listener ready"); + let boundary = Arc::new(BoundaryRuntime::new(config, runtime_directory)?); + tracing::info!(%address, "MXC Sandbox Protocol reverse control ready"); loop { let (stream, _) = listener .accept() @@ -445,6 +447,8 @@ enum Lifecycle { struct BoundaryRuntime { config: BoundaryConfig, + runtime_directory: PathBuf, + proxy_url: String, authenticator: SandboxProtocolAuthenticator, connections: SandboxConnectionRegistry, connection_shutdowns: Mutex>>, @@ -462,7 +466,11 @@ struct BoundaryRuntime { } impl BoundaryRuntime { - fn new(config: BoundaryConfig) -> Result { + fn new( + launch: crate::launch::MxcBoundaryConfig, + runtime_directory: PathBuf, + ) -> Result { + let config = launch.runtime; let sandbox_id = SandboxId::parse(config.boundary_id.clone()) .map_err(|error| format!("validate MXC sandbox ID: {error}"))?; let generation = openshell_core::sandbox_generation::SandboxGenerationId::parse( @@ -491,6 +499,8 @@ impl BoundaryRuntime { ), connections: SandboxConnectionRegistry::new(config.session_id, config.session_rotation), config, + runtime_directory, + proxy_url: launch.proxy_url, connection_shutdowns: Mutex::new(HashMap::new()), active_connection: Mutex::new(None), lifecycle: Mutex::new(Lifecycle::AwaitingAttach), @@ -830,6 +840,7 @@ impl BoundaryRuntime { } } let ca_paths = match install_ca_material( + &self.runtime_directory, &self.config.generation, ca_cert.as_deref().map(str::as_bytes), ca_bundle.as_deref().map(str::as_bytes), @@ -1216,6 +1227,7 @@ fn workload_environment( } fn install_ca_material( + runtime_directory: &Path, generation: &str, certificate: Option<&[u8]>, bundle: Option<&[u8]>, @@ -1223,7 +1235,10 @@ fn install_ca_material( let (Some(certificate), Some(bundle)) = (certificate, bundle) else { return Ok(None); }; - let directory = std::env::temp_dir().join(format!("openshell-ca-{generation}")); + // The driver already grants and protects the generation's boundary state. + // Host APPDATA/TEMP may be isolated or inaccessible inside ProcessContainer; + // never rely on those ambient paths or broaden workload filesystem grants. + let directory = runtime_directory.join(format!("openshell-ca-{generation}")); std::fs::create_dir_all(&directory) .map_err(|error| format!("create MXC CA directory: {error}"))?; let certificate_path = directory.join("openshell-ca.pem"); @@ -1743,6 +1758,41 @@ mod tests { .expect("shutdown notification"); } + #[test] + fn ca_material_stays_under_the_explicit_boundary_runtime_directory() { + let root = std::env::temp_dir().join(format!( + "openshell-ca-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + let paths = install_ca_material(&root, "test-generation", Some(b"cert"), Some(b"bundle")) + .unwrap() + .unwrap(); + assert_eq!( + paths.0, + root.join("openshell-ca-test-generation/openshell-ca.pem") + ); + assert_eq!( + paths.1, + root.join("openshell-ca-test-generation/ca-bundle.pem") + ); + assert_eq!(std::fs::read(&paths.0).unwrap(), b"cert"); + assert_eq!(std::fs::read(&paths.1).unwrap(), b"bundle"); + assert!( + install_ca_material(&root, "unused", None, Some(b"bundle")) + .unwrap() + .is_none() + ); + assert!(!root.join("openshell-ca-unused").exists()); + std::fs::remove_file(paths.0).unwrap(); + std::fs::remove_file(paths.1).unwrap(); + std::fs::remove_dir(root.join("openshell-ca-test-generation")).unwrap(); + std::fs::remove_dir(root).unwrap(); + } + #[test] fn installed_ca_replaces_case_insensitive_trust_overrides() { let certificate = Path::new(r"C:\trusted\ca.pem");