mirror of
https://github.com/openai/codex.git
synced 2026-06-02 11:22:01 +00:00
Add exec-server sandbox launch intent
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -3621,6 +3621,7 @@ dependencies = [
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
|
||||
@@ -41,6 +41,7 @@ use crate::unified_exec::UnifiedExecError;
|
||||
use crate::unified_exec::UnifiedExecProcess;
|
||||
use crate::unified_exec::UnifiedExecProcessManager;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::ExecServerCapability;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_protocol::error::CodexErr;
|
||||
use codex_protocol::error::SandboxErr;
|
||||
@@ -110,6 +111,24 @@ fn unified_exec_options(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
enum DirectExecServerLaunch {
|
||||
Materialized,
|
||||
SandboxIntent,
|
||||
}
|
||||
|
||||
fn direct_exec_server_launch(
|
||||
environment_is_remote: bool,
|
||||
supports_sandbox_intent: bool,
|
||||
requires_core_network_realization: bool,
|
||||
) -> DirectExecServerLaunch {
|
||||
if environment_is_remote && supports_sandbox_intent && !requires_core_network_realization {
|
||||
DirectExecServerLaunch::SandboxIntent
|
||||
} else {
|
||||
DirectExecServerLaunch::Materialized
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -370,6 +389,41 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
let command =
|
||||
build_sandbox_command(&command, &req.cwd, &env, req.additional_permissions.clone())
|
||||
.map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?;
|
||||
let supports_sandbox_intent = if environment_is_remote {
|
||||
req.environment
|
||||
.supports_exec_server_capability(ExecServerCapability::ProcessStartSandboxIntent)
|
||||
.await
|
||||
.map_err(|err| ToolError::Rejected(err.to_string()))?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if direct_exec_server_launch(
|
||||
environment_is_remote,
|
||||
supports_sandbox_intent,
|
||||
managed_network.is_some() || attempt.enforce_managed_network,
|
||||
) == DirectExecServerLaunch::SandboxIntent
|
||||
{
|
||||
return self
|
||||
.manager
|
||||
.open_session_with_exec_server_sandbox_intent(
|
||||
req.process_id,
|
||||
command,
|
||||
attempt.exec_server_sandbox_intent(),
|
||||
req.exec_server_env_config.as_ref(),
|
||||
req.tty,
|
||||
req.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 options = unified_exec_options(attempt.network_denial_cancellation_token.clone());
|
||||
let mut exec_env = attempt
|
||||
.env_for(command, options, managed_network)
|
||||
@@ -427,6 +481,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_remote_exec_server_uses_sandbox_intent_only_when_network_stays_executor_local() {
|
||||
assert_eq!(
|
||||
direct_exec_server_launch(
|
||||
/*environment_is_remote*/ true, /*supports_sandbox_intent*/ true,
|
||||
/*requires_core_network_realization*/ false,
|
||||
),
|
||||
DirectExecServerLaunch::SandboxIntent,
|
||||
);
|
||||
assert_eq!(
|
||||
direct_exec_server_launch(
|
||||
/*environment_is_remote*/ true, /*supports_sandbox_intent*/ false,
|
||||
/*requires_core_network_realization*/ false,
|
||||
),
|
||||
DirectExecServerLaunch::Materialized,
|
||||
);
|
||||
assert_eq!(
|
||||
direct_exec_server_launch(
|
||||
/*environment_is_remote*/ true, /*supports_sandbox_intent*/ true,
|
||||
/*requires_core_network_realization*/ true,
|
||||
),
|
||||
DirectExecServerLaunch::Materialized,
|
||||
);
|
||||
assert_eq!(
|
||||
direct_exec_server_launch(
|
||||
/*environment_is_remote*/ false, /*supports_sandbox_intent*/ true,
|
||||
/*requires_core_network_realization*/ false,
|
||||
),
|
||||
DirectExecServerLaunch::Materialized,
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_exec_uses_the_trusted_sandbox_cwd() {
|
||||
let cwd_dir = tempdir().expect("create process temp dir");
|
||||
|
||||
@@ -421,6 +421,22 @@ pub(crate) struct SandboxAttempt<'a> {
|
||||
}
|
||||
|
||||
impl<'a> SandboxAttempt<'a> {
|
||||
pub fn exec_server_sandbox_intent(&self) -> codex_exec_server::ExecSandboxIntent {
|
||||
codex_exec_server::ExecSandboxIntent {
|
||||
sandbox: if self.sandbox == SandboxType::None {
|
||||
codex_exec_server::ExecSandboxMode::None
|
||||
} else {
|
||||
codex_exec_server::ExecSandboxMode::Platform
|
||||
},
|
||||
permissions: self.permissions.clone(),
|
||||
sandbox_policy_cwd: self.sandbox_cwd.clone(),
|
||||
use_legacy_landlock: self.use_legacy_landlock,
|
||||
windows_sandbox_level: self.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: self.windows_sandbox_private_desktop,
|
||||
additional_permissions: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn env_for(
|
||||
&self,
|
||||
command: SandboxCommand,
|
||||
|
||||
@@ -129,19 +129,20 @@ fn env_overlay_for_exec_server(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn exec_server_env_for_request(
|
||||
request: &ExecRequest,
|
||||
fn exec_server_env_for_command(
|
||||
env: &HashMap<String, String>,
|
||||
exec_server_env_config: Option<&ExecServerEnvConfig>,
|
||||
) -> (
|
||||
Option<codex_exec_server::ExecEnvPolicy>,
|
||||
HashMap<String, String>,
|
||||
) {
|
||||
if let Some(exec_server_env_config) = &request.exec_server_env_config {
|
||||
if let Some(exec_server_env_config) = exec_server_env_config {
|
||||
(
|
||||
Some(exec_server_env_config.policy.clone()),
|
||||
env_overlay_for_exec_server(&request.env, &exec_server_env_config.local_policy_env),
|
||||
env_overlay_for_exec_server(env, &exec_server_env_config.local_policy_env),
|
||||
)
|
||||
} else {
|
||||
(None, request.env.clone())
|
||||
(None, env.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +151,8 @@ fn exec_server_params_for_request(
|
||||
request: &ExecRequest,
|
||||
tty: bool,
|
||||
) -> codex_exec_server::ExecParams {
|
||||
let (env_policy, env) = exec_server_env_for_request(request);
|
||||
let (env_policy, env) =
|
||||
exec_server_env_for_command(&request.env, request.exec_server_env_config.as_ref());
|
||||
codex_exec_server::ExecParams {
|
||||
process_id: exec_server_process_id(process_id).into(),
|
||||
argv: request.command.clone(),
|
||||
@@ -160,9 +162,45 @@ fn exec_server_params_for_request(
|
||||
tty,
|
||||
pipe_stdin: false,
|
||||
arg0: request.arg0.clone(),
|
||||
launch: codex_exec_server::ExecLaunch::Materialized,
|
||||
}
|
||||
}
|
||||
|
||||
fn exec_server_params_for_sandbox_intent(
|
||||
process_id: i32,
|
||||
command: codex_sandboxing::SandboxCommand,
|
||||
mut intent: codex_exec_server::ExecSandboxIntent,
|
||||
exec_server_env_config: Option<&ExecServerEnvConfig>,
|
||||
tty: bool,
|
||||
) -> Result<codex_exec_server::ExecParams, UnifiedExecError> {
|
||||
let codex_sandboxing::SandboxCommand {
|
||||
program,
|
||||
args,
|
||||
cwd,
|
||||
env,
|
||||
additional_permissions,
|
||||
} = command;
|
||||
let program = program.into_string().map_err(|program| {
|
||||
UnifiedExecError::create_process(format!(
|
||||
"exec-server sandbox intent program is not UTF-8: {}",
|
||||
program.to_string_lossy()
|
||||
))
|
||||
})?;
|
||||
let (env_policy, env) = exec_server_env_for_command(&env, exec_server_env_config);
|
||||
intent.additional_permissions = additional_permissions;
|
||||
Ok(codex_exec_server::ExecParams {
|
||||
process_id: exec_server_process_id(process_id).into(),
|
||||
argv: std::iter::once(program).chain(args).collect(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env_policy,
|
||||
env,
|
||||
tty,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: codex_exec_server::ExecLaunch::SandboxIntent { intent },
|
||||
})
|
||||
}
|
||||
|
||||
/// Borrowed process state prepared for a `write_stdin` or poll operation.
|
||||
struct PreparedProcessHandles {
|
||||
process: Arc<UnifiedExecProcess>,
|
||||
@@ -989,6 +1027,40 @@ impl UnifiedExecProcessManager {
|
||||
UnifiedExecProcess::from_spawned(spawned, request.sandbox, spawn_lifecycle).await
|
||||
}
|
||||
|
||||
pub(crate) async fn open_session_with_exec_server_sandbox_intent(
|
||||
&self,
|
||||
process_id: i32,
|
||||
command: codex_sandboxing::SandboxCommand,
|
||||
intent: codex_exec_server::ExecSandboxIntent,
|
||||
exec_server_env_config: Option<&ExecServerEnvConfig>,
|
||||
tty: bool,
|
||||
environment: &codex_exec_server::Environment,
|
||||
) -> Result<UnifiedExecProcess, UnifiedExecError> {
|
||||
if !environment.is_remote() {
|
||||
return Err(UnifiedExecError::create_process(
|
||||
"exec-server sandbox intent requires a remote environment".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let started = environment
|
||||
.get_exec_backend()
|
||||
.start(exec_server_params_for_sandbox_intent(
|
||||
process_id,
|
||||
command,
|
||||
intent,
|
||||
exec_server_env_config,
|
||||
tty,
|
||||
)?)
|
||||
.await
|
||||
.map_err(|err| UnifiedExecError::create_process(err.to_string()))?;
|
||||
let sandbox = started.sandbox.ok_or_else(|| {
|
||||
UnifiedExecError::create_process(
|
||||
"exec-server sandbox intent response omitted materialized sandbox".to_string(),
|
||||
)
|
||||
})?;
|
||||
UnifiedExecProcess::from_exec_server_started(started, sandbox).await
|
||||
}
|
||||
|
||||
pub(super) async fn open_session_with_sandbox(
|
||||
&self,
|
||||
request: &ExecCommandRequest,
|
||||
|
||||
@@ -123,6 +123,55 @@ fn exec_server_params_use_env_policy_overlay_contract() {
|
||||
("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),
|
||||
])
|
||||
);
|
||||
assert_eq!(params.launch, codex_exec_server::ExecLaunch::Materialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_sandbox_intent_params_keep_pre_transform_command() {
|
||||
let cwd: codex_utils_absolute_path::AbsolutePathBuf = std::env::current_dir()
|
||||
.expect("current dir")
|
||||
.try_into()
|
||||
.expect("absolute path");
|
||||
let command = codex_sandboxing::SandboxCommand {
|
||||
program: "bash".into(),
|
||||
args: vec!["-lc".to_string(), "true".to_string()],
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::from([("PATH".to_string(), "/sandbox-path".to_string())]),
|
||||
additional_permissions: None,
|
||||
};
|
||||
let intent = codex_exec_server::ExecSandboxIntent {
|
||||
sandbox: codex_exec_server::ExecSandboxMode::None,
|
||||
permissions: codex_protocol::models::PermissionProfile::Disabled,
|
||||
sandbox_policy_cwd: cwd,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
additional_permissions: None,
|
||||
};
|
||||
|
||||
let params = exec_server_params_for_sandbox_intent(
|
||||
/*process_id*/ 123,
|
||||
command,
|
||||
intent.clone(),
|
||||
/*exec_server_env_config*/ None,
|
||||
/*tty*/ true,
|
||||
)
|
||||
.expect("sandbox intent params");
|
||||
|
||||
assert_eq!(
|
||||
params,
|
||||
codex_exec_server::ExecParams {
|
||||
process_id: "123".into(),
|
||||
argv: vec!["bash".to_string(), "-lc".to_string(), "true".to_string()],
|
||||
cwd: std::env::current_dir().expect("current dir"),
|
||||
env_policy: None,
|
||||
env: HashMap::from([("PATH".to_string(), "/sandbox-path".to_string())]),
|
||||
tty: true,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: codex_exec_server::ExecLaunch::SandboxIntent { intent },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -79,6 +79,7 @@ async fn remote_process(write_status: WriteStatus) -> UnifiedExecProcess {
|
||||
read_responses: Mutex::new(VecDeque::new()),
|
||||
wake_tx,
|
||||
}),
|
||||
sandbox: None,
|
||||
};
|
||||
|
||||
UnifiedExecProcess::from_exec_server_started(started, SandboxType::None)
|
||||
@@ -145,6 +146,7 @@ async fn remote_process_waits_for_early_exit_event() {
|
||||
}])),
|
||||
wake_tx: wake_tx.clone(),
|
||||
}),
|
||||
sandbox: None,
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -41,6 +41,7 @@ use crate::protocol::ExecExitedNotification;
|
||||
use crate::protocol::ExecOutputDeltaNotification;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecResponse;
|
||||
use crate::protocol::ExecServerCapability;
|
||||
use crate::protocol::FS_COPY_METHOD;
|
||||
use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
|
||||
use crate::protocol::FS_GET_METADATA_METHOD;
|
||||
@@ -175,6 +176,7 @@ struct Inner {
|
||||
http_body_streams_write_lock: Mutex<()>,
|
||||
http_body_stream_next_id: AtomicU64,
|
||||
session_id: std::sync::RwLock<Option<String>>,
|
||||
capabilities: std::sync::RwLock<Vec<ExecServerCapability>>,
|
||||
reader_task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
@@ -341,6 +343,14 @@ impl ExecServerClient {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
*session_id = Some(response.session_id.clone());
|
||||
}
|
||||
{
|
||||
let mut capabilities = self
|
||||
.inner
|
||||
.capabilities
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
*capabilities = response.capabilities.clone();
|
||||
}
|
||||
self.notify_initialized().await?;
|
||||
Ok(response)
|
||||
})
|
||||
@@ -459,6 +469,14 @@ impl ExecServerClient {
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn supports(&self, capability: ExecServerCapability) -> bool {
|
||||
self.inner
|
||||
.capabilities
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.contains(&capability)
|
||||
}
|
||||
|
||||
fn is_disconnected(&self) -> bool {
|
||||
self.inner.disconnected.get().is_some() || self.inner.client.is_disconnected()
|
||||
}
|
||||
@@ -510,6 +528,7 @@ impl ExecServerClient {
|
||||
http_body_streams_write_lock: Mutex::new(()),
|
||||
http_body_stream_next_id: AtomicU64::new(1),
|
||||
session_id: std::sync::RwLock::new(None),
|
||||
capabilities: std::sync::RwLock::new(Vec::new()),
|
||||
reader_task,
|
||||
}
|
||||
});
|
||||
@@ -1047,6 +1066,7 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(InitializeResponse {
|
||||
session_id: session_id.to_string(),
|
||||
capabilities: Vec::new(),
|
||||
})
|
||||
.expect("initialize response should serialize"),
|
||||
}),
|
||||
@@ -1280,6 +1300,7 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(InitializeResponse {
|
||||
session_id: "session-1".to_string(),
|
||||
capabilities: Vec::new(),
|
||||
})
|
||||
.expect("initialize response should serialize"),
|
||||
}),
|
||||
@@ -1423,6 +1444,7 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(InitializeResponse {
|
||||
session_id: "session-1".to_string(),
|
||||
capabilities: Vec::new(),
|
||||
})
|
||||
.expect("initialize response should serialize"),
|
||||
}),
|
||||
@@ -1560,6 +1582,7 @@ mod tests {
|
||||
id: request.id,
|
||||
result: serde_json::to_value(InitializeResponse {
|
||||
session_id: "session-1".to_string(),
|
||||
capabilities: Vec::new(),
|
||||
})
|
||||
.expect("initialize response should serialize"),
|
||||
}),
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::ExecServerCapability;
|
||||
use crate::ExecServerError;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ExecutorFileSystem;
|
||||
@@ -289,6 +290,7 @@ pub struct Environment {
|
||||
exec_backend: Arc<dyn ExecBackend>,
|
||||
filesystem: Arc<dyn ExecutorFileSystem>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
remote_client: Option<LazyRemoteExecServerClient>,
|
||||
local_runtime_paths: Option<ExecServerRuntimePaths>,
|
||||
}
|
||||
|
||||
@@ -301,6 +303,7 @@ impl Environment {
|
||||
exec_backend: Arc::new(LocalProcess::default()),
|
||||
filesystem: Arc::new(LocalFileSystem::unsandboxed()),
|
||||
http_client: Arc::new(ReqwestHttpClient),
|
||||
remote_client: None,
|
||||
local_runtime_paths: None,
|
||||
}
|
||||
}
|
||||
@@ -359,6 +362,7 @@ impl Environment {
|
||||
local_runtime_paths.clone(),
|
||||
)),
|
||||
http_client: Arc::new(ReqwestHttpClient),
|
||||
remote_client: None,
|
||||
local_runtime_paths: Some(local_runtime_paths),
|
||||
}
|
||||
}
|
||||
@@ -394,7 +398,8 @@ impl Environment {
|
||||
remote_transport: Some(remote_transport),
|
||||
exec_backend,
|
||||
filesystem,
|
||||
http_client: Arc::new(client),
|
||||
http_client: Arc::new(client.clone()),
|
||||
remote_client: Some(client),
|
||||
local_runtime_paths,
|
||||
}
|
||||
}
|
||||
@@ -420,6 +425,16 @@ impl Environment {
|
||||
Arc::clone(&self.http_client)
|
||||
}
|
||||
|
||||
pub async fn supports_exec_server_capability(
|
||||
&self,
|
||||
capability: ExecServerCapability,
|
||||
) -> Result<bool, ExecServerError> {
|
||||
let Some(client) = &self.remote_client else {
|
||||
return Ok(false);
|
||||
};
|
||||
Ok(client.get().await?.supports(capability))
|
||||
}
|
||||
|
||||
pub fn get_filesystem(&self) -> Arc<dyn ExecutorFileSystem> {
|
||||
Arc::clone(&self.filesystem)
|
||||
}
|
||||
@@ -806,6 +821,7 @@ mod tests {
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: crate::ExecLaunch::Materialized,
|
||||
})
|
||||
.await
|
||||
.expect("start process");
|
||||
|
||||
@@ -12,6 +12,7 @@ mod local_file_system;
|
||||
mod local_process;
|
||||
mod process;
|
||||
mod process_id;
|
||||
mod process_sandbox;
|
||||
mod protocol;
|
||||
mod relay;
|
||||
mod relay_proto;
|
||||
@@ -58,10 +59,14 @@ pub use process_id::ProcessId;
|
||||
pub use protocol::ExecClosedNotification;
|
||||
pub use protocol::ExecEnvPolicy;
|
||||
pub use protocol::ExecExitedNotification;
|
||||
pub use protocol::ExecLaunch;
|
||||
pub use protocol::ExecOutputDeltaNotification;
|
||||
pub use protocol::ExecOutputStream;
|
||||
pub use protocol::ExecParams;
|
||||
pub use protocol::ExecResponse;
|
||||
pub use protocol::ExecSandboxIntent;
|
||||
pub use protocol::ExecSandboxMode;
|
||||
pub use protocol::ExecServerCapability;
|
||||
pub use protocol::FsCopyParams;
|
||||
pub use protocol::FsCopyResponse;
|
||||
pub use protocol::FsCreateDirectoryParams;
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::ExecServerError;
|
||||
use crate::ProcessId;
|
||||
use crate::StartedExecProcess;
|
||||
use crate::process::ExecProcessEventLog;
|
||||
use crate::process_sandbox::ProcessSandboxTransformer;
|
||||
use crate::protocol::EXEC_CLOSED_METHOD;
|
||||
use crate::protocol::ExecClosedNotification;
|
||||
use crate::protocol::ExecEnvPolicy;
|
||||
@@ -84,6 +85,7 @@ enum ProcessEntry {
|
||||
struct Inner {
|
||||
notifications: std::sync::RwLock<Option<RpcNotificationSender>>,
|
||||
processes: Mutex<HashMap<ProcessId, ProcessEntry>>,
|
||||
sandbox: Option<ProcessSandboxTransformer>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -113,6 +115,20 @@ impl LocalProcess {
|
||||
inner: Arc::new(Inner {
|
||||
notifications: std::sync::RwLock::new(Some(notifications)),
|
||||
processes: Mutex::new(HashMap::new()),
|
||||
sandbox: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_runtime_paths(
|
||||
notifications: RpcNotificationSender,
|
||||
runtime_paths: crate::ExecServerRuntimePaths,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Inner {
|
||||
notifications: std::sync::RwLock::new(Some(notifications)),
|
||||
processes: Mutex::new(HashMap::new()),
|
||||
sandbox: Some(ProcessSandboxTransformer::new(runtime_paths)),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -146,6 +162,21 @@ impl LocalProcess {
|
||||
&self,
|
||||
params: ExecParams,
|
||||
) -> Result<(ExecResponse, watch::Sender<u64>, ExecProcessEventLog), JSONRPCErrorError> {
|
||||
let materialized = match (&self.inner.sandbox, ¶ms.launch) {
|
||||
(Some(sandbox), _) => sandbox.materialize(params)?,
|
||||
(None, crate::protocol::ExecLaunch::Materialized) => {
|
||||
crate::process_sandbox::MaterializedProcess {
|
||||
params,
|
||||
sandbox: None,
|
||||
}
|
||||
}
|
||||
(None, crate::protocol::ExecLaunch::SandboxIntent { .. }) => {
|
||||
return Err(crate::rpc::invalid_request(
|
||||
"sandbox intent requires exec-server runtime paths".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let params = materialized.params;
|
||||
let process_id = params.process_id.clone();
|
||||
let (program, args) = params
|
||||
.argv
|
||||
@@ -259,7 +290,14 @@ impl LocalProcess {
|
||||
output_notify,
|
||||
));
|
||||
|
||||
Ok((ExecResponse { process_id }, wake_tx, events))
|
||||
Ok((
|
||||
ExecResponse {
|
||||
process_id,
|
||||
sandbox: materialized.sandbox,
|
||||
},
|
||||
wake_tx,
|
||||
events,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) async fn exec(&self, params: ExecParams) -> Result<ExecResponse, JSONRPCErrorError> {
|
||||
@@ -406,7 +444,7 @@ impl LocalProcess {
|
||||
}
|
||||
}
|
||||
|
||||
fn child_env(params: &ExecParams) -> HashMap<String, String> {
|
||||
pub(crate) fn child_env(params: &ExecParams) -> HashMap<String, String> {
|
||||
let Some(env_policy) = ¶ms.env_policy else {
|
||||
return params.env.clone();
|
||||
};
|
||||
@@ -444,6 +482,7 @@ impl ExecBackend for LocalProcess {
|
||||
.await
|
||||
.map_err(map_handler_error)?;
|
||||
Ok(StartedExecProcess {
|
||||
sandbox: response.sandbox,
|
||||
process: Arc::new(LocalExecProcess {
|
||||
process_id: response.process_id,
|
||||
backend: self.clone(),
|
||||
@@ -706,6 +745,7 @@ fn notification_sender(inner: &Inner) -> Option<RpcNotificationSender> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::ExecLaunch;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
use codex_utils_pty::ProcessDriver;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -722,6 +762,7 @@ mod tests {
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,11 @@ use crate::protocol::ExecParams;
|
||||
use crate::protocol::ProcessOutputChunk;
|
||||
use crate::protocol::ReadResponse;
|
||||
use crate::protocol::WriteResponse;
|
||||
use codex_sandboxing::SandboxType;
|
||||
|
||||
pub struct StartedExecProcess {
|
||||
pub process: Arc<dyn ExecProcess>,
|
||||
pub sandbox: Option<SandboxType>,
|
||||
}
|
||||
|
||||
/// Pushed process events for consumers that want to follow process output as it
|
||||
|
||||
181
codex-rs/exec-server/src/process_sandbox.rs
Normal file
181
codex-rs/exec-server/src/process_sandbox.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::protocol::ExecLaunch;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecSandboxMode;
|
||||
use crate::rpc::invalid_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ProcessSandboxTransformer {
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
}
|
||||
|
||||
pub(crate) struct MaterializedProcess {
|
||||
pub(crate) params: ExecParams,
|
||||
pub(crate) sandbox: Option<SandboxType>,
|
||||
}
|
||||
|
||||
impl ProcessSandboxTransformer {
|
||||
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self { runtime_paths }
|
||||
}
|
||||
|
||||
pub(crate) fn materialize(
|
||||
&self,
|
||||
mut params: ExecParams,
|
||||
) -> Result<MaterializedProcess, JSONRPCErrorError> {
|
||||
let ExecLaunch::SandboxIntent { intent } = params.launch.clone() else {
|
||||
return Ok(MaterializedProcess {
|
||||
params,
|
||||
sandbox: None,
|
||||
});
|
||||
};
|
||||
let (program, args) = params
|
||||
.argv
|
||||
.split_first()
|
||||
.ok_or_else(|| invalid_request("sandbox intent argv must not be empty".to_string()))?;
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(params.cwd.as_path())
|
||||
.map_err(|err| invalid_request(format!("sandbox intent cwd is invalid: {err}")))?;
|
||||
let sandbox = executor_sandbox_type(intent.sandbox, intent.windows_sandbox_level)?;
|
||||
let command = SandboxCommand {
|
||||
program: program.clone().into(),
|
||||
args: args.to_vec(),
|
||||
cwd,
|
||||
env: crate::local_process::child_env(¶ms),
|
||||
additional_permissions: intent.additional_permissions,
|
||||
};
|
||||
let request = SandboxManager::new()
|
||||
.transform(SandboxTransformRequest {
|
||||
command,
|
||||
permissions: &intent.permissions,
|
||||
sandbox,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
sandbox_policy_cwd: intent.sandbox_policy_cwd.as_path(),
|
||||
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
|
||||
use_legacy_landlock: intent.use_legacy_landlock,
|
||||
windows_sandbox_level: intent.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: intent.windows_sandbox_private_desktop,
|
||||
})
|
||||
.map_err(|err| invalid_request(format!("failed to prepare process sandbox: {err}")))?;
|
||||
params.argv = request.command;
|
||||
params.cwd = request.cwd.to_path_buf();
|
||||
params.env_policy = None;
|
||||
params.env = request.env;
|
||||
params.arg0 = request.arg0;
|
||||
params.launch = ExecLaunch::Materialized;
|
||||
Ok(MaterializedProcess {
|
||||
params,
|
||||
sandbox: Some(sandbox),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn executor_sandbox_type(
|
||||
sandbox: ExecSandboxMode,
|
||||
windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
||||
) -> Result<SandboxType, JSONRPCErrorError> {
|
||||
match sandbox {
|
||||
ExecSandboxMode::None => Ok(SandboxType::None),
|
||||
ExecSandboxMode::Platform => {
|
||||
let sandbox = codex_sandboxing::get_platform_sandbox(
|
||||
windows_sandbox_level
|
||||
!= codex_protocol::config_types::WindowsSandboxLevel::Disabled,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
invalid_request(
|
||||
"process sandbox intent requires a platform sandbox on the executor"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
if sandbox == SandboxType::WindowsRestrictedToken {
|
||||
return Err(invalid_request(
|
||||
"process sandbox intent does not support WindowsRestrictedToken yet"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
Ok(sandbox)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::ProcessSandboxTransformer;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ProcessId;
|
||||
use crate::protocol::ExecLaunch;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecSandboxIntent;
|
||||
use crate::protocol::ExecSandboxMode;
|
||||
|
||||
#[test]
|
||||
fn platform_sandbox_intent_uses_executor_platform() {
|
||||
let sandbox =
|
||||
super::executor_sandbox_type(ExecSandboxMode::Platform, WindowsSandboxLevel::Disabled);
|
||||
|
||||
match codex_sandboxing::get_platform_sandbox(/*windows_sandbox_enabled*/ false) {
|
||||
Some(expected) => assert_eq!(sandbox.expect("platform sandbox"), expected),
|
||||
None => assert!(sandbox.is_err()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materializes_sandbox_intent_with_executor_runtime_paths() {
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(
|
||||
std::env::current_dir().expect("current dir").as_path(),
|
||||
)
|
||||
.expect("absolute cwd");
|
||||
let params = ExecParams {
|
||||
process_id: ProcessId::from("sandbox-intent"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: cwd.to_path_buf(),
|
||||
env_policy: None,
|
||||
env: HashMap::from([("PATH".to_string(), "/usr/bin".to_string())]),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::SandboxIntent {
|
||||
intent: ExecSandboxIntent {
|
||||
sandbox: ExecSandboxMode::None,
|
||||
permissions: PermissionProfile::Disabled,
|
||||
sandbox_policy_cwd: cwd,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
additional_permissions: None,
|
||||
},
|
||||
},
|
||||
};
|
||||
let transformer = ProcessSandboxTransformer::new(
|
||||
ExecServerRuntimePaths::new(
|
||||
std::env::current_exe().expect("current exe"),
|
||||
/*codex_linux_sandbox_exe*/ None,
|
||||
)
|
||||
.expect("runtime paths"),
|
||||
);
|
||||
|
||||
let materialized = transformer.materialize(params).expect("materialize");
|
||||
|
||||
assert_eq!(materialized.params.launch, ExecLaunch::Materialized);
|
||||
assert_eq!(materialized.params.argv, vec!["true".to_string()]);
|
||||
assert_eq!(materialized.params.env_policy, None);
|
||||
assert_eq!(
|
||||
materialized.sandbox,
|
||||
Some(codex_sandboxing::SandboxType::None)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ use std::path::PathBuf;
|
||||
use crate::FileSystemSandboxContext;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::AdditionalPermissionProfile;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
@@ -59,6 +63,50 @@ pub struct InitializeParams {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct InitializeResponse {
|
||||
pub session_id: String,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<ExecServerCapability>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ExecServerCapability {
|
||||
ProcessStartSandboxIntent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ExecSandboxMode {
|
||||
None,
|
||||
Platform,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecSandboxIntent {
|
||||
pub sandbox: ExecSandboxMode,
|
||||
pub permissions: PermissionProfile,
|
||||
pub sandbox_policy_cwd: AbsolutePathBuf,
|
||||
pub use_legacy_landlock: bool,
|
||||
pub windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub additional_permissions: Option<AdditionalPermissionProfile>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum ExecLaunch {
|
||||
#[default]
|
||||
Materialized,
|
||||
SandboxIntent {
|
||||
intent: ExecSandboxIntent,
|
||||
},
|
||||
}
|
||||
|
||||
impl ExecLaunch {
|
||||
fn is_materialized(&self) -> bool {
|
||||
matches!(self, Self::Materialized)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -77,6 +125,8 @@ pub struct ExecParams {
|
||||
#[serde(default)]
|
||||
pub pipe_stdin: bool,
|
||||
pub arg0: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "ExecLaunch::is_materialized")]
|
||||
pub launch: ExecLaunch,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -93,6 +143,8 @@ pub struct ExecEnvPolicy {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExecResponse {
|
||||
pub process_id: ProcessId,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sandbox: Option<SandboxType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -398,9 +450,54 @@ mod base64_bytes {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ExecLaunch;
|
||||
use super::ExecParams;
|
||||
use super::ExecResponse;
|
||||
use super::HttpRequestParams;
|
||||
use super::InitializeResponse;
|
||||
use crate::ProcessId;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn initialize_response_defaults_missing_capabilities() {
|
||||
let response: InitializeResponse =
|
||||
serde_json::from_value(serde_json::json!({ "sessionId": "session-1" }))
|
||||
.expect("initialize response should deserialize");
|
||||
|
||||
assert_eq!(response.capabilities, Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialized_exec_params_omit_launch() {
|
||||
let params = ExecParams {
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: std::env::current_dir().expect("cwd"),
|
||||
env_policy: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(params).expect("exec params should serialize");
|
||||
|
||||
assert_eq!(value.get("launch"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_response_defaults_missing_sandbox() {
|
||||
let response: ExecResponse = serde_json::from_value(serde_json::json!({
|
||||
"processId": "proc-1",
|
||||
}))
|
||||
.expect("legacy exec response should deserialize");
|
||||
|
||||
assert_eq!(response.sandbox, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_request_timeout_treats_omitted_and_null_as_no_timeout() {
|
||||
let omitted: HttpRequestParams = serde_json::from_value(serde_json::json!({
|
||||
|
||||
@@ -37,12 +37,16 @@ impl ExecBackend for RemoteProcess {
|
||||
let process_id = params.process_id.clone();
|
||||
let client = self.client.get().await?;
|
||||
let session = client.register_session(&process_id).await?;
|
||||
if let Err(err) = client.exec(params).await {
|
||||
session.unregister().await;
|
||||
return Err(err);
|
||||
}
|
||||
let response = match client.exec(params).await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
session.unregister().await;
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(StartedExecProcess {
|
||||
sandbox: response.sandbox,
|
||||
process: Arc::new(RemoteExecProcess { session }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -123,7 +123,10 @@ impl ExecServerHandler {
|
||||
.session
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(session);
|
||||
Ok(InitializeResponse { session_id })
|
||||
Ok(InitializeResponse {
|
||||
session_id,
|
||||
capabilities: process_start_capabilities(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn initialized(&self) -> Result<(), String> {
|
||||
@@ -343,5 +346,13 @@ impl ExecServerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
fn process_start_capabilities() -> Vec<crate::protocol::ExecServerCapability> {
|
||||
if cfg!(windows) {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![crate::protocol::ExecServerCapability::ProcessStartSandboxIntent]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -9,6 +9,7 @@ use uuid::Uuid;
|
||||
use super::ExecServerHandler;
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::ProcessId;
|
||||
use crate::protocol::ExecLaunch;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::InitializeParams;
|
||||
use crate::protocol::ReadParams;
|
||||
@@ -32,6 +33,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams {
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +79,7 @@ fn test_runtime_paths() -> ExecServerRuntimePaths {
|
||||
|
||||
async fn initialized_handler() -> Arc<ExecServerHandler> {
|
||||
let (outgoing_tx, _outgoing_rx) = mpsc::channel(16);
|
||||
let registry = SessionRegistry::new();
|
||||
let registry = SessionRegistry::new(test_runtime_paths());
|
||||
let handler = Arc::new(ExecServerHandler::new(
|
||||
registry,
|
||||
RpcNotificationSender::new(outgoing_tx),
|
||||
@@ -155,7 +157,7 @@ async fn terminate_reports_false_after_process_exit() {
|
||||
#[tokio::test]
|
||||
async fn long_poll_read_fails_after_session_resume() {
|
||||
let (first_tx, _first_rx) = mpsc::channel(16);
|
||||
let registry = SessionRegistry::new();
|
||||
let registry = SessionRegistry::new(test_runtime_paths());
|
||||
let first_handler = Arc::new(ExecServerHandler::new(
|
||||
Arc::clone(®istry),
|
||||
RpcNotificationSender::new(first_tx),
|
||||
@@ -228,7 +230,7 @@ async fn long_poll_read_fails_after_session_resume() {
|
||||
#[tokio::test]
|
||||
async fn active_session_resume_is_rejected() {
|
||||
let (first_tx, _first_rx) = mpsc::channel(16);
|
||||
let registry = SessionRegistry::new();
|
||||
let registry = SessionRegistry::new(test_runtime_paths());
|
||||
let first_handler = Arc::new(ExecServerHandler::new(
|
||||
Arc::clone(®istry),
|
||||
RpcNotificationSender::new(first_tx),
|
||||
@@ -272,7 +274,7 @@ async fn active_session_resume_is_rejected() {
|
||||
async fn output_and_exit_are_retained_after_notification_receiver_closes() {
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::channel(16);
|
||||
let handler = Arc::new(ExecServerHandler::new(
|
||||
SessionRegistry::new(),
|
||||
SessionRegistry::new(test_runtime_paths()),
|
||||
RpcNotificationSender::new(outgoing_tx),
|
||||
test_runtime_paths(),
|
||||
));
|
||||
|
||||
@@ -17,9 +17,12 @@ pub(crate) struct ProcessHandler {
|
||||
}
|
||||
|
||||
impl ProcessHandler {
|
||||
pub(crate) fn new(notifications: RpcNotificationSender) -> Self {
|
||||
pub(crate) fn new(
|
||||
notifications: RpcNotificationSender,
|
||||
runtime_paths: crate::ExecServerRuntimePaths,
|
||||
) -> Self {
|
||||
Self {
|
||||
process: LocalProcess::new(notifications),
|
||||
process: LocalProcess::with_runtime_paths(notifications, runtime_paths),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ pub(crate) struct ConnectionProcessor {
|
||||
impl ConnectionProcessor {
|
||||
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self {
|
||||
Self {
|
||||
session_registry: SessionRegistry::new(),
|
||||
session_registry: SessionRegistry::new(runtime_paths.clone()),
|
||||
runtime_paths,
|
||||
}
|
||||
}
|
||||
@@ -213,6 +213,7 @@ mod tests {
|
||||
use crate::protocol::EXEC_METHOD;
|
||||
use crate::protocol::EXEC_READ_METHOD;
|
||||
use crate::protocol::EXEC_TERMINATE_METHOD;
|
||||
use crate::protocol::ExecLaunch;
|
||||
use crate::protocol::ExecParams;
|
||||
use crate::protocol::ExecResponse;
|
||||
use crate::protocol::INITIALIZE_METHOD;
|
||||
@@ -226,7 +227,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn transport_disconnect_detaches_session_during_in_flight_read() {
|
||||
let registry = SessionRegistry::new();
|
||||
let registry = SessionRegistry::new(test_runtime_paths());
|
||||
let (mut first_writer, mut first_lines, first_task) =
|
||||
spawn_test_connection(Arc::clone(®istry), "first");
|
||||
|
||||
@@ -402,6 +403,7 @@ mod tests {
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::ExecServerRuntimePaths;
|
||||
use crate::rpc::RpcNotificationSender;
|
||||
use crate::rpc::invalid_request;
|
||||
use crate::server::process_handler::ProcessHandler;
|
||||
@@ -18,6 +19,7 @@ const DETACHED_SESSION_TTL: Duration = Duration::from_secs(10);
|
||||
|
||||
pub(crate) struct SessionRegistry {
|
||||
sessions: Mutex<HashMap<String, Arc<SessionEntry>>>,
|
||||
runtime_paths: ExecServerRuntimePaths,
|
||||
}
|
||||
|
||||
struct SessionEntry {
|
||||
@@ -49,9 +51,10 @@ pub(crate) struct SessionHandle {
|
||||
}
|
||||
|
||||
impl SessionRegistry {
|
||||
pub(crate) fn new() -> Arc<Self> {
|
||||
pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
sessions: Mutex::new(HashMap::new()),
|
||||
runtime_paths,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,7 +97,7 @@ impl SessionRegistry {
|
||||
let session_id = Uuid::new_v4().to_string();
|
||||
let entry = Arc::new(SessionEntry::new(
|
||||
session_id.clone(),
|
||||
ProcessHandler::new(notifications),
|
||||
ProcessHandler::new(notifications, self.runtime_paths.clone()),
|
||||
connection_id,
|
||||
));
|
||||
sessions.insert(session_id, Arc::clone(&entry));
|
||||
@@ -136,14 +139,6 @@ impl SessionRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SessionRegistry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sessions: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionEntry {
|
||||
fn new(session_id: String, process: ProcessHandler, connection_id: ConnectionId) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -8,6 +8,7 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_exec_server::ExecBackend;
|
||||
use codex_exec_server::ExecLaunch;
|
||||
use codex_exec_server::ExecOutputStream;
|
||||
use codex_exec_server::ExecParams;
|
||||
use codex_exec_server::ExecProcess;
|
||||
@@ -79,6 +80,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), "proc-1");
|
||||
@@ -220,11 +222,12 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.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");
|
||||
@@ -251,11 +254,12 @@ async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()> {
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
|
||||
let StartedExecProcess { process } = session;
|
||||
let StartedExecProcess { process, .. } = session;
|
||||
let actual = collect_process_event_snapshots(process).await?;
|
||||
assert_eq!(
|
||||
actual,
|
||||
@@ -298,11 +302,12 @@ async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Res
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.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 read_result = collect_process_output_from_reads(Arc::clone(&process), wake_rx).await?;
|
||||
assert_eq!(
|
||||
@@ -346,11 +351,12 @@ async fn assert_exec_process_retains_output_after_exit_until_streams_close(
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
|
||||
let StartedExecProcess { process } = session;
|
||||
let StartedExecProcess { process, .. } = session;
|
||||
|
||||
let exit_response = timeout(
|
||||
Duration::from_secs(2),
|
||||
@@ -419,13 +425,14 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
|
||||
tty: true,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.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?;
|
||||
|
||||
@@ -456,6 +463,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
|
||||
tty: false,
|
||||
pipe_stdin: true,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -463,7 +471,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
let write_response = session.process.write(b"hello\n".to_vec()).await?;
|
||||
assert_eq!(write_response.status, WriteStatus::Accepted);
|
||||
let StartedExecProcess { process } = session;
|
||||
let StartedExecProcess { process, .. } = session;
|
||||
let wake_rx = process.subscribe_wake();
|
||||
let actual = collect_process_output_from_reads(process, wake_rx).await?;
|
||||
|
||||
@@ -489,13 +497,14 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool)
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
|
||||
let write_response = session.process.write(b"ignored\n".to_vec()).await?;
|
||||
assert_eq!(write_response.status, WriteStatus::StdinClosed);
|
||||
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?;
|
||||
|
||||
@@ -524,12 +533,13 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.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");
|
||||
@@ -558,6 +568,7 @@ async fn remote_exec_process_reports_transport_disconnect() -> Result<()> {
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -1013,6 +1013,7 @@ impl JsonRpcPeer {
|
||||
request.id,
|
||||
InitializeResponse {
|
||||
session_id: "session-1".to_string(),
|
||||
capabilities: Vec::new(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -4,7 +4,12 @@ mod common;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_exec_server::ExecLaunch;
|
||||
use codex_exec_server::ExecParams;
|
||||
use codex_exec_server::ExecResponse;
|
||||
use codex_exec_server::ExecSandboxIntent;
|
||||
use codex_exec_server::ExecSandboxMode;
|
||||
use codex_exec_server::ExecServerCapability;
|
||||
use codex_exec_server::InitializeParams;
|
||||
use codex_exec_server::InitializeResponse;
|
||||
use codex_exec_server::ProcessId;
|
||||
@@ -12,6 +17,9 @@ use codex_exec_server::ReadResponse;
|
||||
use codex_exec_server::TerminateResponse;
|
||||
use codex_exec_server::WriteResponse;
|
||||
use codex_exec_server::WriteStatus;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use common::exec_server::exec_server;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -27,7 +35,7 @@ async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let _ = server
|
||||
let initialize_response = server
|
||||
.wait_for_event(|event| {
|
||||
matches!(
|
||||
event,
|
||||
@@ -35,6 +43,16 @@ async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> {
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(JSONRPCResponse { result, .. }) = initialize_response else {
|
||||
panic!("expected initialize response");
|
||||
};
|
||||
let initialize_response: InitializeResponse = serde_json::from_value(result)?;
|
||||
let expected_capabilities = if cfg!(windows) {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![ExecServerCapability::ProcessStartSandboxIntent]
|
||||
};
|
||||
assert_eq!(initialize_response.capabilities, expected_capabilities);
|
||||
|
||||
server
|
||||
.send_notification("initialized", serde_json::json!({}))
|
||||
@@ -70,7 +88,161 @@ 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: None,
|
||||
}
|
||||
);
|
||||
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_server_materializes_process_sandbox_intent() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
let initialize_id = server
|
||||
.send_request(
|
||||
"initialize",
|
||||
serde_json::to_value(InitializeParams {
|
||||
client_name: "exec-server-test".to_string(),
|
||||
resume_session_id: None,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let _ = server
|
||||
.wait_for_event(|event| {
|
||||
matches!(
|
||||
event,
|
||||
JSONRPCMessage::Response(JSONRPCResponse { id, .. }) if id == &initialize_id
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
server
|
||||
.send_notification("initialized", serde_json::json!({}))
|
||||
.await?;
|
||||
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(std::env::current_dir()?.as_path())?;
|
||||
let process_start_id = server
|
||||
.send_request(
|
||||
"process/start",
|
||||
serde_json::to_value(ExecParams {
|
||||
process_id: ProcessId::from("proc-sandbox-intent"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: cwd.to_path_buf(),
|
||||
env_policy: None,
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::SandboxIntent {
|
||||
intent: ExecSandboxIntent {
|
||||
sandbox: ExecSandboxMode::None,
|
||||
permissions: PermissionProfile::Disabled,
|
||||
sandbox_policy_cwd: cwd.clone(),
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
additional_permissions: None,
|
||||
},
|
||||
},
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let response = server
|
||||
.wait_for_event(|event| {
|
||||
matches!(
|
||||
event,
|
||||
JSONRPCMessage::Response(JSONRPCResponse { id, .. }) if id == &process_start_id
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(JSONRPCResponse { result, .. }) = response else {
|
||||
panic!("expected process/start response");
|
||||
};
|
||||
let process_start_response: ExecResponse = serde_json::from_value(result)?;
|
||||
|
||||
assert_eq!(
|
||||
process_start_response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-sandbox-intent"),
|
||||
sandbox: Some(codex_sandboxing::SandboxType::None),
|
||||
}
|
||||
);
|
||||
|
||||
server.shutdown().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_server_materializes_platform_process_sandbox_intent() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
let initialize_id = server
|
||||
.send_request(
|
||||
"initialize",
|
||||
serde_json::to_value(InitializeParams {
|
||||
client_name: "exec-server-test".to_string(),
|
||||
resume_session_id: None,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let _ = server
|
||||
.wait_for_event(|event| {
|
||||
matches!(
|
||||
event,
|
||||
JSONRPCMessage::Response(JSONRPCResponse { id, .. }) if id == &initialize_id
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
server
|
||||
.send_notification("initialized", serde_json::json!({}))
|
||||
.await?;
|
||||
|
||||
let cwd = AbsolutePathBuf::from_absolute_path(std::env::current_dir()?.as_path())?;
|
||||
let process_start_id = server
|
||||
.send_request(
|
||||
"process/start",
|
||||
serde_json::to_value(ExecParams {
|
||||
process_id: ProcessId::from("proc-platform-sandbox-intent"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd: cwd.to_path_buf(),
|
||||
env_policy: None,
|
||||
env: Default::default(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::SandboxIntent {
|
||||
intent: ExecSandboxIntent {
|
||||
sandbox: ExecSandboxMode::Platform,
|
||||
permissions: PermissionProfile::Disabled,
|
||||
sandbox_policy_cwd: cwd,
|
||||
use_legacy_landlock: false,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
additional_permissions: None,
|
||||
},
|
||||
},
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let response = server
|
||||
.wait_for_event(|event| {
|
||||
matches!(
|
||||
event,
|
||||
JSONRPCMessage::Response(JSONRPCResponse { id, .. }) if id == &process_start_id
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(JSONRPCResponse { result, .. }) = response else {
|
||||
panic!("expected process/start response");
|
||||
};
|
||||
let process_start_response: ExecResponse = serde_json::from_value(result)?;
|
||||
|
||||
assert_eq!(
|
||||
process_start_response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-platform-sandbox-intent"),
|
||||
sandbox: codex_sandboxing::get_platform_sandbox(/*windows_sandbox_enabled*/ false),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -135,7 +307,8 @@ async fn exec_server_defaults_omitted_pipe_stdin_to_closed_stdin() -> anyhow::Re
|
||||
assert_eq!(
|
||||
process_start_response,
|
||||
ExecResponse {
|
||||
process_id: ProcessId::from("proc-default-stdin")
|
||||
process_id: ProcessId::from("proc-default-stdin"),
|
||||
sandbox: None,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ use anyhow::anyhow;
|
||||
use codex_config::types::McpServerEnvVar;
|
||||
use codex_exec_server::ExecBackend;
|
||||
use codex_exec_server::ExecEnvPolicy;
|
||||
use codex_exec_server::ExecLaunch;
|
||||
use codex_exec_server::ExecParams;
|
||||
use codex_exec_server::ExecProcess;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
@@ -501,6 +502,7 @@ impl ExecutorStdioServerLauncher {
|
||||
tty: false,
|
||||
pipe_stdin: true,
|
||||
arg0: None,
|
||||
launch: ExecLaunch::Materialized,
|
||||
})
|
||||
.await
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
@@ -18,6 +18,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 }
|
||||
regex-lite = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
|
||||
@@ -15,11 +15,14 @@ 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;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum SandboxType {
|
||||
None,
|
||||
MacosSeatbelt,
|
||||
|
||||
Reference in New Issue
Block a user