mirror of
https://github.com/openai/codex.git
synced 2026-04-14 19:41:45 +03:00
Compare commits
28 Commits
exec-env-p
...
exec-serve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d6ed7062f | ||
|
|
59cf71736f | ||
|
|
61500eed40 | ||
|
|
3ef4c50446 | ||
|
|
bfbb55be89 | ||
|
|
d9c23630e4 | ||
|
|
209f50a22d | ||
|
|
7cbc9c6a79 | ||
|
|
0edcaba9eb | ||
|
|
a69385f63f | ||
|
|
5824352d53 | ||
|
|
2a59e8964d | ||
|
|
77174e47af | ||
|
|
dabddb82ce | ||
|
|
d2a1941ab2 | ||
|
|
6fcbd9b8d9 | ||
|
|
083137fccd | ||
|
|
26bc39a982 | ||
|
|
aa64ec9000 | ||
|
|
09f332e2a3 | ||
|
|
c8ede67d06 | ||
|
|
6c3e55f61d | ||
|
|
3a729c0da2 | ||
|
|
f410a7e6a9 | ||
|
|
685f8cb6ee | ||
|
|
df70ed0c73 | ||
|
|
79914729bb | ||
|
|
0ee9282dde |
3
codex-rs/Cargo.lock
generated
3
codex-rs/Cargo.lock
generated
@@ -2095,7 +2095,9 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"clap",
|
||||
"codex-app-server-protocol",
|
||||
"codex-linux-sandbox",
|
||||
"codex-protocol",
|
||||
"codex-sandboxing",
|
||||
"codex-utils-absolute-path",
|
||||
"codex-utils-cargo-bin",
|
||||
"codex-utils-pty",
|
||||
@@ -2685,6 +2687,7 @@ dependencies = [
|
||||
"dunce",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
|
||||
#[cfg(target_os = "linux")]
|
||||
use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0;
|
||||
use codex_utils_home_dir::find_codex_home;
|
||||
#[cfg(unix)]
|
||||
@@ -85,10 +86,9 @@ pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
|
||||
}
|
||||
}
|
||||
|
||||
if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
|
||||
// Safety: [`run_main`] never returns.
|
||||
codex_linux_sandbox::run_main();
|
||||
} else if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 {
|
||||
codex_linux_sandbox::dispatch_if_requested();
|
||||
|
||||
if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 {
|
||||
codex_apply_patch::main();
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,7 @@ use codex_protocol::protocol::ExecCommandOutputDeltaEvent;
|
||||
use codex_protocol::protocol::ExecOutputStream;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -115,21 +114,6 @@ pub enum ExecCapturePolicy {
|
||||
FullBuffer,
|
||||
}
|
||||
|
||||
fn select_process_exec_tool_sandbox_type(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
||||
enforce_managed_network: bool,
|
||||
) -> SandboxType {
|
||||
SandboxManager::new().select_initial(
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
SandboxablePreference::Auto,
|
||||
windows_sandbox_level,
|
||||
enforce_managed_network,
|
||||
)
|
||||
}
|
||||
|
||||
/// Mechanism to terminate an exec invocation before it finishes naturally.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ExecExpiration {
|
||||
@@ -238,15 +222,7 @@ pub fn build_exec_request(
|
||||
codex_linux_sandbox_exe: &Option<PathBuf>,
|
||||
use_legacy_landlock: bool,
|
||||
) -> Result<ExecRequest> {
|
||||
let windows_sandbox_level = params.windows_sandbox_level;
|
||||
let enforce_managed_network = params.network.is_some();
|
||||
let sandbox_type = select_process_exec_tool_sandbox_type(
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
windows_sandbox_level,
|
||||
enforce_managed_network,
|
||||
);
|
||||
tracing::debug!("Sandbox type: {sandbox_type:?}");
|
||||
|
||||
let ExecParams {
|
||||
command,
|
||||
@@ -271,7 +247,6 @@ pub fn build_exec_request(
|
||||
))
|
||||
})?;
|
||||
|
||||
let manager = SandboxManager::new();
|
||||
let command = SandboxCommand {
|
||||
program: program.clone().into(),
|
||||
args: args.to_vec(),
|
||||
@@ -283,23 +258,28 @@ pub fn build_exec_request(
|
||||
expiration,
|
||||
capture_policy,
|
||||
};
|
||||
let mut exec_req = manager
|
||||
.transform(SandboxTransformRequest {
|
||||
let sandbox_launch_config = SandboxLaunchConfig {
|
||||
sandbox_preference: SandboxablePreference::Auto,
|
||||
policy: sandbox_policy.clone(),
|
||||
file_system_policy: file_system_sandbox_policy.clone(),
|
||||
network_policy: network_sandbox_policy,
|
||||
sandbox_policy_cwd: sandbox_cwd.to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
use_legacy_landlock,
|
||||
};
|
||||
let mut exec_req = codex_sandboxing::SandboxManager::new()
|
||||
.transform(
|
||||
command,
|
||||
policy: sandbox_policy,
|
||||
file_system_policy: file_system_sandbox_policy,
|
||||
network_policy: network_sandbox_policy,
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network,
|
||||
network: network.as_ref(),
|
||||
sandbox_policy_cwd: sandbox_cwd,
|
||||
codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
})
|
||||
&sandbox_launch_config,
|
||||
network.as_ref(),
|
||||
codex_linux_sandbox_exe.as_deref(),
|
||||
)
|
||||
.map(|request| ExecRequest::from_sandbox_exec_request(request, options))
|
||||
.map_err(CodexErr::from)?;
|
||||
tracing::debug!("Sandbox type: {:?}", exec_req.sandbox);
|
||||
exec_req.windows_restricted_token_filesystem_overlay =
|
||||
resolve_windows_restricted_token_filesystem_overlay(
|
||||
exec_req.sandbox,
|
||||
|
||||
@@ -688,11 +688,12 @@ fn process_exec_tool_call_uses_platform_sandbox_for_network_only_restrictions()
|
||||
.unwrap_or(SandboxType::None);
|
||||
|
||||
assert_eq!(
|
||||
select_process_exec_tool_sandbox_type(
|
||||
codex_sandboxing::SandboxManager::new().select_initial(
|
||||
&FileSystemSandboxPolicy::unrestricted(),
|
||||
NetworkSandboxPolicy::Restricted,
|
||||
codex_sandboxing::SandboxablePreference::Auto,
|
||||
codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
/*enforce_managed_network*/ false,
|
||||
/*has_managed_network_requirements*/ false,
|
||||
),
|
||||
expected
|
||||
);
|
||||
|
||||
@@ -12,9 +12,6 @@ use crate::exec::ExecExpiration;
|
||||
use crate::exec::StdoutStream;
|
||||
use crate::exec::WindowsRestrictedTokenFilesystemOverlay;
|
||||
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_network_proxy::NetworkProxy;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::exec_output::ExecToolCallOutput;
|
||||
@@ -91,10 +88,12 @@ impl ExecRequest {
|
||||
request: SandboxExecRequest,
|
||||
options: ExecOptions,
|
||||
) -> Self {
|
||||
// SandboxManager::transform prepares spawn env (`CODEX_SANDBOX*`).
|
||||
// This adapter only attaches core-owned exec options.
|
||||
let SandboxExecRequest {
|
||||
command,
|
||||
cwd,
|
||||
mut env,
|
||||
env,
|
||||
network,
|
||||
sandbox,
|
||||
windows_sandbox_level,
|
||||
@@ -108,16 +107,6 @@ impl ExecRequest {
|
||||
expiration,
|
||||
capture_policy,
|
||||
} = options;
|
||||
if !network_sandbox_policy.is_enabled() {
|
||||
env.insert(
|
||||
CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR.to_string(),
|
||||
"1".to_string(),
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if sandbox == SandboxType::MacosSeatbelt {
|
||||
env.insert(CODEX_SANDBOX_ENV_VAR.to_string(), "seatbelt".to_string());
|
||||
}
|
||||
Self {
|
||||
command,
|
||||
cwd,
|
||||
|
||||
@@ -42,8 +42,7 @@ use crate::sandboxing::ExecOptions;
|
||||
use crate::tools::ToolRouter;
|
||||
use crate::tools::context::SharedTurnDiffTracker;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
@@ -1030,20 +1029,12 @@ impl JsReplManager {
|
||||
);
|
||||
}
|
||||
|
||||
let sandbox = SandboxManager::new();
|
||||
let has_managed_network_requirements = turn
|
||||
.config
|
||||
.config_layer_stack
|
||||
.requirements_toml()
|
||||
.network
|
||||
.is_some();
|
||||
let sandbox_type = sandbox.select_initial(
|
||||
&turn.file_system_sandbox_policy,
|
||||
turn.network_sandbox_policy,
|
||||
SandboxablePreference::Auto,
|
||||
turn.windows_sandbox_level,
|
||||
has_managed_network_requirements,
|
||||
);
|
||||
let command = SandboxCommand {
|
||||
program: node_path.into_os_string(),
|
||||
args: vec![
|
||||
@@ -1058,24 +1049,28 @@ impl JsReplManager {
|
||||
expiration: ExecExpiration::DefaultTimeout,
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
};
|
||||
let exec_env = sandbox
|
||||
.transform(SandboxTransformRequest {
|
||||
let sandbox_launch_config = SandboxLaunchConfig {
|
||||
sandbox_preference: SandboxablePreference::Auto,
|
||||
policy: turn.sandbox_policy.get().clone(),
|
||||
file_system_policy: turn.file_system_sandbox_policy.clone(),
|
||||
network_policy: turn.network_sandbox_policy,
|
||||
sandbox_policy_cwd: turn.cwd.to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
windows_sandbox_level: turn.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn
|
||||
.config
|
||||
.permissions
|
||||
.windows_sandbox_private_desktop,
|
||||
use_legacy_landlock: turn.features.use_legacy_landlock(),
|
||||
};
|
||||
let exec_env = codex_sandboxing::SandboxManager::new()
|
||||
.transform(
|
||||
command,
|
||||
policy: &turn.sandbox_policy,
|
||||
file_system_policy: &turn.file_system_sandbox_policy,
|
||||
network_policy: turn.network_sandbox_policy,
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
network: None,
|
||||
sandbox_policy_cwd: &turn.cwd,
|
||||
codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: turn.features.use_legacy_landlock(),
|
||||
windows_sandbox_level: turn.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn
|
||||
.config
|
||||
.permissions
|
||||
.windows_sandbox_private_desktop,
|
||||
})
|
||||
&sandbox_launch_config,
|
||||
/*network*/ None,
|
||||
turn.codex_linux_sandbox_exe.as_deref(),
|
||||
)
|
||||
.map(|request| {
|
||||
crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options)
|
||||
})
|
||||
|
||||
@@ -33,8 +33,8 @@ use codex_protocol::protocol::NetworkPolicyRuleAction;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_shell_command::bash::parse_shell_lc_plain_commands;
|
||||
@@ -817,13 +817,6 @@ impl CoreShellCommandExecutor {
|
||||
.split_first()
|
||||
.ok_or_else(|| anyhow::anyhow!("prepared command must not be empty"))?;
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
file_system_sandbox_policy,
|
||||
network_sandbox_policy,
|
||||
SandboxablePreference::Auto,
|
||||
self.windows_sandbox_level,
|
||||
self.network.is_some(),
|
||||
);
|
||||
let command = SandboxCommand {
|
||||
program: program.clone().into(),
|
||||
args: args.to_vec(),
|
||||
@@ -835,20 +828,24 @@ impl CoreShellCommandExecutor {
|
||||
expiration: ExecExpiration::DefaultTimeout,
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
};
|
||||
let exec_request = sandbox_manager.transform(SandboxTransformRequest {
|
||||
command,
|
||||
policy: sandbox_policy,
|
||||
file_system_policy: file_system_sandbox_policy,
|
||||
let sandbox_launch_config = SandboxLaunchConfig {
|
||||
sandbox_preference: SandboxablePreference::Auto,
|
||||
policy: sandbox_policy.clone(),
|
||||
file_system_policy: file_system_sandbox_policy.clone(),
|
||||
network_policy: network_sandbox_policy,
|
||||
sandbox,
|
||||
sandbox_policy_cwd: self.sandbox_policy_cwd.clone(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: self.network.is_some(),
|
||||
network: self.network.as_ref(),
|
||||
sandbox_policy_cwd: &self.sandbox_policy_cwd,
|
||||
codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})?;
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
};
|
||||
let exec_request = sandbox_manager.transform(
|
||||
command,
|
||||
&sandbox_launch_config,
|
||||
self.network.as_ref(),
|
||||
self.codex_linux_sandbox_exe.as_deref(),
|
||||
)?;
|
||||
let mut exec_request =
|
||||
crate::sandboxing::ExecRequest::from_sandbox_exec_request(exec_request, options);
|
||||
if let Some(network) = exec_request.network.as_ref() {
|
||||
|
||||
@@ -38,6 +38,7 @@ 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::SandboxablePreference;
|
||||
use codex_shell_command::powershell::prefix_powershell_script_with_utf8;
|
||||
use codex_tools::UnifiedExecShellMode;
|
||||
@@ -82,6 +83,24 @@ pub struct UnifiedExecRuntime<'a> {
|
||||
shell_mode: UnifiedExecShellMode,
|
||||
}
|
||||
|
||||
fn build_remote_exec_sandbox_config(
|
||||
attempt: &SandboxAttempt<'_>,
|
||||
additional_permissions: Option<PermissionProfile>,
|
||||
) -> SandboxLaunchConfig {
|
||||
SandboxLaunchConfig {
|
||||
sandbox_preference: attempt.sandbox_preference(),
|
||||
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(),
|
||||
additional_permissions,
|
||||
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 {
|
||||
@@ -219,6 +238,52 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
if let Some(network) = req.network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
}
|
||||
if let Some(environment) = ctx
|
||||
.turn
|
||||
.environment
|
||||
.as_ref()
|
||||
.filter(|environment| environment.exec_server_url().is_some())
|
||||
{
|
||||
// Let the exec-server host perform sandbox transformation for its platform.
|
||||
// The local fallback below still transforms in core, where the session's
|
||||
// linux-sandbox helper path and zsh-fork spawn lifecycle are available.
|
||||
if let UnifiedExecShellMode::ZshFork(_) = &self.shell_mode {
|
||||
return Err(ToolError::Rejected(
|
||||
"unified_exec zsh-fork is not supported when exec_server_url is configured"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let exec_params = codex_exec_server::ExecParams {
|
||||
process_id: req.process_id.to_string().into(),
|
||||
argv: command,
|
||||
cwd: req.cwd.to_path_buf(),
|
||||
env,
|
||||
tty: req.tty,
|
||||
arg0: None,
|
||||
sandbox: build_remote_exec_sandbox_config(
|
||||
attempt,
|
||||
req.additional_permissions.clone(),
|
||||
),
|
||||
};
|
||||
return self
|
||||
.manager
|
||||
.open_session_with_remote_exec(exec_params, 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()),
|
||||
});
|
||||
}
|
||||
let Some(environment) = ctx.turn.environment.as_ref() else {
|
||||
return Err(ToolError::Rejected(
|
||||
"exec_command is unavailable in this session".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())
|
||||
@@ -287,11 +352,6 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
let exec_env = attempt
|
||||
.env_for(command, options, req.network.as_ref())
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
let Some(environment) = ctx.turn.environment.as_ref() else {
|
||||
return Err(ToolError::Rejected(
|
||||
"exec_command is unavailable in this session".to_string(),
|
||||
));
|
||||
};
|
||||
self.manager
|
||||
.open_session_with_exec_env(
|
||||
req.process_id,
|
||||
|
||||
@@ -22,9 +22,9 @@ use codex_protocol::protocol::ReviewDecision;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformError;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use futures::Future;
|
||||
@@ -332,29 +332,41 @@ pub(crate) struct SandboxAttempt<'a> {
|
||||
}
|
||||
|
||||
impl<'a> SandboxAttempt<'a> {
|
||||
pub(crate) fn sandbox_preference(&self) -> SandboxablePreference {
|
||||
match self.sandbox {
|
||||
SandboxType::None => SandboxablePreference::Forbid,
|
||||
SandboxType::MacosSeatbelt
|
||||
| SandboxType::LinuxSeccomp
|
||||
| SandboxType::WindowsRestrictedToken => SandboxablePreference::Require,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn env_for(
|
||||
&self,
|
||||
command: SandboxCommand,
|
||||
options: ExecOptions,
|
||||
network: Option<&NetworkProxy>,
|
||||
) -> Result<crate::sandboxing::ExecRequest, SandboxTransformError> {
|
||||
let sandbox_launch_config = SandboxLaunchConfig {
|
||||
sandbox_preference: self.sandbox_preference(),
|
||||
policy: self.policy.clone(),
|
||||
file_system_policy: self.file_system_policy.clone(),
|
||||
network_policy: self.network_policy,
|
||||
sandbox_policy_cwd: self.sandbox_cwd.to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: self.enforce_managed_network,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
};
|
||||
self.manager
|
||||
.transform(SandboxTransformRequest {
|
||||
.transform(
|
||||
command,
|
||||
policy: self.policy,
|
||||
file_system_policy: self.file_system_policy,
|
||||
network_policy: self.network_policy,
|
||||
sandbox: self.sandbox,
|
||||
enforce_managed_network: self.enforce_managed_network,
|
||||
&sandbox_launch_config,
|
||||
network,
|
||||
sandbox_policy_cwd: self.sandbox_cwd,
|
||||
codex_linux_sandbox_exe: self
|
||||
.codex_linux_sandbox_exe
|
||||
self.codex_linux_sandbox_exe
|
||||
.map(std::path::PathBuf::as_path),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
||||
})
|
||||
)
|
||||
.map(|request| {
|
||||
crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options)
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! Flow at a glance (open process)
|
||||
//! 1) Build a small request `{ command, cwd }`.
|
||||
//! 2) Orchestrator: approval (bypass/cache/prompt) → select sandbox → run.
|
||||
//! 3) Runtime: transform `SandboxTransformRequest` -> `ExecRequest` -> spawn PTY.
|
||||
//! 3) Runtime: transform sandbox config -> `ExecRequest` -> spawn PTY.
|
||||
//! 4) If denial, orchestrator retries with `SandboxType::None`.
|
||||
//! 5) Process handle is returned with streaming output + metadata.
|
||||
//!
|
||||
|
||||
@@ -609,6 +609,9 @@ impl UnifiedExecProcessManager {
|
||||
env: request.env.clone(),
|
||||
tty,
|
||||
arg0: request.arg0.clone(),
|
||||
sandbox: codex_sandboxing::SandboxLaunchConfig::no_sandbox(
|
||||
request.cwd.to_path_buf(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
||||
@@ -643,6 +646,20 @@ impl UnifiedExecProcessManager {
|
||||
UnifiedExecProcess::from_spawned(spawned, request.sandbox, spawn_lifecycle).await
|
||||
}
|
||||
|
||||
pub(crate) async fn open_session_with_remote_exec(
|
||||
&self,
|
||||
params: codex_exec_server::ExecParams,
|
||||
environment: &codex_exec_server::Environment,
|
||||
) -> Result<UnifiedExecProcess, UnifiedExecError> {
|
||||
let started = environment
|
||||
.get_exec_backend()
|
||||
.start(params)
|
||||
.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 {
|
||||
|
||||
@@ -15,10 +15,12 @@ path = "src/bin/codex-exec-server.rs"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive"] }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
@@ -26,6 +28,7 @@ codex-utils-pty = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"fs",
|
||||
@@ -41,6 +44,9 @@ tokio = { workspace = true, features = [
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
codex-linux-sandbox = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
|
||||
@@ -242,7 +242,7 @@ The crate exports:
|
||||
- `RemoteExecServerConnectArgs`
|
||||
- protocol structs `InitializeParams` and `InitializeResponse`
|
||||
- `DEFAULT_LISTEN_URL` and `ExecServerListenUrlParseError`
|
||||
- `run_main_with_listen_url()`
|
||||
- `run_main_with_runtime()`
|
||||
- `run_main()` for embedding the websocket server in a binary
|
||||
|
||||
## Example session
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use clap::Parser;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct ExecServerArgs {
|
||||
/// Transport endpoint URL. Supported values: `ws://IP:PORT` (default).
|
||||
@@ -11,8 +13,31 @@ struct ExecServerArgs {
|
||||
listen: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let args = ExecServerArgs::parse();
|
||||
codex_exec_server::run_main_with_listen_url(&args.listen).await
|
||||
fn main() -> anyhow::Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
codex_linux_sandbox::dispatch_if_requested();
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
runtime.block_on(run_main(linux_sandbox_exe()))
|
||||
}
|
||||
|
||||
async fn run_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
||||
let args = ExecServerArgs::parse();
|
||||
let runtime = codex_exec_server::ExecServerRuntimeConfig::new(codex_linux_sandbox_exe);
|
||||
codex_exec_server::run_main_with_runtime(&args.listen, runtime)
|
||||
.await
|
||||
.map_err(|err| anyhow::Error::msg(err.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_sandbox_exe() -> Option<PathBuf> {
|
||||
std::env::current_exe().ok()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
fn linux_sandbox_exe() -> Option<PathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ use tokio::sync::OnceCell;
|
||||
|
||||
use crate::ExecServerClient;
|
||||
use crate::ExecServerError;
|
||||
use crate::ExecServerRuntimeConfig;
|
||||
use crate::RemoteExecServerConnectArgs;
|
||||
use crate::file_system::ExecutorFileSystem;
|
||||
use crate::local_file_system::LocalFileSystem;
|
||||
@@ -22,6 +23,7 @@ pub const CODEX_EXEC_SERVER_URL_ENV_VAR: &str = "CODEX_EXEC_SERVER_URL";
|
||||
pub struct EnvironmentManager {
|
||||
exec_server_url: Option<String>,
|
||||
disabled: bool,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
current_environment: OnceCell<Option<Arc<Environment>>>,
|
||||
}
|
||||
|
||||
@@ -34,17 +36,33 @@ impl Default for EnvironmentManager {
|
||||
impl EnvironmentManager {
|
||||
/// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value.
|
||||
pub fn new(exec_server_url: Option<String>) -> Self {
|
||||
Self::new_with_runtime(exec_server_url, ExecServerRuntimeConfig::detect())
|
||||
}
|
||||
|
||||
/// Builds a manager from the raw `CODEX_EXEC_SERVER_URL` value and the
|
||||
/// runtime resources available in this client process.
|
||||
pub fn new_with_runtime(
|
||||
exec_server_url: Option<String>,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
) -> Self {
|
||||
let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url);
|
||||
Self {
|
||||
exec_server_url,
|
||||
disabled,
|
||||
runtime,
|
||||
current_environment: OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a manager from process environment variables.
|
||||
pub fn from_env() -> Self {
|
||||
Self::new(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok())
|
||||
Self::from_env_with_runtime(ExecServerRuntimeConfig::detect())
|
||||
}
|
||||
|
||||
/// Builds a manager from process environment variables and explicit local
|
||||
/// runtime resources.
|
||||
pub fn from_env_with_runtime(runtime: ExecServerRuntimeConfig) -> Self {
|
||||
Self::new_with_runtime(std::env::var(CODEX_EXEC_SERVER_URL_ENV_VAR).ok(), runtime)
|
||||
}
|
||||
|
||||
/// Builds a manager from the currently selected environment, or from the
|
||||
@@ -54,11 +72,13 @@ impl EnvironmentManager {
|
||||
Some(environment) => Self {
|
||||
exec_server_url: environment.exec_server_url().map(str::to_owned),
|
||||
disabled: false,
|
||||
runtime: ExecServerRuntimeConfig::detect(),
|
||||
current_environment: OnceCell::new(),
|
||||
},
|
||||
None => Self {
|
||||
exec_server_url: None,
|
||||
disabled: true,
|
||||
runtime: ExecServerRuntimeConfig::detect(),
|
||||
current_environment: OnceCell::new(),
|
||||
},
|
||||
}
|
||||
@@ -82,7 +102,11 @@ impl EnvironmentManager {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(Arc::new(
|
||||
Environment::create(self.exec_server_url.clone()).await?,
|
||||
Environment::create_with_runtime(
|
||||
self.exec_server_url.clone(),
|
||||
self.runtime.clone(),
|
||||
)
|
||||
.await?,
|
||||
)))
|
||||
}
|
||||
})
|
||||
@@ -132,6 +156,15 @@ impl std::fmt::Debug for Environment {
|
||||
impl Environment {
|
||||
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value.
|
||||
pub async fn create(exec_server_url: Option<String>) -> Result<Self, ExecServerError> {
|
||||
Self::create_with_runtime(exec_server_url, ExecServerRuntimeConfig::detect()).await
|
||||
}
|
||||
|
||||
/// Builds an environment from the raw `CODEX_EXEC_SERVER_URL` value and
|
||||
/// runtime resources available when spawning local processes.
|
||||
pub async fn create_with_runtime(
|
||||
exec_server_url: Option<String>,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
) -> Result<Self, ExecServerError> {
|
||||
let (exec_server_url, disabled) = normalize_exec_server_url(exec_server_url);
|
||||
if disabled {
|
||||
return Err(ExecServerError::Protocol(
|
||||
@@ -161,7 +194,7 @@ impl Environment {
|
||||
));
|
||||
}
|
||||
None => {
|
||||
let local_process = LocalProcess::default();
|
||||
let local_process = LocalProcess::default_with_runtime(runtime);
|
||||
local_process
|
||||
.initialize()
|
||||
.map_err(|err| ExecServerError::Protocol(err.message))?;
|
||||
@@ -214,6 +247,7 @@ mod tests {
|
||||
use super::Environment;
|
||||
use super::EnvironmentManager;
|
||||
use crate::ProcessId;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
@@ -291,6 +325,9 @@ mod tests {
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: SandboxLaunchConfig::no_sandbox(
|
||||
std::env::current_dir().expect("read current dir"),
|
||||
),
|
||||
})
|
||||
.await
|
||||
.expect("start process");
|
||||
|
||||
@@ -28,6 +28,7 @@ pub use file_system::FileSystemResult;
|
||||
pub use file_system::ReadDirectoryEntry;
|
||||
pub use file_system::RemoveOptions;
|
||||
pub use local_file_system::LOCAL_FS;
|
||||
pub use local_process::ExecServerRuntimeConfig;
|
||||
pub use process::ExecBackend;
|
||||
pub use process::ExecProcess;
|
||||
pub use process::StartedExecProcess;
|
||||
@@ -66,3 +67,4 @@ pub use server::DEFAULT_LISTEN_URL;
|
||||
pub use server::ExecServerListenUrlParseError;
|
||||
pub use server::run_main;
|
||||
pub use server::run_main_with_listen_url;
|
||||
pub use server::run_main_with_runtime;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
@@ -7,6 +8,11 @@ use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxExecRequest;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_pty::ExecCommandSession;
|
||||
use codex_utils_pty::TerminalSize;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -78,6 +84,7 @@ struct Inner {
|
||||
processes: Mutex<HashMap<ProcessId, ProcessEntry>>,
|
||||
initialize_requested: AtomicBool,
|
||||
initialized: AtomicBool,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -91,23 +98,56 @@ struct LocalExecProcess {
|
||||
wake_tx: watch::Sender<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExecServerRuntimeConfig {
|
||||
codex_linux_sandbox_exe: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl ExecServerRuntimeConfig {
|
||||
pub fn new(codex_linux_sandbox_exe: Option<PathBuf>) -> Self {
|
||||
Self {
|
||||
codex_linux_sandbox_exe,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
struct StartedProcess {
|
||||
process_id: ProcessId,
|
||||
sandbox_type: SandboxType,
|
||||
wake_tx: watch::Sender<u64>,
|
||||
}
|
||||
|
||||
impl Default for LocalProcess {
|
||||
fn default() -> 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::default_with_runtime(ExecServerRuntimeConfig::detect())
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalProcess {
|
||||
pub(crate) fn new(notifications: RpcNotificationSender) -> Self {
|
||||
pub(crate) fn default_with_runtime(runtime: ExecServerRuntimeConfig) -> 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_with_runtime(RpcNotificationSender::new(outgoing_tx), runtime)
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalProcess {
|
||||
pub(crate) fn new_with_runtime(
|
||||
notifications: RpcNotificationSender,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Inner {
|
||||
notifications,
|
||||
processes: Mutex::new(HashMap::new()),
|
||||
initialize_requested: AtomicBool::new(false),
|
||||
initialized: AtomicBool::new(false),
|
||||
runtime,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -162,14 +202,12 @@ impl LocalProcess {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn start_process(
|
||||
&self,
|
||||
params: ExecParams,
|
||||
) -> Result<(ExecResponse, watch::Sender<u64>), JSONRPCErrorError> {
|
||||
async fn start_process(&self, params: ExecParams) -> Result<StartedProcess, JSONRPCErrorError> {
|
||||
self.require_initialized_for("exec")?;
|
||||
let process_id = params.process_id.clone();
|
||||
let (program, args) = params
|
||||
.argv
|
||||
let launch = prepare_exec_launch(¶ms, &self.inner.runtime)?;
|
||||
let (program, args) = launch
|
||||
.command
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
|
||||
|
||||
@@ -188,8 +226,8 @@ impl LocalProcess {
|
||||
program,
|
||||
args,
|
||||
params.cwd.as_path(),
|
||||
¶ms.env,
|
||||
¶ms.arg0,
|
||||
&launch.env,
|
||||
&launch.arg0,
|
||||
TerminalSize::default(),
|
||||
)
|
||||
.await
|
||||
@@ -198,8 +236,8 @@ impl LocalProcess {
|
||||
program,
|
||||
args,
|
||||
params.cwd.as_path(),
|
||||
¶ms.env,
|
||||
¶ms.arg0,
|
||||
&launch.env,
|
||||
&launch.arg0,
|
||||
)
|
||||
.await
|
||||
};
|
||||
@@ -264,13 +302,20 @@ impl LocalProcess {
|
||||
output_notify,
|
||||
));
|
||||
|
||||
Ok((ExecResponse { process_id }, wake_tx))
|
||||
Ok(StartedProcess {
|
||||
process_id,
|
||||
sandbox_type: launch.sandbox,
|
||||
wake_tx,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn exec(&self, params: ExecParams) -> Result<ExecResponse, JSONRPCErrorError> {
|
||||
self.start_process(params)
|
||||
.await
|
||||
.map(|(response, _)| response)
|
||||
.map(|started| ExecResponse {
|
||||
process_id: started.process_id,
|
||||
sandbox: started.sandbox_type,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn exec_read(
|
||||
@@ -414,16 +459,17 @@ impl LocalProcess {
|
||||
#[async_trait]
|
||||
impl ExecBackend for LocalProcess {
|
||||
async fn start(&self, params: ExecParams) -> Result<StartedExecProcess, ExecServerError> {
|
||||
let (response, wake_tx) = self
|
||||
let started = self
|
||||
.start_process(params)
|
||||
.await
|
||||
.map_err(map_handler_error)?;
|
||||
Ok(StartedExecProcess {
|
||||
process: Arc::new(LocalExecProcess {
|
||||
process_id: response.process_id,
|
||||
process_id: started.process_id,
|
||||
backend: self.clone(),
|
||||
wake_tx,
|
||||
wake_tx: started.wake_tx,
|
||||
}),
|
||||
sandbox_type: started.sandbox_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -458,6 +504,35 @@ impl ExecProcess for LocalExecProcess {
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_exec_launch(
|
||||
params: &ExecParams,
|
||||
runtime: &ExecServerRuntimeConfig,
|
||||
) -> Result<SandboxExecRequest, JSONRPCErrorError> {
|
||||
let (program, args) = params
|
||||
.argv
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_params("argv must not be empty".to_string()))?;
|
||||
let command = SandboxCommand {
|
||||
program: program.clone().into(),
|
||||
args: args.to_vec(),
|
||||
cwd: AbsolutePathBuf::try_from(params.cwd.as_path())
|
||||
.map_err(|err| invalid_params(format!("cwd must be absolute: {err}")))?,
|
||||
env: params.env.clone(),
|
||||
additional_permissions: params.sandbox.additional_permissions.clone(),
|
||||
};
|
||||
SandboxManager::new()
|
||||
.transform(
|
||||
command,
|
||||
¶ms.sandbox,
|
||||
// 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_deref(),
|
||||
)
|
||||
.map_err(|err| internal_error(format!("failed to build sandbox launch: {err}")))
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
@@ -3,6 +3,8 @@ use std::path::PathBuf;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -63,12 +65,14 @@ pub struct ExecParams {
|
||||
pub env: HashMap<String, String>,
|
||||
pub tty: bool,
|
||||
pub arg0: Option<String>,
|
||||
pub sandbox: SandboxLaunchConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecResponse {
|
||||
pub process_id: ProcessId,
|
||||
pub sandbox: 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,12 @@ pub async fn run_main() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
|
||||
pub async fn run_main_with_listen_url(
|
||||
listen_url: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
transport::run_transport(listen_url).await
|
||||
run_main_with_runtime(listen_url, crate::ExecServerRuntimeConfig::detect()).await
|
||||
}
|
||||
|
||||
pub async fn run_main_with_runtime(
|
||||
listen_url: &str,
|
||||
runtime: crate::ExecServerRuntimeConfig,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
transport::run_transport(listen_url, runtime).await
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
|
||||
use crate::ExecServerRuntimeConfig;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecResponse;
|
||||
use crate::protocol::FsCopyParams;
|
||||
@@ -34,9 +35,12 @@ pub(crate) struct ExecServerHandler {
|
||||
}
|
||||
|
||||
impl ExecServerHandler {
|
||||
pub(crate) fn new(notifications: RpcNotificationSender) -> Self {
|
||||
pub(crate) fn new(
|
||||
notifications: RpcNotificationSender,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
process: ProcessHandler::new(notifications),
|
||||
process: ProcessHandler::new(notifications, runtime),
|
||||
file_system: FileSystemHandler::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ use pretty_assertions::assert_eq;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use super::ExecServerHandler;
|
||||
use crate::ExecServerRuntimeConfig;
|
||||
use crate::ProcessId;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::InitializeResponse;
|
||||
use crate::protocol::TerminateParams;
|
||||
use crate::protocol::TerminateResponse;
|
||||
use crate::rpc::RpcNotificationSender;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
|
||||
fn exec_params(process_id: &str) -> ExecParams {
|
||||
let mut env = HashMap::new();
|
||||
@@ -29,14 +31,16 @@ fn exec_params(process_id: &str) -> ExecParams {
|
||||
env,
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: SandboxLaunchConfig::no_sandbox(std::env::current_dir().expect("cwd")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn initialized_handler() -> Arc<ExecServerHandler> {
|
||||
let (outgoing_tx, _outgoing_rx) = mpsc::channel(16);
|
||||
let handler = Arc::new(ExecServerHandler::new(RpcNotificationSender::new(
|
||||
outgoing_tx,
|
||||
)));
|
||||
let handler = Arc::new(ExecServerHandler::new(
|
||||
RpcNotificationSender::new(outgoing_tx),
|
||||
ExecServerRuntimeConfig::detect(),
|
||||
));
|
||||
assert_eq!(
|
||||
handler.initialize().expect("initialize"),
|
||||
InitializeResponse {}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
|
||||
use crate::ExecServerRuntimeConfig;
|
||||
use crate::local_process::LocalProcess;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecResponse;
|
||||
@@ -18,9 +19,12 @@ pub(crate) struct ProcessHandler {
|
||||
}
|
||||
|
||||
impl ProcessHandler {
|
||||
pub(crate) fn new(notifications: RpcNotificationSender) -> Self {
|
||||
pub(crate) fn new(
|
||||
notifications: RpcNotificationSender,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
process: LocalProcess::new(notifications),
|
||||
process: LocalProcess::new_with_runtime(notifications, runtime),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ExecServerRuntimeConfig;
|
||||
use crate::connection::CHANNEL_CAPACITY;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::connection::JsonRpcConnectionEvent;
|
||||
@@ -15,13 +16,16 @@ use crate::rpc::method_not_found;
|
||||
use crate::server::ExecServerHandler;
|
||||
use crate::server::registry::build_router;
|
||||
|
||||
pub(crate) async fn run_connection(connection: JsonRpcConnection) {
|
||||
pub(crate) async fn run_connection(
|
||||
connection: JsonRpcConnection,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
) {
|
||||
let router = Arc::new(build_router());
|
||||
let (json_outgoing_tx, mut incoming_rx, connection_tasks) = connection.into_parts();
|
||||
let (outgoing_tx, mut outgoing_rx) =
|
||||
mpsc::channel::<RpcServerOutboundMessage>(CHANNEL_CAPACITY);
|
||||
let notifications = RpcNotificationSender::new(outgoing_tx.clone());
|
||||
let handler = Arc::new(ExecServerHandler::new(notifications));
|
||||
let handler = Arc::new(ExecServerHandler::new(notifications, runtime));
|
||||
|
||||
let outbound_task = tokio::spawn(async move {
|
||||
while let Some(message) = outgoing_rx.recv().await {
|
||||
|
||||
@@ -4,6 +4,7 @@ use tokio::net::TcpListener;
|
||||
use tokio_tungstenite::accept_async;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::ExecServerRuntimeConfig;
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::server::processor::run_connection;
|
||||
|
||||
@@ -48,13 +49,15 @@ pub(crate) fn parse_listen_url(
|
||||
|
||||
pub(crate) async fn run_transport(
|
||||
listen_url: &str,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let bind_address = parse_listen_url(listen_url)?;
|
||||
run_websocket_listener(bind_address).await
|
||||
run_websocket_listener(bind_address, runtime).await
|
||||
}
|
||||
|
||||
async fn run_websocket_listener(
|
||||
bind_address: SocketAddr,
|
||||
runtime: ExecServerRuntimeConfig,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let listener = TcpListener::bind(bind_address).await?;
|
||||
let local_addr = listener.local_addr()?;
|
||||
@@ -63,13 +66,17 @@ async fn run_websocket_listener(
|
||||
|
||||
loop {
|
||||
let (stream, peer_addr) = listener.accept().await?;
|
||||
let runtime = runtime.clone();
|
||||
tokio::spawn(async move {
|
||||
match accept_async(stream).await {
|
||||
Ok(websocket) => {
|
||||
run_connection(JsonRpcConnection::from_websocket(
|
||||
websocket,
|
||||
format!("exec-server websocket {peer_addr}"),
|
||||
))
|
||||
run_connection(
|
||||
JsonRpcConnection::from_websocket(
|
||||
websocket,
|
||||
format!("exec-server websocket {peer_addr}"),
|
||||
),
|
||||
runtime,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
|
||||
@@ -12,7 +12,15 @@ use codex_exec_server::ExecProcess;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_exec_server::ReadResponse;
|
||||
use codex_exec_server::StartedExecProcess;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use test_case::test_case;
|
||||
use tokio::sync::watch;
|
||||
use tokio::time::Duration;
|
||||
@@ -26,6 +34,16 @@ struct ProcessContext {
|
||||
server: Option<ExecServerHarness>,
|
||||
}
|
||||
|
||||
fn platform_sandbox_type() -> SandboxType {
|
||||
if cfg!(target_os = "macos") {
|
||||
SandboxType::MacosSeatbelt
|
||||
} else if cfg!(target_os = "linux") {
|
||||
SandboxType::LinuxSeccomp
|
||||
} else {
|
||||
SandboxType::None
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_process_context(use_remote: bool) -> Result<ProcessContext> {
|
||||
if use_remote {
|
||||
let server = exec_server().await?;
|
||||
@@ -45,15 +63,17 @@ async fn create_process_context(use_remote: bool) -> Result<ProcessContext> {
|
||||
|
||||
async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let session = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: std::env::current_dir()?,
|
||||
cwd: cwd.clone(),
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: SandboxLaunchConfig::no_sandbox(cwd),
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), "proc-1");
|
||||
@@ -116,6 +136,7 @@ async fn collect_process_output_from_reads(
|
||||
|
||||
async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let process_id = "proc-stream".to_string();
|
||||
let session = context
|
||||
.backend
|
||||
@@ -126,15 +147,16 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
|
||||
"-c".to_string(),
|
||||
"sleep 0.05; printf 'session output\\n'".to_string(),
|
||||
],
|
||||
cwd: std::env::current_dir()?,
|
||||
cwd: cwd.clone(),
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: SandboxLaunchConfig::no_sandbox(cwd),
|
||||
})
|
||||
.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");
|
||||
@@ -145,27 +167,29 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
|
||||
|
||||
async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let process_id = "proc-stdin".to_string();
|
||||
let session = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
process_id: process_id.clone().into(),
|
||||
argv: vec![
|
||||
"/usr/bin/python3".to_string(),
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
"import sys; line = sys.stdin.readline(); sys.stdout.write(f'from-stdin:{line}'); sys.stdout.flush()".to_string(),
|
||||
"IFS= read -r line; printf 'from-stdin:%s\\n' \"$line\"".to_string(),
|
||||
],
|
||||
cwd: std::env::current_dir()?,
|
||||
cwd: cwd.clone(),
|
||||
env: Default::default(),
|
||||
tty: true,
|
||||
arg0: None,
|
||||
sandbox: SandboxLaunchConfig::no_sandbox(cwd),
|
||||
})
|
||||
.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?;
|
||||
|
||||
@@ -182,6 +206,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
|
||||
use_remote: bool,
|
||||
) -> Result<()> {
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let cwd = std::env::current_dir()?;
|
||||
let session = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
@@ -191,16 +216,17 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
|
||||
"-c".to_string(),
|
||||
"printf 'queued output\\n'".to_string(),
|
||||
],
|
||||
cwd: std::env::current_dir()?,
|
||||
cwd: cwd.clone(),
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: SandboxLaunchConfig::no_sandbox(cwd),
|
||||
})
|
||||
.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");
|
||||
@@ -209,6 +235,75 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_outside_workspace_sandbox(workspace_root: &std::path::Path) -> SandboxLaunchConfig {
|
||||
let mut policy = SandboxPolicy::new_workspace_write_policy();
|
||||
if let SandboxPolicy::WorkspaceWrite {
|
||||
exclude_tmpdir_env_var,
|
||||
exclude_slash_tmp,
|
||||
..
|
||||
} = &mut policy
|
||||
{
|
||||
*exclude_tmpdir_env_var = true;
|
||||
*exclude_slash_tmp = true;
|
||||
}
|
||||
SandboxLaunchConfig {
|
||||
sandbox_preference: SandboxablePreference::Require,
|
||||
policy: policy.clone(),
|
||||
file_system_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy(
|
||||
&policy,
|
||||
workspace_root,
|
||||
),
|
||||
network_policy: NetworkSandboxPolicy::from(&policy),
|
||||
sandbox_policy_cwd: workspace_root.to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
use_legacy_landlock: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn assert_exec_process_sandbox_denies_write_outside_workspace(
|
||||
use_remote: bool,
|
||||
) -> Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let workspace_root = temp_dir.path().join("workspace");
|
||||
std::fs::create_dir(&workspace_root)?;
|
||||
let blocked_path = temp_dir.path().join("blocked.txt");
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let session = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
process_id: ProcessId::from("proc-sandbox-denied"),
|
||||
argv: vec![
|
||||
"/bin/sh".to_string(),
|
||||
"-c".to_string(),
|
||||
"printf blocked > \"$1\"".to_string(),
|
||||
"write-outside-workspace".to_string(),
|
||||
blocked_path.to_string_lossy().into_owned(),
|
||||
],
|
||||
cwd: workspace_root.clone(),
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: write_outside_workspace_sandbox(&workspace_root),
|
||||
})
|
||||
.await?;
|
||||
|
||||
assert_eq!(session.sandbox_type, platform_sandbox_type());
|
||||
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_ne!(exit_code, Some(0));
|
||||
assert!(closed);
|
||||
assert!(
|
||||
!blocked_path.exists(),
|
||||
"sandboxed process unexpectedly wrote outside the workspace root"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_exec_process_reports_transport_disconnect() -> Result<()> {
|
||||
let mut context = create_process_context(/*use_remote*/ true).await?;
|
||||
@@ -225,6 +320,9 @@ async fn remote_exec_process_reports_transport_disconnect() -> Result<()> {
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: SandboxLaunchConfig::no_sandbox(
|
||||
std::env::current_dir().expect("read current dir"),
|
||||
),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -279,3 +377,8 @@ async fn exec_process_write_then_read(use_remote: bool) -> Result<()> {
|
||||
async fn exec_process_preserves_queued_events_before_subscribe(use_remote: bool) -> Result<()> {
|
||||
assert_exec_process_preserves_queued_events_before_subscribe(use_remote).await
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn remote_exec_process_sandbox_denies_write_outside_workspace() -> Result<()> {
|
||||
assert_exec_process_sandbox_denies_write_outside_workspace(/*use_remote*/ true).await
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@ mod common;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_exec_server::ExecParams;
|
||||
use codex_exec_server::ExecResponse;
|
||||
use codex_exec_server::InitializeParams;
|
||||
use codex_exec_server::ProcessId;
|
||||
use codex_sandboxing::SandboxLaunchConfig;
|
||||
use common::exec_server::exec_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
async fn initialize_server(
|
||||
server: &mut common::exec_server::ExecServerHarness,
|
||||
) -> anyhow::Result<()> {
|
||||
let initialize_id = server
|
||||
.send_request(
|
||||
"initialize",
|
||||
@@ -34,17 +36,26 @@ async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
.send_notification("initialized", serde_json::json!({}))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
initialize_server(&mut server).await?;
|
||||
|
||||
let process_start_id = server
|
||||
.send_request(
|
||||
"process/start",
|
||||
serde_json::json!({
|
||||
"processId": "proc-1",
|
||||
"argv": ["true"],
|
||||
"cwd": std::env::current_dir()?,
|
||||
"env": {},
|
||||
"tty": false,
|
||||
"arg0": null
|
||||
}),
|
||||
serde_json::to_value(ExecParams {
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: std::env::current_dir()?,
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
arg0: None,
|
||||
sandbox: SandboxLaunchConfig::no_sandbox(std::env::current_dir()?),
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let response = server
|
||||
@@ -63,7 +74,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: codex_sandboxing::SandboxType::None,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -25,3 +25,19 @@ pub fn run_main() -> ! {
|
||||
pub fn run_main() -> ! {
|
||||
panic!("codex-linux-sandbox is only supported on Linux");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn dispatch_if_requested() {
|
||||
let argv0 = std::env::args_os().next().unwrap_or_default();
|
||||
let exe_name = std::path::Path::new(&argv0)
|
||||
.file_name()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
if exe_name == codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0 {
|
||||
run_main();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn dispatch_if_requested() {}
|
||||
|
||||
@@ -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,9 +12,9 @@ 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::SandboxManager;
|
||||
pub use manager::SandboxTransformError;
|
||||
pub use manager::SandboxTransformRequest;
|
||||
pub use manager::SandboxType;
|
||||
pub use manager::SandboxablePreference;
|
||||
pub use manager::get_platform_sandbox;
|
||||
|
||||
@@ -16,11 +16,15 @@ use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
use codex_protocol::permissions::NetworkSandboxPolicy;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
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,
|
||||
@@ -39,13 +43,46 @@ impl SandboxType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum SandboxablePreference {
|
||||
Auto,
|
||||
Require,
|
||||
Forbid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SandboxLaunchConfig {
|
||||
pub sandbox_preference: SandboxablePreference,
|
||||
pub policy: SandboxPolicy,
|
||||
pub file_system_policy: FileSystemSandboxPolicy,
|
||||
pub network_policy: NetworkSandboxPolicy,
|
||||
pub sandbox_policy_cwd: PathBuf,
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
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 no_sandbox(sandbox_policy_cwd: PathBuf) -> Self {
|
||||
Self {
|
||||
sandbox_preference: SandboxablePreference::Forbid,
|
||||
policy: SandboxPolicy::DangerFullAccess,
|
||||
file_system_policy: FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Enabled,
|
||||
sandbox_policy_cwd,
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
use_legacy_landlock: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
|
||||
if cfg!(target_os = "macos") {
|
||||
Some(SandboxType::MacosSeatbelt)
|
||||
@@ -86,24 +123,20 @@ pub struct SandboxExecRequest {
|
||||
pub arg0: Option<String>,
|
||||
}
|
||||
|
||||
/// Bundled arguments for sandbox transformation.
|
||||
///
|
||||
/// This keeps call sites self-documenting when several fields are optional.
|
||||
pub struct SandboxTransformRequest<'a> {
|
||||
pub command: SandboxCommand,
|
||||
pub policy: &'a SandboxPolicy,
|
||||
pub file_system_policy: &'a FileSystemSandboxPolicy,
|
||||
pub network_policy: NetworkSandboxPolicy,
|
||||
pub sandbox: SandboxType,
|
||||
pub enforce_managed_network: bool,
|
||||
// TODO(viyatb): Evaluate switching this to Option<Arc<NetworkProxy>>
|
||||
// to make shared ownership explicit across runtime/sandbox plumbing.
|
||||
pub network: Option<&'a NetworkProxy>,
|
||||
pub sandbox_policy_cwd: &'a Path,
|
||||
pub codex_linux_sandbox_exe: Option<&'a Path>,
|
||||
pub use_legacy_landlock: bool,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
impl SandboxExecRequest {
|
||||
pub fn prepare_env_for_spawn(&mut self) {
|
||||
if !self.network_sandbox_policy.is_enabled() {
|
||||
self.env.insert(
|
||||
"CODEX_SANDBOX_NETWORK_DISABLED".to_string(),
|
||||
"1".to_string(),
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
if self.sandbox == SandboxType::MacosSeatbelt {
|
||||
self.env
|
||||
.insert("CODEX_SANDBOX".to_string(), "seatbelt".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -166,32 +199,30 @@ impl SandboxManager {
|
||||
|
||||
pub fn transform(
|
||||
&self,
|
||||
request: SandboxTransformRequest<'_>,
|
||||
mut command: SandboxCommand,
|
||||
launch: &SandboxLaunchConfig,
|
||||
network: Option<&NetworkProxy>,
|
||||
codex_linux_sandbox_exe: Option<&Path>,
|
||||
) -> Result<SandboxExecRequest, SandboxTransformError> {
|
||||
let SandboxTransformRequest {
|
||||
mut command,
|
||||
policy,
|
||||
file_system_policy,
|
||||
network_policy,
|
||||
sandbox,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
sandbox_policy_cwd,
|
||||
codex_linux_sandbox_exe,
|
||||
use_legacy_landlock,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
} = request;
|
||||
let additional_permissions = command.additional_permissions.take();
|
||||
let EffectiveSandboxPermissions {
|
||||
sandbox_policy: effective_policy,
|
||||
} = EffectiveSandboxPermissions::new(policy, additional_permissions.as_ref());
|
||||
} = EffectiveSandboxPermissions::new(&launch.policy, additional_permissions.as_ref());
|
||||
let effective_file_system_policy = effective_file_system_sandbox_policy(
|
||||
file_system_policy,
|
||||
&launch.file_system_policy,
|
||||
additional_permissions.as_ref(),
|
||||
);
|
||||
let effective_network_policy =
|
||||
effective_network_sandbox_policy(network_policy, additional_permissions.as_ref());
|
||||
let effective_network_policy = effective_network_sandbox_policy(
|
||||
launch.network_policy,
|
||||
additional_permissions.as_ref(),
|
||||
);
|
||||
let sandbox = self.select_initial(
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
launch.sandbox_preference,
|
||||
launch.windows_sandbox_level,
|
||||
launch.enforce_managed_network,
|
||||
);
|
||||
let mut argv = Vec::with_capacity(1 + command.args.len());
|
||||
argv.push(command.program);
|
||||
argv.extend(command.args.into_iter().map(OsString::from));
|
||||
@@ -204,8 +235,8 @@ impl SandboxManager {
|
||||
os_argv_to_strings(argv),
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
sandbox_policy_cwd,
|
||||
enforce_managed_network,
|
||||
launch.sandbox_policy_cwd.as_path(),
|
||||
launch.enforce_managed_network,
|
||||
network,
|
||||
);
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
@@ -218,15 +249,15 @@ impl SandboxManager {
|
||||
SandboxType::LinuxSeccomp => {
|
||||
let exe = codex_linux_sandbox_exe
|
||||
.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
|
||||
let allow_proxy_network = allow_network_for_proxy(enforce_managed_network);
|
||||
let allow_proxy_network = allow_network_for_proxy(launch.enforce_managed_network);
|
||||
let mut args = create_linux_sandbox_command_args_for_policies(
|
||||
os_argv_to_strings(argv),
|
||||
command.cwd.as_path(),
|
||||
&effective_policy,
|
||||
&effective_file_system_policy,
|
||||
effective_network_policy,
|
||||
sandbox_policy_cwd,
|
||||
use_legacy_landlock,
|
||||
launch.sandbox_policy_cwd.as_path(),
|
||||
launch.use_legacy_landlock,
|
||||
allow_proxy_network,
|
||||
);
|
||||
let mut full_command = Vec::with_capacity(1 + args.len());
|
||||
@@ -240,19 +271,21 @@ impl SandboxManager {
|
||||
SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
|
||||
};
|
||||
|
||||
Ok(SandboxExecRequest {
|
||||
let mut request = SandboxExecRequest {
|
||||
command: argv,
|
||||
cwd: command.cwd,
|
||||
env: command.env,
|
||||
network: network.cloned(),
|
||||
sandbox,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
windows_sandbox_level: launch.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: launch.windows_sandbox_private_desktop,
|
||||
sandbox_policy: effective_policy,
|
||||
file_system_sandbox_policy: effective_file_system_policy,
|
||||
network_sandbox_policy: effective_network_policy,
|
||||
arg0: arg0_override,
|
||||
})
|
||||
};
|
||||
request.prepare_env_for_spawn();
|
||||
Ok(request)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::SandboxCommand;
|
||||
use super::SandboxLaunchConfig;
|
||||
use super::SandboxManager;
|
||||
use super::SandboxTransformRequest;
|
||||
use super::SandboxType;
|
||||
use super::SandboxablePreference;
|
||||
use super::get_platform_sandbox;
|
||||
@@ -76,28 +76,31 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let exec_request = manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
.transform(
|
||||
SandboxCommand {
|
||||
program: "true".into(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
additional_permissions: None,
|
||||
},
|
||||
policy: &SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
&SandboxLaunchConfig {
|
||||
sandbox_preference: SandboxablePreference::Forbid,
|
||||
policy: SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
},
|
||||
file_system_policy: FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: cwd.as_path().to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
use_legacy_landlock: false,
|
||||
},
|
||||
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
/*network*/ None,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("transform");
|
||||
|
||||
assert_eq!(
|
||||
@@ -120,8 +123,8 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
)
|
||||
.expect("absolute temp dir");
|
||||
let exec_request = manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
.transform(
|
||||
SandboxCommand {
|
||||
program: "true".into(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd.clone(),
|
||||
@@ -136,20 +139,23 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
}),
|
||||
}),
|
||||
},
|
||||
policy: &SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
&SandboxLaunchConfig {
|
||||
sandbox_preference: SandboxablePreference::Forbid,
|
||||
policy: SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
},
|
||||
file_system_policy: FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: cwd.as_path().to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
use_legacy_landlock: false,
|
||||
},
|
||||
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
/*network*/ None,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("transform");
|
||||
|
||||
assert_eq!(
|
||||
@@ -176,8 +182,8 @@ fn transform_additional_permissions_preserves_denied_entries() {
|
||||
let allowed_path = workspace_root.join("allowed");
|
||||
let denied_path = workspace_root.join("denied");
|
||||
let exec_request = manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
.transform(
|
||||
SandboxCommand {
|
||||
program: "true".into(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd.clone(),
|
||||
@@ -190,34 +196,37 @@ fn transform_additional_permissions_preserves_denied_entries() {
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
policy: &SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
&SandboxLaunchConfig {
|
||||
sandbox_preference: SandboxablePreference::Forbid,
|
||||
policy: SandboxPolicy::ReadOnly {
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
network_access: false,
|
||||
},
|
||||
file_system_policy: FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: denied_path.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]),
|
||||
network_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox_policy_cwd: cwd.as_path().to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
use_legacy_landlock: false,
|
||||
},
|
||||
file_system_policy: &FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: denied_path.clone(),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]),
|
||||
network_policy: NetworkSandboxPolicy::Restricted,
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: None,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
/*network*/ None,
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("transform");
|
||||
|
||||
assert_eq!(
|
||||
@@ -252,26 +261,29 @@ fn transform_linux_seccomp_request(
|
||||
let manager = SandboxManager::new();
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
manager
|
||||
.transform(SandboxTransformRequest {
|
||||
command: SandboxCommand {
|
||||
.transform(
|
||||
SandboxCommand {
|
||||
program: "true".into(),
|
||||
args: Vec::new(),
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
additional_permissions: None,
|
||||
},
|
||||
policy: &SandboxPolicy::DangerFullAccess,
|
||||
file_system_policy: &FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Enabled,
|
||||
sandbox: SandboxType::LinuxSeccomp,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe),
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
})
|
||||
&SandboxLaunchConfig {
|
||||
sandbox_preference: SandboxablePreference::Require,
|
||||
policy: SandboxPolicy::DangerFullAccess,
|
||||
file_system_policy: FileSystemSandboxPolicy::unrestricted(),
|
||||
network_policy: NetworkSandboxPolicy::Enabled,
|
||||
sandbox_policy_cwd: cwd.as_path().to_path_buf(),
|
||||
additional_permissions: None,
|
||||
enforce_managed_network: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
use_legacy_landlock: false,
|
||||
},
|
||||
/*network*/ None,
|
||||
Some(codex_linux_sandbox_exe),
|
||||
)
|
||||
.expect("transform")
|
||||
}
|
||||
|
||||
|
||||
@@ -27,9 +27,8 @@ 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());
|
||||
assert!(
|
||||
stderr == expected
|
||||
stderr.contains(&path.display().to_string())
|
||||
|| stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted"),
|
||||
"unexpected stderr: {stderr}"
|
||||
);
|
||||
|
||||
@@ -38,6 +38,7 @@ use codex_core::path_utils;
|
||||
use codex_core::read_session_meta_line;
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
use codex_exec_server::EnvironmentManager;
|
||||
use codex_exec_server::ExecServerRuntimeConfig;
|
||||
use codex_login::AuthConfig;
|
||||
use codex_login::default_client::set_default_client_residency_requirement;
|
||||
use codex_login::enforce_login_restrictions;
|
||||
@@ -709,7 +710,9 @@ pub async fn run_main(
|
||||
}
|
||||
};
|
||||
|
||||
let environment_manager = Arc::new(EnvironmentManager::from_env());
|
||||
let environment_manager = Arc::new(EnvironmentManager::from_env_with_runtime(
|
||||
ExecServerRuntimeConfig::new(arg0_paths.codex_linux_sandbox_exe.clone()),
|
||||
));
|
||||
let cwd = cli.cwd.clone();
|
||||
let config_cwd =
|
||||
config_cwd_for_app_server_target(cwd.as_deref(), &app_server_target, &environment_manager)?;
|
||||
|
||||
Reference in New Issue
Block a user