Skip to main content

nils_common/provider_runtime/
exec.rs

1use 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}