mirror of
https://github.com/openai/codex.git
synced 2026-08-23 13:09:46 +00:00
Report network disconnects during approval (#39284)
## Why When a local proxy request disconnects before network approval completes, the owning tool call needs a model-visible explanation instead of remaining tied to the abandoned request. ## What changed - Track disconnect timing while plain HTTP and CONNECT requests await policy decisions. - Cancel the owning execution and report how long the request waited when it disconnects before approval completes. - Preserve an explicit approval outcome when disconnect cleanup runs afterward. ## Testing Added unit coverage for disconnect tracking and outcome precedence, plus end-to-end coverage for plain HTTP and CONNECT requests. GitOrigin-RevId: b354b29bbe86f38e252fcaf529541f177480136b
This commit is contained in:
@@ -19,6 +19,7 @@ use crate::reasons::REASON_MITM_REQUIRED;
|
||||
use crate::reasons::REASON_NOT_ALLOWED;
|
||||
use crate::reasons::REASON_PROXY_DISABLED;
|
||||
use crate::reasons::REASON_UNIX_SOCKET_UNSUPPORTED;
|
||||
use crate::request_disconnect::NetworkRequestDisconnect;
|
||||
use crate::responses::PolicyDecisionDetails;
|
||||
use crate::responses::blocked_header_value;
|
||||
use crate::responses::blocked_message_with_policy;
|
||||
@@ -179,6 +180,7 @@ async fn http_connect_accept(
|
||||
environment_id: Option<String>,
|
||||
mut req: Request,
|
||||
) -> Result<(Response, Request), Response> {
|
||||
let started_at = Instant::now();
|
||||
let app_state = req
|
||||
.extensions()
|
||||
.get::<Arc<NetworkProxyState>>()
|
||||
@@ -218,7 +220,8 @@ async fn http_connect_accept(
|
||||
.await);
|
||||
}
|
||||
|
||||
let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
let disconnect = NetworkRequestDisconnect::default();
|
||||
let mut request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
protocol: NetworkProtocol::HttpsConnect,
|
||||
host: host.clone(),
|
||||
port: authority.port,
|
||||
@@ -229,7 +232,14 @@ async fn http_connect_accept(
|
||||
exec_policy_hint: None,
|
||||
});
|
||||
|
||||
match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await {
|
||||
request.disconnect = Some(disconnect.clone());
|
||||
match disconnect
|
||||
.track_http_request(
|
||||
started_at,
|
||||
evaluate_host_policy(&app_state, policy_decider.as_ref(), &request),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(NetworkDecision::Deny {
|
||||
reason,
|
||||
source,
|
||||
@@ -510,6 +520,7 @@ async fn http_plain_proxy(
|
||||
environment_id: Option<String>,
|
||||
mut req: Request,
|
||||
) -> Result<Response, Infallible> {
|
||||
let started_at = Instant::now();
|
||||
let app_state = match req.extensions().get::<Arc<NetworkProxyState>>().cloned() {
|
||||
Some(state) => state,
|
||||
None => {
|
||||
@@ -709,7 +720,8 @@ async fn http_plain_proxy(
|
||||
.await);
|
||||
}
|
||||
|
||||
let request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
let disconnect = NetworkRequestDisconnect::default();
|
||||
let mut request = NetworkPolicyRequest::new(NetworkPolicyRequestArgs {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: host.clone(),
|
||||
port,
|
||||
@@ -720,7 +732,14 @@ async fn http_plain_proxy(
|
||||
exec_policy_hint: None,
|
||||
});
|
||||
|
||||
match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await {
|
||||
request.disconnect = Some(disconnect.clone());
|
||||
match disconnect
|
||||
.track_http_request(
|
||||
started_at,
|
||||
evaluate_host_policy(&app_state, policy_decider.as_ref(), &request),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(NetworkDecision::Deny {
|
||||
reason,
|
||||
source,
|
||||
|
||||
@@ -16,6 +16,7 @@ mod policy;
|
||||
mod proxy;
|
||||
mod reasons;
|
||||
mod remote_config;
|
||||
mod request_disconnect;
|
||||
mod responses;
|
||||
mod runtime;
|
||||
mod socks5;
|
||||
@@ -82,6 +83,7 @@ pub use proxy::proxy_url_env_value;
|
||||
pub use proxy::strip_managed_proxy_env;
|
||||
pub use remote_config::RemoteNetworkProxyConfig;
|
||||
pub use remote_config::RemoteNetworkProxyLaunchConfig;
|
||||
pub use request_disconnect::NetworkRequestDisconnect;
|
||||
pub use runtime::BlockedRequest;
|
||||
pub use runtime::BlockedRequestArgs;
|
||||
pub use runtime::BlockedRequestObserver;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::reasons::REASON_POLICY_DENIED;
|
||||
use crate::request_disconnect::NetworkRequestDisconnect;
|
||||
use crate::runtime::HostBlockDecision;
|
||||
use crate::runtime::HostBlockReason;
|
||||
use crate::state::NetworkProxyState;
|
||||
@@ -106,6 +107,8 @@ pub struct NetworkPolicyRequest {
|
||||
pub command: Option<String>,
|
||||
pub exec_policy_hint: Option<String>,
|
||||
pub execution_id: Option<String>,
|
||||
/// Present only when the local HTTP transport can identify an abandoned request.
|
||||
pub disconnect: Option<NetworkRequestDisconnect>,
|
||||
}
|
||||
|
||||
pub struct NetworkPolicyRequestArgs {
|
||||
@@ -141,6 +144,7 @@ impl NetworkPolicyRequest {
|
||||
command,
|
||||
exec_policy_hint,
|
||||
execution_id: None,
|
||||
disconnect: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
codex-rs/network-proxy/src/request_disconnect.rs
Normal file
45
codex-rs/network-proxy/src/request_disconnect.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::future::Future;
|
||||
use std::pin::pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Transport metadata available while cleaning up an abandoned HTTP policy request.
|
||||
/// This does not cancel the approval reviewer or change its decision.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct NetworkRequestDisconnect(Arc<OnceLock<Duration>>);
|
||||
|
||||
impl NetworkRequestDisconnect {
|
||||
pub fn elapsed(&self) -> Option<Duration> {
|
||||
self.0.get().copied()
|
||||
}
|
||||
|
||||
pub(crate) async fn track_http_request<F: Future>(
|
||||
&self,
|
||||
started_at: Instant,
|
||||
future: F,
|
||||
) -> F::Output {
|
||||
// Publish the cause before dropping the policy future: its cleanup may
|
||||
// immediately finish the owning tool call.
|
||||
let mut future = pin!(future);
|
||||
let mut guard = HttpRequestGuard(Some((self, started_at)));
|
||||
let result = future.as_mut().await;
|
||||
guard.0 = None;
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
struct HttpRequestGuard<'a>(Option<(&'a NetworkRequestDisconnect, Instant)>);
|
||||
|
||||
impl Drop for HttpRequestGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
if let Some((disconnect, started_at)) = self.0 {
|
||||
let _ = disconnect.0.set(started_at.elapsed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "request_disconnect_tests.rs"]
|
||||
mod tests;
|
||||
41
codex-rs/network-proxy/src/request_disconnect_tests.rs
Normal file
41
codex-rs/network-proxy/src/request_disconnect_tests.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use super::NetworkRequestDisconnect;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::future::pending;
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
#[tokio::test]
|
||||
async fn disconnect_is_published_before_policy_cleanup() {
|
||||
struct ObserveOnDrop(NetworkRequestDisconnect);
|
||||
impl Drop for ObserveOnDrop {
|
||||
fn drop(&mut self) {
|
||||
assert!(self.0.elapsed().is_some());
|
||||
}
|
||||
}
|
||||
|
||||
let disconnect = NetworkRequestDisconnect::default();
|
||||
let observer = ObserveOnDrop(disconnect.clone());
|
||||
let started_at = Instant::now();
|
||||
let decision = disconnect.track_http_request(started_at, async move {
|
||||
let _observer = observer;
|
||||
pending::<()>().await;
|
||||
});
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_millis(1), decision)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(disconnect.elapsed().expect("disconnect time") <= started_at.elapsed());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn completed_policy_request_is_not_a_disconnect() {
|
||||
let disconnect = NetworkRequestDisconnect::default();
|
||||
assert_eq!(
|
||||
disconnect
|
||||
.track_http_request(Instant::now(), async { 42 })
|
||||
.await,
|
||||
42
|
||||
);
|
||||
assert_eq!(disconnect.elapsed(), None);
|
||||
}
|
||||
Reference in New Issue
Block a user