fix(codex): enforce Copilot replay policy at dispatch

Reapply the endpoint-aware preflight after request and execution
middleware so no override can reintroduce a connection-scoped ID.
This commit is contained in:
kshitijk4poor 2026-07-11 11:46:36 +05:30 committed by kshitij
parent 22d5a35c16
commit bce17bf6a2
5 changed files with 87 additions and 6 deletions

View file

@ -600,7 +600,11 @@ def _chat_messages_to_responses_input(
# Input preflight / validation
# ---------------------------------------------------------------------------
def _preflight_codex_input_items(raw_items: Any) -> List[Dict[str, Any]]:
def _preflight_codex_input_items(
raw_items: Any,
*,
is_github_responses: bool = False,
) -> List[Dict[str, Any]]:
if not isinstance(raw_items, list):
raise ValueError("Codex Responses input must be a list of input items.")
@ -741,7 +745,11 @@ def _preflight_codex_input_items(raw_items: Any) -> List[Dict[str, Any]]:
"content": normalized_content,
}
item_id = item.get("id")
if isinstance(item_id, str) and item_id.strip():
if (
not is_github_responses
and isinstance(item_id, str)
and item_id.strip()
):
stripped_id = item_id.strip()
if len(stripped_id) <= _MAX_RESPONSES_ITEM_ID_LENGTH:
normalized_item["id"] = stripped_id
@ -816,6 +824,7 @@ def _preflight_codex_api_kwargs(
api_kwargs: Any,
*,
allow_stream: bool = False,
is_github_responses: bool = False,
) -> Dict[str, Any]:
if not isinstance(api_kwargs, dict):
raise ValueError("Codex Responses request must be a dict.")
@ -837,7 +846,10 @@ def _preflight_codex_api_kwargs(
instructions = str(instructions)
instructions = instructions.strip() or DEFAULT_AGENT_IDENTITY
normalized_input = _preflight_codex_input_items(api_kwargs.get("input"))
normalized_input = _preflight_codex_input_items(
api_kwargs.get("input"),
is_github_responses=is_github_responses,
)
tools = api_kwargs.get("tools")
normalized_tools = None

View file

@ -1167,7 +1167,11 @@ def run_conversation(
if agent._force_ascii_payload:
_sanitize_structure_non_ascii(api_kwargs)
if agent.api_mode == "codex_responses":
api_kwargs = agent._get_transport().preflight_kwargs(api_kwargs, allow_stream=False)
api_kwargs = agent._get_transport().preflight_kwargs(
api_kwargs,
allow_stream=False,
is_github_responses=agent._is_copilot_url(),
)
# Copilot x-initiator: the first API call of a user turn is
# marked "user" so Copilot bills a premium request; tool-loop
# follow-ups keep the default "agent" header (#3040).
@ -1314,6 +1318,12 @@ def run_conversation(
_use_streaming = False
def _perform_api_call(next_api_kwargs):
if agent.api_mode == "codex_responses":
next_api_kwargs = agent._get_transport().preflight_kwargs(
next_api_kwargs,
allow_stream=False,
is_github_responses=agent._is_copilot_url(),
)
if _use_streaming:
return agent._interruptible_streaming_api_call(
next_api_kwargs, on_first_delta=_stop_spinner

View file

@ -440,13 +440,23 @@ class ResponsesApiTransport(ProviderTransport):
return False
return True
def preflight_kwargs(self, api_kwargs: Any, *, allow_stream: bool = False) -> dict:
def preflight_kwargs(
self,
api_kwargs: Any,
*,
allow_stream: bool = False,
is_github_responses: bool = False,
) -> dict:
"""Validate and sanitize Codex API kwargs before the call.
Normalizes input items, strips unsupported fields, validates structure.
"""
from agent.codex_responses_adapter import _preflight_codex_api_kwargs
return _preflight_codex_api_kwargs(api_kwargs, allow_stream=allow_stream)
return _preflight_codex_api_kwargs(
api_kwargs,
allow_stream=allow_stream,
is_github_responses=is_github_responses,
)
def map_finish_reason(self, raw_reason: str) -> str:
"""Map Codex response.status to OpenAI finish_reason.

View file

@ -238,6 +238,27 @@ def test_preflight_codex_input_items_keeps_short_message_id():
assert items[0]["id"] == _VALID_ITEM_ID
def test_preflight_codex_input_items_drops_short_id_for_github_responses():
items = _preflight_codex_input_items(
[
{
"type": "message",
"role": "assistant",
"status": "in_progress",
"content": [{"type": "output_text", "text": "pong"}],
"id": _VALID_ITEM_ID,
"phase": "final_answer",
}
],
is_github_responses=True,
)
assert "id" not in items[0]
assert items[0]["status"] == "in_progress"
assert items[0]["phase"] == "final_answer"
assert items[0]["content"] == [{"type": "output_text", "text": "pong"}]
def test_preflight_codex_api_kwargs_drops_oversized_message_id_end_to_end():
kwargs = _preflight_codex_api_kwargs(
{

View file

@ -181,6 +181,34 @@ class TestCodexBuildKwargs:
message_item = next(item for item in kw["input"] if item.get("type") == "message")
assert message_item["id"] == "msg_short_id"
def test_github_preflight_drops_id_reintroduced_by_request_override(self, transport):
injected = {
"type": "message",
"role": "assistant",
"status": "in_progress",
"content": [{"type": "output_text", "text": "pong"}],
"id": "stale_short",
"phase": "final_answer",
}
kw = transport.build_kwargs(
model="gpt-5.5",
messages=[{"role": "user", "content": "continue"}],
tools=[],
is_github_responses=True,
request_overrides={"input": [injected]},
)
preflight = transport.preflight_kwargs(
kw,
is_github_responses=True,
)
message_item = preflight["input"][0]
assert "id" not in message_item
assert message_item["status"] == "in_progress"
assert message_item["phase"] == "final_answer"
assert message_item["content"] == [{"type": "output_text", "text": "pong"}]
def test_non_github_responses_keeps_message_item_id_end_to_end(self, transport):
messages = [
{"role": "system", "content": "You are Hermes."},