1pub(super) fn build_turn_argv(
14 base: &[String],
15 resume_strategy: &str,
16 resume_flag: &str,
17 session_id: Option<&str>,
18) -> Result<Vec<String>, String> {
19 let session_id = session_id.filter(|sid| !sid.trim().is_empty());
20
21 match resume_strategy {
22 "codex-exec" => build_codex_argv(base, session_id),
23 _ if session_id.is_none() => Ok(base.to_vec()),
24 "" => build_legacy_argv(base, resume_flag, session_id.unwrap()),
25 "none" => Ok(base.to_vec()),
26 "flag" => {
27 if resume_flag.is_empty() {
28 return Err("resume strategy 'flag' requires a non-empty resume flag".to_string());
29 }
30 let mut argv = base.to_vec();
31 argv.push(resume_flag.to_string());
32 argv.push(session_id.unwrap().to_string());
33 Ok(argv)
34 }
35 other => Err(format!("unsupported resume strategy '{other}'")),
36 }
37}
38
39pub(super) const MAX_PROMPT_ARG_UNITS: usize = 30_000;
43
44pub(super) fn append_prompt_arg(args: &mut Vec<String>, flag: &str, prompt: &str) -> Result<(), String> {
49 let units = prompt.encode_utf16().count();
50 if units > MAX_PROMPT_ARG_UNITS {
51 return Err(format!(
52 "This message is too long for this agent's CLI, which takes it on the command line: {units} characters, over the {MAX_PROMPT_ARG_UNITS} limit. Shorten it, or attach the long part as a file."
53 ));
54 }
55 args.push(flag.to_string());
56 args.push(prompt.to_string());
57 Ok(())
58}
59
60pub(super) fn args_for_log(args: &[String], prompt_in_argv: bool) -> Vec<String> {
64 let mut out = args.to_vec();
65 if prompt_in_argv {
66 if let Some(prompt) = out.last_mut() {
67 *prompt = format!("<prompt: {} chars>", prompt.chars().count());
68 }
69 }
70 out
71}
72
73fn build_legacy_argv(
74 base: &[String],
75 resume_flag: &str,
76 session_id: &str,
77) -> Result<Vec<String>, String> {
78 if resume_flag.is_empty() {
79 return Ok(base.to_vec());
80 }
81 let mut argv = base.to_vec();
82 argv.push(resume_flag.to_string());
83 argv.push(session_id.to_string());
84 Ok(argv)
85}
86
87pub(crate) fn build_side_question_argv(base_one_shot_args: &[String]) -> Vec<String> {
115 let mut argv = base_one_shot_args.to_vec();
116 argv.push("--disallowedTools".to_string());
117 argv.push("*".to_string());
118 argv.push("--max-turns".to_string());
119 argv.push("1".to_string());
120 argv
121}
122
123const CODEX_TOOL_FEATURES: &[&str] = &[
130 "shell_tool",
131 "unified_exec",
132 "view_image",
133 "multi_agent",
134 "goals",
135 "apps",
136 "plugins",
137 "browser_use",
138 "computer_use",
139 "image_generation",
140 "sleep_tool",
141 "tool_suggest",
142];
143
144pub(crate) fn build_codex_side_question_argv(model: Option<&str>) -> Vec<String> {
160 let mut argv: Vec<String> = [
161 "exec",
162 "--json",
163 "--ignore-user-config",
164 "--ephemeral",
165 "--skip-git-repo-check",
166 "--sandbox",
167 "read-only",
168 ]
169 .iter()
170 .map(|s| s.to_string())
171 .collect();
172 for feature in CODEX_TOOL_FEATURES {
173 argv.push("--disable".to_string());
174 argv.push(feature.to_string());
175 }
176 argv.push("-c".to_string());
177 argv.push("web_search=\"disabled\"".to_string());
178 if let Some(model) = model {
179 argv.push("-m".to_string());
180 argv.push(model.to_string());
181 }
182 argv.push("-".to_string());
184 argv
185}
186
187pub(crate) const GEMINI_DENY_ALL_TOOLS_POLICY: &str = "\
192[[rule]]
193toolName = \"*\"
194decision = \"deny\"
195priority = 999
196
197[[rule]]
198toolName = \"*\"
199mcpName = \"*\"
200decision = \"deny\"
201priority = 999
202";
203
204pub(crate) fn build_gemini_side_question_argv(policy_path: &str, model: Option<&str>) -> Vec<String> {
210 let mut argv: Vec<String> = [
211 "--output-format",
212 "stream-json",
213 "--approval-mode",
214 "default",
215 "--policy",
216 policy_path,
217 ]
218 .iter()
219 .map(|s| s.to_string())
220 .collect();
221 if let Some(model) = model {
222 argv.push("-m".to_string());
223 argv.push(model.to_string());
224 }
225 argv.push("-p".to_string());
227 argv.push(String::new());
228 argv
229}
230
231pub(crate) fn model_flag_value(args: &[String]) -> Option<String> {
233 let mut it = args.iter();
234 while let Some(arg) = it.next() {
235 if arg == "-m" || arg == "--model" {
236 return it.next().filter(|v| !v.is_empty() && !v.starts_with('-')).cloned();
237 }
238 if let Some(v) = arg.strip_prefix("--model=") {
239 return Some(v.to_string()).filter(|v| !v.is_empty());
240 }
241 }
242 None
243}
244
245fn build_codex_argv(base: &[String], session_id: Option<&str>) -> Result<Vec<String>, String> {
246 let exec_index = base
247 .iter()
248 .position(|arg| arg == "exec")
249 .ok_or_else(|| "codex resume argv is missing the 'exec' subcommand".to_string())?;
250 let prompt_index = base
251 .iter()
252 .rposition(|arg| arg == "-")
253 .ok_or_else(|| "codex resume argv is missing the stdin prompt marker '-'".to_string())?;
254 if prompt_index <= exec_index {
255 return Err("codex resume argv has the stdin marker before 'exec'".to_string());
256 }
257
258 let mut argv = base.to_vec();
262 argv.remove(prompt_index);
263 if let Some(session_id) = session_id {
264 argv.insert(exec_index + 1, "resume".to_string());
265 argv.push(session_id.to_string());
266 }
267 argv.push("-".to_string());
268 Ok(argv)
269}
270
271#[cfg(test)]
272mod prompt_arg_tests {
273 use super::{append_prompt_arg, args_for_log, build_turn_argv, MAX_PROMPT_ARG_UNITS};
274
275 #[test]
276 fn the_logged_args_never_carry_the_prompt() {
277 let mut args = strings(&["--output-format", "stream-json"]);
278 append_prompt_arg(&mut args, "-p", "my token is abc123").unwrap();
279 let logged = args_for_log(&args, true);
280 assert_eq!(logged, strings(&["--output-format", "stream-json", "-p", "<prompt: 18 chars>"]));
281 assert!(!logged.iter().any(|a| a.contains("abc123")));
282 assert_eq!(args_for_log(&strings(&["-p", "--verbose"]), false), strings(&["-p", "--verbose"]));
284 }
285
286 fn strings(v: &[&str]) -> Vec<String> {
287 v.iter().map(|s| s.to_string()).collect()
288 }
289
290 #[test]
291 fn the_prompt_goes_last_after_the_resume_flag() {
292 let base = strings(&["--output-format", "stream-json"]);
293 let mut args = build_turn_argv(&base, "flag", "--conversation", Some("c1")).unwrap();
294 append_prompt_arg(&mut args, "-p", "hello & \"bye\"").unwrap();
295 assert_eq!(
296 args,
297 strings(&["--output-format", "stream-json", "--conversation", "c1", "-p", "hello & \"bye\""])
298 );
299 }
300
301 #[test]
302 fn a_prompt_too_long_for_the_command_line_is_refused_not_cut() {
303 let mut args = Vec::new();
304 assert!(append_prompt_arg(&mut args, "-p", &"x".repeat(MAX_PROMPT_ARG_UNITS)).is_ok());
305 let mut args = Vec::new();
306 let err = append_prompt_arg(&mut args, "-p", &"x".repeat(MAX_PROMPT_ARG_UNITS + 1)).unwrap_err();
307 assert!(err.contains("too long"), "{err}");
308 assert!(args.is_empty(), "nothing is appended for a refused prompt");
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::{
315 build_codex_side_question_argv, build_gemini_side_question_argv, build_side_question_argv,
316 build_turn_argv, model_flag_value, CODEX_TOOL_FEATURES,
317 };
318
319 fn strings(values: &[&str]) -> Vec<String> {
320 values.iter().map(|value| (*value).to_string()).collect()
321 }
322
323 #[test]
324 fn first_codex_turn_keeps_exec_argv_unchanged() {
325 let base = strings(&[
326 "exec",
327 "--json",
328 "--dangerously-bypass-approvals-and-sandbox",
329 "-",
330 ]);
331 assert_eq!(
332 build_turn_argv(&base, "codex-exec", "", None).unwrap(),
333 base,
334 );
335 }
336
337 #[test]
338 fn first_codex_turn_moves_appended_provider_flags_before_stdin() {
339 let base = strings(&["exec", "--json", "-", "--model", "gpt-5.4"]);
340 assert_eq!(
341 build_turn_argv(&base, "codex-exec", "", None).unwrap(),
342 strings(&["exec", "--json", "--model", "gpt-5.4", "-"]),
343 );
344 }
345
346 #[test]
347 fn resumed_codex_turn_inserts_subcommand_and_session_before_stdin() {
348 let base = strings(&[
349 "exec",
350 "--json",
351 "--dangerously-bypass-approvals-and-sandbox",
352 "-",
353 ]);
354 assert_eq!(
355 build_turn_argv(
356 &base,
357 "codex-exec",
358 "",
359 Some("00000000-0000-0000-0000-000000000005")
360 )
361 .unwrap(),
362 strings(&[
363 "exec",
364 "resume",
365 "--json",
366 "--dangerously-bypass-approvals-and-sandbox",
367 "00000000-0000-0000-0000-000000000005",
368 "-",
369 ]),
370 );
371 }
372
373 #[test]
374 fn container_codex_argv_preserves_executable_prefix() {
375 let base = strings(&["codex", "exec", "--json", "-"]);
376 assert_eq!(
377 build_turn_argv(&base, "codex-exec", "", Some("thread-name")).unwrap(),
378 strings(&["codex", "exec", "resume", "--json", "thread-name", "-"]),
379 );
380 }
381
382 #[test]
383 fn flag_resume_remains_backward_compatible() {
384 let base = strings(&["-p", "--output-format", "stream-json"]);
385 assert_eq!(
386 build_turn_argv(&base, "flag", "--resume", Some("claude-session")).unwrap(),
387 strings(&[
388 "-p",
389 "--output-format",
390 "stream-json",
391 "--resume",
392 "claude-session"
393 ]),
394 );
395 }
396
397 #[test]
398 fn malformed_codex_argv_fails_instead_of_starting_a_fresh_turn() {
399 let base = strings(&["exec", "--json"]);
400 assert!(build_turn_argv(&base, "codex-exec", "", Some("thread-id"))
401 .unwrap_err()
402 .contains("stdin prompt marker"));
403 }
404
405 #[test]
408 fn side_question_argv_disables_every_tool_and_caps_turns() {
409 let base = strings(&["-p", "--output-format", "stream-json", "--verbose"]);
410 let got = build_side_question_argv(&base);
411 assert_eq!(
412 got,
413 strings(&[
414 "-p",
415 "--output-format",
416 "stream-json",
417 "--verbose",
418 "--disallowedTools",
419 "*",
420 "--max-turns",
421 "1",
422 ]),
423 );
424 }
425
426 #[test]
427 fn side_question_argv_never_contains_resume() {
428 let base = strings(&["-p", "--output-format", "stream-json"]);
433 let got = build_side_question_argv(&base);
434 assert!(!got.iter().any(|a| a == "--resume"));
435 }
436
437 #[test]
438 fn side_question_argv_preserves_base_args_verbatim_and_in_order() {
439 let base = strings(&["-p", "--model", "opus", "--my-custom-flag", "42"]);
440 let got = build_side_question_argv(&base);
441 assert_eq!(&got[..base.len()], base.as_slice());
442 }
443
444 #[test]
445 fn side_question_argv_composes_with_build_turn_argv_resume_free() {
446 let base = strings(&["-p", "--output-format", "stream-json"]);
453 let augmented = build_side_question_argv(&base);
454 let final_argv = build_turn_argv(&augmented, "flag", "--resume", None).unwrap();
455 assert_eq!(final_argv, augmented);
456 assert!(!final_argv.iter().any(|a| a == "--resume"));
457 assert!(final_argv.iter().any(|a| a == "--disallowedTools"));
458 assert!(final_argv.iter().any(|a| a == "--max-turns"));
459 }
460
461 #[test]
464 fn the_codex_side_question_is_tool_less_and_keeps_stdin_last() {
465 let got = build_codex_side_question_argv(Some("gpt-5"));
466 assert_eq!(&got[..2], &strings(&["exec", "--json"])[..]);
467 for flag in ["--ignore-user-config", "--ephemeral", "--skip-git-repo-check"] {
468 assert!(got.iter().any(|a| a == flag), "{flag}");
469 }
470 assert!(got.windows(2).any(|w| w == strings(&["--sandbox", "read-only"])));
471 for feature in CODEX_TOOL_FEATURES {
472 assert!(got.windows(2).any(|w| w[0] == "--disable" && w[1] == *feature), "{feature}");
473 }
474 assert!(got.windows(2).any(|w| w == strings(&["-c", "web_search=\"disabled\""])));
475 assert!(got.windows(2).any(|w| w == strings(&["-m", "gpt-5"])));
476 assert!(!got.iter().any(|a| a.contains("dangerously")));
477 assert_eq!(got.last().map(String::as_str), Some("-"));
478 assert!(!build_codex_side_question_argv(None).iter().any(|a| a == "-m"));
479 }
480
481 #[test]
482 fn the_gemini_side_question_denies_every_tool_without_yolo() {
483 let got = build_gemini_side_question_argv("/data/btw/gemini.toml", Some("gemini-3-pro"));
484 assert!(got.windows(2).any(|w| w == strings(&["--policy", "/data/btw/gemini.toml"])));
485 assert!(got.windows(2).any(|w| w == strings(&["--approval-mode", "default"])));
486 assert!(got.windows(2).any(|w| w == strings(&["--output-format", "stream-json"])));
487 assert!(got.windows(2).any(|w| w == strings(&["-m", "gemini-3-pro"])));
488 for flag in ["--yolo", "-y", "--skip-trust"] {
489 assert!(!got.iter().any(|a| a == flag), "{flag}");
490 }
491 assert_eq!(&got[got.len() - 2..], &strings(&["-p", ""])[..]);
492 }
493
494 #[test]
495 fn the_pane_model_is_read_from_either_flag_spelling() {
496 assert_eq!(model_flag_value(&strings(&["exec", "-m", "o3", "-"])), Some("o3".into()));
497 assert_eq!(model_flag_value(&strings(&["--model", "x"])), Some("x".into()));
498 assert_eq!(model_flag_value(&strings(&["--model=y", "-p", ""])), Some("y".into()));
499 assert_eq!(model_flag_value(&strings(&["-p", "", "--yolo"])), None);
500 assert_eq!(model_flag_value(&strings(&["-m"])), None);
501 }
502}