diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d30207835b..86273fec15 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2064,6 +2064,7 @@ dependencies = [ "clap", "codex-app-server-protocol", "codex-linux-sandbox", + "codex-network-proxy", "codex-protocol", "codex-sandboxing", "codex-utils-absolute-path", diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index dce76bf3d2..55f4e6cb9a 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -1279,6 +1279,18 @@ impl Session { } } + pub(crate) async fn managed_network_proxy_spec( + &self, + ) -> Option { + let state = self.state.lock().await; + state + .session_configuration + .original_config_do_not_use + .permissions + .network + .clone() + } + /// Builds the `x-codex-beta-features` header value for this session. /// /// `ModelClient` is session-scoped and intentionally does not depend on the full `Config`, so diff --git a/codex-rs/core/src/config/network_proxy_spec.rs b/codex-rs/core/src/config/network_proxy_spec.rs index 0a37f46cdf..3fa0b3fec8 100644 --- a/codex-rs/core/src/config/network_proxy_spec.rs +++ b/codex-rs/core/src/config/network_proxy_spec.rs @@ -78,6 +78,25 @@ impl NetworkProxySpec { self.config.network.enabled } + pub(crate) fn config(&self) -> &NetworkProxyConfig { + &self.config + } + + pub(crate) fn constraints(&self) -> &NetworkProxyConstraints { + &self.constraints + } + + pub(crate) fn requires_remote_approval_callbacks( + &self, + sandbox_policy: &SandboxPolicy, + ) -> bool { + !self.hard_deny_allowlist_misses + && matches!( + sandbox_policy, + SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } + ) + } + pub fn proxy_host_and_port(&self) -> String { host_and_port_from_network_addr(&self.config.network.proxy_url, /*default_port*/ 3128) } diff --git a/codex-rs/core/src/config/network_proxy_spec_tests.rs b/codex-rs/core/src/config/network_proxy_spec_tests.rs index 742512d600..1b31f2b3c0 100644 --- a/codex-rs/core/src/config/network_proxy_spec_tests.rs +++ b/codex-rs/core/src/config/network_proxy_spec_tests.rs @@ -38,6 +38,32 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() { assert_eq!(state.audit_metadata(), &metadata); } +#[test] +fn requires_remote_approval_callbacks_only_for_restricted_expandable_managed_network() { + let expandable_spec = NetworkProxySpec { + config: NetworkProxyConfig::default(), + constraints: NetworkProxyConstraints::default(), + hard_deny_allowlist_misses: false, + }; + assert!( + expandable_spec.requires_remote_approval_callbacks(&SandboxPolicy::new_read_only_policy()) + ); + assert!( + expandable_spec + .requires_remote_approval_callbacks(&SandboxPolicy::new_workspace_write_policy()) + ); + assert!(!expandable_spec.requires_remote_approval_callbacks(&SandboxPolicy::DangerFullAccess)); + + let hard_deny_spec = NetworkProxySpec { + config: NetworkProxyConfig::default(), + constraints: NetworkProxyConstraints::default(), + hard_deny_allowlist_misses: true, + }; + assert!( + !hard_deny_spec.requires_remote_approval_callbacks(&SandboxPolicy::new_read_only_policy()) + ); +} + #[test] fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() { let mut config = NetworkProxyConfig::default(); diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 0c6a23031b..1487417589 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -33,7 +33,9 @@ use crate::unified_exec::NoopSpawnLifecycle; use crate::unified_exec::UnifiedExecError; use crate::unified_exec::UnifiedExecProcess; use crate::unified_exec::UnifiedExecProcessManager; +use codex_exec_server::ManagedNetworkConfig; use codex_network_proxy::NetworkProxy; +use codex_network_proxy::NetworkProxyAuditMetadata; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::models::PermissionProfile; @@ -83,6 +85,11 @@ pub struct UnifiedExecRuntime<'a> { shell_mode: UnifiedExecShellMode, } +struct RemoteManagedNetworkLaunch { + config: ManagedNetworkConfig, + requires_approval_callbacks: bool, +} + fn build_remote_exec_sandbox_config( attempt: &SandboxAttempt<'_>, additional_permissions: Option, @@ -105,6 +112,35 @@ fn build_remote_exec_sandbox_config( } } +async fn build_remote_exec_managed_network_launch( + session: &crate::codex::Session, + sandbox_policy: &codex_protocol::protocol::SandboxPolicy, +) -> Option { + let spec = session.managed_network_proxy_spec().await?; + let spec = spec + .with_exec_policy_network_rules(session.services.exec_policy.current().as_ref()) + .map_err(|err| { + tracing::warn!( + "failed to apply execpolicy network rules to remote managed proxy; continuing with configured network policy: {err}" + ); + err + }) + .unwrap_or_else(|_| spec.clone()); + + Some(RemoteManagedNetworkLaunch { + config: ManagedNetworkConfig { + config: spec.config().clone(), + constraints: spec.constraints().clone(), + audit_metadata: NetworkProxyAuditMetadata { + conversation_id: Some(session.conversation_id.to_string()), + app_version: Some(env!("CARGO_PKG_VERSION").to_string()), + ..NetworkProxyAuditMetadata::default() + }, + }, + requires_approval_callbacks: spec.requires_remote_approval_callbacks(sandbox_policy), + }) +} + 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 { @@ -238,10 +274,7 @@ impl<'a> ToolRuntime for UnifiedExecRunt command }; - let mut env = req.env.clone(); - if let Some(network) = req.network.as_ref() { - network.apply_to_env(&mut env); - } + let base_env = req.env.clone(); if let Some(environment) = ctx .turn .environment @@ -254,17 +287,38 @@ impl<'a> ToolRuntime for UnifiedExecRunt .to_string(), )); } + let managed_network = if req.network.is_some() { + let managed_network = + build_remote_exec_managed_network_launch(ctx.session.as_ref(), attempt.policy) + .await + .ok_or_else(|| { + ToolError::Rejected( + "remote unified_exec is missing managed-network proxy configuration" + .to_string(), + ) + })?; + if managed_network.requires_approval_callbacks { + return Err(ToolError::Rejected( + "remote unified_exec does not yet support managed-network approval callbacks" + .to_string(), + )); + } + Some(managed_network.config) + } else { + None + }; let exec_params = codex_exec_server::ExecParams { process_id: req.process_id.to_string().into(), argv: command, cwd: req.cwd.clone(), - env, + env: base_env.clone(), tty: req.tty, arg0: None, sandbox: build_remote_exec_sandbox_config( attempt, req.additional_permissions.clone(), ), + managed_network, }; return self .manager @@ -280,6 +334,10 @@ impl<'a> ToolRuntime for UnifiedExecRunt other => ToolError::Rejected(other.to_string()), }); } + let mut env = base_env; + if let Some(network) = req.network.as_ref() { + network.apply_to_env(&mut env); + } let Some(environment) = ctx.turn.environment.as_ref() else { return Err(ToolError::Rejected( "exec_command is unavailable in this session".to_string(), diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index 76845c6983..ef15801154 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -610,6 +610,7 @@ impl UnifiedExecProcessManager { tty, arg0: request.arg0.clone(), sandbox: codex_sandboxing::SandboxLaunchConfig::no_sandbox(request.cwd.clone()), + managed_network: None, }) .await .map_err(|err| UnifiedExecError::create_process(err.to_string()))?; diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 317ec7f45d..30902ec478 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -20,6 +20,7 @@ arc-swap = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } clap = { workspace = true, features = ["derive"] } +codex-network-proxy = { workspace = true } codex-protocol = { workspace = true } codex-sandboxing = { workspace = true } codex-app-server-protocol = { workspace = true } diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 2385b186a2..3858a2b90a 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -280,6 +280,7 @@ mod tests { sandbox: SandboxLaunchConfig::no_sandbox( std::env::current_dir().expect("read current dir"), ), + managed_network: None, }) .await .expect("start process"); diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 88378da9ee..ae5fac23ec 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -54,6 +54,7 @@ pub use protocol::ExecParams; pub use protocol::ExecResponse; pub use protocol::InitializeParams; pub use protocol::InitializeResponse; +pub use protocol::ManagedNetworkConfig; pub use protocol::ReadParams; pub use protocol::ReadResponse; pub use protocol::TerminateParams; diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index 09f6a695d6..4d13ca7dd4 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -9,6 +9,13 @@ use std::time::Duration; use async_trait::async_trait; use codex_app_server_protocol::JSONRPCErrorError; +use codex_network_proxy::ConfigReloader; +use codex_network_proxy::ConfigState; +use codex_network_proxy::NetworkProxy; +use codex_network_proxy::NetworkProxyHandle; +use codex_network_proxy::NetworkProxyState; +use codex_network_proxy::build_config_state; +use codex_network_proxy::validate_policy_against_constraints; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxExecRequest; use codex_sandboxing::SandboxType; @@ -62,6 +69,7 @@ struct RetainedOutputChunk { struct RunningProcess { session: ExecCommandSession, + _managed_network: Option, tty: bool, output: VecDeque, retained_bytes: usize, @@ -122,6 +130,36 @@ struct StartedProcess { wake_tx: watch::Sender, } +struct ManagedNetworkRuntime { + proxy: NetworkProxy, + _handle: NetworkProxyHandle, +} + +#[derive(Clone)] +struct StaticNetworkProxyReloader { + state: ConfigState, +} + +#[async_trait] +impl ConfigReloader for StaticNetworkProxyReloader { + async fn maybe_reload(&self) -> anyhow::Result> { + Ok(None) + } + + async fn reload_now(&self) -> anyhow::Result { + Ok(self.state.clone()) + } + + fn source_label(&self) -> String { + "ExecServerStaticNetworkProxyReloader".to_string() + } +} + +struct PreparedExecLaunch { + request: SandboxExecRequest, + managed_network: Option, +} + impl Default for LocalProcess { fn default() -> Self { let (outgoing_tx, mut outgoing_rx) = @@ -197,8 +235,9 @@ impl LocalProcess { async fn start_process(&self, params: ExecParams) -> Result { self.require_initialized_for("exec")?; let process_id = params.process_id.clone(); - let launch = prepare_exec_launch(¶ms, &self.inner.runtime)?; + let launch = prepare_exec_launch(¶ms, &self.inner.runtime).await?; let (program, args) = launch + .request .command .split_first() .ok_or_else(|| invalid_params("argv must not be empty".to_string()))?; @@ -218,8 +257,8 @@ impl LocalProcess { program, args, params.cwd.as_path(), - &launch.env, - &launch.arg0, + &launch.request.env, + &launch.request.arg0, TerminalSize::default(), ) .await @@ -228,8 +267,8 @@ impl LocalProcess { program, args, params.cwd.as_path(), - &launch.env, - &launch.arg0, + &launch.request.env, + &launch.request.arg0, ) .await }; @@ -252,6 +291,7 @@ impl LocalProcess { process_id.clone(), ProcessEntry::Running(Box::new(RunningProcess { session: spawned.session, + _managed_network: launch.managed_network, tty: params.tty, output: VecDeque::new(), retained_bytes: 0, @@ -296,7 +336,7 @@ impl LocalProcess { Ok(StartedProcess { process_id, - sandbox_type: launch.sandbox, + sandbox_type: launch.request.sandbox, wake_tx, }) } @@ -513,27 +553,70 @@ fn build_sandbox_command( }) } -fn prepare_exec_launch( +async fn start_managed_network_runtime( + config: &crate::protocol::ManagedNetworkConfig, +) -> Result { + validate_policy_against_constraints(&config.config, &config.constraints) + .map_err(|err| internal_error(format!("invalid managed network config: {err}")))?; + let state = + build_config_state(config.config.clone(), config.constraints.clone()).map_err(|err| { + internal_error(format!( + "failed to build managed network proxy state: {err}" + )) + })?; + let reloader_state = state.clone(); + let state = NetworkProxyState::with_reloader_and_audit_metadata( + state, + Arc::new(StaticNetworkProxyReloader { + state: reloader_state, + }), + config.audit_metadata.clone(), + ); + let proxy = NetworkProxy::builder() + .state(Arc::new(state)) + .build() + .await + .map_err(|err| internal_error(format!("failed to build managed network proxy: {err}")))?; + let handle = proxy + .run() + .await + .map_err(|err| internal_error(format!("failed to run managed network proxy: {err}")))?; + Ok(ManagedNetworkRuntime { + proxy, + _handle: handle, + }) +} + +async fn prepare_exec_launch( params: &ExecParams, runtime: &ExecServerRuntimeConfig, -) -> Result { +) -> Result { + let managed_network = match params.managed_network.as_ref() { + Some(config) => Some(start_managed_network_runtime(config).await?), + None => None, + }; + let mut env = params.env.clone(); + if let Some(network) = managed_network.as_ref() { + network.proxy.apply_to_env(&mut env); + } let command = build_sandbox_command( ¶ms.argv, params.cwd.as_path(), - ¶ms.env, + &env, params.sandbox.additional_permissions.clone(), )?; - params + let request = params .sandbox .transform( command, - // TODO: Thread managed-network proxy state across exec-server so - // sandbox profile generation preserves proxy-specific allowances. - /*network*/ - None, + managed_network.as_ref().map(|network| &network.proxy), runtime.codex_linux_sandbox_exe.as_ref(), ) - .map_err(|err| internal_error(format!("failed to build sandbox launch: {err}"))) + .map_err(|err| internal_error(format!("failed to build sandbox launch: {err}")))?; + Ok(PreparedExecLaunch { + request, + managed_network, + }) } impl LocalProcess { diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index dd11028935..abbb7ca159 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -2,6 +2,9 @@ use std::collections::HashMap; use std::path::PathBuf; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use codex_network_proxy::NetworkProxyAuditMetadata; +use codex_network_proxy::NetworkProxyConfig; +use codex_network_proxy::NetworkProxyConstraints; use codex_sandboxing::SandboxLaunchConfig; use serde::Deserialize; use serde::Serialize; @@ -51,6 +54,14 @@ pub struct InitializeParams { #[serde(rename_all = "camelCase")] pub struct InitializeResponse {} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ManagedNetworkConfig { + pub config: NetworkProxyConfig, + pub constraints: NetworkProxyConstraints, + pub audit_metadata: NetworkProxyAuditMetadata, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExecParams { @@ -63,6 +74,7 @@ pub struct ExecParams { pub tty: bool, pub arg0: Option, pub sandbox: SandboxLaunchConfig, + pub managed_network: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/codex-rs/exec-server/src/server/handler/tests.rs b/codex-rs/exec-server/src/server/handler/tests.rs index 42e4866cba..1b02726cc8 100644 --- a/codex-rs/exec-server/src/server/handler/tests.rs +++ b/codex-rs/exec-server/src/server/handler/tests.rs @@ -31,6 +31,7 @@ fn exec_params(process_id: &str) -> ExecParams { tty: false, arg0: None, sandbox: SandboxLaunchConfig::no_sandbox(std::env::current_dir().expect("cwd")), + managed_network: None, } } diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index f411b7a0f8..bd7113757f 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -9,9 +9,13 @@ use codex_exec_server::Environment; use codex_exec_server::ExecBackend; use codex_exec_server::ExecParams; use codex_exec_server::ExecProcess; +use codex_exec_server::ManagedNetworkConfig; use codex_exec_server::ProcessId; use codex_exec_server::ReadResponse; use codex_exec_server::StartedExecProcess; +use codex_network_proxy::NetworkProxyAuditMetadata; +use codex_network_proxy::NetworkProxyConfig; +use codex_network_proxy::NetworkProxyConstraints; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; @@ -63,6 +67,7 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> { tty: false, arg0: None, sandbox: SandboxLaunchConfig::no_sandbox(cwd), + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), "proc-1"); @@ -141,6 +146,7 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> { tty: false, arg0: None, sandbox: SandboxLaunchConfig::no_sandbox(cwd), + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -172,6 +178,7 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> { tty: true, arg0: None, sandbox: SandboxLaunchConfig::no_sandbox(cwd), + managed_network: None, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -210,6 +217,7 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe( tty: false, arg0: None, sandbox: SandboxLaunchConfig::no_sandbox(cwd), + managed_network: None, }) .await?; @@ -262,13 +270,32 @@ fn write_outside_workspace_sandbox(workspace_root: &std::path::Path) -> SandboxL } } +fn managed_network_config() -> ManagedNetworkConfig { + let mut config = NetworkProxyConfig::default(); + config.network.enabled = true; + ManagedNetworkConfig { + config, + constraints: NetworkProxyConstraints { + enabled: Some(true), + ..Default::default() + }, + audit_metadata: NetworkProxyAuditMetadata { + conversation_id: Some("exec-server-smoke".to_string()), + ..Default::default() + }, + } +} + async fn assert_exec_process_sandbox_denies_write_outside_workspace( use_remote: bool, ) -> Result<()> { let temp_dir = TempDir::new()?; let workspace_root = temp_dir.path().join("workspace"); std::fs::create_dir(&workspace_root)?; - let blocked_path = temp_dir.path().join("blocked.txt"); + let blocked_dir = tempfile::Builder::new() + .prefix("exec-server-sandbox-blocked-") + .tempdir_in(std::env::current_dir()?)?; + let blocked_path = blocked_dir.path().join("blocked.txt"); let context = create_process_context(use_remote).await?; let session = context .backend @@ -286,6 +313,7 @@ async fn assert_exec_process_sandbox_denies_write_outside_workspace( tty: false, arg0: None, sandbox: write_outside_workspace_sandbox(&workspace_root), + managed_network: None, }) .await?; @@ -303,6 +331,44 @@ async fn assert_exec_process_sandbox_denies_write_outside_workspace( Ok(()) } +async fn assert_remote_exec_process_applies_managed_network_proxy_env() -> Result<()> { + let context = create_process_context(/*use_remote*/ true).await?; + let session = context + .backend + .start(ExecParams { + process_id: ProcessId::from("proc-managed-network"), + argv: vec![ + "/usr/bin/python3".to_string(), + "-c".to_string(), + "import os; print('HTTP_PROXY=' + os.environ['HTTP_PROXY']); print('HTTPS_PROXY=' + os.environ['HTTPS_PROXY'])" + .to_string(), + ], + cwd: std::env::current_dir()?, + env: Default::default(), + tty: false, + arg0: None, + sandbox: SandboxLaunchConfig::no_sandbox(std::env::current_dir()?), + managed_network: Some(managed_network_config()), + }) + .await?; + + 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!( + output.contains("HTTP_PROXY=http://127.0.0.1:"), + "expected HTTP proxy env from managed network runtime, got {output:?}" + ); + assert!( + output.contains("HTTPS_PROXY=http://127.0.0.1:"), + "expected HTTPS proxy env from managed network runtime, got {output:?}" + ); + assert_eq!(exit_code, Some(0)); + assert!(closed); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_exec_process_reports_transport_disconnect() -> Result<()> { let mut context = create_process_context(/*use_remote*/ true).await?; @@ -322,6 +388,7 @@ async fn remote_exec_process_reports_transport_disconnect() -> Result<()> { sandbox: SandboxLaunchConfig::no_sandbox( std::env::current_dir().expect("read current dir"), ), + managed_network: None, }) .await?; @@ -381,3 +448,8 @@ async fn exec_process_preserves_queued_events_before_subscribe(use_remote: bool) async fn remote_exec_process_sandbox_denies_write_outside_workspace() -> Result<()> { assert_exec_process_sandbox_denies_write_outside_workspace(/*use_remote*/ true).await } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_exec_process_applies_managed_network_proxy_env() -> Result<()> { + assert_remote_exec_process_applies_managed_network_proxy_env().await +} diff --git a/codex-rs/network-proxy/src/runtime.rs b/codex-rs/network-proxy/src/runtime.rs index e967832564..88241321c8 100644 --- a/codex-rs/network-proxy/src/runtime.rs +++ b/codex-rs/network-proxy/src/runtime.rs @@ -19,6 +19,7 @@ use anyhow::Result; use async_trait::async_trait; use codex_utils_absolute_path::AbsolutePathBuf; use globset::GlobSet; +use serde::Deserialize; use serde::Serialize; use std::collections::HashSet; use std::collections::VecDeque; @@ -39,7 +40,7 @@ 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, PartialEq, Eq, Serialize, Deserialize)] pub struct NetworkProxyAuditMetadata { pub conversation_id: Option, pub app_version: Option, diff --git a/codex-rs/network-proxy/src/state.rs b/codex-rs/network-proxy/src/state.rs index 01e4966b5a..7ae1a08d3d 100644 --- a/codex-rs/network-proxy/src/state.rs +++ b/codex-rs/network-proxy/src/state.rs @@ -9,6 +9,7 @@ use crate::policy::compile_denylist_globset; use crate::policy::is_global_wildcard_domain_pattern; use crate::runtime::ConfigState; use serde::Deserialize; +use serde::Serialize; use std::collections::HashSet; use std::sync::Arc; @@ -19,7 +20,7 @@ pub use crate::runtime::NetworkProxyState; #[cfg(test)] pub(crate) use crate::runtime::network_proxy_state_for_policy; -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct NetworkProxyConstraints { pub enabled: Option, pub mode: Option,