Sandbox remote exec commands on the executor

This commit is contained in:
jif-oai
2026-06-19 15:03:03 +02:00
parent f3c0ccae96
commit c570c3263a
20 changed files with 317 additions and 7 deletions

View File

@@ -442,6 +442,7 @@ pub(crate) async fn execute_exec_request(
windows_sandbox_filesystem_overrides,
network_environment_id,
arg0,
exec_server_sandbox: _,
} = exec_request;
// TODO(anp): Keep PathUri through the local process launch boundary.

View File

@@ -14,6 +14,7 @@ use crate::exec::execute_exec_request;
#[cfg(target_os = "macos")]
use crate::spawn::CODEX_SANDBOX_ENV_VAR;
use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_exec_server::FileSystemSandboxContext;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::exec_output::ExecToolCallOutput;
@@ -60,6 +61,7 @@ pub struct ExecRequest {
pub network_sandbox_policy: NetworkSandboxPolicy,
pub(crate) windows_sandbox_filesystem_overrides: Option<WindowsSandboxFilesystemOverrides>,
pub arg0: Option<String>,
pub(crate) exec_server_sandbox: Option<FileSystemSandboxContext>,
}
impl ExecRequest {
@@ -102,6 +104,7 @@ impl ExecRequest {
network_sandbox_policy,
windows_sandbox_filesystem_overrides: None,
arg0,
exec_server_sandbox: None,
}
}
@@ -158,6 +161,7 @@ impl ExecRequest {
network_sandbox_policy,
windows_sandbox_filesystem_overrides: None,
arg0,
exec_server_sandbox: None,
}
}
}

View File

@@ -224,6 +224,7 @@ pub(crate) async fn execute_user_shell_command(
network_sandbox_policy: permission_profile.network_sandbox_policy(),
windows_sandbox_filesystem_overrides: None,
arg0: None,
exec_server_sandbox: None,
};
let stdout_stream = Some(StdoutStream {

View File

@@ -164,6 +164,7 @@ pub(super) async fn try_run_zsh_fork(
network_sandbox_policy,
windows_sandbox_filesystem_overrides: _windows_sandbox_filesystem_overrides,
arg0,
exec_server_sandbox: _,
} = sandbox_exec_request;
let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?;
let effective_timeout = Duration::from_millis(
@@ -893,6 +894,7 @@ impl CoreShellCommandExecutor {
network_sandbox_policy: self.network_sandbox_policy,
windows_sandbox_filesystem_overrides: None,
arg0: self.arg0.clone(),
exec_server_sandbox: None,
},
/*stdout_stream*/ None,
after_spawn,

View File

@@ -382,9 +382,12 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
error @ ToolError::Codex(_) => error,
})?;
let options = unified_exec_options(attempt.network_denial_cancellation_token.clone());
let mut exec_env = attempt
.env_for(command, options, managed_network)
.map_err(ToolError::Codex)?;
let mut exec_env = if req.turn_environment.environment.is_remote() {
attempt.env_for_exec_server(command, options, managed_network)
} else {
attempt.env_for(command, options, managed_network)
}
.map_err(ToolError::Codex)?;
exec_env.exec_server_env_config = req.exec_server_env_config.clone();
match zsh_fork_backend::maybe_prepare_unified_exec(
req,
@@ -442,9 +445,12 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
error @ ToolError::Codex(_) => error,
})?;
let options = unified_exec_options(attempt.network_denial_cancellation_token.clone());
let mut exec_env = attempt
.env_for(command, options, managed_network)
.map_err(ToolError::Codex)?;
let mut exec_env = if req.turn_environment.environment.is_remote() {
attempt.env_for_exec_server(command, options, managed_network)
} else {
attempt.env_for(command, options, managed_network)
}
.map_err(ToolError::Codex)?;
exec_env.exec_server_env_config = req.exec_server_env_config.clone();
self.manager
.open_session_with_exec_env(

View File

@@ -11,6 +11,7 @@ use crate::session::turn_context::TurnContext;
use crate::state::SessionServices;
use crate::tools::hook_names::HookToolName;
use crate::tools::network_approval::NetworkApprovalSpec;
use codex_exec_server::FileSystemSandboxContext;
use codex_network_proxy::NetworkProxy;
use codex_protocol::approvals::ExecPolicyAmendment;
use codex_protocol::approvals::NetworkApprovalContext;
@@ -451,6 +452,53 @@ impl<'a> SandboxAttempt<'a> {
self.workspace_roots.to_vec(),
))
}
pub fn env_for_exec_server(
&self,
command: SandboxCommand,
options: ExecOptions,
network: Option<&NetworkProxy>,
) -> Result<crate::sandboxing::ExecRequest, CodexErr> {
let request = self
.manager
.transform(SandboxTransformRequest {
command,
permissions: self.permissions,
// The exec-server chooses and applies its native platform sandbox.
sandbox: SandboxType::None,
enforce_managed_network: self.enforce_managed_network,
environment_id: None,
network,
sandbox_policy_cwd: self.sandbox_cwd,
codex_linux_sandbox_exe: None,
use_legacy_landlock: self.use_legacy_landlock,
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
})
.map_err(CodexErr::from)?;
let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request(
request,
options,
self.workspace_roots.to_vec(),
);
if self.sandbox != SandboxType::None {
exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {
permissions: exec_request.permission_profile.clone().into(),
cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()),
workspace_roots: self
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect(),
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
use_legacy_landlock: self.use_legacy_landlock,
});
// Preserve denial classification while leaving argv native to the executor.
exec_request.sandbox = self.sandbox;
}
Ok(exec_request)
}
}
#[cfg(test)]

