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
Unverified
parent 97b90094cd
commit e0fb3ca1db
27 changed files with 216 additions and 179 deletions
+8 -5
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(),
+8 -7
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,
};
+4 -25
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,
})
+2 -1
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,
};
+11 -12
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);
}
@@ -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,
+2 -1
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 {
@@ -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,
})
}
+3 -2
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,
})
}
+6 -5
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
@@ -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()))?;
+12 -12
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()),
},
);
+3 -2
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,
@@ -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,
);