mirror of
https://github.com/openai/codex.git
synced 2026-04-05 23:21:43 +03:00
Compare commits
1 Commits
pr16640
...
exec-serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13c8fde3cf |
3
codex-rs/Cargo.lock
generated
3
codex-rs/Cargo.lock
generated
@@ -2048,6 +2048,8 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-pty",
|
||||
@@ -2628,6 +2630,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
|
||||
@@ -38,6 +38,8 @@ use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxLaunchMode;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_shell_command::powershell::prefix_powershell_script_with_utf8;
|
||||
use codex_tools::UnifiedExecShellMode;
|
||||
@@ -82,6 +84,24 @@ pub struct UnifiedExecRuntime<'a> {
|
||||
shell_mode: UnifiedExecShellMode,
|
||||
}
|
||||
|
||||
fn build_remote_exec_sandbox_config(attempt: &SandboxAttempt<'_>) -> SandboxLaunchConfig {
|
||||
SandboxLaunchConfig {
|
||||
mode: if matches!(attempt.sandbox, codex_sandboxing::SandboxType::None) {
|
||||
SandboxLaunchMode::Disabled
|
||||
} else {
|
||||
SandboxLaunchMode::Auto
|
||||
},
|
||||
policy: attempt.policy.clone(),
|
||||
file_system_policy: attempt.file_system_policy.clone(),
|
||||
network_policy: attempt.network_policy,
|
||||
sandbox_policy_cwd: attempt.sandbox_cwd.to_path_buf(),
|
||||
enforce_managed_network: attempt.enforce_managed_network,
|
||||
windows_sandbox_level: attempt.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop,
|
||||
use_legacy_landlock: attempt.use_legacy_landlock,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> UnifiedExecRuntime<'a> {
|
||||
/// Creates a runtime bound to the shared unified-exec process manager.
|
||||
pub fn new(manager: &'a UnifiedExecProcessManager, shell_mode: UnifiedExecShellMode) -> Self {
|
||||
@@ -218,6 +238,31 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
if let Some(network) = req.network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
}
|
||||
// Remote exec-server now owns sandbox argv construction, so this branch
|
||||
// keeps sending raw command data until we collapse the launch APIs.
|
||||
if ctx.turn.environment.exec_server_url().is_some() {
|
||||
return self
|
||||
.manager
|
||||
.open_session_with_remote_exec(
|
||||
req.process_id,
|
||||
command,
|
||||
req.cwd.clone(),
|
||||
env,
|
||||
req.tty,
|
||||
Some(build_remote_exec_sandbox_config(attempt)),
|
||||
ctx.turn.environment.as_ref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| match err {
|
||||
UnifiedExecError::SandboxDenied { output, .. } => {
|
||||
ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied {
|
||||
output: Box::new(output),
|
||||
network_policy_decision: None,
|
||||
}))
|
||||
}
|
||||
other => ToolError::Rejected(other.to_string()),
|
||||
});
|
||||
}
|
||||
if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {
|
||||
let command =
|
||||
build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())
|
||||
@@ -239,6 +284,9 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
.await?
|
||||
{
|
||||
Some(prepared) => {
|
||||
// TODO: Move unified-exec zsh-fork into exec-server once
|
||||
// remote launch can participate in the existing approval
|
||||
// and escalation flow.
|
||||
if ctx.turn.environment.exec_server_url().is_some() {
|
||||
return Err(ToolError::Rejected(
|
||||
"unified_exec zsh-fork is not supported when exec_server_url is configured".to_string(),
|
||||
|
||||
@@ -609,10 +609,12 @@ impl UnifiedExecProcessManager {
|
||||
env: env.env.clone(),
|
||||
tty,
|
||||
arg0: env.arg0.clone(),
|
||||
sandbox: None,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
||||
return UnifiedExecProcess::from_remote_started(started, env.sandbox).await;
|
||||
let sandbox_type = started.sandbox_type;
|
||||
return UnifiedExecProcess::from_remote_started(started, sandbox_type).await;
|
||||
}
|
||||
|
||||
let spawn_result = if tty {
|
||||
@@ -643,6 +645,33 @@ impl UnifiedExecProcessManager {
|
||||
UnifiedExecProcess::from_spawned(spawned, env.sandbox, spawn_lifecycle).await
|
||||
}
|
||||
|
||||
pub(crate) async fn open_session_with_remote_exec(
|
||||
&self,
|
||||
process_id: i32,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
env: HashMap<String, String>,
|
||||
tty: bool,
|
||||
sandbox: Option<codex_sandboxing::SandboxLaunchConfig>,
|
||||
environment: &codex_exec_server::Environment,
|
||||
) -> Result<UnifiedExecProcess, UnifiedExecError> {
|
||||
let started = environment
|
||||
.get_exec_backend()
|
||||
.start(codex_exec_server::ExecParams {
|
||||
process_id: exec_server_process_id(process_id).into(),
|
||||
argv: command,
|
||||
cwd,
|
||||
env,
|
||||
tty,
|
||||
arg0: None,
|
||||
sandbox,
|
||||
})
|
||||
.await
|
||||
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
||||
let sandbox_type = started.sandbox_type;
|
||||
UnifiedExecProcess::from_remote_started(started, sandbox_type).await
|
||||
}
|
||||
|
||||
pub(super) async fn open_session_with_sandbox(
|
||||
&self,
|
||||
request: &ExecCommandRequest,
|
||||
|
||||
@@ -74,6 +74,7 @@ async fn remote_process(write_status: WriteStatus) -> UnifiedExecProcess {
|
||||
read_responses: Mutex::new(VecDeque::new()),
|
||||
wake_tx,
|
||||
}),
|
||||
sandbox_type: SandboxType::None,
|
||||
};
|
||||
|
||||
UnifiedExecProcess::from_remote_started(started, SandboxType::None)
|
||||
@@ -126,6 +127,7 @@ async fn remote_process_waits_for_early_exit_event() {
|
||||
}])),
|
||||
wake_tx: wake_tx.clone(),
|
||||
}),
|
||||
sandbox_type: SandboxType::None,
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -947,13 +947,15 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<(
|
||||
|
||||
let open_call_id = "uexec-delayed-open";
|
||||
let open_args = json!({
|
||||
"cmd": "sleep 3 && echo MARKER1 && sleep 3 && echo MARKER2",
|
||||
"cmd": "sleep 2 && echo MARKER1 && sleep 5 && echo MARKER2",
|
||||
"yield_time_ms": 10,
|
||||
"tty": true,
|
||||
});
|
||||
|
||||
// Poll stdin three times: first for no output, second after the first marker,
|
||||
// and a final long poll to capture the second marker.
|
||||
// and a final long poll to capture the second marker. Keep a wider buffer
|
||||
// between MARKER1 and process exit so slower environments still reach the
|
||||
// third write while the session is live.
|
||||
let first_poll_call_id = "uexec-delayed-poll-1";
|
||||
let first_poll_args = json!({
|
||||
"chars": "x",
|
||||
@@ -965,7 +967,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<(
|
||||
let second_poll_args = json!({
|
||||
"chars": "x",
|
||||
"session_id": 1000,
|
||||
"yield_time_ms": 4000,
|
||||
"yield_time_ms": 3500,
|
||||
});
|
||||
|
||||
let third_poll_call_id = "uexec-delayed-poll-3";
|
||||
|
||||
@@ -19,6 +19,8 @@ arc-swap = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-pty = { workspace = true }
|
||||
|
||||
@@ -202,6 +202,7 @@ mod tests {
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
})
|
||||
.await
|
||||
.expect("start process");
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -7,6 +9,9 @@ use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_utils_pty::ExecCommandSession;
|
||||
use codex_utils_pty::TerminalSize;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -78,6 +83,7 @@ struct Inner {
|
||||
processes: Mutex<HashMap<ProcessId, ProcessEntry>>,
|
||||
initialize_requested: AtomicBool,
|
||||
initialized: AtomicBool,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -91,6 +97,33 @@ struct LocalExecProcess {
|
||||
wake_tx: watch::Sender<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct ExecServerRuntimeConfig {
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ExecServerRuntimeConfig {
|
||||
fn detect() -> Self {
|
||||
let env_path = std::env::var_os("CODEX_LINUX_SANDBOX_EXE").map(PathBuf::from);
|
||||
let sibling_path = std::env::current_exe().ok().and_then(|current_exe| {
|
||||
current_exe
|
||||
.parent()
|
||||
.map(|parent| parent.join(CODEX_LINUX_SANDBOX_ARG0))
|
||||
.filter(|candidate| candidate.exists())
|
||||
});
|
||||
Self {
|
||||
codex_linux_sandbox_exe: env_path.or(sibling_path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PreparedExecLaunch {
|
||||
argv: Vec<String>,
|
||||
env: HashMap<String, String>,
|
||||
arg0: Option<String>,
|
||||
sandbox_type: SandboxType,
|
||||
}
|
||||
|
||||
impl Default for LocalProcess {
|
||||
fn default() -> Self {
|
||||
let (outgoing_tx, mut outgoing_rx) =
|
||||
@@ -108,6 +141,7 @@ impl LocalProcess {
|
||||
processes: Mutex::new(HashMap::new()),
|
||||
initialize_requested: AtomicBool::new(false),
|
||||
initialized: AtomicBool::new(false),
|
||||
runtime: ExecServerRuntimeConfig::detect(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -168,7 +202,8 @@ impl LocalProcess {
|
||||
) -> Result<(ExecResponse, watch::Sender<u64>), JSONRPCErrorError> {
|
||||
self.require_initialized_for("exec")?;
|
||||
let process_id = params.process_id.clone();
|
||||
let (program, args) = params
|
||||
let launch = prepare_exec_launch(¶ms, &self.inner.runtime)?;
|
||||
let (program, args) = launch
|
||||
.argv
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
|
||||
@@ -188,8 +223,8 @@ impl LocalProcess {
|
||||
program,
|
||||
args,
|
||||
params.cwd.as_path(),
|
||||
¶ms.env,
|
||||
¶ms.arg0,
|
||||
&launch.env,
|
||||
&launch.arg0,
|
||||
TerminalSize::default(),
|
||||
)
|
||||
.await
|
||||
@@ -198,8 +233,8 @@ impl LocalProcess {
|
||||
program,
|
||||
args,
|
||||
params.cwd.as_path(),
|
||||
¶ms.env,
|
||||
¶ms.arg0,
|
||||
&launch.env,
|
||||
&launch.arg0,
|
||||
)
|
||||
.await
|
||||
};
|
||||
@@ -264,7 +299,13 @@ impl LocalProcess {
|
||||
output_notify,
|
||||
));
|
||||
|
||||
Ok((ExecResponse { process_id }, wake_tx))
|
||||
Ok((
|
||||
ExecResponse {
|
||||
process_id,
|
||||
sandbox_type: launch.sandbox_type,
|
||||
},
|
||||
wake_tx,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn exec(&self, params: ExecParams) -> Result<ExecResponse, JSONRPCErrorError> {
|
||||
@@ -424,6 +465,7 @@ impl ExecBackend for LocalProcess {
|
||||
backend: self.clone(),
|
||||
wake_tx,
|
||||
}),
|
||||
sandbox_type: response.sandbox_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -458,6 +500,56 @@ impl ExecProcess for LocalExecProcess {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_sandbox_command(
|
||||
argv: &[String],
|
||||
cwd: &Path,
|
||||
env: &HashMap<String, String>,
|
||||
) -> Result<SandboxCommand, JSONRPCErrorError> {
|
||||
let (program, args) = argv
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
|
||||
Ok(SandboxCommand {
|
||||
program: program.clone().into(),
|
||||
args: args.to_vec(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: env.clone(),
|
||||
additional_permissions: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_exec_launch(
|
||||
params: &ExecParams,
|
||||
runtime: &ExecServerRuntimeConfig,
|
||||
) -> Result<PreparedExecLaunch, JSONRPCErrorError> {
|
||||
let Some(sandbox) = params.sandbox.as_ref() else {
|
||||
return Ok(PreparedExecLaunch {
|
||||
argv: params.argv.clone(),
|
||||
env: params.env.clone(),
|
||||
arg0: params.arg0.clone(),
|
||||
sandbox_type: SandboxType::None,
|
||||
});
|
||||
};
|
||||
|
||||
let command = build_sandbox_command(¶ms.argv, params.cwd.as_path(), ¶ms.env)?;
|
||||
let transformed = sandbox
|
||||
.transform(
|
||||
command,
|
||||
// TODO: Thread managed-network proxy state across exec-server so
|
||||
// sandbox profile generation preserves proxy-specific allowances.
|
||||
/*network*/
|
||||
None,
|
||||
runtime.codex_linux_sandbox_exe.as_ref(),
|
||||
)
|
||||
.map_err(|err| internal_error(format!("failed to build sandbox launch: {err}")))?;
|
||||
|
||||
Ok(PreparedExecLaunch {
|
||||
argv: transformed.command,
|
||||
env: transformed.env,
|
||||
arg0: transformed.arg0,
|
||||
sandbox_type: transformed.sandbox,
|
||||
})
|
||||
}
|
||||
|
||||
impl LocalProcess {
|
||||
async fn read(
|
||||
&self,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use tokio::sync::watch;
|
||||
|
||||
use crate::ExecServerError;
|
||||
@@ -11,6 +12,7 @@ use crate::protocol::WriteResponse;
|
||||
|
||||
pub struct StartedExecProcess {
|
||||
pub process: Arc<dyn ExecProcess>,
|
||||
pub sandbox_type: SandboxType,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -2,6 +2,8 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -61,12 +63,14 @@ pub struct ExecParams {
|
||||
pub env: HashMap<String, String>,
|
||||
pub tty: bool,
|
||||
pub arg0: Option<String>,
|
||||
pub sandbox: Option<SandboxLaunchConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecResponse {
|
||||
pub process_id: ProcessId,
|
||||
pub sandbox_type: SandboxType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -35,13 +35,17 @@ impl ExecBackend for RemoteProcess {
|
||||
async fn start(&self, params: ExecParams) -> Result<StartedExecProcess, ExecServerError> {
|
||||
let process_id = params.process_id.clone();
|
||||
let session = self.client.register_session(&process_id).await?;
|
||||
if let Err(err) = self.client.exec(params).await {
|
||||
session.unregister().await;
|
||||
return Err(err);
|
||||
}
|
||||
let response = match self.client.exec(params).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
session.unregister().await;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(StartedExecProcess {
|
||||
process: Arc::new(RemoteExecProcess { session }),
|
||||
sandbox_type: response.sandbox_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ fn exec_params(process_id: &str) -> ExecParams {
|
||||
env,
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), "proc-1");
|
||||
@@ -130,11 +131,12 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
|
||||
let StartedExecProcess { process } = session;
|
||||
let StartedExecProcess { process, .. } = session;
|
||||
let wake_rx = process.subscribe_wake();
|
||||
let (output, exit_code, closed) = collect_process_output_from_reads(process, wake_rx).await?;
|
||||
assert_eq!(output, "session output\n");
|
||||
@@ -159,13 +161,14 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
|
||||
env: Default::default(),
|
||||
tty: true,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
session.process.write(b"hello\n".to_vec()).await?;
|
||||
let StartedExecProcess { process } = session;
|
||||
let StartedExecProcess { process, .. } = session;
|
||||
let wake_rx = process.subscribe_wake();
|
||||
let (output, exit_code, closed) = collect_process_output_from_reads(process, wake_rx).await?;
|
||||
|
||||
@@ -195,12 +198,13 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let StartedExecProcess { process } = session;
|
||||
let StartedExecProcess { process, .. } = session;
|
||||
let wake_rx = process.subscribe_wake();
|
||||
let (output, exit_code, closed) = collect_process_output_from_reads(process, wake_rx).await?;
|
||||
assert_eq!(output, "queued output\n");
|
||||
@@ -225,6 +229,7 @@ async fn remote_exec_process_reports_transport_disconnect() -> Result<()> {
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_exec_server::ExecResponse;
|
||||
use codex_exec_server::InitializeParams;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use common::exec_server::exec_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -63,7 +64,8 @@ async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
process_start_response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-1")
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
sandbox_type: SandboxType::None,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
url = { workspace = true }
|
||||
|
||||
@@ -12,6 +12,8 @@ pub use bwrap::find_system_bwrap_in_path;
|
||||
pub use bwrap::system_bwrap_warning;
|
||||
pub use manager::SandboxCommand;
|
||||
pub use manager::SandboxExecRequest;
|
||||
pub use manager::SandboxLaunchConfig;
|
||||
pub use manager::SandboxLaunchMode;
|
||||
pub use manager::SandboxManager;
|
||||
pub use manager::SandboxTransformError;
|
||||
pub use manager::SandboxTransformRequest;
|
||||
|
||||
@@ -15,12 +15,15 @@ use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SandboxType {
|
||||
None,
|
||||
MacosSeatbelt,
|
||||
@@ -46,6 +49,68 @@ pub enum SandboxablePreference {
|
||||
Forbid,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SandboxLaunchMode {
|
||||
Disabled,
|
||||
Auto,
|
||||
Require,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SandboxLaunchConfig {
|
||||
pub mode: SandboxLaunchMode,
|
||||
pub policy: SandboxPolicy,
|
||||
pub file_system_policy: FileSystemSandboxPolicy,
|
||||
pub network_policy: NetworkSandboxPolicy,
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
pub enforce_managed_network: bool,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
pub use_legacy_landlock: bool,
|
||||
}
|
||||
|
||||
impl SandboxLaunchConfig {
|
||||
pub fn sandbox_type(&self) -> SandboxType {
|
||||
let preference = match self.mode {
|
||||
SandboxLaunchMode::Disabled => return SandboxType::None,
|
||||
SandboxLaunchMode::Auto => SandboxablePreference::Auto,
|
||||
SandboxLaunchMode::Require => SandboxablePreference::Require,
|
||||
};
|
||||
|
||||
SandboxManager::new().select_initial(
|
||||
&self.file_system_policy,
|
||||
self.network_policy,
|
||||
preference,
|
||||
self.windows_sandbox_level,
|
||||
self.enforce_managed_network,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn transform(
|
||||
&self,
|
||||
command: SandboxCommand,
|
||||
network: Option<&NetworkProxy>,
|
||||
codex_linux_sandbox_exe: Option<&PathBuf>,
|
||||
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
||||
SandboxManager::new().transform(SandboxTransformRequest {
|
||||
command,
|
||||
policy: &self.policy,
|
||||
file_system_policy: &self.file_system_policy,
|
||||
network_policy: self.network_policy,
|
||||
sandbox: self.sandbox_type(),
|
||||
enforce_managed_network: self.enforce_managed_network,
|
||||
network,
|
||||
sandbox_policy_cwd: self.sandbox_policy_cwd.as_path(),
|
||||
codex_linux_sandbox_exe,
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
|
||||
if cfg!(target_os = "macos") {
|
||||
Some(SandboxType::MacosSeatbelt)
|
||||
|
||||
@@ -27,9 +27,9 @@ use tempfile::TempDir;
|
||||
|
||||
fn assert_seatbelt_denied(stderr: &[u8], path: &Path) {
|
||||
let stderr = String::from_utf8_lossy(stderr);
|
||||
let expected = format!("bash: {}: Operation not permitted\n", path.display());
|
||||
let path_display = path.display().to_string();
|
||||
assert!(
|
||||
stderr == expected
|
||||
(stderr.contains(&path_display) && stderr.contains("Operation not permitted"))
|
||||
|| stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted"),
|
||||
"unexpected stderr: {stderr}"
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user