Skip to main content

codex_wrapper/command/
sandbox.rs

1//! Run commands within a Codex-provided sandbox (`codex sandbox`).
2//!
3//! As of `codex-cli` 0.145.0 the platform is auto-detected (Seatbelt on macOS,
4//! and so on); the old `codex sandbox <macos|linux|windows>` positional was
5//! removed. The command to run is passed after a `--` separator.
6
7use crate::Codex;
8use crate::command::CodexCommand;
9use crate::error::Result;
10use crate::exec::{self, CommandOutput};
11
12/// Run a command within a Codex-provided sandbox.
13///
14/// Wraps `codex sandbox [OPTIONS] -- <command> [args...]`.
15#[derive(Debug, Clone)]
16pub struct SandboxCommand {
17    command: String,
18    command_args: Vec<String>,
19    config_overrides: Vec<String>,
20    enabled_features: Vec<String>,
21    disabled_features: Vec<String>,
22    permission_profile: Option<String>,
23    profile: Option<String>,
24    cd: Option<String>,
25}
26
27impl SandboxCommand {
28    /// Create a sandbox command for the given program.
29    #[must_use]
30    pub fn new(command: impl Into<String>) -> Self {
31        Self {
32            command: command.into(),
33            command_args: Vec::new(),
34            config_overrides: Vec::new(),
35            enabled_features: Vec::new(),
36            disabled_features: Vec::new(),
37            permission_profile: None,
38            profile: None,
39            cd: None,
40        }
41    }
42
43    /// Add an argument to the sandboxed command.
44    #[must_use]
45    pub fn arg(mut self, arg: impl Into<String>) -> Self {
46        self.command_args.push(arg.into());
47        self
48    }
49
50    /// Add multiple arguments to the sandboxed command.
51    #[must_use]
52    pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
53        self.command_args.extend(args.into_iter().map(Into::into));
54        self
55    }
56
57    /// Override a config key (`-c key=value`). May be called multiple times.
58    #[must_use]
59    pub fn config(mut self, key_value: impl Into<String>) -> Self {
60        self.config_overrides.push(key_value.into());
61        self
62    }
63
64    /// Enable an optional feature flag (`--enable <feature>`).
65    #[must_use]
66    pub fn enable(mut self, feature: impl Into<String>) -> Self {
67        self.enabled_features.push(feature.into());
68        self
69    }
70
71    /// Disable an optional feature flag (`--disable <feature>`).
72    #[must_use]
73    pub fn disable(mut self, feature: impl Into<String>) -> Self {
74        self.disabled_features.push(feature.into());
75        self
76    }
77
78    /// Named permissions profile to apply (`-P, --permission-profile <NAME>`).
79    #[must_use]
80    pub fn permission_profile(mut self, name: impl Into<String>) -> Self {
81        self.permission_profile = Some(name.into());
82        self
83    }
84
85    /// Named config profile to layer on top of the base config
86    /// (`-p, --profile <NAME>`).
87    #[must_use]
88    pub fn profile(mut self, name: impl Into<String>) -> Self {
89        self.profile = Some(name.into());
90        self
91    }
92
93    /// Working directory for profile resolution and command execution
94    /// (`-C, --cd <DIR>`).
95    #[must_use]
96    pub fn cd(mut self, dir: impl Into<String>) -> Self {
97        self.cd = Some(dir.into());
98        self
99    }
100}
101
102impl CodexCommand for SandboxCommand {
103    type Output = CommandOutput;
104
105    fn args(&self) -> Vec<String> {
106        let mut args = vec!["sandbox".to_string()];
107        for value in &self.config_overrides {
108            args.push("-c".into());
109            args.push(value.clone());
110        }
111        for value in &self.enabled_features {
112            args.push("--enable".into());
113            args.push(value.clone());
114        }
115        for value in &self.disabled_features {
116            args.push("--disable".into());
117            args.push(value.clone());
118        }
119        if let Some(name) = &self.permission_profile {
120            args.push("--permission-profile".into());
121            args.push(name.clone());
122        }
123        if let Some(name) = &self.profile {
124            args.push("--profile".into());
125            args.push(name.clone());
126        }
127        if let Some(dir) = &self.cd {
128            args.push("--cd".into());
129            args.push(dir.clone());
130        }
131        args.push("--".into());
132        args.push(self.command.clone());
133        args.extend(self.command_args.clone());
134        args
135    }
136
137    async fn execute(&self, codex: &Codex) -> Result<CommandOutput> {
138        exec::run_codex(codex, self.args()).await
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::command::CodexCommand;
146
147    #[test]
148    fn sandbox_basic_args() {
149        let cmd = SandboxCommand::new("ls").arg("-la");
150        assert_eq!(CodexCommand::args(&cmd), vec!["sandbox", "--", "ls", "-la"]);
151    }
152
153    #[test]
154    fn sandbox_args_with_options() {
155        let cmd = SandboxCommand::new("cat")
156            .permission_profile("readonly")
157            .cd("/tmp")
158            .args(["/etc/hosts"]);
159        assert_eq!(
160            CodexCommand::args(&cmd),
161            vec![
162                "sandbox",
163                "--permission-profile",
164                "readonly",
165                "--cd",
166                "/tmp",
167                "--",
168                "cat",
169                "/etc/hosts",
170            ]
171        );
172    }
173}