Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/network-proxy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ codex-utils-absolute-path = { workspace = true }
codex-utils-home-dir = { workspace = true }
codex-utils-rustls-provider = { workspace = true }
globset = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
Expand Down
17 changes: 17 additions & 0 deletions codex-rs/network-proxy/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ pub struct NetworkProxyConfig {
pub network: NetworkProxySettings,
}

impl NetworkProxyConfig {
pub fn set_credential_broker_enabled(&mut self, enabled: bool) {
self.network.credential_broker = enabled;
self.network.mitm |= enabled;
}
}

/// Variant order encodes effective precedence for duplicate patterns:
/// `None < Allow < Deny`, so deny wins over allow when entries conflict.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
Expand Down Expand Up @@ -142,6 +149,10 @@ pub struct NetworkProxySettings {
#[serde(default)]
pub mitm: bool,
#[serde(default)]
pub credential_broker: bool,
#[serde(default)]
pub dangerously_allow_plaintext_credential_injection: bool,
#[serde(default)]
pub mitm_hooks: Vec<MitmHookConfig>,
}

Expand All @@ -161,6 +172,8 @@ impl Default for NetworkProxySettings {
unix_sockets: None,
allow_local_binding: false,
mitm: false,
credential_broker: false,
dangerously_allow_plaintext_credential_injection: false,
mitm_hooks: Vec::new(),
}
}
Expand Down Expand Up @@ -593,6 +606,8 @@ mod tests {
unix_sockets: None,
allow_local_binding: false,
mitm: false,
credential_broker: false,
dangerously_allow_plaintext_credential_injection: false,
mitm_hooks: Vec::new(),
}
);
Expand Down Expand Up @@ -658,6 +673,8 @@ mod tests {
"unix_sockets": null,
"allow_local_binding": false,
"mitm": false,
"credential_broker": false,
"dangerously_allow_plaintext_credential_injection": false,
"mitm_hooks": [],
}
})
Expand Down
270 changes: 270 additions & 0 deletions codex-rs/network-proxy/src/credential_broker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
mod providers;

use crate::policy::normalize_host;
use rama_http::HeaderMap;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;

pub const CREDENTIAL_BROKER_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_CREDENTIAL_BROKER_ACTIVE";
pub(crate) const BROKERED_CREDENTIALS_ENV_KEY: &str = "CODEX_NETWORK_PROXY_BROKERED_CREDENTIALS";

#[derive(Clone)]
pub(crate) struct CredentialBroker {
state: Arc<RwLock<CredentialBrokerState>>,
}

#[derive(Default)]
struct CredentialBrokerState {
enabled: bool,
credentials: Vec<CredentialRecord>,
}

struct CredentialRecord {
env_var: String,
provider: &'static providers::CredentialProvider,
host_binding: providers::CredentialHostBinding,
real_value: String,
dummy_value: String,
}

impl CredentialBroker {
pub(crate) fn new(enabled: bool) -> Self {
Self {
state: Arc::new(RwLock::new(CredentialBrokerState {
enabled,
..CredentialBrokerState::default()
})),
}
}

pub(crate) fn enabled(&self) -> bool {
self.read_state().enabled
}

pub(crate) fn virtualize_child_env(&self, env: &mut HashMap<String, String>) {
let mut state = self.write_state();
if !state.enabled {
env.remove(CREDENTIAL_BROKER_ACTIVE_ENV_KEY);
env.remove(BROKERED_CREDENTIALS_ENV_KEY);
return;
}
env.insert(
CREDENTIAL_BROKER_ACTIVE_ENV_KEY.to_string(),
"1".to_string(),
);

for provider in providers::credential_providers() {
for source in provider.sources() {
if let Some(host_binding) = (source.host_binding)(env) {
for env_var in source.env_vars {
virtualize_env_var(
env,
&mut state,
env_var,
provider,
host_binding.clone(),
);
}
}
}
}
update_brokered_credentials_marker(&state, env);
}

pub(crate) fn host_requires_mitm(&self, host: &str) -> bool {
let normalized_host = normalize_host(host);
let state = self.read_state();
state.enabled
&& state
.credentials
.iter()
.any(|credential| credential.matches_host(&normalized_host))
}

pub(crate) fn inject_request_headers(&self, host: &str, headers: &mut HeaderMap) {
let normalized_host = normalize_host(host);
let state = self.read_state();
if !state.enabled {
return;
}

let matching_credentials = state
.credentials
.iter()
.filter(|credential| credential.matches_host(&normalized_host))
.collect::<Vec<_>>();
let Some(credential) = select_credential(headers, &matching_credentials) else {
return;
};
let Some(header_value) = credential
.provider
.request_header_value(&credential.real_value)
else {
return;
};
credential
.provider
.insert_request_header(headers, header_value);
}

fn read_state(&self) -> std::sync::RwLockReadGuard<'_, CredentialBrokerState> {
self.state
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}

