mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] reject remote images at app-server ingress (#29419)
## Stack Stacked on #29417. Review and land that PR first. ## Summary - reject HTTP(S) image URLs in the handlers for `turn/start` and `turn/steer` - validate `thread/inject_items` after its existing JSON-to-`ResponseItem` conversion, so each item is deserialized once - turn invalid dynamic-tool image responses into the existing unsuccessful text fallback; the model receives the validation message as the function output - leave `thread/resume.history` compatible with legacy history; #29417 replaces remote images before model input - continue accepting inline data URLs and `localImage` inputs - keep this policy in app-server; this PR does not add a shared protocol API or change core image preparation ## Test plan - `just test -p codex-app-server -E 'test(/request_handlers_reject_remote_image_urls|dynamic_tool_remote_image_response_becomes_model_visible_error|dynamic_tool_call_round_trip_sends_content_items_to_model|turn_start_tracks_turn_event_analytics|standalone_image_edit_uses_recent_pathless_image/)'` (5 passed) - `just fix -p codex-app-server` - `just fmt`
This commit is contained in:
@@ -8,6 +8,8 @@ use std::sync::Arc;
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::error;
|
||||
|
||||
use crate::image_url::REMOTE_IMAGE_URL_ERROR;
|
||||
use crate::image_url::is_remote_image_url;
|
||||
use crate::outgoing_message::ClientRequestResult;
|
||||
use crate::server_request_error::is_turn_transition_server_request_error;
|
||||
|
||||
@@ -54,6 +56,21 @@ pub(crate) async fn on_call_response(
|
||||
|
||||
fn decode_response(value: serde_json::Value) -> (DynamicToolCallResponse, Option<String>) {
|
||||
match serde_json::from_value::<DynamicToolCallResponse>(value) {
|
||||
Ok(response)
|
||||
if response.content_items.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
DynamicToolCallOutputContentItem::InputImage { image_url }
|
||||
if is_remote_image_url(image_url)
|
||||
)
|
||||
}) =>
|
||||
{
|
||||
error!(
|
||||
message = REMOTE_IMAGE_URL_ERROR,
|
||||
"dynamic tool response was invalid"
|
||||
);
|
||||
fallback_response(REMOTE_IMAGE_URL_ERROR)
|
||||
}
|
||||
Ok(response) => (response, None),
|
||||
Err(err) => {
|
||||
error!("failed to deserialize DynamicToolCallResponse: {err}");
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
pub(crate) const REMOTE_IMAGE_URL_ERROR: &str =
|
||||
"remote image URLs are not supported; use an inline data URL instead";
|
||||
|
||||
pub(crate) fn is_remote_image_url(image_url: &str) -> bool {
|
||||
image_url.split_once(':').is_some_and(|(scheme, _)| {
|
||||
scheme.eq_ignore_ascii_case("http") || scheme.eq_ignore_ascii_case("https")
|
||||
})
|
||||
}
|
||||
@@ -96,6 +96,7 @@ mod extensions;
|
||||
mod filters;
|
||||
mod fs_watch;
|
||||
mod fuzzy_file_search;
|
||||
mod image_url;
|
||||
pub mod in_process;
|
||||
mod mcp_refresh;
|
||||
mod message_processor;
|
||||
|
||||
@@ -1,14 +1,70 @@
|
||||
use super::*;
|
||||
use codex_protocol::config_types::MultiAgentMode;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
|
||||
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
|
||||
use codex_protocol::protocol::MultiAgentVersion;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
|
||||
use crate::image_url::REMOTE_IMAGE_URL_ERROR;
|
||||
use crate::image_url::is_remote_image_url;
|
||||
|
||||
const DIRECT_INPUT_TO_MULTI_AGENT_V2_SUBAGENT_ERROR: &str =
|
||||
"direct app-server input is not allowed for multi-agent v2 sub-agents";
|
||||
|
||||
fn validate_user_input_image_urls(input: &[V2UserInput]) -> Result<(), JSONRPCErrorError> {
|
||||
if input.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
V2UserInput::Image { url, .. } if is_remote_image_url(url)
|
||||
)
|
||||
}) {
|
||||
return Err(invalid_request(REMOTE_IMAGE_URL_ERROR));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_response_item_image_urls(items: &[ResponseItem]) -> Result<(), JSONRPCErrorError> {
|
||||
if items.iter().any(|item| match item {
|
||||
ResponseItem::Message { content, .. } => content.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
ContentItem::InputImage { image_url, .. } if is_remote_image_url(image_url)
|
||||
)
|
||||
}),
|
||||
ResponseItem::FunctionCallOutput { output, .. }
|
||||
| ResponseItem::CustomToolCallOutput { output, .. } => {
|
||||
output.content_items().is_some_and(|content| {
|
||||
content.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
FunctionCallOutputContentItem::InputImage { image_url, .. }
|
||||
if is_remote_image_url(image_url)
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
ResponseItem::Reasoning { .. }
|
||||
| ResponseItem::AgentMessage { .. }
|
||||
| ResponseItem::LocalShellCall { .. }
|
||||
| ResponseItem::FunctionCall { .. }
|
||||
| ResponseItem::ToolSearchCall { .. }
|
||||
| ResponseItem::CustomToolCall { .. }
|
||||
| ResponseItem::ToolSearchOutput { .. }
|
||||
| ResponseItem::WebSearchCall { .. }
|
||||
| ResponseItem::ImageGenerationCall { .. }
|
||||
| ResponseItem::Compaction { .. }
|
||||
| ResponseItem::CompactionTrigger { .. }
|
||||
| ResponseItem::ContextCompaction { .. }
|
||||
| ResponseItem::Other => false,
|
||||
}) {
|
||||
return Err(invalid_request(REMOTE_IMAGE_URL_ERROR));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct TurnRequestProcessor {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
@@ -105,6 +161,7 @@ impl TurnRequestProcessor {
|
||||
app_server_client_version: Option<String>,
|
||||
supports_openai_form_elicitation: bool,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
validate_user_input_image_urls(¶ms.input)?;
|
||||
self.turn_start_inner(
|
||||
request_id,
|
||||
params,
|
||||
@@ -140,6 +197,7 @@ impl TurnRequestProcessor {
|
||||
request_id: &ConnectionRequestId,
|
||||
params: TurnSteerParams,
|
||||
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
|
||||
validate_user_input_image_urls(¶ms.input)?;
|
||||
self.turn_steer_inner(request_id, params)
|
||||
.await
|
||||
.map(|response| Some(response.into()))
|
||||
@@ -764,6 +822,7 @@ impl TurnRequestProcessor {
|
||||
})
|
||||
.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(invalid_request)?;
|
||||
validate_response_item_image_urls(&items)?;
|
||||
|
||||
thread
|
||||
.inject_response_items(items)
|
||||
|
||||
Reference in New Issue
Block a user