permissions: make runtime config profile-backed

This commit is contained in:
Michael Bolin
2026-04-26 03:04:00 -07:00
parent 9881dc7306
commit 0d7e33845b
83 changed files with 1690 additions and 705 deletions
@@ -359,6 +359,7 @@ use codex_rmcp_client::perform_oauth_login_return_url;
use codex_rollout::state_db::StateDbHandle;
use codex_rollout::state_db::get_state_db;
use codex_rollout::state_db::reconcile_rollout;
use codex_sandboxing::compatibility_sandbox_policy_for_permission_profile;
use codex_state::StateRuntime;
use codex_state::ThreadMetadata;
use codex_state::ThreadMetadataBuilder;
@@ -2272,44 +2273,34 @@ impl CodexMessageProcessor {
arg0: None,
};
let (
effective_policy,
effective_file_system_sandbox_policy,
effective_network_sandbox_policy,
) = if let Some(permission_profile) = permission_profile {
let effective_permission_profile = if let Some(permission_profile) = permission_profile {
let permission_profile =
codex_protocol::models::PermissionProfile::from(permission_profile);
let sandbox_policy = match permission_profile.to_legacy_sandbox_policy(&sandbox_cwd) {
Ok(sandbox_policy) => sandbox_policy,
Err(err) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("invalid permission profile: {err}"),
data: None,
};
self.outgoing.send_error(request, error).await;
return;
}
};
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
Self::preserve_configured_deny_read_restrictions(
&mut file_system_sandbox_policy,
&self.config.permissions.file_system_sandbox_policy,
);
let effective_permission_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
permission_profile.enforcement(),
&file_system_sandbox_policy,
network_sandbox_policy,
);
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
&effective_permission_profile,
&file_system_sandbox_policy,
network_sandbox_policy,
sandbox_cwd.as_path(),
);
match self
.config
.permissions
.sandbox_policy
.can_set(&sandbox_policy)
{
Ok(()) => {
let (mut file_system_sandbox_policy, network_sandbox_policy) =
permission_profile.to_runtime_permissions();
Self::preserve_configured_deny_read_restrictions(
&mut file_system_sandbox_policy,
&self.config.permissions.file_system_sandbox_policy,
);
(
sandbox_policy,
file_system_sandbox_policy,
network_sandbox_policy,
)
}
Ok(()) => effective_permission_profile,
Err(err) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
@@ -2327,7 +2318,13 @@ impl CodexMessageProcessor {
codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, &sandbox_cwd);
let network_sandbox_policy =
codex_protocol::permissions::NetworkSandboxPolicy::from(&policy);
(policy, file_system_sandbox_policy, network_sandbox_policy)
codex_protocol::models::PermissionProfile::from_runtime_permissions_with_enforcement(
codex_protocol::models::SandboxEnforcement::from_legacy_sandbox_policy(
&policy,
),
&file_system_sandbox_policy,
network_sandbox_policy,
)
}
Err(err) => {
let error = JSONRPCErrorError {
@@ -2340,11 +2337,7 @@ impl CodexMessageProcessor {
}
}
} else {
(
self.config.permissions.sandbox_policy.get().clone(),
self.config.permissions.file_system_sandbox_policy.clone(),
self.config.permissions.network_sandbox_policy,
)
self.config.permissions.permission_profile()
};
let codex_linux_sandbox_exe = self.arg0_paths.codex_linux_sandbox_exe.clone();
@@ -2363,9 +2356,7 @@ impl CodexMessageProcessor {
match codex_core::exec::build_exec_request(
exec_params,
&effective_policy,
&effective_file_system_sandbox_policy,
effective_network_sandbox_policy,
&effective_permission_profile,
&sandbox_cwd,
&codex_linux_sandbox_exe,
use_legacy_landlock,
@@ -10161,16 +10152,20 @@ fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path)
.permission_profile
.as_ref()
.is_some_and(|profile| {
profile
.to_legacy_sandbox_policy(cwd)
.is_ok_and(|sandbox_policy| {
matches!(
sandbox_policy,
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_protocol::protocol::SandboxPolicy::DangerFullAccess
| codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. }
)
})
let (file_system_sandbox_policy, network_sandbox_policy) =
profile.to_runtime_permissions();
let sandbox_policy = compatibility_sandbox_policy_for_permission_profile(
profile,
&file_system_sandbox_policy,
network_sandbox_policy,
cwd,
);
matches!(
sandbox_policy,
codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. }
| codex_protocol::protocol::SandboxPolicy::DangerFullAccess
| codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. }
)
})
}
@@ -10672,16 +10667,10 @@ mod tests {
#[test]
fn thread_response_permission_profile_preserves_enforcement() {
let full_access_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::DangerFullAccess,
);
let external_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::ExternalSandbox {
network_access: codex_protocol::protocol::NetworkAccess::Restricted,
},
);
let full_access_profile = codex_protocol::models::PermissionProfile::Disabled;
let external_profile = codex_protocol::models::PermissionProfile::External {
network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
};
assert_eq!(
thread_response_permission_profile(external_profile.clone()),
@@ -10696,17 +10685,20 @@ mod tests {
#[test]
fn requested_permissions_trust_project_uses_permission_profile_intent() {
let cwd = test_path_buf("/tmp/project").abs();
let full_access_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::DangerFullAccess,
);
let workspace_write_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_workspace_write_policy(),
);
let read_only_profile =
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&SandboxPolicy::new_read_only_policy(),
let full_access_profile = codex_protocol::models::PermissionProfile::Disabled;
let workspace_write_profile = codex_protocol::models::PermissionProfile::workspace_write();
let read_only_profile = codex_protocol::models::PermissionProfile::read_only();
let direct_write_profile =
codex_protocol::models::PermissionProfile::from_runtime_permissions(
&codex_protocol::permissions::FileSystemSandboxPolicy::restricted(vec![
FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: test_path_buf("/tmp/other").abs(),
},
access: FileSystemAccessMode::Write,
},
]),
codex_protocol::permissions::NetworkSandboxPolicy::Restricted,
);
assert!(requested_permissions_trust_project(
@@ -10723,6 +10715,13 @@ mod tests {
},
cwd.as_path()
));
assert!(requested_permissions_trust_project(
&ConfigOverrides {
permission_profile: Some(direct_write_profile),
..Default::default()
},
cwd.as_path()
));
assert!(!requested_permissions_trust_project(
&ConfigOverrides {
permission_profile: Some(read_only_profile),
@@ -10915,10 +10914,7 @@ mod tests {
approval_policy: codex_protocol::protocol::AskForApproval::OnRequest,
approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer::User,
sandbox_policy: codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
permission_profile:
codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy(
&codex_protocol::protocol::SandboxPolicy::DangerFullAccess,
),
permission_profile: codex_protocol::models::PermissionProfile::Disabled,
cwd,
ephemeral: false,
reasoning_effort: None,
+7 -17
View File
@@ -710,9 +710,7 @@ mod tests {
use std::collections::HashMap;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
#[cfg(not(target_os = "windows"))]
@@ -729,12 +727,10 @@ mod tests {
use crate::outgoing_message::OutgoingMessage;
fn windows_sandbox_exec_request() -> ExecRequest {
let sandbox_policy = SandboxPolicy::ReadOnly {
network_access: false,
};
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
ExecRequest::new(
vec!["cmd".to_string()],
AbsolutePathBuf::current_dir().expect("current dir"),
cwd,
HashMap::new(),
/*network*/ None,
ExecExpiration::DefaultTimeout,
@@ -742,9 +738,7 @@ mod tests {
SandboxType::WindowsRestrictedToken,
WindowsSandboxLevel::Disabled,
/*windows_sandbox_private_desktop*/ false,
sandbox_policy.clone(),
FileSystemSandboxPolicy::from(&sandbox_policy),
NetworkSandboxPolicy::from(&sandbox_policy),
PermissionProfile::read_only(),
/*arg0*/ None,
)
}
@@ -834,9 +828,7 @@ mod tests {
connection_id: ConnectionId(8),
request_id: codex_app_server_protocol::RequestId::Integer(100),
};
let sandbox_policy = SandboxPolicy::ReadOnly {
network_access: false,
};
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
manager
.start(StartCommandExecParams {
@@ -845,7 +837,7 @@ mod tests {
process_id: Some("proc-100".to_string()),
exec_request: ExecRequest::new(
vec!["sh".to_string(), "-lc".to_string(), "sleep 30".to_string()],
AbsolutePathBuf::current_dir().expect("current dir"),
cwd.clone(),
HashMap::new(),
/*network*/ None,
ExecExpiration::Cancellation(CancellationToken::new()),
@@ -853,9 +845,7 @@ mod tests {
SandboxType::None,
WindowsSandboxLevel::Disabled,
/*windows_sandbox_private_desktop*/ false,
sandbox_policy.clone(),
FileSystemSandboxPolicy::from(&sandbox_policy),
NetworkSandboxPolicy::from(&sandbox_policy),
PermissionProfile::read_only(),
/*arg0*/ None,
),
started_network_proxy: None,
+9 -5
View File
@@ -95,6 +95,8 @@ use tokio::time::timeout;
use tracing::Instrument;
const EXTERNAL_AUTH_REFRESH_TIMEOUT: Duration = Duration::from_secs(10);
const TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR: &str =
"CODEX_APP_SERVER_TEST_DISABLE_PLUGIN_STARTUP_TASKS";
#[derive(Clone)]
struct ExternalAuthRefreshBridge {
@@ -315,11 +317,13 @@ impl MessageProcessor {
feedback,
log_db,
});
// Keep plugin startup warmups aligned at app-server startup.
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
thread_manager
.plugins_manager()
.maybe_start_plugin_startup_tasks_for_config(&config, auth_manager.clone());
if std::env::var_os(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR).is_none() {
// Keep plugin startup warmups aligned at app-server startup.
// TODO(xl): Move into PluginManager once this no longer depends on config feature gating.
thread_manager
.plugins_manager()
.maybe_start_plugin_startup_tasks_for_config(&config, auth_manager.clone());
}
let config_api = ConfigApi::new(
config_manager,
thread_manager.clone(),
+1
View File
@@ -26,6 +26,7 @@ pub use core_test_support::test_tmp_path;
pub use core_test_support::test_tmp_path_buf;
pub use mcp_process::DEFAULT_CLIENT_NAME;
pub use mcp_process::McpProcess;
pub use mcp_process::TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR;
pub use mock_model_server::create_mock_responses_server_repeating_assistant;
pub use mock_model_server::create_mock_responses_server_sequence;
pub use mock_model_server::create_mock_responses_server_sequence_unchecked;
@@ -106,6 +106,8 @@ pub struct McpProcess {
}
pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
pub const TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR: &str =
"CODEX_APP_SERVER_TEST_DISABLE_PLUGIN_STARTUP_TASKS";
const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG";
impl McpProcess {
@@ -147,7 +149,8 @@ impl McpProcess {
cmd.stderr(Stdio::piped());
cmd.current_dir(codex_home);
cmd.env("CODEX_HOME", codex_home);
cmd.env("RUST_LOG", "info");
cmd.env("RUST_LOG", "warn");
cmd.env(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR, "1");
// Keep integration tests isolated from host managed configuration.
cmd.env(
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
+1
View File
@@ -503,6 +503,7 @@ async fn get_auth_status_returns_token_after_proactive_refresh_recovery() -> Res
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn login_api_key_rejected_when_forced_chatgpt() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml_forced_login(codex_home.path(), "chatgpt")?;
@@ -92,6 +92,7 @@ async fn get_conversation_summary_by_thread_id_reads_rollout() -> Result<()> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn get_conversation_summary_by_rollout_path_rejects_remote_thread_store() -> Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
@@ -317,6 +317,7 @@ connectors = false
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn list_apps_reports_is_enabled_from_config() -> Result<()> {
let connectors = vec![AppInfo {
id: "beta".to_string(),
@@ -1269,6 +1270,7 @@ async fn list_apps_force_refetch_patches_updates_from_cached_snapshots() -> Resu
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn experimental_feature_enablement_set_refreshes_apps_list_when_apps_turn_on() -> Result<()> {
let initial_connectors = vec![AppInfo {
id: "alpha".to_string(),
@@ -252,6 +252,7 @@ async fn thread_compact_start_triggers_compaction_and_returns_empty_response() -
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn thread_compact_start_rejects_invalid_thread_id() -> Result<()> {
skip_if_no_network!(Ok(()));
@@ -572,6 +572,7 @@ model = "gpt-old"
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn config_read_after_pipelined_write_sees_written_value() -> Result<()> {
let temp_dir = TempDir::new()?;
let codex_home = temp_dir.path().canonicalize()?;
+11 -1
View File
@@ -33,6 +33,7 @@ use std::process::Command;
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
#[cfg(not(any(target_os = "macos", windows)))]
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
const OPTIONAL_FS_CHANGE_TIMEOUT: Duration = Duration::from_secs(2);
async fn initialized_mcp(codex_home: &TempDir) -> Result<McpProcess> {
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -722,6 +723,7 @@ async fn fs_watch_directory_reports_changed_child_paths_and_unwatch_stops_notifi
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn fs_watch_file_reports_atomic_replace_events() -> Result<()> {
let codex_home = TempDir::new()?;
let git_dir = codex_home.path().join("repo").join(".git");
@@ -832,7 +834,7 @@ async fn maybe_fs_changed_notification(
mcp: &mut McpProcess,
) -> Result<Option<FsChangedNotification>> {
match timeout(
DEFAULT_READ_TIMEOUT,
OPTIONAL_FS_CHANGE_TIMEOUT,
mcp.read_stream_until_notification_message("fs/changed"),
)
.await
@@ -845,6 +847,14 @@ async fn maybe_fs_changed_notification(
fn replace_file_atomically(path: &PathBuf, contents: &str) -> Result<()> {
let temp_path = path.with_extension("lock");
std::fs::write(&temp_path, contents)?;
#[cfg(windows)]
match std::fs::remove_file(path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
std::fs::rename(temp_path, path)?;
Ok(())
}
@@ -17,6 +17,9 @@ use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
#[cfg(windows)]
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(25);
#[cfg(not(windows))]
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces";
@@ -63,13 +66,14 @@ fn commit_marketplace_marker(root: &Path, marker: &str) -> Result<String> {
fn configured_git_marketplace_update<'a>(
source: &'a str,
last_revision: Option<&'a str>,
ref_name: Option<&'a str>,
) -> MarketplaceConfigUpdate<'a> {
MarketplaceConfigUpdate {
last_updated: "2026-04-13T00:00:00Z",
last_revision,
source_type: "git",
source,
ref_name: None,
ref_name,
sparse_paths: &[],
}
}
@@ -90,12 +94,13 @@ fn record_git_marketplace(
marketplace_name: &str,
source: &Path,
last_revision: &str,
ref_name: Option<&str>,
) -> Result<()> {
let source = source.display().to_string();
record_user_marketplace(
codex_home,
marketplace_name,
&configured_git_marketplace_update(&source, Some(last_revision)),
&configured_git_marketplace_update(&source, Some(last_revision), ref_name),
)?;
Ok(())
}
@@ -153,12 +158,14 @@ async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> {
"debug",
debug_source.path(),
&debug_old_revision,
Some(&debug_new_revision),
)?;
record_git_marketplace(
codex_home.path(),
"tools",
tools_source.path(),
&tools_old_revision,
Some(&tools_new_revision),
)?;
disable_plugin_startup_tasks(codex_home.path())?;
@@ -205,12 +212,14 @@ async fn marketplace_upgrade_named_marketplace_only() -> Result<()> {
"debug",
debug_source.path(),
&debug_old_revision,
/*ref_name*/ None,
)?;
record_git_marketplace(
codex_home.path(),
"tools",
tools_source.path(),
&tools_old_revision,
/*ref_name*/ None,
)?;
disable_plugin_startup_tasks(codex_home.path())?;
@@ -246,7 +255,13 @@ async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Re
let source = TempDir::new()?;
let old_revision = init_marketplace_repo(source.path(), "debug", "debug old")?;
commit_marketplace_marker(source.path(), "debug new")?;
record_git_marketplace(codex_home.path(), "debug", source.path(), &old_revision)?;
record_git_marketplace(
codex_home.path(),
"debug",
source.path(),
&old_revision,
/*ref_name*/ None,
)?;
disable_plugin_startup_tasks(codex_home.path())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -133,6 +133,7 @@ url = "{mcp_server_url}/mcp"
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn mcp_server_tool_call_returns_error_for_unknown_thread() -> Result<()> {
let codex_home = TempDir::new()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -278,6 +278,7 @@ async fn plugin_install_writes_remote_plugin_to_cloud_when_remote_plugin_enabled
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn plugin_install_rejects_invalid_remote_plugin_name() -> Result<()> {
let codex_home = TempDir::new()?;
write_remote_plugin_catalog_config(codex_home.path(), "https://example.invalid/backend-api/")?;
@@ -4,6 +4,7 @@ use anyhow::Result;
use anyhow::bail;
use app_test_support::ChatGptAuthFixture;
use app_test_support::McpProcess;
use app_test_support::TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCResponse;
@@ -1066,7 +1067,11 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
.join(STARTUP_REMOTE_PLUGIN_SYNC_MARKER_FILE);
{
let mut mcp = McpProcess::new(codex_home.path()).await?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR, None)],
)
.await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
wait_for_path_exists(&marker_path).await?;
@@ -1102,7 +1107,11 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
assert!(config.contains(r#"[plugins."linear@openai-curated"]"#));
{
let mut mcp = McpProcess::new(codex_home.path()).await?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR, None)],
)
.await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
}
@@ -1490,7 +1499,11 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() ->
.mount(&server)
.await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR, None)],
)
.await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
wait_for_featured_plugin_request_count(&server, /*expected_count*/ 1).await?;
@@ -334,6 +334,7 @@ async fn plugin_read_maps_missing_remote_plugin_to_invalid_request() -> Result<(
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn plugin_read_rejects_remote_marketplace_when_plugins_are_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
@@ -281,7 +281,7 @@ impl RealtimeE2eHarness {
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
mcp.initialize().await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
login_with_api_key(&mut mcp, "sk-test-key").await?;
let thread_start_request_id = mcp
@@ -345,10 +345,16 @@ impl RealtimeE2eHarness {
/// Returns the nth JSON message app-server wrote to the fake Realtime API
/// sideband websocket.
async fn sideband_outbound_request(&self, request_index: usize) -> Value {
self.realtime_server
.wait_for_request(/*connection_index*/ 0, request_index)
.await
.body_json()
timeout(
DEFAULT_TIMEOUT,
self.realtime_server
.wait_for_request(/*connection_index*/ 0, request_index),
)
.await
.unwrap_or_else(|_| {
panic!("timed out waiting for realtime sideband request {request_index}")
})
.body_json()
}
async fn append_audio(&mut self, thread_id: String) -> Result<()> {
@@ -534,7 +540,7 @@ async fn realtime_conversation_streams_v2_notifications() -> Result<()> {
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
mcp.initialize().await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
login_with_api_key(&mut mcp, "sk-test-key").await?;
let thread_start_request_id = mcp
@@ -783,7 +789,7 @@ async fn realtime_text_output_modality_requests_text_output_and_final_transcript
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
mcp.initialize().await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
login_with_api_key(&mut mcp, "sk-test-key").await?;
let thread_start_request_id = mcp
@@ -885,7 +891,7 @@ async fn realtime_list_voices_returns_supported_names() -> Result<()> {
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
mcp.initialize().await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_thread_realtime_list_voices_request(ThreadRealtimeListVoicesParams {})
@@ -957,7 +963,7 @@ async fn realtime_conversation_stop_emits_closed_notification() -> Result<()> {
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
mcp.initialize().await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
login_with_api_key(&mut mcp, "sk-test-key").await?;
let thread_start_request_id = mcp
@@ -1053,7 +1059,7 @@ async fn realtime_webrtc_start_emits_sdp_notification() -> Result<()> {
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
mcp.initialize().await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
login_with_api_key(&mut mcp, "sk-test-key").await?;
let thread_start_request_id = mcp
@@ -1541,6 +1547,7 @@ async fn webrtc_v2_text_input_is_append_only_when_response_is_cancelled() -> Res
/// output to realtime and then requests a new `response.create` so realtime can
/// react to that final output.
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn webrtc_v2_background_agent_tool_call_delegates_and_returns_function_output() -> Result<()>
{
skip_if_no_network!(Ok(()));
@@ -1968,7 +1975,7 @@ async fn realtime_webrtc_start_surfaces_backend_error() -> Result<()> {
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
mcp.initialize().await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
login_with_api_key(&mut mcp, "sk-test-key").await?;
// Phase 2: start a normal app-server thread and request realtime over WebRTC.
@@ -2029,7 +2036,7 @@ async fn realtime_conversation_requires_feature_flag() -> Result<()> {
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
mcp.initialize().await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let thread_start_request_id = mcp
.send_thread_start_request(ThreadStartParams::default())
@@ -1268,6 +1268,7 @@ async fn thread_list_default_sorts_by_created_at() -> Result<()> {
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn thread_list_sort_updated_at_orders_by_mtime() -> Result<()> {
let codex_home = TempDir::new()?;
create_minimal_config(codex_home.path())?;
@@ -63,6 +63,7 @@ async fn thread_memory_mode_set_updates_loaded_thread_state() -> Result<()> {
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn thread_memory_mode_set_updates_stored_thread_state() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
@@ -125,6 +125,7 @@ async fn thread_metadata_update_patches_git_branch_and_returns_updated_thread()
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn thread_metadata_update_rejects_empty_git_info_patch() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
@@ -761,6 +761,7 @@ async fn thread_resume_by_path_uses_remote_thread_store_error() -> Result<()> {
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn thread_resume_emits_restored_token_usage_before_next_turn() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
@@ -1793,6 +1794,7 @@ async fn thread_resume_rejects_mismatched_path_when_thread_is_running() -> Resul
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn thread_resume_rejoins_running_thread_even_with_override_mismatch() -> Result<()> {
let server = responses::start_mock_server().await;
let first_response = responses::sse_response(responses::sse(vec![
@@ -30,6 +30,7 @@ use tokio::time::timeout;
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn thread_unarchive_moves_rollout_back_into_sessions_directory() -> Result<()> {
let server = create_mock_responses_server_repeating_assistant("Done").await;
let codex_home = TempDir::new()?;
@@ -749,13 +749,17 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> {
#[tokio::test]
async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> Result<()> {
let codex_home = TempDir::new()?;
let unsupported_write_root = TempDir::new()?;
let disallowed_write_root = TempDir::new()?;
create_config_toml(
codex_home.path(),
"http://localhost/unused",
"never",
&BTreeMap::from([(Feature::Personality, true)]),
)?;
std::fs::write(
codex_home.path().join("managed_config.toml"),
"sandbox_mode = \"read-only\"\n",
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
@@ -772,7 +776,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let unsupported_write_root = AbsolutePathBuf::from_absolute_path(unsupported_write_root.path())
let disallowed_write_root = AbsolutePathBuf::from_absolute_path(disallowed_write_root.path())
.expect("tempdir path should be absolute");
let turn_req = mcp
@@ -787,7 +791,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
file_system: PermissionProfileFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: unsupported_write_root,
path: disallowed_write_root,
},
access: FileSystemAccessMode::Write,
}],
@@ -806,9 +810,9 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
assert_eq!(err.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(err.error.message.contains("invalid turn context override"));
assert!(
err.error
.message
.contains("filesystem writes outside the workspace root")
err.error.message.contains("allowed set [ReadOnly]"),
"unexpected error message: {}",
err.error.message
);
let turn_started = tokio::time::timeout(
std::time::Duration::from_millis(250),
@@ -2790,6 +2794,7 @@ async fn turn_start_emits_spawn_agent_item_with_model_metadata_v2() -> Result<()
}
#[tokio::test]
#[cfg_attr(windows, ignore = "temporary CI isolation for #19606 Windows hang")]
async fn turn_start_emits_spawn_agent_item_with_effective_role_model_metadata_v2() -> Result<()> {
skip_if_no_network!(Ok(()));