fn write_state(&self) -> std::sync::RwLockWriteGuard<'_, CredentialBrokerState> {
self.state
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}

fn virtualize_env_var(
env: &mut HashMap<String, String>,
state: &mut CredentialBrokerState,
env_var: &str,
provider: &'static providers::CredentialProvider,
host_binding: providers::CredentialHostBinding,
) {
let Some(real_value) = brokerable_credential_value(env, state, env_var, provider) else {
return;
};

let dummy_value = state.register(env_var, provider, host_binding, real_value);
env.insert(env_var.to_string(), dummy_value);
}

fn brokerable_credential_value<'a>(
env: &'a HashMap<String, String>,
state: &CredentialBrokerState,
env_var: &str,
provider: &providers::CredentialProvider,
) -> Option<&'a str> {
let real_value = env.get(env_var)?.trim();
(!real_value.is_empty()
&& !state.is_dummy_value(real_value)
&& provider.request_header_value(real_value).is_some())
.then_some(real_value)
}

impl CredentialBrokerState {
fn register(
&mut self,
env_var: &str,
provider: &'static providers::CredentialProvider,
host_binding: providers::CredentialHostBinding,
real_value: &str,
) -> String {
if let Some(existing) = self.credentials.iter().find(|credential| {
credential.env_var == env_var
&& std::ptr::eq(credential.provider, provider)
&& credential.host_binding == host_binding
&& credential.real_value == real_value
}) {
return existing.dummy_value.clone();
}

let dummy_value = loop {
let candidate = provider.dummy_value(real_value);
if candidate != real_value && !self.is_dummy_value(&candidate) {
break candidate;
}
};
self.credentials.push(CredentialRecord {
env_var: env_var.to_string(),
provider,
host_binding,
real_value: real_value.to_string(),
dummy_value: dummy_value.clone(),
});
dummy_value
}

fn is_dummy_value(&self, value: &str) -> bool {
self.credentials
.iter()
.any(|credential| credential.dummy_value == value)
}
}

impl CredentialRecord {
fn matches_host(&self, host: &str) -> bool {
self.host_binding.matches_host(host)
}
}

fn select_credential<'a>(
headers: &HeaderMap,
matching_credentials: &[&'a CredentialRecord],
) -> Option<&'a CredentialRecord> {
let dummy_matches = matching_credentials
.iter()
.copied()
.filter(|credential| {
credential
.provider
.request_header(headers)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.contains(&credential.dummy_value))
})
.collect::<Vec<_>>();
match dummy_matches.as_slice() {
[credential] => Some(*credential),
[] | [_, _, ..] => None,
}
}

fn update_brokered_credentials_marker(
state: &CredentialBrokerState,
env: &mut HashMap<String, String>,
) {
let brokered = providers::credential_broker_env_keys()
.filter_map(|key| {
let value = env.get(key)?;
state.is_dummy_value(value).then_some((key, value.as_str()))
})
.collect::<Vec<_>>();
match serde_json::to_string(&brokered) {
Ok(marker) => {
env.insert(BROKERED_CREDENTIALS_ENV_KEY.to_string(), marker);
}
Err(_) => {
env.remove(BROKERED_CREDENTIALS_ENV_KEY);
}
}
}

/// Returns supported environment keys whose current values still match the child-scoped dummy
/// values recorded by the credential broker.
///
/// The broker marker is treated as untrusted: malformed metadata, unsupported keys, and values
/// replaced by the user are ignored. The environment is not mutated; callers own the decision to
/// remove the returned keys.
pub fn brokered_credential_dummy_env_keys(env: &HashMap<String, String>) -> Vec<String> {
env.get(BROKERED_CREDENTIALS_ENV_KEY)
.and_then(|marker| serde_json::from_str::<Vec<(String, String)>>(marker).ok())
.unwrap_or_default()
.into_iter()
.filter_map(|(key, dummy_value)| {
(providers::credential_broker_env_keys().any(|candidate| candidate == key.as_str())
&& env.get(&key) == Some(&dummy_value))
.then_some(key)
})
.collect()
}

/// Returns supported credential keys only for an environment with an active broker.
pub fn brokered_credential_env_keys(
env: &HashMap<String, String>,
) -> impl Iterator<Item = &'static str> {
let active = env
.get(CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
.is_some_and(|value| value == "1");
providers::credential_broker_env_keys().filter(move |_| active)
}

#[cfg(test)]
#[path = "credential_broker_tests.rs"]
mod tests;
Loading
Loading