mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Track Windows sandbox policy and per-thread executor hosts in the TUI (#45813)
## What changed - Add `WindowsSandboxConfig` helpers to read app-server configuration and managed requirements, preserve legacy feature flag fallbacks, and select an allowed setup mode with elevated mode preferred when the configured mode is disallowed. - Store the sandbox host classification in thread session state using reported environments across start, resume, fork, and replay paths. Classify missing or empty environments as `Unknown`, and distinguish local, remote, and mixed selections. ## Testing Add coverage for managed setup restrictions, legacy configuration precedence, unknown policy state, environment classification, and remote host state in inactive-thread replay. GitOrigin-RevId: ae8920d9c2a1c80b641ac90617ae607b171bd55a
This commit is contained in:
@@ -1775,6 +1775,7 @@ enabled = false
|
||||
|
||||
app.chat_widget
|
||||
.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(crate) enum WindowsSandboxHost {
|
||||
Local,
|
||||
Mixed,
|
||||
Remote,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -4403,6 +4403,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
||||
let primary_cwd = test_path_buf("/tmp/main").abs();
|
||||
let shared_root = test_path_buf("/tmp/shared").abs();
|
||||
let primary_session = ThreadSessionState {
|
||||
windows_sandbox_host: WindowsSandboxHost::Local,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::workspace_write(),
|
||||
runtime_workspace_roots: vec![primary_cwd.clone(), shared_root.clone()],
|
||||
@@ -4427,7 +4428,11 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
||||
ServerNotification::ThreadStarted(ThreadStartedNotification {
|
||||
thread: Thread {
|
||||
originator: None,
|
||||
environments: None,
|
||||
environments: Some(vec![codex_app_server_protocol::ThreadEnvironment {
|
||||
environment_id: "remote".to_string(),
|
||||
cwd: test_path_buf("/tmp/agent").abs().into(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
}]),
|
||||
id: agent_thread_id.to_string(),
|
||||
extra: None,
|
||||
session_id: agent_thread_id.to_string(),
|
||||
@@ -4474,6 +4479,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re
|
||||
drop(store);
|
||||
|
||||
assert_eq!(session.thread_id, agent_thread_id);
|
||||
assert_eq!(session.windows_sandbox_host, WindowsSandboxHost::Remote);
|
||||
assert_eq!(session.thread_name, Some("agent thread".to_string()));
|
||||
assert_eq!(session.model, "gpt-agent");
|
||||
assert_eq!(session.model_provider_id, "agent-provider");
|
||||
@@ -5083,6 +5089,7 @@ async fn side_thread_snapshot_hides_forked_parent_transcript() {
|
||||
let side_thread_id = ThreadId::new();
|
||||
let mut store = ThreadEventStore::new(/*capacity*/ 4);
|
||||
let session = ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
fork_parent_title: None,
|
||||
..test_thread_session(side_thread_id, test_path_buf("/tmp/side"))
|
||||
@@ -5150,6 +5157,7 @@ async fn side_thread_snapshot_skips_session_header_preamble() {
|
||||
let snapshot = ThreadEventSnapshot {
|
||||
delegated_turns: Vec::new(),
|
||||
session: Some(ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
forked_from_id: Some(parent_thread_id),
|
||||
fork_parent_title: None,
|
||||
..test_thread_session(side_thread_id, test_path_buf("/tmp/side"))
|
||||
@@ -5731,6 +5739,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
|
||||
};
|
||||
let make_header = |is_first| -> Arc<dyn HistoryCell> {
|
||||
let session = ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -6277,6 +6286,7 @@ async fn replace_goal_confirmation_snapshot() {
|
||||
|
||||
fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState {
|
||||
ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -7314,6 +7324,7 @@ async fn backtrack_selection_preserves_selected_prompt_and_requests_branch() {
|
||||
|
||||
let make_header = |is_first| {
|
||||
let session = ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -7386,6 +7397,7 @@ async fn backtrack_selection_preserves_selected_prompt_and_requests_branch() {
|
||||
let base_id = ThreadId::new();
|
||||
app.chat_widget
|
||||
.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: base_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -8545,6 +8557,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() {
|
||||
|
||||
let thread_id = ThreadId::new();
|
||||
let event = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -9279,6 +9292,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() {
|
||||
let thread_id = ThreadId::new();
|
||||
app.chat_widget
|
||||
.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -21,6 +21,7 @@ async fn fork_current_session_preserves_conversation_ultra() -> Result<()> {
|
||||
.expect("create source rollout"),
|
||||
)?;
|
||||
app.chat_widget.handle_thread_session(ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
model: "gpt-5.4".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::Ultra),
|
||||
..test_thread_session(source_thread_id, test_path_buf("/tmp/project"))
|
||||
@@ -53,11 +54,13 @@ async fn switching_from_ultra_thread_restores_configured_plan_effort() {
|
||||
app.chat_widget
|
||||
.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
|
||||
let ultra_session = ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
model: "gpt-5.4".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::Ultra),
|
||||
..test_thread_session(ThreadId::new(), test_path_buf("/tmp/ultra"))
|
||||
};
|
||||
let normal_session = ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
model: "gpt-5.4".to_string(),
|
||||
reasoning_effort: Some(ReasoningEffortConfig::Medium),
|
||||
..test_thread_session(ThreadId::new(), test_path_buf("/tmp/normal"))
|
||||
|
||||
@@ -1633,3 +1633,36 @@ async fn external_writer_startup_keeps_initial_prompt_as_draft() -> Result<()> {
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn windows_sandbox_host_from_observed_environments() {
|
||||
for (ids, expected) in [
|
||||
(None, WindowsSandboxHost::Unknown),
|
||||
(Some(vec![]), WindowsSandboxHost::Unknown),
|
||||
(
|
||||
Some(vec![codex_exec_server::LOCAL_ENVIRONMENT_ID]),
|
||||
WindowsSandboxHost::Local,
|
||||
),
|
||||
(Some(vec!["remote"]), WindowsSandboxHost::Remote),
|
||||
(
|
||||
Some(vec!["remote", codex_exec_server::LOCAL_ENVIRONMENT_ID]),
|
||||
WindowsSandboxHost::Mixed,
|
||||
),
|
||||
] {
|
||||
let environments = ids.map(|ids| {
|
||||
ids.into_iter()
|
||||
.map(|id| codex_app_server_protocol::ThreadEnvironment {
|
||||
environment_id: id.to_string(),
|
||||
cwd: codex_utils_absolute_path::AbsolutePathBuf::try_from(std::env::temp_dir())
|
||||
.unwrap()
|
||||
.into(),
|
||||
runtime_workspace_roots: Vec::new(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(
|
||||
crate::windows_sandbox::host_from_environments(environments.as_deref()),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,6 +661,7 @@ mod tests {
|
||||
|
||||
fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState {
|
||||
ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -1366,6 +1366,9 @@ impl App {
|
||||
) -> Option<ThreadSessionState> {
|
||||
let mut session = self.primary_session_configured.clone()?;
|
||||
session.thread_id = thread_id;
|
||||
session.windows_sandbox_host = crate::windows_sandbox::host_from_environments(
|
||||
notification.thread.environments.as_deref(),
|
||||
);
|
||||
session.thread_name = notification.thread.name.clone();
|
||||
session.model_provider_id = notification.thread.model_provider.clone();
|
||||
session
|
||||
|
||||
@@ -87,6 +87,7 @@ impl App {
|
||||
session
|
||||
} else {
|
||||
ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -111,6 +112,8 @@ impl App {
|
||||
rollout_path: thread.path.clone(),
|
||||
}
|
||||
};
|
||||
session.windows_sandbox_host =
|
||||
crate::windows_sandbox::host_from_environments(thread.environments.as_deref());
|
||||
session.thread_id = thread_id;
|
||||
session.thread_name = thread.name.clone();
|
||||
session.model_provider_id = thread.model_provider.clone();
|
||||
@@ -169,6 +172,7 @@ mod tests {
|
||||
|
||||
fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState {
|
||||
ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -2301,6 +2301,7 @@ async fn thread_session_state_from_thread_start_response(
|
||||
);
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
crate::windows_sandbox::host_from_environments(response.thread.environments.as_deref()),
|
||||
response.thread.forked_from_id.clone(),
|
||||
response.thread.name.clone(),
|
||||
response.thread.path.clone(),
|
||||
@@ -2344,6 +2345,7 @@ async fn thread_session_state_from_thread_resume_response(
|
||||
};
|
||||
let mut session = thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
crate::windows_sandbox::host_from_environments(response.thread.environments.as_deref()),
|
||||
response.thread.forked_from_id.clone(),
|
||||
response.thread.name.clone(),
|
||||
response.thread.path.clone(),
|
||||
@@ -2380,6 +2382,7 @@ async fn thread_session_state_from_thread_fork_response(
|
||||
);
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
crate::windows_sandbox::host_from_environments(response.thread.environments.as_deref()),
|
||||
response.thread.forked_from_id.clone(),
|
||||
response.thread.name.clone(),
|
||||
response.thread.path.clone(),
|
||||
@@ -2431,6 +2434,7 @@ fn display_permission_profile_from_thread_response(
|
||||
)]
|
||||
async fn thread_session_state_from_thread_response(
|
||||
thread_id: &str,
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost,
|
||||
forked_from_id: Option<String>,
|
||||
thread_name: Option<String>,
|
||||
rollout_path: Option<PathBuf>,
|
||||
@@ -2461,6 +2465,7 @@ async fn thread_session_state_from_thread_response(
|
||||
);
|
||||
let (log_id, entry_count) = codex_message_history::history_metadata(&history_config).await;
|
||||
Ok(ThreadSessionState {
|
||||
windows_sandbox_host,
|
||||
thread_id,
|
||||
forked_from_id,
|
||||
fork_parent_title: None,
|
||||
@@ -4152,6 +4157,7 @@ mod tests {
|
||||
|
||||
let session = thread_session_state_from_thread_response(
|
||||
&thread_id.to_string(),
|
||||
crate::app::WindowsSandboxHost::Local,
|
||||
/*forked_from_id*/ None,
|
||||
Some("restore".to_string()),
|
||||
/*rollout_path*/ None,
|
||||
@@ -4188,6 +4194,7 @@ mod tests {
|
||||
|
||||
let session = thread_session_state_from_thread_response(
|
||||
&thread_id.to_string(),
|
||||
crate::app::WindowsSandboxHost::Local,
|
||||
Some(forked_from_id.to_string()),
|
||||
Some("restore".to_string()),
|
||||
/*rollout_path*/ None,
|
||||
|
||||
@@ -41,6 +41,7 @@ impl AppServerSession {
|
||||
.await?;
|
||||
let session = thread_session_state_from_thread_response(
|
||||
&thread.id,
|
||||
crate::windows_sandbox::host_from_environments(thread.environments.as_deref()),
|
||||
thread.forked_from_id.clone(),
|
||||
thread.name.clone(),
|
||||
thread.path.clone(),
|
||||
|
||||
@@ -44,6 +44,7 @@ fn thread_settings_for_test(
|
||||
|
||||
fn configured_thread_session(thread_id: ThreadId) -> crate::session_state::ThreadSessionState {
|
||||
crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -384,6 +384,7 @@ async fn submission_preserves_text_elements_and_local_images() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -493,6 +494,7 @@ async fn submission_includes_configured_active_permission_profile() {
|
||||
};
|
||||
let expected_active_permission_profile = ActivePermissionProfile::new("custom");
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -548,6 +550,7 @@ async fn submission_omits_active_permission_profile_for_legacy_snapshot() {
|
||||
file_system: ManagedFileSystemPermissions::Unrestricted,
|
||||
};
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -593,6 +596,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -693,6 +697,7 @@ async fn enter_with_only_remote_images_submits_user_turn() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -762,6 +767,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -803,6 +809,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -844,6 +851,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -888,6 +896,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -1299,6 +1299,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -19,6 +19,7 @@ async fn resumed_initial_messages_render_history() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -133,6 +134,7 @@ async fn restored_conversation_ultra_remains_selected_after_switching_to_plan()
|
||||
chat.set_plan_mode_reasoning_effort(Some(ReasoningEffortConfig::High));
|
||||
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -420,6 +422,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -492,6 +495,7 @@ async fn replayed_user_message_preserves_remote_image_urls() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -598,6 +602,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() {
|
||||
.expect("permission profile should project to legacy sandbox policy");
|
||||
let expected_sandbox = SandboxPolicy::from(expected_core_sandbox);
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -671,6 +676,7 @@ async fn session_configured_preserves_profile_workspace_roots() {
|
||||
let session_permission_profile = PermissionProfile::workspace_write()
|
||||
.materialize_project_roots_with_workspace_roots(&session_effective_workspace_roots);
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -718,6 +724,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
};
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -759,6 +766,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -819,6 +827,7 @@ async fn replayed_user_message_with_only_local_images_renders_history_cell() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -1116,6 +1125,7 @@ async fn replayed_reasoning_item_preserves_summary_parts_and_hides_raw_reasoning
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.show_raw_agent_reasoning = false;
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -1167,6 +1177,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.config.show_raw_agent_reasoning = true;
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -745,6 +745,7 @@ async fn required_windows_sandbox_setup_defers_configured_initial_prompt() {
|
||||
create_initial_user_message(Some(initial_prompt.clone()), Vec::new(), Vec::new());
|
||||
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -1168,6 +1169,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure
|
||||
.set_enabled(Feature::GuardianApproval, /*enabled*/ true);
|
||||
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -1216,6 +1218,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w
|
||||
let permission_profile = app_server_workspace_write_profile(extra_root);
|
||||
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -15,6 +15,7 @@ fn paste_hidden_plan_shell_payload(chat: &mut ChatWidget) -> String {
|
||||
|
||||
fn plan_test_session(thread_id: ThreadId) -> crate::session_state::ThreadSessionState {
|
||||
crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
@@ -1249,6 +1250,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() {
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
let configured = crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -4321,6 +4321,7 @@ async fn session_configured_clears_goal_status_footer() {
|
||||
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
chat.handle_thread_session(crate::session_state::ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -596,6 +596,7 @@ fn image_generation_call_renders_saved_path() {
|
||||
|
||||
fn session_configured_event(model: &str) -> ThreadSessionState {
|
||||
ThreadSessionState {
|
||||
windows_sandbox_host: crate::app::WindowsSandboxHost::Local,
|
||||
thread_id: ThreadId::new(),
|
||||
forked_from_id: None,
|
||||
fork_parent_title: None,
|
||||
|
||||
@@ -236,7 +236,6 @@ mod updates_cache;
|
||||
mod version;
|
||||
mod vim_search;
|
||||
mod width;
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
mod windows_sandbox;
|
||||
mod workspace_command;
|
||||
mod workspace_messages;
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(crate) struct MessageHistoryMetadata {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ThreadSessionState {
|
||||
pub(crate) windows_sandbox_host: crate::app::WindowsSandboxHost,
|
||||
pub(crate) thread_id: ThreadId,
|
||||
pub(crate) forked_from_id: Option<ThreadId>,
|
||||
pub(crate) fork_parent_title: Option<String>,
|
||||
|
||||
@@ -1,10 +1,138 @@
|
||||
//! Windows sandbox display state derived from configuration.
|
||||
//! Windows sandbox configuration, managed requirements, and executor selection for the TUI.
|
||||
|
||||
use crate::legacy_core::config::Config;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ConfigReadResponse;
|
||||
use codex_app_server_protocol::ConfigRequirementsReadResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::WindowsSandboxImplementation;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
||||
use codex_config::types::WindowsSandboxModeToml;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct WindowsSandboxConfig {
|
||||
pub(crate) mode: Option<WindowsSandboxSetupMode>,
|
||||
// None means policy has not been loaded; a loaded null list allows both modes.
|
||||
pub(crate) requirements: Option<Option<Vec<WindowsSandboxSetupMode>>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl WindowsSandboxConfig {
|
||||
pub(crate) fn from_responses(
|
||||
config: &ConfigReadResponse,
|
||||
requirements: ConfigRequirementsReadResponse,
|
||||
) -> Self {
|
||||
let mut state = Self {
|
||||
mode: config
|
||||
.config
|
||||
.additional
|
||||
.get("windows")
|
||||
.and_then(|windows| windows.get("sandbox"))
|
||||
.and_then(|mode| serde_json::from_value(mode.clone()).ok())
|
||||
.or_else(|| {
|
||||
let features = config.config.additional.get("features")?;
|
||||
[
|
||||
(
|
||||
"elevated_windows_sandbox",
|
||||
WindowsSandboxSetupMode::Elevated,
|
||||
),
|
||||
(
|
||||
"experimental_windows_sandbox",
|
||||
WindowsSandboxSetupMode::Unelevated,
|
||||
),
|
||||
(
|
||||
"enable_experimental_windows_sandbox",
|
||||
WindowsSandboxSetupMode::Unelevated,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|(key, mode)| {
|
||||
(features.get(key).and_then(serde_json::Value::as_bool) == Some(true))
|
||||
.then_some(mode)
|
||||
})
|
||||
}),
|
||||
requirements: Some(
|
||||
requirements
|
||||
.requirements
|
||||
.and_then(|requirements| requirements.allowed_windows_sandbox_implementations)
|
||||
.map(|allowed| {
|
||||
allowed
|
||||
.into_iter()
|
||||
.filter_map(|implementation| match implementation {
|
||||
WindowsSandboxImplementation::Elevated => {
|
||||
Some(WindowsSandboxSetupMode::Elevated)
|
||||
}
|
||||
WindowsSandboxImplementation::Unelevated => {
|
||||
Some(WindowsSandboxSetupMode::Unelevated)
|
||||
}
|
||||
WindowsSandboxImplementation::Mxc => None,
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
),
|
||||
};
|
||||
if let Some(Some(allowed)) = &state.requirements
|
||||
&& !state.mode.is_some_and(|mode| allowed.contains(&mode))
|
||||
{
|
||||
// Managed requirements prefer elevated when the configured value is disallowed.
|
||||
state.mode = [
|
||||
WindowsSandboxSetupMode::Elevated,
|
||||
WindowsSandboxSetupMode::Unelevated,
|
||||
]
|
||||
.into_iter()
|
||||
.find(|mode| allowed.contains(mode));
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
pub(crate) fn level(&self) -> WindowsSandboxLevel {
|
||||
match self.mode {
|
||||
Some(WindowsSandboxSetupMode::Elevated) => WindowsSandboxLevel::Elevated,
|
||||
Some(WindowsSandboxSetupMode::Unelevated) => WindowsSandboxLevel::RestrictedToken,
|
||||
None => WindowsSandboxLevel::Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn allows(&self, mode: WindowsSandboxSetupMode) -> bool {
|
||||
match &self.requirements {
|
||||
Some(Some(allowed)) => allowed.contains(&mode),
|
||||
Some(None) => true,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn requires_elevated(&self) -> bool {
|
||||
self.mode == Some(WindowsSandboxSetupMode::Elevated)
|
||||
&& matches!(self.requirements, Some(Some(_)))
|
||||
}
|
||||
|
||||
pub(crate) async fn read(
|
||||
handle: AppServerRequestHandle,
|
||||
cwd: String,
|
||||
) -> color_eyre::Result<Self> {
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
||||
let config = crate::config_update::read_effective_config(handle.clone(), cwd).await?;
|
||||
let requirements = handle
|
||||
.request_typed(ClientRequest::ConfigRequirementsRead {
|
||||
request_id: RequestId::String(format!(
|
||||
"tui-windows-sandbox-requirements-{}",
|
||||
Uuid::new_v4()
|
||||
)),
|
||||
params: None,
|
||||
})
|
||||
.await?;
|
||||
Ok(Self::from_responses(&config, requirements))
|
||||
})
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
|
||||
pub(crate) fn level_from_config(config: &Config) -> WindowsSandboxLevel {
|
||||
match config.permissions.windows_sandbox_mode {
|
||||
Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
|
||||
@@ -18,3 +146,29 @@ pub(crate) fn level_from_config(config: &Config) -> WindowsSandboxLevel {
|
||||
None => WindowsSandboxLevel::Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
/// A server-local connection can still select remote executors. Missing selections are unknown.
|
||||
pub(crate) fn host_from_environments(
|
||||
environments: Option<&[codex_app_server_protocol::ThreadEnvironment]>,
|
||||
) -> crate::app::WindowsSandboxHost {
|
||||
use crate::app::WindowsSandboxHost;
|
||||
let Some(environments) = environments.filter(|environments| !environments.is_empty()) else {
|
||||
return WindowsSandboxHost::Unknown;
|
||||
};
|
||||
let local = environments
|
||||
.iter()
|
||||
.any(|environment| environment.environment_id == codex_exec_server::LOCAL_ENVIRONMENT_ID);
|
||||
let remote = environments
|
||||
.iter()
|
||||
.any(|environment| environment.environment_id != codex_exec_server::LOCAL_ENVIRONMENT_ID);
|
||||
match (local, remote) {
|
||||
(true, false) => WindowsSandboxHost::Local,
|
||||
(true, true) => WindowsSandboxHost::Mixed,
|
||||
(false, true) => WindowsSandboxHost::Remote,
|
||||
(false, false) => WindowsSandboxHost::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "windows_sandbox_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use super::*;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode::Elevated;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode::Unelevated;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn windows_sandbox_server_policy_controls_setup_choices() {
|
||||
for (configured, allowed, mode, expected) in [
|
||||
(
|
||||
Some("unelevated"),
|
||||
serde_json::Value::Null,
|
||||
Some(Unelevated),
|
||||
(true, true, false),
|
||||
),
|
||||
(
|
||||
Some("unelevated"),
|
||||
json!(["elevated"]),
|
||||
Some(Elevated),
|
||||
(true, false, true),
|
||||
),
|
||||
(
|
||||
None,
|
||||
json!(["unelevated", "elevated"]),
|
||||
Some(Elevated),
|
||||
(true, true, true),
|
||||
),
|
||||
(
|
||||
None,
|
||||
json!(["unelevated"]),
|
||||
Some(Unelevated),
|
||||
(false, true, false),
|
||||
),
|
||||
(Some("unelevated"), json!([]), None, (false, false, false)),
|
||||
(
|
||||
Some("unelevated"),
|
||||
json!(["mxc"]),
|
||||
None,
|
||||
(false, false, false),
|
||||
),
|
||||
] {
|
||||
let config = serde_json::from_value(json!({
|
||||
"config": {"windows": {"sandbox": configured}}, "origins": {}
|
||||
}))
|
||||
.unwrap();
|
||||
let requirements = serde_json::from_value(json!({
|
||||
"requirements": {"allowedWindowsSandboxImplementations": allowed}
|
||||
}))
|
||||
.unwrap();
|
||||
let state = WindowsSandboxConfig::from_responses(&config, requirements);
|
||||
assert_eq!(state.mode, mode);
|
||||
assert_eq!(
|
||||
(
|
||||
state.allows(Elevated),
|
||||
state.allows(Unelevated),
|
||||
state.requires_elevated()
|
||||
),
|
||||
expected
|
||||
);
|
||||
}
|
||||
for (key, mode) in [
|
||||
("elevated_windows_sandbox", Elevated),
|
||||
("experimental_windows_sandbox", Unelevated),
|
||||
("enable_experimental_windows_sandbox", Unelevated),
|
||||
] {
|
||||
for (explicit, expected) in [(None, mode), (Some("elevated"), Elevated)] {
|
||||
let config = serde_json::from_value(json!({
|
||||
"config": {"windows": {"sandbox": explicit}, "features": {key: true}},
|
||||
"origins": {}
|
||||
}))
|
||||
.unwrap();
|
||||
let requirements = serde_json::from_value(json!({"requirements": null})).unwrap();
|
||||
assert_eq!(
|
||||
WindowsSandboxConfig::from_responses(&config, requirements).mode,
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
}
|
||||
let unknown = WindowsSandboxConfig::default();
|
||||
assert!(!unknown.allows(Elevated));
|
||||
assert!(!unknown.allows(Unelevated));
|
||||
}
|
||||
Reference in New Issue
Block a user