Improve Windows sandbox setup refresh diagnostics (#26471)

## Why

Users have been seeing opaque Windows sandbox setup refresh failures
such as `windows sandbox: spawn setup refresh`, including reports in
#24391 and #21208. The setup refresh path already runs the Windows
sandbox setup helper, but it was not using the same structured
`setup_error.json` reporting path that elevated setup uses. As a result,
when the helper exited non-zero, Codex only surfaced a generic refresh
status instead of the helper's `SetupFailure` code and message.

## What changed

- Clear stale `setup_error.json` before non-elevated setup refresh
launches the helper.
- When the refresh helper exits non-zero, read the helper-written report
through the existing `report_helper_failure` path.
- Keep a parent-side launch diagnostic for cases where the helper never
starts, including the helper path, cwd, sandbox log path, and spawn
error.
- Clear the setup error report after a successful refresh.
- Add regression coverage for report consumption and stale-report
avoidance.

## Verification

- `cargo test -p codex-windows-sandbox setup::tests::`
This commit is contained in:
iceweasel-oai authored and GitHub committed 2026-06-04 16:52:10 -07:00
1 parent b9ff450902
commit 0b2e7b5eb1
2 files changed
+97 -15

No files matched your search

+96 -14
View File
@@ -15,6 +15,7 @@ use crate::allow::compute_allow_paths_for_permissions;
use crate::helper_materialization::bundled_executable_path_for_exe; use crate::helper_materialization::bundled_executable_path_for_exe;
use crate::helper_materialization::helper_bin_dir; use crate::helper_materialization::helper_bin_dir;
use crate::identity::sandbox_setup_is_complete; use crate::identity::sandbox_setup_is_complete;
use crate::logging::current_log_file_path;
use crate::logging::log_note; use crate::logging::log_note;
use crate::path_normalization::canonical_path_key; use crate::path_normalization::canonical_path_key;
use crate::path_normalization::canonicalize_path; use crate::path_normalization::canonicalize_path;
@@ -25,7 +26,6 @@ use crate::setup_error::clear_setup_error_report;
use crate::setup_error::failure; use crate::setup_error::failure;
use crate::setup_error::read_setup_error_report; use crate::setup_error::read_setup_error_report;
use crate::ssh_config_dependencies::ssh_config_dependency_paths; use crate::ssh_config_dependencies::ssh_config_dependency_paths;
use anyhow::Context;
use anyhow::Result; use anyhow::Result;
use anyhow::anyhow; use anyhow::anyhow;
use base64::Engine; use base64::Engine;
@@ -210,6 +210,18 @@ fn run_setup_refresh_inner(
let json = serde_json::to_vec(&payload)?; let json = serde_json::to_vec(&payload)?;
let b64 = BASE64_STANDARD.encode(json); let b64 = BASE64_STANDARD.encode(json);
let exe = find_setup_exe(); let exe = find_setup_exe();
let sbx_dir = sandbox_dir(request.codex_home);
let log_path = current_log_file_path(&sbx_dir);
let cleared_report = match clear_setup_error_report(request.codex_home) {
Ok(()) => true,
Err(err) => {
log_note(
&format!("setup refresh: failed to clear setup_error.json before launch: {err}"),
Some(&sbx_dir),
);
false
}
};
// Refresh should never request elevation; ensure verb isn't set and we don't trigger UAC. // Refresh should never request elevation; ensure verb isn't set and we don't trigger UAC.
let mut cmd = Command::new(&exe); let mut cmd = Command::new(&exe);
cmd.arg(&b64).stdout(Stdio::null()).stderr(Stdio::null()); cmd.arg(&b64).stdout(Stdio::null()).stderr(Stdio::null());
@@ -221,24 +233,34 @@ fn run_setup_refresh_inner(
cwd.display(), cwd.display(),
b64.len() b64.len()
), ),
Some(&sandbox_dir(request.codex_home)), Some(&sbx_dir),
); );
let status = cmd let status = cmd.status().map_err(|err| {
.status() let message = format!(
.map_err(|e| { "setup refresh failed to launch helper: helper={}, cwd={}, log={}, error={err}",
log_note( exe.display(),
&format!("setup refresh: failed to spawn {}: {e}", exe.display()), cwd.display(),
Some(&sandbox_dir(request.codex_home)), log_path.display()
); );
e log_note(&format!("setup refresh: {message}"), Some(&sbx_dir));
}) failure(SetupErrorCode::OrchestratorHelperLaunchFailed, message)
.context("spawn setup refresh")?; })?;
if !status.success() { if !status.success() {
log_note( log_note(
&format!("setup refresh: exited with status {status:?}"), &format!("setup refresh: exited with status {status:?}"),
Some(&sandbox_dir(request.codex_home)), Some(&sbx_dir),
);
return Err(report_helper_failure(
request.codex_home,
cleared_report,
status.code(),
));
}
if let Err(err) = clear_setup_error_report(request.codex_home) {
log_note(
&format!("setup refresh: failed to clear setup_error.json after success: {err}"),
Some(&sbx_dir),
); );
return Err(anyhow!("setup refresh failed with status {status}"));
} }
Ok(()) Ok(())
} }
@@ -1087,7 +1109,9 @@ mod tests {
use crate::helper_materialization::helper_bin_dir; use crate::helper_materialization::helper_bin_dir;
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions; use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
use crate::setup_error::SetupErrorCode; use crate::setup_error::SetupErrorCode;
use crate::setup_error::SetupErrorReport;
use crate::setup_error::extract_failure; use crate::setup_error::extract_failure;
use crate::setup_error::write_setup_error_report;
use codex_protocol::models::PermissionProfile; use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBuf;
@@ -1146,6 +1170,64 @@ mod tests {
) )
} }
#[test]
fn report_helper_failure_uses_setup_error_report_when_clear_succeeded() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
write_setup_error_report(
codex_home.as_path(),
&SetupErrorReport {
code: super::SetupErrorCode::HelperFirewallPolicyAccessFailed,
message: "firewall policy unavailable".to_string(),
},
)
.expect("write setup error report");
let err = super::report_helper_failure(
codex_home.as_path(),
/*cleared_report*/ true,
/*exit_code*/ Some(1),
);
let failure = extract_failure(&err).expect("structured setup failure");
assert_eq!(
&super::SetupFailure::new(
super::SetupErrorCode::HelperFirewallPolicyAccessFailed,
"firewall policy unavailable",
),
failure
);
}
#[test]
fn report_helper_failure_ignores_setup_error_report_when_clear_failed() {
let tmp = TempDir::new().expect("tempdir");
let codex_home = tmp.path().join("codex-home");
write_setup_error_report(
codex_home.as_path(),
&SetupErrorReport {
code: super::SetupErrorCode::HelperFirewallPolicyAccessFailed,
message: "stale report".to_string(),
},
)
.expect("write setup error report");
let err = super::report_helper_failure(
codex_home.as_path(),
/*cleared_report*/ false,
/*exit_code*/ Some(1),
);
let failure = extract_failure(&err).expect("structured setup failure");
assert_eq!(
&super::SetupFailure::new(
super::SetupErrorCode::OrchestratorHelperExitNonzero,
"setup helper exited with status Some(1)",
),
failure
);
}
#[test] #[test]
fn setup_refresh_skips_profiles_without_managed_filesystem_permissions() { fn setup_refresh_skips_profiles_without_managed_filesystem_permissions() {
let tmp = TempDir::new().expect("tempdir"); let tmp = TempDir::new().expect("tempdir");
@@ -117,7 +117,7 @@ pub struct SetupErrorReport {
pub message: String, pub message: String,
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
pub struct SetupFailure { pub struct SetupFailure {
pub code: SetupErrorCode, pub code: SetupErrorCode,
pub message: String, pub message: String,