Fix daemon socket isolation checks for private tmp mounts (#46125)

## Why

Private `/tmp` bind mounts can leave hidden entries in `/proc/self/mountinfo`, causing the Linux sandbox to reject layouts that safely isolate daemon sockets. The proc-mount preflight also needs the main sandbox's WSL masks when checking aliases.

## What changed

- Identify the opened socket directory's mount using `fdinfo`, with a `statx` fallback, and follow its mount ancestry to distinguish hidden paths from exposed aliases.
- Accept safe private and stacked mounts while rejecting covered mounts, exposed aliases, and nested mounts. Retain conservative checks when no mount ID is available.
- Preserve WSL interop and WSLg masks in the proc-mount preflight.
- Heap-allocate large lifecycle futures in the TUI approval-gated MCP tool test to reduce Windows test-thread stack usage.

## Testing

Add mount-layout regression cases and a namespace integration test that verifies daemon sockets remain inaccessible under private `/tmp`, unrelated sockets remain reachable, and an exposed alias prevents startup. Add a preflight test for preserving WSL masks.

GitOrigin-RevId: 289b4446ba7776a7dd57ef5ff09e87bda19b069c
This commit is contained in:
open-matt
2026-09-17 05:06:30 +00:00
committed by copyberry
parent c5d079470e
commit 1bd1bfa7ca
6 changed files with 386 additions and 73 deletions

View File

@@ -1,9 +1,13 @@
//! Reject host mount aliases that would bypass the privileged socket directory mask.
//! Mount roots describe filesystem identity; canonical paths alone miss bind mounts.
use rustix::fs::AtFlags;
use rustix::fs::StatxFlags;
use rustix::fs::statx;
use std::collections::BTreeSet;
use std::fs;
use std::io;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
@@ -13,10 +17,29 @@ pub(crate) fn reject_daemon_mount_aliases(
directory: &Path,
masked_root: Option<&Path>,
) -> io::Result<()> {
let device = fs::metadata(directory)?.dev();
let directory_file = fs::File::open(directory)?;
let device = directory_file.metadata()?.dev();
let mount_id = fs::read_to_string(format!("/proc/self/fdinfo/{}", directory_file.as_raw_fd()))
.ok()
.and_then(|fdinfo| {
fdinfo
.lines()
.find_map(|line| line.strip_prefix("mnt_id:"))
.and_then(|id| id.trim().parse::<u64>().ok())
})
.or_else(|| {
// Query the same open directory, using the ID shared with mountinfo.
// Older kernels may succeed without returning the requested field.
statx(&directory_file, "", AtFlags::EMPTY_PATH, StatxFlags::MNT_ID)
.ok()
.filter(|stat| stat.stx_mask & StatxFlags::MNT_ID.bits() != 0)
.map(|stat| stat.stx_mnt_id)
})
.map(|id| id.to_string());
check_mounts(
directory,
&format!("{}:{}", libc::major(device), libc::minor(device)),
mount_id.as_deref(),
&fs::read("/proc/self/mountinfo")?,
masked_root,
)
@@ -25,36 +48,78 @@ pub(crate) fn reject_daemon_mount_aliases(
fn check_mounts(
directory: &Path,
device: &str,
mount_id: Option<&str>,
mountinfo: &[u8],
masked_root: Option<&Path>,
) -> io::Result<()> {
let invalid = || io::Error::other("cannot establish app-server socket mount isolation");
let mut mounts = Vec::new();
let mut locations = BTreeSet::new();
for line in mountinfo
.split(|byte| *byte == b'\n')
.filter(|line| !line.is_empty())
{
let fields: Vec<_> = line.split(|byte| *byte == b' ').take(5).collect();
let [_, _, mount_device, root, destination] = fields.as_slice() else {
let [id, parent, mount_device, root, destination] = fields.as_slice() else {
return Err(invalid());
};
let root = mount_path(root)?;
let destination = mount_path(destination)?;
if *mount_device == device.as_bytes()
&& let Ok(relative) = directory.strip_prefix(&destination)
{
locations.insert(root.join(relative));
mounts.push((*id, *parent, *mount_device, root, destination));
}
let (location, containing_mount) = if let Some(mount_id) = mount_id {
// fdinfo/statx identifies the opened mount, which may have been covered
// by another mount before we read mountinfo.
let selected = mounts
.iter()
.find(|(id, ..)| *id == mount_id.as_bytes())
.ok_or_else(invalid)?;
let (_, _, mount_device, root, destination) = selected;
if *mount_device != device.as_bytes() {
return Err(invalid());
}
mounts.push((*mount_device, root, destination));
}
// Overmounts can leave hidden entries in mountinfo. Require every possible
// containing mount to agree instead of guessing which root is visible.
if locations.len() != 1 {
return Err(invalid());
}
let location = locations.into_iter().next().ok_or_else(invalid)?;
for (mount_device, root, destination) in &mounts {
let relative = directory.strip_prefix(destination).map_err(|_| invalid())?;
let mut current = Some(selected);
let mut visible_child: Option<&Path> = None;
let mut visited = BTreeSet::new();
while let Some((id, parent, _, _, destination)) = current {
if !visited.insert(id)
|| mounts.iter().any(|(child_id, child_parent, _, _, child)| {
child_id != id
&& child_parent == id
&& directory.starts_with(child)
&& !visible_child.is_some_and(|visible| child.starts_with(visible))
})
{
return Err(invalid());
}
if id == parent {
break;
}
// Follow the selected branch towards the namespace root. Sibling
// mounts below this branch are hidden; mounts above it cover it.
visible_child = Some(destination);
current = mounts.iter().find(|(id, ..)| id == parent);
}
(root.join(relative), Some((mount_id, destination)))
} else {
// Without a mount ID, require every possible containing mount to agree
// on the backing location, and do not assume any aliases are hidden.
let locations: BTreeSet<_> = mounts
.iter()
.filter(|(_, _, mount_device, ..)| *mount_device == device.as_bytes())
.filter_map(|(_, _, _, root, destination)| {
directory
.strip_prefix(destination)
.ok()
.map(|relative| root.join(relative))
})
.collect();
if locations.len() != 1 {
return Err(invalid());
}
(locations.into_iter().next().ok_or_else(invalid)?, None)
};
for (id, _, mount_device, root, destination) in &mounts {
// Nested mounts can introduce another filesystem (or an individual socket) under the mask.
let nested = destination != directory && destination.starts_with(directory);
let alias = if *mount_device == device.as_bytes() {
@@ -70,8 +135,16 @@ fn check_mounts(
};
if nested
|| alias.is_some_and(|path| {
// An ancestor's path beneath this mount is hidden by it. Keep
// checking other mounts, including aliases mounted beneath it.
let hidden = containing_mount.is_some_and(|(mount_id, containing_mount)| {
*id != mount_id.as_bytes()
&& containing_mount.starts_with(destination)
&& path.starts_with(containing_mount)
});
!path.starts_with(directory)
&& !masked_root.is_some_and(|root| path.starts_with(root))
&& !hidden
})
{
return Err(io::Error::new(

View File

@@ -3,8 +3,15 @@ use pretty_assertions::assert_eq;
use test_case::test_case;
// Most cases have no independently masked subtree.
fn check_mounts(directory: &Path, device: &str, mountinfo: &[u8]) -> io::Result<()> {
super::check_mounts(directory, device, mountinfo, /*masked_root*/ None)
fn check_mounts(
directory: &Path,
device: &str,
mount_id: Option<&str>,
mountinfo: &[u8],
) -> io::Result<()> {
super::check_mounts(
directory, device, mount_id, mountinfo, /*masked_root*/ None,
)
}
#[test_case("/tmp", "/host-tmp", false; "ancestor alias")]
@@ -18,37 +25,117 @@ fn check_mounts(directory: &Path, device: &str, mountinfo: &[u8]) -> io::Result<
fn rejects_only_mounts_that_compromise_the_directory(root: &str, destination: &str, allowed: bool) {
let mounts =
format!("1 0 0:1 / / rw - ext4 disk rw\n2 1 0:1 {root} {destination} rw - ext4 disk rw\n");
assert_eq!(
check_mounts(
Path::new("/tmp/codex-daemon-1000"),
"0:1",
mounts.as_bytes()
)
.is_ok(),
allowed
);
let visible_mount = if destination == "/tmp" { "2" } else { "1" };
for mount_id in [Some(visible_mount), None] {
assert_eq!(
check_mounts(
Path::new("/tmp/codex-daemon-1000"),
"0:1",
mount_id,
mounts.as_bytes()
)
.is_ok(),
allowed,
"mount_id: {mount_id:?}"
);
}
}
#[test]
fn rejects_alias_when_tmp_is_itself_a_bind_mount() {
let mounts = b"1 0 0:1 / / rw - ext4 disk rw\n2 1 0:1 /backing/tmp /tmp rw - ext4 disk rw\n";
assert!(check_mounts(Path::new("/tmp/codex-daemon-1000"), "0:1", mounts).is_err());
assert!(
check_mounts(
Path::new("/tmp/codex-daemon-1000"),
"0:1",
Some("2"),
mounts
)
.is_err()
);
// A hidden deeper mount must not override the actual /tmp backing location.
let hidden = [
mounts.as_slice(),
b"3 1 0:1 /tmp/codex-daemon-1000 /tmp/codex-daemon-1000 rw - ext4 disk rw\n",
]
.concat();
assert!(check_mounts(Path::new("/tmp/codex-daemon-1000"), "0:1", &hidden).is_err());
assert!(
check_mounts(
Path::new("/tmp/codex-daemon-1000"),
"0:1",
Some("2"),
&hidden
)
.is_err()
);
}
#[test]
fn accepts_private_tmp_filesystem_but_rejects_ambiguous_stacked_mounts() {
fn accepts_private_tmp_filesystem_and_resolves_stacked_mounts() {
let mounts = "1 0 0:1 / / rw - ext4 disk rw\n2 1 0:2 / /tmp rw - tmpfs tmpfs rw\n";
let directory = Path::new("/tmp/codex-daemon-1000");
assert!(check_mounts(directory, "0:2", mounts.as_bytes()).is_ok());
assert!(check_mounts(directory, "0:2", Some("2"), mounts.as_bytes()).is_ok());
assert!(check_mounts(directory, "0:2", /*mount_id*/ None, mounts.as_bytes()).is_ok());
// Missing or inconsistent precise IDs must not fall back to the otherwise
// acceptable conservative interpretation.
assert!(check_mounts(directory, "0:2", Some("missing"), mounts.as_bytes()).is_err());
assert!(check_mounts(directory, "0:2", Some("1"), mounts.as_bytes()).is_err());
assert!(check_mounts(directory, "0:3", /*mount_id*/ None, mounts.as_bytes()).is_err());
let stacked = format!("{mounts}3 2 0:2 /other /tmp rw - tmpfs tmpfs rw\n");
assert!(check_mounts(directory, "0:2", stacked.as_bytes()).is_err());
assert!(check_mounts(directory, "0:2", Some("3"), stacked.as_bytes()).is_ok());
assert!(check_mounts(directory, "0:2", /*mount_id*/ None, stacked.as_bytes()).is_err());
}
#[test]
fn rejects_open_mount_that_has_been_covered() {
let directory = Path::new("/tmp/codex-daemon-1000");
let mounts = "1 0 0:1 / / rw - ext4 disk rw\n\
2 1 0:1 /tmp/private-old/tmp /tmp rw - ext4 disk rw\n\
3 2 0:1 /tmp/private-new/tmp /tmp rw - ext4 disk rw\n";
assert!(check_mounts(directory, "0:1", Some("2"), mounts.as_bytes()).is_err());
assert!(check_mounts(directory, "0:1", Some("3"), mounts.as_bytes()).is_ok());
let exposed = format!(
"{mounts}4 1 0:1 /tmp/private-new/tmp/codex-daemon-1000 /outside rw - ext4 disk rw\n"
);
for mount_id in [Some("2"), Some("3"), None] {
assert!(check_mounts(directory, "0:1", mount_id, exposed.as_bytes()).is_err());
}
}
#[test]
fn rejects_mount_hidden_by_an_ancestor_overmount() {
let directory = Path::new("/tmp/private/codex-daemon-1000");
let mounts = "1 0 0:1 / / rw - ext4 disk rw\n\
2 1 0:2 / /tmp rw - tmpfs tmpfs rw\n\
3 2 0:3 / /tmp/private rw - tmpfs tmpfs rw\n";
assert!(check_mounts(directory, "0:3", Some("3"), mounts.as_bytes()).is_ok());
let covered = format!("{mounts}4 2 0:2 /other /tmp rw - tmpfs tmpfs rw\n");
assert!(check_mounts(directory, "0:3", Some("3"), covered.as_bytes()).is_err());
assert!(check_mounts(directory, "0:2", Some("4"), covered.as_bytes()).is_ok());
}
#[test_case("/tmp/systemd-private-service/tmp", "/"; "tmp on root filesystem")]
#[test_case("/systemd-private-service/tmp", "/tmp"; "tmp on separate filesystem")]
fn accepts_private_tmp_bind_but_rejects_exposed_aliases(root: &str, parent: &str) {
let directory = Path::new("/tmp/codex-daemon-1000");
let mounts =
format!("1 0 0:1 / {parent} rw - ext4 disk rw\n2 1 0:1 {root} /tmp rw - ext4 disk rw\n");
assert!(check_mounts(directory, "0:1", Some("2"), mounts.as_bytes()).is_ok());
// PrivateTmp remains ambiguous when neither fdinfo nor statx supplies an ID.
assert!(check_mounts(directory, "0:1", /*mount_id*/ None, mounts.as_bytes()).is_err());
// A real alias beneath /tmp is not hidden by the private /tmp mount.
for (alias_root, destination) in [
(root.to_owned(), "/tmp/exposed"),
(
format!("{root}/codex-daemon-1000/rpc.sock"),
"/tmp/alias.sock",
),
("/".to_owned(), "/host"),
] {
let exposed = format!("{mounts}3 2 0:1 {alias_root} {destination} rw - ext4 disk rw\n");
assert!(check_mounts(directory, "0:1", Some("2"), exposed.as_bytes()).is_err());
}
}
#[test]
@@ -56,8 +143,10 @@ fn masked_wslg_alias_does_not_allow_other_exposed_aliases() {
let mounts = "1 0 0:1 / / rw - ext4 disk rw\n2 1 0:1 / /mnt/wslg/distro rw - ext4 disk rw\n";
let directory = Path::new("/tmp/codex-daemon-1000");
let mask = Some(Path::new(crate::bwrap::WSLG_DISTRO_ROOT));
assert!(check_mounts(directory, "0:1", mounts.as_bytes()).is_err());
assert!(super::check_mounts(directory, "0:1", mounts.as_bytes(), mask).is_ok());
let exposed = format!("{mounts}3 1 0:1 /tmp /host-tmp rw - ext4 disk rw\n");
assert!(super::check_mounts(directory, "0:1", exposed.as_bytes(), mask).is_err());
for mount_id in [Some("1"), None] {
assert!(check_mounts(directory, "0:1", mount_id, mounts.as_bytes()).is_err());
assert!(super::check_mounts(directory, "0:1", mount_id, mounts.as_bytes(), mask).is_ok());
assert!(super::check_mounts(directory, "0:1", mount_id, exposed.as_bytes(), mask).is_err());
}
}

View File

@@ -422,7 +422,7 @@ fn run_bwrap_with_proc_fallback(
let command_cwd = command_cwd.unwrap_or(sandbox_policy_cwd);
if options.mount_proc
&& !preflight_proc_mount_support(options.network_mode)
&& !preflight_proc_mount_support(options)
.unwrap_or_else(|err| exit_with_bwrap_build_error(err))
{
// Keep the retry silent so sandbox-internal diagnostics do not leak into the
@@ -526,15 +526,13 @@ fn current_process_argv0() -> String {
}
}
fn preflight_proc_mount_support(network_mode: BwrapNetworkMode) -> CodexResult<bool> {
let preflight_argv = build_preflight_bwrap_argv(network_mode)?;
fn preflight_proc_mount_support(options: BwrapOptions) -> CodexResult<bool> {
let preflight_argv = build_preflight_bwrap_argv(options)?;
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
Ok(!is_proc_mount_failure(stderr.as_str()))
}
fn build_preflight_bwrap_argv(
network_mode: BwrapNetworkMode,
) -> CodexResult<crate::bwrap::BwrapArgs> {
fn build_preflight_bwrap_argv(options: BwrapOptions) -> CodexResult<crate::bwrap::BwrapArgs> {
let file_system_sandbox_policy =
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
@@ -551,8 +549,8 @@ fn build_preflight_bwrap_argv(
Path::new("/"),
BwrapOptions {
mount_proc: true,
network_mode,
..Default::default()
// The alias check must see the same WSL masks as the main sandbox.
..options
},
)
}

View File

@@ -326,16 +326,19 @@ fn managed_proxy_preflight_argv_unshares_network() {
NetworkSandboxPolicy::Enabled,
/*allow_network_for_proxy*/ true,
);
let argv = build_preflight_bwrap_argv(mode)
.expect("build preflight argv")
.args;
let argv = build_preflight_bwrap_argv(BwrapOptions {
network_mode: mode,
..Default::default()
})
.expect("build preflight argv")
.args;
assert!(argv.iter().any(|arg| arg == "--"));
assert!(argv.iter().any(|arg| arg == "--unshare-net"));
}
#[test]
fn proc_mount_preflight_does_not_bind_the_full_filesystem() {
let argv = build_preflight_bwrap_argv(BwrapNetworkMode::FullAccess)
let argv = build_preflight_bwrap_argv(BwrapOptions::default())
.expect("build preflight argv")
.args;
@@ -349,6 +352,34 @@ fn proc_mount_preflight_does_not_bind_the_full_filesystem() {
assert!(!argv.windows(3).any(|window| window == ["--bind", "/", "/"]));
}
#[test]
fn proc_mount_preflight_preserves_wsl_masks() {
let argv = build_preflight_bwrap_argv(BwrapOptions {
mask_wsl_interop: true,
mask_wslg_distro: true,
..Default::default()
})
.expect("build WSL preflight argv")
.args;
assert!(argv.windows(2).any(|window| window == ["--proc", "/proc"]));
assert!(
argv.windows(2)
.any(|window| window == ["--tmpfs", WSL_INTEROP_DIR])
);
assert!(argv.windows(6).any(|window| {
window
== [
"--perms",
"000",
"--tmpfs",
WSLG_DISTRO_ROOT,
"--remount-ro",
WSLG_DISTRO_ROOT,
]
}));
}
#[test]
fn cleanup_synthetic_mount_targets_removes_only_empty_mount_targets() {
let temp_dir = tempfile::TempDir::new().expect("tempdir");

View File

@@ -4,6 +4,129 @@ use std::os::fd::AsRawFd;
use std::os::unix::net::UnixListener;
use std::os::unix::net::UnixStream;
#[tokio::test]
async fn private_tmp_mount_preserves_daemon_socket_isolation() {
if should_skip_bwrap_tests().await {
return;
}
// The parent owns cleanup after the child's disposable mount namespace exits.
let private = tempfile::tempdir_in("/tmp").unwrap();
let test_executable = std::env::current_exe().unwrap();
// Keep both executables available after the original /tmp is hidden.
std::fs::copy(&test_executable, private.path().join("test")).unwrap();
std::fs::copy(codex_linux_sandbox_exe(), private.path().join("sandbox")).unwrap();
let (_, test_module) = module_path!().split_once("::").unwrap();
let fixture_test = format!("{test_module}::private_tmp_fixture");
// Mount setup needs capabilities, which bubblewrap only accepts as namespace root.
let output = tokio::process::Command::new("unshare")
.args([
"--user",
"--map-root-user",
"--mount",
"--propagation",
"private",
"--",
])
.arg(test_executable)
.args([
"--exact",
&fixture_test,
"--ignored",
"--nocapture",
"--test-threads=1",
])
.env("CODEX_TEST_PRIVATE_TMP", private.path())
.kill_on_drop(true)
.output()
.await
.unwrap();
if output.status.code() == Some(77)
|| String::from_utf8_lossy(&output.stderr).starts_with("unshare:")
{
eprintln!("skipping private tmp test: user/mount namespaces are unavailable");
return;
}
assert_eq!(output.status.code(), Some(0), "{output:?}");
assert!(String::from_utf8_lossy(&output.stdout).contains("private-tmp-validated"));
}
#[test]
#[ignore = "invoked inside a disposable mount namespace"]
fn private_tmp_fixture() {
let Some(private) = std::env::var_os("CODEX_TEST_PRIVATE_TMP") else {
return;
};
let mount = std::process::Command::new("mount")
.arg("--bind")
.arg(private)
.arg("/tmp")
.output()
.unwrap();
if !mount.status.success() {
std::process::exit(/*code*/ 77);
}
let root = codex_uds::prepare_shared_daemon_socket_directory().unwrap();
let endpoint = root.join("rpc.sock");
let _daemon = UnixListener::bind(&endpoint).unwrap();
let _other = UnixListener::bind("/tmp/other.sock").unwrap();
UnixStream::connect(&endpoint).expect("host can reach daemon");
let profile = PermissionProfile::workspace_write_with(
&[AbsolutePathBuf::from_absolute_path("/tmp").unwrap()],
NetworkSandboxPolicy::Enabled,
/*exclude_tmpdir_env_var*/ true,
/*exclude_slash_tmp*/ false,
);
let mut command = std::process::Command::new("/tmp/sandbox");
command
.args(["--sandbox-policy-cwd", "/tmp", "--permission-profile"])
.arg(serde_json::to_string(&profile).unwrap())
.arg("--")
.current_dir("/tmp");
let (_, test_module) = module_path!().split_once("::").unwrap();
let client_test = format!("{test_module}::private_tmp_client");
command
.args([
"/tmp/test",
"--exact",
&client_test,
"--ignored",
"--nocapture",
"--test-threads=1",
])
.env("CODEX_TEST_DAEMON_SOCKET", &endpoint);
let output = command.output().unwrap();
assert_eq!(output.status.code(), Some(0), "{output:?}");
assert!(String::from_utf8_lossy(&output.stdout).contains("private-tmp-isolated"));
// A real alias below the private mount must still prevent startup.
std::fs::create_dir("/tmp/alias").unwrap();
let mount = std::process::Command::new("mount")
.arg("--bind")
.arg(&root)
.arg("/tmp/alias")
.output()
.unwrap();
assert!(mount.status.success(), "{mount:?}");
UnixStream::connect("/tmp/alias/rpc.sock").expect("host can reach alias");
let output = command.output().unwrap();
assert!(!output.status.success(), "{output:?}");
assert!(String::from_utf8_lossy(&output.stderr).contains("unsupported host mount"));
assert!(!String::from_utf8_lossy(&output.stdout).contains("private-tmp-client-started"));
println!("private-tmp-validated");
}
#[test]
#[ignore = "invoked inside the private tmp sandbox"]
fn private_tmp_client() {
let Some(endpoint) = std::env::var_os("CODEX_TEST_DAEMON_SOCKET") else {
return;
};
println!("private-tmp-client-started");
assert!(UnixStream::connect(endpoint).is_err(), "daemon reachable");
UnixStream::connect("/tmp/other.sock").expect("unrelated socket reachable");
println!("private-tmp-isolated");
}
#[tokio::test]
async fn daemon_socket_bind_mount_alias_rejects_sandbox_startup() {
if should_skip_bwrap_tests().await {

View File

@@ -1091,11 +1091,12 @@ async fn local_daemon_registers_approval_gated_mcp_tools_for_both_start_paths()
codex_home.path().join("config.toml"),
"web_search = \"disabled\"\n",
)?;
let (mut app_server, mut requests, mut proxy) = start_recording_app_server(
// Keep the large lifecycle futures off the Windows test thread's stack.
let (mut app_server, mut requests, mut proxy) = Box::pin(start_recording_app_server(
&app.config,
/*blocked_thread_list*/ None,
/*failed_thread_name*/ None,
)
))
.await?;
app_server
.start_dynamic_tool_mcp(
@@ -1190,14 +1191,13 @@ async fn local_daemon_registers_approval_gated_mcp_tools_for_both_start_paths()
ThreadHistoryMode::Legacy,
"Approved task source",
)?;
app_server
.resume_thread(
&app.local_settings,
app.config.clone(),
delegation_source,
crate::app_server_session::ResumeModelSettings::RestoreFromThread,
)
.await?;
Box::pin(app_server.resume_thread(
&app.local_settings,
app.config.clone(),
delegation_source,
crate::app_server_session::ResumeModelSettings::RestoreFromThread,
))
.await?;
let resumed = recorded_params(&requests, "thread/resume")
.pop()
.expect("resumed task request");
@@ -1205,14 +1205,13 @@ async fn local_daemon_registers_approval_gated_mcp_tools_for_both_start_paths()
resumed["config"]["mcp_servers.codex_tui"],
starts[0]["config"]["mcp_servers.codex_tui"]
);
app_server
.resume_thread(
&app.local_settings,
app.config.clone(),
delegation_source,
crate::app_server_session::ResumeModelSettings::PreserveExistingThread,
)
.await?;
Box::pin(app_server.resume_thread(
&app.local_settings,
app.config.clone(),
delegation_source,
crate::app_server_session::ResumeModelSettings::PreserveExistingThread,
))
.await?;
let reattached = recorded_params(&requests, "thread/resume")
.pop()
.expect("reattached task request");
@@ -1220,8 +1219,7 @@ async fn local_daemon_registers_approval_gated_mcp_tools_for_both_start_paths()
reattached["config"]["mcp_servers.codex_tui"],
starts[0]["config"]["mcp_servers.codex_tui"]
);
app_server
.fork_thread(&app.local_settings, app.config.clone(), delegation_source)
Box::pin(app_server.fork_thread(&app.local_settings, app.config.clone(), delegation_source))
.await?;
let forked = recorded_params(&requests, "thread/fork")
.pop()
@@ -1272,12 +1270,13 @@ async fn local_daemon_registers_approval_gated_mcp_tools_for_both_start_paths()
paused.contains("TUI is reconnecting; tool was not sent"),
"{paused}"
);
let (replacement, replacement_requests, replacement_proxy) = start_recording_app_server(
&app.config,
/*blocked_thread_list*/ None,
/*failed_thread_name*/ None,
)
.await?;
let (replacement, replacement_requests, replacement_proxy) =
Box::pin(start_recording_app_server(
&app.config,
/*blocked_thread_list*/ None,
/*failed_thread_name*/ None,
))
.await?;
let (new_tx, new_rx) = mpsc::unbounded_channel();
let new_sender = AppEventSender::new(new_tx);
drop(events);