diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e89497c3c5..b5fc6347b4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4972,6 +4972,14 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "codex-windows-sandbox-service" +version = "0.0.0" +dependencies = [ + "anyhow", + "windows-sys 0.52.0", +] + [[package]] name = "codex-workload-identity" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index d257adee34..3811c219bd 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -103,6 +103,7 @@ members = [ "tools", "v8-poc", "websocket-client", + "windows-sandbox-service", "worktree", "workload-identity", "utils/absolute-path", diff --git a/codex-rs/windows-sandbox-service/BUILD.bazel b/codex-rs/windows-sandbox-service/BUILD.bazel new file mode 100644 index 0000000000..a4479363fb --- /dev/null +++ b/codex-rs/windows-sandbox-service/BUILD.bazel @@ -0,0 +1,7 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "windows-sandbox-service", + binary_test_target_compatible_with = ["@platforms//os:windows"], + crate_name = "codex_windows_sandbox_service", +) diff --git a/codex-rs/windows-sandbox-service/Cargo.toml b/codex-rs/windows-sandbox-service/Cargo.toml new file mode 100644 index 0000000000..080378a67f --- /dev/null +++ b/codex-rs/windows-sandbox-service/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "codex-windows-sandbox-service" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +name = "codex_windows_sandbox_service" +path = "src/lib.rs" +doctest = false + +[[bin]] +name = "codex-windows-sandbox-service" +path = "src/main.rs" + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } + +[target.'cfg(windows)'.dependencies.windows-sys] +version = "0.52" +features = [ + "Win32_Foundation", + "Win32_NetworkManagement_NetManagement", + "Win32_Security", + "Win32_Security_Authorization", + "Win32_Storage_FileSystem", + "Win32_Storage_Packaging_Appx", + "Win32_System_Com", + "Win32_System_Environment", + "Win32_System_EventLog", + "Win32_System_IO", + "Win32_System_Memory", + "Win32_System_Pipes", + "Win32_System_Services", + "Win32_System_SystemServices", + "Win32_System_Threading", + "Win32_UI_Shell", +] diff --git a/codex-rs/windows-sandbox-service/src/ipc.rs b/codex-rs/windows-sandbox-service/src/ipc.rs new file mode 100644 index 0000000000..f3bc65363a --- /dev/null +++ b/codex-rs/windows-sandbox-service/src/ipc.rs @@ -0,0 +1,16 @@ +//! Disabled provisioning IPC until authenticated request handling is installed. + +use anyhow::Result; +use std::sync::Arc; +use std::sync::atomic::AtomicBool; + +pub(crate) const PIPE_NAME: &str = r"\\.\pipe\OpenAI.CodexSandbox"; + +pub(crate) fn run( + _shutdown: Arc, + _on_ready: impl FnOnce() -> Result<()>, +) -> Result<()> { + anyhow::bail!("sandbox provisioning IPC is disabled until authenticated handling is installed") +} + +pub(crate) fn wake() {} diff --git a/codex-rs/windows-sandbox-service/src/lib.rs b/codex-rs/windows-sandbox-service/src/lib.rs new file mode 100644 index 0000000000..56dc54d141 --- /dev/null +++ b/codex-rs/windows-sandbox-service/src/lib.rs @@ -0,0 +1,30 @@ +use anyhow::Result; + +#[cfg(windows)] +mod ipc; +#[cfg(windows)] +mod service; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RunMode { + Service, + #[cfg(debug_assertions)] + Foreground, +} + +pub fn run(mode: RunMode) -> Result<()> { + #[cfg(windows)] + { + match mode { + RunMode::Service => service::run(), + #[cfg(debug_assertions)] + RunMode::Foreground => service::run_foreground(), + } + } + + #[cfg(not(windows))] + { + let _ = mode; + anyhow::bail!("the Codex sandbox service is only available on Windows") + } +} diff --git a/codex-rs/windows-sandbox-service/src/main.rs b/codex-rs/windows-sandbox-service/src/main.rs new file mode 100644 index 0000000000..12f5710fd0 --- /dev/null +++ b/codex-rs/windows-sandbox-service/src/main.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use codex_windows_sandbox_service::RunMode; + +fn main() -> Result<()> { + codex_windows_sandbox_service::run(parse_mode(std::env::args().skip(1))?) +} + +fn parse_mode(arguments: impl Iterator) -> Result { + let arguments = arguments.collect::>(); + match arguments.as_slice() { + [] => Ok(RunMode::Service), + [argument] if argument == "--service" => Ok(RunMode::Service), + #[cfg(debug_assertions)] + [argument] if argument == "--foreground" => Ok(RunMode::Foreground), + [argument] => anyhow::bail!("unrecognized service argument {argument:?}"), + _ => anyhow::bail!("expected at most one service argument"), + } +} + +#[cfg(test)] +#[path = "main_tests.rs"] +mod tests; diff --git a/codex-rs/windows-sandbox-service/src/main_tests.rs b/codex-rs/windows-sandbox-service/src/main_tests.rs new file mode 100644 index 0000000000..d3a1eac3ef --- /dev/null +++ b/codex-rs/windows-sandbox-service/src/main_tests.rs @@ -0,0 +1,44 @@ +use anyhow::Result; + +use super::RunMode; +use super::parse_mode; + +#[test] +fn defaults_to_service_mode() -> Result<()> { + assert_eq!(parse_mode(std::iter::empty())?, RunMode::Service); + Ok(()) +} + +#[test] +fn accepts_explicit_service_mode() -> Result<()> { + let arguments = [String::from("--service")].into_iter(); + assert_eq!(parse_mode(arguments)?, RunMode::Service); + Ok(()) +} + +#[test] +#[cfg(debug_assertions)] +fn accepts_foreground_mode() -> Result<()> { + let arguments = [String::from("--foreground")].into_iter(); + assert_eq!(parse_mode(arguments)?, RunMode::Foreground); + Ok(()) +} + +#[test] +#[cfg(not(debug_assertions))] +fn rejects_foreground_mode_in_release_builds() { + let arguments = [String::from("--foreground")].into_iter(); + assert!(parse_mode(arguments).is_err()); +} + +#[test] +fn rejects_unknown_arguments() { + let arguments = [String::from("--unknown")].into_iter(); + assert!(parse_mode(arguments).is_err()); +} + +#[test] +fn rejects_multiple_arguments() { + let arguments = [String::from("--service"), String::from("--foreground")].into_iter(); + assert!(parse_mode(arguments).is_err()); +} diff --git a/codex-rs/windows-sandbox-service/src/service.rs b/codex-rs/windows-sandbox-service/src/service.rs new file mode 100644 index 0000000000..993d7e1f86 --- /dev/null +++ b/codex-rs/windows-sandbox-service/src/service.rs @@ -0,0 +1,276 @@ +//! Windows service lifecycle and event-log integration for sandbox provisioning. + +use std::ffi::c_void; +use std::io; +use std::ptr; +use std::sync::Arc; +use std::sync::OnceLock; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::AtomicU32; +use std::sync::atomic::Ordering; + +use anyhow::Context; +use anyhow::Result; +use windows_sys::Win32::Foundation::ERROR_CALL_NOT_IMPLEMENTED; +use windows_sys::Win32::Foundation::ERROR_SERVICE_SPECIFIC_ERROR; +use windows_sys::Win32::Foundation::NO_ERROR; +use windows_sys::Win32::System::EventLog::DeregisterEventSource; +use windows_sys::Win32::System::EventLog::EVENTLOG_ERROR_TYPE; +use windows_sys::Win32::System::EventLog::EVENTLOG_INFORMATION_TYPE; +use windows_sys::Win32::System::EventLog::RegisterEventSourceW; +use windows_sys::Win32::System::EventLog::ReportEventW; +use windows_sys::Win32::System::Services::RegisterServiceCtrlHandlerExW; +use windows_sys::Win32::System::Services::SERVICE_ACCEPT_SHUTDOWN; +use windows_sys::Win32::System::Services::SERVICE_ACCEPT_STOP; +use windows_sys::Win32::System::Services::SERVICE_CONTROL_INTERROGATE; +use windows_sys::Win32::System::Services::SERVICE_CONTROL_SHUTDOWN; +use windows_sys::Win32::System::Services::SERVICE_CONTROL_STOP; +use windows_sys::Win32::System::Services::SERVICE_RUNNING; +use windows_sys::Win32::System::Services::SERVICE_START_PENDING; +use windows_sys::Win32::System::Services::SERVICE_STATUS; +use windows_sys::Win32::System::Services::SERVICE_STATUS_HANDLE; +use windows_sys::Win32::System::Services::SERVICE_STOP_PENDING; +use windows_sys::Win32::System::Services::SERVICE_STOPPED; +use windows_sys::Win32::System::Services::SERVICE_TABLE_ENTRYW; +use windows_sys::Win32::System::Services::SERVICE_WIN32_OWN_PROCESS; +use windows_sys::Win32::System::Services::SetServiceStatus; +use windows_sys::Win32::System::Services::StartServiceCtrlDispatcherW; + +pub(crate) const SERVICE_NAME: &str = "CodexSandboxService"; +const EVENT_SERVICE_STARTED: u32 = 1000; +const EVENT_SERVICE_STOP_REQUESTED: u32 = 1001; +const EVENT_SERVICE_STOPPED: u32 = 1002; +const EVENT_SERVICE_FAILED: u32 = 1003; +const MAX_EVENT_MESSAGE_UNITS: usize = 1024; + +static SERVICE_STATE: OnceLock = OnceLock::new(); + +struct ServiceState { + shutdown: Arc, + status_handle: OnceLock, + current_status: AtomicU32, +} + +pub(crate) fn run() -> Result<()> { + let state = ServiceState { + shutdown: Arc::new(AtomicBool::new(false)), + status_handle: OnceLock::new(), + current_status: AtomicU32::new(SERVICE_START_PENDING), + }; + SERVICE_STATE + .set(state) + .map_err(|_| anyhow::anyhow!("the service dispatcher was already initialized"))?; + + let mut service_name = SERVICE_NAME + .encode_utf16() + .chain(std::iter::once(0)) + .collect::>(); + let service_table = [ + SERVICE_TABLE_ENTRYW { + lpServiceName: service_name.as_mut_ptr(), + lpServiceProc: Some(service_main), + }, + SERVICE_TABLE_ENTRYW { + lpServiceName: ptr::null_mut(), + lpServiceProc: None, + }, + ]; + + // The table and its service-name buffer remain alive until the dispatcher returns. + let dispatched = unsafe { StartServiceCtrlDispatcherW(service_table.as_ptr()) }; + if dispatched == 0 { + return Err(io::Error::last_os_error()).context("start the Windows service dispatcher"); + } + + Ok(()) +} + +#[cfg(debug_assertions)] +pub(crate) fn run_foreground() -> Result<()> { + crate::ipc::run(Arc::new(AtomicBool::new(false)), || { + eprintln!("{SERVICE_NAME} listening on {}", crate::ipc::PIPE_NAME); + Ok(()) + }) +} + +unsafe extern "system" fn service_main(_argument_count: u32, _arguments: *mut *mut u16) { + let Some(state) = SERVICE_STATE.get() else { + return; + }; + + if let Err(error) = service_main_inner(state) { + log_error( + EVENT_SERVICE_FAILED, + &format!("The Codex sandbox service encountered a fatal error: {error:#}"), + ); + eprintln!("{SERVICE_NAME} failed: {error:#}"); + if state.status_handle.get().is_some() { + let _ = state.report_status(SERVICE_STOPPED, ERROR_SERVICE_SPECIFIC_ERROR); + } + } +} + +fn service_main_inner(state: &ServiceState) -> Result<()> { + let service_name = SERVICE_NAME + .encode_utf16() + .chain(std::iter::once(0)) + .collect::>(); + + // The service controller only reads the name during registration. + let status_handle = unsafe { + RegisterServiceCtrlHandlerExW( + service_name.as_ptr(), + Some(service_control_handler), + ptr::null(), + ) + }; + if status_handle == 0 { + return Err(io::Error::last_os_error()).context("register the service control handler"); + } + state + .status_handle + .set(status_handle) + .map_err(|_| anyhow::anyhow!("the service status handle was already registered"))?; + + state.report_status(SERVICE_START_PENDING, NO_ERROR)?; + crate::ipc::run(Arc::clone(&state.shutdown), || { + state.report_status(SERVICE_RUNNING, NO_ERROR)?; + log_information( + EVENT_SERVICE_STARTED, + "The Codex sandbox service is running.", + ); + Ok(()) + }) + .context("run the sandbox provisioning broker")?; + + log_information( + EVENT_SERVICE_STOPPED, + "The Codex sandbox service has stopped.", + ); + state.report_status(SERVICE_STOPPED, NO_ERROR) +} + +unsafe extern "system" fn service_control_handler( + control: u32, + _event_type: u32, + _event_data: *mut c_void, + _context: *mut c_void, +) -> u32 { + let Some(state) = SERVICE_STATE.get() else { + return ERROR_CALL_NOT_IMPLEMENTED; + }; + + match control { + SERVICE_CONTROL_STOP | SERVICE_CONTROL_SHUTDOWN => { + if !state.shutdown.swap(true, Ordering::AcqRel) { + if let Err(error) = state.report_status(SERVICE_STOP_PENDING, NO_ERROR) { + eprintln!("unable to report service shutdown: {error:#}"); + } + crate::ipc::wake(); + log_information( + EVENT_SERVICE_STOP_REQUESTED, + "The Codex sandbox service was asked to stop.", + ); + } + NO_ERROR + } + SERVICE_CONTROL_INTERROGATE => { + let current_status = state.current_status.load(Ordering::Acquire); + if let Err(error) = state.report_status(current_status, NO_ERROR) { + log_error( + EVENT_SERVICE_FAILED, + "The Codex sandbox service could not report its current status.", + ); + eprintln!("unable to report the current service status: {error:#}"); + } + NO_ERROR + } + _ => ERROR_CALL_NOT_IMPLEMENTED, + } +} + +pub(crate) fn log_information(event_id: u32, message: &str) { + log_event(EVENTLOG_INFORMATION_TYPE, event_id, message); +} + +pub(crate) fn log_error(event_id: u32, message: &str) { + log_event(EVENTLOG_ERROR_TYPE, event_id, message); +} + +fn log_event(event_type: u16, event_id: u32, message: &str) { + let source = SERVICE_NAME + .encode_utf16() + .chain(std::iter::once(0)) + .collect::>(); + let event_log = unsafe { RegisterEventSourceW(ptr::null(), source.as_ptr()) }; + if event_log == 0 { + eprintln!("unable to open the Windows event log: {message}"); + return; + } + + let mut message_units = Vec::with_capacity(MAX_EVENT_MESSAGE_UNITS + 1); + for character in message.chars() { + let character = if character.is_control() { + ' ' + } else { + character + }; + let mut buffer = [0_u16; 2]; + let encoded = character.encode_utf16(&mut buffer); + if message_units.len() + encoded.len() > MAX_EVENT_MESSAGE_UNITS { + break; + } + message_units.extend_from_slice(encoded); + } + message_units.push(0); + let strings = [message_units.as_ptr()]; + let reported = unsafe { + ReportEventW( + event_log, + event_type, + 0, + event_id, + ptr::null_mut(), + 1, + 0, + strings.as_ptr(), + ptr::null(), + ) + }; + if reported == 0 { + eprintln!("unable to write a Windows event-log entry (event {event_id})"); + } + unsafe { DeregisterEventSource(event_log) }; +} + +impl ServiceState { + fn report_status(&self, current_status: u32, win32_exit_code: u32) -> Result<()> { + let status_handle = *self + .status_handle + .get() + .context("the service status handle was not registered")?; + let is_pending = matches!(current_status, SERVICE_START_PENDING | SERVICE_STOP_PENDING); + let status = SERVICE_STATUS { + dwServiceType: SERVICE_WIN32_OWN_PROCESS, + dwCurrentState: current_status, + dwControlsAccepted: if current_status == SERVICE_RUNNING { + SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN + } else { + 0 + }, + dwWin32ExitCode: win32_exit_code, + dwServiceSpecificExitCode: u32::from(win32_exit_code == ERROR_SERVICE_SPECIFIC_ERROR), + dwCheckPoint: u32::from(is_pending), + dwWaitHint: if is_pending { 10_000 } else { 0 }, + }; + + self.current_status.store(current_status, Ordering::Release); + // The SCM synchronously copies the status structure during this call. + let updated = unsafe { SetServiceStatus(status_handle, &status) }; + if updated == 0 { + return Err(io::Error::last_os_error()).context("update the Windows service status"); + } + + Ok(()) + } +}