[codex] Forward turn moderation metadata through app-server (#25710)

## Why
First-party backends can supply turn-scoped moderation metadata that
app-server clients need for client-side presentation. Exposing this as
an experimental typed notification lets opted-in clients consume it
without interpreting raw Responses API events.

## What changed
- forward `response.metadata.openai_chatgpt_moderation_metadata` from
Responses API SSE and WebSocket streams as turn-scoped moderation
metadata
- emit the experimental app-server v2 `turn/moderationMetadata`
notification with `{ threadId, turnId, metadata }`
- add app-server integration coverage for the typed moderation metadata
notification

## Testing
- `just test -p codex-core
build_ws_client_metadata_includes_window_lineage_and_turn_metadata`
- `just test -p codex-core` (fails locally: 46 failures and 1 timeout,
primarily missing `test_stdio_server` and shell snapshot timeouts)
- `just test -p codex-app-server-protocol`
- `just test -p codex-app-server
turn_moderation_metadata_emits_typed_notification_v2`
- `just test -p codex-app-server` (fails locally: 792 passed, 10 failed,
and 5 timed out; failures are in existing environment-sensitive tests,
primarily because nested macOS `sandbox-exec` is not permitted)
- `just write-app-server-schema --experimental --schema-root
/tmp/codex-app-server-schema-experimental`
This commit is contained in:
carlc-oai
2026-06-05 02:41:06 -07:00
committed by GitHub
Unverified
parent 6dc28ba6e0
commit 55aa071b17
26 changed files with 395 additions and 2 deletions
@@ -75,6 +75,7 @@ use codex_app_server_protocol::TurnDiffUpdatedNotification;
use codex_app_server_protocol::TurnError;
use codex_app_server_protocol::TurnInterruptResponse;
use codex_app_server_protocol::TurnItemsView;
use codex_app_server_protocol::TurnModerationMetadataNotification;
use codex_app_server_protocol::TurnPlanStep;
use codex_app_server_protocol::TurnPlanUpdatedNotification;
use codex_app_server_protocol::TurnStartedNotification;
@@ -337,6 +338,16 @@ pub(crate) async fn apply_bespoke_event_handling(
.send_server_notification(ServerNotification::ModelVerification(notification))
.await;
}
EventMsg::TurnModerationMetadata(event) => {
let notification = TurnModerationMetadataNotification {
thread_id: conversation_id.to_string(),
turn_id: event_turn_id.clone(),
metadata: event.metadata,
};
outgoing
.send_server_notification(ServerNotification::TurnModerationMetadata(notification))
.await;
}
EventMsg::RealtimeConversationStarted(event) => {
let notification = ThreadRealtimeStartedNotification {
thread_id: conversation_id.to_string(),
@@ -725,6 +725,7 @@ mod tests {
use codex_app_server_protocol::RateLimitWindow;
use codex_app_server_protocol::ServerResponse;
use codex_app_server_protocol::ToolRequestUserInputParams;
use codex_app_server_protocol::TurnModerationMetadataNotification;
use codex_protocol::ThreadId;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -951,6 +952,31 @@ mod tests {
);
}
#[test]
fn verify_turn_moderation_metadata_notification_serialization() {
let notification =
ServerNotification::TurnModerationMetadata(TurnModerationMetadataNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
metadata: json!({"presentation": "inline"}),
});
let jsonrpc_notification = OutgoingMessage::AppServerNotification(notification);
assert_eq!(
json!({
"method": "turn/moderationMetadata",
"params": {
"threadId": "thread-1",
"turnId": "turn-1",
"metadata": {"presentation": "inline"},
},
}),
serde_json::to_value(jsonrpc_notification)
.expect("ensure the notification serializes correctly"),
"ensure the notification serializes correctly"
);
}
#[test]
fn server_request_response_from_result_decodes_typed_response() {
let request = ServerRequest::CommandExecutionRequestApproval {