Skip to content

Add safety check notification and error handling#19055

Merged
etraut-openai merged 19 commits intomainfrom
etraut/safety-notification
Apr 23, 2026
Merged

Add safety check notification and error handling#19055
etraut-openai merged 19 commits intomainfrom
etraut/safety-notification

Conversation

@etraut-openai
Copy link
Copy Markdown
Collaborator

Adds a new app-server notification that fires when a user account has been flagged for potential safety reasons.

@etraut-openai etraut-openai requested a review from a team as a code owner April 22, 2026 23:27
Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3ef5031e9c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/codex-api/src/sse/responses.rs Outdated
@etraut-openai etraut-openai changed the title Add model verification notification to app-server Add safety check notification and error handling Apr 23, 2026
Comment thread codex-rs/codex-api/src/sse/responses.rs Outdated
#[serde(rename = "type")]
pub(crate) kind: String,
#[serde(rename = "openai_verification_recommendation")]
openai_model_verification: Option<Value>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

considering we fire an event already, why do we also need an extra field per ResponsesStreamEvent?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, good point. I removed the field from ResponsesStreamEvent.

Comment thread codex-rs/codex-api/src/api_bridge.rs Outdated
message: Option<String>,
}

fn cyber_policy_message_from_body(body: &str) -> Option<String> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have a helper for generic error shape parsing somwehere?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find any that would work for this error shape. Let me know if you think I missed something.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we parse inline for other errors in this file.

Comment thread codex-rs/codex-api/src/api_bridge.rs
Comment thread codex-rs/core/src/session/mod.rs Outdated
)
.await;

if let Some(warning_message) = warning_message {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we emit both Warning and ModelVerification? We should pick one

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[codex] The two events serve different consumers: ModelVerification is the typed, app-server-visible signal that GUI clients can localize or render however they want, while Warning is the existing user-visible path the TUI renders today. The warning is intentionally not recorded into model-visible history, so it is only a client/UI notice. If you prefer, we can make the TUI render from the structured event and drop the paired warning, but that would mean adding client-side rendering behavior instead of reusing the existing warning path.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TUI/app can render whatever message it needs based on the event

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. I changed this so core only emits ModelVerification and then changed the TUI to handle it.

This dual-emit pattern exists all over the place currently. We should be able to remove a bunch of this redundancy once I do a few more turns of the crank on cleaning up the TUI.

Comment thread codex-rs/core/src/session/turn.rs Outdated
&explicitly_enabled_connectors,
skills_outcome,
&mut server_model_warning_emitted_for_turn,
&mut model_verification_emitted_for_turn,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there something we can do to avoid carrying this state through the agent loop only to deduplicate warnings?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. This was based on the way that the existing server_model_warning_emitted_for_turn was handled, but I agree that there are cleaner ways to handle this. I've removed the new model_verification_emitted_for_turn flag and the existing flag.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

continue;
}
};
let model_verifications = model_verifications_from_event_value(&event_value);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add metadata to ResponsesStreamEvent and conition on event.kind() == "response.metadata" ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread codex-rs/codex-api/src/sse/responses.rs Outdated
.map(ToString::to_string);
let model_verifications = stream_response
.headers
.get(OPENAI_MODEL_VERIFICATION_HEADER)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we care about the header, we always stream.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, good point. That simplifies things. Done.

Comment thread codex-rs/codex-api/src/sse/responses.rs Outdated
})
}

pub(crate) fn model_verifications_from_event_value(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method is very permissive

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Tightened it up.

Comment thread codex-rs/codex-api/src/sse/responses.rs Outdated
}
}

fn json_value_as_model_verifications(value: &Value) -> Vec<ModelVerification> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in general this parsing can be simplified I think considering we know the exact format.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment thread codex-rs/codex-api/src/sse/responses.rs Outdated
trace!("SSE event: {}", &sse.data);

let event: ResponsesStreamEvent = match serde_json::from_str(&sse.data) {
let event_value: Value = match serde_json::from_str(&sse.data) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably should reuse ResponsesStreamEvent below.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@etraut-openai etraut-openai force-pushed the etraut/safety-notification branch from 1103252 to 5b565f9 Compare April 23, 2026 04:38
Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

let server_model = stream_response
.headers
.get(OPENAI_MODEL_HEADER)
.and_then(|v| v.to_str().ok())
.map(ToString::to_string);
let reasoning_included = stream_response
.headers
.get(X_REASONING_INCLUDED_HEADER)
.is_some();

P1 Badge Parse verification headers in spawn_response_stream

spawn_response_stream extracts OpenAI-Model and other response headers but never reads OpenAI-Verification-Recommendation. On HTTP/SSE transport (including websocket fallback), that header can be the only verification signal, so ResponseEvent::ModelVerifications is not emitted and downstream model/verification notifications are silently skipped.


let reasoning_included = response.headers().contains_key(X_REASONING_INCLUDED_HEADER);
let models_etag = response
.headers()
.get(X_MODELS_ETAG_HEADER)
.and_then(|value| value.to_str().ok())
.map(ToString::to_string);
let server_model = response
.headers()
.get(OPENAI_MODEL_HEADER)
.and_then(|value| value.to_str().ok())
.map(ToString::to_string);

P1 Badge Emit verification from websocket upgrade headers

connect_websocket() caches X-Reasoning-Included, X-Models-Etag, and OpenAI-Model from the handshake response, but never reads OpenAI-Verification-Recommendation. stream_request() only emits cached handshake-derived signals, so if verification is supplied only via upgrade headers, ResponseEvent::ModelVerifications is never produced.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread codex-rs/codex-api/src/sse/responses.rs
Copy link
Copy Markdown
Contributor

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5873bd93c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +206 to +214
pub(crate) fn model_verifications(&self) -> Option<Vec<ModelVerification>> {
if self.kind() != "response.metadata" {
return None;
}

self.metadata
.as_ref()
.and_then(|metadata| metadata.get("openai_verification_recommendation"))
.and_then(model_verifications_from_json_value)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse verification recommendations from header metadata

model_verifications() only reads metadata.openai_verification_recommendation and requires type == "response.metadata". It never inspects response.headers/top-level headers (unlike response_model()), so header-delivered verification signals are dropped and ResponseEvent::ModelVerifications is never emitted. Fresh evidence: spawn_response_stream_ignores_model_verification_header codifies this loss path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional for streaming Responses traffic. The agreed carrier for HTTP streaming and websocket is the response.metadata event; OpenAI-Verification-Recommendation headers are not used on these paths. The test is codifying that streaming headers are ignored so we don’t accidentally support the wrong carrier.

@etraut-openai etraut-openai merged commit bbff4ee into main Apr 23, 2026
25 checks passed
@etraut-openai etraut-openai deleted the etraut/safety-notification branch April 23, 2026 05:24
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants