refactoring with_escalated_permissions to use SandboxPermissions instead (#7750)

helpful in the future if we want more granularity for requesting
escalated permissions:
e.g when running in readonly sandbox, model can request to escalate to a
sandbox that allows writes
This commit is contained in:
zhao-oai
2025-12-10 09:18:48 -08:00
committed by GitHub
parent 97b90094cd
commit e0fb3ca1db
27 changed files with 216 additions and 179 deletions

View File

@@ -136,6 +136,7 @@ use codex_core::protocol::ReviewRequest;
use codex_core::protocol::ReviewTarget as CoreReviewTarget;
use codex_core::protocol::SessionConfiguredEvent;
use codex_core::read_head_for_summary;
use codex_core::sandboxing::SandboxPermissions;
use codex_feedback::CodexFeedback;
use codex_login::ServerOptions as LoginServerOptions;
use codex_login::ShutdownHandle;
@@ -1191,7 +1192,7 @@ impl CodexMessageProcessor {
cwd,
expiration: timeout_ms.into(),
env,
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};

View File

@@ -48,7 +48,7 @@ When you are running with `approval_policy == on-request`, and sandboxing enable
- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
- You are running sandboxed and need to run a command that requires network access (e.g. installing packages)
- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `with_escalated_permissions` and `justification` parameters - do not message the user before requesting approval for the command.
- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters - do not message the user before requesting approval for the command.
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for
- (for all of these, you should weigh alternative paths that do not require approval)
@@ -59,8 +59,8 @@ You will be told what filesystem sandboxing, network sandboxing, and approval mo
Although they introduce friction to the user because your work is paused until the user responds, you should leverage them when necessary to accomplish important work. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task unless it is set to "never", in which case never ask for approvals.
When requesting approval to execute a command that will require escalated privileges:
- Provide the `with_escalated_permissions` parameter with the boolean value true
- Include a short, 1 sentence explanation for why you need to enable `with_escalated_permissions` in the justification parameter
- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
- Include a short, 1 sentence explanation for why you need escalated permissions in the justification parameter
## Special user requests

View File

@@ -182,7 +182,7 @@ When you are running with `approval_policy == on-request`, and sandboxing enable
- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
- You are running sandboxed and need to run a command that requires network access (e.g. installing packages)
- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `with_escalated_permissions` and `justification` parameters. Within this harness, prefer requesting approval via the tool over asking in natural language.
- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters. Within this harness, prefer requesting approval via the tool over asking in natural language.
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for
- (for all of these, you should weigh alternative paths that do not require approval)
@@ -193,8 +193,8 @@ You will be told what filesystem sandboxing, network sandboxing, and approval mo
Although they introduce friction to the user because your work is paused until the user responds, you should leverage them when necessary to accomplish important work. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task unless it is set to "never", in which case never ask for approvals.
When requesting approval to execute a command that will require escalated privileges:
- Provide the `with_escalated_permissions` parameter with the boolean value true
- Include a short, 1 sentence explanation for why you need to enable `with_escalated_permissions` in the justification parameter
- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
- Include a short, 1 sentence explanation for why you need escalated permissions in the justification parameter
## Validating your work

View File

@@ -48,7 +48,7 @@ When you are running with `approval_policy == on-request`, and sandboxing enable
- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
- You are running sandboxed and need to run a command that requires network access (e.g. installing packages)
- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `with_escalated_permissions` and `justification` parameters - do not message the user before requesting approval for the command.
- If you run a command that is important to solving the user's query, but it fails because of sandboxing, rerun the command with approval. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters - do not message the user before requesting approval for the command.
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for
- (for all of these, you should weigh alternative paths that do not require approval)
@@ -59,8 +59,8 @@ You will be told what filesystem sandboxing, network sandboxing, and approval mo
Although they introduce friction to the user because your work is paused until the user responds, you should leverage them when necessary to accomplish important work. If the completing the task requires escalated permissions, Do not let these settings or the sandbox deter you from attempting to accomplish the user's task unless it is set to "never", in which case never ask for approvals.
When requesting approval to execute a command that will require escalated privileges:
- Provide the `with_escalated_permissions` parameter with the boolean value true
- Include a short, 1 sentence explanation for why you need to enable `with_escalated_permissions` in the justification parameter
- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
- Include a short, 1 sentence explanation for why you need escalated permissions in the justification parameter
## Special user requests

View File

@@ -3325,6 +3325,7 @@ mod tests {
use crate::exec::ExecParams;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use crate::sandboxing::SandboxPermissions;
use crate::turn_diff_tracker::TurnDiffTracker;
use std::collections::HashMap;
@@ -3335,6 +3336,7 @@ mod tests {
let mut turn_context = Arc::new(turn_context_raw);
let timeout_ms = 1000;
let sandbox_permissions = SandboxPermissions::RequireEscalated;
let params = ExecParams {
command: if cfg!(windows) {
vec![
@@ -3352,13 +3354,13 @@ mod tests {
cwd: turn_context.cwd.clone(),
expiration: timeout_ms.into(),
env: HashMap::new(),
with_escalated_permissions: Some(true),
sandbox_permissions,
justification: Some("test".to_string()),
arg0: None,
};
let params2 = ExecParams {
with_escalated_permissions: Some(false),
sandbox_permissions: SandboxPermissions::UseDefault,
command: params.command.clone(),
cwd: params.cwd.clone(),
expiration: timeout_ms.into(),
@@ -3385,7 +3387,7 @@ mod tests {
"command": params.command.clone(),
"workdir": Some(turn_context.cwd.to_string_lossy().to_string()),
"timeout_ms": params.expiration.timeout_ms(),
"with_escalated_permissions": params.with_escalated_permissions,
"sandbox_permissions": params.sandbox_permissions,
"justification": params.justification.clone(),
})
.to_string(),
@@ -3422,7 +3424,7 @@ mod tests {
"command": params2.command.clone(),
"workdir": Some(turn_context.cwd.to_string_lossy().to_string()),
"timeout_ms": params2.expiration.timeout_ms(),
"with_escalated_permissions": params2.with_escalated_permissions,
"sandbox_permissions": params2.sandbox_permissions,
"justification": params2.justification.clone(),
})
.to_string(),
@@ -3455,6 +3457,7 @@ mod tests {
#[tokio::test]
async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request() {
use crate::protocol::AskForApproval;
use crate::sandboxing::SandboxPermissions;
use crate::turn_diff_tracker::TurnDiffTracker;
let (session, mut turn_context_raw) = make_session_and_context();
@@ -3474,7 +3477,7 @@ mod tests {
payload: ToolPayload::Function {
arguments: serde_json::json!({
"cmd": "echo hi",
"with_escalated_permissions": true,
"sandbox_permissions": SandboxPermissions::RequireEscalated,
"justification": "need unsandboxed execution",
})
.to_string(),

View File

@@ -28,6 +28,7 @@ use crate::protocol::SandboxPolicy;
use crate::sandboxing::CommandSpec;
use crate::sandboxing::ExecEnv;
use crate::sandboxing::SandboxManager;
use crate::sandboxing::SandboxPermissions;
use crate::spawn::StdioPolicy;
use crate::spawn::spawn_child_async;
use crate::text_encoding::bytes_to_string_smart;
@@ -55,7 +56,7 @@ pub struct ExecParams {
pub cwd: PathBuf,
pub expiration: ExecExpiration,
pub env: HashMap<String, String>,
pub with_escalated_permissions: Option<bool>,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
pub arg0: Option<String>,
}
@@ -144,7 +145,7 @@ pub async fn process_exec_tool_call(
cwd,
expiration,
env,
with_escalated_permissions,
sandbox_permissions,
justification,
arg0: _,
} = params;
@@ -162,7 +163,7 @@ pub async fn process_exec_tool_call(
cwd,
env,
expiration,
with_escalated_permissions,
sandbox_permissions,
justification,
};
@@ -192,7 +193,7 @@ pub(crate) async fn execute_exec_env(
env,
expiration,
sandbox,
with_escalated_permissions,
sandbox_permissions,
justification,
arg0,
} = env;
@@ -202,7 +203,7 @@ pub(crate) async fn execute_exec_env(
cwd,
expiration,
env,
with_escalated_permissions,
sandbox_permissions,
justification,
arg0,
};
@@ -857,7 +858,7 @@ mod tests {
cwd: std::env::current_dir()?,
expiration: 500.into(),
env,
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};
@@ -902,7 +903,7 @@ mod tests {
cwd: cwd.clone(),
expiration: ExecExpiration::Cancellation(cancel_token),
env,
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};

View File

@@ -23,32 +23,11 @@ use crate::seatbelt::create_seatbelt_command_args;
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use crate::tools::sandboxing::SandboxablePreference;
pub use codex_protocol::models::SandboxPermissions;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SandboxPermissions {
UseDefault,
RequireEscalated,
}
impl SandboxPermissions {
pub fn requires_escalated_permissions(self) -> bool {
matches!(self, SandboxPermissions::RequireEscalated)
}
}
impl From<bool> for SandboxPermissions {
fn from(with_escalated_permissions: bool) -> Self {
if with_escalated_permissions {
SandboxPermissions::RequireEscalated
} else {
SandboxPermissions::UseDefault
}
}
}
#[derive(Debug)]
pub struct CommandSpec {
pub program: String,
@@ -56,7 +35,7 @@ pub struct CommandSpec {
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub expiration: ExecExpiration,
pub with_escalated_permissions: Option<bool>,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
}
@@ -67,7 +46,7 @@ pub struct ExecEnv {
pub env: HashMap<String, String>,
pub expiration: ExecExpiration,
pub sandbox: SandboxType,
pub with_escalated_permissions: Option<bool>,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
pub arg0: Option<String>,
}
@@ -181,7 +160,7 @@ impl SandboxManager {
env,
expiration: spec.expiration,
sandbox,
with_escalated_permissions: spec.with_escalated_permissions,
sandbox_permissions: spec.sandbox_permissions,
justification: spec.justification,
arg0: arg0_override,
})

View File

@@ -24,6 +24,7 @@ use crate::protocol::ExecCommandSource;
use crate::protocol::SandboxPolicy;
use crate::protocol::TaskStartedEvent;
use crate::sandboxing::ExecEnv;
use crate::sandboxing::SandboxPermissions;
use crate::state::TaskKind;
use crate::tools::format_exec_output_str;
use crate::user_shell_command::user_shell_command_record_item;
@@ -100,7 +101,7 @@ impl SessionTask for UserShellCommandTask {
// should use that instead of an "arbitrarily large" timeout here.
expiration: USER_SHELL_TIMEOUT_MS.into(),
sandbox: SandboxType::None,
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};

View File

@@ -10,7 +10,6 @@ use crate::exec_policy::create_exec_approval_requirement_for_command;
use crate::function_tool::FunctionCallError;
use crate::is_safe_command::is_known_safe_command;
use crate::protocol::ExecCommandSource;
use crate::sandboxing::SandboxPermissions;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
@@ -35,7 +34,7 @@ impl ShellHandler {
cwd: turn_context.resolve_path(params.workdir.clone()),
expiration: params.timeout_ms.into(),
env: create_env(&turn_context.shell_environment_policy),
with_escalated_permissions: params.with_escalated_permissions,
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
justification: params.justification,
arg0: None,
}
@@ -56,7 +55,7 @@ impl ShellCommandHandler {
cwd: turn_context.resolve_path(params.workdir.clone()),
expiration: params.timeout_ms.into(),
env: create_env(&turn_context.shell_environment_policy),
with_escalated_permissions: params.with_escalated_permissions,
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
justification: params.justification,
arg0: None,
}
@@ -206,7 +205,9 @@ impl ShellHandler {
freeform: bool,
) -> Result<ToolOutput, FunctionCallError> {
// Approval policy guard for explicit escalation in non-OnRequest modes.
if exec_params.with_escalated_permissions.unwrap_or(false)
if exec_params
.sandbox_permissions
.requires_escalated_permissions()
&& !matches!(
turn.approval_policy,
codex_protocol::protocol::AskForApproval::OnRequest
@@ -251,7 +252,7 @@ impl ShellHandler {
&exec_params.command,
turn.approval_policy,
&turn.sandbox_policy,
SandboxPermissions::from(exec_params.with_escalated_permissions.unwrap_or(false)),
exec_params.sandbox_permissions,
)
.await;
@@ -260,7 +261,7 @@ impl ShellHandler {
cwd: exec_params.cwd.clone(),
timeout_ms: exec_params.expiration.timeout_ms(),
env: exec_params.env.clone(),
with_escalated_permissions: exec_params.with_escalated_permissions,
sandbox_permissions: exec_params.sandbox_permissions,
justification: exec_params.justification.clone(),
exec_approval_requirement,
};
@@ -295,6 +296,7 @@ mod tests {
use crate::codex::make_session_and_context;
use crate::exec_env::create_env;
use crate::is_safe_command::is_known_safe_command;
use crate::sandboxing::SandboxPermissions;
use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::handlers::ShellCommandHandler;
@@ -343,7 +345,7 @@ mod tests {
let workdir = Some("subdir".to_string());
let login = None;
let timeout_ms = Some(1234);
let with_escalated_permissions = Some(true);
let sandbox_permissions = SandboxPermissions::RequireEscalated;
let justification = Some("because tests".to_string());
let expected_command = session.user_shell().derive_exec_args(&command, true);
@@ -355,7 +357,7 @@ mod tests {
workdir,
login,
timeout_ms,
with_escalated_permissions,
sandbox_permissions: Some(sandbox_permissions),
justification: justification.clone(),
};
@@ -366,10 +368,7 @@ mod tests {
assert_eq!(exec_params.cwd, expected_cwd);
assert_eq!(exec_params.env, expected_env);
assert_eq!(exec_params.expiration.timeout_ms(), timeout_ms);
assert_eq!(
exec_params.with_escalated_permissions,
with_escalated_permissions
);
assert_eq!(exec_params.sandbox_permissions, sandbox_permissions);
assert_eq!(exec_params.justification, justification);
assert_eq!(exec_params.arg0, None);
}

View File

@@ -3,6 +3,7 @@ use crate::is_safe_command::is_known_safe_command;
use crate::protocol::EventMsg;
use crate::protocol::ExecCommandSource;
use crate::protocol::TerminalInteractionEvent;
use crate::sandboxing::SandboxPermissions;
use crate::shell::Shell;
use crate::shell::get_shell_by_model_provided_path;
use crate::tools::context::ToolInvocation;
@@ -40,7 +41,7 @@ struct ExecCommandArgs {
#[serde(default)]
max_output_tokens: Option<usize>,
#[serde(default)]
with_escalated_permissions: Option<bool>,
sandbox_permissions: SandboxPermissions,
#[serde(default)]
justification: Option<String>,
}
@@ -131,12 +132,12 @@ impl ToolHandler for UnifiedExecHandler {
login,
yield_time_ms,
max_output_tokens,
with_escalated_permissions,
sandbox_permissions,
justification,
..
} = args;
if with_escalated_permissions.unwrap_or(false)
if sandbox_permissions.requires_escalated_permissions()
&& !matches!(
context.turn.approval_policy,
codex_protocol::protocol::AskForApproval::OnRequest
@@ -200,7 +201,7 @@ impl ToolHandler for UnifiedExecHandler {
yield_time_ms,
max_output_tokens,
workdir,
with_escalated_permissions,
sandbox_permissions,
justification,
},
&context,

View File

@@ -5,6 +5,7 @@ use crate::client_common::tools::ToolSpec;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
use crate::sandboxing::SandboxPermissions;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolPayload;
@@ -114,7 +115,7 @@ impl ToolRouter {
command: exec.command,
workdir: exec.working_directory,
timeout_ms: exec.timeout_ms,
with_escalated_permissions: None,
sandbox_permissions: Some(SandboxPermissions::UseDefault),
justification: None,
};
Ok(Some(ToolCall {

View File

@@ -7,6 +7,7 @@
use crate::CODEX_APPLY_PATCH_ARG1;
use crate::exec::ExecToolCallOutput;
use crate::sandboxing::CommandSpec;
use crate::sandboxing::SandboxPermissions;
use crate::sandboxing::execute_env;
use crate::tools::sandboxing::Approvable;
use crate::tools::sandboxing::ApprovalCtx;
@@ -70,7 +71,7 @@ impl ApplyPatchRuntime {
expiration: req.timeout_ms.into(),
// Run apply_patch with a minimal environment for determinism and to avoid leaks.
env: HashMap::new(),
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
})
}

View File

@@ -6,6 +6,7 @@ small and focused and reuses the orchestrator for approvals + sandbox + retry.
*/
use crate::exec::ExecExpiration;
use crate::sandboxing::CommandSpec;
use crate::sandboxing::SandboxPermissions;
use crate::tools::sandboxing::ToolError;
use std::collections::HashMap;
use std::path::Path;
@@ -21,7 +22,7 @@ pub(crate) fn build_command_spec(
cwd: &Path,
env: &HashMap<String, String>,
expiration: ExecExpiration,
with_escalated_permissions: Option<bool>,
sandbox_permissions: SandboxPermissions,
justification: Option<String>,
) -> Result<CommandSpec, ToolError> {
let (program, args) = command
@@ -33,7 +34,7 @@ pub(crate) fn build_command_spec(
cwd: cwd.to_path_buf(),
env: env.clone(),
expiration,
with_escalated_permissions,
sandbox_permissions,
justification,
})
}

View File

@@ -5,6 +5,7 @@ Executes shell requests under the orchestrator: asks for approval when needed,
builds a CommandSpec, and runs it under the current SandboxAttempt.
*/
use crate::exec::ExecToolCallOutput;
use crate::sandboxing::SandboxPermissions;
use crate::sandboxing::execute_env;
use crate::tools::runtimes::build_command_spec;
use crate::tools::sandboxing::Approvable;
@@ -30,7 +31,7 @@ pub struct ShellRequest {
pub cwd: PathBuf,
pub timeout_ms: Option<u64>,
pub env: std::collections::HashMap<String, String>,
pub with_escalated_permissions: Option<bool>,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
pub exec_approval_requirement: ExecApprovalRequirement,
}
@@ -51,7 +52,7 @@ pub struct ShellRuntime;
pub(crate) struct ApprovalKey {
command: Vec<String>,
cwd: PathBuf,
escalated: bool,
sandbox_permissions: SandboxPermissions,
}
impl ShellRuntime {
@@ -84,7 +85,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
ApprovalKey {
command: req.command.clone(),
cwd: req.cwd.clone(),
escalated: req.with_escalated_permissions.unwrap_or(false),
sandbox_permissions: req.sandbox_permissions,
}
}
@@ -129,7 +130,7 @@ impl Approvable<ShellRequest> for ShellRuntime {
}
fn sandbox_mode_for_first_attempt(&self, req: &ShellRequest) -> SandboxOverride {
if req.with_escalated_permissions.unwrap_or(false)
if req.sandbox_permissions.requires_escalated_permissions()
|| matches!(
req.exec_approval_requirement,
ExecApprovalRequirement::Skip {
@@ -157,7 +158,7 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
&req.cwd,
&req.env,
req.timeout_ms.into(),
req.with_escalated_permissions,
req.sandbox_permissions,
req.justification.clone(),
)?;
let env = attempt

View File

@@ -7,6 +7,7 @@ the session manager to spawn PTYs once an ExecEnv is prepared.
use crate::error::CodexErr;
use crate::error::SandboxErr;
use crate::exec::ExecExpiration;
use crate::sandboxing::SandboxPermissions;
use crate::tools::runtimes::build_command_spec;
use crate::tools::sandboxing::Approvable;
use crate::tools::sandboxing::ApprovalCtx;
@@ -34,7 +35,7 @@ pub struct UnifiedExecRequest {
pub command: Vec<String>,
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub with_escalated_permissions: Option<bool>,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
pub exec_approval_requirement: ExecApprovalRequirement,
}
@@ -52,7 +53,7 @@ impl ProvidesSandboxRetryData for UnifiedExecRequest {
pub struct UnifiedExecApprovalKey {
pub command: Vec<String>,
pub cwd: PathBuf,
pub escalated: bool,
pub sandbox_permissions: SandboxPermissions,
}
pub struct UnifiedExecRuntime<'a> {
@@ -64,7 +65,7 @@ impl UnifiedExecRequest {
command: Vec<String>,
cwd: PathBuf,
env: HashMap<String, String>,
with_escalated_permissions: Option<bool>,
sandbox_permissions: SandboxPermissions,
justification: Option<String>,
exec_approval_requirement: ExecApprovalRequirement,
) -> Self {
@@ -72,7 +73,7 @@ impl UnifiedExecRequest {
command,
cwd,
env,
with_escalated_permissions,
sandbox_permissions,
justification,
exec_approval_requirement,
}
@@ -102,7 +103,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
UnifiedExecApprovalKey {
command: req.command.clone(),
cwd: req.cwd.clone(),
escalated: req.with_escalated_permissions.unwrap_or(false),
sandbox_permissions: req.sandbox_permissions,
}
}
@@ -150,7 +151,7 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
}
fn sandbox_mode_for_first_attempt(&self, req: &UnifiedExecRequest) -> SandboxOverride {
if req.with_escalated_permissions.unwrap_or(false)
if req.sandbox_permissions.requires_escalated_permissions()
|| matches!(
req.exec_approval_requirement,
ExecApprovalRequirement::Skip {
@@ -178,7 +179,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecSession> for UnifiedExecRunt
&req.cwd,
&req.env,
ExecExpiration::DefaultTimeout,
req.with_escalated_permissions,
req.sandbox_permissions,
req.justification.clone(),
)
.map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?;

View File

@@ -174,10 +174,10 @@ fn create_exec_command_tool() -> ToolSpec {
},
);
properties.insert(
"with_escalated_permissions".to_string(),
JsonSchema::Boolean {
"sandbox_permissions".to_string(),
JsonSchema::String {
description: Some(
"Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions"
"Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
.to_string(),
),
},
@@ -186,7 +186,7 @@ fn create_exec_command_tool() -> ToolSpec {
"justification".to_string(),
JsonSchema::String {
description: Some(
"Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command."
"Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command."
.to_string(),
),
},
@@ -274,15 +274,15 @@ fn create_shell_tool() -> ToolSpec {
);
properties.insert(
"with_escalated_permissions".to_string(),
JsonSchema::Boolean {
description: Some("Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions".to_string()),
"sandbox_permissions".to_string(),
JsonSchema::String {
description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()),
},
);
properties.insert(
"justification".to_string(),
JsonSchema::String {
description: Some("Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.".to_string()),
description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()),
},
);
@@ -347,15 +347,15 @@ fn create_shell_command_tool() -> ToolSpec {
},
);
properties.insert(
"with_escalated_permissions".to_string(),
JsonSchema::Boolean {
description: Some("Whether to request escalated permissions. Set to true if command needs to be run without sandbox restrictions".to_string()),
"sandbox_permissions".to_string(),
JsonSchema::String {
description: Some("Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\".".to_string()),
},
);
properties.insert(
"justification".to_string(),
JsonSchema::String {
description: Some("Only set if with_escalated_permissions is true. 1-sentence explanation of why we want to run this command.".to_string()),
description: Some("Only set if sandbox_permissions is \"require_escalated\". 1-sentence explanation of why we want to run this command.".to_string()),
},
);

View File

@@ -33,6 +33,7 @@ use tokio::sync::Mutex;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::sandboxing::SandboxPermissions;
mod async_watcher;
mod errors;
@@ -93,7 +94,7 @@ pub(crate) struct ExecCommandRequest {
pub yield_time_ms: u64,
pub max_output_tokens: Option<usize>,
pub workdir: Option<PathBuf>,
pub with_escalated_permissions: Option<bool>,
pub sandbox_permissions: SandboxPermissions,
pub justification: Option<String>,
}
@@ -217,7 +218,7 @@ mod tests {
yield_time_ms,
max_output_tokens: None,
workdir: None,
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
},
&context,

View File

@@ -126,7 +126,7 @@ impl UnifiedExecSessionManager {
.open_session_with_sandbox(
&request.command,
cwd.clone(),
request.with_escalated_permissions,
request.sandbox_permissions,
request.justification,
context,
)
@@ -476,7 +476,7 @@ impl UnifiedExecSessionManager {
&self,
command: &[String],
cwd: PathBuf,
with_escalated_permissions: Option<bool>,
sandbox_permissions: SandboxPermissions,
justification: Option<String>,
context: &UnifiedExecContext,
) -> Result<UnifiedExecSession, UnifiedExecError> {
@@ -490,14 +490,14 @@ impl UnifiedExecSessionManager {
command,
context.turn.approval_policy,
&context.turn.sandbox_policy,
SandboxPermissions::from(with_escalated_permissions.unwrap_or(false)),
sandbox_permissions,
)
.await;
let req = UnifiedExecToolRequest::new(
command.to_vec(),
cwd,
env,
with_escalated_permissions,
sandbox_permissions,
justification,
exec_approval_requirement,
);

View File

@@ -9,6 +9,7 @@ use codex_core::protocol::ExecApprovalRequestEvent;
use codex_core::protocol::ExecPolicyAmendment;
use codex_core::protocol::Op;
use codex_core::protocol::SandboxPolicy;
use codex_core::sandboxing::SandboxPermissions;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::user_input::UserInput;
@@ -96,14 +97,14 @@ impl ActionKind {
test: &TestCodex,
server: &MockServer,
call_id: &str,
with_escalated_permissions: bool,
sandbox_permissions: SandboxPermissions,
) -> Result<(Value, Option<String>)> {
match self {
ActionKind::WriteFile { target, content } => {
let (path, _) = target.resolve_for_patch(test);
let _ = fs::remove_file(&path);
let command = format!("printf {content:?} > {path:?} && cat {path:?}");
let event = shell_event(call_id, &command, 1_000, with_escalated_permissions)?;
let event = shell_event(call_id, &command, 1_000, sandbox_permissions)?;
Ok((event, Some(command)))
}
ActionKind::FetchUrl {
@@ -125,11 +126,11 @@ impl ActionKind {
);
let command = format!("python3 -c \"{script}\"");
let event = shell_event(call_id, &command, 1_000, with_escalated_permissions)?;
let event = shell_event(call_id, &command, 1_000, sandbox_permissions)?;
Ok((event, Some(command)))
}
ActionKind::RunCommand { command } => {
let event = shell_event(call_id, command, 1_000, with_escalated_permissions)?;
let event = shell_event(call_id, command, 1_000, sandbox_permissions)?;
Ok((event, Some(command.to_string())))
}
ActionKind::RunUnifiedExecCommand {
@@ -140,7 +141,7 @@ impl ActionKind {
call_id,
command,
Some(1000),
with_escalated_permissions,
sandbox_permissions,
*justification,
)?;
Ok((event, Some(command.to_string())))
@@ -156,7 +157,7 @@ impl ActionKind {
let _ = fs::remove_file(&path);
let patch = build_add_file_patch(&patch_path, content);
let command = shell_apply_patch_command(&patch);
let event = shell_event(call_id, &command, 5_000, with_escalated_permissions)?;
let event = shell_event(call_id, &command, 5_000, sandbox_permissions)?;
Ok((event, Some(command)))
}
}
@@ -181,14 +182,14 @@ fn shell_event(
call_id: &str,
command: &str,
timeout_ms: u64,
with_escalated_permissions: bool,
sandbox_permissions: SandboxPermissions,
) -> Result<Value> {
let mut args = json!({
"command": command,
"timeout_ms": timeout_ms,
});
if with_escalated_permissions {
args["with_escalated_permissions"] = json!(true);
if sandbox_permissions.requires_escalated_permissions() {
args["sandbox_permissions"] = json!(sandbox_permissions);
}
let args_str = serde_json::to_string(&args)?;
Ok(ev_function_call(call_id, "shell_command", &args_str))
@@ -198,7 +199,7 @@ fn exec_command_event(
call_id: &str,
cmd: &str,
yield_time_ms: Option<u64>,
with_escalated_permissions: bool,
sandbox_permissions: SandboxPermissions,
justification: Option<&str>,
) -> Result<Value> {
let mut args = json!({
@@ -207,8 +208,8 @@ fn exec_command_event(
if let Some(yield_time_ms) = yield_time_ms {
args["yield_time_ms"] = json!(yield_time_ms);
}
if with_escalated_permissions {
args["with_escalated_permissions"] = json!(true);
if sandbox_permissions.requires_escalated_permissions() {
args["sandbox_permissions"] = json!(sandbox_permissions);
let reason = justification.unwrap_or(DEFAULT_UNIFIED_EXEC_JUSTIFICATION);
args["justification"] = json!(reason);
}
@@ -466,7 +467,7 @@ struct ScenarioSpec {
approval_policy: AskForApproval,
sandbox_policy: SandboxPolicy,
action: ActionKind,
with_escalated_permissions: bool,
sandbox_permissions: SandboxPermissions,
features: Vec<Feature>,
model_override: Option<&'static str>,
outcome: Outcome,
@@ -637,7 +638,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("dfa_on_request.txt"),
content: "danger-on-request",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -654,7 +655,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("dfa_on_request_5_1.txt"),
content: "danger-on-request",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::Auto,
@@ -671,7 +672,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
endpoint: "/dfa/network",
response_body: "danger-network-ok",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -687,7 +688,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
endpoint: "/dfa/network",
response_body: "danger-network-ok",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::Auto,
@@ -702,7 +703,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
action: ActionKind::RunCommand {
command: "echo trusted-unless",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -717,7 +718,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
action: ActionKind::RunCommand {
command: "echo trusted-unless",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::Auto,
@@ -733,7 +734,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("dfa_on_failure.txt"),
content: "danger-on-failure",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -750,7 +751,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("dfa_on_failure_5_1.txt"),
content: "danger-on-failure",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::Auto,
@@ -767,7 +768,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("dfa_unless_trusted.txt"),
content: "danger-unless-trusted",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -787,7 +788,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("dfa_unless_trusted_5_1.txt"),
content: "danger-unless-trusted",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::ExecApproval {
@@ -807,7 +808,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("dfa_never.txt"),
content: "danger-never",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -824,7 +825,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("dfa_never_5_1.txt"),
content: "danger-never",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::Auto,
@@ -841,7 +842,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ro_on_request.txt"),
content: "read-only-approval",
},
with_escalated_permissions: true,
sandbox_permissions: SandboxPermissions::RequireEscalated,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -861,7 +862,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ro_on_request_5_1.txt"),
content: "read-only-approval",
},
with_escalated_permissions: true,
sandbox_permissions: SandboxPermissions::RequireEscalated,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::ExecApproval {
@@ -880,7 +881,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
action: ActionKind::RunCommand {
command: "echo trusted-read-only",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -895,7 +896,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
action: ActionKind::RunCommand {
command: "echo trusted-read-only",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::Auto,
@@ -911,7 +912,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
endpoint: "/ro/network-blocked",
response_body: "should-not-see",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: None,
outcome: Outcome::Auto,
@@ -925,7 +926,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ro_on_request_denied.txt"),
content: "should-not-write",
},
with_escalated_permissions: true,
sandbox_permissions: SandboxPermissions::RequireEscalated,
features: vec![],
model_override: None,
outcome: Outcome::ExecApproval {
@@ -946,7 +947,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ro_on_failure.txt"),
content: "read-only-on-failure",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -967,7 +968,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ro_on_failure_5_1.txt"),
content: "read-only-on-failure",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::ExecApproval {
@@ -987,7 +988,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
endpoint: "/ro/network-approved",
response_body: "read-only-network-ok",
},
with_escalated_permissions: true,
sandbox_permissions: SandboxPermissions::RequireEscalated,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -1006,7 +1007,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
endpoint: "/ro/network-approved",
response_body: "read-only-network-ok",
},
with_escalated_permissions: true,
sandbox_permissions: SandboxPermissions::RequireEscalated,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::ExecApproval {
@@ -1025,7 +1026,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("apply_patch_shell.txt"),
content: "shell-apply-patch",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: None,
outcome: Outcome::PatchApproval {
@@ -1045,7 +1046,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("apply_patch_function.txt"),
content: "function-apply-patch",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1-codex"),
outcome: Outcome::Auto,
@@ -1062,7 +1063,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("apply_patch_function_danger.txt"),
content: "function-patch-danger",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![Feature::ApplyPatchFreeform],
model_override: Some("gpt-5.1-codex"),
outcome: Outcome::Auto,
@@ -1079,7 +1080,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("apply_patch_function_outside.txt"),
content: "function-patch-outside",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1-codex"),
outcome: Outcome::PatchApproval {
@@ -1099,7 +1100,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("apply_patch_function_outside_denied.txt"),
content: "function-patch-outside-denied",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1-codex"),
outcome: Outcome::PatchApproval {
@@ -1119,7 +1120,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("apply_patch_shell_outside.txt"),
content: "shell-patch-outside",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: None,
outcome: Outcome::PatchApproval {
@@ -1139,7 +1140,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("apply_patch_function_unless_trusted.txt"),
content: "function-patch-unless-trusted",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1-codex"),
outcome: Outcome::PatchApproval {
@@ -1159,7 +1160,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("apply_patch_function_never.txt"),
content: "function-patch-never",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1-codex"),
outcome: Outcome::Auto,
@@ -1178,7 +1179,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ro_unless_trusted.txt"),
content: "read-only-unless-trusted",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -1198,7 +1199,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ro_unless_trusted_5_1.txt"),
content: "read-only-unless-trusted",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5.1"),
outcome: Outcome::ExecApproval {
@@ -1218,7 +1219,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ro_never.txt"),
content: "read-only-never",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: None,
outcome: Outcome::Auto,
@@ -1241,7 +1242,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
action: ActionKind::RunCommand {
command: "echo trusted-never",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -1257,7 +1258,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::Workspace("ww_on_request.txt"),
content: "workspace-on-request",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -1274,7 +1275,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
endpoint: "/ww/network-blocked",
response_body: "workspace-network-blocked",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: None,
outcome: Outcome::Auto,
@@ -1288,7 +1289,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("ww_on_request_outside.txt"),
content: "workspace-on-request-outside",
},
with_escalated_permissions: true,
sandbox_permissions: SandboxPermissions::RequireEscalated,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -1308,7 +1309,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
endpoint: "/ww/network-ok",
response_body: "workspace-network-ok",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -1325,7 +1326,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("ww_on_failure.txt"),
content: "workspace-on-failure",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -1345,7 +1346,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("ww_unless_trusted.txt"),
content: "workspace-unless-trusted",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -1365,7 +1366,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
target: TargetPath::OutsideWorkspace("ww_never.txt"),
content: "workspace-never",
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![],
model_override: None,
outcome: Outcome::Auto,
@@ -1389,7 +1390,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
command: "echo \"hello unified exec\"",
justification: None,
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![Feature::UnifiedExec],
model_override: Some("gpt-5"),
outcome: Outcome::Auto,
@@ -1407,7 +1408,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
command: "python3 -c 'print('\"'\"'escalated unified exec'\"'\"')'",
justification: Some(DEFAULT_UNIFIED_EXEC_JUSTIFICATION),
},
with_escalated_permissions: true,
sandbox_permissions: SandboxPermissions::RequireEscalated,
features: vec![Feature::UnifiedExec],
model_override: Some("gpt-5"),
outcome: Outcome::ExecApproval {
@@ -1426,7 +1427,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
command: "git reset --hard",
justification: None,
},
with_escalated_permissions: false,
sandbox_permissions: SandboxPermissions::UseDefault,
features: vec![Feature::UnifiedExec],
model_override: None,
outcome: Outcome::ExecApproval {
@@ -1472,7 +1473,7 @@ async fn run_scenario(scenario: &ScenarioSpec) -> Result<()> {
let call_id = scenario.name;
let (event, expected_command) = scenario
.action
.prepare(&test, &server, call_id, scenario.with_escalated_permissions)
.prepare(&test, &server, call_id, scenario.sandbox_permissions)
.await?;
let _ = mount_sse_once(
@@ -1578,7 +1579,12 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
let (first_event, expected_command) = ActionKind::RunCommand {
command: "touch allow-prefix.txt",
}
.prepare(&test, &server, call_id_first, false)
.prepare(
&test,
&server,
call_id_first,
SandboxPermissions::UseDefault,
)
.await?;
let expected_command =
expected_command.expect("execpolicy amendment scenario should produce a shell command");
@@ -1656,7 +1662,12 @@ async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts
let (second_event, second_command) = ActionKind::RunCommand {
command: "touch allow-prefix.txt",
}
.prepare(&test, &server, call_id_second, false)
.prepare(
&test,
&server,
call_id_second,
SandboxPermissions::UseDefault,
)
.await?;
assert_eq!(second_command.as_deref(), Some(expected_command.as_str()));

View File

@@ -5,6 +5,7 @@ use codex_core::protocol::ReviewDecision;
use codex_core::protocol::ReviewRequest;
use codex_core::protocol::ReviewTarget;
use codex_core::protocol::SandboxPolicy;
use codex_core::sandboxing::SandboxPermissions;
use core_test_support::responses::ev_apply_patch_function_call;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
@@ -31,7 +32,7 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() {
let args = serde_json::json!({
"command": "rm -rf delegated",
"timeout_ms": 1000,
"with_escalated_permissions": true,
"sandbox_permissions": SandboxPermissions::RequireEscalated,
})
.to_string();
let sse1 = sse(vec![

View File

@@ -8,6 +8,7 @@ use codex_core::exec::ExecToolCallOutput;
use codex_core::exec::SandboxType;
use codex_core::exec::process_exec_tool_call;
use codex_core::protocol::SandboxPolicy;
use codex_core::sandboxing::SandboxPermissions;
use codex_core::spawn::CODEX_SANDBOX_ENV_VAR;
use tempfile::TempDir;
@@ -34,7 +35,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput
cwd: tmp.path().to_path_buf(),
expiration: 1000.into(),
env: HashMap::new(),
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};

View File

@@ -10,6 +10,7 @@ use anyhow::Result;
use codex_core::features::Feature;
use codex_core::protocol::AskForApproval;
use codex_core::protocol::SandboxPolicy;
use codex_core::sandboxing::SandboxPermissions;
use core_test_support::assert_regex_match;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
@@ -105,7 +106,7 @@ async fn shell_escalated_permissions_rejected_then_ok() -> Result<()> {
let first_args = json!({
"command": command,
"timeout_ms": 1_000,
"with_escalated_permissions": true,
"sandbox_permissions": SandboxPermissions::RequireEscalated,
});
let second_args = json!({
"command": command,

View File

@@ -63,6 +63,7 @@ use anyhow::Context as _;
use clap::Parser;
use codex_core::config::find_codex_home;
use codex_core::is_dangerous_command::command_might_be_dangerous;
use codex_core::sandboxing::SandboxPermissions;
use codex_execpolicy::Decision;
use codex_execpolicy::Policy;
use codex_execpolicy::RuleMatch;
@@ -202,13 +203,19 @@ pub(crate) fn evaluate_exec_policy(
&& rule_match.decision() == evaluation.decision
});
let sandbox_permissions = if decision_driven_by_policy {
SandboxPermissions::RequireEscalated
} else {
SandboxPermissions::UseDefault
};
Ok(match evaluation.decision {
Decision::Forbidden => ExecPolicyOutcome::Forbidden,
Decision::Prompt => ExecPolicyOutcome::Prompt {
run_with_escalated_permissions: decision_driven_by_policy,
sandbox_permissions,
},
Decision::Allow => ExecPolicyOutcome::Allow {
run_with_escalated_permissions: decision_driven_by_policy,
sandbox_permissions,
},
})
}
@@ -231,6 +238,7 @@ async fn load_exec_policy() -> anyhow::Result<Policy> {
#[cfg(test)]
mod tests {
use super::*;
use codex_core::sandboxing::SandboxPermissions;
use codex_execpolicy::Decision;
use codex_execpolicy::Policy;
use pretty_assertions::assert_eq;
@@ -247,7 +255,7 @@ mod tests {
assert_eq!(
outcome,
ExecPolicyOutcome::Prompt {
run_with_escalated_permissions: false
sandbox_permissions: SandboxPermissions::UseDefault
}
);
}
@@ -276,7 +284,7 @@ mod tests {
assert_eq!(
outcome,
ExecPolicyOutcome::Allow {
run_with_escalated_permissions: true
sandbox_permissions: SandboxPermissions::RequireEscalated
}
);
}

View File

@@ -10,6 +10,7 @@ use path_absolutize::Absolutize as _;
use codex_core::SandboxState;
use codex_core::exec::process_exec_tool_call;
use codex_core::sandboxing::SandboxPermissions;
use tokio::process::Command;
use tokio_util::sync::CancellationToken;
@@ -85,7 +86,7 @@ impl EscalateServer {
cwd: PathBuf::from(&workdir),
expiration: ExecExpiration::Cancellation(cancel_rx),
env,
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
},

View File

@@ -1,5 +1,6 @@
use std::path::Path;
use codex_core::sandboxing::SandboxPermissions;
use codex_execpolicy::Policy;
use rmcp::ErrorData as McpError;
use rmcp::RoleServer;
@@ -18,10 +19,10 @@ use tokio::sync::RwLock;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ExecPolicyOutcome {
Allow {
run_with_escalated_permissions: bool,
sandbox_permissions: SandboxPermissions,
},
Prompt {
run_with_escalated_permissions: bool,
sandbox_permissions: SandboxPermissions,
},
Forbidden,
}
@@ -108,16 +109,16 @@ impl EscalationPolicy for McpEscalationPolicy {
crate::posix::evaluate_exec_policy(&policy, file, argv, self.preserve_program_paths)?;
let action = match outcome {
ExecPolicyOutcome::Allow {
run_with_escalated_permissions,
sandbox_permissions,
} => {
if run_with_escalated_permissions {
if sandbox_permissions.requires_escalated_permissions() {
EscalateAction::Escalate
} else {
EscalateAction::Run
}
}
ExecPolicyOutcome::Prompt {
run_with_escalated_permissions,
sandbox_permissions,
} => {
let result = self
.prompt(file, argv, workdir, self.context.clone())
@@ -125,7 +126,7 @@ impl EscalationPolicy for McpEscalationPolicy {
// TODO: Extract reason from `result.content`.
match result.action {
ElicitationAction::Accept => {
if run_with_escalated_permissions {
if sandbox_permissions.requires_escalated_permissions() {
EscalateAction::Escalate
} else {
EscalateAction::Run

View File

@@ -6,6 +6,7 @@ use codex_core::exec::ExecParams;
use codex_core::exec::process_exec_tool_call;
use codex_core::exec_env::create_env;
use codex_core::protocol::SandboxPolicy;
use codex_core::sandboxing::SandboxPermissions;
use std::collections::HashMap;
use std::path::PathBuf;
use tempfile::NamedTempFile;
@@ -41,7 +42,7 @@ async fn run_cmd(cmd: &[&str], writable_roots: &[PathBuf], timeout_ms: u64) {
cwd,
expiration: timeout_ms.into(),
env: create_env_from_core_vars(),
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};
@@ -143,7 +144,7 @@ async fn assert_network_blocked(cmd: &[&str]) {
// do not stall the suite.
expiration: NETWORK_TIMEOUT_MS.into(),
env: create_env_from_core_vars(),
with_escalated_permissions: None,
sandbox_permissions: SandboxPermissions::UseDefault,
justification: None,
arg0: None,
};

View File

@@ -14,6 +14,25 @@ use codex_git::GhostCommit;
use codex_utils_image::error::ImageProcessingError;
use schemars::JsonSchema;
/// Controls whether a command should use the session sandbox or bypass it.
#[derive(
Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS,
)]
#[serde(rename_all = "snake_case")]
pub enum SandboxPermissions {
/// Run with the configured sandbox
#[default]
UseDefault,
/// Request to run outside the sandbox
RequireEscalated,
}
impl SandboxPermissions {
pub fn requires_escalated_permissions(self) -> bool {
matches!(self, SandboxPermissions::RequireEscalated)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ResponseInputItem {
@@ -327,8 +346,9 @@ pub struct ShellToolCallParams {
/// This is the maximum time in milliseconds that the command is allowed to run.
#[serde(alias = "timeout")]
pub timeout_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub with_escalated_permissions: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub sandbox_permissions: Option<SandboxPermissions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub justification: Option<String>,
}
@@ -346,8 +366,9 @@ pub struct ShellCommandToolCallParams {
/// This is the maximum time in milliseconds that the command is allowed to run.
#[serde(alias = "timeout")]
pub timeout_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub with_escalated_permissions: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub sandbox_permissions: Option<SandboxPermissions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub justification: Option<String>,
}
@@ -742,7 +763,7 @@ mod tests {
command: vec!["ls".to_string(), "-l".to_string()],
workdir: Some("/tmp".to_string()),
timeout_ms: Some(1000),
with_escalated_permissions: None,
sandbox_permissions: None,
justification: None,
},
params