View File

@@ -4,7 +4,13 @@ use crate::tools::hook_names::HookToolName;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_sandboxing::SandboxCommand;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxType;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -193,3 +199,66 @@ fn deny_read_blocks_explicit_escalation_and_policy_bypass() {
"exec-policy allow rules would drop deny-read filesystem policy, so keep the first attempt sandboxed",
);
}
#[test]
fn exec_server_env_preserves_command_and_carries_sandbox_context() {
let cwd: AbsolutePathBuf = std::env::current_dir()
.expect("current dir")
.try_into()
.expect("absolute cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let permissions = codex_protocol::models::PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::default(),
NetworkSandboxPolicy::Restricted,
);
let manager = SandboxManager::new();
let attempt = SandboxAttempt {
sandbox: SandboxType::MacosSeatbelt,
permissions: &permissions,
enforce_managed_network: false,
manager: &manager,
sandbox_cwd: &cwd_uri,
workspace_roots: std::slice::from_ref(&cwd),
codex_linux_sandbox_exe: None,
use_legacy_landlock: false,
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
network_denial_cancellation_token: None,
};
let command = SandboxCommand {
program: "/bin/bash".into(),
args: vec!["-lc".to_string(), "pwd".to_string()],
cwd: cwd_uri.clone(),
env: HashMap::new(),
additional_permissions: None,
};
let options = ExecOptions {
expiration: crate::exec::ExecExpiration::DefaultTimeout,
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
};
let request = attempt
.env_for_exec_server(command, options, /*network*/ None)
.expect("prepare remote exec request");
assert_eq!(
request.command,
vec![
"/bin/bash".to_string(),
"-lc".to_string(),
"pwd".to_string()
]
);
assert_eq!(request.sandbox, SandboxType::MacosSeatbelt);
assert_eq!(
request.exec_server_sandbox,
Some(codex_exec_server::FileSystemSandboxContext {
permissions: request.permission_profile.clone().into(),
cwd: Some(cwd_uri.clone()),
workspace_roots: vec![cwd_uri],
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
use_legacy_landlock: false,
})
);
}

View File

@@ -162,6 +162,7 @@ fn exec_server_params_for_request(
tty,
pipe_stdin: false,
arg0: request.arg0.clone(),
sandbox: request.exec_server_sandbox.clone(),
}
}

View File

@@ -111,6 +111,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
network_sandbox_policy,
windows_sandbox_filesystem_overrides: None,
arg0: None,
exec_server_sandbox: None,
};
let params =

View File

