Preserve MITM hook order

This commit is contained in:
Eva Wong
2026-05-15 05:20:56 -07:00
committed by Winston Howes
parent b4a1e10ce5
commit 60c59733f2
5 changed files with 69 additions and 23 deletions
+1
View File
@@ -2432,6 +2432,7 @@ dependencies = [
"dunce",
"futures",
"gethostname",
"indexmap 2.13.0",
"libc",
"multimap",
"pretty_assertions",
+2 -1
View File
@@ -28,6 +28,7 @@ codex-utils-path = { workspace = true }
dunce = { workspace = true }
futures = { workspace = true, features = ["alloc", "std"] }
gethostname = { workspace = true }
indexmap = { workspace = true, features = ["serde"] }
multimap = { workspace = true }
prost = "0.14.3"
schemars = { workspace = true }
@@ -38,7 +39,7 @@ serde_path_to_error = { workspace = true }
sha2 = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["fs"] }
toml = { workspace = true }
toml = { workspace = true, features = ["preserve_order"] }
toml_edit = { workspace = true }
tonic = { workspace = true }
tonic-prost = { workspace = true }
+11 -8
View File
@@ -11,6 +11,7 @@ use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission;
use codex_network_proxy::normalize_host;
use codex_protocol::permissions::FileSystemAccessMode;
use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -184,15 +185,17 @@ pub struct NetworkToml {
#[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>>,
#[schemars(with = "Option<BTreeMap<String, NetworkMitmHookToml>>")]
pub hooks: Option<IndexMap<String, NetworkMitmHookToml>>,
#[schemars(with = "Option<BTreeMap<String, NetworkMitmActionToml>>")]
pub actions: Option<IndexMap<String, NetworkMitmActionToml>>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct NetworkMitmTomlUnchecked {
pub hooks: Option<BTreeMap<String, NetworkMitmHookToml>>,
pub actions: Option<BTreeMap<String, NetworkMitmActionToml>>,
pub hooks: Option<IndexMap<String, NetworkMitmHookToml>>,
pub actions: Option<IndexMap<String, NetworkMitmActionToml>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
@@ -282,7 +285,7 @@ impl NetworkMitmToml {
pub fn validate_action_references(
&self,
actions_by_name: &BTreeMap<String, NetworkMitmActionToml>,
actions_by_name: &IndexMap<String, NetworkMitmActionToml>,
) -> Result<(), String> {
self.validate_action_definitions()?;
@@ -305,7 +308,7 @@ impl NetworkMitmToml {
pub fn to_runtime_hooks(
&self,
actions_by_name: Option<&BTreeMap<String, NetworkMitmActionToml>>,
actions_by_name: Option<&IndexMap<String, NetworkMitmActionToml>>,
) -> Vec<MitmHookConfig> {
self.hooks
.as_ref()
@@ -394,7 +397,7 @@ impl NetworkToml {
impl NetworkMitmHookToml {
fn to_runtime(
&self,
actions_by_name: Option<&BTreeMap<String, NetworkMitmActionToml>>,
actions_by_name: Option<&IndexMap<String, NetworkMitmActionToml>>,
) -> MitmHookConfig {
MitmHookConfig {
host: self.host.clone(),
@@ -411,7 +414,7 @@ impl NetworkMitmHookToml {
fn selected_actions(
&self,
actions_by_name: Option<&BTreeMap<String, NetworkMitmActionToml>>,
actions_by_name: Option<&IndexMap<String, NetworkMitmActionToml>>,
) -> MitmHookActionsConfig {
let Some(actions_by_name) = actions_by_name else {
return MitmHookActionsConfig::default();
+48 -4
View File
@@ -98,6 +98,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;
@@ -819,7 +820,7 @@ strip_request_headers = ["authorization"]
unix_sockets: None,
allow_local_binding: None,
mitm: Some(NetworkMitmToml {
hooks: Some(BTreeMap::from([(
hooks: Some(IndexMap::from([(
"github_write".to_string(),
NetworkMitmHookToml {
host: "api.github.com".to_string(),
@@ -831,7 +832,7 @@ strip_request_headers = ["authorization"]
action: vec!["strip_auth".to_string()],
},
)])),
actions: Some(BTreeMap::from([(
actions: Some(IndexMap::from([(
"strip_auth".to_string(),
NetworkMitmActionToml {
strip_request_headers: vec!["authorization".to_string()],
@@ -900,7 +901,7 @@ fn permissions_profile_network_to_proxy_config_preserves_mitm_hooks() {
let network = NetworkToml {
mode: Some(NetworkMode::Full),
mitm: Some(NetworkMitmToml {
hooks: Some(BTreeMap::from([(
hooks: Some(IndexMap::from([(
"github_write".to_string(),
NetworkMitmHookToml {
host: "api.github.com".to_string(),
@@ -910,7 +911,7 @@ fn permissions_profile_network_to_proxy_config_preserves_mitm_hooks() {
..NetworkMitmHookToml::default()
},
)])),
actions: Some(BTreeMap::from([(
actions: Some(IndexMap::from([(
"strip_auth".to_string(),
NetworkMitmActionToml {
strip_request_headers: vec!["authorization".to_string()],
@@ -937,6 +938,49 @@ fn permissions_profile_network_to_proxy_config_preserves_mitm_hooks() {
);
}
#[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<()> {
+7 -10
View File
@@ -31,8 +31,8 @@ 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::collections::BTreeMap;
use std::sync::Arc;
use tokio::sync::RwLock;
@@ -214,8 +214,8 @@ fn apply_network_tables(config: &mut NetworkProxyConfig, parsed: NetworkTablesTo
#[derive(Default)]
struct NetworkConfigAccumulator {
config: NetworkProxyConfig,
mitm_hooks: BTreeMap<String, NetworkMitmHookToml>,
mitm_actions: BTreeMap<String, NetworkMitmActionToml>,
mitm_hooks: IndexMap<String, NetworkMitmHookToml>,
mitm_actions: IndexMap<String, NetworkMitmActionToml>,
}
impl NetworkConfigAccumulator {
@@ -242,17 +242,14 @@ impl NetworkConfigAccumulator {
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(self.mitm_actions),
actions: Some(actions.clone()),
};
let actions = mitm
.actions
.as_ref()
.expect("effective MITM actions should be present");
mitm.validate_action_references(actions)
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_hooks = mitm.to_runtime_hooks(Some(&actions));
}
self.config.network.mitm = self.config.network.mode == NetworkMode::Limited