mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Preserve Windows sandbox identity during credential retry (#29624)
## Summary - recognize stale Windows sandbox credentials from both runner logon and child startup failures - refresh credentials once without changing the original command, permissions, file rules, desktop mode, or managed-network identity - add a Windows regression test that forces error 1312 and inspects the real retry arguments ## Why Elevated unified exec starts commands in two steps: ```text Codex -> sandbox command runner -> requested command ``` Either process start can fail when Windows invalidates the sandbox logon session. The child-side failure was previously returned as text, so the parent could not reliably recognize Windows error 1312. The existing retry also refreshed credentials with `proxy_enforced = false`, even when the original request used managed networking. That could change the selected Windows sandbox identity from offline to online during the retry. ## How - carry the failure stage and numeric Windows error code through the command-runner IPC protocol - preserve native `CreateProcessAsUserW` error codes instead of parsing error messages - keep every retry-sensitive field in one request and use it for both attempts - retry exactly once after refreshing credentials, then return the second failure - share the retry rule with the elevated capture path The Windows test injects error 1312 on both attempts and verifies: - two spawn attempts and one credential refresh - stale credentials are replaced by refreshed credentials - both attempts receive the same command, environment, cwd, permissions, roots, deny paths, TTY settings, and private-desktop mode - credential refresh receives the original `proxy_enforced` value ## Tests - `just test -p codex-windows-sandbox` - the new Windows-only regression test is included in the Windows nextest CI archive
This commit is contained in:
9 files changed
+488
-103
No files matched your search
@@ -14,6 +14,7 @@ mod cwd_junction;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use codex_windows_sandbox::ErrorPayload;
|
use codex_windows_sandbox::ErrorPayload;
|
||||||
|
use codex_windows_sandbox::ErrorStage;
|
||||||
use codex_windows_sandbox::ExitPayload;
|
use codex_windows_sandbox::ExitPayload;
|
||||||
use codex_windows_sandbox::FramedMessage;
|
use codex_windows_sandbox::FramedMessage;
|
||||||
use codex_windows_sandbox::IPC_PROTOCOL_VERSION;
|
use codex_windows_sandbox::IPC_PROTOCOL_VERSION;
|
||||||
@@ -166,13 +167,19 @@ fn open_pipe(name: &str, access: u32) -> Result<HANDLE> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send an error frame back to the parent process.
|
/// Send an error frame back to the parent process.
|
||||||
fn send_error(writer: &Arc<StdMutex<File>>, code: &str, message: String) -> Result<()> {
|
fn send_error(
|
||||||
|
writer: &Arc<StdMutex<File>>,
|
||||||
|
stage: ErrorStage,
|
||||||
|
windows_error_code: Option<u32>,
|
||||||
|
message: String,
|
||||||
|
) -> Result<()> {
|
||||||
let msg = FramedMessage {
|
let msg = FramedMessage {
|
||||||
version: IPC_PROTOCOL_VERSION,
|
version: IPC_PROTOCOL_VERSION,
|
||||||
message: Message::Error {
|
message: Message::Error {
|
||||||
payload: ErrorPayload {
|
payload: ErrorPayload {
|
||||||
message,
|
message,
|
||||||
code: code.to_string(),
|
stage,
|
||||||
|
windows_error_code,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -182,6 +189,15 @@ fn send_error(writer: &Arc<StdMutex<File>>, code: &str, message: String) -> Resu
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn windows_error_code(err: &anyhow::Error) -> Option<u32> {
|
||||||
|
err.chain().find_map(|cause| {
|
||||||
|
cause
|
||||||
|
.downcast_ref::<std::io::Error>()
|
||||||
|
.and_then(std::io::Error::raw_os_error)
|
||||||
|
.and_then(|code| u32::try_from(code).ok())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Read and validate the initial spawn request frame.
|
/// Read and validate the initial spawn request frame.
|
||||||
fn read_spawn_request(reader: &mut File) -> Result<SpawnRequest> {
|
fn read_spawn_request(reader: &mut File) -> Result<SpawnRequest> {
|
||||||
let Some(msg) = read_frame(reader)? else {
|
let Some(msg) = read_frame(reader)? else {
|
||||||
@@ -528,7 +544,12 @@ pub fn main() -> Result<()> {
|
|||||||
let req = match read_spawn_request(&mut pipe_read) {
|
let req = match read_spawn_request(&mut pipe_read) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let _ = send_error(&pipe_write, "spawn_failed", err.to_string());
|
let _ = send_error(
|
||||||
|
&pipe_write,
|
||||||
|
ErrorStage::ReadSpawnRequest,
|
||||||
|
/*windows_error_code*/ None,
|
||||||
|
err.to_string(),
|
||||||
|
);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -536,7 +557,12 @@ pub fn main() -> Result<()> {
|
|||||||
let ipc_spawn = match spawn_ipc_process(&req) {
|
let ipc_spawn = match spawn_ipc_process(&req) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let _ = send_error(&pipe_write, "spawn_failed", err.to_string());
|
let _ = send_error(
|
||||||
|
&pipe_write,
|
||||||
|
ErrorStage::SpawnChild,
|
||||||
|
windows_error_code(&err),
|
||||||
|
err.to_string(),
|
||||||
|
);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -570,7 +596,12 @@ pub fn main() -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
anyhow::bail!("runner spawn_ready write failed: pipe_write lock poisoned");
|
anyhow::bail!("runner spawn_ready write failed: pipe_write lock poisoned");
|
||||||
} {
|
} {
|
||||||
let _ = send_error(&pipe_write, "spawn_failed", err.to_string());
|
let _ = send_error(
|
||||||
|
&pipe_write,
|
||||||
|
ErrorStage::WriteSpawnReady,
|
||||||
|
/*windows_error_code*/ None,
|
||||||
|
err.to_string(),
|
||||||
|
);
|
||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
let log_dir_owned = log_dir.map(Path::to_path_buf);
|
let log_dir_owned = log_dir.map(Path::to_path_buf);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use crate::proc_thread_attr::ProcThreadAttributeList;
|
|||||||
use crate::winutil::format_last_error;
|
use crate::winutil::format_last_error;
|
||||||
use crate::winutil::quote_windows_arg;
|
use crate::winutil::quote_windows_arg;
|
||||||
use crate::winutil::to_wide;
|
use crate::winutil::to_wide;
|
||||||
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use codex_utils_pty::PsuedoCon;
|
use codex_utils_pty::PsuedoCon;
|
||||||
use codex_utils_pty::RawConPty;
|
use codex_utils_pty::RawConPty;
|
||||||
@@ -145,14 +146,15 @@ pub fn spawn_conpty_process_as_user(
|
|||||||
};
|
};
|
||||||
if ok == 0 {
|
if ok == 0 {
|
||||||
let err = unsafe { GetLastError() } as i32;
|
let err = unsafe { GetLastError() } as i32;
|
||||||
return Err(anyhow::anyhow!(
|
let message = format!(
|
||||||
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={}",
|
"CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={}",
|
||||||
err,
|
err,
|
||||||
format_last_error(err),
|
format_last_error(err),
|
||||||
cwd.display(),
|
cwd.display(),
|
||||||
cmdline_str,
|
cmdline_str,
|
||||||
env_block.len()
|
env_block.len()
|
||||||
));
|
);
|
||||||
|
return Err(std::io::Error::from_raw_os_error(err)).context(message);
|
||||||
}
|
}
|
||||||
Ok((pi, conpty))
|
Ok((pi, conpty))
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ use std::path::PathBuf;
|
|||||||
const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;
|
const MAX_FRAME_LEN: usize = 8 * 1024 * 1024;
|
||||||
|
|
||||||
/// Protocol version shared by the parent process and elevated command runner.
|
/// Protocol version shared by the parent process and elevated command runner.
|
||||||
pub const IPC_PROTOCOL_VERSION: u8 = 3;
|
pub const IPC_PROTOCOL_VERSION: u8 = 4;
|
||||||
|
|
||||||
/// Length-prefixed, JSON-encoded frame.
|
/// Length-prefixed, JSON-encoded frame.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
@@ -118,7 +118,17 @@ pub struct ExitPayload {
|
|||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct ErrorPayload {
|
pub struct ErrorPayload {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub code: String,
|
pub stage: ErrorStage,
|
||||||
|
pub windows_error_code: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runner startup stage that produced an error.
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ErrorStage {
|
||||||
|
ReadSpawnRequest,
|
||||||
|
SpawnChild,
|
||||||
|
WriteSpawnReady,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Empty payload for control messages.
|
/// Empty payload for control messages.
|
||||||
@@ -236,4 +246,32 @@ mod tests {
|
|||||||
assert_eq!(PermissionProfile::read_only(), payload.permission_profile);
|
assert_eq!(PermissionProfile::read_only(), payload.permission_profile);
|
||||||
assert_eq!(workspace_roots, payload.workspace_roots);
|
assert_eq!(workspace_roots, payload.workspace_roots);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_payload_serializes_stage_and_windows_error_code() {
|
||||||
|
let msg = FramedMessage {
|
||||||
|
version: IPC_PROTOCOL_VERSION,
|
||||||
|
message: Message::Error {
|
||||||
|
payload: ErrorPayload {
|
||||||
|
message: "CreateProcessAsUserW failed".to_string(),
|
||||||
|
stage: ErrorStage::SpawnChild,
|
||||||
|
windows_error_code: Some(1312),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let encoded = serde_json::to_value(&msg).expect("serialize");
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::json!({
|
||||||
|
"version": IPC_PROTOCOL_VERSION,
|
||||||
|
"type": "error",
|
||||||
|
"payload": {
|
||||||
|
"message": "CreateProcessAsUserW failed",
|
||||||
|
"stage": "spawn_child",
|
||||||
|
"windows_error_code": 1312,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
encoded
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
use crate::identity::SandboxCreds;
|
use crate::identity::SandboxCreds;
|
||||||
|
use crate::ipc_framed::ErrorPayload;
|
||||||
|
use crate::ipc_framed::ErrorStage;
|
||||||
use crate::ipc_framed::FramedMessage;
|
use crate::ipc_framed::FramedMessage;
|
||||||
use crate::ipc_framed::IPC_PROTOCOL_VERSION;
|
use crate::ipc_framed::IPC_PROTOCOL_VERSION;
|
||||||
use crate::ipc_framed::Message;
|
use crate::ipc_framed::Message;
|
||||||
@@ -29,6 +31,7 @@ use windows_sys::Win32::Foundation::CloseHandle;
|
|||||||
use windows_sys::Win32::Foundation::DUPLICATE_SAME_ACCESS;
|
use windows_sys::Win32::Foundation::DUPLICATE_SAME_ACCESS;
|
||||||
use windows_sys::Win32::Foundation::DuplicateHandle;
|
use windows_sys::Win32::Foundation::DuplicateHandle;
|
||||||
use windows_sys::Win32::Foundation::ERROR_LOGON_FAILURE;
|
use windows_sys::Win32::Foundation::ERROR_LOGON_FAILURE;
|
||||||
|
use windows_sys::Win32::Foundation::ERROR_NO_SUCH_LOGON_SESSION;
|
||||||
use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
|
use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
|
||||||
use windows_sys::Win32::Foundation::GetLastError;
|
use windows_sys::Win32::Foundation::GetLastError;
|
||||||
use windows_sys::Win32::Foundation::HANDLE;
|
use windows_sys::Win32::Foundation::HANDLE;
|
||||||
@@ -63,14 +66,69 @@ impl std::fmt::Display for RunnerLogonError {
|
|||||||
|
|
||||||
impl std::error::Error for RunnerLogonError {}
|
impl std::error::Error for RunnerLogonError {}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct RunnerStartupError {
|
||||||
|
payload: ErrorPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RunnerStartupError {
|
||||||
|
pub(crate) fn new(payload: ErrorPayload) -> Self {
|
||||||
|
Self { payload }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RunnerStartupError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"runner failed during {:?}: {}",
|
||||||
|
self.payload.stage, self.payload.message
|
||||||
|
)?;
|
||||||
|
if let Some(code) = self.payload.windows_error_code {
|
||||||
|
write!(f, " (Windows error {code})")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for RunnerStartupError {}
|
||||||
|
|
||||||
pub(crate) struct RunnerTransport {
|
pub(crate) struct RunnerTransport {
|
||||||
pipe_write: File,
|
pipe_write: File,
|
||||||
pipe_read: File,
|
pipe_read: File,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_stale_sandbox_creds_error(err: &anyhow::Error) -> bool {
|
fn is_refreshable_windows_error(code: u32) -> bool {
|
||||||
err.downcast_ref::<RunnerLogonError>()
|
matches!(code, ERROR_LOGON_FAILURE | ERROR_NO_SUCH_LOGON_SESSION)
|
||||||
.is_some_and(|err| err.code == ERROR_LOGON_FAILURE)
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_refreshable_sandbox_creds_error(err: &anyhow::Error) -> bool {
|
||||||
|
if err
|
||||||
|
.downcast_ref::<RunnerLogonError>()
|
||||||
|
.is_some_and(|err| is_refreshable_windows_error(err.code))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
err.downcast_ref::<RunnerStartupError>().is_some_and(|err| {
|
||||||
|
err.payload.stage == ErrorStage::SpawnChild
|
||||||
|
&& err
|
||||||
|
.payload
|
||||||
|
.windows_error_code
|
||||||
|
.is_some_and(is_refreshable_windows_error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn retry_runner_spawn_once<T>(
|
||||||
|
sandbox_creds: SandboxCreds,
|
||||||
|
mut spawn: impl FnMut(SandboxCreds) -> Result<T>,
|
||||||
|
refresh: impl FnOnce() -> Result<SandboxCreds>,
|
||||||
|
) -> Result<T> {
|
||||||
|
match spawn(sandbox_creds) {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(err) if is_refreshable_sandbox_creds_error(&err) => spawn(refresh()?),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RunnerTransport {
|
impl RunnerTransport {
|
||||||
@@ -90,7 +148,7 @@ impl RunnerTransport {
|
|||||||
.ok_or_else(|| anyhow::anyhow!("runner pipe closed before spawn_ready"))?;
|
.ok_or_else(|| anyhow::anyhow!("runner pipe closed before spawn_ready"))?;
|
||||||
match msg.message {
|
match msg.message {
|
||||||
Message::SpawnReady { .. } => Ok(()),
|
Message::SpawnReady { .. } => Ok(()),
|
||||||
Message::Error { payload } => Err(anyhow::anyhow!("runner error: {}", payload.message)),
|
Message::Error { payload } => Err(RunnerStartupError::new(payload).into()),
|
||||||
other => Err(anyhow::anyhow!(
|
other => Err(anyhow::anyhow!(
|
||||||
"expected spawn_ready from runner, got {other:?}"
|
"expected spawn_ready from runner, got {other:?}"
|
||||||
)),
|
)),
|
||||||
@@ -416,20 +474,46 @@ fn wait_for_complete_frame(pipe_read: &File, timeout: Duration) -> Result<()> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::RunnerLogonError;
|
use super::RunnerLogonError;
|
||||||
use super::is_stale_sandbox_creds_error;
|
use super::RunnerStartupError;
|
||||||
|
use super::is_refreshable_sandbox_creds_error;
|
||||||
|
use crate::ipc_framed::ErrorPayload;
|
||||||
|
use crate::ipc_framed::ErrorStage;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use windows_sys::Win32::Foundation::ERROR_LOGON_FAILURE;
|
use windows_sys::Win32::Foundation::ERROR_LOGON_FAILURE;
|
||||||
|
use windows_sys::Win32::Foundation::ERROR_NO_SUCH_LOGON_SESSION;
|
||||||
use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
|
use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_sandbox_creds_error_recognizes_logon_failures() {
|
fn refreshable_sandbox_creds_error_recognizes_credential_and_child_start_failures() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
[ERROR_LOGON_FAILURE, ERROR_NOT_FOUND].map(|code| {
|
[
|
||||||
|
ERROR_LOGON_FAILURE,
|
||||||
|
ERROR_NO_SUCH_LOGON_SESSION,
|
||||||
|
ERROR_NOT_FOUND,
|
||||||
|
]
|
||||||
|
.map(|code| {
|
||||||
let err =
|
let err =
|
||||||
anyhow::Error::new(RunnerLogonError { code }).context("runner launch failed");
|
anyhow::Error::new(RunnerLogonError { code }).context("runner launch failed");
|
||||||
is_stale_sandbox_creds_error(&err)
|
is_refreshable_sandbox_creds_error(&err)
|
||||||
}),
|
}),
|
||||||
[true, false]
|
[true, true, false]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
[
|
||||||
|
(ErrorStage::SpawnChild, ERROR_NO_SUCH_LOGON_SESSION),
|
||||||
|
(ErrorStage::SpawnChild, ERROR_NOT_FOUND),
|
||||||
|
(ErrorStage::ReadSpawnRequest, ERROR_NO_SUCH_LOGON_SESSION),
|
||||||
|
]
|
||||||
|
.map(|(stage, windows_error_code)| {
|
||||||
|
let err = anyhow::Error::new(RunnerStartupError::new(ErrorPayload {
|
||||||
|
message: "runner startup failed".to_string(),
|
||||||
|
stage,
|
||||||
|
windows_error_code: Some(windows_error_code),
|
||||||
|
}));
|
||||||
|
is_refreshable_sandbox_creds_error(&err)
|
||||||
|
}),
|
||||||
|
[true, false, false]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ mod windows_impl {
|
|||||||
use crate::logging::log_start;
|
use crate::logging::log_start;
|
||||||
use crate::logging::log_success;
|
use crate::logging::log_success;
|
||||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
use crate::runner_client::is_stale_sandbox_creds_error;
|
use crate::runner_client::retry_runner_spawn_once;
|
||||||
use crate::runner_client::spawn_runner_transport;
|
use crate::runner_client::spawn_runner_transport;
|
||||||
use crate::sandbox_utils::ensure_codex_home_exists;
|
use crate::sandbox_utils::ensure_codex_home_exists;
|
||||||
use crate::sandbox_utils::inject_git_safe_directory;
|
use crate::sandbox_utils::inject_git_safe_directory;
|
||||||
@@ -139,7 +139,7 @@ mod windows_impl {
|
|||||||
|
|
||||||
let logs_base_dir: Option<&Path> = Some(sandbox_base.as_path());
|
let logs_base_dir: Option<&Path> = Some(sandbox_base.as_path());
|
||||||
log_start(&command, logs_base_dir);
|
log_start(&command, logs_base_dir);
|
||||||
let mut sandbox_creds = require_logon_sandbox_creds(
|
let sandbox_creds = require_logon_sandbox_creds(
|
||||||
&permissions,
|
&permissions,
|
||||||
cwd,
|
cwd,
|
||||||
&env_map,
|
&env_map,
|
||||||
@@ -195,16 +195,19 @@ mod windows_impl {
|
|||||||
stdin_open: false,
|
stdin_open: false,
|
||||||
use_private_desktop,
|
use_private_desktop,
|
||||||
};
|
};
|
||||||
let transport = match spawn_runner_transport(
|
let transport = retry_runner_spawn_once(
|
||||||
codex_home,
|
sandbox_creds,
|
||||||
cwd,
|
|sandbox_creds| {
|
||||||
&sandbox_creds,
|
spawn_runner_transport(
|
||||||
logs_base_dir,
|
codex_home,
|
||||||
spawn_request.clone(),
|
cwd,
|
||||||
) {
|
&sandbox_creds,
|
||||||
Ok(transport) => transport,
|
logs_base_dir,
|
||||||
Err(err) if is_stale_sandbox_creds_error(&err) => {
|
spawn_request.clone(),
|
||||||
sandbox_creds = refresh_logon_sandbox_creds(
|
)
|
||||||
|
},
|
||||||
|
|| {
|
||||||
|
refresh_logon_sandbox_creds(
|
||||||
&permissions,
|
&permissions,
|
||||||
cwd,
|
cwd,
|
||||||
&env_map,
|
&env_map,
|
||||||
@@ -216,17 +219,9 @@ mod windows_impl {
|
|||||||
&deny_write_paths_override,
|
&deny_write_paths_override,
|
||||||
proxy_enforced,
|
proxy_enforced,
|
||||||
crate::WindowsSandboxProxySettingsMode::Reconcile,
|
crate::WindowsSandboxProxySettingsMode::Reconcile,
|
||||||
)?;
|
)
|
||||||
spawn_runner_transport(
|
},
|
||||||
codex_home,
|
)?;
|
||||||
cwd,
|
|
||||||
&sandbox_creds,
|
|
||||||
logs_base_dir,
|
|
||||||
spawn_request,
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
};
|
|
||||||
let (pipe_write, mut pipe_read) = transport.into_files();
|
let (pipe_write, mut pipe_read) = transport.into_files();
|
||||||
let cancel_writer = spawn_cancel_writer(&pipe_write, cancellation)?;
|
let cancel_writer = spawn_cancel_writer(&pipe_write, cancellation)?;
|
||||||
|
|
||||||
|
|||||||
@@ -195,6 +195,8 @@ pub use identity::sandbox_setup_is_complete;
|
|||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use ipc_framed::ErrorPayload;
|
pub use ipc_framed::ErrorPayload;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
pub use ipc_framed::ErrorStage;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
pub use ipc_framed::ExitPayload;
|
pub use ipc_framed::ExitPayload;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub use ipc_framed::FramedMessage;
|
pub use ipc_framed::FramedMessage;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::proc_thread_attr::ProcThreadAttributeList;
|
|||||||
use crate::winutil::argv_to_command_line;
|
use crate::winutil::argv_to_command_line;
|
||||||
use crate::winutil::format_last_error;
|
use crate::winutil::format_last_error;
|
||||||
use crate::winutil::to_wide;
|
use crate::winutil::to_wide;
|
||||||
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -146,7 +147,7 @@ pub unsafe fn create_process_as_user(
|
|||||||
creation_flags,
|
creation_flags,
|
||||||
);
|
);
|
||||||
logging::debug_log(&msg, logs_base_dir);
|
logging::debug_log(&msg, logs_base_dir);
|
||||||
return Err(anyhow!("CreateProcessAsUserW failed: {err}"));
|
return Err(std::io::Error::from_raw_os_error(err)).context(msg);
|
||||||
}
|
}
|
||||||
Ok(CreatedProcess {
|
Ok(CreatedProcess {
|
||||||
process_info: pi,
|
process_info: pi,
|
||||||
@@ -187,7 +188,7 @@ pub unsafe fn create_process_as_user(
|
|||||||
creation_flags,
|
creation_flags,
|
||||||
);
|
);
|
||||||
logging::debug_log(&msg, logs_base_dir);
|
logging::debug_log(&msg, logs_base_dir);
|
||||||
return Err(anyhow!("CreateProcessAsUserW failed: {err}"));
|
return Err(std::io::Error::from_raw_os_error(err)).context(msg);
|
||||||
}
|
}
|
||||||
Ok(CreatedProcess {
|
Ok(CreatedProcess {
|
||||||
process_info: pi,
|
process_info: pi,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::ipc_framed::Message;
|
|||||||
use crate::ipc_framed::SpawnRequest;
|
use crate::ipc_framed::SpawnRequest;
|
||||||
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
use crate::runner_client::RunnerTransport;
|
use crate::runner_client::RunnerTransport;
|
||||||
use crate::runner_client::is_stale_sandbox_creds_error;
|
use crate::runner_client::retry_runner_spawn_once;
|
||||||
use crate::runner_client::spawn_runner_transport;
|
use crate::runner_client::spawn_runner_transport;
|
||||||
use crate::spawn_prep::prepare_elevated_spawn_context_for_permissions;
|
use crate::spawn_prep::prepare_elevated_spawn_context_for_permissions;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -27,20 +27,79 @@ use tokio::sync::broadcast;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
async fn spawn_runner_transport_task(
|
struct RunnerTransportRequest {
|
||||||
|
permissions: ResolvedWindowsSandboxPermissions,
|
||||||
codex_home: PathBuf,
|
codex_home: PathBuf,
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
sandbox_creds: SandboxCreds,
|
env_map: HashMap<String, String>,
|
||||||
logs_base_dir: Option<PathBuf>,
|
logs_base_dir: Option<PathBuf>,
|
||||||
spawn_request: SpawnRequest,
|
spawn_request: SpawnRequest,
|
||||||
|
read_roots_override: Option<Vec<PathBuf>>,
|
||||||
|
read_roots_include_platform_defaults: bool,
|
||||||
|
write_roots_override: Option<Vec<PathBuf>>,
|
||||||
|
deny_read_paths_override: Vec<PathBuf>,
|
||||||
|
deny_write_paths_override: Vec<PathBuf>,
|
||||||
|
proxy_enforced: bool,
|
||||||
|
proxy_settings_mode: crate::WindowsSandboxProxySettingsMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_runner_transport_with_retry<T>(
|
||||||
|
sandbox_creds: SandboxCreds,
|
||||||
|
request: &RunnerTransportRequest,
|
||||||
|
mut spawn: impl FnMut(&Path, &Path, &SandboxCreds, Option<&Path>, SpawnRequest) -> Result<T>,
|
||||||
|
refresh: impl FnOnce(
|
||||||
|
&ResolvedWindowsSandboxPermissions,
|
||||||
|
&Path,
|
||||||
|
&HashMap<String, String>,
|
||||||
|
&Path,
|
||||||
|
Option<&[PathBuf]>,
|
||||||
|
bool,
|
||||||
|
Option<&[PathBuf]>,
|
||||||
|
&[PathBuf],
|
||||||
|
&[PathBuf],
|
||||||
|
bool,
|
||||||
|
crate::WindowsSandboxProxySettingsMode,
|
||||||
|
) -> Result<SandboxCreds>,
|
||||||
|
) -> Result<T> {
|
||||||
|
retry_runner_spawn_once(
|
||||||
|
sandbox_creds,
|
||||||
|
|sandbox_creds| {
|
||||||
|
spawn(
|
||||||
|
&request.codex_home,
|
||||||
|
&request.cwd,
|
||||||
|
&sandbox_creds,
|
||||||
|
request.logs_base_dir.as_deref(),
|
||||||
|
request.spawn_request.clone(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|| {
|
||||||
|
refresh(
|
||||||
|
&request.permissions,
|
||||||
|
&request.cwd,
|
||||||
|
&request.env_map,
|
||||||
|
&request.codex_home,
|
||||||
|
request.read_roots_override.as_deref(),
|
||||||
|
request.read_roots_include_platform_defaults,
|
||||||
|
request.write_roots_override.as_deref(),
|
||||||
|
&request.deny_read_paths_override,
|
||||||
|
&request.deny_write_paths_override,
|
||||||
|
request.proxy_enforced,
|
||||||
|
request.proxy_settings_mode,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_runner_transport_task(
|
||||||
|
sandbox_creds: SandboxCreds,
|
||||||
|
request: RunnerTransportRequest,
|
||||||
) -> Result<RunnerTransport> {
|
) -> Result<RunnerTransport> {
|
||||||
tokio::task::spawn_blocking(move || -> Result<_> {
|
tokio::task::spawn_blocking(move || -> Result<_> {
|
||||||
spawn_runner_transport(
|
spawn_runner_transport_with_retry(
|
||||||
&codex_home,
|
sandbox_creds,
|
||||||
&cwd,
|
&request,
|
||||||
&sandbox_creds,
|
spawn_runner_transport,
|
||||||
logs_base_dir.as_deref(),
|
refresh_logon_sandbox_creds,
|
||||||
spawn_request,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -95,59 +154,36 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
|
|||||||
proxy_settings_mode,
|
proxy_settings_mode,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let spawn_request = SpawnRequest {
|
|
||||||
command: command.clone(),
|
|
||||||
cwd: cwd.to_path_buf(),
|
|
||||||
env: env_map.clone(),
|
|
||||||
permission_profile: permission_profile.clone(),
|
|
||||||
workspace_roots: workspace_roots.to_vec(),
|
|
||||||
codex_home: elevated.sandbox_base.clone(),
|
|
||||||
real_codex_home: codex_home.to_path_buf(),
|
|
||||||
cap_sids: elevated.cap_sids.clone(),
|
|
||||||
timeout_ms,
|
|
||||||
tty,
|
|
||||||
stdin_open,
|
|
||||||
use_private_desktop,
|
|
||||||
};
|
|
||||||
let codex_home = codex_home.to_path_buf();
|
|
||||||
let cwd = cwd.to_path_buf();
|
|
||||||
let sandbox_creds = elevated.sandbox_creds;
|
let sandbox_creds = elevated.sandbox_creds;
|
||||||
let logs_base_dir = elevated.logs_base_dir.clone();
|
let request = RunnerTransportRequest {
|
||||||
let transport = match spawn_runner_transport_task(
|
permissions,
|
||||||
codex_home.clone(),
|
codex_home: codex_home.to_path_buf(),
|
||||||
cwd.clone(),
|
cwd: cwd.to_path_buf(),
|
||||||
sandbox_creds,
|
env_map: env_map.clone(),
|
||||||
logs_base_dir.clone(),
|
logs_base_dir: elevated.logs_base_dir,
|
||||||
spawn_request.clone(),
|
spawn_request: SpawnRequest {
|
||||||
)
|
command,
|
||||||
.await
|
cwd: cwd.to_path_buf(),
|
||||||
{
|
env: env_map,
|
||||||
Ok(transport) => transport,
|
permission_profile: permission_profile.clone(),
|
||||||
Err(err) if is_stale_sandbox_creds_error(&err) => {
|
workspace_roots: workspace_roots.to_vec(),
|
||||||
let sandbox_creds = refresh_logon_sandbox_creds(
|
codex_home: elevated.sandbox_base,
|
||||||
&permissions,
|
real_codex_home: codex_home.to_path_buf(),
|
||||||
&cwd,
|
cap_sids: elevated.cap_sids,
|
||||||
&env_map,
|
timeout_ms,
|
||||||
&codex_home,
|
tty,
|
||||||
read_roots_override,
|
stdin_open,
|
||||||
read_roots_include_platform_defaults,
|
use_private_desktop,
|
||||||
write_roots_override,
|
},
|
||||||
&deny_read_paths_override,
|
read_roots_override: read_roots_override.map(<[PathBuf]>::to_vec),
|
||||||
&deny_write_paths_override,
|
read_roots_include_platform_defaults,
|
||||||
/*proxy_enforced*/ false,
|
write_roots_override: write_roots_override.map(<[PathBuf]>::to_vec),
|
||||||
proxy_settings_mode,
|
deny_read_paths_override,
|
||||||
)?;
|
deny_write_paths_override,
|
||||||
spawn_runner_transport_task(
|
proxy_enforced,
|
||||||
codex_home,
|
proxy_settings_mode,
|
||||||
cwd,
|
|
||||||
sandbox_creds,
|
|
||||||
logs_base_dir,
|
|
||||||
spawn_request,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
};
|
};
|
||||||
|
let transport = spawn_runner_transport_task(sandbox_creds, request).await?;
|
||||||
let (pipe_write, pipe_read) = transport.into_files();
|
let (pipe_write, pipe_read) = transport.into_files();
|
||||||
|
|
||||||
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
let (writer_tx, writer_rx) = mpsc::channel::<Vec<u8>>(128);
|
||||||
@@ -197,3 +233,7 @@ pub(crate) async fn spawn_windows_sandbox_session_elevated_for_permission_profil
|
|||||||
stdin_open,
|
stdin_open,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "elevated_tests.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
use super::RunnerTransportRequest;
|
||||||
|
use super::spawn_runner_transport_with_retry;
|
||||||
|
use crate::WindowsSandboxProxySettingsMode;
|
||||||
|
use crate::identity::SandboxCreds;
|
||||||
|
use crate::ipc_framed::ErrorPayload;
|
||||||
|
use crate::ipc_framed::ErrorStage;
|
||||||
|
use crate::ipc_framed::SpawnRequest;
|
||||||
|
use crate::resolved_permissions::ResolvedWindowsSandboxPermissions;
|
||||||
|
use crate::runner_client::RunnerStartupError;
|
||||||
|
use codex_protocol::models::PermissionProfile;
|
||||||
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::Value;
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use windows_sys::Win32::Foundation::ERROR_NO_SUCH_LOGON_SESSION;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct SpawnObservation {
|
||||||
|
codex_home: PathBuf,
|
||||||
|
cwd: PathBuf,
|
||||||
|
username: String,
|
||||||
|
password: String,
|
||||||
|
logs_base_dir: Option<PathBuf>,
|
||||||
|
spawn_request: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct RefreshObservation {
|
||||||
|
permissions: ResolvedWindowsSandboxPermissions,
|
||||||
|
cwd: PathBuf,
|
||||||
|
env_map: HashMap<String, String>,
|
||||||
|
codex_home: PathBuf,
|
||||||
|
read_roots_override: Option<Vec<PathBuf>>,
|
||||||
|
read_roots_include_platform_defaults: bool,
|
||||||
|
write_roots_override: Option<Vec<PathBuf>>,
|
||||||
|
deny_read_paths_override: Vec<PathBuf>,
|
||||||
|
deny_write_paths_override: Vec<PathBuf>,
|
||||||
|
proxy_enforced: bool,
|
||||||
|
proxy_settings_mode: WindowsSandboxProxySettingsMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retry_uses_original_unified_exec_request_and_stops_after_second_failure() {
|
||||||
|
let workspace_root = AbsolutePathBuf::from_absolute_path(PathBuf::from(r"C:\workspace"))
|
||||||
|
.expect("absolute workspace root");
|
||||||
|
let permission_profile = PermissionProfile::workspace_write();
|
||||||
|
let permissions =
|
||||||
|
ResolvedWindowsSandboxPermissions::try_from_permission_profile_for_workspace_roots(
|
||||||
|
&permission_profile,
|
||||||
|
std::slice::from_ref(&workspace_root),
|
||||||
|
)
|
||||||
|
.expect("resolved permissions");
|
||||||
|
let env_map = HashMap::from([
|
||||||
|
("Path".to_string(), r"C:\tools".to_string()),
|
||||||
|
(
|
||||||
|
"HTTPS_PROXY".to_string(),
|
||||||
|
"http://localhost:1234".to_string(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
let request = RunnerTransportRequest {
|
||||||
|
permissions: permissions.clone(),
|
||||||
|
codex_home: PathBuf::from(r"C:\Users\codex"),
|
||||||
|
cwd: PathBuf::from(r"C:\workspace"),
|
||||||
|
env_map: env_map.clone(),
|
||||||
|
logs_base_dir: Some(PathBuf::from(r"C:\Users\codex\.sandbox")),
|
||||||
|
spawn_request: SpawnRequest {
|
||||||
|
command: vec!["pwsh.exe".to_string(), "-NoProfile".to_string()],
|
||||||
|
cwd: PathBuf::from(r"C:\workspace"),
|
||||||
|
env: env_map.clone(),
|
||||||
|
permission_profile,
|
||||||
|
workspace_roots: vec![workspace_root],
|
||||||
|
codex_home: PathBuf::from(r"C:\Users\codex\.sandbox"),
|
||||||
|
real_codex_home: PathBuf::from(r"C:\Users\codex"),
|
||||||
|
cap_sids: vec!["S-1-15-3-1024-1".to_string()],
|
||||||
|
timeout_ms: Some(5_000),
|
||||||
|
tty: true,
|
||||||
|
stdin_open: true,
|
||||||
|
use_private_desktop: true,
|
||||||
|
},
|
||||||
|
read_roots_override: Some(vec![PathBuf::from(r"C:\workspace\read")]),
|
||||||
|
read_roots_include_platform_defaults: true,
|
||||||
|
write_roots_override: Some(vec![PathBuf::from(r"C:\workspace\write")]),
|
||||||
|
deny_read_paths_override: vec![PathBuf::from(r"C:\secrets")],
|
||||||
|
deny_write_paths_override: vec![PathBuf::from(r"C:\workspace\.codex")],
|
||||||
|
proxy_enforced: true,
|
||||||
|
proxy_settings_mode: WindowsSandboxProxySettingsMode::Preserve,
|
||||||
|
};
|
||||||
|
let expected_spawn_request =
|
||||||
|
serde_json::to_value(&request.spawn_request).expect("serialize spawn request");
|
||||||
|
let expected_refresh = RefreshObservation {
|
||||||
|
permissions,
|
||||||
|
cwd: request.cwd.clone(),
|
||||||
|
env_map,
|
||||||
|
codex_home: request.codex_home.clone(),
|
||||||
|
read_roots_override: request.read_roots_override.clone(),
|
||||||
|
read_roots_include_platform_defaults: true,
|
||||||
|
write_roots_override: request.write_roots_override.clone(),
|
||||||
|
deny_read_paths_override: request.deny_read_paths_override.clone(),
|
||||||
|
deny_write_paths_override: request.deny_write_paths_override.clone(),
|
||||||
|
proxy_enforced: true,
|
||||||
|
proxy_settings_mode: WindowsSandboxProxySettingsMode::Preserve,
|
||||||
|
};
|
||||||
|
let spawn_observations = RefCell::new(Vec::new());
|
||||||
|
let refresh_observations = RefCell::new(Vec::new());
|
||||||
|
let spawn_attempts = Cell::new(0);
|
||||||
|
|
||||||
|
let result = spawn_runner_transport_with_retry(
|
||||||
|
SandboxCreds {
|
||||||
|
username: "stale".to_string(),
|
||||||
|
password: "old".to_string(),
|
||||||
|
},
|
||||||
|
&request,
|
||||||
|
|codex_home, cwd, sandbox_creds, logs_base_dir, spawn_request| {
|
||||||
|
spawn_observations.borrow_mut().push(SpawnObservation {
|
||||||
|
codex_home: codex_home.to_path_buf(),
|
||||||
|
cwd: cwd.to_path_buf(),
|
||||||
|
username: sandbox_creds.username.clone(),
|
||||||
|
password: sandbox_creds.password.clone(),
|
||||||
|
logs_base_dir: logs_base_dir.map(Path::to_path_buf),
|
||||||
|
spawn_request: serde_json::to_value(spawn_request)
|
||||||
|
.expect("serialize spawn request"),
|
||||||
|
});
|
||||||
|
spawn_attempts.set(spawn_attempts.get() + 1);
|
||||||
|
Err::<(), _>(anyhow::Error::new(RunnerStartupError::new(ErrorPayload {
|
||||||
|
message: format!("spawn attempt {} failed", spawn_attempts.get()),
|
||||||
|
stage: ErrorStage::SpawnChild,
|
||||||
|
windows_error_code: Some(ERROR_NO_SUCH_LOGON_SESSION),
|
||||||
|
})))
|
||||||
|
},
|
||||||
|
|permissions,
|
||||||
|
cwd,
|
||||||
|
env_map,
|
||||||
|
codex_home,
|
||||||
|
read_roots_override,
|
||||||
|
read_roots_include_platform_defaults,
|
||||||
|
write_roots_override,
|
||||||
|
deny_read_paths_override,
|
||||||
|
deny_write_paths_override,
|
||||||
|
proxy_enforced,
|
||||||
|
proxy_settings_mode| {
|
||||||
|
refresh_observations.borrow_mut().push(RefreshObservation {
|
||||||
|
permissions: permissions.clone(),
|
||||||
|
cwd: cwd.to_path_buf(),
|
||||||
|
env_map: env_map.clone(),
|
||||||
|
codex_home: codex_home.to_path_buf(),
|
||||||
|
read_roots_override: read_roots_override.map(<[PathBuf]>::to_vec),
|
||||||
|
read_roots_include_platform_defaults,
|
||||||
|
write_roots_override: write_roots_override.map(<[PathBuf]>::to_vec),
|
||||||
|
deny_read_paths_override: deny_read_paths_override.to_vec(),
|
||||||
|
deny_write_paths_override: deny_write_paths_override.to_vec(),
|
||||||
|
proxy_enforced,
|
||||||
|
proxy_settings_mode,
|
||||||
|
});
|
||||||
|
Ok(SandboxCreds {
|
||||||
|
username: "refreshed".to_string(),
|
||||||
|
password: "new".to_string(),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = result.expect_err("second spawn should fail");
|
||||||
|
assert_eq!(
|
||||||
|
"runner failed during SpawnChild: spawn attempt 2 failed (Windows error 1312)",
|
||||||
|
err.to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
vec![
|
||||||
|
SpawnObservation {
|
||||||
|
codex_home: request.codex_home.clone(),
|
||||||
|
cwd: request.cwd.clone(),
|
||||||
|
username: "stale".to_string(),
|
||||||
|
password: "old".to_string(),
|
||||||
|
logs_base_dir: request.logs_base_dir.clone(),
|
||||||
|
spawn_request: expected_spawn_request.clone(),
|
||||||
|
},
|
||||||
|
SpawnObservation {
|
||||||
|
codex_home: request.codex_home.clone(),
|
||||||
|
cwd: request.cwd.clone(),
|
||||||
|
username: "refreshed".to_string(),
|
||||||
|
password: "new".to_string(),
|
||||||
|
logs_base_dir: request.logs_base_dir,
|
||||||
|
spawn_request: expected_spawn_request,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
*spawn_observations.borrow()
|
||||||
|
);
|
||||||
|
assert_eq!(vec![expected_refresh], *refresh_observations.borrow());
|
||||||
|
}
|
||||||
Reference in new issue
Block a user