Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions codex-rs/core/src/tools/sandboxing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,11 +496,7 @@ impl<'a> SandboxAttempt<'a> {
exec_request.exec_server_sandbox = Some(FileSystemSandboxContext {
permissions: exec_server_permissions.into(),
cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()),
workspace_roots: self
.workspace_roots
.iter()
.map(PathUri::from_abs_path)
.collect(),
workspace_roots: Vec::new(),
Comment thread
jif-oai marked this conversation as resolved.
windows_sandbox_level: self.windows_sandbox_level,
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
use_legacy_landlock: self.use_legacy_landlock,
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/tools/sandboxing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
Some(codex_exec_server::FileSystemSandboxContext {
permissions: exec_server_permissions.into(),
cwd: Some(cwd_uri),
workspace_roots: vec![PathUri::from_abs_path(&cwd)],
workspace_roots: Vec::new(),
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
windows_sandbox_private_desktop: false,
use_legacy_landlock: false,
Expand Down
48 changes: 47 additions & 1 deletion codex-rs/exec-server/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,9 @@ impl Environment {
exec_server_url: None,
remote_client: None,
startup_task: Arc::new(Mutex::new(None)),
exec_backend: Arc::new(LocalProcess::default()),
exec_backend: Arc::new(LocalProcess::with_local_runtime_paths(
local_runtime_paths.clone(),
)),
filesystem: Arc::new(LocalFileSystem::with_runtime_paths(
local_runtime_paths.clone(),
)),
Expand Down Expand Up @@ -1165,6 +1167,50 @@ mod tests {
assert_eq!(response.process.process_id().as_str(), "default-env-proc");
}

#[tokio::test]
async fn local_environment_passes_runtime_paths_to_exec_backend() {
let environment = Environment::local(test_runtime_paths());
#[cfg(unix)]
let uri = "file://server/share/checkout";
#[cfg(windows)]
let uri = "file:///usr/local/checkout";
let sandbox_cwd = PathUri::parse(uri).expect("non-native sandbox cwd URI");
let source = sandbox_cwd
.to_abs_path()
.expect_err("sandbox cwd should not be native to this host");
let sandbox = crate::FileSystemSandboxContext::from_permission_profile_with_cwd(
codex_protocol::models::PermissionProfile::workspace_write(),
sandbox_cwd.clone(),
);

let result = environment
.get_exec_backend()
.start(crate::ExecParams {
process_id: ProcessId::from("local-sandbox-proc"),
argv: vec!["true".to_string()],
cwd: PathUri::from_path(std::env::current_dir().expect("read current dir"))
.expect("cwd URI"),
env_policy: None,
env: Default::default(),
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: Some(sandbox),
enforce_managed_network: false,
})
.await;
let Err(err) = result else {
panic!("sandbox cwd should be rejected after resolving runtime paths");
};

assert_eq!(
err.to_string(),
format!(
"exec-server rejected request (-32602): sandbox cwd URI `{sandbox_cwd}` is not valid on this exec-server host: {source}"
)
);
}

#[tokio::test]
async fn test_environment_rejects_sandboxed_filesystem_without_runtime_paths() {
let environment = Environment::default_for_tests();
Expand Down
1 change: 1 addition & 0 deletions codex-rs/exec-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
61 changes: 39 additions & 22 deletions codex-rs/exec-server/src/local_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ use crate::ExecProcessEvent;
use crate::ExecProcessEventReceiver;
use crate::ExecProcessFuture;
use crate::ExecServerError;
use crate::ExecServerRuntimePaths;
use crate::ProcessId;
use crate::StartedExecProcess;
use crate::process::ExecProcessEventLog;
use crate::process_sandbox::prepare_exec_request;
use crate::protocol::EXEC_CLOSED_METHOD;
use crate::protocol::ExecClosedNotification;
use crate::protocol::ExecEnvPolicy;
Expand Down Expand Up @@ -133,6 +135,7 @@ struct Inner {
#[derive(Clone)]
pub(crate) struct LocalProcess {
inner: Arc<Inner>,
runtime_paths: Option<ExecServerRuntimePaths>,
}

struct LocalExecProcess {
Expand All @@ -144,20 +147,39 @@ struct LocalExecProcess {

impl Default for LocalProcess {
fn default() -> Self {
Self::with_discarded_notifications(/*runtime_paths*/ None)
}
}

impl LocalProcess {
pub(crate) fn with_local_runtime_paths(runtime_paths: ExecServerRuntimePaths) -> Self {
Self::with_discarded_notifications(Some(runtime_paths))
}

fn with_discarded_notifications(runtime_paths: Option<ExecServerRuntimePaths>) -> Self {
let (outgoing_tx, mut outgoing_rx) =
mpsc::channel::<RpcServerOutboundMessage>(NOTIFICATION_CHANNEL_CAPACITY);
tokio::spawn(async move { while outgoing_rx.recv().await.is_some() {} });
Self::new(RpcNotificationSender::new(outgoing_tx))
Self::with_runtime_paths(RpcNotificationSender::new(outgoing_tx), runtime_paths)
}
}

impl LocalProcess {
pub(crate) fn new(notifications: RpcNotificationSender) -> Self {
pub(crate) fn new(
notifications: RpcNotificationSender,
runtime_paths: ExecServerRuntimePaths,
) -> Self {
Self::with_runtime_paths(notifications, Some(runtime_paths))
}

fn with_runtime_paths(
notifications: RpcNotificationSender,
runtime_paths: Option<ExecServerRuntimePaths>,
) -> Self {
Self {
inner: Arc::new(Inner {
notifications: std::sync::RwLock::new(Some(notifications)),
processes: Mutex::new(HashMap::new()),
}),
runtime_paths,
}
}

Expand Down Expand Up @@ -191,16 +213,12 @@ impl LocalProcess {
params: ExecParams,
) -> Result<(ExecResponse, watch::Sender<u64>, ExecProcessEventLog), JSONRPCErrorError> {
let process_id = params.process_id.clone();
let (program, args) = params
.argv
let prepared =
prepare_exec_request(&params, child_env(&params), self.runtime_paths.as_ref())?;
let (program, args) = prepared
.command
.split_first()
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
let native_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 start = Arc::new(ProcessStart);
{
Expand All @@ -216,33 +234,32 @@ impl LocalProcess {
);
}

let env = child_env(&params);
let spawned_result = if params.tty {
codex_utils_pty::spawn_pty_process(
program,
args,
native_cwd.as_path(),
&env,
&params.arg0,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
TerminalSize::default(),
)
.await
} else if params.pipe_stdin {
codex_utils_pty::spawn_pipe_process(
program,
args,
native_cwd.as_path(),
&env,
&params.arg0,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
)
.await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(
program,
args,
native_cwd.as_path(),
&env,
&params.arg0,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
)
.await
};
Expand Down
132 changes: 132 additions & 0 deletions codex-rs/exec-server/src/process_sandbox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use std::collections::HashMap;

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;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;

use crate::ExecServerRuntimePaths;
use crate::protocol::ExecParams;
use crate::rpc::invalid_params;

pub(crate) struct PreparedExecRequest {
pub(crate) command: Vec<String>,
pub(crate) cwd: AbsolutePathBuf,
pub(crate) env: HashMap<String, String>,
pub(crate) arg0: Option<String>,
}

pub(crate) fn prepare_exec_request(
params: &ExecParams,
env: HashMap<String, String>,
runtime_paths: Option<&ExecServerRuntimePaths>,
) -> Result<PreparedExecRequest, JSONRPCErrorError> {
let Some(sandbox_context) = params.sandbox.as_ref() else {
return Ok(PreparedExecRequest {
command: params.argv.clone(),
cwd: native_path(&params.cwd, "cwd")?,
env,
arg0: params.arg0.clone(),
});
};
let runtime_paths = runtime_paths
.ok_or_else(|| invalid_params("sandbox runtime paths are not configured".to_string()))?;
// TODO(jif): Transport permissions before orchestrator-local paths are materialized,
// then resolve executor-local helper and workspace paths here.
let permissions: PermissionProfile = sandbox_context
.permissions
.clone()
.try_into()
.map_err(|err| invalid_params(format!("invalid sandbox permission path URI: {err}")))?;
let sandbox_policy_cwd = sandbox_context.cwd.as_ref().unwrap_or(&params.cwd);
let native_sandbox_policy_cwd = native_path(sandbox_policy_cwd, "sandbox cwd")?;
let native_workspace_roots = sandbox_context
.workspace_roots
Comment thread
jif-oai marked this conversation as resolved.
.iter()
.map(|root| native_path(root, "sandbox workspace root"))
.collect::<Result<Vec<_>, _>>()?;
let workspace_roots = if native_workspace_roots.is_empty() {
std::slice::from_ref(&native_sandbox_policy_cwd)
} else {
native_workspace_roots.as_slice()
};
Comment thread
jif-oai marked this conversation as resolved.
let permissions = permissions.materialize_project_roots_with_workspace_roots(workspace_roots);
let (file_system_policy, network_policy) = permissions.to_runtime_permissions();
let sandbox_manager = SandboxManager::new();
let sandbox = sandbox_manager.select_initial(
&file_system_policy,
network_policy,
SandboxablePreference::Require,
sandbox_context.windows_sandbox_level,
params.enforce_managed_network,
);
match sandbox {
SandboxType::None => {
return Err(invalid_params(
"sandbox intent cannot be enforced on this executor".to_string(),
));
}
SandboxType::WindowsRestrictedToken => {
// TODO(jif): Launch generic remote commands through the Windows sandbox session API
// while preserving argv and TTY behavior and passing the child environment out of band.
return Err(invalid_params(
"sandboxed remote process launch is not supported on Windows".to_string(),
Comment thread
jif-oai marked this conversation as resolved.
));
}
SandboxType::MacosSeatbelt | SandboxType::LinuxSeccomp => {}
}
let (program, args) = params
.argv
.split_first()
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
let request = sandbox_manager
.transform_for_direct_spawn(SandboxDirectSpawnTransformRequest {
workspace_roots,
transform: SandboxTransformRequest {
// TODO(jif): Preserve params.arg0 for the inner command across the sandbox
// wrapper, or reject sandboxed requests with a custom arg0.
command: SandboxCommand {
program: program.into(),
args: args.to_vec(),
cwd: params.cwd.clone(),
env,
additional_permissions: None,
Comment thread
jif-oai marked this conversation as resolved.
},
permissions: &permissions,
sandbox,
enforce_managed_network: params.enforce_managed_network,
environment_id: None,
network: None,
Comment thread
jif-oai marked this conversation as resolved.
Comment thread
jif-oai marked this conversation as resolved.
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}")))?;
Ok(PreparedExecRequest {
command: request.command,
cwd: native_path(&request.cwd, "cwd")?,
env: request.env,
Comment thread
jif-oai marked this conversation as resolved.
Comment thread
jif-oai marked this conversation as resolved.
arg0: request.arg0,
})
Comment thread
jif-oai marked this conversation as resolved.
}

fn native_path(path: &PathUri, label: &str) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
path.to_abs_path().map_err(|err| {
invalid_params(format!(
"{label} URI `{path}` is not valid on this exec-server host: {err}"
))
})
}

#[cfg(test)]
#[path = "process_sandbox_tests.rs"]
mod tests;
Loading
Loading