mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Use named MITM permissions config (#18240)
## Stack 1. Parent PR: #18868 adds MITM hook config and model only. 2. Parent PR: #20659 wires hook enforcement into the proxy request path. 3. This PR changes the user facing PermissionProfile TOML shape. ## Why 1. The broader goal is to make MITM clamping usable from the same permission profile that already controls network behavior. 2. This PR is the config UX layer for the stack. It moves MITM policy into `[permissions.<profile>.network.mitm]` instead of exposing the flat runtime shape to users. 3. The named hook and action tables belong here because users need reusable policy blocks that are easy to review, while the proxy runtime only needs a flat hook list. 4. This PR validates action refs during config parsing so mistakes in the user facing policy fail before a proxy session starts. 5. Keeping the lowering here lets the proxy keep its simpler runtime model and lets PermissionProfile remain the single source of network permission policy. ## Summary 1. Keep MITM policy inside `[permissions.<profile>.network.mitm]` so the selected PermissionProfile owns network proxy policy. 2. Use named MITM hooks under `[permissions.<profile>.network.mitm.hooks.<name>]`. 3. Put host, methods, path prefixes, query, headers, body, and action refs on the hook table. 4. Define reusable action blocks under `[permissions.<profile>.network.mitm.actions.<name>]`. 5. Represent action blocks with `NetworkMitmActionToml`, then lower them into the proxy runtime action config. 6. Reject unknown refs, empty refs, and empty action blocks during config parsing. 7. Keep the runtime hook model unchanged by lowering config into the existing proxy hook list. 8. Preserve the #20659 activation fix for nested MITM policy. ## Example ```toml [permissions.workspace.network.mitm] enabled = true [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"] ``` ## Validation 1. Regenerated the config schema. 2. Ran the core MITM config parsing and validation tests. 3. Ran the core PermissionProfile MITM proxy activation tests. 4. Ran the core config schema fixture test. 5. Ran the network proxy MITM policy tests. 6. Ran the scoped Clippy fixer for the network proxy crate. 7. Ran the scoped Clippy fixer for the core crate. --------- Co-authored-by: Winston Howes <winston@openai.com>
This commit is contained in:
co-authored by
Winston Howes
parent
0a4179bb19
commit
3cae84009a
@@ -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;
|
||||
@@ -96,6 +99,7 @@ use core_test_support::PathBufExt;
|
||||
use core_test_support::PathExt;
|
||||
use core_test_support::TempDirExt;
|
||||
use core_test_support::test_absolute_path;
|
||||
use indexmap::IndexMap;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rmcp::model::ElicitationCapability;
|
||||
use rmcp::model::FormElicitationCapability;
|
||||
@@ -760,6 +764,15 @@ mode = "full"
|
||||
|
||||
[permissions.dev.network.domains]
|
||||
"openai.com" = "allow"
|
||||
|
||||
[permissions.dev.network.mitm.hooks.github_write]
|
||||
host = "api.github.com"
|
||||
methods = ["POST", "PUT"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["strip_auth"]
|
||||
|
||||
[permissions.dev.network.mitm.actions.strip_auth]
|
||||
strip_request_headers = ["authorization"]
|
||||
"#;
|
||||
let cfg: ConfigToml =
|
||||
toml::from_str(toml).expect("TOML deserialization should succeed for permissions profiles");
|
||||
@@ -817,6 +830,27 @@ mode = "full"
|
||||
}),
|
||||
unix_sockets: None,
|
||||
allow_local_binding: None,
|
||||
mitm: Some(NetworkMitmToml {
|
||||
hooks: Some(IndexMap::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(IndexMap::from([(
|
||||
"strip_auth".to_string(),
|
||||
NetworkMitmActionToml {
|
||||
strip_request_headers: vec!["authorization".to_string()],
|
||||
inject_request_headers: Vec::new(),
|
||||
},
|
||||
)])),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
)]),
|
||||
@@ -824,6 +858,140 @@ 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(IndexMap::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(IndexMap::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()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_profile_network_to_proxy_config_preserves_mitm_hook_declaration_order() {
|
||||
let toml = r#"
|
||||
default_permissions = "workspace"
|
||||
|
||||
[permissions.workspace.network.mitm.actions.noop]
|
||||
strip_request_headers = ["authorization"]
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.z_first]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/openai/"]
|
||||
action = ["noop"]
|
||||
|
||||
[permissions.workspace.network.mitm.hooks.a_second]
|
||||
host = "api.github.com"
|
||||
methods = ["POST"]
|
||||
path_prefixes = ["/repos/"]
|
||||
action = ["noop"]
|
||||
"#;
|
||||
let cfg: ConfigToml = toml::from_str(toml).expect("permissions profile should deserialize");
|
||||
let permissions = cfg.permissions.expect("permissions should deserialize");
|
||||
let network = permissions
|
||||
.entries
|
||||
.get("workspace")
|
||||
.expect("workspace profile should exist")
|
||||
.network
|
||||
.as_ref()
|
||||
.expect("network profile should exist");
|
||||
|
||||
let config = network.to_network_proxy_config();
|
||||
|
||||
assert_eq!(config.network.mitm_hooks.len(), 2);
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[0].matcher.path_prefixes,
|
||||
vec!["/repos/openai/".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
config.network.mitm_hooks[1].matcher.path_prefixes,
|
||||
vec!["/repos/".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn permissions_profiles_proxy_policy_does_not_start_managed_network_proxy_without_feature()
|
||||
-> std::io::Result<()> {
|
||||
|
||||
@@ -186,6 +186,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);
|
||||
}
|
||||
|
||||
@@ -16,12 +16,16 @@ use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::LoaderOverrides;
|
||||
use codex_config::loader::load_config_layers_state;
|
||||
use codex_config::merge_toml_values;
|
||||
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;
|
||||
@@ -30,6 +34,7 @@ use codex_network_proxy::build_config_state;
|
||||
use codex_network_proxy::normalize_host;
|
||||
use codex_network_proxy::validate_policy_against_constraints;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
@@ -208,6 +213,7 @@ fn selected_network_from_tables(parsed: NetworkTablesToml) -> Result<Option<Netw
|
||||
Ok(profile.profile.network)
|
||||
}
|
||||
|
||||
#[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);
|
||||
@@ -215,11 +221,57 @@ fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesTo
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NetworkConfigAccumulator {
|
||||
config: NetworkProxyConfig,
|
||||
mitm_hooks: IndexMap<String, NetworkMitmHookToml>,
|
||||
mitm_actions: IndexMap<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 actions = self.mitm_actions;
|
||||
let mitm = NetworkMitmToml {
|
||||
hooks: Some(self.mitm_hooks),
|
||||
actions: Some(actions.clone()),
|
||||
};
|
||||
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 merged = toml::Value::Table(toml::map::Map::new());
|
||||
for layer in layers.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
@@ -228,7 +280,9 @@ fn config_from_layers(
|
||||
merge_toml_values(&mut merged, &layer.config);
|
||||
}
|
||||
let parsed = network_tables_from_toml(&merged)?;
|
||||
apply_network_tables(&mut config, parsed)?;
|
||||
let mut accumulator = NetworkConfigAccumulator::default();
|
||||
accumulator.apply_network_tables(parsed)?;
|
||||
let mut config = accumulator.finish()?;
|
||||
apply_exec_policy_network_rules(&mut config, exec_policy);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -112,6 +112,76 @@ default_permissions = "dev"
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user