mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
## What changed - Add client-side handling for exec-server network policy requests, including request validation, per-process decision routing, and allow, deny, or ask responses. - Bound concurrent callbacks and fail closed when requests are invalid, exceed capacity, time out, or outlive their process session. - Replace the callback opt-in flag with a launch-level `policyDecisionTimeoutMs` value and include transport overhead in the executor timeout. - Preserve callback admission limits across connection recovery and clean up policy state when process startup is abandoned or a session ends. ## Testing - Cover decision forwarding, invalid and excess requests, cancellation during process cleanup, abandoned starts, timeout propagation, and launch-config serialization. GitOrigin-RevId: 6ee2a0139e0f8170cf1a5cdd646bd811b7d3c1f6
139 lines
4.6 KiB
Rust
139 lines
4.6 KiB
Rust
use pretty_assertions::assert_eq;
|
|
|
|
use super::RemoteNetworkProxyConfig;
|
|
use super::RemoteNetworkProxyLaunchConfig;
|
|
use crate::MitmHookConfig;
|
|
use crate::NetworkMode;
|
|
use crate::NetworkProxyAuditMetadata;
|
|
use crate::NetworkProxyConfig;
|
|
use crate::NetworkProxyState;
|
|
|
|
#[test]
|
|
fn round_trip_preserves_supported_effective_settings() {
|
|
let mut config = NetworkProxyConfig {
|
|
enabled: true,
|
|
enable_socks5: false,
|
|
enable_socks5_udp: false,
|
|
allow_upstream_proxy: false,
|
|
dangerously_allow_all_unix_sockets: true,
|
|
mode: NetworkMode::Limited,
|
|
allow_local_binding: true,
|
|
..NetworkProxyConfig::default()
|
|
};
|
|
config.set_allowed_domains(vec!["example.com".into()]);
|
|
config.set_denied_domains(vec!["blocked.example.com".into()]);
|
|
config.set_allow_unix_sockets(vec!["/var/run/example.sock".into()]);
|
|
|
|
let remote =
|
|
RemoteNetworkProxyConfig::from_effective_config(&config).expect("supported remote config");
|
|
let round_trip = remote.into_network_proxy_config();
|
|
|
|
assert_eq!(round_trip, config);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_unsupported_configuration() {
|
|
let cases = [
|
|
(
|
|
"MITM",
|
|
NetworkProxyConfig {
|
|
enabled: true,
|
|
mitm: true,
|
|
..NetworkProxyConfig::default()
|
|
},
|
|
),
|
|
(
|
|
"credential broker",
|
|
NetworkProxyConfig {
|
|
enabled: true,
|
|
credential_broker: true,
|
|
..NetworkProxyConfig::default()
|
|
},
|
|
),
|
|
(
|
|
"plaintext credential injection",
|
|
NetworkProxyConfig {
|
|
enabled: true,
|
|
dangerously_allow_plaintext_credential_injection: true,
|
|
..NetworkProxyConfig::default()
|
|
},
|
|
),
|
|
(
|
|
"MITM hooks",
|
|
NetworkProxyConfig {
|
|
enabled: true,
|
|
mitm_hooks: vec![MitmHookConfig::default()],
|
|
..NetworkProxyConfig::default()
|
|
},
|
|
),
|
|
];
|
|
|
|
for (feature, config) in cases {
|
|
assert!(
|
|
RemoteNetworkProxyConfig::from_effective_config(&config).is_err(),
|
|
"{feature} must not cross the remote executor boundary"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn accepts_unsupported_configuration_when_proxy_is_disabled() {
|
|
let config = NetworkProxyConfig {
|
|
mitm: true,
|
|
credential_broker: true,
|
|
dangerously_allow_plaintext_credential_injection: true,
|
|
mitm_hooks: vec![MitmHookConfig::default()],
|
|
..NetworkProxyConfig::default()
|
|
};
|
|
|
|
let remote = RemoteNetworkProxyConfig::from_effective_config(&config)
|
|
.expect("disabled proxy configuration does not cross the executor boundary");
|
|
|
|
assert!(!remote.enabled);
|
|
}
|
|
|
|
#[test]
|
|
fn launch_config_materializes_audit_and_execution_attribution() {
|
|
let proxy = RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig {
|
|
enabled: true,
|
|
..NetworkProxyConfig::default()
|
|
})
|
|
.expect("supported remote config");
|
|
let audit_metadata = NetworkProxyAuditMetadata {
|
|
conversation_id: Some("conversation-1".to_string()),
|
|
user_account_id: Some("account-1".to_string()),
|
|
originator: Some("codex_cli_rs".to_string()),
|
|
model: Some("model-1".to_string()),
|
|
..NetworkProxyAuditMetadata::default()
|
|
};
|
|
let state = NetworkProxyState::from_remote_launch_config(RemoteNetworkProxyLaunchConfig {
|
|
proxy,
|
|
audit_metadata: audit_metadata.clone(),
|
|
environment_id: Some("remote".to_string()),
|
|
execution_id: Some("execution-1".to_string()),
|
|
policy_decision_timeout_ms: None,
|
|
})
|
|
.expect("remote launch state");
|
|
|
|
assert_eq!(state.audit_metadata(), &audit_metadata);
|
|
assert_eq!(state.environment_id(), Some("remote"));
|
|
assert_eq!(state.execution_id().as_deref(), Some("execution-1"));
|
|
}
|
|
|
|
#[test]
|
|
fn policy_decision_callback_timeout_round_trips() {
|
|
let config = RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig::default())
|
|
.expect("supported remote config");
|
|
let mut launch = RemoteNetworkProxyLaunchConfig::new(config);
|
|
let without_timeout = serde_json::to_value(&launch).expect("serialize launch config");
|
|
assert_eq!(without_timeout.get("policyDecisionTimeoutMs"), None);
|
|
launch.policy_decision_timeout_ms = Some(900_000);
|
|
let with_timeout = serde_json::to_value(&launch).expect("serialize launch timeout");
|
|
assert_eq!(with_timeout["policyDecisionTimeoutMs"], 900_000);
|
|
assert_eq!(
|
|
serde_json::from_value::<RemoteNetworkProxyLaunchConfig>(with_timeout)
|
|
.expect("deserialize launch timeout"),
|
|
launch
|
|
);
|
|
}
|