nils_common/provider_runtime/
exec.rs1use crate::env as shared_env;
2use crate::process as shared_process;
3use std::io::Write;
4use std::sync::atomic::Ordering;
5
6use super::error::CoreError;
7use super::profile::{ExecInvocation, ProviderProfile};
8
9#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
10pub struct ExecOptions {
11 pub ephemeral: bool,
12}
13
14pub fn require_allow_dangerous(
15 profile: &ProviderProfile,
16 caller: Option<&str>,
17 stderr: &mut impl Write,
18) -> bool {
19 if is_true_env(profile, stderr) {
20 return true;
21 }
22
23 let prefix = match caller {
24 Some(value) if !value.is_empty() => value,
25 _ => profile.exec.default_caller_prefix,
26 };
27 let _ = writeln!(
28 stderr,
29 "{prefix}: disabled (set {}=true)",
30 profile.env.allow_dangerous_enabled,
31 );
32 false
33}
34
35pub fn allow_dangerous_status(
36 profile: &ProviderProfile,
37 caller: Option<&str>,
38) -> (bool, Option<String>) {
39 let mut stderr = Vec::new();
40 let enabled = require_allow_dangerous(profile, caller, &mut stderr);
41 let text = String::from_utf8_lossy(&stderr).trim_end().to_string();
42 (enabled, if text.is_empty() { None } else { Some(text) })
43}
44
45pub fn check_allow_dangerous(
46 profile: &ProviderProfile,
47 caller: Option<&str>,
48) -> Result<(), CoreError> {
49 let (enabled, message) = allow_dangerous_status(profile, caller);
50 if enabled {
51 return Ok(());
52 }
53 Err(CoreError::validation(
54 "disabled-policy",
55 message.unwrap_or_else(|| {
56 format!(
57 "execution disabled (set {}=true)",
58 profile.env.allow_dangerous_enabled,
59 )
60 }),
61 )
62 .with_retryable(false))
63}
64
65pub fn exec_dangerous(
66 profile: &ProviderProfile,
67 prompt: &str,
68 caller: &str,
69 stderr: &mut impl Write,
70) -> i32 {
71 exec_dangerous_with_options(profile, prompt, caller, stderr, ExecOptions::default())
72}
73
74pub fn exec_dangerous_with_options(
75 profile: &ProviderProfile,
76 prompt: &str,
77 caller: &str,
78 stderr: &mut impl Write,
79 options: ExecOptions,
80) -> i32 {
81 if prompt.is_empty() {
82 let _ = writeln!(
83 stderr,
84 "{}: missing prompt",
85 profile.exec.missing_prompt_label
86 );
87 return 1;
88 }
89
90 if !require_allow_dangerous(profile, Some(caller), stderr) {
91 return 1;
92 }
93
94 match profile.exec.invocation {
95 ExecInvocation::CodexStyle => exec_dangerous_codex_style(profile, prompt, stderr, options),
96 ExecInvocation::GeminiStyle => exec_dangerous_gemini_style(profile, prompt, stderr),
97 }
98}
99
100fn exec_dangerous_codex_style(
101 profile: &ProviderProfile,
102 prompt: &str,
103 stderr: &mut impl Write,
104 options: ExecOptions,
105) -> i32 {
106 let model = shared_env::env_or_default(profile.env.model, profile.defaults.model);
107 let reasoning = shared_env::env_or_default(profile.env.reasoning, profile.defaults.reasoning);
108 let reasoning_arg = format!("model_reasoning_effort=\"{}\"", reasoning);
109 let mut args = vec![
110 "exec".to_string(),
111 "--dangerously-bypass-approvals-and-sandbox".to_string(),
112 "-s".to_string(),
113 "workspace-write".to_string(),
114 "-m".to_string(),
115 model,
116 "-c".to_string(),
117 reasoning_arg,
118 ];
119 if options.ephemeral {
120 args.push("--ephemeral".to_string());
121 }
122 args.push("--".to_string());
123 args.push(prompt.to_string());
124
125 let arg_refs = args.iter().map(String::as_str).collect::<Vec<_>>();
126 run_exec(profile, &arg_refs, stderr)
127}
128
129fn exec_dangerous_gemini_style(
130 profile: &ProviderProfile,
131 prompt: &str,
132 stderr: &mut impl Write,
133) -> i32 {
134 let model = shared_env::env_or_default(profile.env.model, profile.defaults.model);
135 let prompt_arg = format!("--prompt={prompt}");
136 let args = [
137 prompt_arg.as_str(),
138 "--model",
139 model.as_str(),
140 "--approval-mode",
141 "yolo",
142 ];
143
144 run_exec(profile, &args, stderr)
145}
146
147fn run_exec(profile: &ProviderProfile, args: &[&str], stderr: &mut impl Write) -> i32 {
148 match shared_process::run_status_inherit(profile.exec.binary_name, args) {
149 Ok(status) => status.code().unwrap_or(1),
150 Err(err) => {
151 let _ = writeln!(stderr, "{}: {err}", profile.exec.failed_exec_message_prefix,);
152 1
153 }
154 }
155}
156
157fn is_true_env(profile: &ProviderProfile, stderr: &mut impl Write) -> bool {
158 let key = profile.env.allow_dangerous_enabled;
159 let Ok(raw) = std::env::var(key) else {
160 return false;
161 };
162
163 let trimmed = raw.trim();
164 if trimmed.is_empty() {
165 return false;
166 }
167
168 match trimmed.to_ascii_lowercase().as_str() {
169 "true" => true,
170 "false" => false,
171 _ => {
172 if !profile
173 .exec
174 .warned_invalid_allow_dangerous
175 .swap(true, Ordering::SeqCst)
176 {
177 let _ = writeln!(
178 stderr,
179 "warning: {key} must be true|false (got: {raw}); treating as false"
180 );
181 }
182 false
183 }
184 }
185}