diff --git a/codex-rs/windows-sandbox-rs/src/app_package.rs b/codex-rs/windows-sandbox-rs/src/app_package.rs index da121c3e12..50a6e7dbdc 100644 --- a/codex-rs/windows-sandbox-rs/src/app_package.rs +++ b/codex-rs/windows-sandbox-rs/src/app_package.rs @@ -196,13 +196,8 @@ pub(crate) fn registered_runner_alias( let package = current_package_full_name()?.context("registered Core launch requires an installed app")?; let owner = crate::winutil::resolve_sid(&crate::runtime_ownership::current_setup_user()?)?; - anyhow::ensure!( - record.user_sid - == crate::winutil::string_from_sid_bytes(&owner).map_err(anyhow::Error::msg)? - && record.codex_home == codex_home.canonicalize()? - && record.runtime()?.ready_for_package(&package), - "registered Core setup belongs to another owner or is being removed" - ); + let owner = crate::winutil::string_from_sid_bytes(&owner).map_err(anyhow::Error::msg)?; + authorize_runner_receipt(&record, &owner, &codex_home.canonicalize()?, &package)?; let entry = record .runtime()? .accounts @@ -227,6 +222,32 @@ pub(crate) fn registered_runner_alias( Ok(path.clone()) } +fn authorize_runner_receipt( + record: &crate::installation_record::InstallationRecord, + owner: &str, + codex_home: &Path, + package: &str, +) -> Result<()> { + ensure!( + record.user_sid == owner, + "registered Core setup belongs to another owner" + ); + ensure!( + record.codex_home == codex_home, + "registered Core setup belongs to another Codex home" + ); + let runtime = record.runtime()?; + ensure!( + runtime.retiring.is_none(), + "registered Core setup is being removed" + ); + ensure!( + runtime.ready_for_package(package), + "registered Core setup is not complete for the installed app; retry sandbox setup" + ); + Ok(()) +} + #[cfg(test)] #[path = "app_package_tests.rs"] mod tests; diff --git a/codex-rs/windows-sandbox-rs/src/app_package_tests.rs b/codex-rs/windows-sandbox-rs/src/app_package_tests.rs index 015acffcf8..d4d06e841e 100644 --- a/codex-rs/windows-sandbox-rs/src/app_package_tests.rs +++ b/codex-rs/windows-sandbox-rs/src/app_package_tests.rs @@ -1,11 +1,17 @@ //! Runtime opt-in and bounded OS package-name results, without installed-state changes. +use super::authorize_runner_receipt; use super::query_package_name; use super::requested_value; +use crate::installation_record::InstallationRecord; +use crate::runtime_ownership::RuntimeAccountRegistration; +use crate::runtime_ownership::RuntimeRegistration; +use crate::runtime_ownership::SandboxRuntimeAccount; use pretty_assertions::assert_eq; use std::ffi::OsStr; use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; +use std::path::Path; use windows_sys::Win32::Foundation::APPMODEL_ERROR_NO_PACKAGE; use windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED; use windows_sys::Win32::Foundation::ERROR_INSUFFICIENT_BUFFER; @@ -29,6 +35,78 @@ fn only_exact_opt_in_requests_registered_core() { assert!(!requested_value(Some(&OsString::from_wide(&[0xd800])))); } +#[test] +fn runner_receipt_distinguishes_incomplete_setup_from_owner_and_removal() { + let owner = "S-1-5-21-100"; + let home = Path::new(r"C:\Users\owner\.codex"); + let package = "OpenAI.Codex_current"; + let runtime = RuntimeRegistration { + package_family: "OpenAI.Codex_family".to_owned(), + accounts: Vec::new(), + metadata_roots: Vec::new(), + ready_package: None, + retiring: None, + }; + let mut record = InstallationRecord { + user_sid: owner.to_owned(), + codex_home: home.to_owned(), + session_id: 1, + desktop_installation: None, + runtime: Some(runtime), + }; + let incomplete = + "registered Core setup is not complete for the installed app; retry sandbox setup"; + assert_eq!( + authorize_runner_receipt(&record, owner, home, package) + .unwrap_err() + .to_string(), + incomplete + ); + record.runtime.as_mut().unwrap().retiring = Some("removing".to_owned()); + for (candidate_owner, candidate_home, message) in [ + ( + "S-1-5-21-200", + home, + "registered Core setup belongs to another owner", + ), + ( + owner, + Path::new(r"C:\Users\other\.codex"), + "registered Core setup belongs to another Codex home", + ), + (owner, home, "registered Core setup is being removed"), + ] { + assert_eq!( + authorize_runner_receipt(&record, candidate_owner, candidate_home, package) + .unwrap_err() + .to_string(), + message + ); + } + let runtime = record.runtime.as_mut().unwrap(); + runtime.retiring = None; + runtime.accounts = [ + SandboxRuntimeAccount::Offline, + SandboxRuntimeAccount::Online, + ] + .into_iter() + .map(|account| RuntimeAccountRegistration { + cleanup_logon_pending: false, + account, + user_sid: format!("{account:?}"), + alias_path: Some(home.join(account.username())), + }) + .collect(); + runtime.ready_package = Some(package.to_owned()); + assert!(authorize_runner_receipt(&record, owner, home, package).is_ok()); + assert_eq!( + authorize_runner_receipt(&record, owner, home, "OpenAI.Codex_updated") + .unwrap_err() + .to_string(), + incomplete + ); +} + #[test] fn package_query_distinguishes_absence_from_failure_and_bounds_allocation() { assert_eq!( diff --git a/codex-rs/windows-sandbox-service/src/provisioning/registered.rs b/codex-rs/windows-sandbox-service/src/provisioning/registered.rs index 4445524d5c..43e958b2e8 100644 --- a/codex-rs/windows-sandbox-service/src/provisioning/registered.rs +++ b/codex-rs/windows-sandbox-service/src/provisioning/registered.rs @@ -77,13 +77,15 @@ pub(super) fn run( retiring: None, }); // Persist the authenticated owner before setup can rotate shared credentials. - crate::installation_record::save_runtime(&installation)?; + crate::installation_record::save_runtime(&installation) + .context("persist registered sandbox owner")?; // Expired passwords require full setup to rotate and persist credentials before logon. for account in [ codex_windows_sandbox::OFFLINE_USERNAME, codex_windows_sandbox::ONLINE_USERNAME, ] { - setup_complete &= codex_windows_sandbox::local_user_flags(account)? + setup_complete &= codex_windows_sandbox::local_user_flags(account) + .with_context(|| format!("inspect registered sandbox account {account}"))? .is_some_and(|flags| flags & (UF_ACCOUNTDISABLE | UF_PASSWORD_EXPIRED) == 0); } if !setup_complete { @@ -93,7 +95,8 @@ pub(super) fn run( "registration refresh cannot repair sandbox setup" ); // Setup resolves this name for ACL trustees; it must still name the held token's user. - let setup_owner_sid = codex_windows_sandbox::resolve_sid(&identity.account)?; + let setup_owner_sid = codex_windows_sandbox::resolve_sid(&identity.account) + .context("resolve registered sandbox setup owner")?; ensure!( string_from_sid_bytes(&setup_owner_sid).map_err(anyhow::Error::msg)? == identity.user_sid, @@ -104,14 +107,18 @@ pub(super) fn run( &identity.account, settings, identity.runtime, - )?; + ) + .context("provision registered sandbox accounts and permissions")?; } crate::registered_runtime::provision( identity, &mut installation, - &package.FullName()?, + &package + .FullName() + .context("read registered sandbox package name")?, shutdown, ) + .context("prepare registered sandbox runtime") })(); result .inspect_err(|error| { diff --git a/codex-rs/windows-sandbox-service/src/registered_runtime.rs b/codex-rs/windows-sandbox-service/src/registered_runtime.rs index e9b82eb3fb..d1132d96e3 100644 --- a/codex-rs/windows-sandbox-service/src/registered_runtime.rs +++ b/codex-rs/windows-sandbox-service/src/registered_runtime.rs @@ -106,7 +106,8 @@ pub(crate) fn provision( &mut pins, )?; logon_existing_sandbox_account(&identity.codex_home, account) - })?; + }) + .with_context(|| format!("log on managed runtime account {account:?}"))?; let expected = record .runtime()? .accounts @@ -116,8 +117,10 @@ pub(crate) fn provision( token, account, expected.map(|entry| entry.user_sid.as_str()), - )?; - let installed = registered_packages(&profile.user_sid, &family)?; + ) + .with_context(|| format!("prepare managed runtime profile for {account:?}"))?; + let installed = registered_packages(&profile.user_sid, &family) + .with_context(|| format!("query existing runtime packages for {account:?}"))?; if expected.is_none() { ensure!( installed.is_empty(), @@ -128,7 +131,8 @@ pub(crate) fn provision( profile.token.as_raw_handle() as _, &FOLDERID_LocalAppData, /*flags*/ 0, - )? + ) + .with_context(|| format!("resolve runtime alias location for {account:?}"))? .join(r"Microsoft\WindowsApps") .join(&family) .join(APP_CORE_RUNNER_ALIAS); @@ -148,7 +152,8 @@ pub(crate) fn provision( record.runtime_mut()?.accounts = accounts; if !receipt_current { record.runtime_mut()?.ready_package = None; - crate::installation_record::save_runtime(record)?; + crate::installation_record::save_runtime(record) + .context("persist managed runtime account registration")?; } for (mut profile, installed) in profiles { if !installed {