@@ -1156,6 +1156,7 @@ mod tests {
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await
.expect("start process");

View File

@@ -16,6 +16,7 @@ mod noise_channel;
mod noise_relay;
mod process;
mod process_id;
mod process_sandbox;
mod protocol;
mod regular_file;
mod relay;

View File

@@ -902,6 +902,7 @@ mod tests {
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
}
}

View File

@@ -0,0 +1,150 @@
use crate::ExecServerRuntimePaths;
use crate::protocol::ExecParams;
use crate::rpc::invalid_params;
use codex_app_server_protocol::JSONRPCErrorError;
use codex_protocol::models::PermissionProfile;
use codex_sandboxing::SandboxCommand;
use codex_sandboxing::SandboxDirectSpawnTransformRequest;
use codex_sandboxing::SandboxManager;
use codex_sandboxing::SandboxTransformRequest;
use codex_sandboxing::SandboxType;
use codex_sandboxing::SandboxablePreference;
/// Converts a remote launch's sandbox policy into this host's native wrapper.
pub(crate) fn prepare_exec_params(
mut params: ExecParams,
runtime_paths: &ExecServerRuntimePaths,
) -> Result<ExecParams, JSONRPCErrorError> {
let Some(sandbox_context) = params.sandbox.take() else {
return Ok(params);
};
let native_permissions: PermissionProfile = sandbox_context
.permissions
.try_into()
.map_err(|err| invalid_params(format!("invalid sandbox permission path URI: {err}")))?;
let (file_system_policy, network_policy) = native_permissions.to_runtime_permissions();
let sandbox_manager = SandboxManager::new();
let sandbox = sandbox_manager.select_initial(
&file_system_policy,
network_policy,
SandboxablePreference::Auto,
sandbox_context.windows_sandbox_level,
/*has_managed_network_requirements*/ false,
);
if sandbox == SandboxType::None {
return Ok(params);
}
let (program, args) = params
.argv
.split_first()
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
let command = SandboxCommand {
program: program.into(),
args: args.to_vec(),
cwd: params.cwd.clone(),
env: params.env.clone(),
additional_permissions: None,
};
let sandbox_policy_cwd = sandbox_context.cwd.as_ref().unwrap_or(&params.cwd);
let native_workspace_roots = sandbox_context
.workspace_roots
.iter()
.map(|root| {
root.to_abs_path().map_err(|err| {
invalid_params(format!(
"sandbox workspace root URI `{root}` is not valid on this exec-server host: {err}"
))
})
})
.collect::<Result<Vec<_>, _>>()?;
let command_cwd = params.cwd.to_abs_path().map_err(|err| {
invalid_params(format!(
"cwd URI `{}` is not valid on this exec-server host: {err}",
params.cwd
))
})?;
let workspace_roots = if native_workspace_roots.is_empty() {
std::slice::from_ref(&command_cwd)
} else {
native_workspace_roots.as_slice()
};
let request = sandbox_manager
.transform_for_direct_spawn(SandboxDirectSpawnTransformRequest {
workspace_roots,
transform: SandboxTransformRequest {
command,
permissions: &native_permissions,
sandbox,
enforce_managed_network: false,
environment_id: None,
network: None,
sandbox_policy_cwd,
codex_linux_sandbox_exe: runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
windows_sandbox_level: sandbox_context.windows_sandbox_level,
windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop,
},
})
.map_err(|err| invalid_params(format!("failed to prepare process sandbox: {err}")))?;
params.argv = request.command;
params.env = request.env;
params.arg0 = request.arg0;
Ok(params)
}
#[cfg(all(test, target_os = "linux"))]
mod tests {
use std::collections::HashMap;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use super::*;
use crate::FileSystemSandboxContext;
use crate::ProcessId;
#[test]
fn remote_sandbox_uses_executor_linux_helper() {
let cwd = std::env::current_dir().expect("cwd");
let cwd_uri = PathUri::from_path(&cwd).expect("cwd URI");
let permissions = PermissionProfile::from_runtime_permissions(
&FileSystemSandboxPolicy::default(),
NetworkSandboxPolicy::Restricted,
);
let runtime_paths = ExecServerRuntimePaths::new(
"/executor/codex".into(),
Some("/executor/codex-linux-sandbox".into()),
)
.expect("runtime paths");
let params = ExecParams {
process_id: ProcessId::from("sandboxed"),
argv: vec![
"/bin/bash".to_string(),
"-lc".to_string(),
"pwd".to_string(),
],
cwd: cwd_uri.clone(),
env_policy: None,
env: HashMap::new(),
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: Some(FileSystemSandboxContext::from_permission_profile_with_cwd(
permissions,
cwd_uri,
)),
};
let params = prepare_exec_params(params, &runtime_paths).expect("prepare sandbox");
assert_eq!(
params.argv.first(),
Some(&"/executor/codex-linux-sandbox".to_string())
);
assert_eq!(params.arg0, Some("codex-linux-sandbox".to_string()));
assert_eq!(params.sandbox, None);
}
}

