[codex] expose sandbox ingress to host

This commit is contained in:
Eugenio Panero
2026-06-25 20:05:12 -07:00
parent 25f50de6ed
commit 6c6d291f77
47 changed files with 1207 additions and 46 deletions
+1
View File
@@ -1374,6 +1374,7 @@ mod tests {
env: HashMap::new(),
tty: false,
pipe_stdin: false,
ingress: None,
arg0: None,
sandbox: None,
enforce_managed_network: false,
+2
View File
@@ -1172,6 +1172,7 @@ mod tests {
env: Default::default(),
tty: false,
pipe_stdin: false,
ingress: None,
arg0: None,
sandbox: None,
enforce_managed_network: false,
@@ -1212,6 +1213,7 @@ mod tests {
env: Default::default(),
tty: false,
pipe_stdin: false,
ingress: None,
arg0: None,
sandbox: Some(sandbox),
enforce_managed_network: false,
+1
View File
@@ -140,6 +140,7 @@ impl FileSystemSandboxRunner {
enforce_managed_network: false,
environment_id: None,
network: None,
ingress: None,
sandbox_policy_cwd: &cwd.uri,
codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
+52 -5
View File
@@ -14,6 +14,10 @@ use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::exec_output::StreamOutput;
use codex_protocol::shell_environment;
use codex_sandboxing::SandboxType;
#[cfg(unix)]
use codex_sandboxing::ingress::IngressListener;
#[cfg(unix)]
use codex_sandboxing::ingress::IngressListenerError;
use codex_sandboxing::is_likely_sandbox_denied;
use codex_utils_pty::ExecCommandSession;
use codex_utils_pty::ProcessSignal as PtyProcessSignal;
@@ -234,8 +238,30 @@ impl LocalProcess {
params: ExecParams,
) -> Result<(ExecResponse, watch::Sender<u64>, ExecProcessEventLog), JSONRPCErrorError> {
let process_id = params.process_id.clone();
let prepared =
prepare_exec_request(&params, child_env(&params), self.runtime_paths.as_ref())?;
let mut env = child_env(&params);
#[cfg(unix)]
let ingress = match IngressListener::prepare(params.ingress) {
Ok(ingress) => ingress,
Err(error) => return Err(ingress_error(error)),
};
#[cfg(unix)]
let inherited_fds = ingress
.as_ref()
.map(|ingress| vec![ingress.inherited_fd()])
.unwrap_or_default();
#[cfg(unix)]
if let Some(ingress) = ingress.as_ref() {
ingress.add_to_child_env(&mut env);
}
#[cfg(not(unix))]
if params.ingress.is_some() {
return Err(invalid_params(
"ingress is only supported on Unix exec-server hosts".to_string(),
));
}
#[cfg(not(unix))]
let inherited_fds = Vec::new();
let prepared = prepare_exec_request(&params, env, self.runtime_paths.as_ref())?;
let (program, args) = prepared
.command
.split_first()
@@ -256,31 +282,34 @@ impl LocalProcess {
}
let spawned_result = if params.tty {
codex_utils_pty::spawn_pty_process(
codex_utils_pty::pty::spawn_process_with_inherited_fds(
program,
args,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
TerminalSize::default(),
&inherited_fds,
)
.await
} else if params.pipe_stdin {
codex_utils_pty::spawn_pipe_process(
codex_utils_pty::pipe::spawn_process_with_inherited_fds(
program,
args,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
&inherited_fds,
)
.await
} else {
codex_utils_pty::spawn_pipe_process_no_stdin(
codex_utils_pty::pipe::spawn_process_no_stdin_with_inherited_fds(
program,
args,
prepared.cwd.as_path(),
&prepared.env,
&prepared.arg0,
&inherited_fds,
)
.await
};
@@ -297,6 +326,8 @@ impl LocalProcess {
return Err(internal_error(err.to_string()));
}
};
#[cfg(unix)]
drop(ingress);
let output_notify = Arc::new(Notify::new());
let (wake_tx, _wake_rx) = watch::channel(0);
@@ -584,6 +615,21 @@ fn child_env(params: &ExecParams) -> HashMap<String, String> {
env
}
#[cfg(unix)]
fn ingress_error(error: IngressListenerError) -> JSONRPCErrorError {
match error {
IngressListenerError::InvalidPort => {
invalid_request("ingress port must be between 1 and 65535".to_string())
}
IngressListenerError::PortInUse(port) => invalid_request(format!(
"ingress port {port} is already in use by another process"
)),
IngressListenerError::Io(error) => {
internal_error(format!("failed to prepare ingress listener: {error}"))
}
}
}
fn shell_environment_policy(env_policy: &ExecEnvPolicy) -> ShellEnvironmentPolicy {
ShellEnvironmentPolicy {
inherit: env_policy.inherit.clone(),
@@ -995,6 +1041,7 @@ mod tests {
env,
tty: false,
pipe_stdin: false,
ingress: None,
arg0: None,
sandbox: None,
enforce_managed_network: false,
@@ -32,6 +32,11 @@ pub(crate) fn prepare_exec_request(
runtime_paths: Option<&ExecServerRuntimePaths>,
) -> Result<PreparedExecRequest, JSONRPCErrorError> {
let Some(sandbox_context) = params.sandbox.as_ref() else {
if params.ingress.is_some() {
return Err(invalid_params(
"ingress requires sandbox intent on exec-server".to_string(),
));
}
return Ok(PreparedExecRequest {
command: params.argv.clone(),
cwd: native_path(&params.cwd, "cwd")?,
@@ -125,6 +130,7 @@ pub(crate) fn prepare_exec_request(
enforce_managed_network: params.enforce_managed_network,
environment_id: None,
network: None,
ingress: params.ingress,
sandbox_policy_cwd,
codex_linux_sandbox_exe: runtime_paths.codex_linux_sandbox_exe.as_deref(),
use_legacy_landlock: sandbox_context.use_legacy_landlock,
@@ -43,6 +43,7 @@ fn sandbox_request_wraps_native_argv_on_executor() {
env: HashMap::new(),
tty: false,
pipe_stdin: false,
ingress: None,
arg0: None,
sandbox: Some(sandbox),
enforce_managed_network: false,
@@ -81,6 +82,47 @@ fn sandbox_request_wraps_native_argv_on_executor() {
);
}
#[cfg(target_os = "linux")]
#[test]
fn sandbox_request_forwards_ingress_to_linux_sandbox() {
let cwd: AbsolutePathBuf = std::env::current_dir()
.expect("current directory")
.try_into()
.expect("absolute cwd");
let cwd_uri = PathUri::from_abs_path(&cwd);
let self_exe = std::env::current_exe().expect("current executable");
let runtime_paths =
ExecServerRuntimePaths::new(self_exe.clone(), Some(self_exe)).expect("runtime paths");
let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
PermissionProfile::workspace_write(),
cwd_uri.clone(),
);
let params = ExecParams {
process_id: ProcessId::from("ingress"),
argv: vec!["/usr/bin/true".to_string()],
cwd: cwd_uri,
env_policy: None,
env: HashMap::new(),
tty: false,
pipe_stdin: false,
ingress: Some(4173),
arg0: None,
sandbox: Some(sandbox),
enforce_managed_network: false,
managed_network: None,
};
let prepared = prepare_exec_request(&params, HashMap::new(), Some(&runtime_paths))
.expect("prepare ingress sandbox request");
assert!(
prepared
.command
.windows(2)
.any(|args| args[0] == "--ingress" && args[1] == "4173")
);
}
#[cfg(target_os = "macos")]
#[test]
fn sandbox_request_allows_prepared_managed_proxy_port() {
@@ -104,6 +146,7 @@ fn sandbox_request_allows_prepared_managed_proxy_port() {
env: HashMap::new(),
tty: false,
pipe_stdin: false,
ingress: None,
arg0: None,
sandbox: Some(sandbox),
enforce_managed_network: true,
@@ -140,6 +183,7 @@ fn native_request_preserves_native_launch_fields() {
env: HashMap::new(),
tty: false,
pipe_stdin: false,
ingress: None,
arg0: Some("custom-arg0".to_string()),
sandbox: None,
enforce_managed_network: false,
@@ -33,6 +33,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams {
env: inherited_path_env(),
tty: false,
pipe_stdin: false,
ingress: None,
arg0: None,
sandbox: None,
enforce_managed_network: false,
@@ -590,6 +590,7 @@ mod tests {
env,
tty: false,
pipe_stdin: false,
ingress: None,
arg0: None,
sandbox: None,
enforce_managed_network: false,