|
| 1 | +use crate::cc_switch; |
| 2 | +use crate::manager::acp::mode_normalize::normalize_requested_mode; |
| 3 | +use crate::shared_kernel::PersistedSessionState; |
| 4 | +use aionui_api_types::{AcpBuildExtra, AgentMetadata}; |
| 5 | +use aionui_common::CommandSpec; |
| 6 | + |
| 7 | +const CODEX_CONFIG_FLAG: &str = "-c"; |
| 8 | +const CODEX_ENV_POLICY_INHERIT_ALL: &str = "shell_environment_policy.inherit=all"; |
| 9 | +const CODEX_ENV_POLICY_CLEAR_INCLUDE_ONLY: &str = "shell_environment_policy.include_only=[]"; |
| 10 | +const CODEX_WINDOWS_UNELEVATED_SANDBOX: &str = "windows.sandbox=\"unelevated\""; |
| 11 | + |
| 12 | +pub(super) struct AcpLaunchPolicyInput<'a> { |
| 13 | + pub metadata: &'a AgentMetadata, |
| 14 | + pub config: &'a AcpBuildExtra, |
| 15 | + pub session_snapshot: Option<&'a PersistedSessionState>, |
| 16 | + pub runtime_env: &'a [(String, String)], |
| 17 | +} |
| 18 | + |
| 19 | +pub(super) fn apply_acp_launch_policy(command_spec: &mut CommandSpec, input: AcpLaunchPolicyInput<'_>) { |
| 20 | + apply_codex_runtime_config_args( |
| 21 | + command_spec, |
| 22 | + input.metadata, |
| 23 | + initial_mode_from_build_context(input.metadata, input.config, input.session_snapshot).as_deref(), |
| 24 | + ); |
| 25 | + append_runtime_env(command_spec, input.runtime_env); |
| 26 | + append_claude_provider_env(command_spec, input.metadata); |
| 27 | +} |
| 28 | + |
| 29 | +fn append_runtime_env(command_spec: &mut CommandSpec, runtime_env: &[(String, String)]) { |
| 30 | + for (name, value) in runtime_env { |
| 31 | + command_spec.env.push(aionui_common::EnvVar { |
| 32 | + name: name.clone(), |
| 33 | + value: value.clone(), |
| 34 | + }); |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +fn append_claude_provider_env(command_spec: &mut CommandSpec, metadata: &AgentMetadata) { |
| 39 | + if metadata.backend.as_deref() != Some("claude") { |
| 40 | + return; |
| 41 | + } |
| 42 | + |
| 43 | + let cc_switch_env = cc_switch::read_claude_provider_env(); |
| 44 | + if cc_switch_env.is_empty() { |
| 45 | + return; |
| 46 | + } |
| 47 | + |
| 48 | + let keys: Vec<&str> = cc_switch_env.keys().map(|key| key.as_str()).collect(); |
| 49 | + for (name, value) in &cc_switch_env { |
| 50 | + command_spec.env.push(aionui_common::EnvVar { |
| 51 | + name: name.clone(), |
| 52 | + value: value.clone(), |
| 53 | + }); |
| 54 | + } |
| 55 | + tracing::info!(?keys, "cc-switch: env vars injected"); |
| 56 | +} |
| 57 | + |
| 58 | +fn initial_mode_from_build_context( |
| 59 | + metadata: &AgentMetadata, |
| 60 | + config: &AcpBuildExtra, |
| 61 | + session_snapshot: Option<&PersistedSessionState>, |
| 62 | +) -> Option<String> { |
| 63 | + session_snapshot |
| 64 | + .and_then(|snapshot| snapshot.current_mode_id.as_ref()) |
| 65 | + .map(|mode| normalize_requested_mode(metadata, mode.as_str())) |
| 66 | + .or_else(|| { |
| 67 | + config |
| 68 | + .session_mode |
| 69 | + .as_ref() |
| 70 | + .map(|mode| normalize_requested_mode(metadata, mode)) |
| 71 | + }) |
| 72 | + .filter(|mode| !mode.is_empty()) |
| 73 | +} |
| 74 | + |
| 75 | +fn apply_codex_runtime_config_args( |
| 76 | + command_spec: &mut CommandSpec, |
| 77 | + metadata: &AgentMetadata, |
| 78 | + initial_mode: Option<&str>, |
| 79 | +) { |
| 80 | + if metadata.backend.as_deref() != Some("codex") { |
| 81 | + return; |
| 82 | + } |
| 83 | + |
| 84 | + push_codex_config_arg(command_spec, CODEX_ENV_POLICY_INHERIT_ALL); |
| 85 | + push_codex_config_arg(command_spec, CODEX_ENV_POLICY_CLEAR_INCLUDE_ONLY); |
| 86 | + |
| 87 | + let sandbox_mode = codex_sandbox_mode_for_requested_mode(initial_mode); |
| 88 | + push_codex_config_arg(command_spec, &format!("sandbox_mode=\"{sandbox_mode}\"")); |
| 89 | + if sandbox_mode == "danger-full-access" { |
| 90 | + push_codex_config_arg(command_spec, CODEX_WINDOWS_UNELEVATED_SANDBOX); |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +fn push_codex_config_arg(command_spec: &mut CommandSpec, value: &str) { |
| 95 | + command_spec.args.push(CODEX_CONFIG_FLAG.to_owned()); |
| 96 | + command_spec.args.push(value.to_owned()); |
| 97 | +} |
| 98 | + |
| 99 | +fn codex_sandbox_mode_for_requested_mode(mode: Option<&str>) -> &'static str { |
| 100 | + match mode.map(str::trim) { |
| 101 | + Some("full-access" | "yoloNoSandbox") => "danger-full-access", |
| 102 | + _ => "workspace-write", |
| 103 | + } |
| 104 | +} |
| 105 | + |
| 106 | +#[cfg(test)] |
| 107 | +mod tests { |
| 108 | + use super::*; |
| 109 | + |
| 110 | + fn agent_metadata_with_backend(backend: Option<&str>) -> AgentMetadata { |
| 111 | + AgentMetadata { |
| 112 | + id: "agent-1".into(), |
| 113 | + icon: None, |
| 114 | + name: "Test ACP".into(), |
| 115 | + name_i18n: None, |
| 116 | + description: None, |
| 117 | + description_i18n: None, |
| 118 | + backend: backend.map(str::to_owned), |
| 119 | + agent_type: aionui_common::AgentType::Acp, |
| 120 | + agent_source: aionui_api_types::AgentSource::Builtin, |
| 121 | + agent_source_info: aionui_api_types::AgentSourceInfo::default(), |
| 122 | + enabled: true, |
| 123 | + available: true, |
| 124 | + command: None, |
| 125 | + resolved_command: None, |
| 126 | + args: vec![], |
| 127 | + env: vec![], |
| 128 | + native_skills_dirs: None, |
| 129 | + behavior_policy: aionui_api_types::BehaviorPolicy::default(), |
| 130 | + yolo_id: Some("full-access".into()), |
| 131 | + sort_order: 0, |
| 132 | + team_capable: false, |
| 133 | + last_check_status: None, |
| 134 | + last_check_kind: None, |
| 135 | + last_check_error_code: None, |
| 136 | + last_check_error_message: None, |
| 137 | + last_check_error_details: None, |
| 138 | + last_check_guidance: None, |
| 139 | + last_check_latency_ms: None, |
| 140 | + last_check_at: None, |
| 141 | + last_success_at: None, |
| 142 | + last_failure_at: None, |
| 143 | + handshake: aionui_api_types::AgentHandshake::default(), |
| 144 | + has_command_override: false, |
| 145 | + env_override_key_count: 0, |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + #[test] |
| 150 | + fn apply_acp_launch_policy_adds_runtime_env_and_codex_full_access_config() { |
| 151 | + let mut command_spec = CommandSpec { |
| 152 | + command: "node".into(), |
| 153 | + args: vec!["codex-acp.js".into()], |
| 154 | + env: vec![], |
| 155 | + cwd: None, |
| 156 | + }; |
| 157 | + let metadata = agent_metadata_with_backend(Some("codex")); |
| 158 | + let config = AcpBuildExtra { |
| 159 | + session_mode: Some("full-access".into()), |
| 160 | + ..Default::default() |
| 161 | + }; |
| 162 | + |
| 163 | + apply_acp_launch_policy( |
| 164 | + &mut command_spec, |
| 165 | + AcpLaunchPolicyInput { |
| 166 | + metadata: &metadata, |
| 167 | + config: &config, |
| 168 | + session_snapshot: None, |
| 169 | + runtime_env: &[("AIONUI_CONVERSATION_ID".into(), "conv-1".into())], |
| 170 | + }, |
| 171 | + ); |
| 172 | + |
| 173 | + assert_eq!( |
| 174 | + command_spec.args, |
| 175 | + vec![ |
| 176 | + "codex-acp.js", |
| 177 | + "-c", |
| 178 | + "shell_environment_policy.inherit=all", |
| 179 | + "-c", |
| 180 | + "shell_environment_policy.include_only=[]", |
| 181 | + "-c", |
| 182 | + "sandbox_mode=\"danger-full-access\"", |
| 183 | + "-c", |
| 184 | + "windows.sandbox=\"unelevated\"", |
| 185 | + ] |
| 186 | + ); |
| 187 | + assert!( |
| 188 | + command_spec |
| 189 | + .env |
| 190 | + .iter() |
| 191 | + .any(|entry| entry.name == "AIONUI_CONVERSATION_ID" && entry.value == "conv-1") |
| 192 | + ); |
| 193 | + } |
| 194 | + |
| 195 | + #[test] |
| 196 | + fn apply_acp_launch_policy_skips_codex_config_for_non_codex_agents() { |
| 197 | + let mut command_spec = CommandSpec { |
| 198 | + command: "node".into(), |
| 199 | + args: vec!["claude-agent-acp.js".into()], |
| 200 | + env: vec![], |
| 201 | + cwd: None, |
| 202 | + }; |
| 203 | + let metadata = agent_metadata_with_backend(Some("claude")); |
| 204 | + let config = AcpBuildExtra::default(); |
| 205 | + |
| 206 | + apply_acp_launch_policy( |
| 207 | + &mut command_spec, |
| 208 | + AcpLaunchPolicyInput { |
| 209 | + metadata: &metadata, |
| 210 | + config: &config, |
| 211 | + session_snapshot: None, |
| 212 | + runtime_env: &[], |
| 213 | + }, |
| 214 | + ); |
| 215 | + |
| 216 | + assert_eq!(command_spec.args, vec!["claude-agent-acp.js"]); |
| 217 | + } |
| 218 | + |
| 219 | + #[test] |
| 220 | + fn initial_mode_from_build_context_prefers_persisted_snapshot() { |
| 221 | + let mut snapshot = PersistedSessionState::default(); |
| 222 | + snapshot.current_mode_id = Some(crate::shared_kernel::ModeId::new("full-access")); |
| 223 | + let config = AcpBuildExtra { |
| 224 | + session_mode: Some("auto".into()), |
| 225 | + ..Default::default() |
| 226 | + }; |
| 227 | + |
| 228 | + let mode = |
| 229 | + initial_mode_from_build_context(&agent_metadata_with_backend(Some("codex")), &config, Some(&snapshot)); |
| 230 | + |
| 231 | + assert_eq!(mode.as_deref(), Some("full-access")); |
| 232 | + } |
| 233 | +} |
0 commit comments