1use crate::Codex;
2use crate::command::CodexCommand;
3#[cfg(feature = "json")]
4use crate::error::Error;
5use crate::error::Result;
6use crate::exec::{self, CommandOutput};
7use crate::types::{ApprovalPolicyConfig, Color, SandboxMode, WebSearchMode};
8#[cfg(feature = "json")]
9use crate::types::{JsonLineEvent, QueryResult};
10
11pub(crate) fn push_typed_config(
18 args: &mut Vec<String>,
19 approval_policy: Option<ApprovalPolicyConfig>,
20 web_search: Option<WebSearchMode>,
21) {
22 if let Some(policy) = approval_policy {
23 args.push("-c".into());
24 args.push(format!("approval_policy=\"{}\"", policy.as_config_value()));
25 }
26 if let Some(mode) = web_search {
27 args.push("-c".into());
28 args.push(format!("web_search=\"{}\"", mode.as_config_value()));
29 }
30}
31
32pub(crate) fn effective_sandbox(
38 sandbox: Option<SandboxMode>,
39 full_auto: bool,
40) -> Option<SandboxMode> {
41 sandbox.or(full_auto.then_some(SandboxMode::WorkspaceWrite))
42}
43
44#[derive(Debug, Clone)]
68pub struct ExecCommand {
69 approve_for_me: bool,
70 prompt: Option<String>,
71 prompt_via_stdin: bool,
72 approval_policy: Option<ApprovalPolicyConfig>,
73 web_search: Option<WebSearchMode>,
74 config_overrides: Vec<String>,
75 enabled_features: Vec<String>,
76 disabled_features: Vec<String>,
77 images: Vec<String>,
78 model: Option<String>,
79 oss: bool,
80 local_provider: Option<String>,
81 sandbox: Option<SandboxMode>,
82 strict_config: bool,
83 dangerously_bypass_hook_trust: bool,
84 ignore_user_config: bool,
85 ignore_rules: bool,
86 profile: Option<String>,
87 full_auto: bool,
88 dangerously_bypass_approvals_and_sandbox: bool,
89 cd: Option<String>,
90 skip_git_repo_check: bool,
91 add_dirs: Vec<String>,
92 ephemeral: bool,
93 output_schema: Option<String>,
94 color: Option<Color>,
95 json: bool,
96 output_last_message: Option<String>,
97 retry_policy: Option<crate::retry::RetryPolicy>,
98}
99
100impl ExecCommand {
101 #[must_use]
103 pub fn new(prompt: impl Into<String>) -> Self {
104 Self {
105 approve_for_me: false,
106 prompt: Some(prompt.into()),
107 prompt_via_stdin: false,
108 approval_policy: None,
109 web_search: None,
110 config_overrides: Vec::new(),
111 enabled_features: Vec::new(),
112 disabled_features: Vec::new(),
113 images: Vec::new(),
114 model: None,
115 oss: false,
116 local_provider: None,
117 sandbox: None,
118 strict_config: false,
119 dangerously_bypass_hook_trust: false,
120 ignore_user_config: false,
121 ignore_rules: false,
122 profile: None,
123 full_auto: false,
124 dangerously_bypass_approvals_and_sandbox: false,
125 cd: None,
126 skip_git_repo_check: false,
127 add_dirs: Vec::new(),
128 ephemeral: false,
129 output_schema: None,
130 color: None,
131 json: false,
132 output_last_message: None,
133 retry_policy: None,
134 }
135 }
136
137 #[must_use]
161 pub fn from_stdin(prompt: impl Into<String>) -> Self {
162 Self::new(prompt).prompt_via_stdin()
163 }
164
165 #[must_use]
176 pub fn prompt_via_stdin(mut self) -> Self {
177 self.prompt_via_stdin = true;
178 self
179 }
180
181 #[cfg(feature = "json")]
186 pub(crate) fn stdin_prompt(&self) -> Option<&str> {
187 self.prompt_via_stdin
188 .then(|| self.prompt.as_deref().unwrap_or_default())
189 }
190
191 #[must_use]
198 pub fn config(mut self, key_value: impl Into<String>) -> Self {
199 self.config_overrides.push(key_value.into());
200 self
201 }
202
203 #[must_use]
219 pub fn approval_policy(mut self, policy: impl Into<ApprovalPolicyConfig>) -> Self {
220 self.approval_policy = Some(policy.into());
221 self
222 }
223
224 #[must_use]
229 pub fn search(self) -> Self {
230 self.search_mode(WebSearchMode::Live)
231 }
232
233 #[must_use]
239 pub fn search_mode(mut self, mode: WebSearchMode) -> Self {
240 self.web_search = Some(mode);
241 self
242 }
243
244 #[must_use]
248 pub fn enable(mut self, feature: impl Into<String>) -> Self {
249 self.enabled_features.push(feature.into());
250 self
251 }
252
253 #[must_use]
257 pub fn disable(mut self, feature: impl Into<String>) -> Self {
258 self.disabled_features.push(feature.into());
259 self
260 }
261
262 #[must_use]
266 pub fn image(mut self, path: impl Into<String>) -> Self {
267 self.images.push(path.into());
268 self
269 }
270
271 #[must_use]
275 pub fn model(mut self, model: impl Into<String>) -> Self {
276 let model = model.into();
277 assert!(!model.is_empty(), "model name must not be empty");
278 self.model = Some(model);
279 self
280 }
281
282 #[must_use]
284 pub fn oss(mut self) -> Self {
285 self.oss = true;
286 self
287 }
288
289 #[must_use]
291 pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
292 self.local_provider = Some(provider.into());
293 self
294 }
295
296 #[must_use]
298 pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
299 self.sandbox = Some(sandbox);
300 self
301 }
302
303 #[must_use]
305 pub fn strict_config(mut self) -> Self {
306 self.strict_config = true;
307 self
308 }
309
310 #[must_use]
314 pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
315 self.dangerously_bypass_hook_trust = true;
316 self
317 }
318
319 #[must_use]
321 pub fn ignore_user_config(mut self) -> Self {
322 self.ignore_user_config = true;
323 self
324 }
325
326 #[must_use]
328 pub fn ignore_rules(mut self) -> Self {
329 self.ignore_rules = true;
330 self
331 }
332
333 #[must_use]
335 pub fn profile(mut self, profile: impl Into<String>) -> Self {
336 self.profile = Some(profile.into());
337 self
338 }
339
340 #[must_use]
352 pub fn full_auto(mut self) -> Self {
353 self.full_auto = true;
354 self
355 }
356
357 #[must_use]
365 pub fn approve_for_me(mut self) -> Self {
366 self.approve_for_me = true;
367 self
368 }
369
370 #[must_use]
374 pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
375 self.dangerously_bypass_approvals_and_sandbox = true;
376 self
377 }
378
379 #[must_use]
381 pub fn cd(mut self, dir: impl Into<String>) -> Self {
382 self.cd = Some(dir.into());
383 self
384 }
385
386 #[must_use]
388 pub fn skip_git_repo_check(mut self) -> Self {
389 self.skip_git_repo_check = true;
390 self
391 }
392
393 #[must_use]
397 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
398 self.add_dirs.push(dir.into());
399 self
400 }
401
402 #[must_use]
404 pub fn ephemeral(mut self) -> Self {
405 self.ephemeral = true;
406 self
407 }
408
409 #[must_use]
411 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
412 self.output_schema = Some(path.into());
413 self
414 }
415
416 #[must_use]
418 pub fn color(mut self, color: Color) -> Self {
419 self.color = Some(color);
420 self
421 }
422
423 #[must_use]
429 pub fn json(mut self) -> Self {
430 self.json = true;
431 self
432 }
433
434 #[must_use]
436 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
437 self.output_last_message = Some(path.into());
438 self
439 }
440
441 #[must_use]
445 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
446 self.retry_policy = Some(policy);
447 self
448 }
449
450 #[cfg(feature = "json")]
473 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
474 where
475 F: FnMut(JsonLineEvent),
476 {
477 crate::streaming::stream_exec(codex, self, handler).await
478 }
479
480 #[cfg(feature = "json")]
485 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
486 let mut args = self.args();
487 if !self.json {
488 args.push("--json".into());
489 }
490
491 let output = if self.prompt_via_stdin {
492 let prompt = self.prompt.as_deref().unwrap_or_default();
493 exec::run_codex_with_stdin_prompt(codex, args, prompt).await?
494 } else {
495 exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?
496 };
497 parse_json_lines(&output.stdout)
498 }
499
500 #[cfg(feature = "json")]
506 pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
507 let events = self.execute_json_lines(codex).await?;
508 Ok(QueryResult::from_events(events))
509 }
510}
511
512impl CodexCommand for ExecCommand {
513 type Output = CommandOutput;
514
515 fn args(&self) -> Vec<String> {
516 let mut args = vec!["exec".to_string()];
517
518 push_typed_config(&mut args, self.approval_policy, self.web_search);
519 push_repeat(&mut args, "-c", &self.config_overrides);
520 push_repeat(&mut args, "--enable", &self.enabled_features);
521 push_repeat(&mut args, "--disable", &self.disabled_features);
522 push_repeat(&mut args, "--image", &self.images);
523
524 if let Some(model) = &self.model {
525 args.push("--model".into());
526 args.push(model.clone());
527 }
528 if self.oss {
529 args.push("--oss".into());
530 }
531 if let Some(local_provider) = &self.local_provider {
532 args.push("--local-provider".into());
533 args.push(local_provider.clone());
534 }
535 if let Some(sandbox) = effective_sandbox(self.sandbox, self.full_auto) {
536 args.push("--sandbox".into());
537 args.push(sandbox.as_arg().into());
538 }
539 if self.strict_config {
540 args.push("--strict-config".into());
541 }
542 if let Some(profile) = &self.profile {
543 args.push("--profile".into());
544 args.push(profile.clone());
545 }
546 if self.approve_for_me {
547 args.push("--approve-for-me".into());
548 }
549 if self.dangerously_bypass_approvals_and_sandbox {
550 args.push("--dangerously-bypass-approvals-and-sandbox".into());
551 }
552 if self.dangerously_bypass_hook_trust {
553 args.push("--dangerously-bypass-hook-trust".into());
554 }
555 if let Some(cd) = &self.cd {
556 args.push("--cd".into());
557 args.push(cd.clone());
558 }
559 if self.skip_git_repo_check {
560 args.push("--skip-git-repo-check".into());
561 }
562 push_repeat(&mut args, "--add-dir", &self.add_dirs);
563 if self.ephemeral {
564 args.push("--ephemeral".into());
565 }
566 if self.ignore_user_config {
567 args.push("--ignore-user-config".into());
568 }
569 if self.ignore_rules {
570 args.push("--ignore-rules".into());
571 }
572 if let Some(output_schema) = &self.output_schema {
573 args.push("--output-schema".into());
574 args.push(output_schema.clone());
575 }
576 if let Some(color) = self.color {
577 args.push("--color".into());
578 args.push(color.as_arg().into());
579 }
580 if self.json {
581 args.push("--json".into());
582 }
583 if let Some(path) = &self.output_last_message {
584 args.push("--output-last-message".into());
585 args.push(path.clone());
586 }
587 if self.prompt_via_stdin {
588 args.push("-".into());
591 } else if let Some(prompt) = &self.prompt {
592 args.push(prompt.clone());
593 }
594
595 args
596 }
597
598 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
599 if self.prompt_via_stdin {
600 let prompt = self.prompt.as_deref().unwrap_or_default();
601 return exec::run_codex_with_stdin_prompt(codex, self.args(), prompt).await;
602 }
603 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
604 }
605}
606
607#[derive(Debug, Clone)]
612pub struct ExecResumeCommand {
613 session_id: Option<String>,
614 prompt: Option<String>,
615 last: bool,
616 all: bool,
617 approval_policy: Option<ApprovalPolicyConfig>,
618 web_search: Option<WebSearchMode>,
619 config_overrides: Vec<String>,
620 enabled_features: Vec<String>,
621 disabled_features: Vec<String>,
622 images: Vec<String>,
623 model: Option<String>,
624 strict_config: bool,
625 dangerously_bypass_hook_trust: bool,
626 ignore_user_config: bool,
627 ignore_rules: bool,
628 output_schema: Option<String>,
629 full_auto: bool,
630 dangerously_bypass_approvals_and_sandbox: bool,
631 skip_git_repo_check: bool,
632 ephemeral: bool,
633 json: bool,
634 output_last_message: Option<String>,
635 retry_policy: Option<crate::retry::RetryPolicy>,
636}
637
638impl ExecResumeCommand {
639 #[must_use]
641 pub fn new() -> Self {
642 Self {
643 session_id: None,
644 prompt: None,
645 last: false,
646 all: false,
647 approval_policy: None,
648 web_search: None,
649 config_overrides: Vec::new(),
650 enabled_features: Vec::new(),
651 disabled_features: Vec::new(),
652 images: Vec::new(),
653 model: None,
654 strict_config: false,
655 dangerously_bypass_hook_trust: false,
656 ignore_user_config: false,
657 ignore_rules: false,
658 output_schema: None,
659 full_auto: false,
660 dangerously_bypass_approvals_and_sandbox: false,
661 skip_git_repo_check: false,
662 ephemeral: false,
663 json: false,
664 output_last_message: None,
665 retry_policy: None,
666 }
667 }
668
669 #[must_use]
671 pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
672 self.session_id = Some(session_id.into());
673 self
674 }
675
676 #[must_use]
678 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
679 self.prompt = Some(prompt.into());
680 self
681 }
682
683 #[must_use]
685 pub fn last(mut self) -> Self {
686 self.last = true;
687 self
688 }
689
690 #[must_use]
692 pub fn all(mut self) -> Self {
693 self.all = true;
694 self
695 }
696
697 #[must_use]
701 pub fn model(mut self, model: impl Into<String>) -> Self {
702 let model = model.into();
703 assert!(!model.is_empty(), "model name must not be empty");
704 self.model = Some(model);
705 self
706 }
707
708 #[must_use]
712 pub fn image(mut self, path: impl Into<String>) -> Self {
713 self.images.push(path.into());
714 self
715 }
716
717 #[must_use]
719 pub fn json(mut self) -> Self {
720 self.json = true;
721 self
722 }
723
724 #[must_use]
726 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
727 self.output_last_message = Some(path.into());
728 self
729 }
730
731 #[must_use]
738 pub fn config(mut self, key_value: impl Into<String>) -> Self {
739 self.config_overrides.push(key_value.into());
740 self
741 }
742
743 #[must_use]
750 pub fn approval_policy(mut self, policy: impl Into<ApprovalPolicyConfig>) -> Self {
751 self.approval_policy = Some(policy.into());
752 self
753 }
754
755 #[must_use]
760 pub fn search(self) -> Self {
761 self.search_mode(WebSearchMode::Live)
762 }
763
764 #[must_use]
770 pub fn search_mode(mut self, mode: WebSearchMode) -> Self {
771 self.web_search = Some(mode);
772 self
773 }
774
775 #[must_use]
779 pub fn enable(mut self, feature: impl Into<String>) -> Self {
780 self.enabled_features.push(feature.into());
781 self
782 }
783
784 #[must_use]
788 pub fn disable(mut self, feature: impl Into<String>) -> Self {
789 self.disabled_features.push(feature.into());
790 self
791 }
792
793 #[must_use]
795 pub fn strict_config(mut self) -> Self {
796 self.strict_config = true;
797 self
798 }
799
800 #[must_use]
804 pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
805 self.dangerously_bypass_hook_trust = true;
806 self
807 }
808
809 #[must_use]
811 pub fn ignore_user_config(mut self) -> Self {
812 self.ignore_user_config = true;
813 self
814 }
815
816 #[must_use]
818 pub fn ignore_rules(mut self) -> Self {
819 self.ignore_rules = true;
820 self
821 }
822
823 #[must_use]
825 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
826 self.output_schema = Some(path.into());
827 self
828 }
829
830 #[must_use]
836 pub fn full_auto(mut self) -> Self {
837 self.full_auto = true;
838 self
839 }
840
841 #[must_use]
845 pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
846 self.dangerously_bypass_approvals_and_sandbox = true;
847 self
848 }
849
850 #[must_use]
852 pub fn skip_git_repo_check(mut self) -> Self {
853 self.skip_git_repo_check = true;
854 self
855 }
856
857 #[must_use]
859 pub fn ephemeral(mut self) -> Self {
860 self.ephemeral = true;
861 self
862 }
863
864 #[must_use]
868 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
869 self.retry_policy = Some(policy);
870 self
871 }
872
873 #[cfg(feature = "json")]
878 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
879 let mut args = self.args();
880 if !self.json {
881 args.push("--json".into());
882 }
883
884 let output = exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?;
885 parse_json_lines(&output.stdout)
886 }
887
888 #[cfg(feature = "json")]
893 pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
894 let events = self.execute_json_lines(codex).await?;
895 Ok(QueryResult::from_events(events))
896 }
897
898 #[cfg(feature = "json")]
904 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
905 where
906 F: FnMut(JsonLineEvent),
907 {
908 crate::streaming::stream_exec_resume(codex, self, handler).await
909 }
910}
911
912impl Default for ExecResumeCommand {
913 fn default() -> Self {
914 Self::new()
915 }
916}
917
918impl CodexCommand for ExecResumeCommand {
919 type Output = CommandOutput;
920
921 fn args(&self) -> Vec<String> {
922 let mut args = vec!["exec".into(), "resume".into()];
923 push_typed_config(&mut args, self.approval_policy, self.web_search);
924 if self.full_auto {
927 args.push("-c".into());
928 args.push(format!(
929 "sandbox_mode=\"{}\"",
930 SandboxMode::WorkspaceWrite.as_arg()
931 ));
932 }
933 push_repeat(&mut args, "-c", &self.config_overrides);
934 push_repeat(&mut args, "--enable", &self.enabled_features);
935 push_repeat(&mut args, "--disable", &self.disabled_features);
936 if self.last {
937 args.push("--last".into());
938 }
939 if self.all {
940 args.push("--all".into());
941 }
942 push_repeat(&mut args, "--image", &self.images);
943 if let Some(model) = &self.model {
944 args.push("--model".into());
945 args.push(model.clone());
946 }
947 if self.strict_config {
948 args.push("--strict-config".into());
949 }
950 if self.dangerously_bypass_approvals_and_sandbox {
951 args.push("--dangerously-bypass-approvals-and-sandbox".into());
952 }
953 if self.dangerously_bypass_hook_trust {
954 args.push("--dangerously-bypass-hook-trust".into());
955 }
956 if self.skip_git_repo_check {
957 args.push("--skip-git-repo-check".into());
958 }
959 if self.ephemeral {
960 args.push("--ephemeral".into());
961 }
962 if self.ignore_user_config {
963 args.push("--ignore-user-config".into());
964 }
965 if self.ignore_rules {
966 args.push("--ignore-rules".into());
967 }
968 if let Some(output_schema) = &self.output_schema {
969 args.push("--output-schema".into());
970 args.push(output_schema.clone());
971 }
972 if self.json {
973 args.push("--json".into());
974 }
975 if let Some(path) = &self.output_last_message {
976 args.push("--output-last-message".into());
977 args.push(path.clone());
978 }
979 if let Some(session_id) = &self.session_id {
980 args.push(session_id.clone());
981 }
982 if let Some(prompt) = &self.prompt {
983 args.push(prompt.clone());
984 }
985 args
986 }
987
988 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
989 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
990 }
991}
992
993fn push_repeat(args: &mut Vec<String>, flag: &str, values: &[String]) {
994 for value in values {
995 args.push(flag.into());
996 args.push(value.clone());
997 }
998}
999
1000#[cfg(feature = "json")]
1001fn parse_json_lines(stdout: &str) -> Result<Vec<JsonLineEvent>> {
1002 stdout
1003 .lines()
1004 .filter(|line| line.trim_start().starts_with('{'))
1005 .map(|line| {
1006 serde_json::from_str(line).map_err(|source| Error::Json {
1007 message: format!("failed to parse JSONL event: {line}"),
1008 source,
1009 })
1010 })
1011 .collect()
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017 use crate::types::ApprovalPolicy;
1018
1019 #[test]
1020 fn exec_args() {
1021 let args = ExecCommand::new("fix the test")
1022 .model("gpt-5")
1023 .sandbox(SandboxMode::WorkspaceWrite)
1024 .strict_config()
1025 .skip_git_repo_check()
1026 .ephemeral()
1027 .ignore_user_config()
1028 .ignore_rules()
1029 .json()
1030 .args();
1031
1032 assert_eq!(
1033 args,
1034 vec![
1035 "exec",
1036 "--model",
1037 "gpt-5",
1038 "--sandbox",
1039 "workspace-write",
1040 "--strict-config",
1041 "--skip-git-repo-check",
1042 "--ephemeral",
1043 "--ignore-user-config",
1044 "--ignore-rules",
1045 "--json",
1046 "fix the test",
1047 ]
1048 );
1049 }
1050
1051 #[test]
1052 fn exec_args_hook_trust() {
1053 let args = ExecCommand::new("go")
1054 .set_bypass_approvals_and_sandbox()
1055 .set_bypass_hook_trust()
1056 .args();
1057
1058 assert_eq!(
1059 args,
1060 vec![
1061 "exec",
1062 "--dangerously-bypass-approvals-and-sandbox",
1063 "--dangerously-bypass-hook-trust",
1064 "go",
1065 ]
1066 );
1067 }
1068
1069 #[test]
1070 #[should_panic(expected = "model name must not be empty")]
1071 fn exec_model_empty_panics() {
1072 let _ = ExecCommand::new("prompt").model("");
1073 }
1074
1075 #[test]
1076 #[should_panic(expected = "model name must not be empty")]
1077 fn exec_resume_model_empty_panics() {
1078 let _ = ExecResumeCommand::new().model("");
1079 }
1080
1081 #[test]
1082 fn exec_resume_args() {
1083 let args = ExecResumeCommand::new()
1084 .last()
1085 .model("gpt-5")
1086 .json()
1087 .prompt("continue")
1088 .args();
1089
1090 assert_eq!(
1091 args,
1092 vec![
1093 "exec", "resume", "--last", "--model", "gpt-5", "--json", "continue",
1094 ]
1095 );
1096 }
1097
1098 #[test]
1099 fn exec_resume_new_flags() {
1100 let args = ExecResumeCommand::new()
1101 .last()
1102 .strict_config()
1103 .set_bypass_hook_trust()
1104 .args();
1105
1106 assert_eq!(
1107 args,
1108 vec![
1109 "exec",
1110 "resume",
1111 "--last",
1112 "--strict-config",
1113 "--dangerously-bypass-hook-trust",
1114 ]
1115 );
1116 }
1117
1118 #[test]
1121 fn exec_approval_and_search_emit_config_keys() {
1122 let args = ExecCommand::new("hi")
1123 .approval_policy(ApprovalPolicy::Never)
1124 .search()
1125 .args();
1126 assert_eq!(
1127 args,
1128 vec![
1129 "exec",
1130 "-c",
1131 "approval_policy=\"never\"",
1132 "-c",
1133 "web_search=\"live\"",
1134 "hi"
1135 ]
1136 );
1137 assert!(
1138 !args
1139 .iter()
1140 .any(|a| a == "--ask-for-approval" || a == "--search")
1141 );
1142 }
1143
1144 #[test]
1147 fn exec_approval_accepts_config_only_values() {
1148 let args = ExecCommand::new("hi")
1149 .approval_policy(ApprovalPolicyConfig::Granular)
1150 .args();
1151 assert_eq!(
1152 args,
1153 vec!["exec", "-c", "approval_policy=\"granular\"", "hi"]
1154 );
1155 }
1156
1157 #[test]
1158 fn exec_search_mode_variants() {
1159 for (mode, expected) in [
1160 (WebSearchMode::Disabled, "disabled"),
1161 (WebSearchMode::Cached, "cached"),
1162 (WebSearchMode::Indexed, "indexed"),
1163 (WebSearchMode::Live, "live"),
1164 ] {
1165 let args = ExecCommand::new("hi").search_mode(mode).args();
1166 assert_eq!(args[2], format!("web_search=\"{expected}\""));
1167 }
1168 }
1169
1170 #[test]
1173 fn exec_raw_config_is_emitted_after_typed_config() {
1174 let args = ExecCommand::new("hi")
1175 .approval_policy(ApprovalPolicy::Never)
1176 .config("approval_policy=\"untrusted\"")
1177 .args();
1178 let typed = args
1179 .iter()
1180 .position(|a| a == "approval_policy=\"never\"")
1181 .unwrap();
1182 let raw = args
1183 .iter()
1184 .position(|a| a == "approval_policy=\"untrusted\"")
1185 .unwrap();
1186 assert!(typed < raw, "raw override must win: {args:?}");
1187 }
1188
1189 #[test]
1191 fn exec_full_auto_emits_sandbox_workspace_write() {
1192 let args = ExecCommand::new("hi").full_auto().args();
1193 assert_eq!(args, vec!["exec", "--sandbox", "workspace-write", "hi"]);
1194 assert!(!args.iter().any(|a| a == "--full-auto"));
1195 }
1196
1197 #[test]
1198 fn exec_explicit_sandbox_wins_over_full_auto() {
1199 let args = ExecCommand::new("hi")
1200 .full_auto()
1201 .sandbox(SandboxMode::ReadOnly)
1202 .args();
1203 assert_eq!(args, vec!["exec", "--sandbox", "read-only", "hi"]);
1204 }
1205
1206 #[test]
1209 fn exec_resume_full_auto_emits_sandbox_config_key() {
1210 let args = ExecResumeCommand::new().last().full_auto().args();
1211 assert_eq!(
1212 args,
1213 vec![
1214 "exec",
1215 "resume",
1216 "-c",
1217 "sandbox_mode=\"workspace-write\"",
1218 "--last"
1219 ]
1220 );
1221 assert!(!args.iter().any(|a| a == "--full-auto"));
1222 }
1223
1224 #[test]
1225 fn exec_resume_approval_and_search_emit_config_keys() {
1226 let args = ExecResumeCommand::new()
1227 .last()
1228 .approval_policy(ApprovalPolicyConfig::OnFailure)
1229 .search_mode(WebSearchMode::Cached)
1230 .args();
1231 assert_eq!(
1232 args,
1233 vec![
1234 "exec",
1235 "resume",
1236 "-c",
1237 "approval_policy=\"on-failure\"",
1238 "-c",
1239 "web_search=\"cached\"",
1240 "--last"
1241 ]
1242 );
1243 }
1244
1245 #[test]
1248 fn exec_resume_ignore_and_output_schema_args() {
1249 let args = ExecResumeCommand::new()
1250 .last()
1251 .ignore_user_config()
1252 .ignore_rules()
1253 .output_schema("/tmp/schema.json")
1254 .args();
1255 assert_eq!(
1256 args,
1257 vec![
1258 "exec",
1259 "resume",
1260 "--last",
1261 "--ignore-user-config",
1262 "--ignore-rules",
1263 "--output-schema",
1264 "/tmp/schema.json"
1265 ]
1266 );
1267 }
1268
1269 #[cfg(all(unix, feature = "json"))]
1274 #[tokio::test]
1275 async fn stdin_prompt_reaches_the_child() {
1276 let codex = echoing_stdin_codex();
1277 let prompt = "a prompt too awkward for argv\nwith a second line";
1278
1279 let result = ExecCommand::from_stdin(prompt)
1280 .execute_json(&codex)
1281 .await
1282 .unwrap();
1283
1284 assert_eq!(result.result, prompt);
1285 }
1286
1287 #[cfg(all(unix, feature = "json"))]
1289 #[tokio::test]
1290 async fn stdin_prompt_reaches_the_child_when_streaming() {
1291 let codex = echoing_stdin_codex();
1292 let prompt = "streamed stdin prompt";
1293 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1294 let sink = std::sync::Arc::clone(&seen);
1295
1296 ExecCommand::from_stdin(prompt)
1297 .stream(&codex, move |event| {
1298 if let Some(text) = event.agent_message_text() {
1299 sink.lock().unwrap().push(text);
1300 }
1301 })
1302 .await
1303 .unwrap();
1304
1305 assert_eq!(seen.lock().unwrap().as_slice(), &[prompt.to_string()]);
1306 }
1307
1308 #[cfg(all(unix, feature = "json"))]
1311 #[tokio::test]
1312 async fn a_prompt_larger_than_the_pipe_buffer_still_completes() {
1313 let codex = echoing_stdin_codex();
1314 let prompt = "x".repeat(512 * 1024);
1316
1317 let result = ExecCommand::from_stdin(&prompt)
1318 .execute_json(&codex)
1319 .await
1320 .unwrap();
1321
1322 assert_eq!(result.result.len(), prompt.len());
1323 }
1324
1325 #[cfg(all(unix, feature = "json"))]
1326 fn echoing_stdin_codex() -> Codex {
1327 let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1328 .join("tests")
1329 .join("fake-codex-echo-stdin.sh");
1330 Codex::builder()
1331 .binary("/bin/bash")
1332 .arg(script.to_str().unwrap())
1333 .build()
1334 .expect("bash must exist")
1335 }
1336
1337 #[test]
1338 fn from_stdin_emits_the_dash_positional_not_the_prompt() {
1339 let args = ExecCommand::from_stdin("secret prompt").ephemeral().args();
1340 assert_eq!(args, vec!["exec", "--ephemeral", "-"]);
1341 assert!(!args.iter().any(|a| a.contains("secret")));
1344 }
1345
1346 #[test]
1347 fn prompt_via_stdin_converts_an_existing_prompt() {
1348 let args = ExecCommand::new("hello").prompt_via_stdin().args();
1349 assert_eq!(args, vec!["exec", "-"]);
1350 }
1351}