mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
Use named MITM permissions config
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use codex_network_proxy::InjectedHeaderConfig;
|
||||
use codex_network_proxy::MitmHookActionsConfig;
|
||||
use codex_network_proxy::MitmHookBodyConfig;
|
||||
use codex_network_proxy::MitmHookConfig;
|
||||
use codex_network_proxy::MitmHookMatchConfig;
|
||||
use codex_network_proxy::NetworkDomainPermission as ProxyNetworkDomainPermission;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
@@ -173,6 +178,36 @@ pub struct NetworkToml {
|
||||
pub domains: Option<NetworkDomainPermissionsToml>,
|
||||
pub unix_sockets: Option<NetworkUnixSocketPermissionsToml>,
|
||||
pub allow_local_binding: Option<bool>,
|
||||
pub mitm: Option<NetworkMitmToml>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct NetworkMitmToml {
|
||||
pub hooks: Option<BTreeMap<String, NetworkMitmHookToml>>,
|
||||
pub actions: Option<BTreeMap<String, NetworkMitmActionToml>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct NetworkMitmTomlUnchecked {
|
||||
pub hooks: Option<BTreeMap<String, NetworkMitmHookToml>>,
|
||||
pub actions: Option<BTreeMap<String, NetworkMitmActionToml>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct NetworkMitmHookToml {
|
||||
pub host: String,
|
||||
pub methods: Vec<String>,
|
||||
pub path_prefixes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub query: BTreeMap<String, Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub headers: BTreeMap<String, Vec<String>>,
|
||||
#[schemars(with = "Option<MitmHookBodyConfigSchema>")]
|
||||
pub body: Option<MitmHookBodyConfig>,
|
||||
pub action: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -182,6 +217,114 @@ enum NetworkModeSchema {
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct NetworkMitmActionToml {
|
||||
pub strip_request_headers: Vec<String>,
|
||||
pub inject_request_headers: Vec<NetworkMitmInjectedHeaderToml>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(default)]
|
||||
pub struct NetworkMitmInjectedHeaderToml {
|
||||
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<'de> Deserialize<'de> for NetworkMitmToml {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let unchecked = NetworkMitmTomlUnchecked::deserialize(deserializer)?;
|
||||
let mitm = Self {
|
||||
hooks: unchecked.hooks,
|
||||
actions: unchecked.actions,
|
||||
};
|
||||
mitm.validate_action_definitions()
|
||||
.map_err(serde::de::Error::custom)?;
|
||||
Ok(mitm)
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMitmToml {
|
||||
pub fn validate_action_definitions(&self) -> Result<(), String> {
|
||||
if let Some(actions) = self.actions.as_ref() {
|
||||
for (action_name, action) in actions {
|
||||
if action.is_empty() {
|
||||
return Err(format!(
|
||||
"network.mitm.actions.{action_name} must define at least one operation"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let Some(hooks) = self.hooks.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for (hook_name, hook) in hooks {
|
||||
if hook.action.is_empty() {
|
||||
return Err(format!(
|
||||
"network.mitm.hooks.{hook_name}.action must not be empty"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_action_references(
|
||||
&self,
|
||||
actions_by_name: &BTreeMap<String, NetworkMitmActionToml>,
|
||||
) -> Result<(), String> {
|
||||
self.validate_action_definitions()?;
|
||||
|
||||
let Some(hooks) = self.hooks.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for (hook_name, hook) in hooks {
|
||||
for action_name in &hook.action {
|
||||
if !actions_by_name.contains_key(action_name) {
|
||||
return Err(format!(
|
||||
"network.mitm.hooks.{hook_name}.action references undefined action `{action_name}`"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_runtime_hooks(
|
||||
&self,
|
||||
actions_by_name: Option<&BTreeMap<String, NetworkMitmActionToml>>,
|
||||
) -> Vec<MitmHookConfig> {
|
||||
self.hooks
|
||||
.as_ref()
|
||||
.map(|hooks| {
|
||||
hooks
|
||||
.values()
|
||||
.map(|hook| hook.to_runtime(actions_by_name))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMitmActionToml {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.strip_request_headers.is_empty() && self.inject_request_headers.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkToml {
|
||||
pub fn apply_to_network_proxy_config(&self, config: &mut NetworkProxyConfig) {
|
||||
if let Some(enabled) = self.enabled {
|
||||
@@ -234,6 +377,11 @@ 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.as_ref() {
|
||||
config.network.mitm_hooks = mitm.to_runtime_hooks(mitm.actions.as_ref());
|
||||
}
|
||||
config.network.mitm =
|
||||
config.network.mode == NetworkMode::Limited || !config.network.mitm_hooks.is_empty();
|
||||
}
|
||||
|
||||
pub fn to_network_proxy_config(&self) -> NetworkProxyConfig {
|
||||
@@ -243,6 +391,61 @@ impl NetworkToml {
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMitmHookToml {
|
||||
fn to_runtime(
|
||||
&self,
|
||||
actions_by_name: Option<&BTreeMap<String, NetworkMitmActionToml>>,
|
||||
) -> MitmHookConfig {
|
||||
MitmHookConfig {
|
||||
host: self.host.clone(),
|
||||
matcher: MitmHookMatchConfig {
|
||||
methods: self.methods.clone(),
|
||||
path_prefixes: self.path_prefixes.clone(),
|
||||
query: self.query.clone(),
|
||||
headers: self.headers.clone(),
|
||||
body: self.body.clone(),
|
||||
},
|
||||
actions: self.selected_actions(actions_by_name),
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_actions(
|
||||
&self,
|
||||
actions_by_name: Option<&BTreeMap<String, NetworkMitmActionToml>>,
|
||||
) -> MitmHookActionsConfig {
|
||||
let Some(actions_by_name) = actions_by_name else {
|
||||
return MitmHookActionsConfig::default();
|
||||
};
|
||||
|
||||
let mut selected = MitmHookActionsConfig::default();
|
||||
for action_name in &self.action {
|
||||
if let Some(action) = actions_by_name.get(action_name) {
|
||||
selected
|
||||
.strip_request_headers
|
||||
.extend(action.strip_request_headers.clone());
|
||||
selected.inject_request_headers.extend(
|
||||
action
|
||||
.inject_request_headers
|
||||
.iter()
|
||||
.map(NetworkMitmInjectedHeaderToml::to_runtime),
|
||||
);
|
||||
}
|
||||
}
|
||||
selected
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkMitmInjectedHeaderToml {
|
||||
fn to_runtime(&self) -> InjectedHeaderConfig {
|
||||
InjectedHeaderConfig {
|
||||
name: self.name.clone(),
|
||||
secret_env_var: self.secret_env_var.clone(),
|
||||
secret_file: self.secret_file.clone(),
|
||||
prefix: self.prefix.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlay_network_domain_permissions(
|
||||
config: &mut NetworkProxyConfig,
|
||||
domains: &NetworkDomainPermissionsToml,
|
||||
|
||||
@@ -1535,6 +1535,118 @@
|
||||
"NetworkDomainPermissionsToml": {
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkMitmActionToml": {
|
||||
"properties": {
|
||||
"inject_request_headers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/NetworkMitmInjectedHeaderToml"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"strip_request_headers": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkMitmHookToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"action": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"body": true,
|
||||
"headers": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {},
|
||||
"type": "object"
|
||||
},
|
||||
"host": {
|
||||
"type": "string"
|
||||
},
|
||||
"methods": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"path_prefixes": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"query": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"default": {},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"action",
|
||||
"host",
|
||||
"methods",
|
||||
"path_prefixes"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkMitmInjectedHeaderToml": {
|
||||
"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"
|
||||
},
|
||||
"NetworkMitmToml": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"actions": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkMitmActionToml"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"hooks": {
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/NetworkMitmHookToml"
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"NetworkModeSchema": {
|
||||
"enum": [
|
||||
"limited",
|
||||
@@ -1638,6 +1750,9 @@
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"mitm": {
|
||||
"$ref": "#/definitions/NetworkMitmToml"
|
||||
},
|
||||
"mode": {
|
||||
"$ref": "#/definitions/NetworkModeSchema"
|
||||
},
|
||||
|
||||
@@ -26,6 +26,9 @@ 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::NetworkMitmActionToml;
|
||||
use codex_config::permissions_toml::NetworkMitmHookToml;
|
||||
use codex_config::permissions_toml::NetworkMitmToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::PermissionProfileToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
@@ -755,6 +758,15 @@ mode = "full"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"openai.com" = "allow"
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST", "PUT"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
"#;
|
||||
let cfg: ConfigToml =
|
||||
toml::from_str(toml).expect("TOML deserialization should succeed for permissions profiles");
|
||||
@@ -806,6 +818,27 @@ mode = "full"
|
||||
}),
|
||||
unix_sockets: None,
|
||||
allow_local_binding: None,
|
||||
mitm: Some(NetworkMitmToml {
|
||||
hooks: Some(BTreeMap::from([(
|
||||
"github_write".to_string(),
|
||||
NetworkMitmHookToml {
|
||||
host: "api.github.com".to_string(),
|
||||
methods: vec!["POST".to_string(), "PUT".to_string()],
|
||||
path_prefixes: vec!["/repos/openai/".to_string()],
|
||||
query: BTreeMap::new(),
|
||||
headers: BTreeMap::new(),
|
||||
body: None,
|
||||
action: vec!["strip_auth".to_string()],
|
||||
},
|
||||
)])),
|
||||
actions: Some(BTreeMap::from([(
|
||||
"strip_auth".to_string(),
|
||||
NetworkMitmActionToml {
|
||||
strip_request_headers: vec!["authorization".to_string()],
|
||||
inject_request_headers: Vec::new(),
|
||||
},
|
||||
)])),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
@@ -813,6 +846,97 @@ mode = "full"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_rejects_empty_mitm_action_reference_list() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = []
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
"#;
|
||||
|
||||
let err =
|
||||
toml::from_str::<ConfigToml>(toml).expect_err("empty MITM action refs should fail closed");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("network.mitm.hooks.github_write.action must not be empty"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_toml_rejects_empty_mitm_action_definition() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
"#;
|
||||
|
||||
let err = toml::from_str::<ConfigToml>(toml)
|
||||
.expect_err("empty MITM action definitions should fail closed");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("network.mitm.actions.strip_auth must define at least one operation"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profile_network_to_proxy_config_preserves_mitm_hooks() {
|
||||
let network = NetworkToml {
|
||||
mode: Some(NetworkMode::Full),
|
||||
mitm: Some(NetworkMitmToml {
|
||||
hooks: Some(BTreeMap::from([(
|
||||
"github_write".to_string(),
|
||||
NetworkMitmHookToml {
|
||||
host: "api.github.com".to_string(),
|
||||
methods: vec!["POST".to_string()],
|
||||
path_prefixes: vec!["/repos/openai/".to_string()],
|
||||
action: vec!["strip_auth".to_string()],
|
||||
..NetworkMitmHookToml::default()
|
||||
},
|
||||
)])),
|
||||
actions: Some(BTreeMap::from([(
|
||||
"strip_auth".to_string(),
|
||||
NetworkMitmActionToml {
|
||||
strip_request_headers: vec!["authorization".to_string()],
|
||||
inject_request_headers: Vec::new(),
|
||||
},
|
||||
)])),
|
||||
}),
|
||||
..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.len(), 1);
|
||||
assert_eq!(config.network.mitm_hooks[0].host, "api.github.com");
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[0].matcher.methods,
|
||||
vec!["POST".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[0].actions.strip_request_headers,
|
||||
vec!["authorization".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_without_feature()
|
||||
-> std::io::Result<()> {
|
||||
|
||||
@@ -185,6 +185,7 @@ pub(crate) fn apply_network_proxy_feature_config(
|
||||
}
|
||||
}),
|
||||
allow_local_binding: feature_config.allow_local_binding,
|
||||
mitm: None,
|
||||
}
|
||||
.apply_to_network_proxy_config(config);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,16 @@ use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_config::permissions_toml::NetworkMitmActionToml;
|
||||
use codex_config::permissions_toml::NetworkMitmHookToml;
|
||||
use codex_config::permissions_toml::NetworkMitmToml;
|
||||
use codex_config::permissions_toml::NetworkToml;
|
||||
use codex_config::permissions_toml::PermissionsToml;
|
||||
use codex_config::permissions_toml::overlay_network_domain_permissions;
|
||||
use codex_exec_server::LOCAL_FS;
|
||||
use codex_network_proxy::ConfigReloader;
|
||||
use codex_network_proxy::ConfigState;
|
||||
use codex_network_proxy::NetworkMode;
|
||||
use codex_network_proxy::NetworkProxyConfig;
|
||||
use codex_network_proxy::NetworkProxyConstraintError;
|
||||
use codex_network_proxy::NetworkProxyConstraints;
|
||||
@@ -28,6 +32,7 @@ use codex_network_proxy::normalize_host;
|
||||
use codex_network_proxy::validate_policy_against_constraints;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -198,6 +203,7 @@ fn selected_network_from_tables(parsed: NetworkTablesToml) -> Result<Option<Netw
|
||||
Ok(profile.network.clone())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesToml) -> Result<()> {
|
||||
if let Some(network) = selected_network_from_tables(parsed)? {
|
||||
network.apply_to_network_proxy_config(config);
|
||||
@@ -205,18 +211,69 @@ fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesTo
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NetworkConfigAccumulator {
|
||||
config: NetworkProxyConfig,
|
||||
mitm_hooks: BTreeMap<String, NetworkMitmHookToml>,
|
||||
mitm_actions: BTreeMap<String, NetworkMitmActionToml>,
|
||||
}
|
||||
|
||||
impl NetworkConfigAccumulator {
|
||||
fn apply_network_tables(&mut self, parsed: NetworkTablesToml) -> Result<()> {
|
||||
if let Some(network) = selected_network_from_tables(parsed)? {
|
||||
self.apply_network(network);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_network(&mut self, mut network: NetworkToml) {
|
||||
let mitm = network.mitm.take();
|
||||
network.apply_to_network_proxy_config(&mut self.config);
|
||||
|
||||
if let Some(mitm) = mitm {
|
||||
if let Some(actions) = mitm.actions {
|
||||
self.mitm_actions.extend(actions);
|
||||
}
|
||||
if let Some(hooks) = mitm.hooks {
|
||||
self.mitm_hooks.extend(hooks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> Result<NetworkProxyConfig> {
|
||||
if !self.mitm_hooks.is_empty() {
|
||||
let mitm = NetworkMitmToml {
|
||||
hooks: Some(self.mitm_hooks),
|
||||
actions: Some(self.mitm_actions),
|
||||
};
|
||||
let actions = mitm
|
||||
.actions
|
||||
.as_ref()
|
||||
.expect("effective MITM actions should be present");
|
||||
mitm.validate_action_references(actions)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
self.config.network.mitm_hooks = mitm.to_runtime_hooks(Some(actions));
|
||||
}
|
||||
|
||||
self.config.network.mitm = self.config.network.mode == NetworkMode::Limited
|
||||
|| !self.config.network.mitm_hooks.is_empty();
|
||||
Ok(self.config)
|
||||
}
|
||||
}
|
||||
|
||||
fn config_from_layers(
|
||||
layers: &ConfigLayerStack,
|
||||
exec_policy: &codex_execpolicy::Policy,
|
||||
) -> Result<NetworkProxyConfig> {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
let mut accumulator = NetworkConfigAccumulator::default();
|
||||
for layer in layers.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
) {
|
||||
let parsed = network_tables_from_toml(&layer.config)?;
|
||||
apply_network_tables(&mut config, parsed)?;
|
||||
accumulator.apply_network_tables(parsed)?;
|
||||
}
|
||||
let mut config = accumulator.finish()?;
|
||||
apply_exec_policy_network_rules(&mut config, exec_policy);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -104,6 +104,76 @@ default_permissions = "workspace"
|
||||
assert_eq!(config.network.denied_domains(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_profile_network_overrides_named_mitm_actions() {
|
||||
let lower_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
mode = "full"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"lower.example.com" = "allow"
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
"#,
|
||||
)
|
||||
.expect("lower layer should parse");
|
||||
let higher_network: toml::Value = toml::from_str(
|
||||
r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network]
|
||||
mode = "full"
|
||||
|
||||
[permissions.workspace.network.domains]
|
||||
"higher.example.com" = "allow"
|
||||
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["x-api-key"]
|
||||
"#,
|
||||
)
|
||||
.expect("higher layer should parse");
|
||||
|
||||
let mut accumulator = NetworkConfigAccumulator::default();
|
||||
accumulator
|
||||
.apply_network_tables(
|
||||
network_tables_from_toml(&lower_network).expect("lower layer should deserialize"),
|
||||
)
|
||||
.expect("lower layer should apply");
|
||||
accumulator
|
||||
.apply_network_tables(
|
||||
network_tables_from_toml(&higher_network).expect("higher layer should deserialize"),
|
||||
)
|
||||
.expect("higher layer should apply");
|
||||
let config = accumulator.finish().expect("merged config should build");
|
||||
|
||||
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!["POST"]);
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[0].actions.strip_request_headers,
|
||||
vec!["x-api-key"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execpolicy_network_rules_overlay_network_lists() {
|
||||
let mut config = NetworkProxyConfig::default();
|
||||
|
||||
@@ -33,8 +33,7 @@ allow_upstream_proxy = true
|
||||
# If you want to expose these listeners beyond localhost, you must opt in explicitly.
|
||||
dangerously_allow_non_loopback_proxy = false
|
||||
mode = "full" # default when unset; use "limited" for read-only mode
|
||||
# When true, HTTPS CONNECT can be terminated so limited-mode method policy still applies.
|
||||
mitm = false
|
||||
# HTTPS MITM is enabled automatically when `mode = "limited"` or when MITM hooks are configured.
|
||||
# CA cert/key are managed internally under $CODEX_HOME/proxy/ (ca.pem + ca.key).
|
||||
|
||||
# If false, local/private networking is rejected. Explicit allowlisting of local IP literals
|
||||
@@ -57,6 +56,17 @@ dangerously_allow_all_unix_sockets = false
|
||||
"::1" = "allow"
|
||||
"evil.example" = "deny"
|
||||
|
||||
# MITM hooks match HTTPS requests after CONNECT is terminated.
|
||||
[permissions.workspace.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST", "PUT"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
# Named actions can be shared across hooks and overridden by higher-precedence config layers.
|
||||
[permissions.workspace.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
|
||||
# macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`.
|
||||
[permissions.workspace.network.unix_sockets]
|
||||
"/tmp/example.sock" = "allow"
|
||||
|
||||
@@ -186,7 +186,7 @@ async fn mitm_policy_allows_matching_hooked_write_in_full_mode() {
|
||||
|
||||
assert!(
|
||||
response.is_none(),
|
||||
"matching hook should be allowed in full mode"
|
||||
"matching hook should bypass method clamp"
|
||||
);
|
||||
assert_eq!(app_state.blocked_snapshot().await.unwrap().len(), 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user