mirror of
https://github.com/openai/codex.git
synced 2026-05-15 00:32:51 +00:00
Add MITM hook config model
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use codex_network_proxy::MitmHookConfig;
|
||||
use codex_network_proxy::NetworkDomainPermission as ProxyNetworkDomainPermission;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
@@ -158,6 +159,9 @@ pub struct NetworkToml {
|
||||
pub domains: Option<NetworkDomainPermissionsToml>,
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
pub mitm: Option<bool>,
|
||||
#[schemars(with = "Option<Vec<MitmHookConfigSchema>>")]
|
||||
pub mitm_hooks: Option<Vec<MitmHookConfig>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -167,6 +171,46 @@ enum NetworkModeSchema {
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
struct MitmHookConfigSchema {
|
||||
pub host: String,
|
||||
#[serde(rename = "match", default)]
|
||||
pub matcher: MitmHookMatchConfigSchema,
|
||||
#[serde(default)]
|
||||
pub actions: MitmHookActionsConfigSchema,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
struct MitmHookMatchConfigSchema {
|
||||
pub methods: Vec<String>,
|
||||
pub path_prefixes: Vec<String>,
|
||||
pub query: BTreeMap<String, Vec<String>>,
|
||||
pub headers: BTreeMap<String, Vec<String>>,
|
||||
pub body: Option<MitmHookBodyConfigSchema>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
struct MitmHookActionsConfigSchema {
|
||||
pub strip_request_headers: Vec<String>,
|
||||
pub inject_request_headers: Vec<InjectedHeaderConfigSchema>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
struct InjectedHeaderConfigSchema {
|
||||
pub name: String,
|
||||
pub secret_env_var: Option<String>,
|
||||
pub secret_file: Option<String>,
|
||||
pub prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
struct MitmHookBodyConfigSchema(pub serde_json::Value);
|
||||
|
||||
impl NetworkToml {
|
||||
pub fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) {
|
||||
if let Some(enabled) = self.enabled {
|
||||
@@ -219,6 +263,12 @@ impl NetworkToml {
|
||||
if let Some(allow_local_binding) = self.allow_local_binding {
|
||||
config.network.allow_local_binding = allow_local_binding;
|
||||
}
|
||||
if let Some(mitm) = self.mitm {
|
||||
config.network.mitm = mitm;
|
||||
}
|
||||
if let Some(mitm_hooks) = self.mitm_hooks.as_ref() {
|
||||
config.network.mitm_hooks = mitm_hooks.clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_network_proxy_config(&self) -> NetworkProxyConfig {
|
||||
|
||||
@@ -1098,6 +1098,27 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"InjectedHeaderConfigSchema": {
|
||||
"properties": {
|
||||
"name": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"prefix": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
},
|
||||
"secret_env_var": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
},
|
||||
"secret_file": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"KeybindingsSpec": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -1278,6 +1299,101 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MitmHookActionsConfigSchema": {
|
||||
"properties": {
|
||||
"inject_request_headers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/InjectedHeaderConfigSchema"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"strip_request_headers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MitmHookConfigSchema": {
|
||||
"properties": {
|
||||
"actions": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MitmHookActionsConfigSchema"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"inject_request_headers": [],
|
||||
"strip_request_headers": []
|
||||
}
|
||||
},
|
||||
"host": {
|
||||
"default": "",
|
||||
"type": "string"
|
||||
},
|
||||
"match": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/MitmHookMatchConfigSchema"
|
||||
}
|
||||
],
|
||||
"default": {
|
||||
"body": null,
|
||||
"headers": {},
|
||||
"methods": [],
|
||||
"path_prefixes": [],
|
||||
"query": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"MitmHookMatchConfigSchema": {
|
||||
"properties": {
|
||||
"body": {
|
||||
"default": null
|
||||
},
|
||||
"headers": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {},
|
||||
"type": "object"
|
||||
},
|
||||
"methods": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"path_prefixes": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"query": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ModelAvailabilityNuxConfig": {
|
||||
"additionalProperties": {
|
||||
"format": "uint32",
|
||||
@@ -1616,6 +1732,15 @@
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mitm": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mitm_hooks": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/MitmHookConfigSchema"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"mode": {
|
||||
"$ref": "#/definitions/NetworkModeSchema"
|
||||
},
|
||||
|
||||
@@ -66,6 +66,11 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID;
|
||||
use codex_model_provider_info::WireApi;
|
||||
use codex_models_manager::bundled_models_response;
|
||||
use codex_network_proxy::InjectedHeaderConfig;
|
||||
use codex_network_proxy::MitmHookActionsConfig;
|
||||
use codex_network_proxy::MitmHookConfig;
|
||||
use codex_network_proxy::MitmHookMatchConfig;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
use codex_protocol::models::ActivePermissionProfile;
|
||||
use codex_protocol::models::ActivePermissionProfileModification;
|
||||
@@ -727,9 +732,29 @@ enabled = true
|
||||
proxy_url = "http://127.0.0.1:43128"
|
||||
enable_socks5 = false
|
||||
allow_upstream_proxy = false
|
||||
mode = "full"
|
||||
mitm = true
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"openai.com" = "allow"
|
||||
|
||||
[[permissions.workspace.network.mitm_hooks]]
|
||||
host = "api.github.com"
|
||||
|
||||
[permissions.workspace.network.mitm_hooks.match]
|
||||
methods = ["POST", "PUT"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
|
||||
[permissions.workspace.network.mitm_hooks.match.headers]
|
||||
"x-github-api-version" = ["2022-11-28"]
|
||||
|
||||
[permissions.workspace.network.mitm_hooks.actions]
|
||||
strip_request_headers = ["authorization"]
|
||||
|
||||
[[permissions.workspace.network.mitm_hooks.actions.inject_request_headers]]
|
||||
name = "authorization"
|
||||
secret_env_var = "CODEX_GITHUB_TOKEN"
|
||||
prefix = "Bearer "
|
||||
"#;
|
||||
let cfg: ConfigToml =
|
||||
toml::from_str(toml).expect("TOML deserialization should succeed for permissions profiles");
|
||||
@@ -766,7 +791,7 @@ allow_upstream_proxy = false
|
||||
allow_upstream_proxy: Some(false),
|
||||
dangerously_allow_non_loopback_proxy: None,
|
||||
dangerously_allow_all_unix_sockets: None,
|
||||
mode: None,
|
||||
mode: Some(NetworkMode::Full),
|
||||
domains: Some(NetworkDomainPermissionsToml {
|
||||
entries: BTreeMap::from([(
|
||||
"openai.com".to_string(),
|
||||
@@ -775,6 +800,29 @@ allow_upstream_proxy = false
|
||||
}),
|
||||
unix_sockets: None,
|
||||
allow_local_binding: None,
|
||||
mitm: Some(true),
|
||||
mitm_hooks: Some(vec![MitmHookConfig {
|
||||
host: "api.github.com".to_string(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: vec!["POST".to_string(), "PUT".to_string()],
|
||||
path_prefixes: vec!["/repos/openai/".to_string()],
|
||||
query: BTreeMap::new(),
|
||||
headers: BTreeMap::from([(
|
||||
"x-github-api-version".to_string(),
|
||||
vec!["2022-11-28".to_string()],
|
||||
)]),
|
||||
body: None,
|
||||
},
|
||||
actions: MitmHookActionsConfig {
|
||||
strip_request_headers: vec!["authorization".to_string()],
|
||||
inject_request_headers: vec![InjectedHeaderConfig {
|
||||
name: "authorization".to_string(),
|
||||
secret_env_var: Some("CODEX_GITHUB_TOKEN".to_string()),
|
||||
secret_file: None,
|
||||
prefix: Some("Bearer ".to_string()),
|
||||
}],
|
||||
},
|
||||
}]),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
@@ -782,6 +830,38 @@ allow_upstream_proxy = false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profile_network_to_proxy_config_preserves_mitm_hooks() {
|
||||
let network = NetworkToml {
|
||||
mode: Some(NetworkMode::Full),
|
||||
mitm: Some(true),
|
||||
mitm_hooks: Some(vec![MitmHookConfig {
|
||||
host: "api.github.com".to_string(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: vec!["POST".to_string()],
|
||||
path_prefixes: vec!["/repos/openai/".to_string()],
|
||||
..MitmHookMatchConfig::default()
|
||||
},
|
||||
actions: MitmHookActionsConfig {
|
||||
strip_request_headers: vec!["authorization".to_string()],
|
||||
inject_request_headers: vec![InjectedHeaderConfig {
|
||||
name: "authorization".to_string(),
|
||||
secret_env_var: Some("CODEX_GITHUB_TOKEN".to_string()),
|
||||
secret_file: None,
|
||||
prefix: Some("Bearer ".to_string()),
|
||||
}],
|
||||
},
|
||||
}]),
|
||||
..NetworkToml::default()
|
||||
};
|
||||
|
||||
let config = network.to_network_proxy_config();
|
||||
|
||||
assert_eq!(config.network.mode, NetworkMode::Full);
|
||||
assert!(config.network.mitm);
|
||||
assert_eq!(config.network.mitm_hooks, network.mitm_hooks.unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_without_feature()
|
||||
-> std::io::Result<()> {
|
||||
|
||||
@@ -104,6 +104,75 @@ default_permissions = "workspace"
|
||||
assert_eq!(config.network.denied_domains(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_overrides_mitm_hooks() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
mode = "limited"
|
||||
mitm = false
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
|
||||
[[permissions.workspace.network.mitm_hooks]]
|
||||
host = "lower.example.com"
|
||||
|
||||
[permissions.workspace.network.mitm_hooks.match]
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
mode = "full"
|
||||
mitm = true
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"higher.example.com" = "allow"
|
||||
|
||||
[[permissions.workspace.network.mitm_hooks]]
|
||||
host = "api.github.com"
|
||||
|
||||
[permissions.workspace.network.mitm_hooks.match]
|
||||
methods = ["PUT"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should apply");
|
||||
apply_network_tables(
|
||||
&mut config,
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should apply");
|
||||
|
||||
assert_eq!(config.network.mode, codex_network_proxy::NetworkMode::Full);
|
||||
assert!(config.network.mitm);
|
||||
assert_eq!(
|
||||
config.network.allowed_domains(),
|
||||
Some(vec![
|
||||
"lower.example.com".to_string(),
|
||||
"higher.example.com".to_string()
|
||||
])
|
||||
);
|
||||
assert_eq!(config.network.mitm_hooks.len(), 1);
|
||||
assert_eq!(config.network.mitm_hooks[0].host, "api.github.com");
|
||||
assert_eq!(config.network.mitm_hooks[0].matcher.methods, vec!["PUT"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execpolicy_network_rules_overlay_network_lists() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
|
||||
@@ -13,6 +13,8 @@ use std::path::Path;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
use crate::mitm_hook::MitmHookConfig;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
|
||||
pub struct NetworkProxyConfig {
|
||||
#[serde(default)]
|
||||
@@ -139,6 +141,8 @@ pub struct NetworkProxySettings {
|
||||
pub allow_local_binding: bool,
|
||||
#[serde(default)]
|
||||
pub mitm: bool,
|
||||
#[serde(default)]
|
||||
pub mitm_hooks: Vec<MitmHookConfig>,
|
||||
}
|
||||
|
||||
impl Default for NetworkProxySettings {
|
||||
@@ -157,6 +161,7 @@ impl Default for NetworkProxySettings {
|
||||
unix_sockets: None,
|
||||
allow_local_binding: false,
|
||||
mitm: false,
|
||||
mitm_hooks: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -273,8 +278,8 @@ pub enum NetworkMode {
|
||||
/// blocked unless MITM is enabled so the proxy can enforce method policy on inner requests.
|
||||
/// SOCKS5 remains blocked in limited mode.
|
||||
Limited,
|
||||
/// Full network access: all HTTP methods are allowed, and HTTPS CONNECTs are tunneled without
|
||||
/// MITM interception.
|
||||
/// Full network access: all HTTP methods are allowed. HTTPS CONNECTs are tunneled directly
|
||||
/// unless MITM is needed for host-specific inner-request hooks.
|
||||
#[default]
|
||||
Full,
|
||||
}
|
||||
@@ -588,6 +593,7 @@ mod tests {
|
||||
unix_sockets: None,
|
||||
allow_local_binding: false,
|
||||
mitm: false,
|
||||
mitm_hooks: Vec::new(),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -652,6 +658,7 @@ mod tests {
|
||||
"unix_sockets": null,
|
||||
"allow_local_binding": false,
|
||||
"mitm": false,
|
||||
"mitm_hooks": [],
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ mod config;
|
||||
mod connect_policy;
|
||||
mod http_proxy;
|
||||
mod mitm;
|
||||
mod mitm_hook;
|
||||
mod network_policy;
|
||||
mod policy;
|
||||
mod proxy;
|
||||
@@ -23,6 +24,11 @@ pub use config::NetworkProxyConfig;
|
||||
pub use config::NetworkUnixSocketPermission;
|
||||
pub use config::NetworkUnixSocketPermissions;
|
||||
pub use config::host_and_port_from_network_addr;
|
||||
pub use mitm_hook::InjectedHeaderConfig;
|
||||
pub use mitm_hook::MitmHookActionsConfig;
|
||||
pub use mitm_hook::MitmHookBodyConfig;
|
||||
pub use mitm_hook::MitmHookConfig;
|
||||
pub use mitm_hook::MitmHookMatchConfig;
|
||||
pub use network_policy::NetworkDecision;
|
||||
pub use network_policy::NetworkDecisionSource;
|
||||
pub use network_policy::NetworkPolicyDecider;
|
||||
|
||||
1047
codex-rs/network-proxy/src/mitm_hook.rs
Normal file
1047
codex-rs/network-proxy/src/mitm_hook.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ use crate::config::NetworkProxyConfig;
|
||||
use crate::config::NetworkUnixSocketPermissions;
|
||||
use crate::mitm::MitmState;
|
||||
use crate::mitm::MitmUpstreamConfig;
|
||||
use crate::mitm_hook::MitmHookConfig;
|
||||
use crate::mitm_hook::validate_mitm_hook_config;
|
||||
use crate::policy::DomainPattern;
|
||||
use crate::policy::compile_allowlist_globset;
|
||||
use crate::policy::compile_denylist_globset;
|
||||
@@ -53,6 +55,9 @@ pub struct PartialNetworkConfig {
|
||||
#[serde(default)]
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
pub mitm: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub mitm_hooks: Option<Vec<MitmHookConfig>>,
|
||||
}
|
||||
|
||||
pub fn build_config_state(
|
||||
@@ -116,6 +121,7 @@ pub fn validate_policy_against_constraints(
|
||||
.map(|entry| entry.to_ascii_lowercase())
|
||||
.collect();
|
||||
let config_allow_unix_sockets = config.network.allow_unix_sockets();
|
||||
validate_mitm_hook_config(config).map_err(invalid_mitm_hook_configuration)?;
|
||||
validate_non_global_wildcard_domain_patterns("network.denied_domains", &config_denied_domains)?;
|
||||
if let Some(max_enabled) = constraints.enabled {
|
||||
validate(enabled, move |candidate| {
|
||||
@@ -376,6 +382,14 @@ pub fn validate_policy_against_constraints(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn invalid_mitm_hook_configuration(err: anyhow::Error) -> NetworkProxyConstraintError {
|
||||
NetworkProxyConstraintError::InvalidValue {
|
||||
field_name: "network.mitm_hooks",
|
||||
candidate: err.to_string(),
|
||||
allowed: "valid MITM hook configuration".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_non_global_wildcard_domain_patterns(
|
||||
field_name: &'static str,
|
||||
patterns: &[String],
|
||||
|
||||
Reference in New Issue
Block a user