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::rollout_budget::RolloutBudgetConfig;
8use crate::types::{ApprovalPolicyConfig, Color, SandboxMode, WebSearchMode};
9#[cfg(feature = "json")]
10use crate::types::{JsonLineEvent, QueryResult};
11
12pub(crate) fn push_typed_config(
19 args: &mut Vec<String>,
20 approval_policy: Option<ApprovalPolicyConfig>,
21 web_search: Option<WebSearchMode>,
22) {
23 if let Some(policy) = approval_policy {
24 args.push("-c".into());
25 args.push(format!("approval_policy=\"{}\"", policy.as_config_value()));
26 }
27 if let Some(mode) = web_search {
28 args.push("-c".into());
29 args.push(format!("web_search=\"{}\"", mode.as_config_value()));
30 }
31}
32
33pub(crate) fn effective_sandbox(
39 sandbox: Option<SandboxMode>,
40 full_auto: bool,
41) -> Option<SandboxMode> {
42 sandbox.or(full_auto.then_some(SandboxMode::WorkspaceWrite))
43}
44
45#[derive(Debug, Clone)]
69pub struct ExecCommand {
70 approve_for_me: bool,
71 prompt: Option<String>,
72 prompt_via_stdin: bool,
73 approval_policy: Option<ApprovalPolicyConfig>,
74 web_search: Option<WebSearchMode>,
75 config_overrides: Vec<String>,
76 enabled_features: Vec<String>,
77 disabled_features: Vec<String>,
78 rollout_budget: Option<RolloutBudgetConfig>,
79 images: Vec<String>,
80 model: Option<String>,
81 oss: bool,
82 local_provider: Option<String>,
83 sandbox: Option<SandboxMode>,
84 strict_config: bool,
85 dangerously_bypass_hook_trust: bool,
86 ignore_user_config: bool,
87 ignore_rules: bool,
88 profile: Option<String>,
89 full_auto: bool,
90 dangerously_bypass_approvals_and_sandbox: bool,
91 cd: Option<String>,
92 skip_git_repo_check: bool,
93 add_dirs: Vec<String>,
94 ephemeral: bool,
95 output_schema: Option<String>,
96 color: Option<Color>,
97 json: bool,
98 output_last_message: Option<String>,
99 retry_policy: Option<crate::retry::RetryPolicy>,
100}
101
102impl ExecCommand {
103 #[must_use]
105 pub fn new(prompt: impl Into<String>) -> Self {
106 Self {
107 approve_for_me: false,
108 prompt: Some(prompt.into()),
109 prompt_via_stdin: false,
110 approval_policy: None,
111 web_search: None,
112 config_overrides: Vec::new(),
113 enabled_features: Vec::new(),
114 disabled_features: Vec::new(),
115 rollout_budget: None,
116 images: Vec::new(),
117 model: None,
118 oss: false,
119 local_provider: None,
120 sandbox: None,
121 strict_config: false,
122 dangerously_bypass_hook_trust: false,
123 ignore_user_config: false,
124 ignore_rules: false,
125 profile: None,
126 full_auto: false,
127 dangerously_bypass_approvals_and_sandbox: false,
128 cd: None,
129 skip_git_repo_check: false,
130 add_dirs: Vec::new(),
131 ephemeral: false,
132 output_schema: None,
133 color: None,
134 json: false,
135 output_last_message: None,
136 retry_policy: None,
137 }
138 }
139
140 #[must_use]
164 pub fn from_stdin(prompt: impl Into<String>) -> Self {
165 Self::new(prompt).prompt_via_stdin()
166 }
167
168 #[must_use]
179 pub fn prompt_via_stdin(mut self) -> Self {
180 self.prompt_via_stdin = true;
181 self
182 }
183
184 #[cfg(feature = "json")]
189 pub(crate) fn stdin_prompt(&self) -> Option<&str> {
190 self.prompt_via_stdin
191 .then(|| self.prompt.as_deref().unwrap_or_default())
192 }
193
194 #[must_use]
201 pub fn config(mut self, key_value: impl Into<String>) -> Self {
202 self.config_overrides.push(key_value.into());
203 self
204 }
205
206 #[must_use]
222 pub fn approval_policy(mut self, policy: impl Into<ApprovalPolicyConfig>) -> Self {
223 self.approval_policy = Some(policy.into());
224 self
225 }
226
227 #[must_use]
232 pub fn search(self) -> Self {
233 self.search_mode(WebSearchMode::Live)
234 }
235
236 #[must_use]
242 pub fn search_mode(mut self, mode: WebSearchMode) -> Self {
243 self.web_search = Some(mode);
244 self
245 }
246
247 #[must_use]
251 pub fn enable(mut self, feature: impl Into<String>) -> Self {
252 self.enabled_features.push(feature.into());
253 self
254 }
255
256 #[must_use]
260 pub fn disable(mut self, feature: impl Into<String>) -> Self {
261 self.disabled_features.push(feature.into());
262 self
263 }
264
265 #[must_use]
280 pub fn rollout_budget(mut self, budget: RolloutBudgetConfig) -> Self {
281 self.rollout_budget = Some(budget);
282 self
283 }
284
285 #[must_use]
289 pub fn image(mut self, path: impl Into<String>) -> Self {
290 self.images.push(path.into());
291 self
292 }
293
294 #[must_use]
298 pub fn model(mut self, model: impl Into<String>) -> Self {
299 let model = model.into();
300 assert!(!model.is_empty(), "model name must not be empty");
301 self.model = Some(model);
302 self
303 }
304
305 #[must_use]
307 pub fn oss(mut self) -> Self {
308 self.oss = true;
309 self
310 }
311
312 #[must_use]
314 pub fn local_provider(mut self, provider: impl Into<String>) -> Self {
315 self.local_provider = Some(provider.into());
316 self
317 }
318
319 #[must_use]
321 pub fn sandbox(mut self, sandbox: SandboxMode) -> Self {
322 self.sandbox = Some(sandbox);
323 self
324 }
325
326 #[must_use]
328 pub fn strict_config(mut self) -> Self {
329 self.strict_config = true;
330 self
331 }
332
333 #[must_use]
337 pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
338 self.dangerously_bypass_hook_trust = true;
339 self
340 }
341
342 #[must_use]
344 pub fn ignore_user_config(mut self) -> Self {
345 self.ignore_user_config = true;
346 self
347 }
348
349 #[must_use]
351 pub fn ignore_rules(mut self) -> Self {
352 self.ignore_rules = true;
353 self
354 }
355
356 #[must_use]
358 pub fn profile(mut self, profile: impl Into<String>) -> Self {
359 self.profile = Some(profile.into());
360 self
361 }
362
363 #[must_use]
375 pub fn full_auto(mut self) -> Self {
376 self.full_auto = true;
377 self
378 }
379
380 #[must_use]
388 pub fn approve_for_me(mut self) -> Self {
389 self.approve_for_me = true;
390 self
391 }
392
393 #[must_use]
397 pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
398 self.dangerously_bypass_approvals_and_sandbox = true;
399 self
400 }
401
402 #[must_use]
404 pub fn cd(mut self, dir: impl Into<String>) -> Self {
405 self.cd = Some(dir.into());
406 self
407 }
408
409 #[must_use]
411 pub fn skip_git_repo_check(mut self) -> Self {
412 self.skip_git_repo_check = true;
413 self
414 }
415
416 #[must_use]
420 pub fn add_dir(mut self, dir: impl Into<String>) -> Self {
421 self.add_dirs.push(dir.into());
422 self
423 }
424
425 #[must_use]
427 pub fn ephemeral(mut self) -> Self {
428 self.ephemeral = true;
429 self
430 }
431
432 #[must_use]
434 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
435 self.output_schema = Some(path.into());
436 self
437 }
438
439 #[must_use]
441 pub fn color(mut self, color: Color) -> Self {
442 self.color = Some(color);
443 self
444 }
445
446 #[must_use]
452 pub fn json(mut self) -> Self {
453 self.json = true;
454 self
455 }
456
457 #[must_use]
459 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
460 self.output_last_message = Some(path.into());
461 self
462 }
463
464 #[must_use]
468 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
469 self.retry_policy = Some(policy);
470 self
471 }
472
473 #[cfg(feature = "json")]
496 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
497 where
498 F: FnMut(JsonLineEvent),
499 {
500 crate::streaming::stream_exec(codex, self, handler).await
501 }
502
503 pub async fn execute_cancellable<C>(&self, codex: &Codex, cancel: C) -> Result<CommandOutput>
510 where
511 C: std::future::Future<Output = ()> + Send,
512 {
513 if self.prompt_via_stdin {
514 let prompt = self.prompt.as_deref().unwrap_or_default();
515 return exec::run_codex_with_stdin_prompt_cancellable(
516 codex,
517 self.args(),
518 prompt,
519 cancel,
520 )
521 .await;
522 }
523 exec::run_codex_cancellable(codex, self.args(), cancel).await
524 }
525
526 #[cfg(feature = "json")]
531 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
532 let mut args = self.args();
533 if !self.json {
534 args.push("--json".into());
535 }
536
537 let output = if self.prompt_via_stdin {
538 let prompt = self.prompt.as_deref().unwrap_or_default();
539 exec::run_codex_with_stdin_prompt(codex, args, prompt).await?
540 } else {
541 exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?
542 };
543 parse_json_lines(&output.stdout)
544 }
545
546 #[cfg(feature = "json")]
551 pub async fn execute_json_lines_cancellable<C>(
552 &self,
553 codex: &Codex,
554 cancel: C,
555 ) -> Result<Vec<JsonLineEvent>>
556 where
557 C: std::future::Future<Output = ()> + Send,
558 {
559 let mut args = self.args();
560 if !self.json {
561 args.push("--json".into());
562 }
563
564 let output = if self.prompt_via_stdin {
565 let prompt = self.prompt.as_deref().unwrap_or_default();
566 exec::run_codex_with_stdin_prompt_cancellable(codex, args, prompt, cancel).await?
567 } else {
568 exec::run_codex_cancellable(codex, args, cancel).await?
569 };
570 parse_json_lines(&output.stdout)
571 }
572
573 #[cfg(feature = "json")]
579 pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
580 let events = self.execute_json_lines(codex).await?;
581 Ok(QueryResult::from_events(events))
582 }
583
584 #[cfg(feature = "json")]
589 pub async fn execute_json_cancellable<C>(&self, codex: &Codex, cancel: C) -> Result<QueryResult>
590 where
591 C: std::future::Future<Output = ()> + Send,
592 {
593 let events = self.execute_json_lines_cancellable(codex, cancel).await?;
594 Ok(QueryResult::from_events(events))
595 }
596}
597
598impl CodexCommand for ExecCommand {
599 type Output = CommandOutput;
600
601 fn args(&self) -> Vec<String> {
602 let mut args = vec!["exec".to_string()];
603
604 push_typed_config(&mut args, self.approval_policy, self.web_search);
605 push_repeat(&mut args, "-c", &self.config_overrides);
606 push_feature_toggles(
607 &mut args,
608 &self.enabled_features,
609 &self.disabled_features,
610 self.rollout_budget.is_some(),
611 );
612 if let Some(budget) = &self.rollout_budget {
613 args.push("-c".into());
614 args.push(budget.config_override());
615 }
616 push_repeat(&mut args, "--image", &self.images);
617
618 if let Some(model) = &self.model {
619 args.push("--model".into());
620 args.push(model.clone());
621 }
622 if self.oss {
623 args.push("--oss".into());
624 }
625 if let Some(local_provider) = &self.local_provider {
626 args.push("--local-provider".into());
627 args.push(local_provider.clone());
628 }
629 if let Some(sandbox) = effective_sandbox(self.sandbox, self.full_auto) {
630 args.push("--sandbox".into());
631 args.push(sandbox.as_arg().into());
632 }
633 if self.strict_config {
634 args.push("--strict-config".into());
635 }
636 if let Some(profile) = &self.profile {
637 args.push("--profile".into());
638 args.push(profile.clone());
639 }
640 if self.approve_for_me {
641 args.push("--approve-for-me".into());
642 }
643 if self.dangerously_bypass_approvals_and_sandbox {
644 args.push("--dangerously-bypass-approvals-and-sandbox".into());
645 }
646 if self.dangerously_bypass_hook_trust {
647 args.push("--dangerously-bypass-hook-trust".into());
648 }
649 if let Some(cd) = &self.cd {
650 args.push("--cd".into());
651 args.push(cd.clone());
652 }
653 if self.skip_git_repo_check {
654 args.push("--skip-git-repo-check".into());
655 }
656 push_repeat(&mut args, "--add-dir", &self.add_dirs);
657 if self.ephemeral {
658 args.push("--ephemeral".into());
659 }
660 if self.ignore_user_config {
661 args.push("--ignore-user-config".into());
662 }
663 if self.ignore_rules {
664 args.push("--ignore-rules".into());
665 }
666 if let Some(output_schema) = &self.output_schema {
667 args.push("--output-schema".into());
668 args.push(output_schema.clone());
669 }
670 if let Some(color) = self.color {
671 args.push("--color".into());
672 args.push(color.as_arg().into());
673 }
674 if self.json {
675 args.push("--json".into());
676 }
677 if let Some(path) = &self.output_last_message {
678 args.push("--output-last-message".into());
679 args.push(path.clone());
680 }
681 if self.prompt_via_stdin {
682 args.push("-".into());
685 } else if let Some(prompt) = &self.prompt {
686 args.push(prompt.clone());
687 }
688
689 args
690 }
691
692 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
693 if self.prompt_via_stdin {
694 let prompt = self.prompt.as_deref().unwrap_or_default();
695 return exec::run_codex_with_stdin_prompt(codex, self.args(), prompt).await;
696 }
697 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
698 }
699}
700
701#[derive(Debug, Clone)]
706pub struct ExecResumeCommand {
707 session_id: Option<String>,
708 prompt: Option<String>,
709 prompt_via_stdin: bool,
710 last: bool,
711 all: bool,
712 approval_policy: Option<ApprovalPolicyConfig>,
713 web_search: Option<WebSearchMode>,
714 config_overrides: Vec<String>,
715 enabled_features: Vec<String>,
716 disabled_features: Vec<String>,
717 rollout_budget: Option<RolloutBudgetConfig>,
718 images: Vec<String>,
719 model: Option<String>,
720 strict_config: bool,
721 dangerously_bypass_hook_trust: bool,
722 ignore_user_config: bool,
723 ignore_rules: bool,
724 output_schema: Option<String>,
725 full_auto: bool,
726 dangerously_bypass_approvals_and_sandbox: bool,
727 skip_git_repo_check: bool,
728 ephemeral: bool,
729 json: bool,
730 output_last_message: Option<String>,
731 retry_policy: Option<crate::retry::RetryPolicy>,
732}
733
734impl ExecResumeCommand {
735 #[must_use]
737 pub fn new() -> Self {
738 Self {
739 session_id: None,
740 prompt: None,
741 prompt_via_stdin: false,
742 last: false,
743 all: false,
744 approval_policy: None,
745 web_search: None,
746 config_overrides: Vec::new(),
747 enabled_features: Vec::new(),
748 disabled_features: Vec::new(),
749 rollout_budget: None,
750 images: Vec::new(),
751 model: None,
752 strict_config: false,
753 dangerously_bypass_hook_trust: false,
754 ignore_user_config: false,
755 ignore_rules: false,
756 output_schema: None,
757 full_auto: false,
758 dangerously_bypass_approvals_and_sandbox: false,
759 skip_git_repo_check: false,
760 ephemeral: false,
761 json: false,
762 output_last_message: None,
763 retry_policy: None,
764 }
765 }
766
767 #[must_use]
769 pub fn session_id(mut self, session_id: impl Into<String>) -> Self {
770 self.session_id = Some(session_id.into());
771 self
772 }
773
774 #[must_use]
776 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
777 self.prompt = Some(prompt.into());
778 self
779 }
780
781 #[must_use]
787 pub fn from_stdin(prompt: impl Into<String>) -> Self {
788 Self::new().prompt(prompt).prompt_via_stdin()
789 }
790
791 #[must_use]
797 pub fn prompt_via_stdin(mut self) -> Self {
798 self.prompt_via_stdin = true;
799 self
800 }
801
802 #[cfg(feature = "json")]
805 pub(crate) fn stdin_prompt(&self) -> Option<&str> {
806 self.prompt_via_stdin
807 .then(|| self.prompt.as_deref().unwrap_or_default())
808 }
809
810 #[must_use]
812 pub fn last(mut self) -> Self {
813 self.last = true;
814 self
815 }
816
817 #[must_use]
819 pub fn all(mut self) -> Self {
820 self.all = true;
821 self
822 }
823
824 #[must_use]
828 pub fn model(mut self, model: impl Into<String>) -> Self {
829 let model = model.into();
830 assert!(!model.is_empty(), "model name must not be empty");
831 self.model = Some(model);
832 self
833 }
834
835 #[must_use]
839 pub fn image(mut self, path: impl Into<String>) -> Self {
840 self.images.push(path.into());
841 self
842 }
843
844 #[must_use]
846 pub fn json(mut self) -> Self {
847 self.json = true;
848 self
849 }
850
851 #[must_use]
853 pub fn output_last_message(mut self, path: impl Into<String>) -> Self {
854 self.output_last_message = Some(path.into());
855 self
856 }
857
858 #[must_use]
865 pub fn config(mut self, key_value: impl Into<String>) -> Self {
866 self.config_overrides.push(key_value.into());
867 self
868 }
869
870 #[must_use]
877 pub fn approval_policy(mut self, policy: impl Into<ApprovalPolicyConfig>) -> Self {
878 self.approval_policy = Some(policy.into());
879 self
880 }
881
882 #[must_use]
887 pub fn search(self) -> Self {
888 self.search_mode(WebSearchMode::Live)
889 }
890
891 #[must_use]
897 pub fn search_mode(mut self, mode: WebSearchMode) -> Self {
898 self.web_search = Some(mode);
899 self
900 }
901
902 #[must_use]
906 pub fn enable(mut self, feature: impl Into<String>) -> Self {
907 self.enabled_features.push(feature.into());
908 self
909 }
910
911 #[must_use]
915 pub fn disable(mut self, feature: impl Into<String>) -> Self {
916 self.disabled_features.push(feature.into());
917 self
918 }
919
920 #[must_use]
927 pub fn rollout_budget(mut self, budget: RolloutBudgetConfig) -> Self {
928 self.rollout_budget = Some(budget);
929 self
930 }
931
932 #[must_use]
934 pub fn strict_config(mut self) -> Self {
935 self.strict_config = true;
936 self
937 }
938
939 #[must_use]
943 pub(crate) fn set_bypass_hook_trust(mut self) -> Self {
944 self.dangerously_bypass_hook_trust = true;
945 self
946 }
947
948 #[must_use]
950 pub fn ignore_user_config(mut self) -> Self {
951 self.ignore_user_config = true;
952 self
953 }
954
955 #[must_use]
957 pub fn ignore_rules(mut self) -> Self {
958 self.ignore_rules = true;
959 self
960 }
961
962 #[must_use]
964 pub fn output_schema(mut self, path: impl Into<String>) -> Self {
965 self.output_schema = Some(path.into());
966 self
967 }
968
969 #[must_use]
975 pub fn full_auto(mut self) -> Self {
976 self.full_auto = true;
977 self
978 }
979
980 #[must_use]
984 pub(crate) fn set_bypass_approvals_and_sandbox(mut self) -> Self {
985 self.dangerously_bypass_approvals_and_sandbox = true;
986 self
987 }
988
989 #[must_use]
991 pub fn skip_git_repo_check(mut self) -> Self {
992 self.skip_git_repo_check = true;
993 self
994 }
995
996 #[must_use]
998 pub fn ephemeral(mut self) -> Self {
999 self.ephemeral = true;
1000 self
1001 }
1002
1003 #[must_use]
1007 pub fn retry(mut self, policy: crate::retry::RetryPolicy) -> Self {
1008 self.retry_policy = Some(policy);
1009 self
1010 }
1011
1012 pub async fn execute_cancellable<C>(&self, codex: &Codex, cancel: C) -> Result<CommandOutput>
1019 where
1020 C: std::future::Future<Output = ()> + Send,
1021 {
1022 if self.prompt_via_stdin {
1023 let prompt = self.prompt.as_deref().unwrap_or_default();
1024 return exec::run_codex_with_stdin_prompt_cancellable(
1025 codex,
1026 self.args(),
1027 prompt,
1028 cancel,
1029 )
1030 .await;
1031 }
1032 exec::run_codex_cancellable(codex, self.args(), cancel).await
1033 }
1034
1035 #[cfg(feature = "json")]
1040 pub async fn execute_json_lines(&self, codex: &Codex) -> Result<Vec<JsonLineEvent>> {
1041 let mut args = self.args();
1042 if !self.json {
1043 args.push("--json".into());
1044 }
1045
1046 let output = if self.prompt_via_stdin {
1047 let prompt = self.prompt.as_deref().unwrap_or_default();
1048 exec::run_codex_with_stdin_prompt(codex, args, prompt).await?
1049 } else {
1050 exec::run_codex_with_retry(codex, args, self.retry_policy.as_ref()).await?
1051 };
1052 parse_json_lines(&output.stdout)
1053 }
1054
1055 #[cfg(feature = "json")]
1060 pub async fn execute_json_lines_cancellable<C>(
1061 &self,
1062 codex: &Codex,
1063 cancel: C,
1064 ) -> Result<Vec<JsonLineEvent>>
1065 where
1066 C: std::future::Future<Output = ()> + Send,
1067 {
1068 let mut args = self.args();
1069 if !self.json {
1070 args.push("--json".into());
1071 }
1072
1073 let output = if self.prompt_via_stdin {
1074 let prompt = self.prompt.as_deref().unwrap_or_default();
1075 exec::run_codex_with_stdin_prompt_cancellable(codex, args, prompt, cancel).await?
1076 } else {
1077 exec::run_codex_cancellable(codex, args, cancel).await?
1078 };
1079 parse_json_lines(&output.stdout)
1080 }
1081
1082 #[cfg(feature = "json")]
1087 pub async fn execute_json(&self, codex: &Codex) -> Result<QueryResult> {
1088 let events = self.execute_json_lines(codex).await?;
1089 Ok(QueryResult::from_events(events))
1090 }
1091
1092 #[cfg(feature = "json")]
1097 pub async fn execute_json_cancellable<C>(&self, codex: &Codex, cancel: C) -> Result<QueryResult>
1098 where
1099 C: std::future::Future<Output = ()> + Send,
1100 {
1101 let events = self.execute_json_lines_cancellable(codex, cancel).await?;
1102 Ok(QueryResult::from_events(events))
1103 }
1104
1105 #[cfg(feature = "json")]
1111 pub async fn stream<F>(&self, codex: &Codex, handler: F) -> Result<()>
1112 where
1113 F: FnMut(JsonLineEvent),
1114 {
1115 crate::streaming::stream_exec_resume(codex, self, handler).await
1116 }
1117}
1118
1119impl Default for ExecResumeCommand {
1120 fn default() -> Self {
1121 Self::new()
1122 }
1123}
1124
1125impl CodexCommand for ExecResumeCommand {
1126 type Output = CommandOutput;
1127
1128 fn args(&self) -> Vec<String> {
1129 let mut args = vec!["exec".into(), "resume".into()];
1130 push_typed_config(&mut args, self.approval_policy, self.web_search);
1131 if self.full_auto {
1134 args.push("-c".into());
1135 args.push(format!(
1136 "sandbox_mode=\"{}\"",
1137 SandboxMode::WorkspaceWrite.as_arg()
1138 ));
1139 }
1140 push_repeat(&mut args, "-c", &self.config_overrides);
1141 push_feature_toggles(
1142 &mut args,
1143 &self.enabled_features,
1144 &self.disabled_features,
1145 self.rollout_budget.is_some(),
1146 );
1147 if let Some(budget) = &self.rollout_budget {
1148 args.push("-c".into());
1149 args.push(budget.config_override());
1150 }
1151 if self.last {
1152 args.push("--last".into());
1153 }
1154 if self.all {
1155 args.push("--all".into());
1156 }
1157 push_repeat(&mut args, "--image", &self.images);
1158 if let Some(model) = &self.model {
1159 args.push("--model".into());
1160 args.push(model.clone());
1161 }
1162 if self.strict_config {
1163 args.push("--strict-config".into());
1164 }
1165 if self.dangerously_bypass_approvals_and_sandbox {
1166 args.push("--dangerously-bypass-approvals-and-sandbox".into());
1167 }
1168 if self.dangerously_bypass_hook_trust {
1169 args.push("--dangerously-bypass-hook-trust".into());
1170 }
1171 if self.skip_git_repo_check {
1172 args.push("--skip-git-repo-check".into());
1173 }
1174 if self.ephemeral {
1175 args.push("--ephemeral".into());
1176 }
1177 if self.ignore_user_config {
1178 args.push("--ignore-user-config".into());
1179 }
1180 if self.ignore_rules {
1181 args.push("--ignore-rules".into());
1182 }
1183 if let Some(output_schema) = &self.output_schema {
1184 args.push("--output-schema".into());
1185 args.push(output_schema.clone());
1186 }
1187 if self.json {
1188 args.push("--json".into());
1189 }
1190 if let Some(path) = &self.output_last_message {
1191 args.push("--output-last-message".into());
1192 args.push(path.clone());
1193 }
1194 if let Some(session_id) = &self.session_id {
1195 args.push(session_id.clone());
1196 }
1197 if self.prompt_via_stdin {
1198 args.push("-".into());
1199 } else if let Some(prompt) = &self.prompt {
1200 args.push(prompt.clone());
1201 }
1202 args
1203 }
1204
1205 async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
1206 if self.prompt_via_stdin {
1207 let prompt = self.prompt.as_deref().unwrap_or_default();
1208 return exec::run_codex_with_stdin_prompt(codex, self.args(), prompt).await;
1209 }
1210 exec::run_codex_with_retry(codex, self.args(), self.retry_policy.as_ref()).await
1211 }
1212}
1213
1214fn push_repeat(args: &mut Vec<String>, flag: &str, values: &[String]) {
1215 for value in values {
1216 args.push(flag.into());
1217 args.push(value.clone());
1218 }
1219}
1220
1221fn push_feature_toggles(
1222 args: &mut Vec<String>,
1223 enabled: &[String],
1224 disabled: &[String],
1225 protects_rollout_budget: bool,
1226) {
1227 let keep = |feature: &&String| !protects_rollout_budget || feature.as_str() != "rollout_budget";
1228 for feature in enabled.iter().filter(keep) {
1229 args.push("--enable".into());
1230 args.push(feature.clone());
1231 }
1232 for feature in disabled.iter().filter(keep) {
1233 args.push("--disable".into());
1234 args.push(feature.clone());
1235 }
1236}
1237
1238#[cfg(feature = "json")]
1239fn parse_json_lines(stdout: &str) -> Result<Vec<JsonLineEvent>> {
1240 stdout
1241 .lines()
1242 .filter(|line| line.trim_start().starts_with('{'))
1243 .map(|line| {
1244 serde_json::from_str(line).map_err(|source| Error::Json {
1245 message: format!("failed to parse JSONL event: {line}"),
1246 source,
1247 })
1248 })
1249 .collect()
1250}
1251
1252#[cfg(test)]
1253mod tests {
1254 use super::*;
1255 use crate::types::ApprovalPolicy;
1256
1257 #[test]
1258 fn exec_args() {
1259 let args = ExecCommand::new("fix the test")
1260 .model("gpt-5")
1261 .sandbox(SandboxMode::WorkspaceWrite)
1262 .strict_config()
1263 .skip_git_repo_check()
1264 .ephemeral()
1265 .ignore_user_config()
1266 .ignore_rules()
1267 .json()
1268 .args();
1269
1270 assert_eq!(
1271 args,
1272 vec![
1273 "exec",
1274 "--model",
1275 "gpt-5",
1276 "--sandbox",
1277 "workspace-write",
1278 "--strict-config",
1279 "--skip-git-repo-check",
1280 "--ephemeral",
1281 "--ignore-user-config",
1282 "--ignore-rules",
1283 "--json",
1284 "fix the test",
1285 ]
1286 );
1287 }
1288
1289 #[test]
1290 fn exec_args_hook_trust() {
1291 let args = ExecCommand::new("go")
1292 .set_bypass_approvals_and_sandbox()
1293 .set_bypass_hook_trust()
1294 .args();
1295
1296 assert_eq!(
1297 args,
1298 vec![
1299 "exec",
1300 "--dangerously-bypass-approvals-and-sandbox",
1301 "--dangerously-bypass-hook-trust",
1302 "go",
1303 ]
1304 );
1305 }
1306
1307 #[test]
1308 #[should_panic(expected = "model name must not be empty")]
1309 fn exec_model_empty_panics() {
1310 let _ = ExecCommand::new("prompt").model("");
1311 }
1312
1313 #[test]
1314 #[should_panic(expected = "model name must not be empty")]
1315 fn exec_resume_model_empty_panics() {
1316 let _ = ExecResumeCommand::new().model("");
1317 }
1318
1319 #[test]
1320 fn exec_resume_args() {
1321 let args = ExecResumeCommand::new()
1322 .last()
1323 .model("gpt-5")
1324 .json()
1325 .prompt("continue")
1326 .args();
1327
1328 assert_eq!(
1329 args,
1330 vec![
1331 "exec", "resume", "--last", "--model", "gpt-5", "--json", "continue",
1332 ]
1333 );
1334 }
1335
1336 #[test]
1337 fn exec_resume_new_flags() {
1338 let args = ExecResumeCommand::new()
1339 .last()
1340 .strict_config()
1341 .set_bypass_hook_trust()
1342 .args();
1343
1344 assert_eq!(
1345 args,
1346 vec![
1347 "exec",
1348 "resume",
1349 "--last",
1350 "--strict-config",
1351 "--dangerously-bypass-hook-trust",
1352 ]
1353 );
1354 }
1355
1356 #[test]
1359 fn exec_approval_and_search_emit_config_keys() {
1360 let args = ExecCommand::new("hi")
1361 .approval_policy(ApprovalPolicy::Never)
1362 .search()
1363 .args();
1364 assert_eq!(
1365 args,
1366 vec![
1367 "exec",
1368 "-c",
1369 "approval_policy=\"never\"",
1370 "-c",
1371 "web_search=\"live\"",
1372 "hi"
1373 ]
1374 );
1375 assert!(
1376 !args
1377 .iter()
1378 .any(|a| a == "--ask-for-approval" || a == "--search")
1379 );
1380 }
1381
1382 #[test]
1385 fn exec_approval_accepts_config_only_values() {
1386 let args = ExecCommand::new("hi")
1387 .approval_policy(ApprovalPolicyConfig::Granular)
1388 .args();
1389 assert_eq!(
1390 args,
1391 vec!["exec", "-c", "approval_policy=\"granular\"", "hi"]
1392 );
1393 }
1394
1395 #[test]
1396 fn exec_search_mode_variants() {
1397 for (mode, expected) in [
1398 (WebSearchMode::Disabled, "disabled"),
1399 (WebSearchMode::Cached, "cached"),
1400 (WebSearchMode::Indexed, "indexed"),
1401 (WebSearchMode::Live, "live"),
1402 ] {
1403 let args = ExecCommand::new("hi").search_mode(mode).args();
1404 assert_eq!(args[2], format!("web_search=\"{expected}\""));
1405 }
1406 }
1407
1408 #[test]
1411 fn exec_raw_config_is_emitted_after_typed_config() {
1412 let args = ExecCommand::new("hi")
1413 .approval_policy(ApprovalPolicy::Never)
1414 .config("approval_policy=\"untrusted\"")
1415 .args();
1416 let typed = args
1417 .iter()
1418 .position(|a| a == "approval_policy=\"never\"")
1419 .unwrap();
1420 let raw = args
1421 .iter()
1422 .position(|a| a == "approval_policy=\"untrusted\"")
1423 .unwrap();
1424 assert!(typed < raw, "raw override must win: {args:?}");
1425 }
1426
1427 #[test]
1428 fn native_rollout_budget_is_identical_and_final_on_open_and_resume() {
1429 let budget = RolloutBudgetConfig::builder(10_000)
1430 .reminder_at_remaining_tokens([5_000, 1_000])
1431 .sampling_token_weight(1.0)
1432 .prefill_token_weight(0.25)
1433 .build()
1434 .expect("valid budget");
1435 let expected = budget.config_override();
1436 let opening = ExecCommand::new("hi")
1437 .rollout_budget(budget.clone())
1438 .config("features.rollout_budget=false")
1439 .enable("rollout_budget")
1440 .disable("rollout_budget")
1441 .enable("keep-enabled")
1442 .disable("keep-disabled")
1443 .args();
1444 let resumed = ExecResumeCommand::new()
1445 .session_id("thread")
1446 .rollout_budget(budget)
1447 .config("features.rollout_budget=false")
1448 .enable("rollout_budget")
1449 .disable("rollout_budget")
1450 .enable("keep-enabled")
1451 .disable("keep-disabled")
1452 .args();
1453
1454 for args in [opening, resumed] {
1455 let budget_at = args.iter().position(|arg| arg == &expected).unwrap();
1456 let raw_at = args
1457 .iter()
1458 .position(|arg| arg == "features.rollout_budget=false")
1459 .unwrap();
1460 assert!(
1461 raw_at < budget_at,
1462 "native budget must beat raw config: {args:?}"
1463 );
1464 assert!(
1465 !args.windows(2).any(|pair| {
1466 matches!(pair[0].as_str(), "--enable" | "--disable")
1467 && pair[1] == "rollout_budget"
1468 }),
1469 "native budget must suppress conflicting feature toggles: {args:?}"
1470 );
1471 assert!(
1472 args.windows(2)
1473 .any(|pair| pair == ["--enable", "keep-enabled"])
1474 );
1475 assert!(
1476 args.windows(2)
1477 .any(|pair| pair == ["--disable", "keep-disabled"])
1478 );
1479 }
1480 }
1481
1482 #[test]
1484 fn exec_full_auto_emits_sandbox_workspace_write() {
1485 let args = ExecCommand::new("hi").full_auto().args();
1486 assert_eq!(args, vec!["exec", "--sandbox", "workspace-write", "hi"]);
1487 assert!(!args.iter().any(|a| a == "--full-auto"));
1488 }
1489
1490 #[test]
1491 fn exec_explicit_sandbox_wins_over_full_auto() {
1492 let args = ExecCommand::new("hi")
1493 .full_auto()
1494 .sandbox(SandboxMode::ReadOnly)
1495 .args();
1496 assert_eq!(args, vec!["exec", "--sandbox", "read-only", "hi"]);
1497 }
1498
1499 #[test]
1502 fn exec_resume_full_auto_emits_sandbox_config_key() {
1503 let args = ExecResumeCommand::new().last().full_auto().args();
1504 assert_eq!(
1505 args,
1506 vec![
1507 "exec",
1508 "resume",
1509 "-c",
1510 "sandbox_mode=\"workspace-write\"",
1511 "--last"
1512 ]
1513 );
1514 assert!(!args.iter().any(|a| a == "--full-auto"));
1515 }
1516
1517 #[test]
1518 fn exec_resume_approval_and_search_emit_config_keys() {
1519 let args = ExecResumeCommand::new()
1520 .last()
1521 .approval_policy(ApprovalPolicyConfig::OnFailure)
1522 .search_mode(WebSearchMode::Cached)
1523 .args();
1524 assert_eq!(
1525 args,
1526 vec![
1527 "exec",
1528 "resume",
1529 "-c",
1530 "approval_policy=\"on-failure\"",
1531 "-c",
1532 "web_search=\"cached\"",
1533 "--last"
1534 ]
1535 );
1536 }
1537
1538 #[test]
1541 fn exec_resume_ignore_and_output_schema_args() {
1542 let args = ExecResumeCommand::new()
1543 .last()
1544 .ignore_user_config()
1545 .ignore_rules()
1546 .output_schema("/tmp/schema.json")
1547 .args();
1548 assert_eq!(
1549 args,
1550 vec![
1551 "exec",
1552 "resume",
1553 "--last",
1554 "--ignore-user-config",
1555 "--ignore-rules",
1556 "--output-schema",
1557 "/tmp/schema.json"
1558 ]
1559 );
1560 }
1561
1562 #[cfg(all(unix, feature = "json"))]
1563 #[tokio::test]
1564 async fn fresh_json_cancellation_returns_after_reaping() {
1565 use crate::test_support::{PidFile, blocking_codex, is_running_for_test};
1566
1567 let pid_file = PidFile::new("fresh-json-cancellable");
1568 let codex = blocking_codex(&pid_file)
1569 .termination_grace(std::time::Duration::from_millis(10))
1570 .build()
1571 .expect("bash must exist");
1572
1573 let result = ExecCommand::new("probe")
1574 .execute_json_cancellable(&codex, async {
1575 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1576 })
1577 .await;
1578
1579 assert!(matches!(result, Err(Error::Cancelled { .. })));
1580 let pid = pid_file.read_pid().await;
1581 assert!(
1582 !is_running_for_test(pid),
1583 "codex ({pid}) survived cancellation"
1584 );
1585 }
1586
1587 #[cfg(all(unix, feature = "json"))]
1588 #[tokio::test]
1589 async fn resumed_stdin_json_cancellation_returns_after_reaping() {
1590 use crate::test_support::{PidFile, blocking_codex, is_running_for_test};
1591
1592 let pid_file = PidFile::new("resume-stdin-json-cancellable");
1593 let codex = blocking_codex(&pid_file)
1594 .termination_grace(std::time::Duration::from_millis(10))
1595 .build()
1596 .expect("bash must exist");
1597
1598 let result = ExecResumeCommand::from_stdin("continue")
1599 .session_id("thread-1")
1600 .execute_json_cancellable(&codex, async {
1601 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1602 })
1603 .await;
1604
1605 assert!(matches!(result, Err(Error::Cancelled { .. })));
1606 let pid = pid_file.read_pid().await;
1607 assert!(
1608 !is_running_for_test(pid),
1609 "codex ({pid}) survived cancellation"
1610 );
1611 }
1612
1613 #[cfg(all(unix, feature = "json"))]
1618 #[tokio::test]
1619 async fn stdin_prompt_reaches_the_child() {
1620 let codex = echoing_stdin_codex();
1621 let prompt = "a prompt too awkward for argv\nwith a second line";
1622
1623 let result = ExecCommand::from_stdin(prompt)
1624 .execute_json(&codex)
1625 .await
1626 .unwrap();
1627
1628 assert_eq!(result.result, prompt);
1629 }
1630
1631 #[cfg(unix)]
1632 #[tokio::test]
1633 async fn resume_stdin_prompt_reaches_the_child_for_raw_execution() {
1634 let codex = echoing_stdin_codex();
1635 let prompt = "raw resumed stdin prompt";
1636
1637 let output = ExecResumeCommand::from_stdin(prompt)
1638 .session_id("thread-1")
1639 .execute(&codex)
1640 .await
1641 .unwrap();
1642
1643 assert!(output.stdout.contains(prompt));
1644 }
1645
1646 #[cfg(all(unix, feature = "json"))]
1647 #[tokio::test]
1648 async fn resume_stdin_prompt_reaches_the_child_for_json_execution() {
1649 let codex = echoing_stdin_codex();
1650 let prompt = "json resumed stdin prompt";
1651
1652 let result = ExecResumeCommand::from_stdin(prompt)
1653 .session_id("thread-1")
1654 .execute_json(&codex)
1655 .await
1656 .unwrap();
1657
1658 assert_eq!(result.result, prompt);
1659 }
1660
1661 #[cfg(all(unix, feature = "json"))]
1663 #[tokio::test]
1664 async fn stdin_prompt_reaches_the_child_when_streaming() {
1665 let codex = echoing_stdin_codex();
1666 let prompt = "streamed stdin prompt";
1667 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1668 let sink = std::sync::Arc::clone(&seen);
1669
1670 ExecCommand::from_stdin(prompt)
1671 .stream(&codex, move |event| {
1672 if let Some(text) = event.agent_message_text() {
1673 sink.lock().unwrap().push(text);
1674 }
1675 })
1676 .await
1677 .unwrap();
1678
1679 assert_eq!(seen.lock().unwrap().as_slice(), &[prompt.to_string()]);
1680 }
1681
1682 #[cfg(all(unix, feature = "json"))]
1683 #[tokio::test]
1684 async fn resume_stdin_prompt_reaches_the_child_when_streaming() {
1685 let codex = echoing_stdin_codex();
1686 let prompt = "streamed resumed stdin prompt";
1687 let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1688 let sink = std::sync::Arc::clone(&seen);
1689
1690 ExecResumeCommand::from_stdin(prompt)
1691 .session_id("thread-1")
1692 .stream(&codex, move |event| {
1693 if let Some(text) = event.agent_message_text() {
1694 sink.lock().unwrap().push(text);
1695 }
1696 })
1697 .await
1698 .unwrap();
1699
1700 assert_eq!(seen.lock().unwrap().as_slice(), &[prompt.to_string()]);
1701 }
1702
1703 #[cfg(all(unix, feature = "json"))]
1706 #[tokio::test]
1707 async fn a_prompt_larger_than_the_pipe_buffer_still_completes() {
1708 let codex = echoing_stdin_codex();
1709 let prompt = "x".repeat(512 * 1024);
1711
1712 let result = ExecCommand::from_stdin(&prompt)
1713 .execute_json(&codex)
1714 .await
1715 .unwrap();
1716
1717 assert_eq!(result.result.len(), prompt.len());
1718 }
1719
1720 #[cfg(unix)]
1721 fn echoing_stdin_codex() -> Codex {
1722 let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1723 .join("tests")
1724 .join("fake-codex-echo-stdin.sh");
1725 Codex::builder()
1726 .binary("/bin/bash")
1727 .arg(script.to_str().unwrap())
1728 .build()
1729 .expect("bash must exist")
1730 }
1731
1732 #[test]
1733 fn from_stdin_emits_the_dash_positional_not_the_prompt() {
1734 let args = ExecCommand::from_stdin("secret prompt").ephemeral().args();
1735 assert_eq!(args, vec!["exec", "--ephemeral", "-"]);
1736 assert!(!args.iter().any(|a| a.contains("secret")));
1739 }
1740
1741 #[test]
1742 fn prompt_via_stdin_converts_an_existing_prompt() {
1743 let args = ExecCommand::new("hello").prompt_via_stdin().args();
1744 assert_eq!(args, vec!["exec", "-"]);
1745 }
1746
1747 #[test]
1748 fn resume_from_stdin_emits_the_dash_positional_not_the_prompt() {
1749 let args = ExecResumeCommand::from_stdin("secret prompt")
1750 .session_id("thread-1")
1751 .ephemeral()
1752 .args();
1753 assert_eq!(args, vec!["exec", "resume", "--ephemeral", "thread-1", "-"]);
1754 assert!(!args.iter().any(|arg| arg.contains("secret")));
1755 }
1756
1757 #[test]
1758 fn resume_prompt_via_stdin_converts_an_existing_prompt() {
1759 let args = ExecResumeCommand::new()
1760 .session_id("thread-1")
1761 .prompt("hello")
1762 .prompt_via_stdin()
1763 .args();
1764 assert_eq!(args, vec!["exec", "resume", "thread-1", "-"]);
1765 }
1766}