diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 5e73227a7f6..10efdf62a36 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -12348,6 +12348,84 @@ ], "type": "string" }, + "NetworkRequirements": { + "properties": { + "allowLocalBinding": { + "type": [ + "boolean", + "null" + ] + }, + "allowUnixSockets": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "allowUpstreamProxy": { + "type": [ + "boolean", + "null" + ] + }, + "allowedDomains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "dangerouslyAllowNonLoopbackAdmin": { + "type": [ + "boolean", + "null" + ] + }, + "dangerouslyAllowNonLoopbackProxy": { + "type": [ + "boolean", + "null" + ] + }, + "deniedDomains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "httpPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "socksPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, "OverriddenMetadata": { "properties": { "effectiveValue": true, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index d6ddd651722..3ba96a852a9 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -52,6 +52,84 @@ }, "type": "object" }, + "NetworkRequirements": { + "properties": { + "allowLocalBinding": { + "type": [ + "boolean", + "null" + ] + }, + "allowUnixSockets": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "allowUpstreamProxy": { + "type": [ + "boolean", + "null" + ] + }, + "allowedDomains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "dangerouslyAllowNonLoopbackAdmin": { + "type": [ + "boolean", + "null" + ] + }, + "dangerouslyAllowNonLoopbackProxy": { + "type": [ + "boolean", + "null" + ] + }, + "deniedDomains": { + "items": { + "type": "string" + }, + "type": [ + "array", + "null" + ] + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "httpPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + }, + "socksPort": { + "format": "uint16", + "minimum": 0.0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, "ResidencyRequirement": { "enum": [ "us" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index 89cecfd189d..f99c880697c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -6,4 +6,4 @@ import type { AskForApproval } from "./AskForApproval"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; -export type ConfigRequirements = { allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, enforceResidency: ResidencyRequirement | null, }; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWebSearchModes: Array | null, enforceResidency: ResidencyRequirement | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts new file mode 100644 index 00000000000..b7ac9d2f7a8 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/NetworkRequirements.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type NetworkRequirements = { enabled: boolean | null, httpPort: number | null, socksPort: number | null, allowUpstreamProxy: boolean | null, dangerouslyAllowNonLoopbackProxy: boolean | null, dangerouslyAllowNonLoopbackAdmin: boolean | null, allowedDomains: Array | null, deniedDomains: Array | null, allowUnixSockets: Array | null, allowLocalBinding: boolean | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 9e7547a9c38..3c6caf034d4 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -91,6 +91,7 @@ export type { Model } from "./Model"; export type { ModelListParams } from "./ModelListParams"; export type { ModelListResponse } from "./ModelListResponse"; export type { NetworkAccess } from "./NetworkAccess"; +export type { NetworkRequirements } from "./NetworkRequirements"; export type { OverriddenMetadata } from "./OverriddenMetadata"; export type { PatchApplyStatus } from "./PatchApplyStatus"; export type { PatchChangeKind } from "./PatchChangeKind"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 4f5b3df5b6a..cb55289907a 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -527,7 +527,7 @@ pub struct ConfigReadResponse { pub layers: Option>, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct ConfigRequirements { @@ -535,6 +535,24 @@ pub struct ConfigRequirements { pub allowed_sandbox_modes: Option>, pub allowed_web_search_modes: Option>, pub enforce_residency: Option, + #[experimental("configRequirements/read.network")] + pub network: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct NetworkRequirements { + pub enabled: Option, + pub http_port: Option, + pub socks_port: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_non_loopback_admin: Option, + pub allowed_domains: Option>, + pub denied_domains: Option>, + pub allow_unix_sockets: Option>, + pub allow_local_binding: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 6065e404f9c..49254c8c329 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -116,7 +116,7 @@ Example (from OpenAI's official VSCode extension): - `config/read` — fetch the effective config on disk after resolving config layering. - `config/value/write` — write a single config key/value to the user's config.toml on disk. - `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk. -- `configRequirements/read` — fetch the loaded requirements allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`) and `enforceResidency` from `requirements.toml` and/or MDM (or `null` if none are configured). +- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), `enforceResidency`, and `network` constraints. ### Example: Start or resume a thread diff --git a/codex-rs/app-server/src/config_api.rs b/codex-rs/app-server/src/config_api.rs index 14c4e441733..e1531dce2f6 100644 --- a/codex-rs/app-server/src/config_api.rs +++ b/codex-rs/app-server/src/config_api.rs @@ -9,6 +9,7 @@ use codex_app_server_protocol::ConfigValueWriteParams; use codex_app_server_protocol::ConfigWriteErrorCode; use codex_app_server_protocol::ConfigWriteResponse; use codex_app_server_protocol::JSONRPCErrorError; +use codex_app_server_protocol::NetworkRequirements; use codex_app_server_protocol::SandboxMode; use codex_core::config::ConfigService; use codex_core::config::ConfigServiceError; @@ -129,6 +130,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR enforce_residency: requirements .enforce_residency .map(map_residency_requirement_to_api), + network: requirements.network.map(map_network_requirements_to_api), } } @@ -149,6 +151,23 @@ fn map_residency_requirement_to_api( } } +fn map_network_requirements_to_api( + network: codex_core::config_loader::NetworkRequirementsToml, +) -> NetworkRequirements { + NetworkRequirements { + enabled: network.enabled, + http_port: network.http_port, + socks_port: network.socks_port, + allow_upstream_proxy: network.allow_upstream_proxy, + dangerously_allow_non_loopback_proxy: network.dangerously_allow_non_loopback_proxy, + dangerously_allow_non_loopback_admin: network.dangerously_allow_non_loopback_admin, + allowed_domains: network.allowed_domains, + denied_domains: network.denied_domains, + allow_unix_sockets: network.allow_unix_sockets, + allow_local_binding: network.allow_local_binding, + } +} + fn map_error(err: ConfigServiceError) -> JSONRPCErrorError { if let Some(code) = err.write_error_code() { return config_write_error(code, err.to_string()); @@ -174,6 +193,7 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> #[cfg(test)] mod tests { use super::*; + use codex_core::config_loader::NetworkRequirementsToml as CoreNetworkRequirementsToml; use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use pretty_assertions::assert_eq; @@ -194,6 +214,18 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: Some(CoreResidencyRequirement::Us), + network: Some(CoreNetworkRequirementsToml { + enabled: Some(true), + http_port: Some(8080), + socks_port: Some(1080), + allow_upstream_proxy: Some(false), + dangerously_allow_non_loopback_proxy: Some(false), + dangerously_allow_non_loopback_admin: Some(false), + allowed_domains: Some(vec!["api.openai.com".to_string()]), + denied_domains: Some(vec!["example.com".to_string()]), + allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), + allow_local_binding: Some(true), + }), }; let mapped = map_requirements_toml_to_api(requirements); @@ -217,6 +249,21 @@ mod tests { mapped.enforce_residency, Some(codex_app_server_protocol::ResidencyRequirement::Us), ); + assert_eq!( + mapped.network, + Some(NetworkRequirements { + enabled: Some(true), + http_port: Some(8080), + socks_port: Some(1080), + allow_upstream_proxy: Some(false), + dangerously_allow_non_loopback_proxy: Some(false), + dangerously_allow_non_loopback_admin: Some(false), + allowed_domains: Some(vec!["api.openai.com".to_string()]), + denied_domains: Some(vec!["example.com".to_string()]), + allow_unix_sockets: Some(vec!["/tmp/proxy.sock".to_string()]), + allow_local_binding: Some(true), + }), + ); } #[test] @@ -228,6 +275,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let mapped = map_requirements_toml_to_api(requirements); diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index 30d49bd4066..36787e98f91 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -385,6 +385,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }) ); } @@ -426,6 +427,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }) ); } @@ -470,6 +472,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }) ); assert_eq!(fetcher.request_count.load(Ordering::SeqCst), 2); diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 9d7d9195cf5..dee21376cbd 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1653,6 +1653,7 @@ impl Config { mcp_servers, exec_policy: _, enforce_residency, + network: _network_requirements, } = requirements; apply_requirement_constrained_value( @@ -4379,6 +4380,7 @@ model_verbosity = "high" mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let requirement_source = crate::config_loader::RequirementSource::Unknown; let requirement_source_for_error = requirement_source.clone(); @@ -4936,6 +4938,7 @@ mcp_oauth_callback_port = 5678 mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let config = ConfigBuilder::default() diff --git a/codex-rs/core/src/config_loader/config_requirements.rs b/codex-rs/core/src/config_loader/config_requirements.rs index da475bd4ede..21dbdb3d7f9 100644 --- a/codex-rs/core/src/config_loader/config_requirements.rs +++ b/codex-rs/core/src/config_loader/config_requirements.rs @@ -81,6 +81,8 @@ pub struct ConfigRequirements { pub mcp_servers: Option>>, pub(crate) exec_policy: Option>, pub enforce_residency: ConstrainedWithSource>, + /// Managed network constraints derived from requirements. + pub network: Option>, } impl Default for ConfigRequirements { @@ -101,6 +103,7 @@ impl Default for ConfigRequirements { mcp_servers: None, exec_policy: None, enforce_residency: ConstrainedWithSource::new(Constrained::allow_any(None), None), + network: None, } } } @@ -123,6 +126,64 @@ pub struct McpServerRequirement { pub identity: McpServerIdentity, } +#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct NetworkRequirementsToml { + pub enabled: Option, + pub http_port: Option, + pub socks_port: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_non_loopback_admin: Option, + pub allowed_domains: Option>, + pub denied_domains: Option>, + pub allow_unix_sockets: Option>, + pub allow_local_binding: Option, +} + +/// Normalized network constraints derived from requirements TOML. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct NetworkConstraints { + pub enabled: Option, + pub http_port: Option, + pub socks_port: Option, + pub allow_upstream_proxy: Option, + pub dangerously_allow_non_loopback_proxy: Option, + pub dangerously_allow_non_loopback_admin: Option, + pub allowed_domains: Option>, + pub denied_domains: Option>, + pub allow_unix_sockets: Option>, + pub allow_local_binding: Option, +} + +impl From for NetworkConstraints { + fn from(value: NetworkRequirementsToml) -> Self { + let NetworkRequirementsToml { + enabled, + http_port, + socks_port, + allow_upstream_proxy, + dangerously_allow_non_loopback_proxy, + dangerously_allow_non_loopback_admin, + allowed_domains, + denied_domains, + allow_unix_sockets, + allow_local_binding, + } = value; + Self { + enabled, + http_port, + socks_port, + allow_upstream_proxy, + dangerously_allow_non_loopback_proxy, + dangerously_allow_non_loopback_admin, + allowed_domains, + denied_domains, + allow_unix_sockets, + allow_local_binding, + } + } +} + #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "lowercase")] pub enum WebSearchModeRequirement { @@ -170,6 +231,8 @@ pub struct ConfigRequirementsToml { pub mcp_servers: Option>, pub rules: Option, pub enforce_residency: Option, + #[serde(rename = "experimental_network")] + pub network: Option, } /// Value paired with the requirement source it came from, for better error @@ -202,6 +265,7 @@ pub struct ConfigRequirementsWithSources { pub mcp_servers: Option>>, pub rules: Option>, pub enforce_residency: Option>, + pub network: Option>, } impl ConfigRequirementsWithSources { @@ -236,6 +300,7 @@ impl ConfigRequirementsWithSources { mcp_servers, rules, enforce_residency, + network, } ); } @@ -248,6 +313,7 @@ impl ConfigRequirementsWithSources { mcp_servers, rules, enforce_residency, + network, } = self; ConfigRequirementsToml { allowed_approval_policies: allowed_approval_policies.map(|sourced| sourced.value), @@ -256,6 +322,7 @@ impl ConfigRequirementsWithSources { mcp_servers: mcp_servers.map(|sourced| sourced.value), rules: rules.map(|sourced| sourced.value), enforce_residency: enforce_residency.map(|sourced| sourced.value), + network: network.map(|sourced| sourced.value), } } } @@ -301,6 +368,7 @@ impl ConfigRequirementsToml { && self.mcp_servers.is_none() && self.rules.is_none() && self.enforce_residency.is_none() + && self.network.is_none() } } @@ -315,6 +383,7 @@ impl TryFrom for ConfigRequirements { mcp_servers, rules, enforce_residency, + network, } = toml; let approval_policy = match allowed_approval_policies { @@ -471,6 +540,10 @@ impl TryFrom for ConfigRequirements { } None => ConstrainedWithSource::new(Constrained::allow_any(None), None), }; + let network = network.map(|sourced_network| { + let Sourced { value, source } = sourced_network; + Sourced::new(NetworkConstraints::from(value), source) + }); Ok(ConfigRequirements { approval_policy, sandbox_policy, @@ -478,6 +551,7 @@ impl TryFrom for ConfigRequirements { mcp_servers, exec_policy, enforce_residency, + network, }) } } @@ -506,6 +580,7 @@ mod tests { mcp_servers, rules, enforce_residency, + network, } = toml; ConfigRequirementsWithSources { allowed_approval_policies: allowed_approval_policies @@ -518,6 +593,7 @@ mod tests { rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)), enforce_residency: enforce_residency .map(|value| Sourced::new(value, RequirementSource::Unknown)), + network: network.map(|value| Sourced::new(value, RequirementSource::Unknown)), } } @@ -547,6 +623,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: Some(enforce_residency), + network: None, }; target.merge_unset_fields(source.clone(), other); @@ -566,6 +643,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: Some(Sourced::new(enforce_residency, enforce_source)), + network: None, } ); } @@ -597,6 +675,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, } ); Ok(()) @@ -636,6 +715,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, } ); Ok(()) @@ -951,6 +1031,50 @@ mod tests { Ok(()) } + #[test] + fn network_requirements_are_preserved_as_constraints_with_source() -> Result<()> { + let toml_str = r#" + [experimental_network] + enabled = true + allow_upstream_proxy = false + allowed_domains = ["api.example.com", "*.openai.com"] + denied_domains = ["blocked.example.com"] + allow_unix_sockets = ["/tmp/example.sock"] + allow_local_binding = false + "#; + + let source = RequirementSource::CloudRequirements; + let mut requirements_with_sources = ConfigRequirementsWithSources::default(); + requirements_with_sources.merge_unset_fields(source.clone(), from_str(toml_str)?); + + let requirements = ConfigRequirements::try_from(requirements_with_sources)?; + let sourced_network = requirements + .network + .expect("network requirements should be preserved as constraints"); + + assert_eq!(sourced_network.source, source); + assert_eq!(sourced_network.value.enabled, Some(true)); + assert_eq!(sourced_network.value.allow_upstream_proxy, Some(false)); + assert_eq!( + sourced_network.value.allowed_domains.as_ref(), + Some(&vec![ + "api.example.com".to_string(), + "*.openai.com".to_string() + ]) + ); + assert_eq!( + sourced_network.value.denied_domains.as_ref(), + Some(&vec!["blocked.example.com".to_string()]) + ); + assert_eq!( + sourced_network.value.allow_unix_sockets.as_ref(), + Some(&vec!["/tmp/example.sock".to_string()]) + ); + assert_eq!(sourced_network.value.allow_local_binding, Some(false)); + + Ok(()) + } + #[test] fn deserialize_mcp_server_requirements() -> Result<()> { let toml_str = r#" diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index c79388a71ef..0966ac8747c 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -37,6 +37,8 @@ pub use config_requirements::ConfigRequirementsToml; pub use config_requirements::ConstrainedWithSource; pub use config_requirements::McpServerIdentity; pub use config_requirements::McpServerRequirement; +pub use config_requirements::NetworkConstraints; +pub use config_requirements::NetworkRequirementsToml; pub use config_requirements::RequirementSource; pub use config_requirements::ResidencyRequirement; pub use config_requirements::SandboxModeRequirement; diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 7f9c2a9a8ce..1b5125840f0 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -568,6 +568,7 @@ allowed_approval_policies = ["on-request"] mcp_servers: None, rules: None, enforce_residency: None, + network: None, }) }), ) @@ -615,6 +616,7 @@ allowed_approval_policies = ["on-request"] mcp_servers: None, rules: None, enforce_residency: None, + network: None, }, ); load_requirements_toml(&mut config_requirements_toml, &requirements_file).await?; @@ -651,6 +653,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let expected = requirements.clone(); let cloud_requirements = CloudRequirementsLoader::new(async move { Some(requirements) }); diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 911510aaf80..f574186a891 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -338,6 +338,7 @@ mod tests { )])), rules: None, enforce_residency: Some(ResidencyRequirement::Us), + network: None, }; let user_file = if cfg!(windows) { @@ -393,6 +394,7 @@ mod tests { mcp_servers: None, rules: None, enforce_residency: None, + network: None, }; let stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)