mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Prepare managed network sandbox context (#29456)
## Why Managed network configures commands to use local HTTP and SOCKS proxies. For commands delegated to the exec server, the proxy environment and the sandbox policy were prepared separately. On macOS, that meant a command could receive `HTTPS_PROXY=http://127.0.0.1:43123` while Seatbelt still denied access to port `43123`. ## What changed `NetworkProxy` now prepares the command environment and sandbox context together from the same runtime snapshot: ```text Prepared managed network ├── command environment: HTTPS_PROXY=http://127.0.0.1:43123 └── sandbox context: allow outbound to 127.0.0.1:43123 ``` That context travels with remote exec requests. The exec server preserves the managed proxy and CA environment, and macOS Seatbelt allows only the prepared loopback proxy ports without enabling broad network access or local binding. The protocol field is optional and the existing enforcement flag remains in place, preserving compatibility with callers that do not send the new context.
This commit is contained in:
@@ -20,6 +20,7 @@ codex-app-server-protocol = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-file-system = { workspace = true }
|
||||
codex-network-proxy = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
codex-sandboxing = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
|
||||
@@ -1162,6 +1162,7 @@ mod tests {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await
|
||||
.expect("start process");
|
||||
@@ -1201,6 +1202,7 @@ mod tests {
|
||||
arg0: None,
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await;
|
||||
let Err(err) = result else {
|
||||
|
||||
@@ -115,6 +115,7 @@ impl FileSystemSandboxRunner {
|
||||
args: vec![CODEX_FS_HELPER_ARG1.to_string()],
|
||||
cwd: cwd.uri.clone(),
|
||||
env: self.helper_env.clone(),
|
||||
managed_network: None,
|
||||
additional_permissions: None,
|
||||
};
|
||||
let native_workspace_roots = sandbox_context
|
||||
|
||||
@@ -965,6 +965,7 @@ mod tests {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_network_proxy::CUSTOM_CA_ENV_KEYS;
|
||||
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_sandboxing::SandboxCommand;
|
||||
use codex_sandboxing::SandboxDirectSpawnTransformRequest;
|
||||
@@ -8,6 +10,7 @@ use codex_sandboxing::SandboxManager;
|
||||
use codex_sandboxing::SandboxTransformRequest;
|
||||
use codex_sandboxing::SandboxType;
|
||||
use codex_sandboxing::SandboxablePreference;
|
||||
use codex_sandboxing::with_managed_mitm_ca_readable_root;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
|
||||
@@ -59,6 +62,20 @@ pub(crate) fn prepare_exec_request(
|
||||
native_workspace_roots.as_slice()
|
||||
};
|
||||
let permissions = permissions.materialize_project_roots_with_workspace_roots(workspace_roots);
|
||||
let managed_mitm_ca_trust_bundle_path = params.managed_network.as_ref().and_then(|_| {
|
||||
CUSTOM_CA_ENV_KEYS.iter().find_map(|key| {
|
||||
let path = env.get(*key)?;
|
||||
if !is_managed_mitm_ca_trust_bundle_path(path) {
|
||||
return None;
|
||||
}
|
||||
AbsolutePathBuf::from_absolute_path(path).ok()
|
||||
})
|
||||
});
|
||||
let permissions = with_managed_mitm_ca_readable_root(
|
||||
permissions,
|
||||
managed_mitm_ca_trust_bundle_path.as_ref(),
|
||||
native_sandbox_policy_cwd.as_path(),
|
||||
);
|
||||
let (file_system_policy, network_policy) = permissions.to_runtime_permissions();
|
||||
let sandbox_manager = SandboxManager::new();
|
||||
let sandbox = sandbox_manager.select_initial(
|
||||
@@ -98,6 +115,7 @@ pub(crate) fn prepare_exec_request(
|
||||
args: args.to_vec(),
|
||||
cwd: params.cwd.clone(),
|
||||
env,
|
||||
managed_network: params.managed_network.clone(),
|
||||
additional_permissions: None,
|
||||
},
|
||||
permissions: &permissions,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
#[cfg(unix)]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -44,6 +46,7 @@ fn sandbox_request_wraps_native_argv_on_executor() {
|
||||
arg0: None,
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
};
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths))
|
||||
@@ -78,6 +81,49 @@ fn sandbox_request_wraps_native_argv_on_executor() {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn sandbox_request_allows_prepared_managed_proxy_port() {
|
||||
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("process-managed-network"),
|
||||
argv: vec!["/usr/bin/true".to_string()],
|
||||
cwd: cwd_uri,
|
||||
env_policy: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: Some(sandbox),
|
||||
enforce_managed_network: true,
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123],
|
||||
allow_local_binding: false,
|
||||
}),
|
||||
};
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, HashMap::new(), Some(&runtime_paths))
|
||||
.expect("prepare managed-network sandbox request");
|
||||
let policy = prepared
|
||||
.command
|
||||
.windows(2)
|
||||
.find_map(|args| (args[0] == "-p").then_some(args[1].as_str()))
|
||||
.expect("Seatbelt policy argument");
|
||||
|
||||
assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_request_preserves_native_launch_fields() {
|
||||
let cwd: AbsolutePathBuf = std::env::current_dir()
|
||||
@@ -97,6 +143,7 @@ fn native_request_preserves_native_launch_fields() {
|
||||
arg0: Some("custom-arg0".to_string()),
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
};
|
||||
|
||||
let prepared = prepare_exec_request(¶ms, env.clone(), /*runtime_paths*/ None)
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use serde::Deserialize;
|
||||
@@ -109,6 +110,12 @@ pub struct ExecParams {
|
||||
/// Whether the eventual executor-side sandbox must enforce managed networking.
|
||||
#[serde(default)]
|
||||
pub enforce_managed_network: bool,
|
||||
/// Optional details for enforcing managed networking without a live proxy object.
|
||||
///
|
||||
/// When `enforce_managed_network` is true and these details are absent, the executor must
|
||||
/// continue to fail closed. This preserves compatibility with older clients.
|
||||
#[serde(default)]
|
||||
pub managed_network: Option<ManagedNetworkSandboxContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -504,12 +511,60 @@ mod base64_bytes {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ExecParams;
|
||||
use super::FsReadFileParams;
|
||||
use super::HttpRequestParams;
|
||||
use super::ProcessId;
|
||||
use codex_file_system::FileSystemSandboxContext;
|
||||
use codex_network_proxy::ManagedNetworkSandboxContext;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn exec_params_managed_network_context_round_trips_and_defaults_for_legacy_peers() {
|
||||
let cwd =
|
||||
PathUri::from_host_native_path(std::env::current_dir().expect("current directory"))
|
||||
.expect("cwd URI");
|
||||
let params = ExecParams {
|
||||
process_id: ProcessId::from("managed-network"),
|
||||
argv: vec!["true".to_string()],
|
||||
cwd,
|
||||
env_policy: None,
|
||||
env: HashMap::new(),
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: true,
|
||||
managed_network: Some(ManagedNetworkSandboxContext {
|
||||
loopback_ports: vec![43123, 48081],
|
||||
allow_local_binding: false,
|
||||
}),
|
||||
};
|
||||
|
||||
let mut serialized = serde_json::to_value(¶ms).expect("serialize exec params");
|
||||
assert_eq!(
|
||||
serialized["managedNetwork"],
|
||||
serde_json::json!({
|
||||
"loopbackPorts": [43123, 48081],
|
||||
"allowLocalBinding": false,
|
||||
})
|
||||
);
|
||||
let round_trip: ExecParams =
|
||||
serde_json::from_value(serialized.clone()).expect("deserialize exec params");
|
||||
assert_eq!(round_trip, params);
|
||||
|
||||
serialized
|
||||
.as_object_mut()
|
||||
.expect("exec params object")
|
||||
.remove("managedNetwork");
|
||||
let legacy: ExecParams =
|
||||
serde_json::from_value(serialized).expect("deserialize legacy exec params");
|
||||
assert!(legacy.enforce_managed_network);
|
||||
assert_eq!(legacy.managed_network, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filesystem_protocol_accepts_legacy_absolute_paths_and_serializes_path_uris() {
|
||||
|
||||
@@ -36,6 +36,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -441,6 +441,7 @@ mod tests {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), "proc-1");
|
||||
@@ -226,6 +227,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -259,6 +261,7 @@ async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()> {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -308,6 +311,7 @@ async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Res
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -358,6 +362,7 @@ async fn assert_exec_process_retains_output_after_exit_until_streams_close(
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -433,6 +438,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -472,6 +478,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -507,6 +514,7 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool)
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -543,6 +551,7 @@ async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Resu
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), process_id);
|
||||
@@ -598,6 +607,7 @@ async fn assert_exec_process_signal_reports_unsupported_on_windows(use_remote: b
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -640,6 +650,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -700,6 +711,7 @@ async fn remote_exec_process_recovers_after_transport_disconnect() -> Result<()>
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -152,6 +152,7 @@ async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> {
|
||||
arg0: None,
|
||||
sandbox: None,
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user