From cfead68e5d3984b247cf0758e3e53b19165de848 Mon Sep 17 00:00:00 2001 From: richardopenai Date: Mon, 29 Jun 2026 19:14:47 -0500 Subject: [PATCH 01/10] [codex] disable Nagle on Rendezvous WebSockets (#30269) ## Summary Disable Nagle unconditionally for both exec-server Rendezvous WebSocket connections. - pass `disable_nagle=true` at the executor and harness connection call sites - keep the existing signed URL, protocol, and connection flow unchanged - add no feature flag, rollout schema, path variant, or experiment-specific telemetry The companion internal PR enables `TCP_NODELAY` on accepted Rendezvous sockets: https://github.com/openai/openai/pull/1082463 ## Why Rendezvous carries small, latency-sensitive relay and JSON-RPC frames. Three staging runs of 30 steady-state `process/read` calls per configuration measured p50 improving from 139.1 ms to 81.5 ms and p95 from 162.0 ms to 95.8 ms with Nagle disabled. The expected packet overhead is small at the current connection scale. We will use existing latency, error, packet, and CPU monitoring and revert normally if production regresses. ## Rollout and rollback The client and accepted-socket changes can deploy independently. New connections receive the setting as each side deploys. Rollback is a normal code revert; there is no persisted assignment or gate state to unwind. ## Validation - `just test -p codex-exec-server --lib`: 164 passed - `just fix -p codex-exec-server`: passed - `just fmt`: passed - independent final review found no actionable issue --- codex-rs/exec-server/src/client_transport.rs | 4 +++- codex-rs/exec-server/src/remote.rs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/codex-rs/exec-server/src/client_transport.rs b/codex-rs/exec-server/src/client_transport.rs index 752bf27037..1ccfd69152 100644 --- a/codex-rs/exec-server/src/client_transport.rs +++ b/codex-rs/exec-server/src/client_transport.rs @@ -274,7 +274,9 @@ impl ExecServerClient { connect_async_with_config( request, Some(noise_relay_websocket_config()), - /*disable_nagle*/ false, + // Rendezvous sends small, latency-sensitive frames, so avoid Nagle's coalescing delay. + /*disable_nagle*/ + true, ), ) .await diff --git a/codex-rs/exec-server/src/remote.rs b/codex-rs/exec-server/src/remote.rs index f5e233ce9d..58fcd8c654 100644 --- a/codex-rs/exec-server/src/remote.rs +++ b/codex-rs/exec-server/src/remote.rs @@ -561,7 +561,9 @@ async fn connect_rendezvous( connect_async_with_config( request, Some(noise_relay_websocket_config()), - /*disable_nagle*/ false, + // Rendezvous sends small, latency-sensitive frames, so avoid Nagle's coalescing delay. + /*disable_nagle*/ + true, ) .await .map(|(websocket, _)| websocket) From 020828170fb2224f0d7a7a243a1f7d21cc3df5ee Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 30 Jun 2026 09:31:06 -0700 Subject: [PATCH 02/10] [codex] Update safety notice wording (#30645) ## Summary The TUI biosafety block still included obsolete copy telling approved researchers they may be able to apply for Trusted Access. Remove that sentence and update the UI snapshot to match the approved wording. --- codex-rs/tui/src/history_cell/notices.rs | 2 +- ...istory_cell__tests__safety_access_block_event_snapshot.snap | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/history_cell/notices.rs b/codex-rs/tui/src/history_cell/notices.rs index 7462160d8f..cda68aa839 100644 --- a/codex-rs/tui/src/history_cell/notices.rs +++ b/codex-rs/tui/src/history_cell/notices.rs @@ -96,7 +96,7 @@ const SAFETY_ACCESS_BLOCK_LEARN_MORE_URL: &str = "https://help.openai.com/en/art pub(crate) fn new_safety_access_block_event() -> SafetyAccessBlockCell { SafetyAccessBlockCell { - body: "We take extra caution with requests involving biological research and applications that could pose safety risks. Eligible researchers can apply for Trusted Access. If you’re a researcher at an approved organization, you may be able to apply for Trusted Access.", + body: "We take extra caution with requests involving biological research and applications that could pose safety risks. Eligible researchers can apply for Trusted Access.", trusted_access_url: "https://www.openai.com/form/trusted-access-for-biology-research/", } } diff --git a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__safety_access_block_event_snapshot.snap b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__safety_access_block_event_snapshot.snap index b3ff6dc3e7..29e55178c4 100644 --- a/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__safety_access_block_event_snapshot.snap +++ b/codex-rs/tui/src/history_cell/snapshots/codex_tui__history_cell__tests__safety_access_block_event_snapshot.snap @@ -5,8 +5,7 @@ expression: rendered ⓘ This content can't be shown We take extra caution with requests involving biological research and applications that could pose safety risks. Eligible researchers can apply - for Trusted Access. If you’re a researcher at an approved organization, you - may be able to apply for Trusted Access. + for Trusted Access. Trusted Access: https://www.openai.com/form/trusted-access-for-biology- research/ Learn more: https://help.openai.com/en/articles/20001326 From db887d03e1f907467e33271572dffb73bceecd6b Mon Sep 17 00:00:00 2001 From: Dylan Hurd Date: Tue, 30 Jun 2026 14:18:22 -0700 Subject: [PATCH 03/10] fix(core) Remove full text websocket trace (#30757) ## Summary This is a follow-up to https://github.com/openai/codex/pull/29432 to remove one additional trace statement that is not being filtered by https://github.com/openai/codex/pull/29457. ## Testing - [x] Unit tests pass --- codex-rs/codex-api/src/endpoint/responses_websocket.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index 755228a9f5..df41608184 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -43,7 +43,6 @@ use tracing::debug; use tracing::error; use tracing::info; use tracing::instrument; -use tracing::trace; use tungstenite::extensions::ExtensionsConfig; use tungstenite::extensions::compression::deflate::DeflateConfig; use tungstenite::protocol::WebSocketConfig; @@ -782,8 +781,6 @@ async fn send_websocket_request( telemetry: Option<&Arc>, connection_reused: bool, ) -> Result<(), ApiError> { - trace!("websocket request: {request_text}"); - let request_start = Instant::now(); let result = tokio::time::timeout( idle_timeout, From d059658ad1920bdb36e98798f44009a5f4c51735 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 1 Jul 2026 10:59:02 -0700 Subject: [PATCH 04/10] docs: add tag to fenced code block (#30851) Now consistent with the other examples and the syntax highlighting is improved. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77c8d2199c..bb2a9d18c3 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ curl -fsSL https://chatgpt.com/codex/install.sh | sh Run the following on Windows to install Codex CLI: -``` +```shell powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex" ``` From 042e61726d28d79cad6f307b2f3ab085861c2212 Mon Sep 17 00:00:00 2001 From: richardopenai Date: Wed, 1 Jul 2026 14:15:34 -0700 Subject: [PATCH 05/10] [codex] bound Rendezvous WebSocket liveness (#30643) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - require a Pong within 60 seconds for established Noise Rendezvous WebSockets on both the harness and executor - bound steady-state WebSocket writes and harness event delivery so backpressure cannot mask the deadline - classify executor disconnects with bounded reasons and feed them into the existing reconnect metric and structured log - cover silent peers, responsive peers, continuous non-Pong traffic, and local application backpressure ## Why The existing periodic Pings did not track Pongs, so a half-open or blackholed connection could remain stuck until the operating system's TCP timeout. This adds the smallest explicit liveness contract without new spans, RTT histograms, feature flags, or TCP diagnostics. ## Testing - `just test -p codex-exec-server` on devbox `richard-6` — 300 passed, 2 skipped - `just fix -p codex-exec-server` - `just fmt` - independent correctness, performance/security, and YAGNI reviews — no findings --- codex-rs/exec-server/src/lib.rs | 1 + .../exec-server/src/noise_relay/harness.rs | 314 ++++++++++--- .../src/noise_relay/harness_tests.rs | 442 ++++++++++++++++++ codex-rs/exec-server/src/relay.rs | 102 +++- codex-rs/exec-server/src/relay_noise_tests.rs | 81 ++++ codex-rs/exec-server/src/remote.rs | 12 +- .../src/websocket_pong_watchdog.rs | 44 ++ .../src/websocket_pong_watchdog_tests.rs | 34 ++ 8 files changed, 953 insertions(+), 77 deletions(-) create mode 100644 codex-rs/exec-server/src/noise_relay/harness_tests.rs create mode 100644 codex-rs/exec-server/src/websocket_pong_watchdog.rs create mode 100644 codex-rs/exec-server/src/websocket_pong_watchdog_tests.rs diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 0068c2cc1a..882c83230c 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -29,6 +29,7 @@ mod sandboxed_file_system; mod server; mod telemetry; mod trace_context; +mod websocket_pong_watchdog; use codex_exec_server_protocol as protocol; diff --git a/codex-rs/exec-server/src/noise_relay/harness.rs b/codex-rs/exec-server/src/noise_relay/harness.rs index 1989525224..9969a3e130 100644 --- a/codex-rs/exec-server/src/noise_relay/harness.rs +++ b/codex-rs/exec-server/src/noise_relay/harness.rs @@ -6,6 +6,7 @@ //! normal `JsonRpcConnection`. Outbound JSON-RPC is framed and split into Noise //! records; inbound records are reordered before decryption and reassembly. +use futures::FutureExt; use futures::Sink; use futures::SinkExt; use futures::Stream; @@ -24,6 +25,7 @@ use crate::connection::CHANNEL_CAPACITY; use crate::connection::JsonRpcConnection; use crate::connection::JsonRpcConnectionEvent; use crate::connection::JsonRpcTransport; +use crate::connection::WEBSOCKET_KEEPALIVE_INTERVAL; use crate::noise_channel::InitiatorHandshake; use crate::noise_channel::NoiseChannelIdentity; use crate::noise_channel::NoiseChannelPublicKey; @@ -39,6 +41,9 @@ use crate::relay::decode_relay_message_frame; use crate::relay::encode_relay_message_frame; use crate::relay_proto::RelayData; use crate::relay_proto::RelayMessageFrame; +use crate::websocket_pong_watchdog::WEBSOCKET_PONG_TIMEOUT; +use crate::websocket_pong_watchdog::WEBSOCKET_PONG_TIMEOUT_REASON; +use crate::websocket_pong_watchdog::WebSocketPongWatchdog; /// Values that bind one harness websocket to the intended executor registration. /// @@ -58,6 +63,8 @@ pub(crate) struct NoiseHarnessConnectionArgs { // Preserve the availability signal while replacing attacker-controlled reason // text before it reaches disconnect diagnostics. const NOISE_RELAY_RESET_DISCONNECT_REASON: &str = "Noise relay stream reset"; +// Give a Pong already queued behind data a bounded chance to reach the reader. +const MAX_FRAMES_DRAINED_AFTER_PONG_DEADLINE: usize = 32; /// Adapt one harness rendezvous websocket into an authenticated JSON-RPC connection. /// @@ -112,8 +119,7 @@ where &incoming_tx, &disconnected_tx, format!("failed to start Noise relay handshake: {error}"), - ) - .await; + ); return; } }; @@ -147,8 +153,7 @@ where &incoming_tx, &disconnected_tx, "Noise relay websocket ended during handshake".to_string(), - ) - .await; + ); return; }; let message = match incoming_message { @@ -158,8 +163,7 @@ where &incoming_tx, &disconnected_tx, "Noise relay websocket received close frame during handshake".to_string(), - ) - .await; + ); return; } Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_)) => continue, @@ -168,8 +172,7 @@ where &incoming_tx, &disconnected_tx, "Noise relay transport expects binary protobuf frames".to_string(), - ) - .await; + ); return; } Err(error) => { @@ -179,8 +182,7 @@ where format!( "failed to read Noise relay websocket from {connection_label}: {error}" ), - ) - .await; + ); return; } }; @@ -191,8 +193,7 @@ where &incoming_tx, &disconnected_tx, format!("failed to parse Noise relay frame: {error}"), - ) - .await; + ); return; } }; @@ -209,8 +210,7 @@ where &incoming_tx, &disconnected_tx, format!("invalid Noise relay handshake response: {error}"), - ) - .await; + ); return; } }; @@ -228,8 +228,7 @@ where &incoming_tx, &disconnected_tx, format!("Noise relay handshake failed: {error}"), - ) - .await; + ); return; } } @@ -239,8 +238,7 @@ where &incoming_tx, &disconnected_tx, NOISE_RELAY_RESET_DISCONNECT_REASON.to_string(), - ) - .await; + ); return; } Ok( @@ -253,8 +251,7 @@ where &incoming_tx, &disconnected_tx, "Noise relay received data before handshake completion".to_string(), - ) - .await; + ); return; } } @@ -263,57 +260,177 @@ where // After the handshake, each relay sequence maps to exactly one Noise // transport record. Outbound records are encrypted once; inbound // records are reordered and deduplicated before decryption. + let mut websocket = websocket.peekable(); let mut next_outbound_seq = 0u32; let mut inbound_ciphertexts = OrderedCiphertextFrames::default(); let mut inbound_decoder = JsonRpcMessageDecoder::default(); + let mut keepalive = tokio::time::interval_at( + tokio::time::Instant::now() + WEBSOCKET_KEEPALIVE_INTERVAL, + WEBSOCKET_KEEPALIVE_INTERVAL, + ); + keepalive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut pong_watchdog = WebSocketPongWatchdog::new(WEBSOCKET_PONG_TIMEOUT); + let pong_deadline = tokio::time::sleep(WEBSOCKET_PONG_TIMEOUT); + tokio::pin!(pong_deadline); + // Keep one framed message as a cursor. Sending one Noise record per loop + // creates a scheduling point for keepalive and inbound control frames + // without splitting the WebSocket reader and writer. + let mut pending_outbound: Option<(Vec, usize)> = None; + let mut force_incoming = false; + let mut frames_drained_after_pong_deadline = 0usize; 'relay: loop { + // Consume a due tick before the always-ready record arm below can win + // another select iteration and postpone the keepalive. + if pong_watchdog.deadline().is_none() + && keepalive.tick().now_or_never().is_some() + { + if let Err(error) = send_keepalive_ping( + &mut websocket, + &mut pong_watchdog, + pong_deadline.as_mut(), + ) + .await + { + warn!("failed to write Noise relay keepalive ping: {error}"); + break; + } + frames_drained_after_pong_deadline = 0; + continue; + } + + let pong_deadline_expired = pong_watchdog + .deadline() + .is_some_and(|deadline| tokio::time::Instant::now() >= deadline); + // After expiry, inspect only frames already queued. Forcing the peeked + // item through next() makes the 32-frame grace deterministic. + if pong_deadline_expired && !force_incoming { + if frames_drained_after_pong_deadline + < MAX_FRAMES_DRAINED_AFTER_PONG_DEADLINE + && std::pin::Pin::new(&mut websocket) + .peek() + .now_or_never() + .is_some() + { + force_incoming = true; + } else { + warn!( + noise_reason = WEBSOCKET_PONG_TIMEOUT_REASON, + "Noise harness rendezvous websocket disconnected" + ); + send_disconnected( + &incoming_tx, + &disconnected_tx, + WEBSOCKET_PONG_TIMEOUT_REASON.to_string(), + ); + return; + } + } + + // While a Pong is outstanding, drain already-queued inbound traffic + // before the next fragment so a queued Pong cannot sit behind writes. + if !force_incoming + && pong_watchdog.deadline().is_some() + && pending_outbound.is_some() + && std::pin::Pin::new(&mut websocket) + .peek() + .now_or_never() + .is_some() + { + force_incoming = true; + } + tokio::select! { - maybe_message = outgoing_rx.recv() => { + maybe_message = outgoing_rx.recv(), if pending_outbound.is_none() && !force_incoming && !pong_deadline_expired => { let Some(message) = maybe_message else { break; }; - let framed = match frame_jsonrpc_message(&message) { - Ok(framed) => framed, + pending_outbound = Some(match frame_jsonrpc_message(&message) { + Ok(framed) => (framed, 0), Err(error) => { warn!("failed to frame JSON-RPC payload for Noise relay: {error}"); break; } + }); + } + _ = std::future::ready(()), if pending_outbound.is_some() && !force_incoming && !pong_deadline_expired => { + let seq = match take_next_sequence(&mut next_outbound_seq) { + Ok(seq) => seq, + Err(error) => { + warn!("Noise relay sequence exhausted: {error}"); + break 'relay; + } }; - for plaintext_record in framed.chunks(NOISE_RECORD_PLAINTEXT_LEN) { - let seq = match take_next_sequence(&mut next_outbound_seq) { - Ok(seq) => seq, - Err(error) => { - warn!("Noise relay sequence exhausted: {error}"); - break 'relay; - } + let (ciphertext, next_offset, message_complete) = { + let Some((framed, offset)) = pending_outbound.as_ref() else { + continue; }; - let ciphertext = match transport.encrypt(plaintext_record) { + let next_offset = (*offset + NOISE_RECORD_PLAINTEXT_LEN).min(framed.len()); + let ciphertext = match transport.encrypt(&framed[*offset..next_offset]) { Ok(ciphertext) => ciphertext, Err(error) => { warn!("failed to encrypt JSON-RPC payload for Noise relay: {error}"); break 'relay; } }; - let frame = RelayMessageFrame::data(stream_id.clone(), seq, ciphertext); - if let Err(error) = websocket - .send(Message::Binary(encode_relay_message_frame(&frame).into())) - .await - { - warn!("failed to write Noise relay websocket: {error}"); - break 'relay; - } + (ciphertext, next_offset, next_offset == framed.len()) + }; + let frame = RelayMessageFrame::data(stream_id.clone(), seq, ciphertext); + // A Pong can arrive after the readiness check while this write owns the + // combined sink and stream. A single bounded record can therefore hit the + // deadline and disconnect with that Pong queued. Treat that as write + // backpressure; this loop yields only between records. + if let Err(error) = send_websocket_message( + &mut websocket, + Message::Binary(encode_relay_message_frame(&frame).into()), + pong_watchdog.write_deadline(tokio::time::Instant::now()), + ) + .await + { + warn!("failed to write Noise relay websocket: {error}"); + break 'relay; + } + if message_complete { + pending_outbound = None; + } else if let Some((_framed, offset)) = pending_outbound.as_mut() { + *offset = next_offset; } } + _ = &mut pong_deadline, if pong_watchdog.deadline().is_some() && !force_incoming => { + continue; + } + _ = keepalive.tick(), if pong_watchdog.deadline().is_none() => { + if let Err(error) = send_keepalive_ping( + &mut websocket, + &mut pong_watchdog, + pong_deadline.as_mut(), + ) + .await + { + warn!("failed to write Noise relay keepalive ping: {error}"); + break; + } + frames_drained_after_pong_deadline = 0; + } incoming_message = websocket.next() => { + force_incoming = false; let Some(incoming_message) = incoming_message else { break; }; + // Count each completed read after expiry. If only the deadline arm + // advanced this counter, reads won by a simultaneously ready incoming + // arm would not count toward the 32-frame cap. + if pong_watchdog + .deadline() + .is_some_and(|deadline| tokio::time::Instant::now() >= deadline) + { + frames_drained_after_pong_deadline += 1; + } match incoming_message { Ok(Message::Binary(payload)) => { let frame = match decode_relay_message_frame(payload.as_ref()) { Ok(frame) => frame, Err(error) => { - send_malformed(&incoming_tx, error.to_string()).await; + send_malformed(&incoming_tx, error.to_string()); break; } }; @@ -325,7 +442,7 @@ where let data = match frame.into_data() { Ok(data) => data, Err(error) => { - send_malformed(&incoming_tx, error.to_string()).await; + send_malformed(&incoming_tx, error.to_string()); break; } }; @@ -334,22 +451,26 @@ where &mut transport, &mut inbound_decoder, data, + pong_watchdog.write_deadline(tokio::time::Instant::now()), &incoming_tx, ) .await { - send_malformed(&incoming_tx, error.to_string()).await; + if matches!(error, ExecServerError::Closed) { + break; + } + send_malformed(&incoming_tx, error.to_string()); break; } } Ok(RelayFrameBodyKind::Reset) => { - let _ = incoming_tx - .send(JsonRpcConnectionEvent::Disconnected { + let _ = incoming_tx.try_send( + JsonRpcConnectionEvent::Disconnected { reason: Some( NOISE_RELAY_RESET_DISCONNECT_REASON.to_string(), ), - }) - .await; + }, + ); break; } Ok( @@ -361,20 +482,22 @@ where send_malformed( &incoming_tx, "Noise relay received invalid post-handshake frame".to_string(), - ) - .await; + ); break; } } } Ok(Message::Close(_)) => break, - Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_)) => {} + Ok(Message::Pong(_)) => { + pong_watchdog.received_pong(); + frames_drained_after_pong_deadline = 0; + } + Ok(Message::Ping(_) | Message::Frame(_)) => {} Ok(Message::Text(_)) => { send_malformed( &incoming_tx, "Noise relay transport expects binary protobuf frames".to_string(), - ) - .await; + ); break; } Err(error) => { @@ -398,6 +521,46 @@ where } } +async fn send_websocket_message( + websocket: &mut T, + message: Message, + deadline: tokio::time::Instant, +) -> Result<(), String> +where + T: Sink + Unpin, + E: std::fmt::Display, +{ + match tokio::time::timeout_at(deadline, websocket.send(message)).await { + Ok(Ok(())) => Ok(()), + Ok(Err(error)) => Err(error.to_string()), + Err(_) => Err("websocket write timed out".to_string()), + } +} + +async fn send_keepalive_ping( + websocket: &mut T, + pong_watchdog: &mut WebSocketPongWatchdog, + pong_deadline: std::pin::Pin<&mut tokio::time::Sleep>, +) -> Result<(), String> +where + T: Sink + Unpin, + E: std::fmt::Display, +{ + send_websocket_message( + websocket, + Message::Ping(Vec::new().into()), + pong_watchdog.write_deadline(tokio::time::Instant::now()), + ) + .await?; + // Start the response clock after the Ping flushes; waiting for sink capacity + // is governed by the write deadline above. + pong_watchdog.ping_sent(tokio::time::Instant::now()); + if let Some(deadline) = pong_watchdog.deadline() { + pong_deadline.reset(deadline); + } + Ok(()) +} + /// Order and decrypt one relay frame, then emit any complete JSON-RPC messages. /// Relay records and JSON-RPC messages do not share boundaries, so reassembly /// happens after decryption. @@ -406,6 +569,7 @@ async fn receive_data( transport: &mut NoiseTransport, decoder: &mut JsonRpcMessageDecoder, data: RelayData, + delivery_deadline: tokio::time::Instant, incoming_tx: &mpsc::Sender, ) -> Result<(), ExecServerError> { // Ordering must happen before decryption because Noise transport nonces are @@ -419,30 +583,50 @@ async fn receive_data( // The authenticated byte stream can carry partial or multiple JSON-RPC // messages; emit only complete, successfully parsed messages. for message in decoder.push(&plaintext)? { - incoming_tx - .send(JsonRpcConnectionEvent::Message(message)) - .await - .map_err(|_| ExecServerError::Closed)?; + send_incoming_event( + incoming_tx, + JsonRpcConnectionEvent::Message(message), + delivery_deadline, + ) + .await?; } } Ok(()) } -async fn send_malformed(incoming_tx: &mpsc::Sender, reason: String) { - let _ = incoming_tx - .send(JsonRpcConnectionEvent::MalformedMessage { reason }) - .await; +async fn send_incoming_event( + incoming_tx: &mpsc::Sender, + event: JsonRpcConnectionEvent, + deadline: tokio::time::Instant, +) -> Result<(), ExecServerError> { + match tokio::time::timeout_at(deadline, incoming_tx.send(event)).await { + Ok(Ok(())) => Ok(()), + Ok(Err(_)) => Err(ExecServerError::Closed), + Err(_) => { + warn!( + noise_reason = "application_backpressure", + "Noise harness application event delivery timed out" + ); + Err(ExecServerError::Closed) + } + } } -async fn send_disconnected( +fn send_malformed(incoming_tx: &mpsc::Sender, reason: String) { + let _ = incoming_tx.try_send(JsonRpcConnectionEvent::MalformedMessage { reason }); +} + +fn send_disconnected( incoming_tx: &mpsc::Sender, disconnected_tx: &watch::Sender, reason: String, ) { let _ = disconnected_tx.send(true); - let _ = incoming_tx - .send(JsonRpcConnectionEvent::Disconnected { - reason: Some(reason), - }) - .await; + let _ = incoming_tx.try_send(JsonRpcConnectionEvent::Disconnected { + reason: Some(reason), + }); } + +#[cfg(test)] +#[path = "harness_tests.rs"] +mod tests; diff --git a/codex-rs/exec-server/src/noise_relay/harness_tests.rs b/codex-rs/exec-server/src/noise_relay/harness_tests.rs new file mode 100644 index 0000000000..ce26a8ca8c --- /dev/null +++ b/codex-rs/exec-server/src/noise_relay/harness_tests.rs @@ -0,0 +1,442 @@ +use std::pin::Pin; +use std::sync::Arc; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; +use std::task::Context as TaskContext; +use std::task::Poll; +use std::time::Duration; + +use anyhow::Context; +use anyhow::Result; +use codex_exec_server_protocol::JSONRPCMessage; +use codex_exec_server_protocol::JSONRPCRequest; +use codex_exec_server_protocol::RequestId; +use futures::Sink; +use futures::SinkExt; +use futures::StreamExt; +use futures::channel::mpsc as futures_mpsc; +use pretty_assertions::assert_eq; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tokio::time::Instant; +use tokio::time::timeout; +use tokio_tungstenite::accept_async; +use tokio_tungstenite::connect_async; +use tokio_tungstenite::tungstenite::Message; + +use super::*; +use crate::connection::JsonRpcConnectionEvent; +use crate::noise_channel::PendingResponderHandshake; + +const ENVIRONMENT_ID: &str = "environment-1"; +const EXECUTOR_REGISTRATION_ID: &str = "registration-1"; + +#[tokio::test] +async fn fragmented_writes_yield_to_keepalive_and_queued_pong() -> Result<()> { + let (connection, mut control, mut outbound_rx) = connected_controlled_harness().await?; + + connection + .outgoing_tx + .send(JSONRPCMessage::Request(JSONRPCRequest { + id: RequestId::Integer(1), + method: "large".to_string(), + params: Some(serde_json::json!({ + "payload": "x".repeat(NOISE_RECORD_PLAINTEXT_LEN * 3), + })), + trace: None, + })) + .await?; + + control.wait_for_blocked_write(/*expected*/ 1).await?; + tokio::time::sleep(WEBSOCKET_KEEPALIVE_INTERVAL + Duration::from_millis(10)).await; + control.grant_writes(/*count*/ 1); + let first_data = read_outbound_data(&mut outbound_rx).await?; + assert_eq!(first_data.seq, 0); + + control.wait_for_blocked_write(/*expected*/ 2).await?; + control.grant_writes(/*count*/ 1); + let Message::Ping(ping_payload) = timeout(Duration::from_secs(1), outbound_rx.next()) + .await? + .context("harness closed before sending keepalive")? + else { + anyhow::bail!("expected keepalive between fragmented writes"); + }; + + control.wait_for_blocked_write(/*expected*/ 3).await?; + control.send_inbound(Message::Pong(ping_payload))?; + tokio::time::sleep(WEBSOCKET_KEEPALIVE_INTERVAL + Duration::from_millis(10)).await; + control.grant_writes(/*count*/ 1); + let second_data = read_outbound_data(&mut outbound_rx).await?; + assert_eq!(second_data.seq, 1); + + control.wait_for_blocked_write(/*expected*/ 4).await?; + control.grant_writes(/*count*/ 1); + let next_message = timeout(Duration::from_secs(1), outbound_rx.next()) + .await? + .context("harness closed after receiving queued Pong")?; + assert!(matches!(next_message, Message::Ping(_))); + + for task in &connection.task_handles { + task.abort(); + } + Ok(()) +} + +#[tokio::test(flavor = "current_thread")] +async fn post_deadline_drain_stops_before_frame_33() -> Result<()> { + let (mut connection, mut control, mut outbound_rx) = connected_controlled_harness().await?; + + control.wait_for_blocked_write(/*expected*/ 1).await?; + control.grant_writes(/*count*/ 1); + let Message::Ping(ping_payload) = timeout(Duration::from_secs(1), outbound_rx.next()) + .await? + .context("harness closed before sending keepalive")? + else { + anyhow::bail!("expected keepalive ping"); + }; + let reads_before_deadline = control.inbound_reads(); + + let unrelated_frame = + encode_relay_message_frame(&RelayMessageFrame::resume("unrelated-stream".to_string())); + for _ in 0..MAX_FRAMES_DRAINED_AFTER_PONG_DEADLINE { + control.send_inbound(Message::Binary(unrelated_frame.clone().into()))?; + } + control.send_inbound(Message::Pong(ping_payload))?; + + // Keep the current-thread runtime from consuming the queued frames until the + // Pong deadline and every frame are ready together. + std::thread::sleep(WEBSOCKET_PONG_TIMEOUT + Duration::from_millis(10)); + + let event = timeout(Duration::from_secs(1), connection.incoming_rx.recv()).await?; + let Some(JsonRpcConnectionEvent::Disconnected { reason }) = event else { + anyhow::bail!("expected Pong timeout, got {event:?}"); + }; + assert_eq!(reason.as_deref(), Some(WEBSOCKET_PONG_TIMEOUT_REASON)); + assert_eq!( + control.inbound_reads() - reads_before_deadline, + MAX_FRAMES_DRAINED_AFTER_PONG_DEADLINE + ); + Ok(()) +} + +#[tokio::test] +async fn pong_keeps_harness_alive_until_peer_stops_responding() -> Result<()> { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let websocket_url = format!("ws://{}", listener.local_addr()?); + let harness_connection = tokio::spawn(connect_async(websocket_url)); + let (socket, _peer_addr) = listener.accept().await?; + let mut executor_websocket = accept_async(socket).await?; + let (harness_websocket, _response) = harness_connection.await??; + + let executor_identity = NoiseChannelIdentity::generate()?; + let mut connection = noise_harness_connection_from_websocket( + harness_websocket, + NoiseHarnessConnectionArgs { + connection_label: "test rendezvous".to_string(), + environment_id: ENVIRONMENT_ID.to_string(), + executor_registration_id: EXECUTOR_REGISTRATION_ID.to_string(), + identity: NoiseChannelIdentity::generate()?, + responder_public_key: executor_identity.public_key(), + harness_key_authorization: "authorization".to_string(), + }, + ); + + let resume_message = timeout(Duration::from_secs(1), executor_websocket.next()) + .await? + .context("harness closed before sending resume")??; + let Message::Binary(resume_payload) = resume_message else { + anyhow::bail!("expected resume frame, got {resume_message:?}"); + }; + let resume = decode_relay_message_frame(resume_payload.as_ref())?; + assert_eq!(resume.validate()?, RelayFrameBodyKind::Resume); + + let handshake_message = timeout(Duration::from_secs(1), executor_websocket.next()) + .await? + .context("harness closed before sending handshake")??; + let Message::Binary(handshake_payload) = handshake_message else { + anyhow::bail!("expected handshake frame, got {handshake_message:?}"); + }; + let handshake = decode_relay_message_frame(handshake_payload.as_ref())?; + assert_eq!(handshake.stream_id, resume.stream_id); + let stream_id = handshake.stream_id.clone(); + let prologue = + noise_channel_prologue(ENVIRONMENT_ID, EXECUTOR_REGISTRATION_ID, stream_id.as_str()); + let pending = PendingResponderHandshake::read_request( + &executor_identity, + &prologue, + &handshake.into_handshake_payload()?, + )?; + let (_transport, response) = pending.complete()?; + let response = RelayMessageFrame::handshake(stream_id, response); + executor_websocket + .send(Message::Binary( + encode_relay_message_frame(&response).into(), + )) + .await?; + + let mut pings = 0; + while pings < 6 { + let message = timeout(Duration::from_secs(1), executor_websocket.next()) + .await? + .context("harness disconnected before six keepalive pings")??; + match message { + Message::Ping(payload) => { + executor_websocket.send(Message::Pong(payload)).await?; + pings += 1; + } + Message::Pong(_) | Message::Frame(_) => {} + message => anyhow::bail!("expected keepalive ping, got {message:?}"), + } + } + + // Keep non-Pong traffic flowing after responses stop. It must not defeat + // the bounded grace for a Pong already queued behind data. + let unrelated_frame = + encode_relay_message_frame(&RelayMessageFrame::resume("unrelated-stream".to_string())); + let traffic_task = tokio::spawn(async move { + loop { + if executor_websocket + .send(Message::Binary(unrelated_frame.clone().into())) + .await + .is_err() + { + break; + } + tokio::time::sleep(Duration::from_millis(5)).await; + } + }); + let event = timeout(Duration::from_secs(1), connection.incoming_rx.recv()).await?; + traffic_task.abort(); + let _ = traffic_task.await; + let Some(JsonRpcConnectionEvent::Disconnected { reason }) = event else { + anyhow::bail!("expected pong timeout, got {event:?}"); + }; + assert_eq!(reason.as_deref(), Some(WEBSOCKET_PONG_TIMEOUT_REASON)); + Ok(()) +} + +#[tokio::test] +async fn application_event_delivery_is_bounded() -> Result<()> { + let (incoming_tx, _incoming_rx) = mpsc::channel(1); + incoming_tx + .send(JsonRpcConnectionEvent::MalformedMessage { + reason: "fill queue".to_string(), + }) + .await?; + + let result = send_incoming_event( + &incoming_tx, + JsonRpcConnectionEvent::MalformedMessage { + reason: "blocked event".to_string(), + }, + Instant::now() + Duration::from_millis(10), + ) + .await; + + assert!(matches!(result, Err(ExecServerError::Closed))); + Ok(()) +} + +async fn read_outbound_data( + outbound_rx: &mut futures_mpsc::UnboundedReceiver, +) -> Result { + let Message::Binary(payload) = timeout(Duration::from_secs(1), outbound_rx.next()) + .await? + .context("harness closed before sending data")? + else { + anyhow::bail!("expected relay data frame"); + }; + let frame = decode_relay_message_frame(payload.as_ref())?; + assert_eq!(frame.validate()?, RelayFrameBodyKind::Data); + frame.into_data().map_err(anyhow::Error::from) +} + +async fn connected_controlled_harness() -> Result<( + JsonRpcConnection, + ControlledWebSocketHandle, + futures_mpsc::UnboundedReceiver, +)> { + let (websocket, control, mut outbound_rx) = ControlledWebSocket::new(/*write_permits*/ 2); + let executor_identity = NoiseChannelIdentity::generate()?; + let connection = noise_harness_connection_from_websocket( + websocket, + NoiseHarnessConnectionArgs { + connection_label: "test rendezvous".to_string(), + environment_id: ENVIRONMENT_ID.to_string(), + executor_registration_id: EXECUTOR_REGISTRATION_ID.to_string(), + identity: NoiseChannelIdentity::generate()?, + responder_public_key: executor_identity.public_key(), + harness_key_authorization: "authorization".to_string(), + }, + ); + + let Message::Binary(resume_payload) = timeout(Duration::from_secs(1), outbound_rx.next()) + .await? + .context("harness closed before sending resume")? + else { + anyhow::bail!("expected resume frame"); + }; + let resume = decode_relay_message_frame(resume_payload.as_ref())?; + let Message::Binary(handshake_payload) = timeout(Duration::from_secs(1), outbound_rx.next()) + .await? + .context("harness closed before sending handshake")? + else { + anyhow::bail!("expected handshake frame"); + }; + let handshake = decode_relay_message_frame(handshake_payload.as_ref())?; + let stream_id = handshake.stream_id.clone(); + assert_eq!(stream_id, resume.stream_id); + let prologue = + noise_channel_prologue(ENVIRONMENT_ID, EXECUTOR_REGISTRATION_ID, stream_id.as_str()); + let pending = PendingResponderHandshake::read_request( + &executor_identity, + &prologue, + &handshake.into_handshake_payload()?, + )?; + let (_transport, response) = pending.complete()?; + control.send_inbound(Message::Binary( + encode_relay_message_frame(&RelayMessageFrame::handshake(stream_id, response)).into(), + ))?; + Ok((connection, control, outbound_rx)) +} + +struct ControlledWebSocket { + inbound_rx: futures_mpsc::UnboundedReceiver>, + outbound_tx: futures_mpsc::UnboundedSender, + write_permit_rx: futures_mpsc::UnboundedReceiver<()>, + blocked_write_tx: futures_mpsc::UnboundedSender, + write_waiting: bool, + blocked_writes: usize, + inbound_reads: Arc, +} + +struct ControlledWebSocketHandle { + inbound_tx: futures_mpsc::UnboundedSender>, + write_permit_tx: futures_mpsc::UnboundedSender<()>, + blocked_write_rx: futures_mpsc::UnboundedReceiver, + inbound_reads: Arc, +} + +impl ControlledWebSocket { + fn new( + write_permits: usize, + ) -> ( + Self, + ControlledWebSocketHandle, + futures_mpsc::UnboundedReceiver, + ) { + let (inbound_tx, inbound_rx) = futures_mpsc::unbounded(); + let (outbound_tx, outbound_rx) = futures_mpsc::unbounded(); + let (write_permit_tx, write_permit_rx) = futures_mpsc::unbounded(); + let (blocked_write_tx, blocked_write_rx) = futures_mpsc::unbounded(); + for _ in 0..write_permits { + write_permit_tx + .unbounded_send(()) + .expect("test write permit receiver should stay open"); + } + let inbound_reads = Arc::new(AtomicUsize::new(0)); + ( + Self { + inbound_rx, + outbound_tx, + write_permit_rx, + blocked_write_tx, + write_waiting: false, + blocked_writes: 0, + inbound_reads: Arc::clone(&inbound_reads), + }, + ControlledWebSocketHandle { + inbound_tx, + write_permit_tx, + blocked_write_rx, + inbound_reads, + }, + outbound_rx, + ) + } +} + +impl ControlledWebSocketHandle { + fn send_inbound(&self, message: Message) -> Result<()> { + self.inbound_tx + .unbounded_send(Ok(message)) + .map_err(anyhow::Error::from) + } + + fn grant_writes(&self, count: usize) { + for _ in 0..count { + self.write_permit_tx + .unbounded_send(()) + .expect("test write permit receiver should stay open"); + } + } + + fn inbound_reads(&self) -> usize { + self.inbound_reads.load(Ordering::Acquire) + } + + async fn wait_for_blocked_write(&mut self, expected: usize) -> Result<()> { + let actual = timeout(Duration::from_secs(1), self.blocked_write_rx.next()) + .await? + .context("websocket closed before blocking the expected write")?; + assert_eq!(actual, expected); + Ok(()) + } +} + +impl Sink for ControlledWebSocket { + type Error = std::convert::Infallible; + + fn poll_ready(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let this = self.get_mut(); + match Pin::new(&mut this.write_permit_rx).poll_next(cx) { + Poll::Ready(Some(())) => { + this.write_waiting = false; + Poll::Ready(Ok(())) + } + Poll::Ready(None) | Poll::Pending => { + if !this.write_waiting { + this.write_waiting = true; + this.blocked_writes += 1; + this.blocked_write_tx + .unbounded_send(this.blocked_writes) + .expect("test blocked-write receiver should stay open"); + } + Poll::Pending + } + } + } + + fn start_send(self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> { + self.outbound_tx + .unbounded_send(item) + .expect("test outbound receiver should stay open"); + Ok(()) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut TaskContext<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } +} + +impl futures::Stream for ControlledWebSocket { + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll> { + let result = Pin::new(&mut self.inbound_rx).poll_next(cx); + if matches!(result, Poll::Ready(Some(_))) { + self.inbound_reads.fetch_add(1, Ordering::Release); + } + result + } +} diff --git a/codex-rs/exec-server/src/relay.rs b/codex-rs/exec-server/src/relay.rs index d7e3d9c2a7..b8d477f5db 100644 --- a/codex-rs/exec-server/src/relay.rs +++ b/codex-rs/exec-server/src/relay.rs @@ -41,6 +41,9 @@ use crate::relay_proto::RelayReset; use crate::relay_proto::RelayResume; use crate::relay_proto::relay_message_frame; use crate::server::ConnectionProcessor; +use crate::websocket_pong_watchdog::WEBSOCKET_PONG_TIMEOUT; +use crate::websocket_pong_watchdog::WEBSOCKET_PONG_TIMEOUT_REASON; +use crate::websocket_pong_watchdog::WebSocketPongWatchdog; const RELAY_MESSAGE_FRAME_VERSION: u32 = 1; const MAX_ACTIVE_NOISE_RELAY_STREAMS: usize = 128; @@ -49,6 +52,27 @@ const MAX_HARNESS_KEY_AUTHORIZATION_BYTES: usize = 4096; const MAX_PENDING_HANDSHAKE_VALIDATIONS: usize = 32; const HARNESS_KEY_VALIDATION_TIMEOUT: Duration = Duration::from_secs(10); +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) enum RendezvousDisconnectReason { + PeerClose, + ReadError, + WriteError, + PongTimeout, + LocalShutdown, +} + +impl RendezvousDisconnectReason { + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::PeerClose => "peer_close", + Self::ReadError => "read_error", + Self::WriteError => "write_error", + Self::PongTimeout => WEBSOCKET_PONG_TIMEOUT_REASON, + Self::LocalShutdown => "local_shutdown", + } + } +} + #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub(crate) enum RelayFrameBodyKind { Data, @@ -448,7 +472,8 @@ pub(crate) async fn run_multiplexed_environment( executor_registration_id: String, identity: NoiseChannelIdentity, validator: V, -) where +) -> RendezvousDisconnectReason +where S: AsyncRead + AsyncWrite + Unpin + Send + 'static, V: HarnessKeyValidator + Clone + 'static, { @@ -461,6 +486,7 @@ pub(crate) async fn run_multiplexed_environment( mpsc::channel::>(CHANNEL_CAPACITY); let (closed_stream_tx, mut closed_stream_rx) = mpsc::channel::(MAX_ACTIVE_NOISE_RELAY_STREAMS); + let (pong_tx, mut pong_rx) = mpsc::channel(1); // Use a separate writer so this loop never waits on the channel it drains. let mut physical_writer_task = tokio::spawn(async move { let mut keepalive = tokio::time::interval_at( @@ -468,19 +494,61 @@ pub(crate) async fn run_multiplexed_environment( WEBSOCKET_KEEPALIVE_INTERVAL, ); keepalive.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut pong_watchdog = WebSocketPongWatchdog::new(WEBSOCKET_PONG_TIMEOUT); + let pong_deadline = tokio::time::sleep(WEBSOCKET_PONG_TIMEOUT); + tokio::pin!(pong_deadline); loop { let message = tokio::select! { + pong = pong_rx.recv() => { + let Some(()) = pong else { + break RendezvousDisconnectReason::LocalShutdown; + }; + pong_watchdog.received_pong(); + continue; + } + _ = &mut pong_deadline, if pong_watchdog.deadline().is_some() => { + match pong_rx.try_recv() { + Ok(()) => { + pong_watchdog.received_pong(); + continue; + } + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => { + break RendezvousDisconnectReason::PongTimeout; + } + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => { + break RendezvousDisconnectReason::LocalShutdown; + } + } + } + _ = keepalive.tick(), if pong_watchdog.deadline().is_none() => { + Message::Ping(Vec::new().into()) + } encoded = physical_outgoing_rx.recv() => { let Some(encoded) = encoded else { - break; + break RendezvousDisconnectReason::LocalShutdown; }; Message::Binary(encoded.into()) } - _ = keepalive.tick() => Message::Ping(Vec::new().into()), }; - if let Err(error) = websocket_sink.send(message).await { - warn!("Noise multiplexed environment websocket write failed: {error}"); - break; + let is_keepalive_ping = matches!(message, Message::Ping(_)); + let write_deadline = pong_watchdog.write_deadline(tokio::time::Instant::now()); + match tokio::time::timeout_at(write_deadline, websocket_sink.send(message)).await { + Ok(Ok(())) => { + if is_keepalive_ping { + pong_watchdog.ping_sent(tokio::time::Instant::now()); + if let Some(deadline) = pong_watchdog.deadline() { + pong_deadline.as_mut().reset(deadline); + } + } + } + Ok(Err(error)) => { + warn!("Noise multiplexed environment websocket write failed: {error}"); + break RendezvousDisconnectReason::WriteError; + } + Err(_) => { + warn!("Noise multiplexed environment websocket write timed out"); + break RendezvousDisconnectReason::WriteError; + } } } }); @@ -489,13 +557,18 @@ pub(crate) async fn run_multiplexed_environment( let mut validation_tasks: JoinSet = JoinSet::new(); let mut failed_handshakes = 0usize; let mut next_validation_id = 0u64; + let mut disconnect_reason = RendezvousDisconnectReason::LocalShutdown; loop { // Registry calls run separately so a slow check does not block the relay. let frame = tokio::select! { writer_result = &mut physical_writer_task => { - if let Err(error) = writer_result { - warn!("Noise multiplexed environment websocket writer failed: {error}"); + match writer_result { + Ok(reason) => disconnect_reason = reason, + Err(error) => { + warn!("Noise multiplexed environment websocket writer failed: {error}"); + disconnect_reason = RendezvousDisconnectReason::LocalShutdown; + } } break; } @@ -621,14 +694,22 @@ pub(crate) async fn run_multiplexed_environment( continue; } }, - Some(Ok(Message::Close(_))) | None => break, - Some(Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_))) => continue, + Some(Ok(Message::Close(_))) | None => { + disconnect_reason = RendezvousDisconnectReason::PeerClose; + break; + } + Some(Ok(Message::Pong(_))) => { + let _ = pong_tx.try_send(()); + continue; + } + Some(Ok(Message::Ping(_) | Message::Frame(_))) => continue, Some(Ok(Message::Text(_))) => { warn!("dropping non-binary Noise relay frame from harness"); continue; } Some(Err(error)) => { debug!("Noise multiplexed environment websocket read failed: {error}"); + disconnect_reason = RendezvousDisconnectReason::ReadError; break; } } @@ -802,6 +883,7 @@ pub(crate) async fn run_multiplexed_environment( physical_writer_task.abort(); let _ = physical_writer_task.await; } + disconnect_reason } /// Charge one failed authenticated-channel attempt to this physical relay. diff --git a/codex-rs/exec-server/src/relay_noise_tests.rs b/codex-rs/exec-server/src/relay_noise_tests.rs index 0020efe347..704c48d9a1 100644 --- a/codex-rs/exec-server/src/relay_noise_tests.rs +++ b/codex-rs/exec-server/src/relay_noise_tests.rs @@ -17,6 +17,7 @@ use tokio_tungstenite::tungstenite::Message; use super::HarnessKeyValidator; use super::MAX_FAILED_NOISE_HANDSHAKES; use super::MAX_HARNESS_KEY_AUTHORIZATION_BYTES; +use super::RendezvousDisconnectReason; use super::run_multiplexed_environment; use crate::ExecServerError; use crate::ExecServerRuntimePaths; @@ -33,6 +34,86 @@ use crate::server::ConnectionProcessor; const ENVIRONMENT_ID: &str = "environment-1"; const EXECUTOR_REGISTRATION_ID: &str = "registration-1"; +#[tokio::test] +async fn missing_pong_disconnects_physical_relay() -> Result<()> { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let websocket_url = format!("ws://{}", listener.local_addr()?); + let harness_connection = tokio::spawn(connect_async(websocket_url)); + let (socket, _peer_addr) = listener.accept().await?; + let environment_websocket = accept_async(socket).await?; + let (_harness_websocket, _response) = harness_connection.await??; + + let environment_task = tokio::spawn(run_multiplexed_environment( + environment_websocket, + ConnectionProcessor::new(ExecServerRuntimePaths::new( + std::env::current_exe()?, + /*codex_linux_sandbox_exe*/ None, + )?), + ENVIRONMENT_ID.to_string(), + EXECUTOR_REGISTRATION_ID.to_string(), + NoiseChannelIdentity::generate()?, + BlockingValidator { + calls: Arc::new(AtomicUsize::new(0)), + release: Arc::new(Notify::new()), + }, + )); + + assert_eq!( + timeout(Duration::from_secs(1), environment_task).await??, + RendezvousDisconnectReason::PongTimeout + ); + Ok(()) +} + +#[tokio::test] +async fn pong_keeps_physical_relay_connected() -> Result<()> { + let listener = TcpListener::bind("127.0.0.1:0").await?; + let websocket_url = format!("ws://{}", listener.local_addr()?); + let harness_connection = tokio::spawn(connect_async(websocket_url)); + let (socket, _peer_addr) = listener.accept().await?; + let environment_websocket = accept_async(socket).await?; + let (mut harness_websocket, _response) = harness_connection.await??; + + let environment_task = tokio::spawn(run_multiplexed_environment( + environment_websocket, + ConnectionProcessor::new(ExecServerRuntimePaths::new( + std::env::current_exe()?, + /*codex_linux_sandbox_exe*/ None, + )?), + ENVIRONMENT_ID.to_string(), + EXECUTOR_REGISTRATION_ID.to_string(), + NoiseChannelIdentity::generate()?, + BlockingValidator { + calls: Arc::new(AtomicUsize::new(0)), + release: Arc::new(Notify::new()), + }, + )); + + timeout(Duration::from_secs(1), async { + let mut pings = 0; + while pings < 6 { + match harness_websocket.next().await { + Some(Ok(Message::Ping(payload))) => { + harness_websocket.send(Message::Pong(payload)).await?; + pings += 1; + } + Some(Ok(Message::Pong(_) | Message::Frame(_))) => {} + Some(Ok(message)) => anyhow::bail!("expected keepalive ping, got {message:?}"), + Some(Err(error)) => return Err(error.into()), + None => anyhow::bail!("environment disconnected before six keepalive pings"), + } + } + Ok::<_, anyhow::Error>(()) + }) + .await??; + harness_websocket.close(None).await?; + assert_eq!( + timeout(Duration::from_secs(1), environment_task).await??, + RendezvousDisconnectReason::PeerClose + ); + Ok(()) +} + #[derive(Clone)] struct BlockingValidator { calls: Arc, diff --git a/codex-rs/exec-server/src/remote.rs b/codex-rs/exec-server/src/remote.rs index 58fcd8c654..8870e0efe9 100644 --- a/codex-rs/exec-server/src/remote.rs +++ b/codex-rs/exec-server/src/remote.rs @@ -492,7 +492,7 @@ pub async fn run_remote_environment( noise_outcome = "ok", "Noise executor connected to rendezvous" ); - run_multiplexed_environment( + let disconnect_reason = run_multiplexed_environment( websocket, processor.clone(), response.environment_id.clone(), @@ -505,7 +505,15 @@ pub async fn run_remote_environment( }, ) .await; - config.telemetry.remote_reconnect("disconnected"); + info!( + noise_event = "rendezvous_connection", + noise_outcome = "disconnected", + noise_reason = disconnect_reason.as_str(), + "Noise executor disconnected from rendezvous" + ); + config + .telemetry + .remote_reconnect(disconnect_reason.as_str()); } Err(error) => { let registration_rejected = matches!( diff --git a/codex-rs/exec-server/src/websocket_pong_watchdog.rs b/codex-rs/exec-server/src/websocket_pong_watchdog.rs new file mode 100644 index 0000000000..b656772b3d --- /dev/null +++ b/codex-rs/exec-server/src/websocket_pong_watchdog.rs @@ -0,0 +1,44 @@ +use std::time::Duration; + +use tokio::time::Instant; + +#[cfg(test)] +pub(crate) const WEBSOCKET_PONG_TIMEOUT: Duration = Duration::from_millis(100); +#[cfg(not(test))] +pub(crate) const WEBSOCKET_PONG_TIMEOUT: Duration = Duration::from_secs(60); +pub(crate) const WEBSOCKET_PONG_TIMEOUT_REASON: &str = "pong_timeout"; + +/// Tracks whether a WebSocket peer has acknowledged a keepalive ping. +pub(crate) struct WebSocketPongWatchdog { + timeout: Duration, + deadline: Option, +} + +impl WebSocketPongWatchdog { + pub(crate) fn new(timeout: Duration) -> Self { + Self { + timeout, + deadline: None, + } + } + + pub(crate) fn ping_sent(&mut self, now: Instant) { + self.deadline.get_or_insert(now + self.timeout); + } + + pub(crate) fn deadline(&self) -> Option { + self.deadline + } + + pub(crate) fn write_deadline(&self, now: Instant) -> Instant { + self.deadline.unwrap_or(now + self.timeout) + } + + pub(crate) fn received_pong(&mut self) { + self.deadline = None; + } +} + +#[cfg(test)] +#[path = "websocket_pong_watchdog_tests.rs"] +mod tests; diff --git a/codex-rs/exec-server/src/websocket_pong_watchdog_tests.rs b/codex-rs/exec-server/src/websocket_pong_watchdog_tests.rs new file mode 100644 index 0000000000..2b15a0d952 --- /dev/null +++ b/codex-rs/exec-server/src/websocket_pong_watchdog_tests.rs @@ -0,0 +1,34 @@ +use std::time::Duration; + +use pretty_assertions::assert_eq; +use tokio::time::Instant; + +use super::WebSocketPongWatchdog; + +#[test] +fn repeated_ping_does_not_extend_deadline() { + let started_at = Instant::now(); + let timeout = Duration::from_secs(2); + let mut watchdog = WebSocketPongWatchdog::new(timeout); + + watchdog.ping_sent(started_at); + watchdog.ping_sent(started_at + Duration::from_secs(1)); + assert_eq!( + watchdog.write_deadline(started_at + Duration::from_secs(1)), + started_at + timeout + ); + assert_eq!(watchdog.deadline(), Some(started_at + timeout)); +} + +#[test] +fn pong_starts_a_fresh_deadline() { + let started_at = Instant::now(); + let timeout = Duration::from_secs(2); + let mut watchdog = WebSocketPongWatchdog::new(timeout); + + watchdog.ping_sent(started_at); + watchdog.received_pong(); + assert_eq!(watchdog.deadline(), None); + watchdog.ping_sent(started_at + timeout); + assert_eq!(watchdog.deadline(), Some(started_at + timeout + timeout)); +} From a98a21798c3301cfbeb6c323d6c0f6a804e08a57 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 1 Jul 2026 17:08:11 -0700 Subject: [PATCH 06/10] Consolidate multi-agent v2 communication sends (#30867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why Multi-agent v2 communications currently use separate outbound paths: direct messages, follow-up tasks, and completion results go through `send_inter_agent_communication`, while a spawn's initial message goes through the generic input submission path. That split makes it difficult to add complete communication lifecycle logging in one place. This refactor makes `submit_inter_agent_communication` the common sink for those paths, preparing the follow-up observability work discussed in [#30516](https://github.com/openai/codex/pull/30516). ## What changed - Routed all current outbound `InterAgentCommunication` paths in `AgentControl`—direct messages, follow-up tasks, completion results, and multi-agent v2 spawn initial messages—through `submit_inter_agent_communication`. - Centralized the actual submission and last-task-message bookkeeping there, providing one place for the follow-up PR to instrument communication creation and successful enqueue. - Left non-communication input handling and the multi-agent v1 spawn flow unchanged. ## Testing - `just test -p codex-core 'agent::control::tests::'` (51 passed) - `just test -p codex-core 'suite::subagent_notifications::encrypted_multi_agent_v2_spawn_sends_agent_message_to_child'` (passed) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/30867). * #30872 * __->__ #30867 --- codex-rs/core/src/agent/control.rs | 34 +++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 49bbab0f87..8de6443402 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -154,12 +154,13 @@ impl AgentControl { state: &Arc, initial_operation: Op, ) -> CodexResult { - let last_task_message = match &initial_operation { - Op::InterAgentCommunication { communication } => { - last_task_message_from_communication(communication) - } - _ => non_empty_task_message(render_input_preview(&initial_operation)), - }; + if let Op::InterAgentCommunication { communication } = initial_operation { + return self + .submit_inter_agent_communication(agent_id, state, communication) + .await; + } + + let last_task_message = non_empty_task_message(render_input_preview(&initial_operation)); let result = self .handle_thread_request_result( agent_id, @@ -182,13 +183,26 @@ impl AgentControl { &self, agent_id: ThreadId, communication: InterAgentCommunication, + ) -> CodexResult { + self.send_input(agent_id, Op::InterAgentCommunication { communication }) + .await + } + + async fn submit_inter_agent_communication( + &self, + agent_id: ThreadId, + state: &Arc, + communication: InterAgentCommunication, ) -> CodexResult { let last_task_message = last_task_message_from_communication(&communication); - let state = self.upgrade()?; - let op = Op::InterAgentCommunication { communication }; - self.ensure_execution_capacity_for_op(agent_id, &op).await?; let result = self - .handle_thread_request_result(agent_id, &state, state.send_op(agent_id, op).await) + .handle_thread_request_result( + agent_id, + state, + state + .send_op(agent_id, Op::InterAgentCommunication { communication }) + .await, + ) .await; if result.is_ok() { match last_task_message { From 129ea2aaf5fb426d8ba683ee53f290742f41dd31 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 1 Jul 2026 18:11:09 -0700 Subject: [PATCH 07/10] Log multi-agent communication lifecycle (#30872) ## Why [#30867](https://github.com/openai/codex/pull/30867) makes `submit_inter_agent_communication` the common outbound sink for multi-agent v2 communications. This follow-up uses that single point to log every communication lifecycle without requiring new hooks as spawn, messaging, follow-up, or result paths evolve. For each communication, the logs need to identify its type, sender and receiver threads, and content, while correlating the successful send with receipt by the destination mailbox. The logging path must not query externally supplied time providers because those calls can be expensive for app-server clients. ## What changed - Added structured `INFO` events on the OpenTelemetry-exported `codex_otel.agent_communication` target for `spawn`, `message`, `followup`, and `result` communications. - Logged successful sends from `submit_inter_agent_communication` with the communication kind, sender and receiver thread IDs, content, and submission ID. - Logged receives after the communication has been enqueued in the receiver mailbox, using the same submission ID. - Avoided time-provider calls and other asynchronous work in the logging path. - Narrowed ordinary spawn and send-input APIs to `Vec` so `Op::InterAgentCommunication` cannot bypass the context-bearing centralized path. The refactor does not change submission IDs, capacity checks, last-task bookkeeping, mailbox ordering, protocol types, rollout data, or model-visible context. ## Event shape Illustrative JSON representation of the two independently emitted records: ```json [ { "event.name": "codex.agent_communication", "communication_id": "019f20e1-40d1-7890-a123-456789abcdef", "kind": "spawn", "state": "send", "sender_thread_id": "019f20df-fbe1-7890-a123-456789abcdef", "receiver_thread_id": "019f20e1-3f79-7890-a123-456789abcdef", "content": "inspect the repository" }, { "event.name": "codex.agent_communication", "communication_id": "019f20e1-40d1-7890-a123-456789abcdef", "state": "receive" } ] ``` Consumers join the receive record to the send record by `communication_id` for the immutable communication metadata. ## Testing - Extended the existing end-to-end multi-agent v2 spawn test to verify content, both thread IDs, and a correlated send/receive submission ID. - Re-ran focused control and handler coverage for direct messages, follow-up tasks, and completion results. --- codex-rs/core/src/agent/control.rs | 97 ++++++++++++------- codex-rs/core/src/agent/control/execution.rs | 11 ++- codex-rs/core/src/agent/control/spawn.rs | 62 ++++++++++-- codex-rs/core/src/agent/control_tests.rs | 32 +++--- codex-rs/core/src/agent_communication.rs | 79 +++++++++++++++ codex-rs/core/src/lib.rs | 1 + codex-rs/core/src/session/handlers.rs | 1 + codex-rs/core/src/session/mod.rs | 6 +- .../core/src/tools/handlers/agent_jobs.rs | 2 +- .../src/tools/handlers/multi_agents_common.rs | 8 +- .../src/tools/handlers/multi_agents_tests.rs | 18 ++-- .../src/tools/handlers/multi_agents_v2.rs | 1 - .../handlers/multi_agents_v2/message_tool.rs | 11 ++- .../tools/handlers/multi_agents_v2/spawn.rs | 54 +++++------ .../tests/suite/subagent_notifications.rs | 56 ++++++++++- 15 files changed, 332 insertions(+), 107 deletions(-) create mode 100644 codex-rs/core/src/agent_communication.rs diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index 8de6443402..85e54e2fb9 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -4,6 +4,8 @@ use crate::agent::registry::AgentRegistry; use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::resolve_role_config; use crate::agent::status::is_final; +use crate::agent_communication::AgentCommunicationContext; +use crate::agent_communication::AgentCommunicationKind; use crate::codex_thread::ThreadConfigSnapshot; use crate::config::Config; use crate::config::RolloutBudgetConfig; @@ -139,12 +141,12 @@ impl AgentControl { pub(crate) async fn send_input( &self, agent_id: ThreadId, - initial_operation: Op, + input: Vec, ) -> CodexResult { let state = self.upgrade()?; - self.ensure_execution_capacity_for_op(agent_id, &initial_operation) + self.ensure_execution_capacity_for_turn_start(agent_id, /*starts_turn*/ true) .await?; - self.send_input_after_capacity_check(agent_id, &state, initial_operation) + self.send_input_after_capacity_check(agent_id, &state, input) .await } @@ -152,20 +154,14 @@ impl AgentControl { &self, agent_id: ThreadId, state: &Arc, - initial_operation: Op, + input: Vec, ) -> CodexResult { - if let Op::InterAgentCommunication { communication } = initial_operation { - return self - .submit_inter_agent_communication(agent_id, state, communication) - .await; - } - - let last_task_message = non_empty_task_message(render_input_preview(&initial_operation)); + let last_task_message = non_empty_task_message(render_input_preview(&input)); let result = self .handle_thread_request_result( agent_id, state, - state.send_op(agent_id, initial_operation).await, + state.send_op(agent_id, input.into()).await, ) .await; if result.is_ok() { @@ -183,8 +179,28 @@ impl AgentControl { &self, agent_id: ThreadId, communication: InterAgentCommunication, + agent_communication_context: AgentCommunicationContext, ) -> CodexResult { - self.send_input(agent_id, Op::InterAgentCommunication { communication }) + let state = self.upgrade()?; + self.ensure_execution_capacity_for_turn_start(agent_id, communication.trigger_turn) + .await?; + self.send_inter_agent_communication_after_capacity_check( + agent_id, + &state, + communication, + agent_communication_context, + ) + .await + } + + async fn send_inter_agent_communication_after_capacity_check( + &self, + agent_id: ThreadId, + state: &Arc, + communication: InterAgentCommunication, + context: AgentCommunicationContext, + ) -> CodexResult { + self.submit_inter_agent_communication(agent_id, state, communication, context) .await } @@ -193,8 +209,11 @@ impl AgentControl { agent_id: ThreadId, state: &Arc, communication: InterAgentCommunication, + context: AgentCommunicationContext, ) -> CodexResult { let last_task_message = last_task_message_from_communication(&communication); + let communication_for_log = + crate::agent_communication::logging_enabled().then(|| communication.clone()); let result = self .handle_thread_request_result( agent_id, @@ -204,6 +223,16 @@ impl AgentControl { .await, ) .await; + if let (Some(communication), Ok(communication_id)) = + (communication_for_log, result.as_ref()) + { + crate::agent_communication::emit_agent_communication_send( + communication_id, + &context, + &communication, + agent_id, + ); + } if result.is_ok() { match last_task_message { Some(last_task_message) => self @@ -494,8 +523,10 @@ impl AgentControl { message, /*trigger_turn*/ false, ); + let context = + AgentCommunicationContext::new(AgentCommunicationKind::Result, child_thread_id); let _ = control - .send_inter_agent_communication(parent_thread_id, communication) + .send_inter_agent_communication(parent_thread_id, communication, context) .await; return; } @@ -720,27 +751,23 @@ fn agent_matches_prefix(agent_path: Option<&AgentPath>, prefix: &AgentPath) -> b }) } -pub(crate) fn render_input_preview(initial_operation: &Op) -> String { - match initial_operation { - Op::UserInput { items, .. } => items - .iter() - .map(|item| match item { - UserInput::Text { text, .. } => text.clone(), - UserInput::Image { .. } => "[image]".to_string(), - UserInput::LocalImage { path, .. } => { - format!("[local_image:{}]", path.display()) - } - UserInput::Skill { name, path, .. } => { - format!("[skill:${name}]({})", path.display()) - } - UserInput::Mention { name, path, .. } => format!("[mention:${name}]({path})"), - _ => "[input]".to_string(), - }) - .collect::>() - .join("\n"), - Op::InterAgentCommunication { communication } => communication.content.clone(), - _ => String::new(), - } +pub(crate) fn render_input_preview(input: &[UserInput]) -> String { + input + .iter() + .map(|item| match item { + UserInput::Text { text, .. } => text.clone(), + UserInput::Image { .. } => "[image]".to_string(), + UserInput::LocalImage { path, .. } => { + format!("[local_image:{}]", path.display()) + } + UserInput::Skill { name, path, .. } => { + format!("[skill:${name}]({})", path.display()) + } + UserInput::Mention { name, path, .. } => format!("[mention:${name}]({path})"), + _ => "[input]".to_string(), + }) + .collect::>() + .join("\n") } fn last_task_message_from_communication(communication: &InterAgentCommunication) -> Option { diff --git a/codex-rs/core/src/agent/control/execution.rs b/codex-rs/core/src/agent/control/execution.rs index 42aaa50e02..d8559c1499 100644 --- a/codex-rs/core/src/agent/control/execution.rs +++ b/codex-rs/core/src/agent/control/execution.rs @@ -32,7 +32,16 @@ impl AgentControl { thread_id: ThreadId, op: &Op, ) -> CodexResult<()> { - if !op_starts_turn(op) { + self.ensure_execution_capacity_for_turn_start(thread_id, op_starts_turn(op)) + .await + } + + pub(super) async fn ensure_execution_capacity_for_turn_start( + &self, + thread_id: ThreadId, + starts_turn: bool, + ) -> CodexResult<()> { + if !starts_turn { return Ok(()); } let state = self.upgrade()?; diff --git a/codex-rs/core/src/agent/control/spawn.rs b/codex-rs/core/src/agent/control/spawn.rs index c4c40577ca..9078434f1f 100644 --- a/codex-rs/core/src/agent/control/spawn.rs +++ b/codex-rs/core/src/agent/control/spawn.rs @@ -9,6 +9,17 @@ struct SpawnAgentThreadInheritance { exec_policy: Option>, } +/// Initial input delivered after a spawned agent acquires execution capacity. +/// +/// V2 communication spawns keep the communication and its context paired so centralized +/// submission and lifecycle logging cannot receive one without the other. Other spawn sources +/// provide user input directly, making an uncontextualized inter-agent communication +/// unrepresentable. +enum SpawnInitialInput { + UserInput(Vec), + InterAgentCommunication(InterAgentCommunication, AgentCommunicationContext), +} + fn default_agent_nickname_list() -> Vec<&'static str> { AGENT_NAMES .lines() @@ -89,12 +100,12 @@ impl AgentControl { pub(crate) async fn spawn_agent( &self, config: Config, - initial_operation: Op, + initial_input: Vec, session_source: Option, ) -> CodexResult { let spawned_agent = Box::pin(self.spawn_agent_internal( config, - initial_operation, + SpawnInitialInput::UserInput(initial_input), session_source, SpawnAgentOptions::default(), )) @@ -106,12 +117,34 @@ impl AgentControl { pub(crate) async fn spawn_agent_with_metadata( &self, config: Config, - initial_operation: Op, + initial_input: Vec, session_source: Option, options: SpawnAgentOptions, // TODO(jif) drop with new fork. ) -> CodexResult { - Box::pin(self.spawn_agent_internal(config, initial_operation, session_source, options)) - .await + Box::pin(self.spawn_agent_internal( + config, + SpawnInitialInput::UserInput(initial_input), + session_source, + options, + )) + .await + } + + pub(crate) async fn spawn_agent_with_communication( + &self, + config: Config, + communication: InterAgentCommunication, + context: AgentCommunicationContext, + session_source: Option, + options: SpawnAgentOptions, + ) -> CodexResult { + Box::pin(self.spawn_agent_internal( + config, + SpawnInitialInput::InterAgentCommunication(communication, context), + session_source, + options, + )) + .await } pub(crate) async fn ensure_v2_agent_loaded( @@ -197,7 +230,7 @@ impl AgentControl { async fn spawn_agent_internal( &self, config: Config, - initial_operation: Op, + initial_input: SpawnInitialInput, session_source: Option, options: SpawnAgentOptions, ) -> CodexResult { @@ -356,8 +389,21 @@ impl AgentControl { ) .await; - self.send_input_after_capacity_check(new_thread.thread_id, &state, initial_operation) - .await?; + match initial_input { + SpawnInitialInput::UserInput(input) => { + self.send_input_after_capacity_check(new_thread.thread_id, &state, input) + .await?; + } + SpawnInitialInput::InterAgentCommunication(communication, context) => { + self.send_inter_agent_communication_after_capacity_check( + new_thread.thread_id, + &state, + communication, + context, + ) + .await?; + } + } if multi_agent_version != MultiAgentVersion::V2 { let child_reference = agent_metadata .agent_path diff --git a/codex-rs/core/src/agent/control_tests.rs b/codex-rs/core/src/agent/control_tests.rs index 9863bfb7e2..4741c3c101 100644 --- a/codex-rs/core/src/agent/control_tests.rs +++ b/codex-rs/core/src/agent/control_tests.rs @@ -3,6 +3,8 @@ use crate::CodexThread; use crate::StateDbHandle; use crate::ThreadManager; use crate::agent::agent_status_from_event; +use crate::agent_communication::AgentCommunicationContext; +use crate::agent_communication::AgentCommunicationKind; use crate::config::AgentRoleConfig; use crate::config::Config; use crate::config::ConfigBuilder; @@ -65,12 +67,11 @@ async fn test_config() -> (TempDir, Config) { test_config_with_cli_overrides(Vec::new()).await } -fn text_input(text: &str) -> Op { +fn text_input(text: &str) -> Vec { vec![UserInput::Text { text: text.to_string(), text_elements: Vec::new(), }] - .into() } fn assistant_message(text: &str, phase: Option) -> ResponseItem { @@ -311,8 +312,7 @@ async fn send_input_errors_when_manager_dropped() { vec![UserInput::Text { text: "hello".to_string(), text_elements: Vec::new(), - }] - .into(), + }], ) .await .expect_err("send_input should fail without a manager"); @@ -423,8 +423,7 @@ async fn send_input_errors_when_thread_missing() { vec![UserInput::Text { text: "hello".to_string(), text_elements: Vec::new(), - }] - .into(), + }], ) .await .expect_err("send_input should fail for missing thread"); @@ -490,8 +489,7 @@ async fn send_input_submits_user_message() { vec![UserInput::Text { text: "hello from tests".to_string(), text_elements: Vec::new(), - }] - .into(), + }], ) .await .expect("send_input should succeed"); @@ -531,7 +529,11 @@ async fn send_inter_agent_communication_without_turn_queues_message_without_trig let submission_id = harness .control - .send_inter_agent_communication(thread_id, communication.clone()) + .send_inter_agent_communication( + thread_id, + communication.clone(), + AgentCommunicationContext::new(AgentCommunicationKind::Message, ThreadId::new()), + ) .await .expect("send_inter_agent_communication should succeed"); assert!(!submission_id.is_empty()); @@ -656,7 +658,11 @@ async fn ensure_v2_agent_loaded_reloads_registered_unloaded_agent() { ); harness .control - .send_inter_agent_communication(spawned_agent.thread_id, communication.clone()) + .send_inter_agent_communication( + spawned_agent.thread_id, + communication.clone(), + AgentCommunicationContext::new(AgentCommunicationKind::Message, ThreadId::new()), + ) .await .expect("send_inter_agent_communication should succeed after reload"); let expected = ( @@ -803,7 +809,11 @@ async fn encrypted_inter_agent_communication_clears_existing_last_task_message() ); harness .control - .send_inter_agent_communication(spawned_agent.thread_id, communication) + .send_inter_agent_communication( + spawned_agent.thread_id, + communication, + AgentCommunicationContext::new(AgentCommunicationKind::Followup, ThreadId::new()), + ) .await .expect("send_inter_agent_communication should succeed"); diff --git a/codex-rs/core/src/agent_communication.rs b/codex-rs/core/src/agent_communication.rs new file mode 100644 index 0000000000..e71f50ad96 --- /dev/null +++ b/codex-rs/core/src/agent_communication.rs @@ -0,0 +1,79 @@ +use codex_protocol::ThreadId; +use codex_protocol::protocol::InterAgentCommunication; + +const AGENT_COMMUNICATION_TARGET: &str = "codex_otel.agent_communication"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum AgentCommunicationKind { + Spawn, + Message, + Followup, + Result, +} + +impl AgentCommunicationKind { + fn as_str(self) -> &'static str { + match self { + Self::Spawn => "spawn", + Self::Message => "message", + Self::Followup => "followup", + Self::Result => "result", + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct AgentCommunicationContext { + kind: AgentCommunicationKind, + sender_thread_id: ThreadId, +} + +impl AgentCommunicationContext { + pub(crate) fn new(kind: AgentCommunicationKind, sender_thread_id: ThreadId) -> Self { + Self { + kind, + sender_thread_id, + } + } +} + +pub(crate) fn logging_enabled() -> bool { + tracing::enabled!(target: AGENT_COMMUNICATION_TARGET, tracing::Level::INFO) +} + +pub(crate) fn emit_agent_communication_send( + communication_id: &str, + context: &AgentCommunicationContext, + communication: &InterAgentCommunication, + receiver_thread_id: ThreadId, +) { + tracing::info!( + target: AGENT_COMMUNICATION_TARGET, + { + event.name = "codex.agent_communication", + communication_id, + kind = context.kind.as_str(), + state = "send", + sender_thread_id = %context.sender_thread_id, + receiver_thread_id = %receiver_thread_id, + content = if communication.content.is_empty() { + communication.encrypted_content.as_deref().unwrap_or_default() + } else { + communication.content.as_str() + }, + }, + "agent communication" + ); +} + +pub(crate) fn emit_agent_communication_receive(communication_id: &str) { + tracing::info!( + target: AGENT_COMMUNICATION_TARGET, + { + event.name = "codex.agent_communication", + communication_id, + state = "receive", + }, + "agent communication" + ); +} diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index e9b749fea9..df8753a386 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -31,6 +31,7 @@ pub use codex_thread::TryStartTurnIfIdleError; pub use codex_thread::TryStartTurnIfIdleRejectionReason; pub use session::turn_context::TurnContext; mod agent; +mod agent_communication; mod attestation; mod codex_delegate; mod command_canonicalization; diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index a483ce5e0f..9733a9f66a 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -285,6 +285,7 @@ pub async fn inter_agent_communication( sess.input_queue .enqueue_mailbox_communication(communication) .await; + crate::agent_communication::emit_agent_communication_receive(&sub_id); if trigger_turn { sess.maybe_start_turn_for_pending_work_with_sub_id(sub_id) .await; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index d43724e5db..809fa92015 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -14,6 +14,8 @@ use crate::agent::AgentControl; use crate::agent::AgentStatus; use crate::agent::agent_status_from_event; use crate::agent::status::is_final; +use crate::agent_communication::AgentCommunicationContext; +use crate::agent_communication::AgentCommunicationKind; use crate::attestation::AttestationProvider; use crate::build_available_skills; use crate::compact; @@ -1867,10 +1869,12 @@ impl Session { message, /*trigger_turn*/ false, ); + let context = + AgentCommunicationContext::new(AgentCommunicationKind::Result, self.thread_id); if let Err(err) = self .services .agent_control - .send_inter_agent_communication(parent_thread_id, communication) + .send_inter_agent_communication(parent_thread_id, communication, context) .await { debug!("failed to notify parent thread {parent_thread_id}: {err}"); diff --git a/codex-rs/core/src/tools/handlers/agent_jobs.rs b/codex-rs/core/src/tools/handlers/agent_jobs.rs index 360744973c..6405414731 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs.rs @@ -203,7 +203,7 @@ async fn run_agent_job_loop( .agent_control .spawn_agent_with_metadata( options.spawn_config.clone(), - items.into(), + items, Some(SessionSource::SubAgent(SubAgentSource::Other(format!( "agent_job:{job_id}" )))), diff --git a/codex-rs/core/src/tools/handlers/multi_agents_common.rs b/codex-rs/core/src/tools/handlers/multi_agents_common.rs index 8f91ce5b8e..12ec89d651 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_common.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_common.rs @@ -18,7 +18,6 @@ use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::ReasoningEffortPreset; use codex_protocol::protocol::CollabAgentRef; use codex_protocol::protocol::CollabAgentStatusEntry; -use codex_protocol::protocol::Op; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; use codex_protocol::user_input::UserInput; @@ -163,7 +162,7 @@ pub(crate) fn thread_spawn_source( pub(crate) fn parse_collab_input( message: Option, items: Option>, -) -> Result { +) -> Result, FunctionCallError> { match (message, items) { (Some(_), Some(_)) => Err(FunctionCallError::RespondToModel( "Provide either message or items, but not both".to_string(), @@ -180,8 +179,7 @@ pub(crate) fn parse_collab_input( Ok(vec![UserInput::Text { text: message, text_elements: Vec::new(), - }] - .into()) + }]) } (None, Some(items)) => { if items.is_empty() { @@ -189,7 +187,7 @@ pub(crate) fn parse_collab_input( "Items can't be empty".to_string(), )); } - Ok(items.into()) + Ok(items) } } } diff --git a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs index 8acb0a8c8d..f313fcf0d9 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_tests.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_tests.rs @@ -1377,8 +1377,7 @@ async fn multi_agent_v2_send_message_accepts_root_target_from_child() { vec![UserInput::Text { text: "inspect this repo".to_string(), text_elements: Vec::new(), - }] - .into(), + }], Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, @@ -1454,8 +1453,7 @@ async fn multi_agent_v2_followup_task_rejects_root_target_from_child() { vec![UserInput::Text { text: "inspect this repo".to_string(), text_elements: Vec::new(), - }] - .into(), + }], Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, @@ -1626,8 +1624,7 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() { vec![UserInput::Text { text: "research".to_string(), text_elements: Vec::new(), - }] - .into(), + }], Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, @@ -1647,8 +1644,7 @@ async fn multi_agent_v2_list_agents_filters_by_relative_path_prefix() { vec![UserInput::Text { text: "build".to_string(), text_elements: Vec::new(), - }] - .into(), + }], Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 2, @@ -4097,8 +4093,7 @@ async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_id() { vec![UserInput::Text { text: "inspect this repo".to_string(), text_elements: Vec::new(), - }] - .into(), + }], Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, @@ -4165,8 +4160,7 @@ async fn multi_agent_v2_interrupt_agent_rejects_self_target_by_task_name() { vec![UserInput::Text { text: "inspect this repo".to_string(), text_elements: Vec::new(), - }] - .into(), + }], Some(SessionSource::SubAgent(SubAgentSource::ThreadSpawn { parent_thread_id: root.thread_id, depth: 1, diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs index cae3497a13..8e7f73a762 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs @@ -19,7 +19,6 @@ use codex_protocol::protocol::CollabWaitingEndEvent; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::SubAgentActivityEvent; use codex_protocol::protocol::SubAgentActivityKind; -use codex_protocol::user_input::UserInput; use codex_tools::ToolName; use serde::Deserialize; use serde::Serialize; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs index 7fdb80e8bc..9501b4355a 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/message_tool.rs @@ -4,6 +4,8 @@ //! resulting `InterAgentCommunication` should wake the target immediately. use super::*; +use crate::agent_communication::AgentCommunicationContext; +use crate::agent_communication::AgentCommunicationKind; use crate::tools::context::FunctionToolOutput; use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::protocol::InterAgentCommunication; @@ -46,7 +48,7 @@ pub(crate) struct FollowupTaskArgs { pub(crate) message: String, } -fn message_content(message: String) -> Result { +pub(super) fn message_content(message: String) -> Result { if message.trim().is_empty() { return Err(FunctionCallError::RespondToModel( "Empty message can't be sent to an agent".to_string(), @@ -101,10 +103,15 @@ pub(crate) async fn handle_message_string_tool( .unwrap_or_else(AgentPath::root); let communication = communication_from_tool_message(author, receiver_agent_path.clone(), message); + let kind = match mode { + MessageDeliveryMode::QueueOnly => AgentCommunicationKind::Message, + MessageDeliveryMode::TriggerTurn => AgentCommunicationKind::Followup, + }; + let context = AgentCommunicationContext::new(kind, session.thread_id); let result = session .services .agent_control - .send_inter_agent_communication(receiver_thread_id, mode.apply(communication)) + .send_inter_agent_communication(receiver_thread_id, mode.apply(communication), context) .await .map_err(|err| collab_agent_error(receiver_thread_id, err)); result?; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 2186a4a6d6..8eeb6ddd1b 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -4,11 +4,13 @@ use crate::agent::control::SpawnAgentOptions; use crate::agent::next_thread_spawn_depth; use crate::agent::role::DEFAULT_ROLE_NAME; use crate::agent::role::apply_role_to_config; +use crate::agent_communication::AgentCommunicationContext; +use crate::agent_communication::AgentCommunicationKind; use crate::tools::handlers::multi_agents_spec::SpawnAgentToolOptions; use crate::tools::handlers::multi_agents_spec::create_spawn_agent_tool_v2; +use crate::tools::handlers::multi_agents_v2::message_tool::message_content; use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::AgentPath; -use codex_protocol::protocol::Op; use codex_tools::ToolSpec; #[derive(Default)] @@ -55,8 +57,7 @@ async fn handle_spawn_agent( .map(str::trim) .filter(|role| !role.is_empty()); - let message = args.message.clone(); - let initial_operation = parse_collab_input(Some(args.message), /*items*/ None)?; + let message = message_content(args.message)?; let session_source = turn.session_source.clone(); let child_depth = next_thread_spawn_depth(&session_source); let mut config = @@ -104,33 +105,28 @@ async fn handle_spawn_agent( "spawned agent is missing a canonical task name".to_string(), ) })?; + let author = turn + .session_source + .get_agent_path() + .unwrap_or_else(AgentPath::root); + let communication = communication_from_tool_message(author, new_agent_path.clone(), message); + let context = AgentCommunicationContext::new(AgentCommunicationKind::Spawn, session.thread_id); let spawned_agent = Box::pin( - session.services.agent_control.spawn_agent_with_metadata( - config, - match initial_operation { - Op::UserInput { items, .. } - if items - .iter() - .all(|item| matches!(item, UserInput::Text { .. })) => - { - let author = turn - .session_source - .get_agent_path() - .unwrap_or_else(AgentPath::root); - let communication = - communication_from_tool_message(author, new_agent_path.clone(), message); - Op::InterAgentCommunication { communication } - } - initial_operation => initial_operation, - }, - Some(spawn_source), - SpawnAgentOptions { - fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()), - fork_mode, - parent_thread_id: Some(session.thread_id), - environments: Some(turn.environments.to_selections()), - }, - ), + session + .services + .agent_control + .spawn_agent_with_communication( + config, + communication, + context, + Some(spawn_source), + SpawnAgentOptions { + fork_parent_spawn_call_id: fork_mode.as_ref().map(|_| call_id.clone()), + fork_mode, + parent_thread_id: Some(session.thread_id), + environments: Some(turn.environments.to_selections()), + }, + ), ) .await .map_err(collab_spawn_error)?; diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 951fb92f17..7424d3f360 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -39,10 +39,13 @@ use serde_json::Value; use serde_json::json; use std::fs; use std::path::Path; +use std::sync::Mutex; use std::time::Duration; use test_case::test_case; use tokio::time::Instant; use tokio::time::sleep; +use tracing::Level; +use tracing_test::internal::MockWriter; use wiremock::MockServer; const SPAWN_CALL_ID: &str = "spawn-call-1"; @@ -96,6 +99,13 @@ fn decoded_body(req: &wiremock::Request) -> Option> { } } +fn log_field<'a>(line: &'a str, name: &str) -> Option<&'a str> { + let prefix = format!("{name}="); + line.split_ascii_whitespace() + .find_map(|field| field.strip_prefix(&prefix)) + .map(|value| value.trim_matches('"')) +} + fn has_subagent_notification(req: &ResponsesRequest) -> bool { req.message_input_texts("user") .iter() @@ -1042,8 +1052,16 @@ async fn spawned_multi_agent_v2_child_inherits_parent_developer_context() -> Res Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[tokio::test] async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result<()> { + let output: &'static Mutex> = Box::leak(Box::new(Mutex::new(Vec::new()))); + let subscriber = tracing_subscriber::fmt() + .with_ansi(false) + .with_max_level(Level::INFO) + .with_writer(MockWriter::new(output)) + .finish(); + let _guard = tracing::subscriber::set_default(subscriber); + let server = start_mock_server().await; let encrypted_message = "opaque-encrypted-message"; let spawn_args = serde_json::to_string(&json!({ @@ -1098,6 +1116,7 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result .expect("test config should allow feature update"); }); let test = builder.build(&server).await?; + let root_thread_id = test.session_configured.thread_id; test.submit_turn(TURN_1_PROMPT).await?; @@ -1124,6 +1143,41 @@ async fn encrypted_multi_agent_v2_spawn_sends_agent_message_to_child() -> Result })]) ); + let child_thread_id = test + .thread_manager + .list_thread_ids() + .await + .into_iter() + .find(|thread_id| *thread_id != root_thread_id) + .expect("child thread ID"); + let logs = tokio::time::timeout(Duration::from_secs(5), async { + loop { + let logs = String::from_utf8(output.lock().expect("buffer lock").clone()) + .expect("logs should be UTF-8"); + if logs.contains("kind=\"spawn\"") && logs.contains("state=\"receive\"") { + break logs; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("spawn communication logs should be emitted"); + let send = logs + .lines() + .find(|line| line.contains("kind=\"spawn\"") && line.contains("state=\"send\"")) + .expect("spawn send event"); + assert!(send.contains(&format!("sender_thread_id={root_thread_id}"))); + assert!(send.contains(&format!("receiver_thread_id={child_thread_id}"))); + assert!(send.contains(&format!("content=\"{encrypted_message}\""))); + + let communication_id = log_field(send, "communication_id").expect("communication ID"); + logs.lines() + .find(|line| { + line.contains("state=\"receive\"") + && log_field(line, "communication_id") == Some(communication_id) + }) + .expect("correlated receive event"); + Ok(()) } From 6ff670bd030f7f94ce956d8a176c226deb427666 Mon Sep 17 00:00:00 2001 From: xli-oai Date: Thu, 2 Jul 2026 04:45:03 -0700 Subject: [PATCH 08/10] [codex] emit per-request TTFT completion telemetry (#30883) ## Why Codex telemetry pipeline needs a per-request TTFT value. The existing `codex.turn_ttft` is recorded once per turn, so it cannot represent later inference requests in the same turn and can miss the beginning of hidden reasoning. This restores the low-volume per-request signal proposed in https://github.com/bk-nvidia/codex/pull/3 without bringing back per-WebSocket-event TRACE logging. ## What changed - start a timer when each mapped Responses stream begins - latch the timer on the first `response.output_item.added`, including an empty hidden-reasoning item - attach `ttft_ms` to the existing `codex.sse_event` / `response.completed` telemetry record - cover the new completion field with an integration test ## Semantics The value is per inference request, not per turn. It measures mapped-stream-to-first-output-item latency, matching the customer-proposed metric. For HTTP, the stream is already established before timing begins, so request setup and response-header latency are excluded. `response.output_item.added` is a client-visible proxy for the start of hidden reasoning; this does not claim access to the server's internal first raw-token timestamp. ## Validation - `just test -p codex-otel` (47 passed) - `just test -p codex-core process_sse_emits_completed_telemetry` (1 passed after the final timer-placement change) - attempted `just test -p codex-core`: 2,855 passed and 53 failed because of unrelated local-environment failures (missing `test_stdio_server` fixture binary, shell startup noise, and timing-sensitive tests); the focused telemetry test passed in that run as well --- codex-rs/core/src/client.rs | 7 +++++ codex-rs/core/tests/suite/otel.rs | 30 +++++++++++-------- codex-rs/otel/src/events/session_telemetry.rs | 2 ++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 48623a00e8..a8ef00bcc1 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -1912,6 +1912,7 @@ where let mut logged_error = false; let mut tx_last_response = Some(tx_last_response); let mut items_added: Vec = Vec::new(); + let (request_start, mut ttft_ms) = (Instant::now(), None); let mut api_stream = api_stream; let upstream_request_id = upstream_request_id.as_deref(); if let Some(upstream_request_id) = upstream_request_id { @@ -1961,6 +1962,7 @@ where Some(usage.cached_input_tokens), Some(usage.reasoning_output_tokens), usage.total_tokens, + ttft_ms, ); } inference_trace_attempt.record_completed( @@ -1988,6 +1990,11 @@ where } } Ok(event) => { + if matches!(&event, ResponseEvent::OutputItemAdded(_)) && ttft_ms.is_none() { + ttft_ms = Some( + i64::try_from(request_start.elapsed().as_millis()).unwrap_or(i64::MAX), + ); + } if tx_event.send(Ok(event)).await.is_err() { inference_trace_attempt.record_cancelled( STREAM_DROPPED_REASON, diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index e3415d945b..4a2b2993ad 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -567,19 +567,22 @@ async fn process_sse_emits_completed_telemetry() { mount_sse_once( &server, - sse(vec![serde_json::json!({ - "type": "response.completed", - "response": { - "id": "resp1", - "usage": { - "input_tokens": 3, - "input_tokens_details": { "cached_tokens": 1 }, - "output_tokens": 5, - "output_tokens_details": { "reasoning_tokens": 2 }, - "total_tokens": 9 + sse(vec![ + ev_reasoning_item_added("reasoning-1", &[]), + serde_json::json!({ + "type": "response.completed", + "response": { + "id": "resp1", + "usage": { + "input_tokens": 3, + "input_tokens_details": { "cached_tokens": 1 }, + "output_tokens": 5, + "output_tokens_details": { "reasoning_tokens": 2 }, + "total_tokens": 9 + } } - } - })]), + }), + ]), ) .await; @@ -612,6 +615,9 @@ async fn process_sse_emits_completed_telemetry() { && line.contains("cached_token_count=1") && line.contains("reasoning_token_count=2") && line.contains("tool_token_count=9") + && extract_log_field(line, "ttft_ms") + .and_then(|ttft_ms| ttft_ms.parse::().ok()) + .is_some() }) .map(|_| Ok(())) .unwrap_or(Err("missing response.completed telemetry".to_string())) diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs index 8210c0e5b9..440f8f04a5 100644 --- a/codex-rs/otel/src/events/session_telemetry.rs +++ b/codex-rs/otel/src/events/session_telemetry.rs @@ -910,6 +910,7 @@ impl SessionTelemetry { cached_token_count: Option, reasoning_token_count: Option, tool_token_count: i64, + ttft_ms: Option, ) { log_and_trace_event!( self, @@ -921,6 +922,7 @@ impl SessionTelemetry { cached_token_count = cached_token_count, reasoning_token_count = reasoning_token_count, tool_token_count = %tool_token_count, + ttft_ms = ttft_ms, service_tier = self.metadata.service_tier.as_deref(), model_reasoning_effort = self.metadata.model_reasoning_effort.as_deref(), }, From cbdd7f0047c120c6a406a191abaea3c18be702c4 Mon Sep 17 00:00:00 2001 From: Shijie Rao Date: Thu, 2 Jul 2026 10:01:06 -0700 Subject: [PATCH 09/10] Fix inherited availability metadata for Bedrock models (#30897) ## Why Amazon Bedrock's static catalog derives its GPT model definitions from bundled OpenAI model metadata. The GPT-5.6 variants introduced in #30285 clone GPT-5.5, which carries an `availability_nux`; because app-server forwards that metadata through `model/list`, clients can show GPT-5.5 launch copy for a GPT-5.6 Bedrock model. `upgrade` is also model-catalog availability metadata and should not be inherited by provider-specific Bedrock models. ## What changed - Clear `availability_nux` and `upgrade` when constructing static Bedrock GPT models. - Add a regression test asserting that every static Bedrock model omits both fields. ## Testing - `just test -p codex-model-provider` --- codex-rs/model-provider/src/amazon_bedrock/catalog.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/codex-rs/model-provider/src/amazon_bedrock/catalog.rs b/codex-rs/model-provider/src/amazon_bedrock/catalog.rs index 55e3fdd4d8..95e522e0c4 100644 --- a/codex-rs/model-provider/src/amazon_bedrock/catalog.rs +++ b/codex-rs/model-provider/src/amazon_bedrock/catalog.rs @@ -61,6 +61,8 @@ fn gpt_5_bedrock_model(openai_slug: &str, bedrock_slug: &str, priority: i32) -> model.priority = priority; model.context_window = Some(GPT_5_BEDROCK_CONTEXT_WINDOW); model.max_context_window = Some(GPT_5_BEDROCK_CONTEXT_WINDOW); + model.availability_nux = None; + model.upgrade = None; model } @@ -127,6 +129,15 @@ mod tests { } } + #[test] + fn gpt_5_bedrock_models_do_not_include_availability_nux_or_upgrade() { + let catalog = static_model_catalog(); + + for model in catalog.models { + assert_eq!((model.availability_nux, model.upgrade), (None, None)); + } + } + #[test] fn gpt_5_6_bedrock_models_clone_gpt_5_5_config_with_max_reasoning_effort() { let catalog = static_model_catalog(); From 0ccb676dd0904a424a84080d4ec8755677aea22e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 2 Jul 2026 10:59:00 -0700 Subject: [PATCH 10/10] fix: address quick-xml security advisories (#30941) ## Why The `cargo-deny` job on `main` began failing after [RUSTSEC-2026-0194](https://rustsec.org/advisories/RUSTSEC-2026-0194) and [RUSTSEC-2026-0195](https://rustsec.org/advisories/RUSTSEC-2026-0195) flagged the workspace `quick-xml 0.38.4`. Both denial-of-service issues are fixed in `quick-xml 0.41.0`. A `quick-xml 0.39.4` copy must temporarily remain because the latest `plist` and `wayland-scanner` releases have not adopted 0.41 yet. Neither retained path accepts attacker-controlled XML at runtime: `plist` does not exercise the affected APIs, and `wayland-scanner` parses trusted protocol definitions at build time. Compatible upstream bumps are already open in [rust-plist#191](https://github.com/ebarnard/rust-plist/pull/191) and [wayland-rs#938](https://github.com/Smithay/wayland-rs/pull/938). ## What changed - Upgrade the workspace `quick-xml` dependency used by `codex-protocol` to 0.41.0. - Refresh `Cargo.lock` and `MODULE.bazel.lock`; this also updates `plist` to 1.9.0 and `wayland-scanner` to 0.31.10. - Add synchronized, temporary `cargo-deny` and `cargo-audit` exceptions for the trusted `quick-xml 0.39.4` paths, with both upstream releases recorded as the removal condition. ## Testing - `cargo deny check` - `just test -p codex-protocol` (238 tests) - `just bazel-lock-check` --- MODULE.bazel.lock | 7 ++++--- codex-rs/.cargo/audit.toml | 4 +++- codex-rs/Cargo.lock | 27 ++++++++++++++++++--------- codex-rs/Cargo.toml | 2 +- codex-rs/deny.toml | 4 +++- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 8ae5bef17c..d572f68f21 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -1343,7 +1343,7 @@ "pkcs8_0.10.2": "{\"dependencies\":[{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"pkcs5\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"spki\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"3des\":[\"encryption\",\"pkcs5/3des\"],\"alloc\":[\"der/alloc\",\"der/zeroize\",\"spki/alloc\"],\"des-insecure\":[\"encryption\",\"pkcs5/des-insecure\"],\"encryption\":[\"alloc\",\"pkcs5/alloc\",\"pkcs5/pbes2\",\"rand_core\"],\"getrandom\":[\"rand_core/getrandom\"],\"pem\":[\"alloc\",\"der/pem\",\"spki/pem\"],\"sha1-insecure\":[\"encryption\",\"pkcs5/sha1-insecure\"],\"std\":[\"alloc\",\"der/std\",\"spki/std\"]}}", "pkg-config_0.3.32": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"}],\"features\":{}}", "plain_0.2.3": "{\"dependencies\":[],\"features\":{}}", - "plist_1.8.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"indexmap\",\"req\":\"^2.1.0\"},{\"name\":\"quick_xml\",\"package\":\"quick-xml\",\"req\":\"^0.38.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8.21\"},{\"features\":[\"parsing\",\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.30\"}],\"features\":{\"default\":[\"serde\"],\"enable_unstable_features_that_may_break_with_minor_version_bumps\":[]}}", + "plist_1.9.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"indexmap\",\"req\":\"^2.1.0\"},{\"name\":\"quick_xml\",\"package\":\"quick-xml\",\"req\":\"^0.39.2\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8.21\"},{\"features\":[\"parsing\",\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.47\"}],\"features\":{\"default\":[\"serde\"],\"enable_unstable_features_that_may_break_with_minor_version_bumps\":[]}}", "png_0.18.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"],\"zlib-rs\":[\"flate2/zlib-rs\"]}}", "polling_3.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"features\":[\"event\",\"fs\",\"pipe\",\"process\",\"std\",\"time\"],\"name\":\"rustix\",\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"fuchsia\\\", target_os = \\\"vxworks\\\"))\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3.17\",\"target\":\"cfg(all(unix, not(target_os=\\\"vita\\\")))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_LibraryLoader\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "poly1305_0.8.0": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}", @@ -1390,7 +1390,8 @@ "pulldown-cmark_0.10.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"getopts\",\"optional\":true,\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"name\":\"memchr\",\"req\":\"^2.5\"},{\"name\":\"pulldown-cmark-escape\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.6\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.61\"},{\"name\":\"unicase\",\"req\":\"^2.6\"}],\"features\":{\"default\":[\"getopts\",\"html\"],\"gen-tests\":[],\"html\":[\"pulldown-cmark-escape\"],\"simd\":[\"pulldown-cmark-escape?/simd\"]}}", "pxfm_0.1.27": "{\"dependencies\":[{\"name\":\"num-traits\",\"req\":\"^0.2.3\"}],\"features\":{}}", "quick-error_2.0.1": "{\"dependencies\":[],\"features\":{}}", - "quick-xml_0.38.4": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.8\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.139\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}", + "quick-xml_0.39.4": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.9\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.180\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}", + "quick-xml_0.41.0": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\">=0.4, <0.9\"},{\"name\":\"document-features\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"encoding_rs\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"memchr\",\"req\":\"^2.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\">=1.0.180\"},{\"kind\":\"dev\",\"name\":\"serde-value\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.206\"},{\"default_features\":false,\"features\":[\"io-util\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.10\"},{\"default_features\":false,\"features\":[\"macros\",\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.21\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"}],\"features\":{\"async-tokio\":[\"tokio\"],\"default\":[],\"encoding\":[\"encoding_rs\"],\"escape-html\":[],\"overlapped-lists\":[],\"serde-types\":[\"serde/derive\"],\"serialize\":[\"serde\"]}}", "quickcheck_1.1.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"env_logger\",\"optional\":true,\"req\":\"^0.11\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"sys_rng\"],\"name\":\"rand\",\"req\":\"^0.10\"}],\"features\":{\"default\":[\"regex\",\"use_logging\"],\"regex\":[\"env_logger/regex\"],\"use_logging\":[\"log\",\"env_logger\"]}}", "quinn-proto_0.11.14": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.1\"},{\"default_features\":false,\"name\":\"aws-lc-rs\",\"optional\":true,\"req\":\"^1.9\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"name\":\"fastbloom\",\"optional\":true,\"req\":\"^0.14\"},{\"default_features\":false,\"features\":[\"wasm_js\"],\"name\":\"getrandom\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"name\":\"lru-slab\",\"req\":\"^0.1.2\"},{\"name\":\"qlog\",\"optional\":true,\"req\":\"^0.15.2\"},{\"name\":\"rand\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand_pcg\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"features\":[\"wasm32_unknown_unknown_js\"],\"name\":\"ring\",\"req\":\"^0.17\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"ring\",\"optional\":true,\"req\":\"^0.17\"},{\"name\":\"rustc-hash\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.5\"},{\"features\":[\"web\"],\"name\":\"rustls-pki-types\",\"req\":\"^1.7\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"name\":\"rustls-platform-verifier\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"slab\",\"req\":\"^0.4.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.3\"},{\"features\":[\"alloc\",\"alloc\"],\"name\":\"tinyvec\",\"req\":\"^1.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.10\"},{\"default_features\":false,\"features\":[\"env-filter\",\"fmt\",\"ansi\",\"time\",\"local-time\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.45\"},{\"name\":\"web-time\",\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"}],\"features\":{\"__rustls-post-quantum-test\":[],\"aws-lc-rs\":[\"dep:aws-lc-rs\",\"aws-lc-rs?/aws-lc-sys\",\"aws-lc-rs?/prebuilt-nasm\"],\"aws-lc-rs-fips\":[\"aws-lc-rs\",\"aws-lc-rs?/fips\"],\"bloom\":[\"dep:fastbloom\"],\"default\":[\"rustls-ring\",\"log\",\"bloom\"],\"log\":[\"tracing/log\"],\"platform-verifier\":[\"dep:rustls-platform-verifier\"],\"qlog\":[\"dep:qlog\"],\"ring\":[\"dep:ring\"],\"rustls\":[\"rustls-ring\"],\"rustls-aws-lc-rs\":[\"dep:rustls\",\"rustls?/aws-lc-rs\",\"aws-lc-rs\"],\"rustls-aws-lc-rs-fips\":[\"rustls-aws-lc-rs\",\"aws-lc-rs-fips\"],\"rustls-log\":[\"rustls?/logging\"],\"rustls-ring\":[\"dep:rustls\",\"rustls?/ring\",\"ring\"]}}", "quinn-udp_0.5.14": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cfg_aliases\",\"req\":\"^0.2\"},{\"default_features\":false,\"features\":[\"async_tokio\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"name\":\"libc\",\"req\":\"^0.2.158\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"once_cell\",\"req\":\"^1.19\",\"target\":\"cfg(windows)\"},{\"name\":\"socket2\",\"req\":\">=0.5, <0.7\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"features\":[\"sync\",\"rt\",\"rt-multi-thread\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.28.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.10\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_IO\",\"Win32_Networking_WinSock\"],\"name\":\"windows-sys\",\"req\":\">=0.52, <=0.60\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"tracing\",\"log\"],\"direct-log\":[\"dep:log\"],\"fast-apple-datapath\":[],\"log\":[\"tracing/log\"]}}", @@ -1749,7 +1750,7 @@ "wayland-client_0.31.12": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3.16\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"event\"],\"name\":\"rustix\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"}],\"features\":{}}", "wayland-protocols-wlr_0.3.10": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-client\",\"optional\":true,\"req\":\"^0.31.12\"},{\"name\":\"wayland-protocols\",\"req\":\"^0.32.10\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"},{\"name\":\"wayland-server\",\"optional\":true,\"req\":\"^0.31.11\"}],\"features\":{\"client\":[\"wayland-client\",\"wayland-protocols/client\"],\"server\":[\"wayland-server\",\"wayland-protocols/server\"]}}", "wayland-protocols_0.32.10": "{\"dependencies\":[{\"name\":\"bitflags\",\"req\":\"^2\"},{\"name\":\"wayland-backend\",\"req\":\"^0.3.12\"},{\"name\":\"wayland-client\",\"optional\":true,\"req\":\"^0.31.12\"},{\"name\":\"wayland-scanner\",\"req\":\"^0.31.8\"},{\"name\":\"wayland-server\",\"optional\":true,\"req\":\"^0.31.11\"}],\"features\":{\"client\":[\"wayland-client\"],\"server\":[\"wayland-server\"],\"staging\":[],\"unstable\":[]}}", - "wayland-scanner_0.31.8": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.11\"},{\"name\":\"quick-xml\",\"req\":\"^0.38.3\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2\"}],\"features\":{}}", + "wayland-scanner_0.31.10": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.11\"},{\"name\":\"quick-xml\",\"req\":\"^0.39\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"similar\",\"req\":\"^2\"}],\"features\":{}}", "wayland-sys_0.31.8": "{\"dependencies\":[{\"name\":\"dlib\",\"optional\":true,\"req\":\"^0.5.1\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2\"},{\"name\":\"log\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"memoffset\",\"optional\":true,\"req\":\"^0.9\"},{\"name\":\"once_cell\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"build\",\"name\":\"pkg-config\",\"req\":\"^0.3.7\"}],\"features\":{\"client\":[\"dep:dlib\",\"dep:log\"],\"cursor\":[\"client\"],\"dlopen\":[\"once_cell\"],\"egl\":[\"client\"],\"server\":[\"libc\",\"memoffset\",\"dep:dlib\",\"dep:log\"]}}", "web-sys_0.3.85": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\",\"target\":\"cfg(target_arch = \\\"wasm32\\\")\"},{\"default_features\":false,\"name\":\"js-sys\",\"req\":\"=0.3.85\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"=0.2.108\"}],\"features\":{\"AbortController\":[],\"AbortSignal\":[\"EventTarget\"],\"AddEventListenerOptions\":[],\"AesCbcParams\":[],\"AesCtrParams\":[],\"AesDerivedKeyParams\":[],\"AesGcmParams\":[],\"AesKeyAlgorithm\":[],\"AesKeyGenParams\":[],\"Algorithm\":[],\"AlignSetting\":[],\"AllowedBluetoothDevice\":[],\"AllowedUsbDevice\":[],\"AlphaOption\":[],\"AnalyserNode\":[\"AudioNode\",\"EventTarget\"],\"AnalyserOptions\":[],\"AngleInstancedArrays\":[],\"Animation\":[\"EventTarget\"],\"AnimationEffect\":[],\"AnimationEvent\":[\"Event\"],\"AnimationEventInit\":[],\"AnimationPlayState\":[],\"AnimationPlaybackEvent\":[\"Event\"],\"AnimationPlaybackEventInit\":[],\"AnimationPropertyDetails\":[],\"AnimationPropertyValueDetails\":[],\"AnimationTimeline\":[],\"AssignedNodesOptions\":[],\"AttestationConveyancePreference\":[],\"Attr\":[\"EventTarget\",\"Node\"],\"AttributeNameValue\":[],\"AudioBuffer\":[],\"AudioBufferOptions\":[],\"AudioBufferSourceNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"AudioBufferSourceOptions\":[],\"AudioConfiguration\":[],\"AudioContext\":[\"BaseAudioContext\",\"EventTarget\"],\"AudioContextLatencyCategory\":[],\"AudioContextOptions\":[],\"AudioContextState\":[],\"AudioData\":[],\"AudioDataCopyToOptions\":[],\"AudioDataInit\":[],\"AudioDecoder\":[],\"AudioDecoderConfig\":[],\"AudioDecoderInit\":[],\"AudioDecoderSupport\":[],\"AudioDestinationNode\":[\"AudioNode\",\"EventTarget\"],\"AudioEncoder\":[],\"AudioEncoderConfig\":[],\"AudioEncoderInit\":[],\"AudioEncoderSupport\":[],\"AudioListener\":[],\"AudioNode\":[\"EventTarget\"],\"AudioNodeOptions\":[],\"AudioParam\":[],\"AudioParamMap\":[],\"AudioProcessingEvent\":[\"Event\"],\"AudioSampleFormat\":[],\"AudioScheduledSourceNode\":[\"AudioNode\",\"EventTarget\"],\"AudioSinkInfo\":[],\"AudioSinkOptions\":[],\"AudioSinkType\":[],\"AudioStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"AudioTrack\":[],\"AudioTrackList\":[\"EventTarget\"],\"AudioWorklet\":[\"Worklet\"],\"AudioWorkletGlobalScope\":[\"WorkletGlobalScope\"],\"AudioWorkletNode\":[\"AudioNode\",\"EventTarget\"],\"AudioWorkletNodeOptions\":[],\"AudioWorkletProcessor\":[],\"AuthenticationExtensionsClientInputs\":[],\"AuthenticationExtensionsClientInputsJson\":[],\"AuthenticationExtensionsClientOutputs\":[],\"AuthenticationExtensionsClientOutputsJson\":[],\"AuthenticationExtensionsDevicePublicKeyInputs\":[],\"AuthenticationExtensionsDevicePublicKeyOutputs\":[],\"AuthenticationExtensionsLargeBlobInputs\":[],\"AuthenticationExtensionsLargeBlobOutputs\":[],\"AuthenticationExtensionsPrfInputs\":[],\"AuthenticationExtensionsPrfOutputs\":[],\"AuthenticationExtensionsPrfValues\":[],\"AuthenticationResponseJson\":[],\"AuthenticatorAssertionResponse\":[\"AuthenticatorResponse\"],\"AuthenticatorAssertionResponseJson\":[],\"AuthenticatorAttachment\":[],\"AuthenticatorAttestationResponse\":[\"AuthenticatorResponse\"],\"AuthenticatorAttestationResponseJson\":[],\"AuthenticatorResponse\":[],\"AuthenticatorSelectionCriteria\":[],\"AuthenticatorTransport\":[],\"AutoKeyword\":[],\"AutocompleteInfo\":[],\"BarProp\":[],\"BaseAudioContext\":[\"EventTarget\"],\"BaseComputedKeyframe\":[],\"BaseKeyframe\":[],\"BasePropertyIndexedKeyframe\":[],\"BasicCardRequest\":[],\"BasicCardResponse\":[],\"BasicCardType\":[],\"BatteryManager\":[\"EventTarget\"],\"BeforeUnloadEvent\":[\"Event\"],\"BinaryType\":[],\"BiquadFilterNode\":[\"AudioNode\",\"EventTarget\"],\"BiquadFilterOptions\":[],\"BiquadFilterType\":[],\"Blob\":[],\"BlobEvent\":[\"Event\"],\"BlobEventInit\":[],\"BlobPropertyBag\":[],\"BlockParsingOptions\":[],\"Bluetooth\":[\"EventTarget\"],\"BluetoothAdvertisingEvent\":[\"Event\"],\"BluetoothAdvertisingEventInit\":[],\"BluetoothCharacteristicProperties\":[],\"BluetoothDataFilterInit\":[],\"BluetoothDevice\":[\"EventTarget\"],\"BluetoothLeScanFilterInit\":[],\"BluetoothManufacturerDataMap\":[],\"BluetoothPermissionDescriptor\":[],\"BluetoothPermissionResult\":[\"EventTarget\",\"PermissionStatus\"],\"BluetoothPermissionStorage\":[],\"BluetoothRemoteGattCharacteristic\":[\"EventTarget\"],\"BluetoothRemoteGattDescriptor\":[],\"BluetoothRemoteGattServer\":[],\"BluetoothRemoteGattService\":[\"EventTarget\"],\"BluetoothServiceDataMap\":[],\"BluetoothUuid\":[],\"BoxQuadOptions\":[],\"BroadcastChannel\":[\"EventTarget\"],\"BrowserElementDownloadOptions\":[],\"BrowserElementExecuteScriptOptions\":[],\"BrowserFeedWriter\":[],\"BrowserFindCaseSensitivity\":[],\"BrowserFindDirection\":[],\"ByteLengthQueuingStrategy\":[],\"Cache\":[],\"CacheBatchOperation\":[],\"CacheQueryOptions\":[],\"CacheStorage\":[],\"CacheStorageNamespace\":[],\"CanvasCaptureMediaStream\":[\"EventTarget\",\"MediaStream\"],\"CanvasCaptureMediaStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"CanvasGradient\":[],\"CanvasPattern\":[],\"CanvasRenderingContext2d\":[],\"CanvasWindingRule\":[],\"CaretChangedReason\":[],\"CaretPosition\":[],\"CaretStateChangedEventInit\":[],\"CdataSection\":[\"CharacterData\",\"EventTarget\",\"Node\",\"Text\"],\"ChannelCountMode\":[],\"ChannelInterpretation\":[],\"ChannelMergerNode\":[\"AudioNode\",\"EventTarget\"],\"ChannelMergerOptions\":[],\"ChannelSplitterNode\":[\"AudioNode\",\"EventTarget\"],\"ChannelSplitterOptions\":[],\"CharacterData\":[\"EventTarget\",\"Node\"],\"CheckerboardReason\":[],\"CheckerboardReport\":[],\"CheckerboardReportService\":[],\"ChromeFilePropertyBag\":[],\"ChromeWorker\":[\"EventTarget\",\"Worker\"],\"Client\":[],\"ClientQueryOptions\":[],\"ClientRectsAndTexts\":[],\"ClientType\":[],\"Clients\":[],\"Clipboard\":[\"EventTarget\"],\"ClipboardEvent\":[\"Event\"],\"ClipboardEventInit\":[],\"ClipboardItem\":[],\"ClipboardItemOptions\":[],\"ClipboardPermissionDescriptor\":[],\"ClipboardUnsanitizedFormats\":[],\"CloseEvent\":[\"Event\"],\"CloseEventInit\":[],\"CodecState\":[],\"CollectedClientData\":[],\"ColorSpaceConversion\":[],\"Comment\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"CompositeOperation\":[],\"CompositionEvent\":[\"Event\",\"UiEvent\"],\"CompositionEventInit\":[],\"CompressionFormat\":[],\"CompressionStream\":[],\"ComputedEffectTiming\":[],\"ConnStatusDict\":[],\"ConnectionType\":[],\"ConsoleCounter\":[],\"ConsoleCounterError\":[],\"ConsoleEvent\":[],\"ConsoleInstance\":[],\"ConsoleInstanceOptions\":[],\"ConsoleLevel\":[],\"ConsoleLogLevel\":[],\"ConsoleProfileEvent\":[],\"ConsoleStackEntry\":[],\"ConsoleTimerError\":[],\"ConsoleTimerLogOrEnd\":[],\"ConsoleTimerStart\":[],\"ConstantSourceNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"ConstantSourceOptions\":[],\"ConstrainBooleanParameters\":[],\"ConstrainDomStringParameters\":[],\"ConstrainDoubleRange\":[],\"ConstrainLongRange\":[],\"ContextAttributes2d\":[],\"ConvertCoordinateOptions\":[],\"ConvolverNode\":[\"AudioNode\",\"EventTarget\"],\"ConvolverOptions\":[],\"CookieChangeEvent\":[\"Event\"],\"CookieChangeEventInit\":[],\"CookieInit\":[],\"CookieListItem\":[],\"CookieSameSite\":[],\"CookieStore\":[\"EventTarget\"],\"CookieStoreDeleteOptions\":[],\"CookieStoreGetOptions\":[],\"CookieStoreManager\":[],\"Coordinates\":[],\"CountQueuingStrategy\":[],\"Credential\":[],\"CredentialCreationOptions\":[],\"CredentialPropertiesOutput\":[],\"CredentialRequestOptions\":[],\"CredentialsContainer\":[],\"Crypto\":[],\"CryptoKey\":[],\"CryptoKeyPair\":[],\"CssAnimation\":[\"Animation\",\"EventTarget\"],\"CssBoxType\":[],\"CssConditionRule\":[\"CssGroupingRule\",\"CssRule\"],\"CssCounterStyleRule\":[\"CssRule\"],\"CssFontFaceRule\":[\"CssRule\"],\"CssFontFeatureValuesRule\":[\"CssRule\"],\"CssGroupingRule\":[\"CssRule\"],\"CssImportRule\":[\"CssRule\"],\"CssKeyframeRule\":[\"CssRule\"],\"CssKeyframesRule\":[\"CssRule\"],\"CssMediaRule\":[\"CssConditionRule\",\"CssGroupingRule\",\"CssRule\"],\"CssNamespaceRule\":[\"CssRule\"],\"CssPageRule\":[\"CssRule\"],\"CssPseudoElement\":[],\"CssRule\":[],\"CssRuleList\":[],\"CssStyleDeclaration\":[],\"CssStyleRule\":[\"CssRule\"],\"CssStyleSheet\":[\"StyleSheet\"],\"CssStyleSheetParsingMode\":[],\"CssSupportsRule\":[\"CssConditionRule\",\"CssGroupingRule\",\"CssRule\"],\"CssTransition\":[\"Animation\",\"EventTarget\"],\"CustomElementRegistry\":[],\"CustomEvent\":[\"Event\"],\"CustomEventInit\":[],\"DataTransfer\":[],\"DataTransferItem\":[],\"DataTransferItemList\":[],\"DateTimeValue\":[],\"DecoderDoctorNotification\":[],\"DecoderDoctorNotificationType\":[],\"DecompressionStream\":[],\"DedicatedWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"DelayNode\":[\"AudioNode\",\"EventTarget\"],\"DelayOptions\":[],\"DeviceAcceleration\":[],\"DeviceAccelerationInit\":[],\"DeviceLightEvent\":[\"Event\"],\"DeviceLightEventInit\":[],\"DeviceMotionEvent\":[\"Event\"],\"DeviceMotionEventInit\":[],\"DeviceOrientationEvent\":[\"Event\"],\"DeviceOrientationEventInit\":[],\"DeviceProximityEvent\":[\"Event\"],\"DeviceProximityEventInit\":[],\"DeviceRotationRate\":[],\"DeviceRotationRateInit\":[],\"DhKeyDeriveParams\":[],\"DirectionSetting\":[],\"Directory\":[],\"DirectoryPickerOptions\":[],\"DisplayMediaStreamConstraints\":[],\"DisplayNameOptions\":[],\"DisplayNameResult\":[],\"DistanceModelType\":[],\"DnsCacheDict\":[],\"DnsCacheEntry\":[],\"DnsLookupDict\":[],\"Document\":[\"EventTarget\",\"Node\"],\"DocumentFragment\":[\"EventTarget\",\"Node\"],\"DocumentTimeline\":[\"AnimationTimeline\"],\"DocumentTimelineOptions\":[],\"DocumentType\":[\"EventTarget\",\"Node\"],\"DomError\":[],\"DomException\":[],\"DomImplementation\":[],\"DomMatrix\":[\"DomMatrixReadOnly\"],\"DomMatrix2dInit\":[],\"DomMatrixInit\":[],\"DomMatrixReadOnly\":[],\"DomParser\":[],\"DomPoint\":[\"DomPointReadOnly\"],\"DomPointInit\":[],\"DomPointReadOnly\":[],\"DomQuad\":[],\"DomQuadInit\":[],\"DomQuadJson\":[],\"DomRect\":[\"DomRectReadOnly\"],\"DomRectInit\":[],\"DomRectList\":[],\"DomRectReadOnly\":[],\"DomRequest\":[\"EventTarget\"],\"DomRequestReadyState\":[],\"DomStringList\":[],\"DomStringMap\":[],\"DomTokenList\":[],\"DomWindowResizeEventDetail\":[],\"DoubleRange\":[],\"DragEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"DragEventInit\":[],\"DynamicsCompressorNode\":[\"AudioNode\",\"EventTarget\"],\"DynamicsCompressorOptions\":[],\"EcKeyAlgorithm\":[],\"EcKeyGenParams\":[],\"EcKeyImportParams\":[],\"EcdhKeyDeriveParams\":[],\"EcdsaParams\":[],\"EffectTiming\":[],\"Element\":[\"EventTarget\",\"Node\"],\"ElementCreationOptions\":[],\"ElementDefinitionOptions\":[],\"EncodedAudioChunk\":[],\"EncodedAudioChunkInit\":[],\"EncodedAudioChunkMetadata\":[],\"EncodedAudioChunkType\":[],\"EncodedVideoChunk\":[],\"EncodedVideoChunkInit\":[],\"EncodedVideoChunkMetadata\":[],\"EncodedVideoChunkType\":[],\"EndingTypes\":[],\"ErrorCallback\":[],\"ErrorEvent\":[\"Event\"],\"ErrorEventInit\":[],\"Event\":[],\"EventInit\":[],\"EventListener\":[],\"EventListenerOptions\":[],\"EventModifierInit\":[],\"EventSource\":[\"EventTarget\"],\"EventSourceInit\":[],\"EventTarget\":[],\"Exception\":[],\"ExtBlendMinmax\":[],\"ExtColorBufferFloat\":[],\"ExtColorBufferHalfFloat\":[],\"ExtDisjointTimerQuery\":[],\"ExtFragDepth\":[],\"ExtSRgb\":[],\"ExtShaderTextureLod\":[],\"ExtTextureFilterAnisotropic\":[],\"ExtTextureNorm16\":[],\"ExtendableCookieChangeEvent\":[\"Event\",\"ExtendableEvent\"],\"ExtendableCookieChangeEventInit\":[],\"ExtendableEvent\":[\"Event\"],\"ExtendableEventInit\":[],\"ExtendableMessageEvent\":[\"Event\",\"ExtendableEvent\"],\"ExtendableMessageEventInit\":[],\"External\":[],\"FakePluginMimeEntry\":[],\"FakePluginTagInit\":[],\"FetchEvent\":[\"Event\",\"ExtendableEvent\"],\"FetchEventInit\":[],\"FetchObserver\":[\"EventTarget\"],\"FetchReadableStreamReadDataArray\":[],\"FetchReadableStreamReadDataDone\":[],\"FetchState\":[],\"File\":[\"Blob\"],\"FileCallback\":[],\"FileList\":[],\"FilePickerAcceptType\":[],\"FilePickerOptions\":[],\"FilePropertyBag\":[],\"FileReader\":[\"EventTarget\"],\"FileReaderSync\":[],\"FileSystem\":[],\"FileSystemCreateWritableOptions\":[],\"FileSystemDirectoryEntry\":[\"FileSystemEntry\"],\"FileSystemDirectoryHandle\":[\"FileSystemHandle\"],\"FileSystemDirectoryReader\":[],\"FileSystemEntriesCallback\":[],\"FileSystemEntry\":[],\"FileSystemEntryCallback\":[],\"FileSystemFileEntry\":[\"FileSystemEntry\"],\"FileSystemFileHandle\":[\"FileSystemHandle\"],\"FileSystemFlags\":[],\"FileSystemGetDirectoryOptions\":[],\"FileSystemGetFileOptions\":[],\"FileSystemHandle\":[],\"FileSystemHandleKind\":[],\"FileSystemHandlePermissionDescriptor\":[],\"FileSystemPermissionDescriptor\":[],\"FileSystemPermissionMode\":[],\"FileSystemReadWriteOptions\":[],\"FileSystemRemoveOptions\":[],\"FileSystemSyncAccessHandle\":[],\"FileSystemWritableFileStream\":[\"WritableStream\"],\"FillMode\":[],\"FlashClassification\":[],\"FlowControlType\":[],\"FocusEvent\":[\"Event\",\"UiEvent\"],\"FocusEventInit\":[],\"FocusOptions\":[],\"FontData\":[],\"FontFace\":[],\"FontFaceDescriptors\":[],\"FontFaceLoadStatus\":[],\"FontFaceSet\":[\"EventTarget\"],\"FontFaceSetIterator\":[],\"FontFaceSetIteratorResult\":[],\"FontFaceSetLoadEvent\":[\"Event\"],\"FontFaceSetLoadEventInit\":[],\"FontFaceSetLoadStatus\":[],\"FormData\":[],\"FrameType\":[],\"FuzzingFunctions\":[],\"GainNode\":[\"AudioNode\",\"EventTarget\"],\"GainOptions\":[],\"Gamepad\":[],\"GamepadButton\":[],\"GamepadEffectParameters\":[],\"GamepadEvent\":[\"Event\"],\"GamepadEventInit\":[],\"GamepadHand\":[],\"GamepadHapticActuator\":[],\"GamepadHapticActuatorType\":[],\"GamepadHapticEffectType\":[],\"GamepadHapticsResult\":[],\"GamepadMappingType\":[],\"GamepadPose\":[],\"GamepadTouch\":[],\"Geolocation\":[],\"GestureEvent\":[\"Event\",\"UiEvent\"],\"GetAnimationsOptions\":[],\"GetRootNodeOptions\":[],\"GetUserMediaRequest\":[],\"Gpu\":[],\"GpuAdapter\":[],\"GpuAdapterInfo\":[],\"GpuAddressMode\":[],\"GpuAutoLayoutMode\":[],\"GpuBindGroup\":[],\"GpuBindGroupDescriptor\":[],\"GpuBindGroupEntry\":[],\"GpuBindGroupLayout\":[],\"GpuBindGroupLayoutDescriptor\":[],\"GpuBindGroupLayoutEntry\":[],\"GpuBlendComponent\":[],\"GpuBlendFactor\":[],\"GpuBlendOperation\":[],\"GpuBlendState\":[],\"GpuBuffer\":[],\"GpuBufferBinding\":[],\"GpuBufferBindingLayout\":[],\"GpuBufferBindingType\":[],\"GpuBufferDescriptor\":[],\"GpuBufferMapState\":[],\"GpuCanvasAlphaMode\":[],\"GpuCanvasConfiguration\":[],\"GpuCanvasContext\":[],\"GpuCanvasToneMapping\":[],\"GpuCanvasToneMappingMode\":[],\"GpuColorDict\":[],\"GpuColorTargetState\":[],\"GpuCommandBuffer\":[],\"GpuCommandBufferDescriptor\":[],\"GpuCommandEncoder\":[],\"GpuCommandEncoderDescriptor\":[],\"GpuCompareFunction\":[],\"GpuCompilationInfo\":[],\"GpuCompilationMessage\":[],\"GpuCompilationMessageType\":[],\"GpuComputePassDescriptor\":[],\"GpuComputePassEncoder\":[],\"GpuComputePassTimestampWrites\":[],\"GpuComputePipeline\":[],\"GpuComputePipelineDescriptor\":[],\"GpuCopyExternalImageDestInfo\":[],\"GpuCopyExternalImageSourceInfo\":[],\"GpuCullMode\":[],\"GpuDepthStencilState\":[],\"GpuDevice\":[\"EventTarget\"],\"GpuDeviceDescriptor\":[],\"GpuDeviceLostInfo\":[],\"GpuDeviceLostReason\":[],\"GpuError\":[],\"GpuErrorFilter\":[],\"GpuExtent3dDict\":[],\"GpuExternalTexture\":[],\"GpuExternalTextureBindingLayout\":[],\"GpuExternalTextureDescriptor\":[],\"GpuFeatureName\":[],\"GpuFilterMode\":[],\"GpuFragmentState\":[],\"GpuFrontFace\":[],\"GpuIndexFormat\":[],\"GpuInternalError\":[\"GpuError\"],\"GpuLoadOp\":[],\"GpuMipmapFilterMode\":[],\"GpuMultisampleState\":[],\"GpuObjectDescriptorBase\":[],\"GpuOrigin2dDict\":[],\"GpuOrigin3dDict\":[],\"GpuOutOfMemoryError\":[\"GpuError\"],\"GpuPipelineDescriptorBase\":[],\"GpuPipelineError\":[\"DomException\"],\"GpuPipelineErrorInit\":[],\"GpuPipelineErrorReason\":[],\"GpuPipelineLayout\":[],\"GpuPipelineLayoutDescriptor\":[],\"GpuPowerPreference\":[],\"GpuPrimitiveState\":[],\"GpuPrimitiveTopology\":[],\"GpuProgrammableStage\":[],\"GpuQuerySet\":[],\"GpuQuerySetDescriptor\":[],\"GpuQueryType\":[],\"GpuQueue\":[],\"GpuQueueDescriptor\":[],\"GpuRenderBundle\":[],\"GpuRenderBundleDescriptor\":[],\"GpuRenderBundleEncoder\":[],\"GpuRenderBundleEncoderDescriptor\":[],\"GpuRenderPassColorAttachment\":[],\"GpuRenderPassDepthStencilAttachment\":[],\"GpuRenderPassDescriptor\":[],\"GpuRenderPassEncoder\":[],\"GpuRenderPassLayout\":[],\"GpuRenderPassTimestampWrites\":[],\"GpuRenderPipeline\":[],\"GpuRenderPipelineDescriptor\":[],\"GpuRequestAdapterOptions\":[],\"GpuSampler\":[],\"GpuSamplerBindingLayout\":[],\"GpuSamplerBindingType\":[],\"GpuSamplerDescriptor\":[],\"GpuShaderModule\":[],\"GpuShaderModuleCompilationHint\":[],\"GpuShaderModuleDescriptor\":[],\"GpuStencilFaceState\":[],\"GpuStencilOperation\":[],\"GpuStorageTextureAccess\":[],\"GpuStorageTextureBindingLayout\":[],\"GpuStoreOp\":[],\"GpuSupportedFeatures\":[],\"GpuSupportedLimits\":[],\"GpuTexelCopyBufferInfo\":[],\"GpuTexelCopyBufferLayout\":[],\"GpuTexelCopyTextureInfo\":[],\"GpuTexture\":[],\"GpuTextureAspect\":[],\"GpuTextureBindingLayout\":[],\"GpuTextureDescriptor\":[],\"GpuTextureDimension\":[],\"GpuTextureFormat\":[],\"GpuTextureSampleType\":[],\"GpuTextureView\":[],\"GpuTextureViewDescriptor\":[],\"GpuTextureViewDimension\":[],\"GpuUncapturedErrorEvent\":[\"Event\"],\"GpuUncapturedErrorEventInit\":[],\"GpuValidationError\":[\"GpuError\"],\"GpuVertexAttribute\":[],\"GpuVertexBufferLayout\":[],\"GpuVertexFormat\":[],\"GpuVertexState\":[],\"GpuVertexStepMode\":[],\"GroupedHistoryEventInit\":[],\"HalfOpenInfoDict\":[],\"HardwareAcceleration\":[],\"HashChangeEvent\":[\"Event\"],\"HashChangeEventInit\":[],\"Headers\":[],\"HeadersGuardEnum\":[],\"Hid\":[\"EventTarget\"],\"HidCollectionInfo\":[],\"HidConnectionEvent\":[\"Event\"],\"HidConnectionEventInit\":[],\"HidDevice\":[\"EventTarget\"],\"HidDeviceFilter\":[],\"HidDeviceRequestOptions\":[],\"HidInputReportEvent\":[\"Event\"],\"HidInputReportEventInit\":[],\"HidReportInfo\":[],\"HidReportItem\":[],\"HidUnitSystem\":[],\"HiddenPluginEventInit\":[],\"History\":[],\"HitRegionOptions\":[],\"HkdfParams\":[],\"HmacDerivedKeyParams\":[],\"HmacImportParams\":[],\"HmacKeyAlgorithm\":[],\"HmacKeyGenParams\":[],\"HtmlAllCollection\":[],\"HtmlAnchorElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlAreaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlAudioElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"HtmlMediaElement\",\"Node\"],\"HtmlBaseElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlBodyElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlBrElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlButtonElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlCanvasElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlCollection\":[],\"HtmlDListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDataElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDataListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDetailsElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDialogElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDirectoryElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDivElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlDocument\":[\"Document\",\"EventTarget\",\"Node\"],\"HtmlElement\":[\"Element\",\"EventTarget\",\"Node\"],\"HtmlEmbedElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFieldSetElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFontElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFormControlsCollection\":[\"HtmlCollection\"],\"HtmlFormElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFrameElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlFrameSetElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHeadElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHeadingElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHrElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlHtmlElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlIFrameElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlImageElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlInputElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLabelElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLegendElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLiElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlLinkElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMapElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMediaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMenuElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMenuItemElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMetaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlMeterElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlModElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlObjectElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptGroupElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlOptionsCollection\":[\"HtmlCollection\"],\"HtmlOutputElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlParagraphElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlParamElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlPictureElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlPreElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlProgressElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlQuoteElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlScriptElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSelectElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSlotElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSourceElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlSpanElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlStyleElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableCaptionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableCellElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableColElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableRowElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTableSectionElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTemplateElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTextAreaElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTimeElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTitleElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlTrackElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlUListElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlUnknownElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"Node\"],\"HtmlVideoElement\":[\"Element\",\"EventTarget\",\"HtmlElement\",\"HtmlMediaElement\",\"Node\"],\"HttpConnDict\":[],\"HttpConnInfo\":[],\"HttpConnectionElement\":[],\"IdbCursor\":[],\"IdbCursorDirection\":[],\"IdbCursorWithValue\":[\"IdbCursor\"],\"IdbDatabase\":[\"EventTarget\"],\"IdbFactory\":[],\"IdbFileHandle\":[\"EventTarget\"],\"IdbFileMetadataParameters\":[],\"IdbFileRequest\":[\"DomRequest\",\"EventTarget\"],\"IdbIndex\":[],\"IdbIndexParameters\":[],\"IdbKeyRange\":[],\"IdbLocaleAwareKeyRange\":[\"IdbKeyRange\"],\"IdbMutableFile\":[\"EventTarget\"],\"IdbObjectStore\":[],\"IdbObjectStoreParameters\":[],\"IdbOpenDbOptions\":[],\"IdbOpenDbRequest\":[\"EventTarget\",\"IdbRequest\"],\"IdbRequest\":[\"EventTarget\"],\"IdbRequestReadyState\":[],\"IdbTransaction\":[\"EventTarget\"],\"IdbTransactionDurability\":[],\"IdbTransactionMode\":[],\"IdbTransactionOptions\":[],\"IdbVersionChangeEvent\":[\"Event\"],\"IdbVersionChangeEventInit\":[],\"IdleDeadline\":[],\"IdleRequestOptions\":[],\"IirFilterNode\":[\"AudioNode\",\"EventTarget\"],\"IirFilterOptions\":[],\"ImageBitmap\":[],\"ImageBitmapOptions\":[],\"ImageBitmapRenderingContext\":[],\"ImageCapture\":[],\"ImageCaptureError\":[],\"ImageCaptureErrorEvent\":[\"Event\"],\"ImageCaptureErrorEventInit\":[],\"ImageData\":[],\"ImageDecodeOptions\":[],\"ImageDecodeResult\":[],\"ImageDecoder\":[],\"ImageDecoderInit\":[],\"ImageEncodeOptions\":[],\"ImageOrientation\":[],\"ImageTrack\":[\"EventTarget\"],\"ImageTrackList\":[],\"InputDeviceInfo\":[\"MediaDeviceInfo\"],\"InputEvent\":[\"Event\",\"UiEvent\"],\"InputEventInit\":[],\"IntersectionObserver\":[],\"IntersectionObserverEntry\":[],\"IntersectionObserverEntryInit\":[],\"IntersectionObserverInit\":[],\"IntlUtils\":[],\"IsInputPendingOptions\":[],\"IterableKeyAndValueResult\":[],\"IterableKeyOrValueResult\":[],\"IterationCompositeOperation\":[],\"JsonWebKey\":[],\"KeyAlgorithm\":[],\"KeyEvent\":[],\"KeyFrameRequestEvent\":[\"Event\"],\"KeyIdsInitData\":[],\"KeyboardEvent\":[\"Event\",\"UiEvent\"],\"KeyboardEventInit\":[],\"KeyframeAnimationOptions\":[],\"KeyframeEffect\":[\"AnimationEffect\"],\"KeyframeEffectOptions\":[],\"L10nElement\":[],\"L10nValue\":[],\"LargeBlobSupport\":[],\"LatencyMode\":[],\"LifecycleCallbacks\":[],\"LineAlignSetting\":[],\"ListBoxObject\":[],\"LocalMediaStream\":[\"EventTarget\",\"MediaStream\"],\"LocaleInfo\":[],\"Location\":[],\"Lock\":[],\"LockInfo\":[],\"LockManager\":[],\"LockManagerSnapshot\":[],\"LockMode\":[],\"LockOptions\":[],\"MathMlElement\":[\"Element\",\"EventTarget\",\"Node\"],\"MediaCapabilities\":[],\"MediaCapabilitiesInfo\":[],\"MediaConfiguration\":[],\"MediaDecodingConfiguration\":[],\"MediaDecodingType\":[],\"MediaDeviceInfo\":[],\"MediaDeviceKind\":[],\"MediaDevices\":[\"EventTarget\"],\"MediaElementAudioSourceNode\":[\"AudioNode\",\"EventTarget\"],\"MediaElementAudioSourceOptions\":[],\"MediaEncodingConfiguration\":[],\"MediaEncodingType\":[],\"MediaEncryptedEvent\":[\"Event\"],\"MediaError\":[],\"MediaImage\":[],\"MediaKeyError\":[\"Event\"],\"MediaKeyMessageEvent\":[\"Event\"],\"MediaKeyMessageEventInit\":[],\"MediaKeyMessageType\":[],\"MediaKeyNeededEventInit\":[],\"MediaKeySession\":[\"EventTarget\"],\"MediaKeySessionType\":[],\"MediaKeyStatus\":[],\"MediaKeyStatusMap\":[],\"MediaKeySystemAccess\":[],\"MediaKeySystemConfiguration\":[],\"MediaKeySystemMediaCapability\":[],\"MediaKeySystemStatus\":[],\"MediaKeys\":[],\"MediaKeysPolicy\":[],\"MediaKeysRequirement\":[],\"MediaList\":[],\"MediaMetadata\":[],\"MediaMetadataInit\":[],\"MediaPositionState\":[],\"MediaQueryList\":[\"EventTarget\"],\"MediaQueryListEvent\":[\"Event\"],\"MediaQueryListEventInit\":[],\"MediaRecorder\":[\"EventTarget\"],\"MediaRecorderErrorEvent\":[\"Event\"],\"MediaRecorderErrorEventInit\":[],\"MediaRecorderOptions\":[],\"MediaSession\":[],\"MediaSessionAction\":[],\"MediaSessionActionDetails\":[],\"MediaSessionPlaybackState\":[],\"MediaSource\":[\"EventTarget\"],\"MediaSourceEndOfStreamError\":[],\"MediaSourceEnum\":[],\"MediaSourceReadyState\":[],\"MediaStream\":[\"EventTarget\"],\"MediaStreamAudioDestinationNode\":[\"AudioNode\",\"EventTarget\"],\"MediaStreamAudioSourceNode\":[\"AudioNode\",\"EventTarget\"],\"MediaStreamAudioSourceOptions\":[],\"MediaStreamConstraints\":[],\"MediaStreamError\":[],\"MediaStreamEvent\":[\"Event\"],\"MediaStreamEventInit\":[],\"MediaStreamTrack\":[\"EventTarget\"],\"MediaStreamTrackEvent\":[\"Event\"],\"MediaStreamTrackEventInit\":[],\"MediaStreamTrackGenerator\":[\"EventTarget\",\"MediaStreamTrack\"],\"MediaStreamTrackGeneratorInit\":[],\"MediaStreamTrackProcessor\":[],\"MediaStreamTrackProcessorInit\":[],\"MediaStreamTrackState\":[],\"MediaTrackCapabilities\":[],\"MediaTrackConstraintSet\":[],\"MediaTrackConstraints\":[],\"MediaTrackSettings\":[],\"MediaTrackSupportedConstraints\":[],\"MemoryAttribution\":[],\"MemoryAttributionContainer\":[],\"MemoryBreakdownEntry\":[],\"MemoryMeasurement\":[],\"MessageChannel\":[],\"MessageEvent\":[\"Event\"],\"MessageEventInit\":[],\"MessagePort\":[\"EventTarget\"],\"MidiAccess\":[\"EventTarget\"],\"MidiConnectionEvent\":[\"Event\"],\"MidiConnectionEventInit\":[],\"MidiInput\":[\"EventTarget\",\"MidiPort\"],\"MidiInputMap\":[],\"MidiMessageEvent\":[\"Event\"],\"MidiMessageEventInit\":[],\"MidiOptions\":[],\"MidiOutput\":[\"EventTarget\",\"MidiPort\"],\"MidiOutputMap\":[],\"MidiPort\":[\"EventTarget\"],\"MidiPortConnectionState\":[],\"MidiPortDeviceState\":[],\"MidiPortType\":[],\"MimeType\":[],\"MimeTypeArray\":[],\"MouseEvent\":[\"Event\",\"UiEvent\"],\"MouseEventInit\":[],\"MouseScrollEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"MozDebug\":[],\"MutationEvent\":[\"Event\"],\"MutationObserver\":[],\"MutationObserverInit\":[],\"MutationObservingInfo\":[],\"MutationRecord\":[],\"NamedNodeMap\":[],\"NativeOsFileReadOptions\":[],\"NativeOsFileWriteAtomicOptions\":[],\"NavigationType\":[],\"Navigator\":[],\"NavigatorAutomationInformation\":[],\"NavigatorUaBrandVersion\":[],\"NavigatorUaData\":[],\"NetworkCommandOptions\":[],\"NetworkInformation\":[\"EventTarget\"],\"NetworkResultOptions\":[],\"Node\":[\"EventTarget\"],\"NodeFilter\":[],\"NodeIterator\":[],\"NodeList\":[],\"Notification\":[\"EventTarget\"],\"NotificationAction\":[],\"NotificationDirection\":[],\"NotificationEvent\":[\"Event\",\"ExtendableEvent\"],\"NotificationEventInit\":[],\"NotificationOptions\":[],\"NotificationPermission\":[],\"ObserverCallback\":[],\"OesElementIndexUint\":[],\"OesStandardDerivatives\":[],\"OesTextureFloat\":[],\"OesTextureFloatLinear\":[],\"OesTextureHalfFloat\":[],\"OesTextureHalfFloatLinear\":[],\"OesVertexArrayObject\":[],\"OfflineAudioCompletionEvent\":[\"Event\"],\"OfflineAudioCompletionEventInit\":[],\"OfflineAudioContext\":[\"BaseAudioContext\",\"EventTarget\"],\"OfflineAudioContextOptions\":[],\"OfflineResourceList\":[\"EventTarget\"],\"OffscreenCanvas\":[\"EventTarget\"],\"OffscreenCanvasRenderingContext2d\":[],\"OpenFilePickerOptions\":[],\"OpenWindowEventDetail\":[],\"OptionalEffectTiming\":[],\"OrientationLockType\":[],\"OrientationType\":[],\"OscillatorNode\":[\"AudioNode\",\"AudioScheduledSourceNode\",\"EventTarget\"],\"OscillatorOptions\":[],\"OscillatorType\":[],\"OverSampleType\":[],\"OvrMultiview2\":[],\"PageTransitionEvent\":[\"Event\"],\"PageTransitionEventInit\":[],\"PaintRequest\":[],\"PaintRequestList\":[],\"PaintWorkletGlobalScope\":[\"WorkletGlobalScope\"],\"PannerNode\":[\"AudioNode\",\"EventTarget\"],\"PannerOptions\":[],\"PanningModelType\":[],\"ParityType\":[],\"Path2d\":[],\"PaymentAddress\":[],\"PaymentComplete\":[],\"PaymentMethodChangeEvent\":[\"Event\",\"PaymentRequestUpdateEvent\"],\"PaymentMethodChangeEventInit\":[],\"PaymentRequestUpdateEvent\":[\"Event\"],\"PaymentRequestUpdateEventInit\":[],\"PaymentResponse\":[],\"Pbkdf2Params\":[],\"PcImplIceConnectionState\":[],\"PcImplIceGatheringState\":[],\"PcImplSignalingState\":[],\"PcObserverStateType\":[],\"Performance\":[\"EventTarget\"],\"PerformanceEntry\":[],\"PerformanceEntryEventInit\":[],\"PerformanceEntryFilterOptions\":[],\"PerformanceMark\":[\"PerformanceEntry\"],\"PerformanceMeasure\":[\"PerformanceEntry\"],\"PerformanceNavigation\":[],\"PerformanceNavigationTiming\":[\"PerformanceEntry\",\"PerformanceResourceTiming\"],\"PerformanceObserver\":[],\"PerformanceObserverEntryList\":[],\"PerformanceObserverInit\":[],\"PerformanceResourceTiming\":[\"PerformanceEntry\"],\"PerformanceServerTiming\":[],\"PerformanceTiming\":[],\"PeriodicWave\":[],\"PeriodicWaveConstraints\":[],\"PeriodicWaveOptions\":[],\"PermissionDescriptor\":[],\"PermissionName\":[],\"PermissionState\":[],\"PermissionStatus\":[\"EventTarget\"],\"Permissions\":[],\"PictureInPictureEvent\":[\"Event\"],\"PictureInPictureEventInit\":[],\"PictureInPictureWindow\":[\"EventTarget\"],\"PlaneLayout\":[],\"PlaybackDirection\":[],\"Plugin\":[],\"PluginArray\":[],\"PluginCrashedEventInit\":[],\"PointerEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"PointerEventInit\":[],\"PopStateEvent\":[\"Event\"],\"PopStateEventInit\":[],\"PopupBlockedEvent\":[\"Event\"],\"PopupBlockedEventInit\":[],\"Position\":[],\"PositionAlignSetting\":[],\"PositionError\":[],\"PositionOptions\":[],\"PremultiplyAlpha\":[],\"Presentation\":[],\"PresentationAvailability\":[\"EventTarget\"],\"PresentationConnection\":[\"EventTarget\"],\"PresentationConnectionAvailableEvent\":[\"Event\"],\"PresentationConnectionAvailableEventInit\":[],\"PresentationConnectionBinaryType\":[],\"PresentationConnectionCloseEvent\":[\"Event\"],\"PresentationConnectionCloseEventInit\":[],\"PresentationConnectionClosedReason\":[],\"PresentationConnectionList\":[\"EventTarget\"],\"PresentationConnectionState\":[],\"PresentationReceiver\":[],\"PresentationRequest\":[\"EventTarget\"],\"PresentationStyle\":[],\"ProcessingInstruction\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"ProfileTimelineLayerRect\":[],\"ProfileTimelineMarker\":[],\"ProfileTimelineMessagePortOperationType\":[],\"ProfileTimelineStackFrame\":[],\"ProfileTimelineWorkerOperationType\":[],\"ProgressEvent\":[\"Event\"],\"ProgressEventInit\":[],\"PromiseNativeHandler\":[],\"PromiseRejectionEvent\":[\"Event\"],\"PromiseRejectionEventInit\":[],\"PublicKeyCredential\":[\"Credential\"],\"PublicKeyCredentialCreationOptions\":[],\"PublicKeyCredentialCreationOptionsJson\":[],\"PublicKeyCredentialDescriptor\":[],\"PublicKeyCredentialDescriptorJson\":[],\"PublicKeyCredentialEntity\":[],\"PublicKeyCredentialHints\":[],\"PublicKeyCredentialParameters\":[],\"PublicKeyCredentialRequestOptions\":[],\"PublicKeyCredentialRequestOptionsJson\":[],\"PublicKeyCredentialRpEntity\":[],\"PublicKeyCredentialType\":[],\"PublicKeyCredentialUserEntity\":[],\"PublicKeyCredentialUserEntityJson\":[],\"PushEncryptionKeyName\":[],\"PushEvent\":[\"Event\",\"ExtendableEvent\"],\"PushEventInit\":[],\"PushManager\":[],\"PushMessageData\":[],\"PushPermissionState\":[],\"PushSubscription\":[],\"PushSubscriptionInit\":[],\"PushSubscriptionJson\":[],\"PushSubscriptionKeys\":[],\"PushSubscriptionOptions\":[],\"PushSubscriptionOptionsInit\":[],\"QueryOptions\":[],\"QueuingStrategy\":[],\"QueuingStrategyInit\":[],\"RadioNodeList\":[\"NodeList\"],\"Range\":[],\"RcwnPerfStats\":[],\"RcwnStatus\":[],\"ReadableByteStreamController\":[],\"ReadableStream\":[],\"ReadableStreamByobReader\":[],\"ReadableStreamByobRequest\":[],\"ReadableStreamDefaultController\":[],\"ReadableStreamDefaultReader\":[],\"ReadableStreamGetReaderOptions\":[],\"ReadableStreamIteratorOptions\":[],\"ReadableStreamReadResult\":[],\"ReadableStreamReaderMode\":[],\"ReadableStreamType\":[],\"ReadableWritablePair\":[],\"RecordingState\":[],\"ReferrerPolicy\":[],\"RegisterRequest\":[],\"RegisterResponse\":[],\"RegisteredKey\":[],\"RegistrationOptions\":[],\"RegistrationResponseJson\":[],\"Request\":[],\"RequestCache\":[],\"RequestCredentials\":[],\"RequestDestination\":[],\"RequestDeviceOptions\":[],\"RequestInit\":[],\"RequestMediaKeySystemAccessNotification\":[],\"RequestMode\":[],\"RequestRedirect\":[],\"ResidentKeyRequirement\":[],\"ResizeObserver\":[],\"ResizeObserverBoxOptions\":[],\"ResizeObserverEntry\":[],\"ResizeObserverOptions\":[],\"ResizeObserverSize\":[],\"ResizeQuality\":[],\"Response\":[],\"ResponseInit\":[],\"ResponseType\":[],\"RsaHashedImportParams\":[],\"RsaOaepParams\":[],\"RsaOtherPrimesInfo\":[],\"RsaPssParams\":[],\"RtcAnswerOptions\":[],\"RtcBundlePolicy\":[],\"RtcCertificate\":[],\"RtcCertificateExpiration\":[],\"RtcCodecStats\":[],\"RtcConfiguration\":[],\"RtcDataChannel\":[\"EventTarget\"],\"RtcDataChannelEvent\":[\"Event\"],\"RtcDataChannelEventInit\":[],\"RtcDataChannelInit\":[],\"RtcDataChannelState\":[],\"RtcDataChannelType\":[],\"RtcDegradationPreference\":[],\"RtcEncodedAudioFrame\":[],\"RtcEncodedAudioFrameMetadata\":[],\"RtcEncodedAudioFrameOptions\":[],\"RtcEncodedVideoFrame\":[],\"RtcEncodedVideoFrameMetadata\":[],\"RtcEncodedVideoFrameOptions\":[],\"RtcEncodedVideoFrameType\":[],\"RtcFecParameters\":[],\"RtcIceCandidate\":[],\"RtcIceCandidateInit\":[],\"RtcIceCandidatePairStats\":[],\"RtcIceCandidateStats\":[],\"RtcIceComponentStats\":[],\"RtcIceConnectionState\":[],\"RtcIceCredentialType\":[],\"RtcIceGatheringState\":[],\"RtcIceServer\":[],\"RtcIceTransportPolicy\":[],\"RtcIdentityAssertion\":[],\"RtcIdentityAssertionResult\":[],\"RtcIdentityProvider\":[],\"RtcIdentityProviderDetails\":[],\"RtcIdentityProviderOptions\":[],\"RtcIdentityProviderRegistrar\":[],\"RtcIdentityValidationResult\":[],\"RtcInboundRtpStreamStats\":[],\"RtcMediaStreamStats\":[],\"RtcMediaStreamTrackStats\":[],\"RtcOfferAnswerOptions\":[],\"RtcOfferOptions\":[],\"RtcOutboundRtpStreamStats\":[],\"RtcPeerConnection\":[\"EventTarget\"],\"RtcPeerConnectionIceErrorEvent\":[\"Event\"],\"RtcPeerConnectionIceEvent\":[\"Event\"],\"RtcPeerConnectionIceEventInit\":[],\"RtcPeerConnectionState\":[],\"RtcPriorityType\":[],\"RtcRtcpParameters\":[],\"RtcRtpCapabilities\":[],\"RtcRtpCodecCapability\":[],\"RtcRtpCodecParameters\":[],\"RtcRtpContributingSource\":[],\"RtcRtpEncodingParameters\":[],\"RtcRtpHeaderExtensionCapability\":[],\"RtcRtpHeaderExtensionParameters\":[],\"RtcRtpParameters\":[],\"RtcRtpReceiver\":[],\"RtcRtpScriptTransform\":[],\"RtcRtpScriptTransformer\":[\"EventTarget\"],\"RtcRtpSender\":[],\"RtcRtpSourceEntry\":[],\"RtcRtpSourceEntryType\":[],\"RtcRtpSynchronizationSource\":[],\"RtcRtpTransceiver\":[],\"RtcRtpTransceiverDirection\":[],\"RtcRtpTransceiverInit\":[],\"RtcRtxParameters\":[],\"RtcSdpType\":[],\"RtcSessionDescription\":[],\"RtcSessionDescriptionInit\":[],\"RtcSignalingState\":[],\"RtcStats\":[],\"RtcStatsIceCandidatePairState\":[],\"RtcStatsIceCandidateType\":[],\"RtcStatsReport\":[],\"RtcStatsReportInternal\":[],\"RtcStatsType\":[],\"RtcTrackEvent\":[\"Event\"],\"RtcTrackEventInit\":[],\"RtcTransformEvent\":[\"Event\"],\"RtcTransportStats\":[],\"RtcdtmfSender\":[\"EventTarget\"],\"RtcdtmfToneChangeEvent\":[\"Event\"],\"RtcdtmfToneChangeEventInit\":[],\"RtcrtpContributingSourceStats\":[],\"RtcrtpStreamStats\":[],\"SFrameTransform\":[\"EventTarget\"],\"SFrameTransformErrorEvent\":[\"Event\"],\"SFrameTransformErrorEventInit\":[],\"SFrameTransformErrorEventType\":[],\"SFrameTransformOptions\":[],\"SFrameTransformRole\":[],\"SaveFilePickerOptions\":[],\"Scheduler\":[],\"SchedulerPostTaskOptions\":[],\"Scheduling\":[],\"Screen\":[\"EventTarget\"],\"ScreenColorGamut\":[],\"ScreenDetailed\":[\"EventTarget\",\"Screen\"],\"ScreenDetails\":[\"EventTarget\"],\"ScreenLuminance\":[],\"ScreenOrientation\":[\"EventTarget\"],\"ScriptProcessorNode\":[\"AudioNode\",\"EventTarget\"],\"ScrollAreaEvent\":[\"Event\",\"UiEvent\"],\"ScrollBehavior\":[],\"ScrollBoxObject\":[],\"ScrollIntoViewContainer\":[],\"ScrollIntoViewOptions\":[],\"ScrollLogicalPosition\":[],\"ScrollOptions\":[],\"ScrollRestoration\":[],\"ScrollSetting\":[],\"ScrollState\":[],\"ScrollToOptions\":[],\"ScrollViewChangeEventInit\":[],\"SecurityPolicyViolationEvent\":[\"Event\"],\"SecurityPolicyViolationEventDisposition\":[],\"SecurityPolicyViolationEventInit\":[],\"Selection\":[],\"SelectionMode\":[],\"Serial\":[\"EventTarget\"],\"SerialInputSignals\":[],\"SerialOptions\":[],\"SerialOutputSignals\":[],\"SerialPort\":[\"EventTarget\"],\"SerialPortFilter\":[],\"SerialPortInfo\":[],\"SerialPortRequestOptions\":[],\"ServerSocketOptions\":[],\"ServiceWorker\":[\"EventTarget\"],\"ServiceWorkerContainer\":[\"EventTarget\"],\"ServiceWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"ServiceWorkerRegistration\":[\"EventTarget\"],\"ServiceWorkerState\":[],\"ServiceWorkerUpdateViaCache\":[],\"ShadowRoot\":[\"DocumentFragment\",\"EventTarget\",\"Node\"],\"ShadowRootInit\":[],\"ShadowRootMode\":[],\"ShareData\":[],\"SharedWorker\":[\"EventTarget\"],\"SharedWorkerGlobalScope\":[\"EventTarget\",\"WorkerGlobalScope\"],\"SignResponse\":[],\"SocketElement\":[],\"SocketOptions\":[],\"SocketReadyState\":[],\"SocketsDict\":[],\"SourceBuffer\":[\"EventTarget\"],\"SourceBufferAppendMode\":[],\"SourceBufferList\":[\"EventTarget\"],\"SpeechGrammar\":[],\"SpeechGrammarList\":[],\"SpeechRecognition\":[\"EventTarget\"],\"SpeechRecognitionAlternative\":[],\"SpeechRecognitionError\":[\"Event\"],\"SpeechRecognitionErrorCode\":[],\"SpeechRecognitionErrorInit\":[],\"SpeechRecognitionEvent\":[\"Event\"],\"SpeechRecognitionEventInit\":[],\"SpeechRecognitionResult\":[],\"SpeechRecognitionResultList\":[],\"SpeechSynthesis\":[\"EventTarget\"],\"SpeechSynthesisErrorCode\":[],\"SpeechSynthesisErrorEvent\":[\"Event\",\"SpeechSynthesisEvent\"],\"SpeechSynthesisErrorEventInit\":[],\"SpeechSynthesisEvent\":[\"Event\"],\"SpeechSynthesisEventInit\":[],\"SpeechSynthesisUtterance\":[\"EventTarget\"],\"SpeechSynthesisVoice\":[],\"StereoPannerNode\":[\"AudioNode\",\"EventTarget\"],\"StereoPannerOptions\":[],\"Storage\":[],\"StorageEstimate\":[],\"StorageEvent\":[\"Event\"],\"StorageEventInit\":[],\"StorageManager\":[],\"StorageType\":[],\"StreamPipeOptions\":[],\"StyleRuleChangeEventInit\":[],\"StyleSheet\":[],\"StyleSheetApplicableStateChangeEventInit\":[],\"StyleSheetChangeEventInit\":[],\"StyleSheetList\":[],\"SubmitEvent\":[\"Event\"],\"SubmitEventInit\":[],\"SubtleCrypto\":[],\"SupportedType\":[],\"SvcOutputMetadata\":[],\"SvgAngle\":[],\"SvgAnimateElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimateMotionElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimateTransformElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgAnimatedAngle\":[],\"SvgAnimatedBoolean\":[],\"SvgAnimatedEnumeration\":[],\"SvgAnimatedInteger\":[],\"SvgAnimatedLength\":[],\"SvgAnimatedLengthList\":[],\"SvgAnimatedNumber\":[],\"SvgAnimatedNumberList\":[],\"SvgAnimatedPreserveAspectRatio\":[],\"SvgAnimatedRect\":[],\"SvgAnimatedString\":[],\"SvgAnimatedTransformList\":[],\"SvgAnimationElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgBoundingBoxOptions\":[],\"SvgCircleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgClipPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgComponentTransferFunctionElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgDefsElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgDescElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgElement\":[\"Element\",\"EventTarget\",\"Node\"],\"SvgEllipseElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgFilterElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgForeignObjectElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgGeometryElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgGraphicsElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgImageElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgLength\":[],\"SvgLengthList\":[],\"SvgLineElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgLinearGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGradientElement\"],\"SvgMarkerElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgMaskElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgMatrix\":[],\"SvgMetadataElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgNumber\":[],\"SvgNumberList\":[],\"SvgPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPathSeg\":[],\"SvgPathSegArcAbs\":[\"SvgPathSeg\"],\"SvgPathSegArcRel\":[\"SvgPathSeg\"],\"SvgPathSegClosePath\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicSmoothAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoCubicSmoothRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticRel\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticSmoothAbs\":[\"SvgPathSeg\"],\"SvgPathSegCurvetoQuadraticSmoothRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoHorizontalAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoHorizontalRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoRel\":[\"SvgPathSeg\"],\"SvgPathSegLinetoVerticalAbs\":[\"SvgPathSeg\"],\"SvgPathSegLinetoVerticalRel\":[\"SvgPathSeg\"],\"SvgPathSegList\":[],\"SvgPathSegMovetoAbs\":[\"SvgPathSeg\"],\"SvgPathSegMovetoRel\":[\"SvgPathSeg\"],\"SvgPatternElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgPoint\":[],\"SvgPointList\":[],\"SvgPolygonElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPolylineElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgPreserveAspectRatio\":[],\"SvgRadialGradientElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGradientElement\"],\"SvgRect\":[],\"SvgRectElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGeometryElement\",\"SvgGraphicsElement\"],\"SvgScriptElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgSetElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgAnimationElement\",\"SvgElement\"],\"SvgStopElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgStringList\":[],\"SvgStyleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgSwitchElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgSymbolElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgTextContentElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgTextElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\",\"SvgTextPositioningElement\"],\"SvgTextPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\"],\"SvgTextPositioningElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\"],\"SvgTitleElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgTransform\":[],\"SvgTransformList\":[],\"SvgUnitTypes\":[],\"SvgUseElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgViewElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgZoomAndPan\":[],\"SvgaElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgfeBlendElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeColorMatrixElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeComponentTransferElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeCompositeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeConvolveMatrixElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDiffuseLightingElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDisplacementMapElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDistantLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeDropShadowElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeFloodElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeFuncAElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncBElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncGElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeFuncRElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgComponentTransferFunctionElement\",\"SvgElement\"],\"SvgfeGaussianBlurElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeImageElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMergeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMergeNodeElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeMorphologyElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeOffsetElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfePointLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeSpecularLightingElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeSpotLightElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeTileElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgfeTurbulenceElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvggElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgmPathElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\"],\"SvgsvgElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\"],\"SvgtSpanElement\":[\"Element\",\"EventTarget\",\"Node\",\"SvgElement\",\"SvgGraphicsElement\",\"SvgTextContentElement\",\"SvgTextPositioningElement\"],\"TaskController\":[\"AbortController\"],\"TaskControllerInit\":[],\"TaskPriority\":[],\"TaskPriorityChangeEvent\":[\"Event\"],\"TaskPriorityChangeEventInit\":[],\"TaskSignal\":[\"AbortSignal\",\"EventTarget\"],\"TaskSignalAnyInit\":[],\"TcpReadyState\":[],\"TcpServerSocket\":[\"EventTarget\"],\"TcpServerSocketEvent\":[\"Event\"],\"TcpServerSocketEventInit\":[],\"TcpSocket\":[\"EventTarget\"],\"TcpSocketBinaryType\":[],\"TcpSocketErrorEvent\":[\"Event\"],\"TcpSocketErrorEventInit\":[],\"TcpSocketEvent\":[\"Event\"],\"TcpSocketEventInit\":[],\"Text\":[\"CharacterData\",\"EventTarget\",\"Node\"],\"TextDecodeOptions\":[],\"TextDecoder\":[],\"TextDecoderOptions\":[],\"TextEncoder\":[],\"TextMetrics\":[],\"TextTrack\":[\"EventTarget\"],\"TextTrackCue\":[\"EventTarget\"],\"TextTrackCueList\":[],\"TextTrackKind\":[],\"TextTrackList\":[\"EventTarget\"],\"TextTrackMode\":[],\"TimeEvent\":[\"Event\"],\"TimeRanges\":[],\"ToggleEvent\":[\"Event\"],\"ToggleEventInit\":[],\"TokenBinding\":[],\"TokenBindingStatus\":[],\"Touch\":[],\"TouchEvent\":[\"Event\",\"UiEvent\"],\"TouchEventInit\":[],\"TouchInit\":[],\"TouchList\":[],\"TrackEvent\":[\"Event\"],\"TrackEventInit\":[],\"TransformStream\":[],\"TransformStreamDefaultController\":[],\"Transformer\":[],\"TransitionEvent\":[\"Event\"],\"TransitionEventInit\":[],\"Transport\":[],\"TreeBoxObject\":[],\"TreeCellInfo\":[],\"TreeView\":[],\"TreeWalker\":[],\"U2f\":[],\"U2fClientData\":[],\"ULongRange\":[],\"UaDataValues\":[],\"UaLowEntropyJson\":[],\"UdpMessageEventInit\":[],\"UdpOptions\":[],\"UiEvent\":[\"Event\"],\"UiEventInit\":[],\"UnderlyingSink\":[],\"UnderlyingSource\":[],\"Url\":[],\"UrlSearchParams\":[],\"Usb\":[\"EventTarget\"],\"UsbAlternateInterface\":[],\"UsbConfiguration\":[],\"UsbConnectionEvent\":[\"Event\"],\"UsbConnectionEventInit\":[],\"UsbControlTransferParameters\":[],\"UsbDevice\":[],\"UsbDeviceFilter\":[],\"UsbDeviceRequestOptions\":[],\"UsbDirection\":[],\"UsbEndpoint\":[],\"UsbEndpointType\":[],\"UsbInTransferResult\":[],\"UsbInterface\":[],\"UsbIsochronousInTransferPacket\":[],\"UsbIsochronousInTransferResult\":[],\"UsbIsochronousOutTransferPacket\":[],\"UsbIsochronousOutTransferResult\":[],\"UsbOutTransferResult\":[],\"UsbPermissionDescriptor\":[],\"UsbPermissionResult\":[\"EventTarget\",\"PermissionStatus\"],\"UsbPermissionStorage\":[],\"UsbRecipient\":[],\"UsbRequestType\":[],\"UsbTransferStatus\":[],\"UserActivation\":[],\"UserProximityEvent\":[\"Event\"],\"UserProximityEventInit\":[],\"UserVerificationRequirement\":[],\"ValidityState\":[],\"ValueEvent\":[\"Event\"],\"ValueEventInit\":[],\"VideoColorPrimaries\":[],\"VideoColorSpace\":[],\"VideoColorSpaceInit\":[],\"VideoConfiguration\":[],\"VideoDecoder\":[],\"VideoDecoderConfig\":[],\"VideoDecoderInit\":[],\"VideoDecoderSupport\":[],\"VideoEncoder\":[],\"VideoEncoderConfig\":[],\"VideoEncoderEncodeOptions\":[],\"VideoEncoderInit\":[],\"VideoEncoderSupport\":[],\"VideoFacingModeEnum\":[],\"VideoFrame\":[],\"VideoFrameBufferInit\":[],\"VideoFrameCopyToOptions\":[],\"VideoFrameInit\":[],\"VideoMatrixCoefficients\":[],\"VideoPixelFormat\":[],\"VideoPlaybackQuality\":[],\"VideoStreamTrack\":[\"EventTarget\",\"MediaStreamTrack\"],\"VideoTrack\":[],\"VideoTrackList\":[\"EventTarget\"],\"VideoTransferCharacteristics\":[],\"ViewTransition\":[],\"VisibilityState\":[],\"VisualViewport\":[\"EventTarget\"],\"VoidCallback\":[],\"VrDisplay\":[\"EventTarget\"],\"VrDisplayCapabilities\":[],\"VrEye\":[],\"VrEyeParameters\":[],\"VrFieldOfView\":[],\"VrFrameData\":[],\"VrLayer\":[],\"VrMockController\":[],\"VrMockDisplay\":[],\"VrPose\":[],\"VrServiceTest\":[],\"VrStageParameters\":[],\"VrSubmitFrameResult\":[],\"VttCue\":[\"EventTarget\",\"TextTrackCue\"],\"VttRegion\":[],\"WakeLock\":[],\"WakeLockSentinel\":[\"EventTarget\"],\"WakeLockType\":[],\"WatchAdvertisementsOptions\":[],\"WaveShaperNode\":[\"AudioNode\",\"EventTarget\"],\"WaveShaperOptions\":[],\"WebGl2RenderingContext\":[],\"WebGlActiveInfo\":[],\"WebGlBuffer\":[],\"WebGlContextAttributes\":[],\"WebGlContextEvent\":[\"Event\"],\"WebGlContextEventInit\":[],\"WebGlFramebuffer\":[],\"WebGlPowerPreference\":[],\"WebGlProgram\":[],\"WebGlQuery\":[],\"WebGlRenderbuffer\":[],\"WebGlRenderingContext\":[],\"WebGlSampler\":[],\"WebGlShader\":[],\"WebGlShaderPrecisionFormat\":[],\"WebGlSync\":[],\"WebGlTexture\":[],\"WebGlTransformFeedback\":[],\"WebGlUniformLocation\":[],\"WebGlVertexArrayObject\":[],\"WebKitCssMatrix\":[\"DomMatrix\",\"DomMatrixReadOnly\"],\"WebSocket\":[\"EventTarget\"],\"WebSocketDict\":[],\"WebSocketElement\":[],\"WebTransport\":[],\"WebTransportBidirectionalStream\":[],\"WebTransportCloseInfo\":[],\"WebTransportCongestionControl\":[],\"WebTransportDatagramDuplexStream\":[],\"WebTransportDatagramStats\":[],\"WebTransportError\":[\"DomException\"],\"WebTransportErrorOptions\":[],\"WebTransportErrorSource\":[],\"WebTransportHash\":[],\"WebTransportOptions\":[],\"WebTransportReceiveStream\":[\"ReadableStream\"],\"WebTransportReceiveStreamStats\":[],\"WebTransportReliabilityMode\":[],\"WebTransportSendStream\":[\"WritableStream\"],\"WebTransportSendStreamOptions\":[],\"WebTransportSendStreamStats\":[],\"WebTransportStats\":[],\"WebglColorBufferFloat\":[],\"WebglCompressedTextureAstc\":[],\"WebglCompressedTextureAtc\":[],\"WebglCompressedTextureEtc\":[],\"WebglCompressedTextureEtc1\":[],\"WebglCompressedTexturePvrtc\":[],\"WebglCompressedTextureS3tc\":[],\"WebglCompressedTextureS3tcSrgb\":[],\"WebglDebugRendererInfo\":[],\"WebglDebugShaders\":[],\"WebglDepthTexture\":[],\"WebglDrawBuffers\":[],\"WebglLoseContext\":[],\"WebglMultiDraw\":[],\"WellKnownDirectory\":[],\"WgslLanguageFeatures\":[],\"WheelEvent\":[\"Event\",\"MouseEvent\",\"UiEvent\"],\"WheelEventInit\":[],\"WidevineCdmManifest\":[],\"Window\":[\"EventTarget\"],\"WindowClient\":[\"Client\"],\"Worker\":[\"EventTarget\"],\"WorkerDebuggerGlobalScope\":[\"EventTarget\"],\"WorkerGlobalScope\":[\"EventTarget\"],\"WorkerLocation\":[],\"WorkerNavigator\":[],\"WorkerOptions\":[],\"WorkerType\":[],\"Worklet\":[],\"WorkletGlobalScope\":[],\"WorkletOptions\":[],\"WritableStream\":[],\"WritableStreamDefaultController\":[],\"WritableStreamDefaultWriter\":[],\"WriteCommandType\":[],\"WriteParams\":[],\"XPathExpression\":[],\"XPathNsResolver\":[],\"XPathResult\":[],\"XmlDocument\":[\"Document\",\"EventTarget\",\"Node\"],\"XmlHttpRequest\":[\"EventTarget\",\"XmlHttpRequestEventTarget\"],\"XmlHttpRequestEventTarget\":[\"EventTarget\"],\"XmlHttpRequestResponseType\":[],\"XmlHttpRequestUpload\":[\"EventTarget\",\"XmlHttpRequestEventTarget\"],\"XmlSerializer\":[],\"XrBoundedReferenceSpace\":[\"EventTarget\",\"XrReferenceSpace\",\"XrSpace\"],\"XrEye\":[],\"XrFrame\":[],\"XrHand\":[],\"XrHandJoint\":[],\"XrHandedness\":[],\"XrInputSource\":[],\"XrInputSourceArray\":[],\"XrInputSourceEvent\":[\"Event\"],\"XrInputSourceEventInit\":[],\"XrInputSourcesChangeEvent\":[\"Event\"],\"XrInputSourcesChangeEventInit\":[],\"XrJointPose\":[\"XrPose\"],\"XrJointSpace\":[\"EventTarget\",\"XrSpace\"],\"XrLayer\":[\"EventTarget\"],\"XrPermissionDescriptor\":[],\"XrPermissionStatus\":[\"EventTarget\",\"PermissionStatus\"],\"XrPose\":[],\"XrReferenceSpace\":[\"EventTarget\",\"XrSpace\"],\"XrReferenceSpaceEvent\":[\"Event\"],\"XrReferenceSpaceEventInit\":[],\"XrReferenceSpaceType\":[],\"XrRenderState\":[],\"XrRenderStateInit\":[],\"XrRigidTransform\":[],\"XrSession\":[\"EventTarget\"],\"XrSessionEvent\":[\"Event\"],\"XrSessionEventInit\":[],\"XrSessionInit\":[],\"XrSessionMode\":[],\"XrSessionSupportedPermissionDescriptor\":[],\"XrSpace\":[\"EventTarget\"],\"XrSystem\":[\"EventTarget\"],\"XrTargetRayMode\":[],\"XrView\":[],\"XrViewerPose\":[\"XrPose\"],\"XrViewport\":[],\"XrVisibilityState\":[],\"XrWebGlLayer\":[\"EventTarget\",\"XrLayer\"],\"XrWebGlLayerInit\":[],\"XsltProcessor\":[],\"console\":[],\"css\":[],\"default\":[\"std\"],\"gpu_buffer_usage\":[],\"gpu_color_write\":[],\"gpu_map_mode\":[],\"gpu_shader_stage\":[],\"gpu_texture_usage\":[],\"std\":[\"wasm-bindgen/std\",\"js-sys/std\"]}}", "web-time_1.1.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-channel\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"js\"],\"kind\":\"dev\",\"name\":\"getrandom\",\"req\":\"^0.2\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"js-sys\",\"req\":\"^0.3.20\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"features\":[\"macro\"],\"kind\":\"dev\",\"name\":\"pollster\",\"req\":\"^0.3\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"wasm-bindgen\",\"req\":\"^0.2.70\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-futures\",\"req\":\"^0.4\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"},{\"features\":[\"WorkerGlobalScope\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", target_feature = \\\"atomics\\\"))\"},{\"features\":[\"CssStyleDeclaration\",\"Document\",\"Element\",\"HtmlTableElement\",\"HtmlTableRowElement\",\"Performance\",\"Window\"],\"kind\":\"dev\",\"name\":\"web-sys\",\"req\":\"^0.3\",\"target\":\"cfg(target_family = \\\"wasm\\\")\"}],\"features\":{\"serde\":[\"dep:serde\"]}}", diff --git a/codex-rs/.cargo/audit.toml b/codex-rs/.cargo/audit.toml index c1d8ff87ee..56feb5edb3 100644 --- a/codex-rs/.cargo/audit.toml +++ b/codex-rs/.cargo/audit.toml @@ -1,5 +1,5 @@ [advisories] -# Reviewed 2026-06-07. Keep this list in sync with ../deny.toml. +# Reviewed 2026-07-02. Keep this list in sync with ../deny.toml. ignore = [ "RUSTSEC-2024-0388", # derivative 2.2.0 via starlark/starlark_syntax; upstream crate is unmaintained "RUSTSEC-2025-0057", # fxhash 0.2.1 via starlark_map/bm25; upstream crate is unmaintained @@ -10,4 +10,6 @@ ignore = [ "RUSTSEC-2026-0118", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net "RUSTSEC-2026-0119", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net "RUSTSEC-2026-0173", # proc-macro-error2 via i18n-embed-fl/age/codex-secrets; remove when local secrets storage migrates off age or age drops i18n-embed-fl + "RUSTSEC-2026-0194", # quick-xml via plist/syntect and wayland-scanner; trusted inputs only; remove when rust-plist#191 and wayland-rs#938 are released + "RUSTSEC-2026-0195", # quick-xml via plist/syntect and wayland-scanner; trusted inputs only; remove when rust-plist#191 and wayland-rs#938 are released ] diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 88601e9384..3561550db5 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3712,7 +3712,7 @@ dependencies = [ "icu_provider", "landlock", "pretty_assertions", - "quick-xml", + "quick-xml 0.41.0", "reqwest 0.12.28", "schemars 0.8.22", "seccompiler", @@ -10271,13 +10271,13 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" dependencies = [ "base64 0.22.1", "indexmap 2.14.0", - "quick-xml", + "quick-xml 0.39.4", "serde", "time", ] @@ -10710,9 +10710,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.4" +version = "0.39.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" dependencies = [ "memchr", "serde", @@ -14673,12 +14682,12 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.8" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.39.4", "quote", ] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 34097b56aa..2f73c216a9 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -362,7 +362,7 @@ portable-pty = "0.9.0" predicates = "3" pretty_assertions = "1.4.1" pulldown-cmark = "0.10" -quick-xml = "0.38.4" +quick-xml = "0.41.0" rand = "0.9" ratatui = "0.29.0" ratatui-macros = "0.6.0" diff --git a/codex-rs/deny.toml b/codex-rs/deny.toml index 9d06651e58..4e52422f39 100644 --- a/codex-rs/deny.toml +++ b/codex-rs/deny.toml @@ -70,7 +70,7 @@ feature-depth = 1 # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - # Reviewed 2026-06-07. Keep this list in sync with .cargo/audit.toml. + # Reviewed 2026-07-02. Keep this list in sync with .cargo/audit.toml. # Each exception must identify the dependency path and removal condition. { id = "RUSTSEC-2024-0388", reason = "derivative is unmaintained; pulled in via starlark/starlark_syntax v0.14.2 used by execpolicy/cli/core; no fixed starlark release yet" }, { id = "RUSTSEC-2025-0057", reason = "fxhash is unmaintained; pulled in via starlark_map under starlark v0.14.2 used by execpolicy/cli/core and bm25 used by core; remove when upstream dependencies drop fxhash" }, @@ -82,6 +82,8 @@ ignore = [ { id = "RUSTSEC-2026-0118", reason = "hickory-proto v0.25.2 is pulled in via rama-dns/rama-tcp used by codex-network-proxy; DNSSEC features are not enabled; remove when rama updates to hickory 0.26.1 or hickory-net" }, { id = "RUSTSEC-2026-0119", reason = "hickory-proto v0.25.2 is pulled in via rama-dns/rama-tcp used by codex-network-proxy; no fixed rama release is available yet; remove when rama updates to hickory 0.26.1 or hickory-net" }, { id = "RUSTSEC-2026-0173", reason = "proc-macro-error2 is unmaintained; pulled in via i18n-embed-fl/age used by codex-secrets local storage; remove when codex-secrets migrates off age or age drops i18n-embed-fl" }, + { id = "RUSTSEC-2026-0194", reason = "quick-xml v0.39.4 remains via plist/syntect and wayland-scanner; plist does not exercise the affected APIs and wayland-scanner parses trusted build-time XML; remove when rust-plist#191 and wayland-rs#938 are released" }, + { id = "RUSTSEC-2026-0195", reason = "quick-xml v0.39.4 remains via plist/syntect and wayland-scanner; plist does not exercise the affected APIs and wayland-scanner parses trusted build-time XML; remove when rust-plist#191 and wayland-rs#938 are released" }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library.