diff --git a/Cargo.lock b/Cargo.lock index 9e348a94210..03b429efab4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15735,7 +15735,7 @@ dependencies = [ [[package]] name = "warp_multi_agent_api" version = "0.0.0" -source = "git+https://github.com/warpdotdev/warp-proto-apis.git?rev=60d3132feffaca7e3abfe465e38ba0b72d32dee0#60d3132feffaca7e3abfe465e38ba0b72d32dee0" +source = "git+https://github.com/warpdotdev/warp-proto-apis.git?rev=3d26f166b74469d5c12080ef6a3423fadee4d0e3#3d26f166b74469d5c12080ef6a3423fadee4d0e3" dependencies = [ "prost", "prost-reflect", diff --git a/Cargo.toml b/Cargo.toml index fde475d54e7..429f85e9b86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -342,7 +342,7 @@ version-compare = "0.1" vte = { git = "https://github.com/warpdotdev/vte.git", rev = "4b399c87b63ba88f45709edaa6383fc519f6c900", default-features = false } walkdir = "2" warp-workflows = { git = "https://github.com/warpdotdev/workflows", rev = "793a98ddda6ef19682aed66364faebd2829f0e01" } -warp_multi_agent_api = { git = "https://github.com/warpdotdev/warp-proto-apis.git", rev = "60d3132feffaca7e3abfe465e38ba0b72d32dee0" } +warp_multi_agent_api = { git = "https://github.com/warpdotdev/warp-proto-apis.git", rev = "3d26f166b74469d5c12080ef6a3423fadee4d0e3" } wasm-bindgen = "0.2.89" wasm-bindgen-futures = "0.4.42" web-sys = { version = "0.3.69", features = [ diff --git a/app/src/ai/agent/api/convert_from.rs b/app/src/ai/agent/api/convert_from.rs index 7364a978d4b..518e49c363c 100644 --- a/app/src/ai/agent/api/convert_from.rs +++ b/app/src/ai/agent/api/convert_from.rs @@ -164,6 +164,7 @@ fn convert_run_agents( name: config.name, prompt: config.prompt, title: config.title, + agent_identity_uid: config.agent_identity_uid, }) .collect(), plan_id, @@ -198,6 +199,9 @@ fn convert_start_agent_v2_execution_mode( // Auth secret is plumbed client-side via `RunAgentsRequest`; // StartAgentV2 from the server never carries it. auth_secret_name: None, + // Agent identity is plumbed client-side via `RunAgentsRequest`; + // StartAgentV2 from the server never carries it. + agent_identity_uid: None, } } Some(api::start_agent_v2::execution_mode::Mode::Local(local)) => { diff --git a/app/src/ai/agent/api/convert_from_tests.rs b/app/src/ai/agent/api/convert_from_tests.rs index 8f5a32f3065..64184f72b0c 100644 --- a/app/src/ai/agent/api/convert_from_tests.rs +++ b/app/src/ai/agent/api/convert_from_tests.rs @@ -538,6 +538,7 @@ fn converts_remote_start_agent_with_environment_id() { harness_type: String::new(), title: String::new(), auth_secret_name: None, + agent_identity_uid: None, } ); assert_eq!(lifecycle_subscription, None); @@ -580,6 +581,7 @@ fn converts_remote_start_agent_v2_with_skill_references() { harness_type: "claude-code".to_string(), title: "Remote child".to_string(), auth_secret_name: None, + agent_identity_uid: None, } ); assert_eq!(lifecycle_subscription, None); 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 8be5f9b4ff4..6a79ad3ef1a 100644 --- a/app/src/ai/blocklist/action_model/execute/run_agents.rs +++ b/app/src/ai/blocklist/action_model/execute/run_agents.rs @@ -680,6 +680,15 @@ pub fn run_agents_to_start_agent_mode( ) -> Result { match run_execution_mode { RunAgentsExecutionMode::Local => { + // Named-agent identity requires the public-API dispatch path, which + // only remote children use. Mirrors server-side validation. + if !cfg.agent_identity_uid.trim().is_empty() { + return Err( + "agent_identity_uid requires remote execution; local child agents cannot \ + run as a different named agent." + .to_string(), + ); + } let trimmed = run_harness_type.trim(); // Propagate run-wide model selection for local launches. let trimmed_model_id = run_model_id.trim(); @@ -723,6 +732,8 @@ pub fn run_agents_to_start_agent_mode( auth_secret_name: run_auth_secret_name .map(str::to_string) .filter(|s| !s.trim().is_empty()), + agent_identity_uid: Some(cfg.agent_identity_uid.clone()) + .filter(|s| !s.trim().is_empty()), }) } } 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 3a66f986af6..f70b0752d30 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 @@ -236,6 +236,7 @@ fn remote_run_agents_action(harness_type: &str) -> AIAgentAction { name: "child".to_string(), prompt: "Help".to_string(), title: String::new(), + agent_identity_uid: String::new(), }], plan_id: String::new(), harness_auth_secret_name: None, @@ -258,6 +259,7 @@ fn local_codex_run_agents_maps_to_local_harness_mode_when_flag_enabled() { name: "child".to_string(), prompt: "Investigate the failure".to_string(), title: String::new(), + agent_identity_uid: String::new(), }; let mode = run_agents_to_start_agent_mode( diff --git a/app/src/ai/blocklist/action_model/execute/start_agent.rs b/app/src/ai/blocklist/action_model/execute/start_agent.rs index 6230861a4ea..313866ae945 100644 --- a/app/src/ai/blocklist/action_model/execute/start_agent.rs +++ b/app/src/ai/blocklist/action_model/execute/start_agent.rs @@ -432,6 +432,7 @@ impl StartAgentExecutor { harness_type, title, auth_secret_name, + agent_identity_uid, } => { let harness_type = Harness::parse_orchestration_harness(&harness_type) .map(|harness| harness.to_string()) @@ -479,6 +480,7 @@ impl StartAgentExecutor { harness_type, title, auth_secret_name, + agent_identity_uid, }, Some(parent_run_id), ) diff --git a/app/src/ai/blocklist/action_model/execute/start_agent_tests.rs b/app/src/ai/blocklist/action_model/execute/start_agent_tests.rs index 34b7c440b66..cf2d93fef36 100644 --- a/app/src/ai/blocklist/action_model/execute/start_agent_tests.rs +++ b/app/src/ai/blocklist/action_model/execute/start_agent_tests.rs @@ -1039,6 +1039,7 @@ fn execute_returns_error_when_remote_opencode_harness_is_requested() { harness_type: "opencode".to_string(), title: String::new(), auth_secret_name: None, + agent_identity_uid: None, }, ); diff --git a/app/src/ai/blocklist/block/view_impl/orchestration_tests.rs b/app/src/ai/blocklist/block/view_impl/orchestration_tests.rs index 832dee9e305..307db1cf9a1 100644 --- a/app/src/ai/blocklist/block/view_impl/orchestration_tests.rs +++ b/app/src/ai/blocklist/block/view_impl/orchestration_tests.rs @@ -79,6 +79,7 @@ fn start_agent_copy_uses_remote_labels_for_remote_children() { harness_type: String::new(), title: String::new(), auth_secret_name: None, + agent_identity_uid: None, }; assert_eq!(start_agent_success_suffix(&execution_mode), " remotely."); diff --git a/app/src/ai/blocklist/block_tests.rs b/app/src/ai/blocklist/block_tests.rs index ac822121097..e39b349b23b 100644 --- a/app/src/ai/blocklist/block_tests.rs +++ b/app/src/ai/blocklist/block_tests.rs @@ -380,6 +380,7 @@ fn agent_cfg() -> RunAgentsAgentRunConfig { name: "child".to_string(), prompt: "do X".to_string(), title: "Child".to_string(), + agent_identity_uid: String::new(), } } @@ -413,6 +414,7 @@ fn remote_arm_propagates_skills_into_skill_references() { computer_use_enabled, title, auth_secret_name, + agent_identity_uid, } = mode else { panic!("expected Remote start-agent mode"); @@ -425,6 +427,43 @@ fn remote_arm_propagates_skills_into_skill_references() { assert!(computer_use_enabled); assert_eq!(title, "Child"); assert_eq!(auth_secret_name, None); + assert_eq!(agent_identity_uid, None); +} + +#[test] +fn remote_arm_propagates_agent_identity_uid() { + let mut cfg = agent_cfg(); + cfg.agent_identity_uid = "sa-uid-1".to_string(); + let mode = run_agents_to_start_agent_mode( + &RunAgentsExecutionMode::Remote { + environment_id: "env-1".to_string(), + worker_host: "warp".to_string(), + computer_use_enabled: false, + }, + "oz", + "auto", + &[], + None, + &cfg, + ) + .expect("Remote+oz must convert"); + let StartAgentExecutionMode::Remote { + agent_identity_uid, .. + } = mode + else { + panic!("expected Remote start-agent mode"); + }; + assert_eq!(agent_identity_uid.as_deref(), Some("sa-uid-1")); +} + +#[test] +fn local_arm_rejects_agent_identity_uid() { + let mut cfg = agent_cfg(); + cfg.agent_identity_uid = "sa-uid-1".to_string(); + let err = + run_agents_to_start_agent_mode(&RunAgentsExecutionMode::Local, "", "", &[], None, &cfg) + .expect_err("Local + agent_identity_uid must be rejected"); + assert!(err.contains("agent_identity_uid requires remote execution")); } #[test] 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 70fc7e29b96..42827c48567 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 @@ -31,6 +31,7 @@ fn make_request_with_skills( name: "child".to_string(), prompt: "do work".to_string(), title: "Child agent".to_string(), + agent_identity_uid: String::new(), }], plan_id: String::new(), harness_auth_secret_name: None, diff --git a/app/src/pane_group/pane/terminal_pane.rs b/app/src/pane_group/pane/terminal_pane.rs index 0a37b6aebda..10a2fd6a79b 100644 --- a/app/src/pane_group/pane/terminal_pane.rs +++ b/app/src/pane_group/pane/terminal_pane.rs @@ -1595,6 +1595,7 @@ fn dispatch_start_agent_conversation( harness_type, title, auth_secret_name, + agent_identity_uid, } => { launch_remote_child( group, @@ -1609,6 +1610,7 @@ fn dispatch_start_agent_conversation( harness_type, title, auth_secret_name, + agent_identity_uid, }, ctx, ); @@ -1922,6 +1924,9 @@ struct RemoteLaunchFields { /// harness credentials. Resolved to `AgentConfigSnapshot.harness_auth_secrets` /// when applicable. auth_secret_name: Option, + /// UID of the named agent (service account) the remote child should + /// execute as; forwarded to `SpawnAgentRequest.agent_identity_uid`. + agent_identity_uid: Option, } /// Sets up a hidden ambient-agent pane for a Remote child agent: creates the @@ -1953,6 +1958,7 @@ fn launch_remote_child( harness_type, title, auth_secret_name, + agent_identity_uid, } = fields; let request_id = request.id; @@ -2094,7 +2100,7 @@ fn launch_remote_child( referenced_attachments: vec![], conversation_id: None, initial_snapshot_token: None, - agent_identity_uid: None, + agent_identity_uid: agent_identity_uid.filter(|uid| !uid.trim().is_empty()), snapshot_disabled: should_disable_snapshot(ctx).then_some(true), orchestration_handoff: None, }; diff --git a/crates/ai/src/agent/action/mod.rs b/crates/ai/src/agent/action/mod.rs index 05400c891ff..f17ef090f81 100644 --- a/crates/ai/src/agent/action/mod.rs +++ b/crates/ai/src/agent/action/mod.rs @@ -254,6 +254,11 @@ pub struct RunAgentsAgentRunConfig { pub name: String, pub prompt: String, pub title: String, + /// Optional UID of the named agent (service account) this child run + /// should execute as. Empty means the child runs as the caller. Only + /// meaningful for factory agents dispatching sibling factory agents; + /// requires remote execution and is enforced server-side at dispatch. + pub agent_identity_uid: String, } #[derive(Debug, Clone, Eq, PartialEq)] @@ -281,6 +286,9 @@ pub enum StartAgentExecutionMode { /// `None` means no client-side secret was selected — the remote /// environment falls back to its own ambient credentials. auth_secret_name: Option, + /// UID of the named agent (service account) the remote child run + /// should execute as. `None` means the child runs as the caller. + agent_identity_uid: Option, }, } @@ -311,6 +319,7 @@ impl StartAgentExecutionMode { harness_type: String::new(), title: String::new(), auth_secret_name: None, + agent_identity_uid: None, } } } diff --git a/crates/ai/src/agent/orchestration_config_tests.rs b/crates/ai/src/agent/orchestration_config_tests.rs index 6843908afaa..ba7ff03a440 100644 --- a/crates/ai/src/agent/orchestration_config_tests.rs +++ b/crates/ai/src/agent/orchestration_config_tests.rs @@ -36,6 +36,7 @@ fn make_request(model: &str, harness: &str, remote: bool) -> RunAgentsRequest { name: "a".to_string(), prompt: String::new(), title: String::new(), + agent_identity_uid: String::new(), }], plan_id: String::new(), harness_auth_secret_name: None, diff --git a/review.json b/review.json deleted file mode 100644 index e69de29bb2d..00000000000