diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs index 8696df4813..2510f35d9b 100644 --- a/codex-rs/codex-api/src/common.rs +++ b/codex-rs/codex-api/src/common.rs @@ -93,6 +93,8 @@ pub enum ResponseEvent { ModelVerifications(Vec), /// Emitted when the server includes moderation metadata for first-party turn presentation. TurnModerationMetadata(TurnModerationMetadataEvent), + /// Action-bound Guardian evidence for request-scoped consumers, not model history. + GuardianAssessment(crate::guardian_assessment::GuardianAssessment), /// Emitted when `X-Reasoning-Included: true` is present on the response, /// meaning the server already accounted for past reasoning tokens and the /// client should not re-estimate them. diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index 0c31969017..f43e285d00 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -898,10 +898,9 @@ async fn run_websocket_response_stream( { let _ = turn_state.set(response_turn_state); } - let model_verifications = event.model_verifications(); - let turn_moderation_metadata = event.turn_moderation_metadata(); let safety_buffering = safety_buffering_for_event(&event, &mut safety_buffering_treatment); + let metadata_events = event.metadata_events(safety_buffering); if event.kind() == "codex.rate_limits" { if let Some(snapshot) = parse_rate_limit_event(&text) { let _ = tx_event.send(Ok(ResponseEvent::RateLimits(snapshot))).await; @@ -925,35 +924,12 @@ async fn run_websocket_response_stream( .await; last_server_model = Some(model); } - if let Some(verifications) = model_verifications - && tx_event - .send(Ok(ResponseEvent::ModelVerifications(verifications))) - .await - .is_err() - { - return Err(ApiError::Stream( - "response event consumer dropped".to_string(), - )); - } - if let Some(metadata) = turn_moderation_metadata - && tx_event - .send(Ok(ResponseEvent::TurnModerationMetadata(metadata))) - .await - .is_err() - { - return Err(ApiError::Stream( - "response event consumer dropped".to_string(), - )); - } - if let Some(buffering) = safety_buffering - && tx_event - .send(Ok(ResponseEvent::SafetyBuffering(buffering))) - .await - .is_err() - { - return Err(ApiError::Stream( - "response event consumer dropped".to_string(), - )); + for metadata_event in metadata_events { + if tx_event.send(Ok(metadata_event)).await.is_err() { + return Err(ApiError::Stream( + "response event consumer dropped".to_string(), + )); + } } match event { Ok(Some(event)) => { diff --git a/codex-rs/codex-api/src/guardian_assessment.rs b/codex-rs/codex-api/src/guardian_assessment.rs new file mode 100644 index 0000000000..728d279767 --- /dev/null +++ b/codex-rs/codex-api/src/guardian_assessment.rs @@ -0,0 +1,35 @@ +//! Decodes action-bound Guardian metadata without granting permission to execute. +//! Missing or malformed assessments remain unavailable; consumers must validate the +//! response, completed action, and current authorization before using a low-risk result. + +use serde_json::Value; + +/// A serving-side assessment carried outside model-visible response items. +/// +/// Consumers must validate the response, completed action, and current authorization +/// before using a low-risk result. This assessment does not grant permission to execute. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GuardianAssessment { + pub response_id: String, + pub item_id: String, + /// False only permits considering the action for the Guardian fast path. + pub requires_review: bool, +} + +pub(crate) fn parse_guardian_assessment( + response_id: Option<&str>, + metadata: Option<&Value>, +) -> Option { + let response_id = response_id?; + let guardian = metadata?.get("guardian")?; + let item_id = guardian.get("item_id")?.as_str()?; + let requires_review = guardian.get("requires_review")?.as_bool()?; + if response_id.is_empty() || item_id.is_empty() { + return None; + } + Some(GuardianAssessment { + response_id: response_id.to_owned(), + item_id: item_id.to_owned(), + requires_review, + }) +} diff --git a/codex-rs/codex-api/src/lib.rs b/codex-rs/codex-api/src/lib.rs index 221010a44f..88d5568703 100644 --- a/codex-rs/codex-api/src/lib.rs +++ b/codex-rs/codex-api/src/lib.rs @@ -4,6 +4,7 @@ pub(crate) mod common; pub(crate) mod endpoint; pub(crate) mod error; pub(crate) mod files; +mod guardian_assessment; pub(crate) mod images; pub(crate) mod provider; pub(crate) mod rate_limits; @@ -85,6 +86,7 @@ pub use crate::error::ApiError; pub use crate::files::HostedFileUploadContext; pub use crate::files::OPENAI_FILE_UPLOAD_LIMIT_BYTES; pub use crate::files::upload_openai_file; +pub use crate::guardian_assessment::GuardianAssessment; pub use crate::images::ImageBackground; pub use crate::images::ImageData; pub use crate::images::ImageEditRequest; diff --git a/codex-rs/codex-api/src/sse/responses.rs b/codex-rs/codex-api/src/sse/responses.rs index 40478f4088..67f627e601 100644 --- a/codex-rs/codex-api/src/sse/responses.rs +++ b/codex-rs/codex-api/src/sse/responses.rs @@ -5,6 +5,7 @@ use crate::common::SafetyBuffering; use crate::common::SafetyBufferingTreatment; use crate::error::ApiError; use crate::error::parse_terminal_provider_error; +use crate::guardian_assessment::parse_guardian_assessment; use crate::rate_limits::parse_all_rate_limits; use crate::safety_buffering::treatment_from_headers; use crate::telemetry::SseTelemetry; @@ -164,6 +165,7 @@ pub struct ResponsesStreamEvent { pub(crate) headers: Option, metadata: Option, response: Option, + response_id: Option, error: Option, item: Option, item_id: Option, @@ -219,6 +221,33 @@ impl ResponsesStreamEvent { .and_then(header_turn_state_value_from_json) } + /// Collects independent typed notifications in delivery order. The transport + /// supplies safety buffering after applying its response-header treatment. + pub(crate) fn metadata_events( + &self, + safety_buffering: Option, + ) -> Vec { + let guardian = if self.kind() == "response.metadata" { + parse_guardian_assessment( + self.response_id.as_ref().and_then(Value::as_str), + self.metadata.as_ref(), + ) + } else { + None + }; + [ + self.model_verifications() + .map(ResponseEvent::ModelVerifications), + self.turn_moderation_metadata() + .map(ResponseEvent::TurnModerationMetadata), + safety_buffering.map(ResponseEvent::SafetyBuffering), + guardian.map(ResponseEvent::GuardianAssessment), + ] + .into_iter() + .flatten() + .collect() + } + pub(crate) fn model_verifications(&self) -> Option> { if self.kind() != "response.metadata" { return None; @@ -605,9 +634,8 @@ async fn process_sse_with_treatment( continue; } }; - let model_verifications = event.model_verifications(); - let turn_moderation_metadata = event.turn_moderation_metadata(); let safety_buffering = event.safety_buffering(&safety_buffering_treatment); + let metadata_events = event.metadata_events(safety_buffering); if let Some(model) = event.response_model() && last_server_model.as_deref() != Some(model.as_str()) @@ -621,29 +649,10 @@ async fn process_sse_with_treatment( } last_server_model = Some(model); } - if let Some(verifications) = model_verifications - && tx_event - .send(Ok(ResponseEvent::ModelVerifications(verifications))) - .await - .is_err() - { - return; - } - if let Some(metadata) = turn_moderation_metadata - && tx_event - .send(Ok(ResponseEvent::TurnModerationMetadata(metadata))) - .await - .is_err() - { - return; - } - if let Some(buffering) = safety_buffering - && tx_event - .send(Ok(ResponseEvent::SafetyBuffering(buffering))) - .await - .is_err() - { - return; + for metadata_event in metadata_events { + if tx_event.send(Ok(metadata_event)).await.is_err() { + return; + } } match process_responses_event(event) { diff --git a/codex-rs/codex-api/tests/sse_end_to_end.rs b/codex-rs/codex-api/tests/sse_end_to_end.rs index bf72eda9c0..0a7f14801d 100644 --- a/codex-rs/codex-api/tests/sse_end_to_end.rs +++ b/codex-rs/codex-api/tests/sse_end_to_end.rs @@ -6,6 +6,7 @@ use anyhow::Result; use bytes::Bytes; use codex_api::AuthProvider; use codex_api::Compression; +use codex_api::GuardianAssessment; use codex_api::Provider; use codex_api::ResponseEvent; use codex_api::ResponsesClient; @@ -15,11 +16,13 @@ use codex_client::Response; use codex_client::StreamResponse; use codex_client::TransportError; use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::ModelVerification; use futures::StreamExt; use http::HeaderMap; use http::StatusCode; use pretty_assertions::assert_eq; use serde_json::Value; +use serde_json::json; #[derive(Clone)] struct FixtureSseTransport { @@ -199,3 +202,268 @@ async fn responses_stream_parses_items_and_completed_end_to_end() -> Result<()> Ok(()) } + +#[tokio::test] +async fn responses_stream_delivers_guardian_assessments_before_tool_completion() -> Result<()> { + let body = build_responses_body(vec![ + json!({ + "type": "response.metadata", + "response_id": "resp_1", + "metadata": {"guardian": { + "item_id": "fc_1", "requires_review": false, "future_field": "ignored" + }} + }), + json!({ + "type": "response.output_item.done", + "item": { + "type": "function_call", "id": "fc_1", "call_id": "call_1", + "name": "example", "arguments": "{}" + } + }), + json!({ + "type": "response.metadata", + "response_id": "resp_1", + "metadata": {"guardian": {"item_id": "fc_2", "requires_review": true}} + }), + json!({ + "type": "response.output_item.done", + "item": { + "type": "function_call", "id": "fc_2", "call_id": "call_2", + "name": "example", "arguments": "{}" + } + }), + json!({"type": "response.completed", "response": {"id": "resp_1"}}), + ]); + let client = ResponsesClient::new( + FixtureSseTransport::new(body), + provider("openai"), + Arc::new(NoAuth), + ); + let mut stream = client + .stream( + json!({}), + HeaderMap::new(), + Compression::None, + /*turn_state*/ None, + ) + .await?; + let mut assessments = Vec::new(); + let mut call_ids = Vec::new(); + let mut completed = false; + while let Some(event) = stream.next().await { + match event? { + ResponseEvent::GuardianAssessment(assessment) => { + assessments.push((assessment, call_ids.len())); + } + ResponseEvent::OutputItemDone(ResponseItem::FunctionCall { call_id, .. }) => { + call_ids.push(call_id); + } + ResponseEvent::Completed { .. } => completed = true, + ResponseEvent::RateLimits(_) => {} + other => panic!("unexpected event: {other:?}"), + } + } + assert_eq!( + assessments, + vec![ + ( + GuardianAssessment { + response_id: "resp_1".into(), + item_id: "fc_1".into(), + requires_review: false, + }, + 0 + ), + ( + GuardianAssessment { + response_id: "resp_1".into(), + item_id: "fc_2".into(), + requires_review: true, + }, + 1 + ), + ] + ); + assert_eq!(call_ids, vec!["call_1", "call_2"]); + assert!(completed); + Ok(()) +} + +#[tokio::test] +async fn responses_stream_ignores_unusable_guardian_metadata() -> Result<()> { + let mut events = Vec::new(); + for (response_id, guardian) in [ + (json!("resp_1"), json!(null)), + (json!("resp_1"), json!({"item_id": "fc_1"})), + ( + json!("resp_1"), + json!({"item_id": "fc_1", "requires_review": null}), + ), + ( + json!("resp_1"), + json!({"item_id": "fc_1", "requires_review": "false"}), + ), + (json!("resp_1"), json!({"requires_review": false})), + ( + json!("resp_1"), + json!({"item_id": "", "requires_review": false}), + ), + ( + json!("resp_1"), + json!({"item_id": 1, "requires_review": false}), + ), + ( + json!(""), + json!({"item_id": "fc_1", "requires_review": false}), + ), + ( + json!(null), + json!({"item_id": "fc_1", "requires_review": false}), + ), + ( + json!(1), + json!({"item_id": "fc_1", "requires_review": false}), + ), + ] { + events.push(json!({ + "type": "response.metadata", "response_id": response_id, + "metadata": {"guardian": guardian} + })); + } + events.push(json!({ + "type": "response.completed", "response": {"id": "resp_1"}, + "response_id": 1, + "metadata": {"guardian": {"item_id": "fc_1", "requires_review": false}} + })); + let client = ResponsesClient::new( + FixtureSseTransport::new(build_responses_body(events)), + provider("openai"), + Arc::new(NoAuth), + ); + let mut stream = client + .stream( + json!({}), + HeaderMap::new(), + Compression::None, + /*turn_state*/ None, + ) + .await?; + let mut response_ids = Vec::new(); + while let Some(event) = stream.next().await { + match event? { + ResponseEvent::Completed { response_id, .. } => response_ids.push(response_id), + ResponseEvent::RateLimits(_) => {} + other => panic!("unexpected event: {other:?}"), + } + } + assert_eq!(response_ids, vec!["resp_1"]); + Ok(()) +} + +#[tokio::test] +async fn responses_stream_dispatches_mixed_metadata_independently() -> Result<()> { + let body = build_responses_body(vec![ + json!({ + "type": "response.metadata", "response_id": "resp_1", + "metadata": { + "openai_verification_recommendation": ["trusted_access_for_cyber"], + "openai_chatgpt_moderation_metadata": {"presentation": "inline"}, + "type": "safety_buffering", "use_cases": ["cyber"], + "reasons": ["user_risk"], "retry_model": "gpt-fast", + "guardian": {"item_id": "fc_1", "requires_review": false}, + "future_metadata": {"ignored": true} + } + }), + json!({ + "type": "response.metadata", "response_id": "resp_1", + "metadata": { + "openai_verification_recommendation": ["trusted_access_for_cyber"], + "openai_chatgpt_moderation_metadata": {"presentation": "inline"}, + "type": "safety_buffering", "use_cases": ["cyber"], + "reasons": ["user_risk"], "retry_model": "gpt-fast", + "guardian": {"item_id": "fc_2", "requires_review": "false"} + } + }), + json!({ + "type": "response.metadata", "response_id": "resp_1", + "metadata": { + "openai_verification_recommendation": false, + "guardian": {"item_id": "fc_3", "requires_review": true} + } + }), + json!({"type": "response.completed", "response": {"id": "resp_1"}}), + ]); + let client = ResponsesClient::new( + FixtureSseTransport::new(body), + provider("openai"), + Arc::new(NoAuth), + ); + let mut stream = client + .stream( + json!({}), + HeaderMap::new(), + Compression::None, + /*turn_state*/ None, + ) + .await?; + let mut events = Vec::new(); + while let Some(event) = stream.next().await { + let event = event?; + if !matches!(event, ResponseEvent::RateLimits(_)) { + events.push(event); + } + } + let [ + ResponseEvent::ModelVerifications(verifications), + ResponseEvent::TurnModerationMetadata(moderation), + ResponseEvent::SafetyBuffering(buffering), + ResponseEvent::GuardianAssessment(low_risk), + ResponseEvent::ModelVerifications(other_verifications), + ResponseEvent::TurnModerationMetadata(other_moderation), + ResponseEvent::SafetyBuffering(other_buffering), + ResponseEvent::GuardianAssessment(review_required), + ResponseEvent::Completed { response_id, .. }, + ] = events.as_slice() + else { + panic!("unexpected metadata delivery: {events:?}"); + }; + assert_eq!( + verifications, + &vec![ModelVerification::TrustedAccessForCyber] + ); + assert_eq!(other_verifications, verifications); + assert_eq!(moderation.metadata, json!({"presentation": "inline"})); + assert_eq!(other_moderation, moderation); + assert_eq!( + ( + &buffering.use_cases, + &buffering.reasons, + buffering.show_buffering_ui, + buffering.faster_model.as_deref(), + ), + ( + &vec!["cyber".to_string()], + &vec!["user_risk".to_string()], + true, + Some("gpt-fast"), + ) + ); + assert_eq!(other_buffering, buffering); + assert_eq!( + (low_risk, review_required), + ( + &GuardianAssessment { + response_id: "resp_1".into(), + item_id: "fc_1".into(), + requires_review: false, + }, + &GuardianAssessment { + response_id: "resp_1".into(), + item_id: "fc_3".into(), + requires_review: true, + }, + ) + ); + assert_eq!(response_id, "resp_1"); + Ok(()) +} diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 7e40cb4a1a..b56e7ca2aa 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -3197,6 +3197,10 @@ async fn try_run_sampling_request( sess.emit_turn_moderation_metadata(&turn_context, metadata) .await; } + ResponseEvent::GuardianAssessment(_) => { + // Request-scoped extensions may consume this evidence. It does not + // authorize tools here and must not enter conversation history. + } ResponseEvent::SafetyBuffering(buffering) => { sess.send_event( &turn_context, diff --git a/codex-rs/core/src/turn_timing.rs b/codex-rs/core/src/turn_timing.rs index adac4a295a..de45b7403d 100644 --- a/codex-rs/core/src/turn_timing.rs +++ b/codex-rs/core/src/turn_timing.rs @@ -494,6 +494,7 @@ fn response_event_records_turn_ttft(event: &ResponseEvent) -> bool { | ResponseEvent::ServerModel(_) | ResponseEvent::ModelVerifications(_) | ResponseEvent::TurnModerationMetadata(_) + | ResponseEvent::GuardianAssessment(_) | ResponseEvent::SafetyBuffering(_) | ResponseEvent::ServerReasoningIncluded(_) | ResponseEvent::ToolCallInputDelta { .. } diff --git a/codex-rs/core/tests/suite/model_request.rs b/codex-rs/core/tests/suite/model_request.rs index 0dd5e3d7e7..3163db9886 100644 --- a/codex-rs/core/tests/suite/model_request.rs +++ b/codex-rs/core/tests/suite/model_request.rs @@ -2,6 +2,7 @@ use anyhow::Result; use codex_core::TurnInputRequest; use codex_extension_api::ExtensionRegistryBuilder; +use codex_extension_api::GuardianAssessment; use codex_extension_api::ModelRequestContributor; use codex_extension_api::ModelRequestInput; use codex_extension_api::ModelRequestKind; @@ -20,6 +21,7 @@ use core_test_support::test_codex::test_codex; use core_test_support::wait_for_event; use futures::StreamExt; use pretty_assertions::assert_eq; +use serde_json::json; use std::sync::Arc; use tokio::sync::Semaphore; use tokio::time::Duration; @@ -31,6 +33,8 @@ struct Gates { output_waiting: Semaphore, cancelled: Semaphore, output_items: std::sync::atomic::AtomicUsize, + guardian_assessments: std::sync::Mutex>, + metadata_event_types: std::sync::Mutex>, } #[derive(Debug)] struct Contributor(Arc); @@ -60,6 +64,27 @@ impl ModelResponseInterceptor for Permit { let mut events = Vec::new(); let mut output_items = 0; while let Some(event) = stream.next().await { + let metadata_event_type = match &event { + Ok(ResponseEvent::ModelVerifications(_)) => Some("verification"), + Ok(ResponseEvent::TurnModerationMetadata(_)) => Some("moderation"), + Ok(ResponseEvent::SafetyBuffering(_)) => Some("buffering"), + Ok(ResponseEvent::GuardianAssessment(_)) => Some("guardian"), + _ => None, + }; + if let Some(metadata_event_type) = metadata_event_type { + self.gates + .metadata_event_types + .lock() + .expect("metadata events mutex is not poisoned") + .push(metadata_event_type); + } + if let Ok(ResponseEvent::GuardianAssessment(assessment)) = &event { + self.gates + .guardian_assessments + .lock() + .expect("guardian assessments mutex is not poisoned") + .push((assessment.clone(), output_items)); + } if matches!(&event, Ok(ResponseEvent::OutputItemDone(_))) { output_items += 1; } @@ -99,6 +124,10 @@ async fn model_request_gates_output_and_cancellation() -> Result<()> { let server = responses::start_mock_server().await; let response = sse(vec![ ev_response_created("r1"), + json!({ + "type": "response.metadata", "response_id": "r1", + "metadata": {"guardian": {"item_id": "m1", "requires_review": false}} + }), ev_assistant_message("m1", "first"), ev_assistant_message("m2", "done"), ev_completed("r1"), @@ -109,6 +138,8 @@ async fn model_request_gates_output_and_cancellation() -> Result<()> { output_waiting: Semaphore::new(0), cancelled: Semaphore::new(0), output_items: std::sync::atomic::AtomicUsize::new(0), + guardian_assessments: std::sync::Mutex::new(Vec::new()), + metadata_event_types: std::sync::Mutex::new(Vec::new()), }); let mut registry = ExtensionRegistryBuilder::new(); registry.model_request_contributor(Arc::new(Contributor(gates.clone()))); @@ -124,6 +155,17 @@ async fn model_request_gates_output_and_cancellation() -> Result<()> { .await?? .forget(); assert_eq!(model.requests().len(), 1); + assert_eq!( + *gates.guardian_assessments.lock().unwrap(), + vec![( + GuardianAssessment { + response_id: "r1".into(), + item_id: "m1".into(), + requires_review: false, + }, + 0 + )] + ); assert_eq!( gates.output_items.load(std::sync::atomic::Ordering::SeqCst), 2 @@ -162,6 +204,12 @@ async fn model_request_gates_output_and_cancellation() -> Result<()> { .await?? .forget(); assert_eq!(model.requests().len(), 2); + assert!( + !model.requests()[1] + .body_json() + .to_string() + .contains("requires_review") + ); Ok(()) } @@ -171,6 +219,26 @@ async fn model_request_gates_websocket_output_but_not_warmup() -> Result<()> { vec![ev_response_created("warm"), ev_completed("warm")], vec![ ev_response_created("response"), + json!({ + "type": "response.metadata", "response_id": "response", + "metadata": { + "guardian": {"item_id": "message", "requires_review": true}, + "openai_verification_recommendation": ["trusted_access_for_cyber"], + "openai_chatgpt_moderation_metadata": {"presentation": "inline"}, + "type": "safety_buffering", "use_cases": ["cyber"], + "reasons": ["user_risk"], "future_metadata": true + } + }), + json!({ + "type": "response.metadata", "response_id": "response", + "metadata": { + "guardian": {"item_id": "other", "requires_review": "false"}, + "openai_verification_recommendation": ["trusted_access_for_cyber"], + "openai_chatgpt_moderation_metadata": {"presentation": "inline"}, + "type": "safety_buffering", "use_cases": ["cyber"], + "reasons": ["user_risk"] + } + }), ev_assistant_message("message", "done"), ev_completed("response"), ], @@ -181,6 +249,8 @@ async fn model_request_gates_websocket_output_but_not_warmup() -> Result<()> { output_waiting: Semaphore::new(0), cancelled: Semaphore::new(0), output_items: std::sync::atomic::AtomicUsize::new(0), + guardian_assessments: std::sync::Mutex::new(Vec::new()), + metadata_event_types: std::sync::Mutex::new(Vec::new()), }); let mut registry = ExtensionRegistryBuilder::new(); registry.model_request_contributor(Arc::new(Contributor(gates.clone()))); @@ -198,6 +268,32 @@ async fn model_request_gates_websocket_output_but_not_warmup() -> Result<()> { .await?? .forget(); let requests = server.single_connection(); + assert_eq!( + *gates + .metadata_event_types + .lock() + .expect("metadata events mutex is not poisoned"), + vec![ + "verification", + "moderation", + "buffering", + "guardian", + "verification", + "moderation", + "buffering", + ] + ); + assert_eq!( + *gates.guardian_assessments.lock().unwrap(), + vec![( + GuardianAssessment { + response_id: "response".into(), + item_id: "message".into(), + requires_review: true, + }, + 0 + )] + ); assert_eq!(requests.len(), 2); assert_eq!(requests[0].body_json()["generate"], false); assert_eq!( diff --git a/codex-rs/ext/extension-api/src/lib.rs b/codex-rs/ext/extension-api/src/lib.rs index 45fdd9f155..147b52ff1b 100644 --- a/codex-rs/ext/extension-api/src/lib.rs +++ b/codex-rs/ext/extension-api/src/lib.rs @@ -126,6 +126,7 @@ pub use user_instructions::UserInstructionsProvider; pub use contributors::ToolDispatchInput; mod model_request; +pub use model_request::GuardianAssessment; pub use model_request::ModelRequestContributor; pub use model_request::ModelRequestInput; pub use model_request::ModelRequestKind; diff --git a/codex-rs/ext/extension-api/src/model_request.rs b/codex-rs/ext/extension-api/src/model_request.rs index 67803d28d3..0f477aa99e 100644 --- a/codex-rs/ext/extension-api/src/model_request.rs +++ b/codex-rs/ext/extension-api/src/model_request.rs @@ -1,5 +1,6 @@ //! Request-scoped interception of model response streams. pub use codex_api::ApiError as ModelResponseError; +pub use codex_api::GuardianAssessment; pub use codex_api::ResponseEvent; use futures::stream::BoxStream; use std::collections::HashMap; diff --git a/codex-rs/otel/src/events/session_telemetry.rs b/codex-rs/otel/src/events/session_telemetry.rs index 53dbe31737..ced543ac75 100644 --- a/codex-rs/otel/src/events/session_telemetry.rs +++ b/codex-rs/otel/src/events/session_telemetry.rs @@ -1409,6 +1409,7 @@ impl SessionTelemetry { ResponseEvent::ServerModel(_) => "server_model".into(), ResponseEvent::ModelVerifications(_) => "model_verifications".into(), ResponseEvent::TurnModerationMetadata(_) => "turn_moderation_metadata".into(), + ResponseEvent::GuardianAssessment(_) => "guardian_assessment".into(), ResponseEvent::SafetyBuffering(_) => "safety_buffering".into(), ResponseEvent::ServerReasoningIncluded(_) => "server_reasoning_included".into(), ResponseEvent::RateLimits(_) => "rate_limits".into(),