feat: flip the sense of the --sandbox option

This commit is contained in:
Michael Bolin
2025-04-29 13:08:57 -07:00
parent 3b39964f81
commit e92caeafac
21 changed files with 370 additions and 217 deletions

View File

@@ -15,7 +15,6 @@ use std::process::ExitStatus;
pub(crate) fn run_landlock(
command: Vec<String>,
sandbox_policy: SandboxPolicy,
writable_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
if command.is_empty() {
anyhow::bail!("command args are empty");
@@ -30,6 +29,7 @@ pub(crate) fn run_landlock(
}
if sandbox_policy.is_file_write_restricted() {
let writable_roots = sandbox_policy.get_writable_roots();
codex_core::linux::install_filesystem_landlock_rules_on_current_thread(writable_roots)?;
}

View File

@@ -7,7 +7,7 @@ use std::path::PathBuf;
use clap::ArgAction;
use clap::Parser;
use codex_core::SandboxModeCliArg;
use codex_core::protocol::SandboxPolicy;
use codex_exec::Cli as ExecCli;
use codex_repl::Cli as ReplCli;
use codex_tui::Cli as TuiCli;
@@ -71,9 +71,9 @@ struct SeatbeltCommand {
#[arg(long = "writable-root", short = 'w', value_name = "DIR", action = ArgAction::Append, use_value_delimiter = false)]
writable_roots: Vec<PathBuf>,
/// Configure the process restrictions for the command.
#[arg(long = "sandbox", short = 's')]
sandbox_policy: SandboxModeCliArg,
/// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR)
#[arg(long = "full-auto", default_value_t = false)]
full_auto: bool,
/// Full command args to run under seatbelt.
#[arg(trailing_var_arg = true)]
@@ -86,9 +86,9 @@ struct LandlockCommand {
#[arg(long = "writable-root", short = 'w', value_name = "DIR", action = ArgAction::Append, use_value_delimiter = false)]
writable_roots: Vec<PathBuf>,
/// Configure the process restrictions for the command.
#[arg(long = "sandbox", short = 's')]
sandbox_policy: SandboxModeCliArg,
/// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR)
#[arg(long = "full-auto", default_value_t = false)]
full_auto: bool,
/// Full command args to run under landlock.
#[arg(trailing_var_arg = true)]
@@ -118,18 +118,20 @@ async fn main() -> anyhow::Result<()> {
Some(Subcommand::Debug(debug_args)) => match debug_args.cmd {
DebugCommand::Seatbelt(SeatbeltCommand {
command,
sandbox_policy,
writable_roots,
full_auto,
}) => {
seatbelt::run_seatbelt(command, sandbox_policy.into(), writable_roots).await?;
let sandbox_policy = create_sandbox_policy(full_auto, writable_roots);
seatbelt::run_seatbelt(command, sandbox_policy).await?;
}
#[cfg(target_os = "linux")]
DebugCommand::Landlock(LandlockCommand {
command,
sandbox_policy,
writable_roots,
full_auto,
}) => {
landlock::run_landlock(command, sandbox_policy.into(), writable_roots)?;
let sandbox_policy = create_sandbox_policy(full_auto, writable_roots);
landlock::run_landlock(command, sandbox_policy)?;
}
#[cfg(not(target_os = "linux"))]
DebugCommand::Landlock(_) => {
@@ -140,3 +142,11 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}
fn create_sandbox_policy(full_auto: bool, writable_roots: Vec<PathBuf>) -> SandboxPolicy {
if full_auto {
SandboxPolicy::new_full_auto_policy_with_writable_roots(writable_roots)
} else {
SandboxPolicy::new_read_only_policy_with_writable_roots(writable_roots)
}
}

View File

@@ -1,13 +1,11 @@
use codex_core::exec::create_seatbelt_command;
use codex_core::protocol::SandboxPolicy;
use std::path::PathBuf;
pub(crate) async fn run_seatbelt(
command: Vec<String>,
sandbox_policy: SandboxPolicy,
writable_roots: Vec<PathBuf>,
) -> anyhow::Result<()> {
let seatbelt_command = create_seatbelt_command(command, sandbox_policy, &writable_roots);
let seatbelt_command = create_seatbelt_command(command, &sandbox_policy);
let status = tokio::process::Command::new(seatbelt_command[0].clone())
.args(&seatbelt_command[1..])
.spawn()

View File

@@ -4,7 +4,6 @@
use clap::ValueEnum;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")]
@@ -24,19 +23,6 @@ pub enum ApprovalModeCliArg {
Never,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum SandboxModeCliArg {
/// Network syscalls will be blocked
NetworkRestricted,
/// Filesystem writes will be restricted
FileWriteRestricted,
/// Network and filesystem writes will be restricted
NetworkAndFileWriteRestricted,
/// No restrictions; full "unsandboxed" mode
DangerousNoRestrictions,
}
impl From<ApprovalModeCliArg> for AskForApproval {
fn from(value: ApprovalModeCliArg) -> Self {
match value {
@@ -46,16 +32,3 @@ impl From<ApprovalModeCliArg> for AskForApproval {
}
}
}
impl From<SandboxModeCliArg> for SandboxPolicy {
fn from(value: SandboxModeCliArg) -> Self {
match value {
SandboxModeCliArg::NetworkRestricted => SandboxPolicy::NetworkRestricted,
SandboxModeCliArg::FileWriteRestricted => SandboxPolicy::FileWriteRestricted,
SandboxModeCliArg::NetworkAndFileWriteRestricted => {
SandboxPolicy::NetworkAndFileWriteRestricted
}
SandboxModeCliArg::DangerousNoRestrictions => SandboxPolicy::DangerousNoRestrictions,
}
}
}

View File

@@ -861,7 +861,7 @@ async fn handle_function_call(
assess_command_safety(
&params.command,
sess.approval_policy,
sess.sandbox_policy,
&sess.sandbox_policy,
&state.approved_commands,
)
};
@@ -916,14 +916,11 @@ async fn handle_function_call(
)
.await;
let roots_snapshot = { sess.writable_roots.lock().unwrap().clone() };
let output_result = process_exec_tool_call(
params.clone(),
sandbox_type,
&roots_snapshot,
sess.ctrl_c.clone(),
sess.sandbox_policy,
&sess.sandbox_policy,
)
.await;
@@ -1006,16 +1003,13 @@ async fn handle_function_call(
)
.await;
let retry_roots = { sess.writable_roots.lock().unwrap().clone() };
// This is an escalated retry; the policy will not be
// examined and the sandbox has been set to `None`.
let retry_output_result = process_exec_tool_call(
params.clone(),
SandboxType::None,
&retry_roots,
sess.ctrl_c.clone(),
sess.sandbox_policy,
&sess.sandbox_policy,
)
.await;

View File

@@ -1,5 +1,6 @@
use crate::flags::OPENAI_DEFAULT_MODEL;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPermission;
use crate::protocol::SandboxPolicy;
use dirs::home_dir;
use serde::Deserialize;
@@ -11,27 +12,66 @@ use std::path::PathBuf;
const EMBEDDED_INSTRUCTIONS: &str = include_str!("../prompt.md");
/// Application configuration loaded from disk and merged with overrides.
#[derive(Deserialize, Debug, Clone)]
#[derive(Debug, Clone)]
pub struct Config {
/// Optional override of model selection.
#[serde(default = "default_model")]
pub model: String,
/// Default approval policy for executing commands.
#[serde(default)]
/// Approval policy for executing commands.
pub approval_policy: AskForApproval,
#[serde(default)]
pub sandbox_policy: SandboxPolicy,
/// Disable server-side response storage (sends the full conversation
/// context with every request). Currently necessary for OpenAI customers
/// who have opted into Zero Data Retention (ZDR).
#[serde(default)]
pub disable_response_storage: bool,
/// System instructions.
pub instructions: Option<String>,
}
/// Base config deserialized from ~/.codex/config.toml.
#[derive(Deserialize, Debug, Clone, Default)]
pub struct ConfigToml {
/// Optional override of model selection.
pub model: Option<String>,
/// Default approval policy for executing commands.
pub approval_policy: Option<AskForApproval>,
pub sandbox_permissions: Option<Vec<SandboxPermission>>,
/// Disable server-side response storage (sends the full conversation
/// context with every request). Currently necessary for OpenAI customers
/// who have opted into Zero Data Retention (ZDR).
pub disable_response_storage: Option<bool>,
/// System instructions.
pub instructions: Option<String>,
}
impl ConfigToml {
/// Attempt to parse the file at `~/.codex/config.toml` into a Config.
fn load_from_toml() -> std::io::Result<Self> {
let config_toml_path = codex_dir()?.join("config.toml");
match std::fs::read_to_string(&config_toml_path) {
Ok(contents) => toml::from_str::<Self>(&contents).map_err(|e| {
tracing::error!("Failed to parse config.toml: {e}");
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::info!("config.toml not found, using defaults");
Ok(Self::default())
}
Err(e) => {
tracing::error!("Failed to read config.toml: {e}");
Err(e)
}
}
}
}
/// Optional overrides for user configuration (e.g., from CLI flags).
#[derive(Default, Debug, Clone)]
pub struct ConfigOverrides {
@@ -46,11 +86,14 @@ impl Config {
/// ~/.codex/config.toml, ~/.codex/instructions.md, embedded defaults, and
/// any values provided in `overrides` (highest precedence).
pub fn load_with_overrides(overrides: ConfigOverrides) -> std::io::Result<Self> {
let mut cfg: Config = Self::load_from_toml()?;
let cfg: ConfigToml = ConfigToml::load_from_toml()?;
tracing::warn!("Config parsed from config.toml: {cfg:?}");
Ok(Self::load_from_base_config_with_overrides(cfg, overrides))
}
fn load_from_base_config_with_overrides(cfg: ConfigToml, overrides: ConfigOverrides) -> Self {
// Instructions: user-provided instructions.md > embedded default.
cfg.instructions =
let instructions =
Self::load_instructions().or_else(|| Some(EMBEDDED_INSTRUCTIONS.to_string()));
// Destructure ConfigOverrides fully to ensure all overrides are applied.
@@ -61,57 +104,48 @@ impl Config {
disable_response_storage,
} = overrides;
if let Some(model) = model {
cfg.model = model;
}
if let Some(approval_policy) = approval_policy {
cfg.approval_policy = approval_policy;
}
if let Some(sandbox_policy) = sandbox_policy {
cfg.sandbox_policy = sandbox_policy;
}
if let Some(disable_response_storage) = disable_response_storage {
cfg.disable_response_storage = disable_response_storage;
}
Ok(cfg)
}
/// Attempt to parse the file at `~/.codex/config.toml` into a Config.
fn load_from_toml() -> std::io::Result<Self> {
let config_toml_path = codex_dir()?.join("config.toml");
match std::fs::read_to_string(&config_toml_path) {
Ok(contents) => toml::from_str::<Self>(&contents).map_err(|e| {
tracing::error!("Failed to parse config.toml: {e}");
std::io::Error::new(std::io::ErrorKind::InvalidData, e)
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
tracing::info!("config.toml not found, using defaults");
Ok(Self::load_default_config())
}
Err(e) => {
tracing::error!("Failed to read config.toml: {e}");
Err(e)
let sandbox_policy = match sandbox_policy {
Some(sandbox_policy) => sandbox_policy,
None => {
// Derive a SandboxPolicy from the permissions in the config.
match cfg.sandbox_permissions {
// Note this means the user can explicitly set permissions
// to the empty list in the config file, granting it no
// permissions whatsoever.
Some(permissions) => SandboxPolicy::from(permissions),
// Default to read only rather than completely locked down.
None => SandboxPolicy::new_read_only_policy(),
}
}
};
Self {
model: model.or(cfg.model).unwrap_or_else(default_model),
approval_policy: approval_policy
.or(cfg.approval_policy)
.unwrap_or_else(AskForApproval::default),
sandbox_policy,
disable_response_storage: disable_response_storage
.or(cfg.disable_response_storage)
.unwrap_or(false),
instructions,
}
}
/// Meant to be used exclusively for tests: load_with_overrides() should be
/// used in all other cases.
pub fn load_default_config_for_test() -> Self {
Self::load_default_config()
}
fn load_default_config() -> Self {
// Load from an empty string to exercise #[serde(default)] to
// get the default values for each field.
toml::from_str::<Self>("").expect("empty string should parse as TOML")
}
fn load_instructions() -> Option<String> {
let mut p = codex_dir().ok()?;
p.push("instructions.md");
std::fs::read_to_string(&p).ok()
}
/// Meant to be used exclusively for tests: load_with_overrides() should be
/// used in all other cases.
pub fn load_default_config_for_test() -> Self {
Self::load_from_base_config_with_overrides(
ConfigToml::default(),
ConfigOverrides::default(),
)
}
}
fn default_model() -> String {

View File

@@ -1,7 +1,6 @@
use std::io;
#[cfg(target_family = "unix")]
use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
use std::process::ExitStatus;
use std::process::Stdio;
use std::sync::Arc;
@@ -33,7 +32,7 @@ const DEFAULT_TIMEOUT_MS: u64 = 10_000;
const SIGKILL_CODE: i32 = 9;
const TIMEOUT_CODE: i32 = 64;
const MACOS_SEATBELT_READONLY_POLICY: &str = include_str!("seatbelt_readonly_policy.sbpl");
const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl");
/// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin`
/// to defend against an attacker trying to inject a malicious version on the
@@ -67,19 +66,17 @@ pub enum SandboxType {
#[cfg(target_os = "linux")]
async fn exec_linux(
params: ExecParams,
writable_roots: &[PathBuf],
ctrl_c: Arc<Notify>,
sandbox_policy: SandboxPolicy,
) -> Result<RawExecToolCallOutput> {
crate::linux::exec_linux(params, writable_roots, ctrl_c, sandbox_policy).await
crate::linux::exec_linux(params, ctrl_c, sandbox_policy).await
}
#[cfg(not(target_os = "linux"))]
async fn exec_linux(
_params: ExecParams,
_writable_roots: &[PathBuf],
_ctrl_c: Arc<Notify>,
_sandbox_policy: SandboxPolicy,
_sandbox_policy: &SandboxPolicy,
) -> Result<RawExecToolCallOutput> {
Err(CodexErr::Io(io::Error::new(
io::ErrorKind::InvalidInput,
@@ -90,9 +87,8 @@ async fn exec_linux(
pub async fn process_exec_tool_call(
params: ExecParams,
sandbox_type: SandboxType,
writable_roots: &[PathBuf],
ctrl_c: Arc<Notify>,
sandbox_policy: SandboxPolicy,
sandbox_policy: &SandboxPolicy,
) -> Result<ExecToolCallOutput> {
let start = Instant::now();
@@ -104,7 +100,7 @@ pub async fn process_exec_tool_call(
workdir,
timeout_ms,
} = params;
let seatbelt_command = create_seatbelt_command(command, sandbox_policy, writable_roots);
let seatbelt_command = create_seatbelt_command(command, sandbox_policy);
exec(
ExecParams {
command: seatbelt_command,
@@ -115,9 +111,7 @@ pub async fn process_exec_tool_call(
)
.await
}
SandboxType::LinuxSeccomp => {
exec_linux(params, writable_roots, ctrl_c, sandbox_policy).await
}
SandboxType::LinuxSeccomp => exec_linux(params, ctrl_c, sandbox_policy).await,
};
let duration = start.elapsed();
match raw_output_result {
@@ -162,41 +156,61 @@ pub async fn process_exec_tool_call(
pub fn create_seatbelt_command(
command: Vec<String>,
sandbox_policy: SandboxPolicy,
writable_roots: &[PathBuf],
sandbox_policy: &SandboxPolicy,
) -> Vec<String> {
let (policies, cli_args): (Vec<String>, Vec<String>) = writable_roots
.iter()
.enumerate()
.map(|(index, root)| {
let param_name = format!("WRITABLE_ROOT_{index}");
let policy: String = format!("(subpath (param \"{param_name}\"))");
let cli_arg = format!("-D{param_name}={}", root.to_string_lossy());
(policy, cli_arg)
})
.unzip();
// TODO(ragona): The seatbelt policy should reflect the SandboxPolicy that
// is passed, but everything is currently hardcoded to use
// MACOS_SEATBELT_READONLY_POLICY.
// TODO(mbolin): apply_patch calls must also honor the SandboxPolicy.
if !matches!(sandbox_policy, SandboxPolicy::NetworkRestricted) {
tracing::error!("specified sandbox policy {sandbox_policy:?} will not be honroed");
}
let full_policy = if policies.is_empty() {
MACOS_SEATBELT_READONLY_POLICY.to_string()
} else {
let scoped_write_policy = format!("(allow file-write*\n{}\n)", policies.join(" "));
format!("{MACOS_SEATBELT_READONLY_POLICY}\n{scoped_write_policy}")
let (file_write_policy, extra_cli_args) = {
if sandbox_policy.has_full_disk_write_access() {
// Allegedly, this is more permissive than `(allow file-write*)`.
(
r#"(allow file-write* (regex #"^/"))"#.to_string(),
Vec::<String>::new(),
)
} else {
let writable_roots = sandbox_policy.get_writable_roots();
let (writable_folder_policies, cli_args): (Vec<String>, Vec<String>) = writable_roots
.iter()
.enumerate()
.map(|(index, root)| {
let param_name = format!("WRITABLE_ROOT_{index}");
let policy: String = format!("(subpath (param \"{param_name}\"))");
let cli_arg = format!("-D{param_name}={}", root.to_string_lossy());
(policy, cli_arg)
})
.unzip();
if writable_folder_policies.is_empty() {
("".to_string(), Vec::<String>::new())
} else {
let file_write_policy = format!(
"(allow file-write*\n{}\n)",
writable_folder_policies.join(" ")
);
(file_write_policy, cli_args)
}
}
};
let file_read_policy = if sandbox_policy.has_full_disk_read_access() {
"; allow read-only file operations\n(allow file-read*)"
} else {
""
};
// TODO(mbolin): apply_patch calls must also honor the SandboxPolicy.
let network_policy = if sandbox_policy.has_full_network_access() {
"(allow network-outbound)\n(allow network-inbound)\n(allow system-socket)"
} else {
""
};
let full_policy = format!(
"{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}"
);
let mut seatbelt_command: Vec<String> = vec![
MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string(),
"-p".to_string(),
full_policy.to_string(),
full_policy,
];
seatbelt_command.extend(cli_args);
seatbelt_command.extend(extra_cli_args);
seatbelt_command.push("--".to_string());
seatbelt_command.extend(command);
seatbelt_command

View File

@@ -27,5 +27,3 @@ pub use codex::Codex;
mod approval_mode_cli_arg;
#[cfg(feature = "cli")]
pub use approval_mode_cli_arg::ApprovalModeCliArg;
#[cfg(feature = "cli")]
pub use approval_mode_cli_arg::SandboxModeCliArg;

View File

@@ -32,14 +32,13 @@ use tokio::sync::Notify;
pub async fn exec_linux(
params: ExecParams,
writable_roots: &[PathBuf],
ctrl_c: Arc<Notify>,
sandbox_policy: SandboxPolicy,
) -> Result<RawExecToolCallOutput> {
// Allow READ on /
// Allow WRITE on /dev/null
let ctrl_c_copy = ctrl_c.clone();
let writable_roots_copy = writable_roots.to_vec();
let writable_roots = sandbox_policy.get_writable_roots();
// Isolate thread to run the sandbox from
let tool_call_output = std::thread::spawn(move || {

View File

@@ -93,44 +93,169 @@ pub enum AskForApproval {
}
/// Determines execution restrictions for model shell commands
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SandboxPolicy {
/// Network syscalls will be blocked
NetworkRestricted,
/// Filesystem writes will be restricted
FileWriteRestricted,
/// Network and filesystem writes will be restricted
#[default]
NetworkAndFileWriteRestricted,
/// No restrictions; full "unsandboxed" mode
DangerousNoRestrictions,
pub struct SandboxPolicy {
permissions: Vec<SandboxPermission>,
}
impl From<Vec<SandboxPermission>> for SandboxPolicy {
fn from(permissions: Vec<SandboxPermission>) -> Self {
Self { permissions }
}
}
impl SandboxPolicy {
pub fn is_dangerous(&self) -> bool {
match self {
SandboxPolicy::NetworkRestricted => false,
SandboxPolicy::FileWriteRestricted => false,
SandboxPolicy::NetworkAndFileWriteRestricted => false,
SandboxPolicy::DangerousNoRestrictions => true,
pub fn new_read_only_policy() -> Self {
Self {
permissions: vec![SandboxPermission::DiskFullReadAccess],
}
}
pub fn is_network_restricted(&self) -> bool {
matches!(
self,
SandboxPolicy::NetworkRestricted | SandboxPolicy::NetworkAndFileWriteRestricted
)
pub fn new_read_only_policy_with_writable_roots(writable_roots: Vec<PathBuf>) -> Self {
let mut permissions = Self::new_read_only_policy().permissions;
permissions.extend(
writable_roots
.into_iter()
.map(|folder| SandboxPermission::DiskWriteFolder { folder }),
);
Self { permissions }
}
pub fn is_file_write_restricted(&self) -> bool {
matches!(
self,
SandboxPolicy::FileWriteRestricted | SandboxPolicy::NetworkAndFileWriteRestricted
)
pub fn new_full_auto_policy() -> Self {
Self {
permissions: vec![
SandboxPermission::DiskFullReadAccess,
SandboxPermission::DiskWritePlatformUserTempFolder,
SandboxPermission::DiskWriteCwd,
],
}
}
pub fn new_full_auto_policy_with_writable_roots(writable_roots: Vec<PathBuf>) -> Self {
let mut permissions = Self::new_full_auto_policy().permissions;
permissions.extend(
writable_roots
.into_iter()
.map(|folder| SandboxPermission::DiskWriteFolder { folder }),
);
Self { permissions }
}
pub fn has_full_disk_read_access(&self) -> bool {
self.permissions
.iter()
.any(|perm| matches!(perm, SandboxPermission::DiskFullReadAccess))
}
pub fn has_full_disk_write_access(&self) -> bool {
self.permissions
.iter()
.any(|perm| matches!(perm, SandboxPermission::DiskFullWriteAccess))
}
pub fn has_full_network_access(&self) -> bool {
self.permissions
.iter()
.any(|perm| matches!(perm, SandboxPermission::NetworkFullAccess))
}
pub fn get_writable_roots(&self) -> Vec<PathBuf> {
let mut writable_roots = Vec::<PathBuf>::new();
for perm in &self.permissions {
use SandboxPermission::*;
match perm {
DiskWritePlatformUserTempFolder => {
if cfg!(target_os = "macos") {
if let Some(tempdir) = std::env::var_os("TMPDIR") {
// Likely something that starts with /var/folders/...
let tmpdir_path = PathBuf::from(&tempdir);
if tmpdir_path.is_absolute() {
writable_roots.push(tmpdir_path.clone());
match tmpdir_path.canonicalize() {
Ok(canonicalized) => {
// Likely something that starts with /private/var/folders/...
if canonicalized != tmpdir_path {
writable_roots.push(canonicalized);
}
}
Err(e) => {
tracing::error!("Failed to canonicalize TMPDIR: {e}");
}
}
} else {
tracing::error!("TMPDIR is not an absolute path: {tempdir:?}");
}
}
}
// For Linux, should this be XDG_RUNTIME_DIR, /run/user/<uid>, or something else?
}
DiskWritePlatformGlobalTempFolder => {
if cfg!(unix) {
writable_roots.push(PathBuf::from("/tmp"));
}
}
DiskWriteCwd => match std::env::current_dir() {
Ok(cwd) => writable_roots.push(cwd),
Err(err) => {
tracing::error!("Failed to get current working directory: {err}");
}
},
DiskWriteFolder { folder } => {
writable_roots.push(folder.clone());
}
DiskFullReadAccess | NetworkFullAccess => {}
DiskFullWriteAccess => {
// Currently, we expect callers to only invoke this method
// after verifying has_full_disk_write_access() is false.
}
}
}
writable_roots
}
pub fn is_unrestricted(&self) -> bool {
self.has_full_disk_read_access()
&& self.has_full_disk_write_access()
&& self.has_full_network_access()
}
}
/// Permissions that should be granted to the sandbox in which the agent
/// operates.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SandboxPermission {
/// Is allowed to read all files on disk.
DiskFullReadAccess,
/// Is allowed to write to the operating system's temp dir that
/// is restricted to the user the agent is running as. For
/// example, on macOS, this is generally something under
/// `/var/folders` as opposed to `/tmp`.
DiskWritePlatformUserTempFolder,
/// Is allowed to write to the operating system's shared temp
/// dir. On UNIX, this is generally `/tmp`.
DiskWritePlatformGlobalTempFolder,
/// Is allowed to write to the current working directory (in practice, this
/// is the `cwd` where `codex` was spawned).
DiskWriteCwd,
/// Is allowed to the specified folder. `PathBuf` must be an
/// absolute path, though it is up to the caller to canonicalize
/// it if the path contains symlinks.
DiskWriteFolder { folder: PathBuf },
/// Is allowed to write to any file on disk.
DiskFullWriteAccess,
/// Can make arbitrary network requests.
NetworkFullAccess,
}
/// User input
#[non_exhaustive]
#[derive(Debug, Clone, Deserialize, Serialize)]

View File

@@ -65,7 +65,7 @@ pub fn assess_patch_safety(
pub fn assess_command_safety(
command: &[String],
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
sandbox_policy: &SandboxPolicy,
approved: &HashSet<Vec<String>>,
) -> SafetyCheck {
let approve_without_sandbox = || SafetyCheck::AutoApprove {
@@ -81,11 +81,10 @@ pub fn assess_command_safety(
}
// Command was not known-safe or allow-listed
match sandbox_policy {
// Only the dangerous sandbox policy will run arbitrary commands outside a sandbox
SandboxPolicy::DangerousNoRestrictions => approve_without_sandbox(),
// All other policies try to run the command in a sandbox if it is available
_ => match get_platform_sandbox() {
if sandbox_policy.is_unrestricted() {
approve_without_sandbox()
} else {
match get_platform_sandbox() {
// We have a sandbox, so we can approve the command in all modes
Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type },
None => {
@@ -99,7 +98,7 @@ pub fn assess_command_safety(
_ => SafetyCheck::AskUser,
}
}
},
}
}
}

View File

@@ -6,9 +6,6 @@
; start with closed-by-default
(deny default)
; allow read-only file operations
(allow file-read*)
; child processes inherit the policy of their parent
(allow process-exec)
(allow process-fork)

View File

@@ -55,7 +55,7 @@ async fn spawn_codex() -> Codex {
model: config.model,
instructions: None,
approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
disable_response_storage: false,
},
})

View File

@@ -95,7 +95,7 @@ async fn keeps_previous_response_id_between_tasks() {
model: config.model,
instructions: None,
approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
disable_response_storage: false,
},
})

View File

@@ -78,7 +78,7 @@ async fn retries_on_early_close() {
model: config.model,
instructions: None,
approval_policy: config.approval_policy,
sandbox_policy: SandboxPolicy::NetworkAndFileWriteRestricted,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
disable_response_storage: false,
},
})

View File

@@ -1,6 +1,5 @@
use clap::Parser;
use clap::ValueEnum;
use codex_core::SandboxModeCliArg;
use std::path::PathBuf;
#[derive(Parser, Debug)]
@@ -14,11 +13,9 @@ pub struct Cli {
#[arg(long, short = 'm')]
pub model: Option<String>,
/// Configure the process restrictions when a command is executed.
///
/// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux.
#[arg(long = "sandbox", short = 's')]
pub sandbox_policy: Option<SandboxModeCliArg>,
/// Convenience alias for low-friction sandboxed automatic execution (network-disabled sandbox that can write to cwd and TMPDIR)
#[arg(long = "full-auto", default_value_t = false)]
pub full_auto: bool,
/// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", default_value_t = false)]

View File

@@ -13,6 +13,7 @@ use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::InputItem;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::util::is_inside_git_repo;
use event_processor::EventProcessor;
use owo_colors::OwoColorize;
@@ -26,7 +27,7 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
let Cli {
images,
model,
sandbox_policy,
full_auto,
skip_git_repo_check,
disable_response_storage,
color,
@@ -61,13 +62,19 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
.with_writer(std::io::stderr)
.try_init();
let sandbox_policy = if full_auto {
Some(SandboxPolicy::new_full_auto_policy())
} else {
None
};
// Load configuration and determine approval policy
let overrides = ConfigOverrides {
model,
// This CLI is intended to be headless and has no affordances for asking
// the user for approval.
approval_policy: Some(AskForApproval::Never),
sandbox_policy: sandbox_policy.map(Into::into),
sandbox_policy,
disable_response_storage: if disable_response_storage {
Some(true)
} else {

View File

@@ -1,7 +1,6 @@
use clap::ArgAction;
use clap::Parser;
use codex_core::ApprovalModeCliArg;
use codex_core::SandboxModeCliArg;
use std::path::PathBuf;
/// Commandline arguments.
@@ -37,11 +36,9 @@ pub struct Cli {
#[arg(long = "ask-for-approval", short = 'a')]
pub approval_policy: Option<ApprovalModeCliArg>,
/// Configure the process restrictions when a command is executed.
///
/// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux.
#[arg(long = "sandbox", short = 's')]
pub sandbox_policy: Option<SandboxModeCliArg>,
/// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, network-disabled sandbox that can write to cwd and TMPDIR)
#[arg(long = "full-auto", default_value_t = false)]
pub full_auto: bool,
/// Allow running Codex outside a Git repository. By default the CLI
/// aborts early when the current working directory is **not** inside a

View File

@@ -6,7 +6,9 @@ use std::sync::Arc;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::protocol;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::FileChange;
use codex_core::protocol::SandboxPolicy;
use codex_core::util::is_inside_git_repo;
use codex_core::util::notify_on_sigint;
use codex_core::Codex;
@@ -76,11 +78,20 @@ pub async fn run_main(cli: Cli) -> anyhow::Result<()> {
// Initialize logging before any other work so early errors are captured.
init_logger(cli.verbose, !cli.no_ansi);
let (sandbox_policy, approval_policy) = if cli.full_auto {
(
Some(SandboxPolicy::new_full_auto_policy()),
Some(AskForApproval::OnFailure),
)
} else {
(None, cli.approval_policy.map(Into::into))
};
// Load config file and apply CLI overrides (model & approval policy)
let overrides = ConfigOverrides {
model: cli.model.clone(),
approval_policy: cli.approval_policy.map(Into::into),
sandbox_policy: cli.sandbox_policy.map(Into::into),
approval_policy,
sandbox_policy,
disable_response_storage: if cli.disable_response_storage {
Some(true)
} else {

View File

@@ -1,6 +1,5 @@
use clap::Parser;
use codex_core::ApprovalModeCliArg;
use codex_core::SandboxModeCliArg;
use std::path::PathBuf;
#[derive(Parser, Debug)]
@@ -21,11 +20,9 @@ pub struct Cli {
#[arg(long = "ask-for-approval", short = 'a')]
pub approval_policy: Option<ApprovalModeCliArg>,
/// Configure the process restrictions when a command is executed.
///
/// Uses OS-specific sandboxing tools; Seatbelt on OSX, landlock+seccomp on Linux.
#[arg(long = "sandbox", short = 's')]
pub sandbox_policy: Option<SandboxModeCliArg>,
/// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, network-disabled sandbox that can write to cwd and TMPDIR)
#[arg(long = "full-auto", default_value_t = false)]
pub full_auto: bool,
/// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", default_value_t = false)]
@@ -34,12 +31,4 @@ pub struct Cli {
/// Disable serverside response storage (sends the full conversation context with every request)
#[arg(long = "disable-response-storage", default_value_t = false)]
pub disable_response_storage: bool,
/// Convenience alias for low-friction sandboxed automatic execution (-a on-failure, -s network-and-file-write-restricted)
#[arg(long = "full-auto", default_value_t = true)]
pub full_auto: bool,
/// Convenience alias for supervised sandboxed execution (-a unless-allow-listed, -s network-and-file-write-restricted)
#[arg(long = "suggest", default_value_t = false)]
pub suggest: bool,
}

View File

@@ -6,6 +6,8 @@
use app::App;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy;
use codex_core::util::is_inside_git_repo;
use log_layer::TuiLogLayer;
use std::fs::OpenOptions;
@@ -33,12 +35,21 @@ pub use cli::Cli;
pub fn run_main(cli: Cli) -> std::io::Result<()> {
assert_env_var_set();
let (sandbox_policy, approval_policy) = if cli.full_auto {
(
Some(SandboxPolicy::new_full_auto_policy()),
Some(AskForApproval::OnFailure),
)
} else {
(None, cli.approval_policy.map(Into::into))
};
let config = {
// Load configuration and support CLI overrides.
let overrides = ConfigOverrides {
model: cli.model.clone(),
approval_policy: cli.approval_policy.map(Into::into),
sandbox_policy: cli.sandbox_policy.map(Into::into),
approval_policy,
sandbox_policy,
disable_response_storage: if cli.disable_response_storage {
Some(true)
} else {