Replace Guardian tickets with parent response IDs (#43002)

## What changed

Capture the ID from `response.created` and pass it as `parent_response_id` in Guardian review and classifier request metadata. Clear the current response ID before sampling retries so tool reviews cannot inherit a failed response's ID.

Request Guardian credits with `guardian_credits_requested` on eligible parent requests. Remove the ticket protocol, transport attachment, and ticket-specific body redaction. Keep ordinary app-server client `parent_response_id` values inside `x-codex-turn-metadata` rather than promoting them to Guardian request metadata.

## Testing

Update coverage for HTTP fallback, WebSocket review reuse, missing response IDs, classifier transport retries, and code-mode yield/resume attribution. Extend app-server tests to verify client metadata placement over HTTP and WebSocket.

GitOrigin-RevId: c1665e56382ecec8d260fc2ca0ecd5e20b0b2391
This commit is contained in:
jif
2026-09-05 13:02:49 +00:00
committed by copyberry
parent a7a4321593
commit dee21ec1bc
32 changed files with 178 additions and 362 deletions
+2 -24
View File
@@ -11,7 +11,7 @@ use std::time::Duration;
/// Clones share the encoded allocation. Internally, the body can also hold the
/// final compressed wire bytes while retaining the original JSON only when
/// request-body trace logging is enabled.
#[derive(Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EncodedJsonBody {
bytes: Bytes,
trace_bytes: Option<Bytes>,
@@ -33,27 +33,11 @@ impl EncodedJsonBody {
&self.bytes
}
/// Omits credential-bearing bodies from tracing and debug output, including retries.
pub fn without_body_logging(mut self) -> Self {
self.trace_bytes = Some(Bytes::from_static(b"<redacted>"));
self
}
pub(crate) fn trace_bytes(&self) -> &[u8] {
self.trace_bytes.as_ref().unwrap_or(&self.bytes)
}
}
impl std::fmt::Debug for EncodedJsonBody {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("EncodedJsonBody")
.field("body", &String::from_utf8_lossy(self.trace_bytes()))
.field("prepared", &self.prepared)
.finish()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RequestCompression {
#[default]
@@ -136,13 +120,7 @@ impl Request {
self.body,
Some(RequestBody::Json(_) | RequestBody::EncodedJson(_))
);
let retained_trace_bytes = match self.body.as_ref() {
Some(RequestBody::EncodedJson(body)) => body.trace_bytes.clone(),
Some(RequestBody::Json(_) | RequestBody::Raw(_)) | None => None,
};
let trace_bytes = if retained_trace_bytes.is_some() {
retained_trace_bytes
} else if self.compression != RequestCompression::None
let trace_bytes = if self.compression != RequestCompression::None
&& tracing::enabled!(target: "codex_http_client::transport", tracing::Level::TRACE)
{
match self.body.as_ref() {
@@ -1,7 +1,4 @@
use super::*;
use crate::request::EncodedJsonBody;
use crate::request::RequestCompression;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::io::Write;
use std::sync::Arc;
@@ -31,35 +28,6 @@ async fn disabled_request_logging_suppresses_transport_url_and_body() {
assert!(!logs.contains("body-secret"));
}
#[test]
fn sensitive_encoded_body_stays_redacted_after_preparation_and_retries() {
let payload = json!({"client_metadata": {"guardian_ticket": "secret-receipt"}});
for compression in [RequestCompression::None, RequestCompression::Zstd] {
let mut request = Request::new(Method::POST, "https://example.com/responses".into())
.with_compression(compression);
request.body = Some(RequestBody::EncodedJson(
EncodedJsonBody::encode(&payload)
.unwrap()
.without_body_logging(),
));
assert_eq!(request_body_for_trace(&request), "<redacted>");
for _ in 0..2 {
request = request.clone().into_prepared().unwrap();
assert_eq!(request_body_for_trace(&request), "<redacted>");
assert!(!format!("{request:?}").contains("secret-receipt"));
let body = request.prepare_body_for_send().unwrap().body.unwrap();
let decoded = match compression {
RequestCompression::None => body.to_vec(),
RequestCompression::Zstd => zstd::stream::decode_all(body.as_ref()).unwrap(),
};
assert_eq!(
serde_json::from_slice::<serde_json::Value>(&decoded).unwrap(),
payload
);
}
}
}
#[tokio::test]
async fn connection_failures_are_classified_without_exposing_request_urls() {
let unavailable_server =