diff --git a/app/src/ai/agent/api/convert_from.rs b/app/src/ai/agent/api/convert_from.rs index b4b3b14997e..a1ea21d3d3c 100644 --- a/app/src/ai/agent/api/convert_from.rs +++ b/app/src/ai/agent/api/convert_from.rs @@ -92,6 +92,23 @@ pub(crate) fn convert_run_agents_harness(harness: Option<&api::Harness>) -> Opti ) } +/// Maps the wire `computer_use_enabled` bool onto the client's tri-state +/// flag. +/// +/// The generated Rust proto type has **no field presence** for this field +/// (the crate's codegen compiles the editions source as `proto3`), so an +/// unset flag and an explicit `false` both decode as `false`. Collapsing +/// that into an explicit `Some(false)` is what forced computer use off for +/// every orchestrated child whose `run_agents` call simply omitted the +/// field, bypassing the server's normal Oz default. Treat `false` as "no +/// opinion" (`None`) so the server resolves its documented default, and +/// only carry an explicit `Some(true)` through. Restoring an explicit +/// `false` end-to-end requires field presence on the wire; the rest of the +/// launch path already carries `Some(false)` faithfully once it does. +fn convert_run_agents_computer_use_enabled(computer_use_enabled: bool) -> Option { + computer_use_enabled.then_some(true) +} + fn convert_run_agents_execution_mode( execution_mode: Option, ) -> RunAgentsExecutionMode { @@ -100,7 +117,9 @@ fn convert_run_agents_execution_mode( RunAgentsExecutionMode::Remote { environment_id: remote.environment_id, worker_host: remote.worker_host, - computer_use_enabled: remote.computer_use_enabled, + computer_use_enabled: convert_run_agents_computer_use_enabled( + remote.computer_use_enabled, + ), runner_id: remote.runner_id, } } diff --git a/app/src/ai/agent/api/convert_from_tests.rs b/app/src/ai/agent/api/convert_from_tests.rs index 7b3ad65a39d..6a766aee702 100644 --- a/app/src/ai/agent/api/convert_from_tests.rs +++ b/app/src/ai/agent/api/convert_from_tests.rs @@ -1,4 +1,4 @@ -use ai::agent::action::AskUserQuestionType; +use ai::agent::action::{AskUserQuestionType, RunAgentsExecutionMode}; use ai::skills::SkillPathOrigin; use warp_multi_agent_api as api; @@ -247,3 +247,88 @@ fn transfer_control_tool_call_converts_to_action_message() { } } } + +/// Builds a `run_agents` tool call whose remote block carries the given +/// `computer_use_enabled` wire value. +fn run_agents_remote_tool_call(computer_use_enabled: bool) -> api::Message { + api::Message { + fetched_memories: vec![], + id: "message".to_string(), + task_id: "task".to_string(), + server_message_data: String::new(), + citations: vec![], + message: Some(api::message::Message::ToolCall(api::message::ToolCall { + tool_call_id: "tool_call".to_string(), + tool: Some(api::message::tool_call::Tool::RunAgents(api::RunAgents { + summary: "summary".to_string(), + base_prompt: "base".to_string(), + skills: vec![], + model_id: "auto".to_string(), + harness: None, + agent_run_configs: vec![], + execution_mode: Some(api::run_agents::ExecutionModeOneOf::Remote( + api::run_agents::Remote { + environment_id: "env-1".to_string(), + worker_host: "warp".to_string(), + computer_use_enabled, + runner_id: String::new(), + }, + )), + plan_id: String::new(), + })), + })), + request_id: "req".to_string(), + timestamp: None, + } +} + +fn converted_run_agents_execution_mode(message: api::Message) -> RunAgentsExecutionMode { + let task_id = TaskId::new("task".to_string()); + let converted = message + .to_client_output_message(ConversionParams { + task_id: &task_id, + current_todo_list: None, + active_code_review: None, + skill_path_origin: &SkillPathOrigin::Local, + }) + .expect("run_agents conversion should succeed"); + let MaybeAIAgentOutputMessage::Message(output) = converted else { + panic!("Expected run_agents tool call to produce a client action"); + }; + let AIAgentOutputMessageType::Action(action) = output.message else { + panic!("Expected an action message"); + }; + let AIAgentActionType::RunAgents(request) = action.action else { + panic!("Expected a RunAgents action"); + }; + request.execution_mode +} + +/// Regression for REMOTE-2444: an omitted `computer_use_enabled` reaches the +/// client as the wire zero value, and must stay unspecified rather than +/// becoming an explicit disable that overrides the cloud default. +#[test] +fn run_agents_remote_without_computer_use_flag_stays_unspecified() { + assert_eq!( + converted_run_agents_execution_mode(run_agents_remote_tool_call(false)), + RunAgentsExecutionMode::Remote { + environment_id: "env-1".to_string(), + worker_host: "warp".to_string(), + computer_use_enabled: None, + runner_id: String::new(), + } + ); +} + +#[test] +fn run_agents_remote_with_computer_use_enabled_round_trips() { + assert_eq!( + converted_run_agents_execution_mode(run_agents_remote_tool_call(true)), + RunAgentsExecutionMode::Remote { + environment_id: "env-1".to_string(), + worker_host: "warp".to_string(), + computer_use_enabled: Some(true), + runner_id: String::new(), + } + ); +} diff --git a/app/src/ai/blocklist/action_model/execute/run_agents.rs b/app/src/ai/blocklist/action_model/execute/run_agents.rs index 67130617c16..1bc0ce6adab 100644 --- a/app/src/ai/blocklist/action_model/execute/run_agents.rs +++ b/app/src/ai/blocklist/action_model/execute/run_agents.rs @@ -36,8 +36,8 @@ use crate::ai::document::plan_publication::{ }; use crate::ai::local_harness_setup::local_harness_product_disabled_message; use crate::ai::orchestration::{ - OrchestrationConfigState, can_execute_with_auth_secret, - populate_default_auth_secret_for_execution, + OrchestrationConfigState, can_execute_with_auth_secret, effective_computer_use_enabled, + orchestration_harness, populate_default_auth_secret_for_execution, }; /// Per-child spawn timeout. If a child agent doesn't report back within @@ -357,7 +357,12 @@ impl RunAgentsExecutor { } => RunAgentsLaunchedExecutionMode::Remote { environment_id: environment_id.clone(), worker_host: worker_host.clone(), - computer_use_enabled: *computer_use_enabled, + // An unspecified per-call flag resolves to the same + // default the children were launched with. + computer_use_enabled: effective_computer_use_enabled( + *computer_use_enabled, + orchestration_harness(&run_harness_type), + ), runner_id: runner_id.clone(), }, }; diff --git a/app/src/ai/blocklist/action_model/execute/run_agents_tests.rs b/app/src/ai/blocklist/action_model/execute/run_agents_tests.rs index 44b7823b949..166ce763945 100644 --- a/app/src/ai/blocklist/action_model/execute/run_agents_tests.rs +++ b/app/src/ai/blocklist/action_model/execute/run_agents_tests.rs @@ -234,7 +234,7 @@ fn remote_run_agents_action(harness_type: &str) -> AIAgentAction { execution_mode: RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, agent_run_configs: vec![RunAgentsAgentRunConfig { diff --git a/app/src/ai/blocklist/block_tests.rs b/app/src/ai/blocklist/block_tests.rs index fcf6a21d05a..860902f8af4 100644 --- a/app/src/ai/blocklist/block_tests.rs +++ b/app/src/ai/blocklist/block_tests.rs @@ -443,7 +443,7 @@ fn remote_arm_propagates_skills_into_skill_references() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), runner_id: String::new(), }, "oz", @@ -473,7 +473,7 @@ fn remote_arm_propagates_skills_into_skill_references() { assert_eq!(worker_host, "warp"); assert_eq!(harness_type, "oz"); assert_eq!(model_id, "auto"); - assert!(computer_use_enabled); + assert_eq!(computer_use_enabled, Some(true)); assert_eq!(title, "Child"); assert_eq!(auth_secret_name, None); assert_eq!(agent_identity_uid, None); @@ -487,7 +487,7 @@ fn remote_arm_propagates_agent_identity_uid() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, "oz", @@ -522,7 +522,7 @@ fn remote_arm_with_empty_skills_propagates_empty_vec() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, "claude", @@ -547,7 +547,7 @@ fn remote_arm_rejects_opencode() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, "opencode", @@ -600,7 +600,7 @@ fn remote_arm_propagates_claude_auth_secret_into_mode() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, "claude", @@ -625,7 +625,7 @@ fn remote_arm_filters_whitespace_auth_secret_name_to_none() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, "codex", diff --git a/app/src/ai/blocklist/inline_action/orchestration_controls.rs b/app/src/ai/blocklist/inline_action/orchestration_controls.rs index 58f50b9821b..2938f1798bb 100644 --- a/app/src/ai/blocklist/inline_action/orchestration_controls.rs +++ b/app/src/ai/blocklist/inline_action/orchestration_controls.rs @@ -242,7 +242,7 @@ fn snapshot_execution_mode(is_local: bool) -> RunAgentsExecutionMode { RunAgentsExecutionMode::Remote { environment_id: String::new(), worker_host: String::new(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), } } @@ -423,7 +423,7 @@ pub fn populate_environment_picker( &RunAgentsExecutionMode::Remote { environment_id: initial_env_id.to_string(), worker_host: String::new(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, ); @@ -565,7 +565,7 @@ pub fn populate_host_picker( &RunAgentsExecutionMode::Remote { environment_id: String::new(), worker_host: initial_host.to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, ); diff --git a/app/src/ai/blocklist/inline_action/run_agents_card_view_tests.rs b/app/src/ai/blocklist/inline_action/run_agents_card_view_tests.rs index 04b2986d0b8..f58542743f7 100644 --- a/app/src/ai/blocklist/inline_action/run_agents_card_view_tests.rs +++ b/app/src/ai/blocklist/inline_action/run_agents_card_view_tests.rs @@ -83,7 +83,10 @@ fn local_to_cloud_initializes_remote_with_empty_environment() { }; assert_eq!(environment_id, ""); assert_eq!(worker_host, "warp"); - assert!(!computer_use_enabled); + assert_eq!( + computer_use_enabled, None, + "toggling to Cloud leaves computer use unspecified so the cloud default applies" + ); } #[test] @@ -93,7 +96,7 @@ fn cloud_to_local_drops_environment() { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, )); @@ -123,7 +126,7 @@ fn cloud_without_env_no_longer_disables_accept() { RunAgentsExecutionMode::Remote { environment_id: String::new(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, )); @@ -144,7 +147,7 @@ fn cloud_with_opencode_disables_accept() { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, )); @@ -200,7 +203,7 @@ fn cloud_with_env_and_non_opencode_harness_allows_accept() { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, )); @@ -234,7 +237,7 @@ fn set_environment_id_updates_remote() { RunAgentsExecutionMode::Remote { environment_id: "old".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, )); @@ -256,7 +259,7 @@ fn set_runner_id_updates_remote_and_round_trips() { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, )); @@ -275,7 +278,7 @@ fn set_runner_id_updates_remote_and_round_trips() { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: "runner-9".to_string(), } ); @@ -301,7 +304,7 @@ fn to_request_round_trips_request_fields() { RunAgentsExecutionMode::Remote { environment_id: "env-2".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), runner_id: String::new(), }, vec![ @@ -535,7 +538,7 @@ mod override_from_approved_config_tests { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), runner_id: String::new(), }, )); @@ -558,7 +561,7 @@ mod override_from_approved_config_tests { RunAgentsExecutionMode::Remote { environment_id: "old-env".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), runner_id: String::new(), }, )); @@ -574,8 +577,9 @@ mod override_from_approved_config_tests { panic!("expected Remote"); }; assert_eq!(environment_id, "new-env", "env should come from config"); - assert!( + assert_eq!( *computer_use_enabled, + Some(true), "computer_use_enabled should be preserved from original request" ); } @@ -594,9 +598,9 @@ mod override_from_approved_config_tests { else { panic!("expected Remote"); }; - assert!( - !*computer_use_enabled, - "computer_use_enabled should default to false when original was Local" + assert_eq!( + *computer_use_enabled, None, + "computer_use_enabled should stay unspecified when original was Local" ); } @@ -621,7 +625,7 @@ fn local_to_cloud_idempotent_when_already_remote() { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), runner_id: String::new(), }, )); @@ -640,8 +644,9 @@ fn local_to_cloud_idempotent_when_already_remote() { environment_id, "env-1", "toggle to Remote when already Remote should not clobber env" ); - assert!( + assert_eq!( computer_use_enabled, + Some(true), "toggle to Remote when already Remote should not clobber computer_use" ); } diff --git a/app/src/ai/orchestration/config_state.rs b/app/src/ai/orchestration/config_state.rs index 352d444554a..01703aa2f9c 100644 --- a/app/src/ai/orchestration/config_state.rs +++ b/app/src/ai/orchestration/config_state.rs @@ -45,7 +45,9 @@ pub struct OrchestrationConfigState { pub execution_mode: RunAgentsExecutionMode, /// Per-call value hidden from the orchestration editors. Kept outside /// `execution_mode` so a temporary switch to Local does not discard it. - remote_computer_use_enabled: bool, + /// `None` means the call expressed no opinion, so the children fall back + /// to the normal cloud default rather than being forced off. + remote_computer_use_enabled: Option, /// Drives the picker display and Accept gate. Persisted as /// `Named(_)` only via `CloudAgentSettings.last_selected_auth_secret`. pub auth_secret_selection: AuthSecretSelection, @@ -96,7 +98,7 @@ impl OrchestrationConfigState { execution_mode: &RunAgentsExecutionMode, ) -> Self { let remote_computer_use_enabled = match execution_mode { - RunAgentsExecutionMode::Local => false, + RunAgentsExecutionMode::Local => None, RunAgentsExecutionMode::Remote { computer_use_enabled, .. @@ -122,7 +124,9 @@ impl OrchestrationConfigState { } => RunAgentsExecutionMode::Remote { environment_id: environment_id.clone(), worker_host: worker_host.clone(), - computer_use_enabled: false, + // The approved plan carries no computer-use opinion, so leave + // it unspecified and let the normal cloud default apply. + computer_use_enabled: None, runner_id: runner_id.clone(), }, }; @@ -130,7 +134,7 @@ impl OrchestrationConfigState { model_id: config.model_id.clone(), harness_type: config.harness_type.clone(), execution_mode, - remote_computer_use_enabled: false, + remote_computer_use_enabled: None, auth_secret_selection: AuthSecretSelection::Unset, }; if matches!(state.execution_mode, RunAgentsExecutionMode::Local) { diff --git a/app/src/ai/orchestration/config_state_tests.rs b/app/src/ai/orchestration/config_state_tests.rs index 6befbb6dd70..cfb54fa1db9 100644 --- a/app/src/ai/orchestration/config_state_tests.rs +++ b/app/src/ai/orchestration/config_state_tests.rs @@ -19,7 +19,7 @@ fn toggle_to_local_sanitizes_disabled_codex() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, ); @@ -42,7 +42,7 @@ fn local_round_trip_preserves_remote_computer_use() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), runner_id: String::new(), }, ); @@ -53,7 +53,7 @@ fn local_round_trip_preserves_remote_computer_use() { assert!(matches!( state.execution_mode, RunAgentsExecutionMode::Remote { - computer_use_enabled: true, + computer_use_enabled: Some(true), .. } )); @@ -67,7 +67,7 @@ fn toggle_to_local_preserves_claude() { &RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), }, ); @@ -114,6 +114,64 @@ fn runner_id_round_trips_through_config() { assert_eq!(state.to_orchestration_config(), config); } +/// Regression for REMOTE-2444: the approved plan carries no computer-use +/// opinion, so a plan-driven launch must leave the flag unspecified instead +/// of seeding an explicit disable. +#[test] +fn from_orchestration_config_leaves_computer_use_unspecified() { + let config = OrchestrationConfig { + model_id: "auto".to_string(), + harness_type: "oz".to_string(), + execution_mode: OrchestrationExecutionMode::Remote { + environment_id: "env-1".to_string(), + worker_host: "warp".to_string(), + runner_id: String::new(), + }, + }; + let state = OrchestrationConfigState::from_orchestration_config(&config); + assert!(matches!( + state.execution_mode, + RunAgentsExecutionMode::Remote { + computer_use_enabled: None, + .. + } + )); +} + +/// An explicit per-call flag survives the approved-config override, which +/// only owns model / harness / execution mode. +#[test] +fn override_from_approved_config_preserves_explicit_computer_use() { + let mut state = OrchestrationConfigState::from_run_agents_fields( + Some("auto"), + Some("oz"), + &RunAgentsExecutionMode::Remote { + environment_id: "env-1".to_string(), + worker_host: "warp".to_string(), + computer_use_enabled: Some(true), + runner_id: String::new(), + }, + ); + + state.override_from_approved_config(&OrchestrationConfig { + model_id: "auto".to_string(), + harness_type: "oz".to_string(), + execution_mode: OrchestrationExecutionMode::Remote { + environment_id: "env-2".to_string(), + worker_host: "warp".to_string(), + runner_id: String::new(), + }, + }); + + assert!(matches!( + state.execution_mode, + RunAgentsExecutionMode::Remote { + computer_use_enabled: Some(true), + .. + } + )); +} + #[test] fn resolve_from_config_sanitizes_disabled_local_codex() { let mut state = OrchestrationConfigState::from_run_agents_fields( diff --git a/app/src/ai/orchestration/edit_state_tests.rs b/app/src/ai/orchestration/edit_state_tests.rs index 0926aa72d8b..7f31a4c4729 100644 --- a/app/src/ai/orchestration/edit_state_tests.rs +++ b/app/src/ai/orchestration/edit_state_tests.rs @@ -9,7 +9,7 @@ fn remote_mode() -> RunAgentsExecutionMode { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), } } diff --git a/app/src/ai/orchestration/mod.rs b/app/src/ai/orchestration/mod.rs index 8d0b5adbf55..1dda893111f 100644 --- a/app/src/ai/orchestration/mod.rs +++ b/app/src/ai/orchestration/mod.rs @@ -32,7 +32,7 @@ pub use remote_child::{ CloudAgentStartupAuthFlow, CloudAgentStartupBlocker, CloudAgentStartupFailure, CloudAgentStartupIssue, CloudAgentStartupPresentation, PrepareRemoteChildLaunchError, PreparedRemoteChildLaunch, RemoteChildLaunchConfig, classify_cloud_agent_startup_error, - oz_run_url, prepare_remote_child_launch, + effective_computer_use_enabled, orchestration_harness, oz_run_url, prepare_remote_child_launch, }; pub(crate) use snapshots::AUTH_SECRET_INHERIT_LABEL; #[cfg_attr(not(feature = "tui"), allow(unused_imports))] diff --git a/app/src/ai/orchestration/remote_child.rs b/app/src/ai/orchestration/remote_child.rs index bc022dc76bb..94a4203ec8e 100644 --- a/app/src/ai/orchestration/remote_child.rs +++ b/app/src/ai/orchestration/remote_child.rs @@ -31,7 +31,10 @@ pub struct RemoteChildLaunchConfig { pub environment_id: String, pub skill_references: Vec, pub model_id: String, - pub computer_use_enabled: bool, + /// Tri-state computer-use flag. `None` (nobody expressed an opinion) + /// leaves the field off the spawn request so the server applies its + /// normal default instead of the child being forced off. + pub computer_use_enabled: Option, pub worker_host: String, pub harness_type: String, pub title: String, @@ -42,14 +45,47 @@ pub struct RemoteChildLaunchConfig { impl RemoteChildLaunchConfig { pub fn orchestration_harness(&self) -> Harness { - if self.harness_type.trim().is_empty() { - Harness::Oz - } else { - Harness::parse_orchestration_harness(&self.harness_type).unwrap_or(Harness::Unknown) - } + orchestration_harness(&self.harness_type) + } +} + +/// Resolves a run-wide orchestration harness string to a [`Harness`]. An +/// empty string means the caller did not override the harness, which is +/// the default Oz harness. +pub fn orchestration_harness(harness_type: &str) -> Harness { + if harness_type.trim().is_empty() { + Harness::Oz + } else { + Harness::parse_orchestration_harness(harness_type).unwrap_or(Harness::Unknown) } } +/// The `computer_use_enabled` value to put on the child's spawn config. +/// +/// Only the Oz harness supports computer use, so an explicit choice is +/// forwarded for Oz children and dropped for third-party harnesses. +/// `None` is left off the request entirely (the field is +/// `skip_serializing_if = "Option::is_none"`), so the server resolves its +/// documented default — Oz runs get computer use, third-party harnesses do +/// not — exactly like a non-orchestrated cloud run. +pub fn spawn_computer_use_enabled( + computer_use_enabled: Option, + orchestration_harness: Harness, +) -> Option { + computer_use_enabled.filter(|_| orchestration_harness == Harness::Oz) +} + +/// The computer-use setting a launched child effectively runs with, used +/// for reporting the resolved orchestration config. Mirrors the server's +/// default resolution for the value [`spawn_computer_use_enabled`] sends. +pub fn effective_computer_use_enabled( + computer_use_enabled: Option, + orchestration_harness: Harness, +) -> bool { + spawn_computer_use_enabled(computer_use_enabled, orchestration_harness) + .unwrap_or(orchestration_harness == Harness::Oz) +} + /// Frontend-neutral output used to launch one remote child. #[cfg_attr(not(feature = "tui"), allow(dead_code))] #[derive(Clone, Debug)] @@ -227,7 +263,7 @@ pub fn prepare_remote_child_launch( } }; let computer_use_enabled = - (orchestration_harness == Harness::Oz).then_some(computer_use_enabled); + spawn_computer_use_enabled(computer_use_enabled, orchestration_harness); let harness_auth_secrets = auth_secret_name .filter(|name| !name.trim().is_empty()) .and_then(|name| match orchestration_harness { diff --git a/app/src/ai/orchestration/remote_child_tests.rs b/app/src/ai/orchestration/remote_child_tests.rs index 8a3ea6edd46..bae459477b5 100644 --- a/app/src/ai/orchestration/remote_child_tests.rs +++ b/app/src/ai/orchestration/remote_child_tests.rs @@ -1,10 +1,12 @@ use anyhow::anyhow; +use warp_cli::agent::Harness; use warpui::App; use super::{ CloudAgentStartupAuthFlow, CloudAgentStartupBlocker, CloudAgentStartupFailure, CloudAgentStartupIssue, CloudAgentStartupPresentation, RemoteChildLaunchConfig, - classify_cloud_agent_startup_error, prepare_remote_child_launch, + classify_cloud_agent_startup_error, effective_computer_use_enabled, + prepare_remote_child_launch, spawn_computer_use_enabled, }; use crate::ai::agent::{StartAgentExecutionMode, UserQueryMode}; use crate::ai::blocklist::StartAgentRequest; @@ -15,7 +17,7 @@ fn config(harness_type: &str) -> RemoteChildLaunchConfig { environment_id: String::new(), skill_references: Vec::new(), model_id: String::new(), - computer_use_enabled: false, + computer_use_enabled: None, worker_host: String::new(), harness_type: harness_type.to_string(), title: String::new(), @@ -25,6 +27,30 @@ fn config(harness_type: &str) -> RemoteChildLaunchConfig { } } +/// A minimal orchestrated child request with the parent run id set. +fn remote_child_request() -> StartAgentRequest { + StartAgentRequest { + id: Default::default(), + name: "researcher".to_string(), + prompt: "Inspect the code".to_string(), + execution_mode: StartAgentExecutionMode::Remote { + environment_id: String::new(), + skill_references: Vec::new(), + model_id: String::new(), + computer_use_enabled: None, + worker_host: String::new(), + harness_type: "oz".to_string(), + title: String::new(), + auth_secret_name: None, + runner_id: String::new(), + agent_identity_uid: None, + }, + lifecycle_subscription: None, + parent_conversation_id: crate::ai::agent::conversation::AIConversationId::new(), + parent_run_id: Some("parent-run".to_string()), + } +} + #[test] fn orchestration_harness_defaults_to_oz_and_parses_known_harnesses() { assert_eq!( @@ -49,7 +75,7 @@ fn prepared_remote_request_matches_gui_wire_semantics() { environment_id: "env-1".to_string(), skill_references: Vec::new(), model_id: "auto".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), worker_host: "warp".to_string(), harness_type: "oz".to_string(), title: "Research".to_string(), @@ -68,7 +94,7 @@ fn prepared_remote_request_matches_gui_wire_semantics() { environment_id: "env-1".to_string(), skill_references: Vec::new(), model_id: "auto".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), worker_host: "warp".to_string(), harness_type: "oz".to_string(), title: "Research".to_string(), @@ -103,6 +129,81 @@ fn prepared_remote_request_matches_gui_wire_semantics() { }); } +/// Regression for REMOTE-2444: a `run_agents` call that left computer use +/// unspecified must not send an explicit `false` on the child spawn request, +/// which would override the server's Oz default and force computer use off. +#[test] +fn unspecified_computer_use_is_omitted_from_the_child_spawn_request() { + App::test((), |mut app| async move { + crate::test_util::terminal::initialize_app_for_terminal_view(&mut app); + let request = remote_child_request(); + app.read(|ctx| { + let mut config = config("oz"); + config.computer_use_enabled = None; + let prepared = prepare_remote_child_launch(&request, config, ctx).unwrap(); + assert_eq!( + prepared.spawn_request.config.unwrap().computer_use_enabled, + None + ); + }); + }); +} + +#[test] +fn explicitly_disabled_computer_use_still_reaches_the_child_spawn_request() { + App::test((), |mut app| async move { + crate::test_util::terminal::initialize_app_for_terminal_view(&mut app); + let request = remote_child_request(); + app.read(|ctx| { + let mut config = config("oz"); + config.computer_use_enabled = Some(false); + let prepared = prepare_remote_child_launch(&request, config, ctx).unwrap(); + assert_eq!( + prepared.spawn_request.config.unwrap().computer_use_enabled, + Some(false) + ); + }); + }); +} + +#[test] +fn computer_use_is_dropped_for_third_party_harness_children() { + App::test((), |mut app| async move { + crate::test_util::terminal::initialize_app_for_terminal_view(&mut app); + let request = remote_child_request(); + app.read(|ctx| { + let mut config = config("claude"); + config.computer_use_enabled = Some(true); + let prepared = prepare_remote_child_launch(&request, config, ctx).unwrap(); + assert_eq!( + prepared.spawn_request.config.unwrap().computer_use_enabled, + None + ); + }); + }); +} + +#[test] +fn computer_use_resolution_mirrors_the_cloud_default() { + // Unspecified defers to the server: Oz enables computer use, third-party + // harnesses do not. + assert_eq!(spawn_computer_use_enabled(None, Harness::Oz), None); + assert!(effective_computer_use_enabled(None, Harness::Oz)); + assert_eq!(spawn_computer_use_enabled(None, Harness::Claude), None); + assert!(!effective_computer_use_enabled(None, Harness::Claude)); + // An explicit choice wins for Oz children. + assert_eq!( + spawn_computer_use_enabled(Some(true), Harness::Oz), + Some(true) + ); + assert!(effective_computer_use_enabled(Some(true), Harness::Oz)); + assert_eq!( + spawn_computer_use_enabled(Some(false), Harness::Oz), + Some(false) + ); + assert!(!effective_computer_use_enabled(Some(false), Harness::Oz)); +} + #[test] fn github_auth_error_is_a_shared_blocker_with_cloud_callback_url() { let error = anyhow::Error::new(ClientError { diff --git a/app/src/ai/orchestration/validation_tests.rs b/app/src/ai/orchestration/validation_tests.rs index cd855c91f6a..0b0f64c1ef7 100644 --- a/app/src/ai/orchestration/validation_tests.rs +++ b/app/src/ai/orchestration/validation_tests.rs @@ -34,7 +34,7 @@ fn cloud() -> RunAgentsExecutionMode { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), } } diff --git a/crates/ai/src/agent/action/mod.rs b/crates/ai/src/agent/action/mod.rs index 9f231d80a95..e6d8760fe9d 100644 --- a/crates/ai/src/agent/action/mod.rs +++ b/crates/ai/src/agent/action/mod.rs @@ -239,7 +239,12 @@ pub enum RunAgentsExecutionMode { Remote { environment_id: String, worker_host: String, - computer_use_enabled: bool, + /// Tri-state per-call computer-use flag. `None` means the caller + /// expressed no opinion, so the children inherit the normal cloud + /// default (Oz harness → enabled, third-party harnesses → + /// disabled) instead of being forced off. `Some(_)` is an explicit + /// choice that applies to the whole batch. + computer_use_enabled: Option, /// Runner UID selecting the children's compute config (docker /// image, instance shape, setup commands). Empty means "no /// override" — fall back to the environment's default runner then @@ -286,7 +291,10 @@ pub enum StartAgentExecutionMode { environment_id: String, skill_references: Vec, model_id: String, - computer_use_enabled: bool, + /// Tri-state computer-use flag for the child run. `None` leaves the + /// field off the spawn request so the server applies its normal + /// default; `Some(_)` is an explicit enable/disable. + computer_use_enabled: Option, worker_host: String, harness_type: String, title: String, diff --git a/crates/ai/src/agent/orchestration_config_tests.rs b/crates/ai/src/agent/orchestration_config_tests.rs index 0f8f49216d2..78360c2d6cb 100644 --- a/crates/ai/src/agent/orchestration_config_tests.rs +++ b/crates/ai/src/agent/orchestration_config_tests.rs @@ -28,7 +28,7 @@ fn make_request(model: &str, harness: &str, remote: bool) -> RunAgentsRequest { RunAgentsExecutionMode::Remote { environment_id: "env-1".to_string(), worker_host: "warp".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, runner_id: String::new(), } } else { @@ -180,7 +180,7 @@ fn computer_use_not_in_match_check() { .. } = request.execution_mode { - *computer_use_enabled = true; + *computer_use_enabled = Some(true); } // computer_use_enabled differs but should still match assert!(matches_active_config(&request, &config)); diff --git a/crates/warp_tui/src/orchestration_block_tests.rs b/crates/warp_tui/src/orchestration_block_tests.rs index 77c29de73e2..94c2055573c 100644 --- a/crates/warp_tui/src/orchestration_block_tests.rs +++ b/crates/warp_tui/src/orchestration_block_tests.rs @@ -80,7 +80,7 @@ fn remote(environment_id: &str, worker_host: &str) -> RunAgentsExecutionMode { RunAgentsExecutionMode::Remote { environment_id: environment_id.to_string(), worker_host: worker_host.to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), runner_id: String::new(), } } @@ -225,7 +225,7 @@ fn build_request_carries_card_fields_and_edited_run_wide_state() { RunAgentsExecutionMode::Remote { environment_id: "env-9".to_string(), worker_host: "self-hosted".to_string(), - computer_use_enabled: true, + computer_use_enabled: Some(true), runner_id: String::new(), }, ); diff --git a/crates/warp_tui/src/orchestration_model_tests.rs b/crates/warp_tui/src/orchestration_model_tests.rs index 40d3e04831b..98ca3c6bc26 100644 --- a/crates/warp_tui/src/orchestration_model_tests.rs +++ b/crates/warp_tui/src/orchestration_model_tests.rs @@ -31,7 +31,7 @@ fn remote_request(parent_conversation_id: AIConversationId) -> StartAgentRequest environment_id: "env-1".to_string(), skill_references: Vec::new(), model_id: "auto".to_string(), - computer_use_enabled: false, + computer_use_enabled: None, worker_host: "warp".to_string(), harness_type: "oz".to_string(), title: "Researcher".to_string(),