diff --git a/services/hermes/agent-configmap.yaml b/services/hermes/agent-configmap.yaml index b5c76d14..0966f3ed 100644 --- a/services/hermes/agent-configmap.yaml +++ b/services/hermes/agent-configmap.yaml @@ -185,6 +185,15 @@ data: tasks when an objective benefits from persistent Codex or Claude Code CLI execution that survives browser disconnects and can resume after restarts. + Project checkouts may provide nearer repository instructions, but they do + not replace these coordinator-wide rules. Never call `kanban_show` without + a known, non-empty task ID. Ad-hoc inspection and acceptance checks do not + need a synthetic Kanban lookup, and must load a skill only when its workflow + materially applies. Atlas HTTPS Git authentication is already supplied by + the runtime-only `GIT_ASKPASS`; use it without reading or exposing the + credential. Coordinator guidance lives at + `/opt/data/workspace/AGENTS.md` when more detail is needed. + The Jetson classifier is mandatory for AUTO selection. Switchyard may use local Qwen for bounded low-risk responses and continuity, or spill to a hosted provider when local capability is insufficient. Do not describe a @@ -296,8 +305,9 @@ data: ## Atlas engineering access - The Atlas organization has private visibility. Its repositories are access- - controlled as either private or Gitea-internal, never public, and are + The Atlas organization has private visibility. Repository visibility is + preserved per project and may be public or private; do not infer a + repository's visibility from the organization setting. Repositories are canonical at `https://scm.bstein.dev/atlas/.git`. HTTPS Git authentication is already supplied through `GIT_ASKPASS`. Verify the remote and cleanly separate pre-existing changes, create a task branch, run the diff --git a/services/hermes/scripts/codex_broker.py b/services/hermes/scripts/codex_broker.py index 87ad1e50..bf641603 100644 --- a/services/hermes/scripts/codex_broker.py +++ b/services/hermes/scripts/codex_broker.py @@ -221,6 +221,32 @@ def _upstream_headers(token: str) -> dict[str, str]: return headers +def _normalize_input_images(response_input: list[Any]) -> None: + """Normalize Chat-Completions image parts for the Codex Responses API.""" + for item in response_input: + if not isinstance(item, dict) or item.get("type") != "message": + continue + content = item.get("content") + if not isinstance(content, list): + continue + for part in content: + if not isinstance(part, dict) or part.get("type") not in { + "image_url", + "input_image", + }: + continue + image_url = part.get("image_url") + if isinstance(image_url, dict): + detail = image_url.get("detail") + image_url = image_url.get("url") + if isinstance(detail, str) and detail and "detail" not in part: + part["detail"] = detail + if not isinstance(image_url, str) or not image_url.strip(): + raise ValueError("non-empty Responses image URL required") + part["type"] = "input_image" + part["image_url"] = image_url + + def _validate_payload(payload: Any) -> dict[str, Any]: """Allow only bounded Responses requests for the approved model catalog.""" if not isinstance(payload, dict): @@ -251,6 +277,7 @@ def _validate_payload(payload: Any) -> dict[str, Any]: payload["input"] = [response_input] elif not isinstance(response_input, list) or not response_input: raise ValueError("non-empty Responses input list required") + _normalize_input_images(payload["input"]) # Switchyard uses ``max_output_tokens`` to bound the tiny classifier call, # but its Responses translation can also copy that internal option onto the # selected provider request. The first-party subscription Codex endpoint diff --git a/testing/tests/test_hermes_chat_quality.py b/testing/tests/test_hermes_chat_quality.py index b22d6647..c4914c78 100644 --- a/testing/tests/test_hermes_chat_quality.py +++ b/testing/tests/test_hermes_chat_quality.py @@ -76,6 +76,7 @@ def test_chat_config_enables_real_research_compute_and_delegation(): def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle(): configmap = _documents(HERMES / "agent-configmap.yaml")[0] instructions = configmap["data"]["AGENTS.md"] + soul = configmap["data"]["SOUL.md"] config = yaml.safe_load(configmap["data"]["config.yaml"]) assert ( @@ -89,11 +90,15 @@ def test_agent_config_keeps_delegated_reviewers_from_owning_task_lifecycle(): assert "must never complete, block, unblock, reclaim" in instructions assert "task's final structured result itself" in instructions assert "Atlas organization has private visibility" in instructions - assert "private or Gitea-internal, never public" in instructions + assert "may be public or private" in instructions + assert "do not infer a\nrepository's visibility" in instructions assert "already supplied through `GIT_ASKPASS`" in instructions assert "Never call `kanban_show` without a known, non-empty task ID" in instructions assert "bounded ad-hoc inspection and acceptance checks may" in instructions assert "load implementation or TDD skills" in instructions + assert "Never call `kanban_show` without\na known, non-empty task ID" in soul + assert "must load a skill only when its workflow\nmaterially applies" in soul + assert "runtime-only `GIT_ASKPASS`" in soul def test_agent_image_completes_parked_kanban_tasks_atomically(): @@ -751,6 +756,44 @@ def test_codex_broker_auth_and_request_contract(tmp_path: Path, monkeypatch): assert module._validate_payload( {"model": "gpt-5.6-terra", "input": response_items} )["input"] is response_items + image_items = [ + { + "type": "message", + "role": "user", + "content": [ + {"type": "input_text", "text": "What color is this?"}, + { + "type": "image_url", + "image_url": { + "url": "data:image/png;base64,cHJpdmF0ZQ==", + "detail": "high", + }, + }, + ], + } + ] + assert module._validate_payload( + {"model": "gpt-5.6-terra", "input": image_items} + )["input"][0]["content"][1] == { + "type": "input_image", + "image_url": "data:image/png;base64,cHJpdmF0ZQ==", + "detail": "high", + } + with pytest.raises(ValueError, match="non-empty Responses image URL"): + module._validate_payload( + { + "model": "gpt-5.6-terra", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + {"type": "input_image", "image_url": {"detail": "high"}} + ], + } + ], + } + ) routed = module._validate_payload( { "model": "route/codex/gpt-5.6-luna/low",