Add credential brokering to network proxy feature config (#40466)

## What changed

- Add `features.network_proxy.credential_broker` and preserve structured network proxy settings when toggling the feature through config APIs.
- Protect broker enablement and provider context variables from project config, while carrying trusted context into filtered shell environments.
- Propagate brokered credentials through shell snapshots and restore real credentials for unsandboxed escalated commands.
- Normalize broker context environment keys case-insensitively on Windows and avoid conflicting remote managed-network enforcement when no proxy launch configuration is available.

## Testing

- Cover config layering, editing, project sanitization, Windows environment handling, shell snapshots, escalated execution, and end-to-end GitHub credential brokering in the zsh-fork runtime.

GitOrigin-RevId: 500e51e804e1988022052ccfe00b8fac5e495f4f
This commit is contained in:
Winston Howes authored and copyberry committed 2026-08-24 19:52:27 +00:00
1 parent c2904b9c5a
commit c3d5e4418f
27 files changed
+1000 -92

No files matched your search

@@ -3713,6 +3713,10 @@ experimental_realtime_ws_base_url = "wss://attacker.example/realtime"
[features]
respect_system_proxy = true
[features.network_proxy]
enabled = true
credential_broker = true
[otel]
environment = "attacker"
@@ -3737,6 +3741,12 @@ wire_api = "responses"
/*project_root_markers*/ None,
)
.await?;
let managed_config_path = tmp.path().join("managed_config.toml");
tokio::fs::write(
&managed_config_path,
"[features.network_proxy]\nenabled = false\ncredential_broker = true\n",
)
.await?;
let cwd = AbsolutePathBuf::from_absolute_path(&project_root)?;
let layers = load_config_layers_state(
@@ -3744,7 +3754,7 @@ wire_api = "responses"
&codex_home,
Some(cwd),
&[] as &[(String, TomlValue)],
LoaderOverrides::default(),
LoaderOverrides::with_managed_config_path_for_tests(managed_config_path),
&codex_config::NoopThreadConfigLoader,
)
.await?;
@@ -3767,6 +3777,8 @@ wire_api = "responses"
"experimental_realtime_ws_base_url",
"otel",
"features.respect_system_proxy",
"features.network_proxy.credential_broker",
"features.network_proxy.enabled",
];
let expected_startup_warnings = vec![format!(
concat!(
@@ -3783,6 +3795,14 @@ wire_api = "responses"
);
let effective_config = layers.effective_config();
assert_eq!(
effective_config
.get("features")
.and_then(|features| features.get("network_proxy"))
.and_then(|network_proxy| network_proxy.get("enabled"))
.and_then(TomlValue::as_bool),
Some(false)
);
assert_eq!(
effective_config.get("model"),
Some(&TomlValue::String("project-model".to_string()))
+23 -13
View File
@@ -2,6 +2,7 @@ use crate::path_utils::resolve_symlink_write_paths;
use crate::path_utils::write_atomically;
use anyhow::Context;
use codex_config::CONFIG_TOML_FILE;
use codex_config::is_structured_feature_path;
use codex_config::types::McpServerConfig;
use codex_config::types::ResumeCwdMode;
use codex_config::types::SessionPickerViewMode;
@@ -323,7 +324,7 @@ impl ConfigDocument {
Ok(self.set_skill_config(SkillConfigSelector::Name(name.clone()), *enabled))
}
ConfigEdit::SetPath { segments, value } => {
if is_multi_agent_v2_feature_path(segments) && value.as_bool().is_some() {
if is_structured_feature_path(segments) && value.as_bool().is_some() {
let mut existing = Some(self.doc.as_item());
for segment in segments {
existing = existing.and_then(|item| item.as_table_like()?.get(segment));
@@ -336,7 +337,26 @@ impl ConfigDocument {
}
Ok(self.insert(segments, value.clone()))
}
ConfigEdit::ClearPath { segments } => Ok(self.clear_owned(segments)),
ConfigEdit::ClearPath { segments } => {
let preserves_broker_settings = is_structured_feature_path(segments)
&& segments
.last()
.is_some_and(|feature| feature == "network_proxy")
&& segments
.iter()
.try_fold(self.doc.as_item(), |item, segment| {
item.as_table_like()?.get(segment)
})
.and_then(TomlItem::as_table_like)
.is_some_and(|feature| feature.contains_key("credential_broker"));
if preserves_broker_settings {
let mut enabled_segments = segments.clone();
enabled_segments.push("enabled".to_string());
Ok(self.insert(&enabled_segments, value(false)))
} else {
Ok(self.clear_owned(segments))
}
}
ConfigEdit::SetProjectTrustLevel { path, level } => {
// Delegate to the existing, tested logic in config.rs to
// ensure tables are explicit and migration is preserved.
@@ -618,7 +638,7 @@ impl ConfigDocument {
}
let item = current.get_mut(segment.as_str())?;
if is_multi_agent_v2_feature_path(&segments[..=index])
if is_structured_feature_path(&segments[..=index])
&& let Some(enabled) = item.as_bool()
{
let mut feature = document_helpers::new_implicit_table();
@@ -669,16 +689,6 @@ impl ConfigDocument {
}
}
fn is_multi_agent_v2_feature_path(segments: &[String]) -> bool {
match segments {
[features, feature] => features == "features" && feature == "multi_agent_v2",
[profiles, _, features, feature] => {
profiles == "profiles" && features == "features" && feature == "multi_agent_v2"
}
_ => false,
}
}
fn normalize_skill_config_path(path: &Path) -> String {
dunce::canonicalize(path)
.unwrap_or_else(|_| path.to_path_buf())
+31
View File
@@ -136,6 +136,37 @@ fn multi_agent_v2_feature_toggle_preserves_nested_configuration() {
);
}
#[test]
fn network_proxy_feature_toggle_preserves_credential_broker_configuration() {
let tmp = tempdir().expect("tmpdir");
let codex_home = tmp.path();
let config_path = codex_home.join(CONFIG_TOML_FILE);
for (initial, disabled, enabled_again) in [
(
"[features]\nnetwork_proxy = true\n",
"[features]\n",
"[features]\nnetwork_proxy = true\n",
),
(
"[features.network_proxy]\nenabled = true\ncredential_broker = true\ndomains = { \"github.com\" = \"allow\" }\n",
"[features.network_proxy]\nenabled = false\ncredential_broker = true\ndomains = { \"github.com\" = \"allow\" }\n",
"[features.network_proxy]\nenabled = true\ncredential_broker = true\ndomains = { \"github.com\" = \"allow\" }\n",
),
] {
std::fs::write(&config_path, initial).expect("write config");
for (enabled, expected) in [(false, disabled), (true, enabled_again)] {
ConfigEditsBuilder::new(codex_home)
.set_feature_enabled("network_proxy", enabled)
.apply_blocking()
.expect("toggle feature");
let updated: TomlValue =
toml::from_str(&std::fs::read_to_string(&config_path).expect("read config"))
.expect("parse config");
assert_eq!(updated, toml::from_str::<TomlValue>(expected).unwrap());
}
}
}
/// Adding nested multi-agent settings must retain an existing legacy boolean toggle.
#[test]
fn multi_agent_v2_nested_edit_preserves_legacy_boolean_toggle() {
+4
View File
@@ -3602,6 +3602,8 @@ impl Config {
network_proxy,
);
}
configured_network_proxy_config
.set_credential_broker_openai_base_url(cfg.openai_base_url.as_deref());
configured_network_proxy_config.enabled = true;
}
if cfg.approval_policy == Some(AskForApproval::UnlessTrusted) {
@@ -4475,6 +4477,8 @@ impl Config {
network_proxy,
);
}
configured_network_proxy_config
.set_credential_broker_openai_base_url(cfg.openai_base_url.as_deref());
configured_network_proxy_config.enabled = true;
}
configured_network_proxy_config
+3
View File
@@ -189,6 +189,9 @@ pub(crate) fn apply_network_proxy_feature_config(
mitm: None,
}
.apply_to_network_proxy_config(config);
if let Some(credential_broker) = feature_config.credential_broker {
config.set_credential_broker_enabled(credential_broker);
}
}
pub(crate) fn resolve_permission_profile(