Handle exec-server network policy requests in the client (#35359)

## 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
This commit is contained in:
viyatb-oai
2026-07-25 15:57:36 +00:00
committed by copyberry
parent 4c43465133
commit 3a08af44b2
12 changed files with 745 additions and 54 deletions

View File

@@ -831,6 +831,7 @@ impl NetworkProxy {
audit_metadata: self.state.audit_metadata().clone(),
environment_id,
execution_id,
policy_decision_timeout_ms: None,
})
}

View File

@@ -24,6 +24,9 @@ pub struct RemoteNetworkProxyLaunchConfig {
pub environment_id: Option<String>,
#[serde(default)]
pub execution_id: Option<String>,
/// Controller-side policy decision budget. The executor adds transport overhead.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub policy_decision_timeout_ms: Option<u64>,
}
impl RemoteNetworkProxyLaunchConfig {
@@ -33,6 +36,7 @@ impl RemoteNetworkProxyLaunchConfig {
audit_metadata: NetworkProxyAuditMetadata::default(),
environment_id: None,
execution_id: None,
policy_decision_timeout_ms: None,
}
}
@@ -66,9 +70,6 @@ pub struct RemoteNetworkProxyConfig {
pub domains: Option<NetworkDomainPermissions>,
pub unix_sockets: Option<NetworkUnixSocketPermissions>,
pub allow_local_binding: bool,
/// Whether the executor sends domain policy decisions back to the client.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub request_policy_decisions: bool,
}
impl RemoteNetworkProxyConfig {
@@ -91,7 +92,6 @@ impl RemoteNetworkProxyConfig {
domains: config.domains.clone(),
unix_sockets: config.unix_sockets.clone(),
allow_local_binding: config.allow_local_binding,
request_policy_decisions: false,
})
}

View File

@@ -111,6 +111,7 @@ fn launch_config_materializes_audit_and_execution_attribution() {
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");
@@ -120,24 +121,18 @@ fn launch_config_materializes_audit_and_execution_attribution() {
}
#[test]
fn policy_decision_callback_opt_in_is_backward_compatible() {
let mut config =
RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig::default())
.expect("supported remote config");
let legacy = serde_json::to_value(&config).expect("serialize legacy config");
assert_eq!(legacy.get("requestPolicyDecisions"), None);
assert!(
!serde_json::from_value::<RemoteNetworkProxyConfig>(legacy)
.expect("deserialize legacy remote config")
.request_policy_decisions
);
config.request_policy_decisions = true;
let enabled = serde_json::to_value(&config).expect("serialize callback-enabled config");
assert_eq!(enabled["requestPolicyDecisions"], true);
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::<RemoteNetworkProxyConfig>(enabled)
.expect("deserialize callback-enabled config"),
config
serde_json::from_value::<RemoteNetworkProxyLaunchConfig>(with_timeout)
.expect("deserialize launch timeout"),
launch
);
}

View File

@@ -284,6 +284,7 @@ impl NetworkProxyState {
audit_metadata,
environment_id,
execution_id,
policy_decision_timeout_ms: _,
} = launch;
anyhow::ensure!(
proxy.enabled,