View File

@@ -118,6 +118,10 @@ pub struct ExecParams {
/// Optional process-visible argv0 override. Values such as `codex-linux-sandbox` are command
/// names rather than paths, so this is not a [`PathUri`].
pub arg0: Option<String>,
/// Sandbox policy for this launch. The exec-server resolves this into its own platform
/// sandbox wrapper so clients never send host-local helper paths in argv.
#[serde(default)]
pub sandbox: Option<FileSystemSandboxContext>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

View File

@@ -14,6 +14,7 @@ use tokio_util::task::TaskTracker;
use crate::ExecServerRuntimePaths;
use crate::client::http_client::PendingReqwestHttpBodyStream;
use crate::client::http_client::ReqwestHttpRequestRunner;
use crate::process_sandbox::prepare_exec_params;
use crate::protocol::EnvironmentInfo;
use crate::protocol::ExecParams;
use crate::protocol::ExecResponse;
@@ -67,6 +68,7 @@ pub(crate) struct ExecServerHandler {
background_task_shutdown: CancellationToken,
background_tasks: TaskTracker,
file_system: FileSystemHandler,
runtime_paths: ExecServerRuntimePaths,
initialize_requested: AtomicBool,
initialized: AtomicBool,
}
@@ -84,7 +86,8 @@ impl ExecServerHandler {
active_body_stream_ids: Mutex::new(HashSet::new()),
background_task_shutdown: CancellationToken::new(),
background_tasks: TaskTracker::new(),
file_system: FileSystemHandler::new(runtime_paths),
file_system: FileSystemHandler::new(runtime_paths.clone()),
runtime_paths,
initialize_requested: AtomicBool::new(false),
initialized: AtomicBool::new(false),
}
@@ -154,6 +157,7 @@ impl ExecServerHandler {
pub(crate) async fn exec(&self, params: ExecParams) -> Result<ExecResponse, JSONRPCErrorError> {
let session = self.require_initialized_for("exec")?;
let params = prepare_exec_params(params, &self.runtime_paths)?;
session.process().exec(params).await
}

View File

@@ -33,6 +33,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams {
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
}
}

View File

@@ -620,6 +620,7 @@ mod tests {
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
}
}

View File

@@ -81,6 +81,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), "proc-1");
@@ -222,6 +223,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -253,6 +255,7 @@ async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()> {
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -300,6 +303,7 @@ async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Res
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -348,6 +352,7 @@ async fn assert_exec_process_retains_output_after_exit_until_streams_close(
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -421,6 +426,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
tty: true,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -458,6 +464,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
tty: false,
pipe_stdin: true,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -491,6 +498,7 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool)
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -525,6 +533,7 @@ async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Resu
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -578,6 +587,7 @@ async fn assert_exec_process_signal_reports_unsupported_on_windows(use_remote: b
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
@@ -618,6 +628,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
@@ -676,6 +687,7 @@ async fn remote_exec_process_recovers_after_transport_disconnect() -> Result<()>
tty: false,
pipe_stdin: true,
arg0: None,
sandbox: None,
})
.await?;

View File

@@ -150,6 +150,7 @@ async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> {
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
})
.await?;
assert_eq!(

View File

@@ -503,6 +503,7 @@ impl ExecutorStdioServerLauncher {
tty: false,
pipe_stdin: true,
arg0: None,
sandbox: None,
})
.await
.map_err(io::Error::other)?;