mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-15 14:22:43 +00:00
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:
parent
22d5a35c16
commit
bce17bf6a2
5 changed files with 87 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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."},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue