From 738f4e3fe57eeb8bdc635d77d6d6b073ba838990 Mon Sep 17 00:00:00 2001 From: Microck Date: Fri, 2 Oct 2026 18:32:30 +0000 Subject: [PATCH 1/2] fix(computer-use): use the Windows desktop native app-server --- .../satelle/src/host/runtime-codex-tests.rs | 155 ++++++++++++++ crates/satelle/src/host/runtime-codex.rs | 201 +++++++++++++++++- .../codex-app-server-capability-matrix.md | 13 ++ 3 files changed, 358 insertions(+), 11 deletions(-) diff --git a/crates/satelle/src/host/runtime-codex-tests.rs b/crates/satelle/src/host/runtime-codex-tests.rs index d4fe37ce..22534871 100644 --- a/crates/satelle/src/host/runtime-codex-tests.rs +++ b/crates/satelle/src/host/runtime-codex-tests.rs @@ -3,6 +3,8 @@ use super::control_plane::MacosNativeSessionResources; use super::control_plane::macos_native_runtime_version; #[cfg(unix)] use super::control_plane::perform_handshake; +use super::control_plane::validate_windows_codex_app_layout; +use super::control_plane::windows_native_runtime_version; use super::control_plane::{ CodexImageInputMode, ControlPlaneAdmission, NATIVE_ISOLATION_TIMEOUT, NativeMcpBinding, PROBE_TIMEOUT, PlannedNativeComputerUseActionPath, bounded_inventory_command_output, @@ -321,6 +323,10 @@ fn windows_native_bridge_env() -> BTreeMap { fn expected_windows_native_binding_env() -> BTreeMap { let reported = windows_native_bridge_env(); BTreeMap::from([ + ( + "CODEX_CLI_PATH".to_string(), + reported["CODEX_CLI_PATH"].clone(), + ), ( "NODE_REPL_UNTRUSTED_ENV_ALLOWLIST".to_string(), "CODEX_WINDOWS_REGISTERED_CORE".to_string(), @@ -2039,6 +2045,155 @@ fn macos_codex_app_layout_rejects_redirected_cli_identity() { ); } +#[test] +fn windows_desktop_cli_layout_rejects_missing_and_redirected_components() { + let directory = tempfile::tempdir().expect("create Windows desktop CLI fixture"); + let root = directory.path().join("OpenAI.Codex"); + let resources = root.join("app").join("resources"); + std::fs::create_dir_all(&resources).expect("create package layout"); + let binary = resources.join("codex.exe"); + std::fs::write(&binary, "official fixture").expect("write CLI fixture"); + assert_eq!( + validate_windows_codex_app_layout(&root).expect("admit regular desktop layout"), + std::fs::canonicalize(&binary).expect("canonical CLI path") + ); + let canonical_root = std::fs::canonicalize(&root).expect("canonical package root"); + assert_eq!( + validate_windows_codex_app_layout(&canonical_root) + .expect("admit canonical Windows verbatim package path"), + std::fs::canonicalize(&binary).expect("canonical CLI path") + ); + let file = File::options() + .write(true) + .open(&binary) + .expect("open CLI fixture"); + file.set_len(310 * 1024 * 1024) + .expect("set current desktop CLI size"); + assert!(validate_windows_codex_app_layout(&root).is_ok()); + file.set_len(512 * 1024 * 1024 + 1) + .expect("exceed bounded CLI size"); + let oversized = validate_windows_codex_app_layout(&root).expect_err("reject oversized CLI"); + assert_eq!( + oversized.details["reason"], + json!("codex_app_runtime_untrusted") + ); + drop(file); + std::fs::remove_file(&binary).expect("remove fixture CLI"); + let missing = validate_windows_codex_app_layout(&root).expect_err("reject missing CLI"); + assert_eq!( + missing.details["reason"], + json!("codex_app_runtime_missing") + ); + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + let replacement = directory.path().join("replacement.exe"); + std::fs::write(&replacement, "unrelated fixture").expect("write redirected CLI"); + symlink(&replacement, &binary).expect("redirect CLI"); + let redirected = + validate_windows_codex_app_layout(&root).expect_err("reject redirected CLI"); + assert_eq!( + redirected.details["reason"], + json!("codex_app_runtime_untrusted") + ); + std::fs::remove_file(&binary).expect("remove redirected CLI"); + std::fs::remove_dir(&resources).expect("remove empty resources"); + let external = directory.path().join("external-resources"); + std::fs::create_dir(&external).expect("create redirected directory"); + symlink(&external, &resources).expect("redirect resources"); + let redirected = + validate_windows_codex_app_layout(&root).expect_err("reject redirected resources"); + assert_eq!( + redirected.details["reason"], + json!("codex_app_runtime_untrusted") + ); + } +} + +#[test] +fn windows_native_cli_inventory_rejects_paths_outside_the_official_cache() { + let command = Path::new( + r"C:\Users\operator\AppData\Local\OpenAI\Codex\runtimes\cua_node\f1359d6e9a17bb1d\bin\node_repl.exe", + ); + let codex_home = Path::new(r"C:\Users\operator\.codex"); + for binary in [ + r"C:\Users\other\AppData\Local\OpenAI\Codex\bin\a61afac3bb4ee395\codex.exe", + r"C:\Users\operator\AppData\Local\OpenAI\Codex\bin\short\codex.exe", + r"C:\Users\operator\AppData\Local\OpenAI\Codex\bin\a61afac3bb4ee395\other.exe", + r"C:\Users\operator\AppData\Local\OpenAI\Codex\bin\a61afac3bb4ee395\..\codex.exe", + r"\\server\Codex\bin\a61afac3bb4ee395\codex.exe", + ] { + let mut env = windows_native_bridge_env(); + env.insert("CODEX_CLI_PATH".to_string(), binary.to_string()); + assert!(trusted_windows_node_repl_env(&env, command, codex_home).is_none()); + } + let mut env = windows_native_bridge_env(); + env.remove("CODEX_CLI_PATH"); + assert!(trusted_windows_node_repl_env(&env, command, codex_home).is_none()); +} + +#[cfg(windows)] +#[test] +fn windows_desktop_cli_lock_authenticates_and_prevents_replacement() { + use super::control_plane::lock_windows_desktop_cli; + use sha2::{Digest, Sha256}; + + let directory = tempfile::tempdir().expect("create desktop CLI cache fixture"); + // CI exposes TEMP through a DOS alias; admitted cache paths are canonical. + let binary = std::fs::canonicalize(directory.path()) + .expect("resolve desktop CLI cache fixture") + .join("codex.exe"); + let bytes = b"current protected desktop CLI"; + std::fs::write(&binary, bytes).expect("write extracted CLI fixture"); + let canonical = std::fs::canonicalize(&binary).expect("resolve extracted CLI fixture"); + assert!( + same_path_for_platform(&canonical, &binary, "windows"), + "CLI fixture must use the admitted canonical cache path: {binary:?} -> {canonical:?}" + ); + assert!(lock_windows_desktop_cli(&binary, &[0; 32]).is_err()); + let digest: [u8; 32] = Sha256::digest(bytes).into(); + let lock = lock_windows_desktop_cli(&binary, &digest).expect("authenticate extracted CLI"); + assert!(File::options().write(true).open(&binary).is_err()); + assert!(std::fs::remove_file(&binary).is_err()); + assert_eq!(std::fs::read(&binary).expect("read locked CLI"), bytes); + drop(lock); + std::fs::remove_file(&binary).expect("release cached CLI lock with session resources"); +} + +#[test] +fn native_binary_hash_failures_identify_the_authenticated_component() { + use super::control_plane::native_binary_digest; + use sha2::{Digest, Sha256}; + + let directory = tempfile::tempdir().expect("create authenticated binary fixture"); + let binary = directory.path().join("binary"); + for reason in ["native_bridge_untrusted", "codex_app_runtime_untrusted"] { + let missing = native_binary_digest(&binary, 4, reason).expect_err("reject missing image"); + assert_eq!(missing.details["reason"], json!(reason)); + } + std::fs::write(&binary, b"image").expect("write authenticated binary fixture"); + for reason in ["native_bridge_untrusted", "codex_app_runtime_untrusted"] { + let oversized = + native_binary_digest(&binary, 4, reason).expect_err("reject oversized image"); + assert_eq!(oversized.details["reason"], json!(reason)); + } + let expected: [u8; 32] = Sha256::digest(b"image").into(); + assert_eq!( + native_binary_digest(&binary, 5, "native_bridge_untrusted").expect("hash bounded image"), + expected + ); +} + +#[test] +fn windows_readiness_identity_invalidates_when_the_desktop_cli_changes() { + let identity = windows_native_runtime_version(&[1; 32], &[2; 32]); + assert_eq!(identity, windows_native_runtime_version(&[1; 32], &[2; 32])); + assert_ne!(identity, windows_native_runtime_version(&[1; 32], &[3; 32])); + assert_ne!(identity, windows_native_runtime_version(&[3; 32], &[2; 32])); + assert!(identity.starts_with("sha256-")); + assert_eq!(identity.len(), 71); +} + #[test] fn macos_setup_maps_only_missing_official_components_to_manual_action() { for reason in [ diff --git a/crates/satelle/src/host/runtime-codex.rs b/crates/satelle/src/host/runtime-codex.rs index e4521c3d..cdf5ebd4 100644 --- a/crates/satelle/src/host/runtime-codex.rs +++ b/crates/satelle/src/host/runtime-codex.rs @@ -63,6 +63,9 @@ const CODEX_PACKAGE_MARKETPLACE: &str = "app/resources/plugins/openai-bundled"; const NATIVE_BRIDGE_FILE_LIMIT: u64 = 64 * 1024 * 1024; #[cfg(target_os = "macos")] const MACOS_NATIVE_LAUNCHER_FILE_LIMIT: u64 = 256 * 1024 * 1024; +// The current protected Windows desktop CLI is larger than the Mac launcher. +#[cfg(any(windows, all(test, unix)))] +const WINDOWS_NATIVE_CLI_FILE_LIMIT: u64 = 512 * 1024 * 1024; #[cfg(any(target_os = "macos", all(test, unix)))] const MACOS_SERVICE_INFO_LIMIT: u64 = 1024 * 1024; const WINDOWS_LOCKED_BRIDGE_SCRIPT: &str = r#"& { param([string]$bridge,[string]$expected) $ErrorActionPreference='Stop'; if ([string]::IsNullOrEmpty($bridge) -or [string]::IsNullOrEmpty($expected)) { exit 64 }; $stream=$null; $child=$null; try { $stream=[System.IO.File]::Open($bridge,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read,[System.IO.FileShare]::Read); $sha=[System.Security.Cryptography.SHA256]::Create(); try { $actual=([System.BitConverter]::ToString($sha.ComputeHash($stream))).Replace('-','') } finally { $sha.Dispose() }; if (-not $actual.Equals($expected,[System.StringComparison]::OrdinalIgnoreCase)) { exit 74 }; $start=New-Object System.Diagnostics.ProcessStartInfo; $start.FileName=$bridge; $start.UseShellExecute=$false; $child=[System.Diagnostics.Process]::Start($start); if ($null -eq $child) { exit 74 } } catch { exit 74 } finally { if ($null -ne $stream) { $stream.Dispose() } }; $child.WaitForExit(); exit $child.ExitCode }"#; @@ -477,6 +480,17 @@ fn verified_computer_use_app_server_with_commands( .native_mcp_binding .args .splice(0..0, prepared_native_bridge.prefix_args); + #[cfg(windows)] + let app_server_command = { + // The authenticated desktop CLI supplies the native execution core. The + // managed CLI remains the current installation and inventory runtime. + drop(app_server_command); + let binary = &isolation.native_mcp_binding.env["CODEX_CLI_PATH"]; + let mut command = Command::new(binary); + command.env("CODEX_HOME", runtime.codex_home()); + command.env("CODEX_CLI_PATH", binary); + command + }; let native_action_path = match isolation.planned_native_action_path { PlannedNativeComputerUseActionPath::WindowsNodeRepl => { NativeComputerUseActionPath::WindowsNodeRepl @@ -1720,7 +1734,33 @@ fn prepare_native_bridge( // holds the checked cache file against writes and replacement // until Windows has opened the child image. let inventory_digest = native_bridge_digest(path)?; - protected_windows_native_bridge(&inventory_digest)?; + let protected_bridge = protected_windows_native_bridge(&inventory_digest)?; + #[cfg(windows)] + let (native_runtime_version, desktop_cli_lock) = { + // Select the CLI from the same protected package that admitted + // this bridge, rather than a mutable cache or a version pin. + let package_root = protected_bridge + .ancestors() + .nth(Path::new(CODEX_PACKAGE_NODE_REPL).components().count()) + .ok_or_else(|| codex_isolation_error("codex_app_runtime_untrusted"))?; + let protected_binary = validate_windows_codex_app_layout(package_root)?; + let codex_digest = native_binary_digest( + &protected_binary, + WINDOWS_NATIVE_CLI_FILE_LIMIT, + "codex_app_runtime_untrusted", + )?; + let binary = Path::new(&native_env["CODEX_CLI_PATH"]); + let lock = lock_windows_desktop_cli(binary, &codex_digest)?; + ( + windows_native_runtime_version(&inventory_digest, &codex_digest), + lock, + ) + }; + #[cfg(not(windows))] + let native_runtime_version = { + let _ = protected_bridge; + format!("sha256-{}", hex_digest(&inventory_digest)) + }; #[cfg(windows)] let native_resources = { let staging = windows_native_staging::WindowsNativeStaging::create().map_err(|_| { @@ -1737,6 +1777,7 @@ fn prepare_native_bridge( } NativeSessionResources { _windows: Some(staging), + _windows_cli: Some(desktop_cli_lock), } }; #[cfg(not(windows))] @@ -1744,7 +1785,7 @@ fn prepare_native_bridge( Ok(PreparedNativeBridge { command: windows_powershell_path()?.to_string_lossy().into_owned(), prefix_args: windows_locked_bridge_args(path, &inventory_digest), - native_runtime_version: format!("sha256-{}", hex_digest(&inventory_digest)), + native_runtime_version, native_resources, }) } @@ -1789,6 +1830,8 @@ struct PreparedNativeBridge { pub(crate) struct NativeSessionResources { #[cfg(windows)] _windows: Option, + #[cfg(windows)] + _windows_cli: Option, #[cfg(target_os = "macos")] _macos: Option, } @@ -1801,6 +1844,7 @@ impl NativeSessionResources { ( Self { _windows: Some(staging), + _windows_cli: None, }, path, ) @@ -1810,6 +1854,8 @@ impl NativeSessionResources { Self { #[cfg(windows)] _windows: None, + #[cfg(windows)] + _windows_cli: None, #[cfg(target_os = "macos")] _macos: None, } @@ -2089,16 +2135,120 @@ pub(super) fn native_bridge_digest(path: &Path) -> Result<[u8; 32], SatelleError { return Err(codex_isolation_error("native_bridge_untrusted")); } - let mut bytes = Vec::new(); - File::open(path) - .map_err(|_| codex_isolation_error("native_bridge_untrusted"))? - .take(NATIVE_BRIDGE_FILE_LIMIT + 1) - .read_to_end(&mut bytes) - .map_err(|_| codex_isolation_error("native_bridge_untrusted"))?; - if bytes.len() > NATIVE_BRIDGE_FILE_LIMIT as usize { - return Err(codex_isolation_error("native_bridge_untrusted")); + native_binary_digest(path, NATIVE_BRIDGE_FILE_LIMIT, "native_bridge_untrusted") +} + +// The caller admits the regular file and size limit at its trust boundary. +// Bound the read too, so a mutable bridge cannot grow beyond that limit. +pub(super) fn native_binary_digest( + path: &Path, + size_limit: u64, + untrusted_reason: &'static str, +) -> Result<[u8; 32], SatelleError> { + let mut reader = File::open(path) + .map_err(|_| codex_isolation_error(untrusted_reason))? + .take(size_limit + 1); + let mut digest = Sha256::new(); + let mut total = 0_u64; + // Desktop CLIs can exceed 300 MiB; hashing must not retain the whole image. + let mut buffer = [0_u8; 64 * 1024]; + loop { + let length = match reader.read(&mut buffer) { + Ok(length) => length, + // Preserve read_to_end's interrupted-read behavior when streaming. + Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue, + Err(_) => return Err(codex_isolation_error(untrusted_reason)), + }; + if length == 0 { + break; + } + total += length as u64; + if total > size_limit { + return Err(codex_isolation_error(untrusted_reason)); + } + digest.update(&buffer[..length]); } - Ok(Sha256::digest(bytes).into()) + Ok(digest.finalize().into()) +} + +#[cfg(any(windows, all(test, unix)))] +pub(super) fn validate_windows_codex_app_layout( + package_root: &Path, +) -> Result { + let resources = package_root.join("app").join("resources"); + for directory in [package_root, &package_root.join("app"), &resources] { + let metadata = fs::symlink_metadata(directory) + .map_err(|_| codex_isolation_error("codex_app_runtime_missing"))?; + if !metadata.is_dir() || metadata.file_type().is_symlink() { + return Err(codex_isolation_error("codex_app_runtime_untrusted")); + } + } + let binary = resources.join("codex.exe"); + let metadata = fs::symlink_metadata(&binary) + .map_err(|_| codex_isolation_error("codex_app_runtime_missing"))?; + let canonical = fs::canonicalize(&binary) + .map_err(|_| codex_isolation_error("codex_app_runtime_untrusted"))?; + let canonical_root = fs::canonicalize(package_root) + .map_err(|_| codex_isolation_error("codex_app_runtime_untrusted"))?; + if !metadata.is_file() + || metadata.file_type().is_symlink() + || metadata.len() > WINDOWS_NATIVE_CLI_FILE_LIMIT + || canonical + != canonical_root + .join("app") + .join("resources") + .join("codex.exe") + { + return Err(codex_isolation_error("codex_app_runtime_untrusted")); + } + Ok(canonical) +} + +#[cfg(any(windows, test))] +pub(super) fn windows_native_runtime_version( + bridge_digest: &[u8; 32], + codex_digest: &[u8; 32], +) -> String { + let mut digest = Sha256::new(); + digest.update(b"satelle-windows-native-runtime-v1\0"); + digest.update(bridge_digest); + digest.update(codex_digest); + format!("sha256-{}", hex_digest(&digest.finalize().into())) +} + +#[cfg(windows)] +pub(super) fn lock_windows_desktop_cli( + binary: &Path, + protected_digest: &[u8; 32], +) -> Result { + use std::os::windows::fs::OpenOptionsExt; + use windows_sys::Win32::Storage::FileSystem::FILE_SHARE_READ; + + // Keep the official extracted image readable but not writable or replaceable. + // The protected AppX executable authenticates it; AppX ACLs prevent launching + // that protected copy directly from the Host process. + let lock = File::options() + .read(true) + .share_mode(FILE_SHARE_READ) + .open(binary) + .map_err(|_| codex_isolation_error("codex_app_runtime_untrusted"))?; + let metadata = fs::symlink_metadata(binary) + .map_err(|_| codex_isolation_error("codex_app_runtime_untrusted"))?; + let canonical = fs::canonicalize(binary) + .map_err(|_| codex_isolation_error("codex_app_runtime_untrusted"))?; + if !metadata.is_file() + || metadata.file_type().is_symlink() + || metadata.len() > WINDOWS_NATIVE_CLI_FILE_LIMIT + || !same_path_for_platform(&canonical, binary, "windows") + || native_binary_digest( + binary, + WINDOWS_NATIVE_CLI_FILE_LIMIT, + "codex_app_runtime_untrusted", + )? != *protected_digest + { + return Err(codex_isolation_error("codex_app_runtime_untrusted")); + } + Ok(lock) } #[cfg(windows)] @@ -3112,6 +3262,10 @@ pub(super) fn trusted_windows_node_repl_env( let trusted_code_paths = trusted_windows_node_repl_code_paths(reported_env, command, codex_home)?; let (node_modules, node) = windows_node_repl_runtime_paths(command)?; + let desktop_cli = reported_env.get("CODEX_CLI_PATH")?; + if !trusted_windows_desktop_cli_path(Path::new(desktop_cli), command) { + return None; + } // Inventory capabilities are a set; only the isolated Sky runtime's // admitted backends are forwarded below, regardless of desktop ordering. let backends = reported_env.get("BROWSER_USE_AVAILABLE_BACKENDS")?; @@ -3168,6 +3322,7 @@ pub(super) fn trusted_windows_node_repl_env( } Some(BTreeMap::from([ + ("CODEX_CLI_PATH".to_string(), desktop_cli.clone()), ( "NODE_REPL_UNTRUSTED_ENV_ALLOWLIST".to_string(), "CODEX_WINDOWS_REGISTERED_CORE".to_string(), @@ -3205,6 +3360,30 @@ pub(super) fn trusted_windows_node_repl_env( ])) } +fn trusted_windows_desktop_cli_path(binary: &Path, bridge: &Path) -> bool { + let Some(binary) = normalized_windows_drive_path(binary) else { + return false; + }; + let Some(bridge) = normalized_windows_drive_path(bridge) else { + return false; + }; + let binary = binary.to_ascii_lowercase(); + let bridge = bridge.to_ascii_lowercase(); + let Some((root, _)) = bridge.rsplit_once("/runtimes/cua_node/") else { + return false; + }; + let prefix = format!("{root}/bin/"); + let Some(suffix) = binary.strip_prefix(&prefix) else { + return false; + }; + let Some((identity, name)) = suffix.split_once('/') else { + return false; + }; + identity.len() == 16 + && identity.bytes().all(|byte| byte.is_ascii_hexdigit()) + && name == "codex.exe" +} + fn windows_node_repl_code_paths(command: &Path, codex_home: &Path) -> Option { let (node_modules, _) = windows_node_repl_runtime_paths(command)?; Some(format!("{};{}", codex_home.to_string_lossy(), node_modules)) diff --git a/docs/reference/codex-app-server-capability-matrix.md b/docs/reference/codex-app-server-capability-matrix.md index f6cc418a..68c43a8c 100644 --- a/docs/reference/codex-app-server-capability-matrix.md +++ b/docs/reference/codex-app-server-capability-matrix.md @@ -101,6 +101,19 @@ so the official provider can forward its registered core to native execution. Satelle does not read, store, or export the opaque core value, and rejects missing declarations or additional inherited environment names. +Windows native Computer Use also uses the Codex app-server embedded in the +current registered desktop package. Satelle admits the latest managed CLI for +installation and inventory, authenticates the bridge against that protected +AppX package, then launches the desktop's official extracted CLI with the same +managed home. Windows does not allow direct execution from the protected package. +The extracted CLI must match the protected executable byte for byte and remains +locked against writes and replacement while the native session owns it. +The native helper receives that authenticated `CODEX_CLI_PATH`. A standalone CLI +does not supply the desktop's native execution core. Satelle does not substitute +an older installed package or pin the managed CLI. Missing or redirected desktop +components fail admission. Both bridge and bundled app-server hashes bind the +Windows readiness identity, so a desktop runtime update requires fresh proof. + Codex Desktop and bundled Computer Use component versions are evidence and cache identity, not admission pins. Satelle authenticates the platform package or OpenAI signing identity, validates bundled provenance and bridge shape, and From e1e83ff3bda4bfbf4276d8644f4ad4cdb6743775 Mon Sep 17 00:00:00 2001 From: Microck Date: Sat, 3 Oct 2026 11:29:46 +0000 Subject: [PATCH 2/2] fix(computer-use): match Windows probe logical coordinates --- .facts | 2 +- .../satelle/src/host/runtime-codex-adapter.rs | 2 +- .../satelle/src/host/windows-native-probe.rs | 23 ++++++++----------- .../codex-app-server-capability-matrix.md | 5 ++++ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/.facts b/.facts index fb3dc5bc..6f166d4b 100644 --- a/.facts +++ b/.facts @@ -423,7 +423,7 @@ - satelle run and steer do not require the user to run satelle doctor --scope computer-use --refresh before first prompt execution @spec @mvp @implemented - satelle run and steer do not start the prompt turn until native Computer Use readiness passes through a valid cache entry or a live harmless readiness smoke test @spec @mvp @implemented - satelle native Computer Use readiness checks distinguish pointer click support from click-and-drag support instead of treating a successful click as full pointer readiness @spec @mvp @implemented -- the Windows native readiness probe uses a borderless 1024 by 678 physical-pixel surface on its own per-monitor DPI-aware thread so native screenshot coordinates match the click and drag targets without display-scale or window-frame offsets @spec @mvp @implemented +- the Windows native readiness probe uses a borderless 1024 by 678 logical-pixel surface on its own DPI-unaware thread so the official Windows SDK coordinate space matches its click and drag targets at every display scale; both native callbacks remain required @spec @mvp @implemented - satelle readiness and diagnostics treat file-management workflows as a distinct Computer Use capability area rather than assuming general desktop readiness covers file selection, file movement, uploads, downloads, and save dialogs @spec @mvp @implemented - satelle quiet output may suppress native readiness smoke-test progress text but does not skip native readiness smoke tests required for prompt execution @spec @mvp @implemented - satelle run and steer JSON event streaming emits readiness events that distinguish native readiness cache hits, live native readiness smoke tests, passes, failures, and manual-action-required blockers before turn_started @spec @mvp @implemented diff --git a/crates/satelle/src/host/runtime-codex-adapter.rs b/crates/satelle/src/host/runtime-codex-adapter.rs index f08c25f7..60258b02 100644 --- a/crates/satelle/src/host/runtime-codex-adapter.rs +++ b/crates/satelle/src/host/runtime-codex-adapter.rs @@ -1515,7 +1515,7 @@ fn native_readiness_prompt( if !allowed_app_ids.contains("satelle.exe") { return Err("native_app_approval_unavailable"); } - // The native probe has a borderless physical-pixel layout. Coordinate input keeps + // The native probe shares the SDK's borderless logical-pixel layout. Coordinate input keeps // this generated cell short enough for the model to copy verbatim, // while the private callback remains the authority for both events. let script = "globalThis.sky??=(await import('@oai/sky')).sky;var w=(await sky.list_windows()).find(x=>x.app.toLowerCase().endsWith('satelle.exe')&&x.title==='Satelle native readiness probe'),g=w=>sky.get_window_state({window:w,include_screenshot:true,include_text:true}),s=await g(w);await sky.click({window:s.window,x:190,y:142,screenshotId:s.screenshots[0].id});s=await g(s.window);await sky.drag({window:s.window,from_x:230,from_y:325,to_x:660,to_y:430,screenshotId:s.screenshots[0].id})".to_string(); diff --git a/crates/satelle/src/host/windows-native-probe.rs b/crates/satelle/src/host/windows-native-probe.rs index a674f5cc..839dc599 100644 --- a/crates/satelle/src/host/windows-native-probe.rs +++ b/crates/satelle/src/host/windows-native-probe.rs @@ -14,9 +14,7 @@ use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW; use windows_sys::Win32::System::RemoteDesktop::ProcessIdToSessionId; use windows_sys::Win32::System::SystemServices::SS_LEFT; use windows_sys::Win32::System::Threading::GetCurrentProcessId; -use windows_sys::Win32::UI::HiDpi::{ - DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetThreadDpiAwarenessContext, -}; +use windows_sys::Win32::UI::HiDpi::{DPI_AWARENESS_CONTEXT_UNAWARE, SetThreadDpiAwarenessContext}; use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ReleaseCapture, SetCapture}; use windows_sys::Win32::UI::WindowsAndMessaging::{ BS_PUSHBUTTON, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, GWLP_USERDATA, @@ -151,10 +149,10 @@ fn run_window( shutdown: Arc, ready_sender: mpsc::SyncSender>, ) { - // This worker owns only the transient readiness window. Physical pixels - // must match Sky screenshots; do not change the Host's process-wide DPI. - if unsafe { SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }.is_null() - { + // The native Windows SDK uses logical window coordinates. Let Windows scale + // this transient surface so its 1024 by 678 layout and input coordinates + // agree at every monitor scale; keep the Host's process-wide DPI unchanged. + if unsafe { SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE) }.is_null() { let _ = ready_sender.send(Err(std::io::Error::last_os_error())); return; } @@ -560,7 +558,7 @@ mod tests { use super::*; #[test] - fn native_probe_surface_has_physical_pixel_geometry_and_no_frame_offset() { + fn native_probe_surface_matches_sdk_logical_coordinates_and_has_no_frame_offset() { use windows_sys::Win32::UI::HiDpi::{ AreDpiAwarenessContextsEqual, GetWindowDpiAwarenessContext, }; @@ -586,15 +584,14 @@ mod tests { unsafe { AreDpiAwarenessContextsEqual( GetWindowDpiAwarenessContext(window), - DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, + DPI_AWARENESS_CONTEXT_UNAWARE, ) }, 0, ); - // Observe from a DPI-aware caller too; otherwise Win32 virtualizes - // the observer's coordinates and hides the physical-pixel contract. - let previous = - unsafe { SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) }; + // Observe in the same logical coordinate space as the native Windows SDK. + // This must remain 1024 by 678 even when Windows scales the surface. + let previous = unsafe { SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE) }; assert!(!previous.is_null()); let mut client = RECT::default(); let mut outer = RECT::default(); diff --git a/docs/reference/codex-app-server-capability-matrix.md b/docs/reference/codex-app-server-capability-matrix.md index 68c43a8c..12749b93 100644 --- a/docs/reference/codex-app-server-capability-matrix.md +++ b/docs/reference/codex-app-server-capability-matrix.md @@ -318,6 +318,11 @@ permissions from app approvals: `ComputerUseAppApprovals.json`; and - app approvals may still require direct user action. +The Windows readiness window uses a borderless 1024 by 678 logical-pixel +layout on a DPI-unaware owner thread. Windows scales the surface while the +official SDK coordinates and native window messages keep the same units. The +probe still requires independently observed click and drag callbacks. + The Windows Host probe obtains the active Codex home from the live app-server `initialize` response, not from a remembered default path. It reads the raw parsed base user layer through `config/read` and requires that layer to identify