mirror of
https://github.com/openai/codex.git
synced 2026-05-06 20:36:33 +00:00
Compare commits
9 Commits
rust-v0.12
...
codex/viya
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f85d1ce10 | ||
|
|
e19744ed58 | ||
|
|
46ab37fdec | ||
|
|
d51647a1c3 | ||
|
|
49610cbe9b | ||
|
|
bae703b98a | ||
|
|
38e54e9b82 | ||
|
|
c587ef7368 | ||
|
|
662d0fa67f |
@@ -58,7 +58,7 @@ use codex_core::config::edit::ConfigEditsBuilder;
|
||||
use codex_core::config::find_codex_home;
|
||||
use codex_features::FEATURES;
|
||||
use codex_features::Stage;
|
||||
use codex_features::is_known_feature_key;
|
||||
use codex_features::feature_toggle_override_key;
|
||||
use codex_login::AuthManager;
|
||||
use codex_memories_write::clear_memory_roots_contents;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
@@ -676,22 +676,23 @@ impl FeatureToggles {
|
||||
fn to_overrides(&self) -> anyhow::Result<Vec<String>> {
|
||||
let mut v = Vec::new();
|
||||
for feature in &self.enable {
|
||||
Self::validate_feature(feature)?;
|
||||
v.push(format!("features.{feature}=true"));
|
||||
let key = Self::feature_override_key(feature)?;
|
||||
v.push(format!("{key}=true"));
|
||||
}
|
||||
for feature in &self.disable {
|
||||
Self::validate_feature(feature)?;
|
||||
v.push(format!("features.{feature}=false"));
|
||||
let key = Self::feature_override_key(feature)?;
|
||||
v.push(format!("{key}=false"));
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn feature_override_key(feature: &str) -> anyhow::Result<String> {
|
||||
feature_toggle_override_key(feature)
|
||||
.ok_or_else(|| anyhow::anyhow!("Unknown feature flag: {feature}"))
|
||||
}
|
||||
|
||||
fn validate_feature(feature: &str) -> anyhow::Result<()> {
|
||||
if is_known_feature_key(feature) {
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("Unknown feature flag: {feature}")
|
||||
}
|
||||
Self::feature_override_key(feature).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2568,6 +2569,22 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_toggles_preserve_configurable_feature_tables() {
|
||||
let toggles = FeatureToggles {
|
||||
enable: vec!["network_proxy".to_string()],
|
||||
disable: vec!["multi_agent_v2".to_string()],
|
||||
};
|
||||
let overrides = toggles.to_overrides().expect("valid features");
|
||||
assert_eq!(
|
||||
overrides,
|
||||
vec![
|
||||
"features.network_proxy.enabled=true".to_string(),
|
||||
"features.multi_agent_v2.enabled=false".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feature_toggles_accept_legacy_linux_sandbox_flag() {
|
||||
let toggles = FeatureToggles {
|
||||
|
||||
@@ -43,6 +43,15 @@ pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if feature.id == codex_features::Feature::NetworkProxy {
|
||||
validation.properties.insert(
|
||||
feature.key.to_string(),
|
||||
schema_gen.subschema_for::<codex_features::FeatureToml<
|
||||
codex_features::NetworkProxyConfigToml,
|
||||
>>(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
validation
|
||||
.properties
|
||||
.insert(feature.key.to_string(), schema_gen.subschema_for::<bool>());
|
||||
|
||||
@@ -481,6 +481,9 @@
|
||||
"multi_agent_v2": {
|
||||
"$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml"
|
||||
},
|
||||
"network_proxy": {
|
||||
"$ref": "#/definitions/FeatureToml_for_NetworkProxyConfigToml"
|
||||
},
|
||||
"personality": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -785,6 +788,16 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FeatureToml_for_NetworkProxyConfigToml": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/NetworkProxyConfigToml"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FeedbackConfigToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -1460,6 +1473,75 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkProxyConfigToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"allow_local_binding": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"allow_upstream_proxy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dangerously_allow_all_unix_sockets": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"dangerously_allow_non_loopback_proxy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"domains": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkProxyDomainPermissionToml"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"enable_socks5": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enable_socks5_udp": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mode": {
|
||||
"$ref": "#/definitions/NetworkProxyModeToml"
|
||||
},
|
||||
"proxy_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"socks_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"unix_sockets": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkProxyUnixSocketPermissionToml"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkProxyDomainPermissionToml": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"deny"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkProxyModeToml": {
|
||||
"enum": [
|
||||
"limited",
|
||||
"full"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkProxyUnixSocketPermissionToml": {
|
||||
"enum": [
|
||||
"allow",
|
||||
"none"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"NetworkToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -3947,6 +4029,9 @@
|
||||
"multi_agent_v2": {
|
||||
"$ref": "#/definitions/FeatureToml_for_MultiAgentV2ConfigToml"
|
||||
},
|
||||
"network_proxy": {
|
||||
"$ref": "#/definitions/FeatureToml_for_NetworkProxyConfigToml"
|
||||
},
|
||||
"personality": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
||||
@@ -733,59 +733,12 @@ allow_upstream_proxy = false
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_network_enabled_allows_runtime_network_without_proxy()
|
||||
async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_without_feature()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
||||
)]),
|
||||
}),
|
||||
network: Some(NetworkToml {
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled
|
||||
);
|
||||
assert!(
|
||||
config.permissions.network.is_none(),
|
||||
"bare profile network.enabled should not start the managed network proxy"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_proxy_policy_starts_managed_network_proxy() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
@@ -818,6 +771,227 @@ async fn permissions_profiles_proxy_policy_starts_managed_network_proxy() -> std
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled
|
||||
);
|
||||
assert!(
|
||||
config.permissions.network.is_none(),
|
||||
"profile proxy policy should not start the managed network proxy without the feature"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn network_proxy_feature_is_no_op_without_sandbox_network() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
features: Some(toml::from_str("network_proxy = true").expect("valid features")),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Restricted
|
||||
);
|
||||
assert!(
|
||||
config.permissions.network.is_none(),
|
||||
"network_proxy should not start the managed network proxy while network access is off"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn network_proxy_feature_matrix_preserves_sandbox_network_semantics() -> std::io::Result<()> {
|
||||
#[derive(Clone, Copy)]
|
||||
enum Surface {
|
||||
PermissionProfile,
|
||||
LegacyWorkspaceWrite,
|
||||
}
|
||||
|
||||
struct Case {
|
||||
name: &'static str,
|
||||
surface: Surface,
|
||||
network_enabled: bool,
|
||||
proxy_enabled: bool,
|
||||
expected_network_policy: NetworkSandboxPolicy,
|
||||
}
|
||||
|
||||
let cases = [
|
||||
Case {
|
||||
name: "permission profile network disabled without proxy",
|
||||
surface: Surface::PermissionProfile,
|
||||
network_enabled: false,
|
||||
proxy_enabled: false,
|
||||
expected_network_policy: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
Case {
|
||||
name: "permission profile network disabled with proxy",
|
||||
surface: Surface::PermissionProfile,
|
||||
network_enabled: false,
|
||||
proxy_enabled: true,
|
||||
expected_network_policy: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
Case {
|
||||
name: "permission profile network enabled without proxy",
|
||||
surface: Surface::PermissionProfile,
|
||||
network_enabled: true,
|
||||
proxy_enabled: false,
|
||||
expected_network_policy: NetworkSandboxPolicy::Enabled,
|
||||
},
|
||||
Case {
|
||||
name: "permission profile network enabled with proxy",
|
||||
surface: Surface::PermissionProfile,
|
||||
network_enabled: true,
|
||||
proxy_enabled: true,
|
||||
expected_network_policy: NetworkSandboxPolicy::Enabled,
|
||||
},
|
||||
Case {
|
||||
name: "legacy workspace write network disabled without proxy",
|
||||
surface: Surface::LegacyWorkspaceWrite,
|
||||
network_enabled: false,
|
||||
proxy_enabled: false,
|
||||
expected_network_policy: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
Case {
|
||||
name: "legacy workspace write network disabled with proxy",
|
||||
surface: Surface::LegacyWorkspaceWrite,
|
||||
network_enabled: false,
|
||||
proxy_enabled: true,
|
||||
expected_network_policy: NetworkSandboxPolicy::Restricted,
|
||||
},
|
||||
Case {
|
||||
name: "legacy workspace write network enabled without proxy",
|
||||
surface: Surface::LegacyWorkspaceWrite,
|
||||
network_enabled: true,
|
||||
proxy_enabled: false,
|
||||
expected_network_policy: NetworkSandboxPolicy::Enabled,
|
||||
},
|
||||
Case {
|
||||
name: "legacy workspace write network enabled with proxy",
|
||||
surface: Surface::LegacyWorkspaceWrite,
|
||||
network_enabled: true,
|
||||
proxy_enabled: true,
|
||||
expected_network_policy: NetworkSandboxPolicy::Enabled,
|
||||
},
|
||||
];
|
||||
|
||||
for case in cases {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?;
|
||||
let features = case
|
||||
.proxy_enabled
|
||||
.then(|| toml::from_str("network_proxy = true").expect("valid features"));
|
||||
let base_config = match case.surface {
|
||||
Surface::PermissionProfile => ConfigToml {
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
||||
)]),
|
||||
}),
|
||||
network: Some(NetworkToml {
|
||||
enabled: Some(case.network_enabled),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
features,
|
||||
..Default::default()
|
||||
},
|
||||
Surface::LegacyWorkspaceWrite => ConfigToml {
|
||||
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
|
||||
sandbox_workspace_write: Some(SandboxWorkspaceWrite {
|
||||
network_access: case.network_enabled,
|
||||
..Default::default()
|
||||
}),
|
||||
windows: Some(WindowsToml {
|
||||
sandbox: Some(WindowsSandboxModeToml::Elevated),
|
||||
sandbox_private_desktop: None,
|
||||
}),
|
||||
features,
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
base_config,
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
case.expected_network_policy,
|
||||
"{}",
|
||||
case.name
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.network.is_some(),
|
||||
case.network_enabled && case.proxy_enabled,
|
||||
"{}",
|
||||
case.name
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn network_proxy_cli_overrides_merge_toggle_with_proxy_config() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
sandbox_mode = "workspace-write"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
|
||||
[windows]
|
||||
sandbox = "elevated"
|
||||
"#,
|
||||
)?;
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cli_overrides(vec![
|
||||
(
|
||||
"features.network_proxy.enabled".to_string(),
|
||||
toml::Value::Boolean(true),
|
||||
),
|
||||
(
|
||||
"features.network_proxy.enable_socks5".to_string(),
|
||||
toml::Value::Boolean(false),
|
||||
),
|
||||
])
|
||||
.harness_overrides(ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled
|
||||
@@ -826,11 +1000,187 @@ async fn permissions_profiles_proxy_policy_starts_managed_network_proxy() -> std
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.expect("profile proxy policy should start the managed network proxy");
|
||||
assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128");
|
||||
.expect("network_proxy should start the managed network proxy");
|
||||
assert_eq!(network.proxy_host_and_port(), "127.0.0.1:3128");
|
||||
assert!(!network.socks_enabled());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn experimental_network_requirements_enable_proxy_without_feature() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Ok(Some(codex_config::ConfigRequirementsToml {
|
||||
network: Some(codex_config::NetworkRequirementsToml {
|
||||
enabled: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}))
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert!(!config.features.enabled(Feature::NetworkProxy));
|
||||
assert!(config.managed_network_requirements_enabled());
|
||||
assert!(
|
||||
!network.socks_enabled(),
|
||||
"profile proxy policy should preserve SOCKS config"
|
||||
config
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.expect("experimental_network should configure the managed proxy")
|
||||
.enabled()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn network_proxy_feature_uses_profile_network_proxy_settings() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
features: Some(toml::from_str("network_proxy = true").expect("valid features")),
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
||||
)]),
|
||||
}),
|
||||
network: Some(NetworkToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
enable_socks5: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
config.permissions.network_sandbox_policy(),
|
||||
NetworkSandboxPolicy::Enabled
|
||||
);
|
||||
let network = config
|
||||
.permissions
|
||||
.network
|
||||
.as_ref()
|
||||
.expect("network_proxy should start the managed network proxy");
|
||||
assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128");
|
||||
assert!(!network.socks_enabled());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn profile_network_proxy_disable_ignores_base_feature_config() -> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
features: Some(
|
||||
toml::from_str(
|
||||
r#"
|
||||
[network_proxy]
|
||||
enabled = true
|
||||
proxy_url = "http://127.0.0.1:43128"
|
||||
"#,
|
||||
)
|
||||
.expect("valid base features"),
|
||||
),
|
||||
profiles: HashMap::from([(
|
||||
"no_proxy".to_string(),
|
||||
ConfigProfile {
|
||||
features: Some(
|
||||
toml::from_str("network_proxy = false").expect("valid profile features"),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
profile: Some("no_proxy".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(!config.features.enabled(Feature::NetworkProxy));
|
||||
assert!(config.permissions.network.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_network_proxy_feature_does_not_start_profile_proxy_policy() -> std::io::Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
let cwd = TempDir::new()?;
|
||||
let config = Config::load_from_base_config_with_overrides(
|
||||
ConfigToml {
|
||||
features: Some(
|
||||
toml::from_str(
|
||||
r#"
|
||||
[network_proxy]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.expect("valid features"),
|
||||
),
|
||||
default_permissions: Some("workspace".to_string()),
|
||||
permissions: Some(PermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"workspace".to_string(),
|
||||
PermissionProfileToml {
|
||||
filesystem: Some(FilesystemPermissionsToml {
|
||||
glob_scan_max_depth: None,
|
||||
entries: BTreeMap::from([(
|
||||
":minimal".to_string(),
|
||||
FilesystemPermissionToml::Access(FileSystemAccessMode::Read),
|
||||
)]),
|
||||
}),
|
||||
network: Some(NetworkToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
enable_socks5: Some(false),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
ConfigOverrides {
|
||||
cwd: Some(cwd.path().to_path_buf()),
|
||||
..Default::default()
|
||||
},
|
||||
codex_home.abs(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert!(!config.features.enabled(Feature::NetworkProxy));
|
||||
assert!(
|
||||
config.permissions.network.is_none(),
|
||||
"disabled feature should keep profile proxy policy from starting the managed proxy"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ use codex_features::FeatureToml;
|
||||
use codex_features::Features;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_features::MultiAgentV2ConfigToml;
|
||||
use codex_features::NetworkProxyConfigToml;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_login::AuthManagerConfig;
|
||||
use codex_mcp::McpConfig;
|
||||
@@ -112,6 +113,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::permissions::BUILT_IN_WORKSPACE_PROFILE;
|
||||
use crate::config::permissions::apply_network_proxy_feature_config;
|
||||
use crate::config::permissions::builtin_permission_profile;
|
||||
use crate::config::permissions::compile_permission_profile_selection;
|
||||
use crate::config::permissions::default_builtin_permission_profile_name;
|
||||
@@ -1954,6 +1956,13 @@ fn apps_mcp_path_override_toml_config(
|
||||
}
|
||||
}
|
||||
|
||||
fn network_proxy_toml_config(features: Option<&FeaturesToml>) -> Option<&NetworkProxyConfigToml> {
|
||||
match features?.network_proxy.as_ref()? {
|
||||
FeatureToml::Enabled(_) => None,
|
||||
FeatureToml::Config(config) => Some(config),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_web_search_mode_for_turn(
|
||||
web_search_mode: &Constrained<WebSearchMode>,
|
||||
permission_profile: &PermissionProfile,
|
||||
@@ -2137,6 +2146,7 @@ impl Config {
|
||||
feature_requirements,
|
||||
&mut startup_warnings,
|
||||
)?;
|
||||
let enable_network_proxy = features.enabled(Feature::NetworkProxy);
|
||||
let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile);
|
||||
let windows_sandbox_private_desktop =
|
||||
resolve_windows_sandbox_private_desktop(&cfg, &config_profile);
|
||||
@@ -2220,7 +2230,7 @@ impl Config {
|
||||
let using_implicit_builtin_profile =
|
||||
permission_config_syntax.is_none() && default_permissions.is_none();
|
||||
let (
|
||||
configured_network_proxy_config,
|
||||
mut configured_network_proxy_config,
|
||||
permission_profile,
|
||||
file_system_sandbox_policy,
|
||||
mut active_permission_profile,
|
||||
@@ -2433,6 +2443,22 @@ impl Config {
|
||||
None,
|
||||
)
|
||||
};
|
||||
if enable_network_proxy && permission_profile.network_sandbox_policy().is_enabled() {
|
||||
if let Some(network_proxy) = network_proxy_toml_config(cfg.features.as_ref()) {
|
||||
apply_network_proxy_feature_config(
|
||||
&mut configured_network_proxy_config,
|
||||
network_proxy,
|
||||
);
|
||||
}
|
||||
if let Some(network_proxy) = network_proxy_toml_config(config_profile.features.as_ref())
|
||||
{
|
||||
apply_network_proxy_feature_config(
|
||||
&mut configured_network_proxy_config,
|
||||
network_proxy,
|
||||
);
|
||||
}
|
||||
configured_network_proxy_config.network.enabled = true;
|
||||
}
|
||||
let approval_policy_was_explicit = approval_policy_override.is_some()
|
||||
|| config_profile.approval_policy.is_some()
|
||||
|| cfg.approval_policy.is_some();
|
||||
|
||||
@@ -6,10 +6,19 @@ use std::path::PathBuf;
|
||||
|
||||
use codex_config::permissions_toml::FilesystemPermissionToml;
|
||||
use codex_config::permissions_toml::FilesystemPermissionsToml;
|
||||
use codex_config::permissions_toml::NetworkDomainPermissionToml;
|
||||
use codex_config::permissions_toml::NetworkDomainPermissionsToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::NetworkUnixSocketPermissionToml;
|
||||
use codex_config::permissions_toml::NetworkUnixSocketPermissionsToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::types::SandboxWorkspaceWrite;
|
||||
use codex_features::NetworkProxyConfigToml;
|
||||
use codex_features::NetworkProxyDomainPermissionToml;
|
||||
use codex_features::NetworkProxyModeToml;
|
||||
use codex_features::NetworkProxyUnixSocketPermissionToml;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
#[cfg(test)]
|
||||
use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission;
|
||||
@@ -108,35 +117,70 @@ pub(crate) fn network_proxy_config_from_profile_network(
|
||||
NetworkProxyConfig::default,
|
||||
NetworkToml::to_network_proxy_config,
|
||||
);
|
||||
// Profile `network.enabled` controls sandbox network access. Do not start a
|
||||
// managed proxy for that bit alone, but keep the proxy enabled when the
|
||||
// profile also supplied policy that only the proxy can enforce.
|
||||
config.network.enabled = network.is_some_and(profile_network_requires_proxy);
|
||||
// Profile `network.enabled` controls sandbox network access. Profiles may
|
||||
// provide proxy settings for the feature gate to consume when that network
|
||||
// access is enabled, but they do not start the managed proxy on their own.
|
||||
config.network.enabled = false;
|
||||
config
|
||||
}
|
||||
|
||||
fn profile_network_requires_proxy(network: &NetworkToml) -> bool {
|
||||
if network.enabled != Some(true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
network.proxy_url.is_some()
|
||||
|| network.enable_socks5 == Some(true)
|
||||
|| network.socks_url.is_some()
|
||||
|| network.enable_socks5_udp == Some(true)
|
||||
|| network.allow_upstream_proxy == Some(true)
|
||||
|| network.dangerously_allow_non_loopback_proxy == Some(true)
|
||||
|| network.dangerously_allow_all_unix_sockets == Some(true)
|
||||
|| network.mode.is_some()
|
||||
|| network
|
||||
pub(crate) fn apply_network_proxy_feature_config(
|
||||
config: &mut NetworkProxyConfig,
|
||||
feature_config: &NetworkProxyConfigToml,
|
||||
) {
|
||||
NetworkToml {
|
||||
enabled: feature_config.enabled,
|
||||
proxy_url: feature_config.proxy_url.clone(),
|
||||
enable_socks5: feature_config.enable_socks5,
|
||||
socks_url: feature_config.socks_url.clone(),
|
||||
enable_socks5_udp: feature_config.enable_socks5_udp,
|
||||
allow_upstream_proxy: feature_config.allow_upstream_proxy,
|
||||
dangerously_allow_non_loopback_proxy: feature_config.dangerously_allow_non_loopback_proxy,
|
||||
dangerously_allow_all_unix_sockets: feature_config.dangerously_allow_all_unix_sockets,
|
||||
mode: feature_config.mode.map(|mode| match mode {
|
||||
NetworkProxyModeToml::Limited => NetworkMode::Limited,
|
||||
NetworkProxyModeToml::Full => NetworkMode::Full,
|
||||
}),
|
||||
domains: feature_config
|
||||
.domains
|
||||
.as_ref()
|
||||
.is_some_and(|domains| !domains.is_empty())
|
||||
|| network
|
||||
.unix_sockets
|
||||
.as_ref()
|
||||
.is_some_and(|unix_sockets| !unix_sockets.is_empty())
|
||||
|| network.allow_local_binding == Some(true)
|
||||
.map(|domains| NetworkDomainPermissionsToml {
|
||||
entries: domains
|
||||
.iter()
|
||||
.map(|(pattern, permission)| {
|
||||
let permission = match permission {
|
||||
NetworkProxyDomainPermissionToml::Allow => {
|
||||
NetworkDomainPermissionToml::Allow
|
||||
}
|
||||
NetworkProxyDomainPermissionToml::Deny => {
|
||||
NetworkDomainPermissionToml::Deny
|
||||
}
|
||||
};
|
||||
(pattern.clone(), permission)
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
unix_sockets: feature_config.unix_sockets.as_ref().map(|unix_sockets| {
|
||||
NetworkUnixSocketPermissionsToml {
|
||||
entries: unix_sockets
|
||||
.iter()
|
||||
.map(|(path, permission)| {
|
||||
let permission = match permission {
|
||||
NetworkProxyUnixSocketPermissionToml::Allow => {
|
||||
NetworkUnixSocketPermissionToml::Allow
|
||||
}
|
||||
NetworkProxyUnixSocketPermissionToml::None => {
|
||||
NetworkUnixSocketPermissionToml::None
|
||||
}
|
||||
};
|
||||
(path.clone(), permission)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}),
|
||||
allow_local_binding: feature_config.allow_local_binding,
|
||||
}
|
||||
.apply_to_network_proxy_config(config);
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_permission_profile<'a>(
|
||||
|
||||
@@ -247,7 +247,7 @@ fn profile_network_proxy_config_keeps_proxy_disabled_for_bare_network_access() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_network_proxy_config_enables_proxy_for_proxy_policy() {
|
||||
fn profile_network_proxy_config_keeps_proxy_disabled_for_proxy_policy() {
|
||||
let config = network_proxy_config_from_profile_network(Some(&NetworkToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
@@ -261,7 +261,7 @@ fn profile_network_proxy_config_enables_proxy_for_proxy_policy() {
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
assert!(config.network.enabled);
|
||||
assert!(!config.network.enabled);
|
||||
assert_eq!(config.network.proxy_url, "http://127.0.0.1:43128");
|
||||
assert!(!config.network.enable_socks5);
|
||||
assert_eq!(
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::assert_regex_match;
|
||||
use core_test_support::managed_network_requirements_loader;
|
||||
use core_test_support::process::process_is_alive;
|
||||
use core_test_support::process::wait_for_pid_file;
|
||||
use core_test_support::process::wait_for_process_exit;
|
||||
@@ -872,13 +873,7 @@ async fn unified_exec_short_lived_network_denial_emits_failed_end_event() -> Res
|
||||
async fn unified_exec_network_denial_test(
|
||||
server: &wiremock::MockServer,
|
||||
) -> Result<(TestCodex, SandboxPolicy)> {
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::Constrained;
|
||||
use codex_config::NetworkConstraints;
|
||||
use codex_config::NetworkRequirementsToml;
|
||||
use codex_config::RequirementSource;
|
||||
use codex_config::Sourced;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -901,46 +896,20 @@ allow_local_binding = true
|
||||
*network_access = true;
|
||||
}
|
||||
let sandbox_policy_for_config = sandbox_policy.clone();
|
||||
let mut builder = test_codex().with_home(home).with_config(move |config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow feature update");
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never);
|
||||
config.permissions.permission_profile = Constrained::allow_any(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy_for_config),
|
||||
);
|
||||
let layers = config
|
||||
.config_layer_stack
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut requirements = config.config_layer_stack.requirements().clone();
|
||||
requirements.network = Some(Sourced::new(
|
||||
NetworkConstraints {
|
||||
enabled: Some(true),
|
||||
allow_local_binding: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
RequirementSource::CloudRequirements,
|
||||
));
|
||||
let mut requirements_toml = config.config_layer_stack.requirements_toml().clone();
|
||||
requirements_toml.network = Some(NetworkRequirementsToml {
|
||||
enabled: Some(true),
|
||||
allow_local_binding: Some(true),
|
||||
..Default::default()
|
||||
let mut builder = test_codex()
|
||||
.with_home(home)
|
||||
.with_cloud_requirements(managed_network_requirements_loader())
|
||||
.with_config(move |config| {
|
||||
config.use_experimental_unified_exec_tool = true;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::UnifiedExec)
|
||||
.expect("test config should allow feature update");
|
||||
config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never);
|
||||
config.permissions.permission_profile = Constrained::allow_any(
|
||||
PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy_for_config),
|
||||
);
|
||||
});
|
||||
config.config_layer_stack =
|
||||
match ConfigLayerStack::new(layers, requirements, requirements_toml) {
|
||||
Ok(stack) => stack,
|
||||
Err(err) => panic!("rebuild config layer stack with network requirements: {err}"),
|
||||
};
|
||||
});
|
||||
let test = builder.build_remote_aware(server).await?;
|
||||
assert!(
|
||||
test.config.permissions.network.is_some(),
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::FeatureConfig;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -54,3 +55,63 @@ impl FeatureConfig for AppsMcpPathOverrideConfigToml {
|
||||
self.enabled = Some(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NetworkProxyConfigToml {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub proxy_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_socks5: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub socks_url: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_socks5_udp: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mode: Option<NetworkProxyModeToml>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub domains: Option<BTreeMap<String, NetworkProxyDomainPermissionToml>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unix_sockets: Option<BTreeMap<String, NetworkProxyUnixSocketPermissionToml>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub allow_local_binding: Option<bool>,
|
||||
}
|
||||
|
||||
impl FeatureConfig for NetworkProxyConfigToml {
|
||||
fn enabled(&self) -> Option<bool> {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = Some(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkProxyModeToml {
|
||||
Limited,
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkProxyDomainPermissionToml {
|
||||
Allow,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkProxyUnixSocketPermissionToml {
|
||||
Allow,
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ mod feature_configs;
|
||||
mod legacy;
|
||||
pub use feature_configs::AppsMcpPathOverrideConfigToml;
|
||||
pub use feature_configs::MultiAgentV2ConfigToml;
|
||||
pub use feature_configs::NetworkProxyConfigToml;
|
||||
pub use feature_configs::NetworkProxyDomainPermissionToml;
|
||||
pub use feature_configs::NetworkProxyModeToml;
|
||||
pub use feature_configs::NetworkProxyUnixSocketPermissionToml;
|
||||
use legacy::LegacyFeatureToggles;
|
||||
pub use legacy::legacy_feature_keys;
|
||||
|
||||
@@ -140,6 +144,8 @@ pub enum Feature {
|
||||
ChildAgentsMd,
|
||||
/// Compress request bodies (zstd) when sending streaming requests to codex-backend.
|
||||
EnableRequestCompression,
|
||||
/// Start the managed network proxy for sandboxed sessions.
|
||||
NetworkProxy,
|
||||
/// Enable collab tools.
|
||||
Collab,
|
||||
/// Enable task-path-based multi-agent routing.
|
||||
@@ -244,6 +250,13 @@ impl Feature {
|
||||
self.info().default_enabled
|
||||
}
|
||||
|
||||
pub fn uses_config_table(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Feature::MultiAgentV2 | Feature::AppsMcpPathOverride | Feature::NetworkProxy
|
||||
)
|
||||
}
|
||||
|
||||
fn info(self) -> &'static FeatureSpec {
|
||||
FEATURES
|
||||
.iter()
|
||||
@@ -559,6 +572,15 @@ pub fn is_known_feature_key(key: &str) -> bool {
|
||||
feature_for_key(key).is_some()
|
||||
}
|
||||
|
||||
pub fn feature_toggle_override_key(key: &str) -> Option<String> {
|
||||
let feature = feature_for_key(key)?;
|
||||
if feature.uses_config_table() {
|
||||
Some(format!("features.{}.enabled", feature.key()))
|
||||
} else {
|
||||
Some(format!("features.{key}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializable features table for TOML.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema)]
|
||||
pub struct FeaturesToml {
|
||||
@@ -566,6 +588,7 @@ pub struct FeaturesToml {
|
||||
pub multi_agent_v2: Option<FeatureToml<MultiAgentV2ConfigToml>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub apps_mcp_path_override: Option<FeatureToml<AppsMcpPathOverrideConfigToml>>,
|
||||
pub network_proxy: Option<FeatureToml<NetworkProxyConfigToml>>,
|
||||
/// Boolean feature toggles keyed by canonical or legacy feature name.
|
||||
#[serde(flatten)]
|
||||
entries: BTreeMap<String, bool>,
|
||||
@@ -591,6 +614,9 @@ impl FeaturesToml {
|
||||
{
|
||||
entries.insert(Feature::AppsMcpPathOverride.key().to_string(), enabled);
|
||||
}
|
||||
if let Some(enabled) = self.network_proxy.as_ref().and_then(FeatureToml::enabled) {
|
||||
entries.insert(Feature::NetworkProxy.key().to_string(), enabled);
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
@@ -598,6 +624,7 @@ impl FeaturesToml {
|
||||
let Self {
|
||||
multi_agent_v2,
|
||||
apps_mcp_path_override,
|
||||
network_proxy,
|
||||
entries,
|
||||
} = self;
|
||||
for key in legacy::legacy_feature_keys() {
|
||||
@@ -609,6 +636,8 @@ impl FeaturesToml {
|
||||
materialize_resolved_feature_enabled(multi_agent_v2, enabled);
|
||||
} else if spec.id == Feature::AppsMcpPathOverride {
|
||||
materialize_resolved_feature_enabled(apps_mcp_path_override, enabled);
|
||||
} else if spec.id == Feature::NetworkProxy {
|
||||
materialize_resolved_feature_enabled(network_proxy, enabled);
|
||||
} else {
|
||||
entries.insert(spec.key.to_string(), enabled);
|
||||
}
|
||||
@@ -873,6 +902,16 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::Stable,
|
||||
default_enabled: true,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::NetworkProxy,
|
||||
key: "network_proxy",
|
||||
stage: Stage::Experimental {
|
||||
name: "Network proxy",
|
||||
menu_description: "Start Codex's managed network proxy for sandboxed sessions. The active permissions profile still controls direct network access.",
|
||||
announcement: "NEW: Network proxy can now be enabled from /experimental. Restart Codex after enabling it.",
|
||||
},
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Collab,
|
||||
key: "multi_agent",
|
||||
|
||||
@@ -138,6 +138,19 @@ fn tool_suggest_is_stable_and_enabled_by_default() {
|
||||
assert_eq!(Feature::ToolSuggest.default_enabled(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_proxy_is_experimental_and_disabled_by_default() {
|
||||
assert_eq!(
|
||||
feature_for_key("network_proxy"),
|
||||
Some(Feature::NetworkProxy)
|
||||
);
|
||||
assert!(matches!(
|
||||
Feature::NetworkProxy.stage(),
|
||||
Stage::Experimental { .. }
|
||||
));
|
||||
assert_eq!(Feature::NetworkProxy.default_enabled(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_search_is_stable_and_enabled_by_default() {
|
||||
assert_eq!(Feature::ToolSearch.stage(), Stage::Stable);
|
||||
@@ -495,6 +508,7 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CodeMode);
|
||||
features.enable(Feature::MultiAgentV2);
|
||||
features.enable(Feature::NetworkProxy);
|
||||
features.disable(Feature::ToolSearch);
|
||||
|
||||
let mut features_toml = FeaturesToml {
|
||||
@@ -503,6 +517,11 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
|
||||
min_wait_timeout_ms: Some(2500),
|
||||
..Default::default()
|
||||
})),
|
||||
network_proxy: Some(FeatureToml::Config(crate::NetworkProxyConfigToml {
|
||||
enabled: Some(false),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
entries: BTreeMap::from([("include_apply_patch_tool".to_string(), true)]),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -527,6 +546,14 @@ fn materialize_resolved_enabled_writes_all_features_and_preserves_custom_config(
|
||||
..Default::default()
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
features_toml.network_proxy,
|
||||
Some(FeatureToml::Config(crate::NetworkProxyConfigToml {
|
||||
enabled: Some(true),
|
||||
proxy_url: Some("http://127.0.0.1:43128".to_string()),
|
||||
..Default::default()
|
||||
}))
|
||||
);
|
||||
let replayed = Features::from_sources(
|
||||
FeatureConfigSource {
|
||||
features: Some(&features_toml),
|
||||
|
||||
Reference in New Issue
Block a user