Launch managed network proxies on remote executors (#33906)

## Why

Remote executions need managed-network proxy listeners in the executor so their
loopback proxy addresses are reachable by the launched process.

## What changed

- Add a capability-gated exec-server protocol field for executor-local proxy
  launch configuration, including network policy, audit metadata, and execution
  attribution.
- Start the proxy while preparing a remote process, replace inherited proxy
  environment variables with its local addresses, and derive the sandbox
  context from its listeners.
- Keep the proxy alive until inherited output streams close, then shut it down.
- Reject unsupported remote settings such as MITM and credential injection.

## Testing

- Cover configuration round trips and rejection of unsupported settings.
- Verify executor-local startup, blocked-domain enforcement, protocol
  compatibility, and proxy lifetime through process closure.

GitOrigin-RevId: c984f54e3e600aa9ebcbf8cf4574046e2c199d11
This commit is contained in:
viyatb-oai
2026-07-17 21:20:14 +00:00
committed by copyberry
parent 7ed19a9758
commit 9f6c29e281
29 changed files with 717 additions and 41 deletions
+1
View File
@@ -461,6 +461,7 @@ pub(crate) async fn execute_exec_request(
exec_server_sandbox: _,
exec_server_enforce_managed_network: _,
exec_server_managed_network: _,
exec_server_network_proxy: _,
} = exec_request;
// TODO(anp): Keep PathUri through the local process launch boundary.
+4
View File
@@ -17,6 +17,7 @@ use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
use codex_file_system::FileSystemSandboxContext;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::models::PermissionProfile;
@@ -65,6 +66,7 @@ pub struct ExecRequest {
pub(crate) exec_server_sandbox: Option<FileSystemSandboxContext>,
pub(crate) exec_server_enforce_managed_network: bool,
pub(crate) exec_server_managed_network: Option<ManagedNetworkSandboxContext>,
pub(crate) exec_server_network_proxy: Option<RemoteNetworkProxyLaunchConfig>,
}
impl ExecRequest {
@@ -110,6 +112,7 @@ impl ExecRequest {
exec_server_sandbox: None,
exec_server_enforce_managed_network: false,
exec_server_managed_network: None,
exec_server_network_proxy: None,
}
}
@@ -169,6 +172,7 @@ impl ExecRequest {
exec_server_sandbox: None,
exec_server_enforce_managed_network: false,
exec_server_managed_network: None,
exec_server_network_proxy: None,
}
}
}
+1
View File
@@ -231,6 +231,7 @@ pub(crate) async fn execute_user_shell_command(
exec_server_sandbox: None,
exec_server_enforce_managed_network: false,
exec_server_managed_network: None,
exec_server_network_proxy: None,
};
let stdout_stream = Some(StdoutStream {
+8 -14
View File
@@ -27,7 +27,6 @@ use codex_protocol::protocol::Event;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::WarningEvent;
use codex_sandboxing::SandboxType;
use indexmap::IndexMap;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -859,7 +858,6 @@ pub(crate) async fn begin_network_approval(
session: &Session,
turn_id: &str,
managed_network_active: bool,
selected_sandbox: SandboxType,
spec: Option<NetworkApprovalSpec>,
) -> Result<Option<ActiveNetworkApproval>, ToolError> {
let NetworkApprovalSpec {
@@ -880,18 +878,14 @@ pub(crate) async fn begin_network_approval(
}
let registration_id = Uuid::new_v4().to_string();
let execution_proxy = if selected_sandbox == SandboxType::LinuxSeccomp {
let attribution_token = Uuid::new_v4().to_string();
network
.for_execution(&environment_id, &registration_id, attribution_token)
.map_err(|err| {
ToolError::Codex(codex_protocol::error::CodexErr::Io(io::Error::other(
format!("failed to create execution-scoped network proxy: {err}"),
)))
})?
} else {
network.clone()
};
let attribution_token = Uuid::new_v4().to_string();
let execution_proxy = network
.for_execution(&environment_id, &registration_id, attribution_token)
.map_err(|err| {
ToolError::Codex(codex_protocol::error::CodexErr::Io(io::Error::other(
format!("failed to create execution-scoped network proxy: {err}"),
)))
})?;
let cancellation_token = CancellationToken::new();
session
.services
-1
View File
@@ -67,7 +67,6 @@ impl ToolOrchestrator {
&tool_ctx.session,
&tool_ctx.turn.sub_id,
managed_network_active,
attempt.sandbox,
tool.network_approval_spec(req, tool_ctx),
)
.await
@@ -169,6 +169,7 @@ pub(super) async fn try_run_zsh_fork(
exec_server_sandbox: _,
exec_server_enforce_managed_network: _,
exec_server_managed_network: _,
exec_server_network_proxy: _,
} = sandbox_exec_request;
let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?;
let effective_timeout = Duration::from_millis(
@@ -904,6 +905,7 @@ impl CoreShellCommandExecutor {
exec_server_sandbox: None,
exec_server_enforce_managed_network: false,
exec_server_managed_network: None,
exec_server_network_proxy: None,
},
/*stdout_stream*/ None,
after_spawn,
@@ -328,7 +328,33 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
launch_sandbox_permissions,
));
let env = exec_env_for_sandbox_permissions(&req.env, launch_sandbox_permissions);
let (env, managed_network_context) = match managed_network {
let (env, managed_network_context, network_proxy_launch) = match managed_network {
Some(network) if environment_is_remote => {
let launch = network.remote_launch_config().await.map_err(|err| {
ToolError::Codex(CodexErr::Io(io::Error::other(err.to_string())))
})?;
if !launch.proxy.enabled {
(env, None, None)
} else {
let environment_info =
req.turn_environment
.environment
.info()
.await
.map_err(|err| {
ToolError::Codex(CodexErr::Io(io::Error::other(format!(
"failed to query exec-server capabilities: {err}"
))))
})?;
if !environment_info.capabilities.network_proxy_launch {
return Err(ToolError::Rejected(
"selected exec-server does not support executor-local network proxy launches"
.to_string(),
));
}
(env, None, Some(launch))
}
}
Some(network) => {
let prepared = network
.prepare_for_optional_environment(
@@ -341,9 +367,9 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
req.turn_environment.environment_id
))))
})?;
(prepared.env, Some(prepared.sandbox_context))
(prepared.env, Some(prepared.sandbox_context), None)
}
None => (env, None),
None => (env, None, None),
};
let explicit_env_overrides = req.explicit_env_overrides.clone();
#[cfg(unix)]
@@ -480,6 +506,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
options,
attempt,
managed_network,
network_proxy_launch,
/*environment_id*/ Some(&req.turn_environment.environment_id),
req.exec_server_env_config.clone(),
req.tty,
+2 -5
View File
@@ -474,10 +474,7 @@ impl<'a> SandboxAttempt<'a> {
&self,
command: SandboxCommand,
options: ExecOptions,
network: Option<&NetworkProxy>,
environment_id: Option<&str>,
) -> Result<crate::sandboxing::ExecRequest, CodexErr> {
let network = self.network_proxy(network);
let managed_network = command.managed_network.clone();
let exec_server_permissions = effective_permission_profile(
self.exec_server_permissions,
@@ -491,8 +488,8 @@ impl<'a> SandboxAttempt<'a> {
// The exec-server must receive the native command, not this host's wrapper.
sandbox: SandboxType::None,
enforce_managed_network: self.enforce_managed_network,
environment_id,
network,
environment_id: None,
network: None,
sandbox_policy_cwd: self.sandbox_cwd,
codex_linux_sandbox_exe: None,
use_legacy_landlock: self.use_legacy_landlock,
+2 -2
View File
@@ -246,7 +246,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
capture_policy: crate::exec::ExecCapturePolicy::ShellTool,
};
let request = attempt
.env_for_exec_server(command(), options(), /*network*/ None, Some("remote"))
.env_for_exec_server(command(), options())
.expect("prepare remote exec request");
assert_eq!(
@@ -278,7 +278,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() {
attempt.sandbox_requested = false;
let request = attempt
.env_for_exec_server(command(), options(), /*network*/ None, Some("remote"))
.env_for_exec_server(command(), options())
.expect("prepare unsandboxed remote exec request");
assert_eq!(request.exec_server_sandbox, None);
@@ -193,6 +193,7 @@ fn exec_server_params_for_request(
sandbox: request.exec_server_sandbox.clone(),
enforce_managed_network: request.exec_server_enforce_managed_network,
managed_network: request.exec_server_managed_network.clone(),
network_proxy: request.exec_server_network_proxy.clone(),
}
}
@@ -959,6 +960,7 @@ impl UnifiedExecProcessManager {
options: ExecOptions,
attempt: &SandboxAttempt<'_>,
network: Option<&NetworkProxy>,
network_proxy_launch: Option<codex_network_proxy::RemoteNetworkProxyLaunchConfig>,
environment_id: Option<&str>,
exec_server_env_config: Option<ExecServerEnvConfig>,
tty: bool,
@@ -966,11 +968,12 @@ impl UnifiedExecProcessManager {
environment: &codex_exec_server::Environment,
) -> Result<UnifiedExecProcess, ToolError> {
let mut request = if environment.is_remote() {
attempt.env_for_exec_server(command, options, network, environment_id)
attempt.env_for_exec_server(command, options)
} else {
attempt.env_for(command, options, network, environment_id)
}
.map_err(ToolError::Codex)?;
request.exec_server_network_proxy = network_proxy_launch;
request.exec_server_env_config = exec_server_env_config;
self.open_session_with_prepared_exec_env(
process_id,
@@ -174,6 +174,7 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() {
exec_server_sandbox: None,
exec_server_enforce_managed_network: true,
exec_server_managed_network: Some(managed_network.clone()),
exec_server_network_proxy: None,
};
let params =
+49 -1
View File
@@ -6,6 +6,7 @@ use codex_file_system::FileSystemSandboxContext;
pub use codex_file_system::WalkOptions;
pub use codex_file_system::WalkOutcome;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
use codex_shell_command::shell_detect::DetectedShell;
@@ -92,6 +93,18 @@ pub struct EnvironmentInfo {
/// Working directory inherited by the exec-server process.
#[serde(default)]
pub cwd: Option<PathUri>,
/// Optional executor features that clients must gate before sending newer request fields.
#[serde(default)]
pub capabilities: EnvironmentCapabilities,
}
/// Features supported by the selected exec-server environment.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnvironmentCapabilities {
/// Whether `exec` accepts instructions for launching an executor-local network proxy.
#[serde(default)]
pub network_proxy_launch: bool,
}
/// Status returned by an initialized exec-server connection.
@@ -121,6 +134,9 @@ impl EnvironmentInfo {
cwd: std::env::current_dir()
.ok()
.and_then(|cwd| PathUri::from_host_native_path(cwd).ok()),
capabilities: EnvironmentCapabilities {
network_proxy_launch: true,
},
}
}
}
@@ -176,6 +192,9 @@ pub struct ExecParams {
/// continue to fail closed. This preserves compatibility with older clients.
#[serde(default)]
pub managed_network: Option<ManagedNetworkSandboxContext>,
/// Optional instructions for starting an executor-local managed-network proxy.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_proxy: Option<RemoteNetworkProxyLaunchConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -710,6 +729,7 @@ mod base64_bytes {
#[cfg(test)]
mod tests {
use super::EnvironmentCapabilities;
use super::EnvironmentInfo;
use super::ExecExitedNotification;
use super::ExecParams;
@@ -719,6 +739,10 @@ mod tests {
use super::ShellInfo;
use codex_file_system::FileSystemSandboxContext;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkProxyAuditMetadata;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::RemoteNetworkProxyConfig;
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
@@ -730,7 +754,7 @@ mod tests {
use std::collections::HashMap;
#[test]
fn exec_params_managed_network_context_round_trips_and_defaults_for_legacy_peers() {
fn exec_params_keeps_proxy_launch_separate_from_sandbox_facts() {
let cwd =
PathUri::from_host_native_path(std::env::current_dir().expect("current directory"))
.expect("cwd URI");
@@ -749,6 +773,17 @@ mod tests {
loopback_ports: vec![43123, 48081],
allow_local_binding: false,
}),
network_proxy: Some(
RemoteNetworkProxyLaunchConfig::new(
RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig::default())
.expect("supported remote config"),
)
.with_audit_metadata(NetworkProxyAuditMetadata {
conversation_id: Some("conversation-1".to_string()),
..NetworkProxyAuditMetadata::default()
})
.for_execution("remote".to_string(), "execution-1".to_string()),
),
};
let mut serialized = serde_json::to_value(&params).expect("serialize exec params");
@@ -759,6 +794,10 @@ mod tests {
"allowLocalBinding": false,
})
);
assert_eq!(
serialized["networkProxy"]["auditMetadata"]["conversationId"],
"conversation-1"
);
let round_trip: ExecParams =
serde_json::from_value(serialized.clone()).expect("deserialize exec params");
assert_eq!(round_trip, params);
@@ -767,10 +806,18 @@ mod tests {
.as_object_mut()
.expect("exec params object")
.remove("managedNetwork");
serialized
.as_object_mut()
.expect("exec params object")
.remove("networkProxy");
let legacy: ExecParams =
serde_json::from_value(serialized).expect("deserialize legacy exec params");
assert!(legacy.enforce_managed_network);
assert_eq!(legacy.managed_network, None);
assert_eq!(legacy.network_proxy, None);
let legacy_serialized =
serde_json::to_value(&legacy).expect("serialize exec params without proxy launch");
assert!(legacy_serialized.get("networkProxy").is_none());
}
#[test]
@@ -788,6 +835,7 @@ mod tests {
path: "/bin/zsh".to_string(),
},
cwd: None,
capabilities: EnvironmentCapabilities::default(),
}
);
}
+1
View File
@@ -1681,6 +1681,7 @@ mod tests {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.instrument(parent_span)
.await
+2
View File
@@ -1459,6 +1459,7 @@ mod tests {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await
.expect("start process");
@@ -1499,6 +1500,7 @@ mod tests {
sandbox: Some(sandbox),
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await;
let Err(err) = result else {
+1
View File
@@ -112,6 +112,7 @@ pub use protocol::CapabilityRootsDiscoverResponse;
pub use protocol::CapabilityTextFile;
pub use protocol::DiscoveredPluginFiles;
pub use protocol::DiscoveredSkillFiles;
pub use protocol::EnvironmentCapabilities;
pub use protocol::EnvironmentInfo;
pub use protocol::EnvironmentStatus;
pub use protocol::EnvironmentStatusKind;
+81 -2
View File
@@ -8,6 +8,7 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_network_proxy::NetworkProxyHandle;
use codex_protocol::config_types::EnvironmentVariablePattern;
use codex_protocol::config_types::ShellEnvironmentPolicy;
use codex_protocol::exec_output::ExecToolCallOutput;
@@ -100,6 +101,7 @@ struct RunningProcess {
termination_requested: bool,
sandbox: SandboxType,
sandbox_denied: bool,
network_proxy_handle: Option<NetworkProxyHandle>,
}
/// Bounded cache of stdin write ids that have already been accepted for one process.
@@ -238,7 +240,7 @@ impl LocalProcess {
) -> 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())?;
prepare_exec_request(&params, child_env(&params), self.runtime_paths.as_ref()).await?;
let (program, args) = prepared
.command
.split_first()
@@ -341,6 +343,7 @@ impl LocalProcess {
termination_requested: false,
sandbox: prepared.sandbox,
sandbox_denied: false,
network_proxy_handle: prepared.network_proxy_handle,
})),
);
}
@@ -924,7 +927,7 @@ async fn finish_output_stream(process_id: ProcessId, inner: Arc<Inner>) {
}
async fn maybe_emit_closed(process_id: ProcessId, inner: Arc<Inner>) {
let (notification, output_notify) = {
let (notification, output_notify, network_proxy_handle) = {
let mut processes = inner.processes.lock().await;
let Some(ProcessEntry::Running(process)) = processes.get_mut(&process_id) else {
return;
@@ -945,9 +948,16 @@ async fn maybe_emit_closed(process_id: ProcessId, inner: Arc<Inner>) {
seq,
},
Arc::clone(&process.output_notify),
process.network_proxy_handle.take(),
)
};
if let Some(network_proxy_handle) = network_proxy_handle
&& let Err(err) = network_proxy_handle.shutdown().await
{
tracing::warn!("failed to shut down executor network proxy: {err}");
}
output_notify.notify_waiters();
let cleanup_process_id = process_id.clone();
let cleanup_inner = Arc::clone(&inner);
@@ -985,6 +995,11 @@ mod tests {
use codex_exec_server_protocol::JSONRPCMessage;
use codex_exec_server_protocol::JSONRPCResponse;
use codex_exec_server_protocol::RequestId;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::NetworkProxyState;
use codex_network_proxy::RemoteNetworkProxyConfig;
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
use codex_otel::MetricsConfig;
use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
use codex_utils_path_uri::PathUri;
@@ -1010,6 +1025,7 @@ mod tests {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
}
}
@@ -1327,6 +1343,68 @@ mod tests {
backend.shutdown().await;
}
#[tokio::test]
async fn exited_process_keeps_network_proxy_until_inherited_streams_close() {
let backend = LocalProcess::default();
let mut process = spawn_test_process(&backend, "proc-background-child").await;
let config = NetworkProxyConfig {
enabled: true,
..Default::default()
};
let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&config)
.expect("build remote network proxy config");
let state = NetworkProxyState::from_remote_launch_config(
RemoteNetworkProxyLaunchConfig::new(proxy_config),
)
.expect("build network proxy state");
let proxy = NetworkProxy::builder()
.state(Arc::new(state))
.build()
.await
.expect("build network proxy");
let handle = proxy.run().await.expect("start network proxy");
let prepared = proxy
.prepare_for_optional_environment(HashMap::new(), /*environment_id*/ None)
.expect("prepare network proxy environment");
let proxy_addr: std::net::SocketAddr = prepared
.env
.get("HTTP_PROXY")
.and_then(|value| value.strip_prefix("http://"))
.expect("HTTP proxy address")
.parse()
.expect("parse HTTP proxy address");
{
let mut processes = backend.inner.processes.lock().await;
let Some(ProcessEntry::Running(running)) = processes.get_mut(&process.process_id)
else {
panic!("test process should be running");
};
running.network_proxy_handle = Some(handle);
}
process.exit(/*exit_code*/ 0);
let exit_response =
read_process_until_change(&backend, &process.process_id, /*after_seq*/ None).await;
assert!(exit_response.exited);
assert!(!exit_response.closed);
tokio::net::TcpStream::connect(proxy_addr)
.await
.expect("proxy should remain available to a child holding inherited output streams");
drop(process.stdout_tx);
drop(process.stderr_tx);
let closed_response = timeout(
Duration::from_secs(1),
read_process_until_closed(&backend, &process.process_id),
)
.await
.expect("process should close");
assert!(closed_response.closed);
assert!(tokio::net::TcpStream::connect(proxy_addr).await.is_err());
backend.shutdown().await;
}
#[tokio::test]
async fn closed_process_is_evicted_after_retention() {
let backend = LocalProcess::default();
@@ -1411,6 +1489,7 @@ mod tests {
termination_requested: false,
sandbox: SandboxType::None,
sandbox_denied: false,
network_proxy_handle: None,
})),
);
assert!(previous.is_none());
+53 -3
View File
@@ -1,7 +1,13 @@
use std::collections::HashMap;
use std::sync::Arc;
use codex_exec_server_protocol::JSONRPCErrorError;
use codex_network_proxy::CUSTOM_CA_ENV_KEYS;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyHandle;
use codex_network_proxy::NetworkProxyState;
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
use codex_protocol::models::PermissionProfile;
use codex_sandboxing::SandboxCommand;
@@ -16,6 +22,7 @@ use codex_utils_path_uri::PathUri;
use crate::ExecServerRuntimePaths;
use crate::protocol::ExecParams;
use crate::rpc::internal_error;
use crate::rpc::invalid_params;
pub(crate) struct PreparedExecRequest {
@@ -24,13 +31,20 @@ pub(crate) struct PreparedExecRequest {
pub(crate) env: HashMap<String, String>,
pub(crate) arg0: Option<String>,
pub(crate) sandbox: SandboxType,
pub(crate) network_proxy_handle: Option<NetworkProxyHandle>,
}
pub(crate) fn prepare_exec_request(
pub(crate) async fn prepare_exec_request(
params: &ExecParams,
env: HashMap<String, String>,
runtime_paths: Option<&ExecServerRuntimePaths>,
) -> Result<PreparedExecRequest, JSONRPCErrorError> {
let (env, managed_network, network_proxy_handle) = prepare_managed_network(
params.managed_network.as_ref(),
params.network_proxy.as_ref(),
env,
)
.await?;
let Some(sandbox_context) = params.sandbox.as_ref() else {
return Ok(PreparedExecRequest {
command: params.argv.clone(),
@@ -38,6 +52,7 @@ pub(crate) fn prepare_exec_request(
env,
arg0: params.arg0.clone(),
sandbox: SandboxType::None,
network_proxy_handle,
});
};
let runtime_paths = runtime_paths
@@ -58,7 +73,7 @@ pub(crate) fn prepare_exec_request(
.collect::<Result<Vec<_>, _>>()?;
let workspace_roots = 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(|_| {
let managed_mitm_ca_trust_bundle_path = 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) {
@@ -129,7 +144,7 @@ pub(crate) fn prepare_exec_request(
args: args.to_vec(),
cwd: params.cwd.clone(),
env,
managed_network: params.managed_network.clone(),
managed_network,
additional_permissions: None,
},
permissions: &permissions,
@@ -151,9 +166,44 @@ pub(crate) fn prepare_exec_request(
env: request.env,
arg0: request.arg0,
sandbox: request.sandbox,
network_proxy_handle,
})
}
async fn prepare_managed_network(
managed_network: Option<&ManagedNetworkSandboxContext>,
network_proxy: Option<&RemoteNetworkProxyLaunchConfig>,
env: HashMap<String, String>,
) -> Result<
(
HashMap<String, String>,
Option<ManagedNetworkSandboxContext>,
Option<NetworkProxyHandle>,
),
JSONRPCErrorError,
> {
let Some(network_proxy) = network_proxy.cloned() else {
return Ok((env, managed_network.cloned(), None));
};
let state = NetworkProxyState::from_remote_launch_config(network_proxy)
.map_err(|err| invalid_params(format!("invalid network proxy config: {err}")))?;
let proxy = NetworkProxy::builder()
.state(Arc::new(state))
.build()
.await
.map_err(|err| internal_error(format!("failed to build executor network proxy: {err}")))?;
let handle = proxy
.run()
.await
.map_err(|err| internal_error(format!("failed to start executor network proxy: {err}")))?;
let prepared = proxy
.prepare_for_optional_environment(env, /*environment_id*/ None)
.map_err(|err| {
internal_error(format!("failed to prepare executor network proxy: {err}"))
})?;
Ok((prepared.env, Some(prepared.sandbox_context), Some(handle)))
}
fn native_path(path: &PathUri, label: &str) -> Result<AbsolutePathBuf, JSONRPCErrorError> {
path.to_abs_path().map_err(|err| {
invalid_params(format!(
@@ -1,12 +1,20 @@
use std::collections::HashMap;
use std::net::SocketAddr;
use std::time::Duration;
#[cfg(target_os = "macos")]
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::RemoteNetworkProxyConfig;
use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
#[cfg(unix)]
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::time::timeout;
use super::prepare_exec_request;
use crate::ExecParams;
@@ -17,8 +25,8 @@ use crate::FileSystemSandboxContext;
use crate::ProcessId;
#[cfg(unix)]
#[test]
fn sandbox_request_wraps_native_argv_on_executor() {
#[tokio::test]
async fn sandbox_request_wraps_native_argv_on_executor() {
let cwd: AbsolutePathBuf = std::env::current_dir()
.expect("current directory")
.try_into()
@@ -47,9 +55,11 @@ fn sandbox_request_wraps_native_argv_on_executor() {
sandbox: Some(sandbox),
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
};
let prepared = prepare_exec_request(&params, HashMap::new(), Some(&runtime_paths))
.await
.expect("prepare sandboxed request");
assert_ne!(prepared.command, params.argv);
@@ -82,8 +92,8 @@ fn sandbox_request_wraps_native_argv_on_executor() {
}
#[cfg(target_os = "macos")]
#[test]
fn sandbox_request_allows_prepared_managed_proxy_port() {
#[tokio::test]
async fn sandbox_request_allows_prepared_managed_proxy_port() {
let cwd: AbsolutePathBuf = std::env::current_dir()
.expect("current directory")
.try_into()
@@ -111,9 +121,11 @@ fn sandbox_request_allows_prepared_managed_proxy_port() {
loopback_ports: vec![43123],
allow_local_binding: false,
}),
network_proxy: None,
};
let prepared = prepare_exec_request(&params, HashMap::new(), Some(&runtime_paths))
.await
.expect("prepare managed-network sandbox request");
let policy = prepared
.command
@@ -124,8 +136,8 @@ fn sandbox_request_allows_prepared_managed_proxy_port() {
assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))"));
}
#[test]
fn native_request_preserves_native_launch_fields() {
#[tokio::test]
async fn native_request_preserves_native_launch_fields() {
let cwd: AbsolutePathBuf = std::env::current_dir()
.expect("current directory")
.try_into()
@@ -144,9 +156,11 @@ fn native_request_preserves_native_launch_fields() {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
};
let prepared = prepare_exec_request(&params, env.clone(), /*runtime_paths*/ None)
.await
.expect("prepare native request");
assert_eq!(prepared.command, params.argv);
@@ -154,3 +168,107 @@ fn native_request_preserves_native_launch_fields() {
assert_eq!(prepared.env, env);
assert_eq!(prepared.arg0, params.arg0);
}
#[tokio::test]
async fn remote_proxy_config_starts_executor_local_proxy() {
let cwd: AbsolutePathBuf = std::env::current_dir()
.expect("current directory")
.try_into()
.expect("absolute cwd");
let mut config = NetworkProxyConfig {
enabled: true,
..NetworkProxyConfig::default()
};
config.set_allowed_domains(vec!["allowed.example".to_string()]);
let proxy_config = RemoteNetworkProxyConfig::from_effective_config(&config)
.expect("supported remote proxy config");
let params = ExecParams {
process_id: ProcessId::from("process-remote-proxy"),
argv: vec!["echo".to_string(), "hello".to_string()],
cwd: PathUri::from_abs_path(&cwd),
env_policy: None,
env: HashMap::new(),
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: Some(
RemoteNetworkProxyLaunchConfig::new(proxy_config)
.for_execution("remote".to_string(), "execution-1".to_string()),
),
};
let stale_proxy = "http://127.0.0.1:9".to_string();
let env = HashMap::from([("HTTP_PROXY".to_string(), stale_proxy.clone())]);
let prepared = prepare_exec_request(&params, env, /*runtime_paths*/ None)
.await
.expect("prepare request with executor-local proxy");
let http_proxy = prepared.env.get("HTTP_PROXY").expect("HTTP proxy env");
assert_ne!(http_proxy, &stale_proxy);
assert!(http_proxy.starts_with("http://127.0.0.1:"));
let proxy_addr: SocketAddr = http_proxy
.strip_prefix("http://")
.expect("HTTP proxy scheme")
.parse()
.expect("HTTP proxy address");
let mut stream = tokio::net::TcpStream::connect(proxy_addr)
.await
.expect("connect to executor proxy");
stream
.write_all(b"CONNECT blocked.example:443 HTTP/1.1\r\nHost: blocked.example:443\r\n\r\n")
.await
.expect("write CONNECT request");
let mut response = [0_u8; 256];
let response_len = timeout(Duration::from_secs(2), stream.read(&mut response))
.await
.expect("proxy response timeout")
.expect("read proxy response");
assert!(String::from_utf8_lossy(&response[..response_len]).starts_with("HTTP/1.1 403"));
prepared
.network_proxy_handle
.expect("running executor proxy")
.shutdown()
.await
.expect("shut down executor proxy");
}
#[tokio::test]
async fn disabled_remote_proxy_config_is_rejected_before_exporting_ports() {
let cwd: AbsolutePathBuf = std::env::current_dir()
.expect("current directory")
.try_into()
.expect("absolute cwd");
let proxy_config =
RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig::default())
.expect("serializable disabled proxy config");
let params = ExecParams {
process_id: ProcessId::from("process-disabled-remote-proxy"),
argv: vec!["echo".to_string(), "hello".to_string()],
cwd: PathUri::from_abs_path(&cwd),
env_policy: None,
env: HashMap::new(),
tty: false,
pipe_stdin: false,
arg0: None,
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: Some(RemoteNetworkProxyLaunchConfig::new(proxy_config)),
};
let error = prepare_exec_request(&params, HashMap::new(), /*runtime_paths*/ None)
.await
.err()
.expect("disabled executor proxy launch must fail closed");
assert_eq!(error.code, -32602);
assert!(
error
.message
.contains("executor-local network proxy launch requires an enabled proxy")
);
}
@@ -37,6 +37,7 @@ fn exec_params_with_argv(process_id: &str, argv: Vec<String>) -> ExecParams {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
}
}
@@ -604,6 +604,7 @@ mod tests {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
}
}
+17 -2
View File
@@ -102,6 +102,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), "proc-1");
@@ -152,7 +153,7 @@ async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() ->
process_id: ProcessId::from("proc-restricted-helper"),
argv: vec!["/bin/cat".to_string(), file.to_string_lossy().into_owned()],
cwd,
env_policy: None,
env_policy: /*env_policy*/ None,
env: HashMap::from([("PATH".to_string(), std::env::var("PATH")?)]),
tty: false,
pipe_stdin: false,
@@ -160,6 +161,7 @@ async fn remote_process_keeps_sandbox_helper_visible_with_restricted_reads() ->
sandbox: Some(sandbox),
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
let output = collect_process_output_from_events(session.process).await?;
@@ -218,7 +220,7 @@ async fn remote_tty_process_uses_configured_sandbox_helper_with_hostile_path() -
process_id: ProcessId::from("proc-hostile-helper-path"),
argv: vec!["/bin/cat".to_string(), file.to_string_lossy().into_owned()],
cwd,
env_policy: None,
env_policy: /*env_policy*/ None,
env: HashMap::from([(
"PATH".to_string(),
hostile_path.to_string_lossy().into_owned(),
@@ -229,6 +231,7 @@ async fn remote_tty_process_uses_configured_sandbox_helper_with_hostile_path() -
sandbox: Some(sandbox),
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
let output = collect_process_output_from_events(session.process).await?;
@@ -279,6 +282,7 @@ async fn remote_process_preserves_empty_workspace_roots() -> Result<()> {
sandbox: Some(sandbox),
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
let (stdout, _stderr, exit_code, closed) =
@@ -423,6 +427,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -457,6 +462,7 @@ async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()> {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -507,6 +513,7 @@ async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Res
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -558,6 +565,7 @@ async fn assert_exec_process_retains_output_after_exit_until_streams_close(
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -634,6 +642,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -674,6 +683,7 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -710,6 +720,7 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool)
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -747,6 +758,7 @@ async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Resu
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(session.process.process_id().as_str(), process_id);
@@ -803,6 +815,7 @@ async fn assert_exec_process_signal_reports_unsupported_on_windows(use_remote: b
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
@@ -846,6 +859,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe(
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
@@ -907,6 +921,7 @@ async fn remote_exec_process_recovers_after_transport_disconnect() -> Result<()>
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
+1
View File
@@ -323,6 +323,7 @@ async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await?;
assert_eq!(
+3
View File
@@ -13,6 +13,7 @@ mod network_policy;
mod policy;
mod proxy;
mod reasons;
mod remote_config;
mod responses;
mod runtime;
mod socks5;
@@ -67,6 +68,8 @@ pub use proxy::PROXY_URL_ENV_KEYS;
pub use proxy::PreparedManagedNetwork;
pub use proxy::has_proxy_url_env_vars;
pub use proxy::proxy_url_env_value;
pub use remote_config::RemoteNetworkProxyConfig;
pub use remote_config::RemoteNetworkProxyLaunchConfig;
pub use runtime::BlockedRequest;
pub use runtime::BlockedRequestArgs;
pub use runtime::BlockedRequestObserver;
+47
View File
@@ -650,6 +650,29 @@ impl NetworkProxy {
self.state.current_cfg().await
}
/// Captures the static inputs needed to launch a matching executor-local proxy.
pub async fn remote_launch_config(&self) -> Result<crate::RemoteNetworkProxyLaunchConfig> {
let proxy = crate::RemoteNetworkProxyConfig::from_effective_config(
&self.state.current_cfg().await?,
)?;
let (environment_id, execution_id) = self
.execution_scope
.as_ref()
.map(|scope| {
(
Some(scope.environment_id.clone()),
Some(scope.execution_id.clone()),
)
})
.unwrap_or_default();
Ok(crate::RemoteNetworkProxyLaunchConfig {
proxy,
audit_metadata: self.state.audit_metadata().clone(),
environment_id,
execution_id,
})
}
pub async fn add_allowed_domain(&self, host: &str) -> Result<()> {
self.state.add_allowed_domain(host).await
}
@@ -1218,6 +1241,30 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn remote_launch_config_carries_execution_scope() -> Result<()> {
let state = Arc::new(network_proxy_state_for_policy(NetworkProxyConfig::default()));
let proxy = match NetworkProxy::builder().state(state).build().await {
Ok(proxy) => proxy,
Err(err) => {
if err
.chain()
.any(|cause| cause.to_string().contains("Operation not permitted"))
{
return Ok(());
}
return Err(err);
}
};
let scoped = proxy.for_execution("remote-env", "execution-1", "token-1".to_string())?;
let launch = scoped.remote_launch_config().await?;
assert_eq!(launch.environment_id.as_deref(), Some("remote-env"));
assert_eq!(launch.execution_id.as_deref(), Some("execution-1"));
Ok(())
}
#[tokio::test]
async fn managed_proxy_builder_does_not_reserve_socks_listener_when_disabled() {
let settings = NetworkProxyConfig {
@@ -2,6 +2,7 @@ use super::*;
pub(super) struct ExecutionScope {
pub(super) environment_id: String,
pub(super) execution_id: String,
pub(super) attribution_token: String,
state: Arc<NetworkProxyState>,
}
@@ -30,6 +31,7 @@ impl NetworkProxy {
let mut proxy = self.clone();
proxy.execution_scope = Some(Arc::new(ExecutionScope {
environment_id: environment_id.to_string(),
execution_id: execution_id.to_string(),
attribution_token,
state: Arc::clone(&self.state),
}));
+112
View File
@@ -0,0 +1,112 @@
use anyhow::Result;
use anyhow::ensure;
use serde::Deserialize;
use serde::Serialize;
use crate::NetworkDomainPermissions;
use crate::NetworkMode;
use crate::NetworkProxyAuditMetadata;
use crate::NetworkProxyConfig;
use crate::NetworkUnixSocketPermissions;
/// Executor-local proxy launch inputs transported with one process start.
///
/// Unlike [`crate::ManagedNetworkSandboxContext`], this describes how the executor should create
/// proxy listeners. The sandbox context is materialized only after those listeners are running.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RemoteNetworkProxyLaunchConfig {
pub proxy: RemoteNetworkProxyConfig,
#[serde(default)]
pub audit_metadata: NetworkProxyAuditMetadata,
#[serde(default)]
pub environment_id: Option<String>,
#[serde(default)]
pub execution_id: Option<String>,
}
impl RemoteNetworkProxyLaunchConfig {
pub fn new(proxy: RemoteNetworkProxyConfig) -> Self {
Self {
proxy,
audit_metadata: NetworkProxyAuditMetadata::default(),
environment_id: None,
execution_id: None,
}
}
pub fn with_audit_metadata(mut self, audit_metadata: NetworkProxyAuditMetadata) -> Self {
self.audit_metadata = audit_metadata;
self
}
pub fn for_execution(mut self, environment_id: String, execution_id: String) -> Self {
self.environment_id = Some(environment_id);
self.execution_id = Some(execution_id);
self
}
}
/// Effective network proxy settings that are safe to send to a remote executor.
///
/// Listener addresses are deliberately omitted because the executor chooses its own loopback
/// ports. MITM, credential injection, and hooks are not represented so their configuration cannot
/// cross the exec-server boundary accidentally.
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RemoteNetworkProxyConfig {
pub enabled: bool,
pub enable_socks5: bool,
pub enable_socks5_udp: bool,
pub allow_upstream_proxy: bool,
pub dangerously_allow_all_unix_sockets: bool,
pub mode: NetworkMode,
pub domains: Option<NetworkDomainPermissions>,
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
pub allow_local_binding: bool,
}
impl RemoteNetworkProxyConfig {
pub fn from_effective_config(config: &NetworkProxyConfig) -> Result<Self> {
ensure!(
!config.enabled
|| (!config.mitm
&& !config.credential_broker
&& !config.dangerously_allow_plaintext_credential_injection
&& config.mitm_hooks.is_empty()),
"remote exec-server network proxy does not support MITM, credential injection, or MITM hooks"
);
Ok(Self {
enabled: config.enabled,
enable_socks5: config.enable_socks5,
enable_socks5_udp: config.enable_socks5_udp,
allow_upstream_proxy: config.allow_upstream_proxy,
dangerously_allow_all_unix_sockets: config.dangerously_allow_all_unix_sockets,
mode: config.mode,
domains: config.domains.clone(),
unix_sockets: config.unix_sockets.clone(),
allow_local_binding: config.allow_local_binding,
})
}
pub(crate) fn into_network_proxy_config(self) -> NetworkProxyConfig {
NetworkProxyConfig {
enabled: self.enabled,
enable_socks5: self.enable_socks5,
enable_socks5_udp: self.enable_socks5_udp,
allow_upstream_proxy: self.allow_upstream_proxy,
dangerously_allow_all_unix_sockets: self.dangerously_allow_all_unix_sockets,
mode: self.mode,
domains: self.domains,
unix_sockets: self.unix_sockets,
allow_local_binding: self.allow_local_binding,
..NetworkProxyConfig::default()
}
}
}
#[cfg(test)]
#[path = "remote_config_tests.rs"]
mod tests;
@@ -0,0 +1,120 @@
use pretty_assertions::assert_eq;
use super::RemoteNetworkProxyConfig;
use super::RemoteNetworkProxyLaunchConfig;
use crate::MitmHookConfig;
use crate::NetworkMode;
use crate::NetworkProxyAuditMetadata;
use crate::NetworkProxyConfig;
use crate::NetworkProxyState;
#[test]
fn round_trip_preserves_supported_effective_settings() {
let mut config = NetworkProxyConfig {
enabled: true,
enable_socks5: false,
enable_socks5_udp: false,
allow_upstream_proxy: false,
dangerously_allow_all_unix_sockets: true,
mode: NetworkMode::Limited,
allow_local_binding: true,
..NetworkProxyConfig::default()
};
config.set_allowed_domains(vec!["example.com".into()]);
config.set_denied_domains(vec!["blocked.example.com".into()]);
config.set_allow_unix_sockets(vec!["/var/run/example.sock".into()]);
let remote =
RemoteNetworkProxyConfig::from_effective_config(&config).expect("supported remote config");
let round_trip = remote.into_network_proxy_config();
assert_eq!(round_trip, config);
}
#[test]
fn rejects_unsupported_configuration() {
let cases = [
(
"MITM",
NetworkProxyConfig {
enabled: true,
mitm: true,
..NetworkProxyConfig::default()
},
),
(
"credential broker",
NetworkProxyConfig {
enabled: true,
credential_broker: true,
..NetworkProxyConfig::default()
},
),
(
"plaintext credential injection",
NetworkProxyConfig {
enabled: true,
dangerously_allow_plaintext_credential_injection: true,
..NetworkProxyConfig::default()
},
),
(
"MITM hooks",
NetworkProxyConfig {
enabled: true,
mitm_hooks: vec![MitmHookConfig::default()],
..NetworkProxyConfig::default()
},
),
];
for (feature, config) in cases {
assert!(
RemoteNetworkProxyConfig::from_effective_config(&config).is_err(),
"{feature} must not cross the remote executor boundary"
);
}
}
#[test]
fn accepts_unsupported_configuration_when_proxy_is_disabled() {
let config = NetworkProxyConfig {
mitm: true,
credential_broker: true,
dangerously_allow_plaintext_credential_injection: true,
mitm_hooks: vec![MitmHookConfig::default()],
..NetworkProxyConfig::default()
};
let remote = RemoteNetworkProxyConfig::from_effective_config(&config)
.expect("disabled proxy configuration does not cross the executor boundary");
assert!(!remote.enabled);
}
#[test]
fn launch_config_materializes_audit_and_execution_attribution() {
let proxy = RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig {
enabled: true,
..NetworkProxyConfig::default()
})
.expect("supported remote config");
let audit_metadata = NetworkProxyAuditMetadata {
conversation_id: Some("conversation-1".to_string()),
user_account_id: Some("account-1".to_string()),
originator: Some("codex_cli_rs".to_string()),
model: Some("model-1".to_string()),
..NetworkProxyAuditMetadata::default()
};
let state = NetworkProxyState::from_remote_launch_config(RemoteNetworkProxyLaunchConfig {
proxy,
audit_metadata: audit_metadata.clone(),
environment_id: Some("remote".to_string()),
execution_id: Some("execution-1".to_string()),
})
.expect("remote launch state");
assert_eq!(state.audit_metadata(), &audit_metadata);
assert_eq!(state.environment_id(), Some("remote"));
assert_eq!(state.execution_id().as_deref(), Some("execution-1"));
}
+46 -1
View File
@@ -23,6 +23,7 @@ use anyhow::Context;
use anyhow::Result;
use codex_utils_absolute_path::AbsolutePathBuf;
use globset::GlobSet;
use serde::Deserialize;
use serde::Serialize;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -47,7 +48,8 @@ const MAX_BLOCKED_EVENTS: usize = 200;
const DNS_LOOKUP_TIMEOUT: Duration = Duration::from_secs(2);
const NETWORK_POLICY_VIOLATION_PREFIX: &str = "CODEX_NETWORK_POLICY_VIOLATION";
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkProxyAuditMetadata {
pub conversation_id: Option<String>,
pub app_version: Option<String>,
@@ -187,6 +189,22 @@ pub trait ConfigReloader: Send + Sync {
pub type ConfigReloaderFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
struct StaticConfigReloader;
impl ConfigReloader for StaticConfigReloader {
fn source_label(&self) -> String {
"static config state".to_string()
}
fn maybe_reload(&self) -> ConfigReloaderFuture<'_, Option<ConfigState>> {
Box::pin(async { Ok(None) })
}
fn reload_now(&self) -> ConfigReloaderFuture<'_, ConfigState> {
Box::pin(async { anyhow::bail!("static config state cannot be reloaded") })
}
}
pub trait BlockedRequestObserver: Send + Sync + 'static {
fn on_blocked_request(&self, request: BlockedRequest) -> BlockedRequestObserverFuture<'_>;
}
@@ -257,6 +275,33 @@ impl Clone for NetworkProxyState {
}
impl NetworkProxyState {
/// Builds runtime state for one executor-local proxy launch.
pub fn from_remote_launch_config(
launch: crate::RemoteNetworkProxyLaunchConfig,
) -> Result<Self> {
let crate::RemoteNetworkProxyLaunchConfig {
proxy,
audit_metadata,
environment_id,
execution_id,
} = launch;
anyhow::ensure!(
proxy.enabled,
"executor-local network proxy launch requires an enabled proxy"
);
let config = proxy.into_network_proxy_config();
let state = build_config_state(config, NetworkProxyConstraints::default())?;
Ok(Self {
environment_id: environment_id.map(Into::into),
execution_id: execution_id.map(Into::into),
..Self::with_reloader_and_audit_metadata(
state,
Arc::new(StaticConfigReloader),
audit_metadata,
)
})
}
pub fn with_reloader(state: ConfigState, reloader: Arc<dyn ConfigReloader>) -> Self {
Self::with_reloader_and_audit_metadata(
state,
@@ -517,6 +517,7 @@ impl ExecutorStdioServerLauncher {
sandbox: None,
enforce_managed_network: false,
managed_network: None,
network_proxy: None,
})
.await
.map_err(io::Error::other)?;