{"repo":"NousResearch/hermes-agent","public_id":"26530","display_label":"Gateway approval routing fix for codex app-server","summary":"These items concern a bug where codex app-server tool calls fail closed when running under a gateway context with no surfaceable approval prompt. The work addresses honoring approvals.mode/yolo settings for proper gateway-context approval routing.","subsystem":"gateway","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":56534,"merged_at":"2026-07-01T17:28:38+00:00"},"resolution":{"state":"resolved","by":"pr-56534"},"freshness":{"state":"verified","last_verified_at":"2026-07-01T17:28:38+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":26530,"pr_number":56534,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-07-01T17:28:38+00:00"}],"members":[{"kind":"issue","number":26530,"title":"codex app-server tool calls fail closed on gateway with no surfaceable approval prompt","url":null,"state":"closed"},{"kind":"pr","number":26533,"title":"fix(codex-app-server): honor approvals.mode/yolo for gateway-context approval routing","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"0b8e81996","number":0},{"kind":"commit","value":"b23e1c307","number":0},{"kind":"pr","value":"#56534","number":56534}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":56534,"title":"fix(codex-app-server): honor approvals.mode/yolo for gateway-context approval routing (salvage of #26533 by @simpolism, closes #26530)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/codex_runtime.py","tests/run_agent/test_codex_app_server_integration.py","tools/approval.py"],"fix_hunks":[{"file":"agent/codex_runtime.py","hunks":["@@ -244,7 +244,10 @@ def run_codex_app_server_turn(\n     Called from run_conversation() when agent.api_mode == \"codex_app_server\".\n     Returns the same dict shape as the chat_completions path.\n     \"\"\"\n-    from agent.transports.codex_app_server_session import CodexAppServerSession\n+    from agent.transports.codex_app_server_session import (\n+        CodexAppServerSession,\n+        _ServerRequestRouting,\n+    )\n \n     # Lazy session: one CodexAppServerSession per AIAgent instance.\n     # Spawned on first turn, reused across turns, closed at AIAgent","@@ -262,6 +265,27 @@ def run_codex_app_server_turn(\n         except Exception:\n             approval_callback = None\n \n+        # Gateway / cron contexts have no UI to surface codex's approval\n+        # requests through, so codex app-server exec / apply_patch requests\n+        # fail closed (silently decline) by default. When the user has\n+        # explicitly opted out of Hermes approvals — via `approvals.mode: off`\n+        # in config, the /yolo session toggle, or --yolo / HERMES_YOLO_MODE —\n+        # honor that and let codex's own sandbox permission profile\n+        # (~/.codex/config.toml) be the policy gate instead of double-gating\n+        # with a missing Hermes UI. Defaults (manual/smart/unset) preserve the\n+        # current fail-closed behavior — this is a no-op for those users.\n+        auto_approve_requests = False\n+        try:\n+            from tools.approval import is_approval_bypass_active\n+\n+            auto_approve_requests = is_approval_bypass_active()\n+        except Exception:\n+            logger.debug(\n+                \"codex app-server: approval-bypass lookup failed; \"\n+                \"keeping fail-closed default\",\n+                exc_info=True,\n+            )\n+\n         def _on_codex_event(note: dict) -> None:\n             # Bridge Codex app-server item/started notifications to Hermes\n             # tool-progress so gateways show verbose \"running X\" breadcrumbs","@@ -281,6 +305,10 @@ def _on_codex_event(note: dict) -> None:\n         agent._codex_session = CodexAppServerSession(\n             cwd=cwd,\n             approval_callback=approval_callback,\n+            request_routing=_ServerRequestRouting(\n+                auto_approve_exec=auto_approve_requests,\n+                auto_approve_apply_patch=auto_approve_requests,\n+            ),\n             on_event=_on_codex_event,\n         )\n "]},{"file":"tests/run_agent/test_codex_app_server_integration.py","hunks":["@@ -326,6 +326,136 @@ def fake_run_turn(self, user_input: str, **kwargs):\n \n         assert captured[\"cwd\"] == str(tmp_path)\n \n+    def _capture_routing_agent(self, monkeypatch):\n+        \"\"\"Build a codex agent with a CodexAppServerSession stub that captures\n+        the request_routing passed at construction time, so we can assert how\n+        the gateway-context approval routing was resolved.\"\"\"\n+        captured: dict = {}\n+\n+        def fake_init(self, **kwargs):\n+            captured.update(kwargs)\n+            self._thread_id = \"thread-stub-1\"\n+\n+        def fake_run_turn(self, user_input: str, **kwargs):\n+            return TurnResult(\n+                final_text=\"ok\",\n+                projected_messages=[{\"role\": \"assistant\", \"content\": \"ok\"}],\n+                turn_id=\"turn-stub-1\",\n+                thread_id=\"thread-stub-1\",\n+            )\n+\n+        monkeypatch.setattr(CodexAppServerSession, \"__init__\", fake_init)\n+        monkeypatch.setattr(CodexAppServerSession, \"run_turn\", fake_run_turn)\n+        monkeypatch.setattr(\n+            CodexAppServerSession, \"ensure_started\", lambda self: \"thread-stub-1\"\n+        )\n+        return captured\n+\n+    def test_approvals_mode_off_auto_approves_codex_server_requests(\n+        self, monkeypatch\n+    ):\n+        \"\"\"When the user disables Hermes approvals, codex app-server approval\n+        requests should not fail closed just because no interactive callback is\n+        wired (the typical gateway path). Codex's own sandbox permission\n+        profile remains the filesystem boundary.\"\"\"\n+        captured = self._capture_routing_agent(monkeypatch)\n+        with patch(\n+            \"hermes_cli.config.load_config\",\n+            return_value={\"approvals\": {\"mode\": \"off\"}},\n+        ):\n+            agent = _make_codex_agent()\n+            with patch.object(\n+                agent, \"_spawn_background_review\", return_value=None\n+            ):\n+                agent.run_conversation(\"write something\")\n+        routing = captured[\"request_routing\"]\n+        assert routing.auto_approve_exec is True\n+        assert routing.auto_approve_apply_patch is True\n+\n+    def test_yaml_boolean_false_approval_mode_also_auto_approves(\n+        self, monkeypatch\n+    ):\n+        \"\"\"YAML 1.1 parses unquoted `off` as False; match the normal approval\n+        subsystem's compatibility behavior for codex app-server routing too.\"\"\"\n+        captured = self._capture_routing_agent(monkeypatch)\n+        with patch(\n+            \"hermes_cli.config.load_config\",\n+            return_value={\"approvals\": {\"mode\": False}},\n+        ):\n+            agent = _make_codex_agent()\n+            with patch.object(\n+                agent, \"_spawn_background_review\", return_value=None\n+            ):\n+                agent.run_conversation(\"write something\")\n+        routing = captured[\"request_routing\"]\n+        assert routing.auto_approve_exec is True\n+        assert routing.auto_approve_apply_patch is True\n+\n+    def test_manual_approvals_keep_codex_server_requests_fail_closed(\n+        self, monkeypatch\n+    ):\n+        \"\"\"Default (manual) approvals must preserve the fail-closed behavior —\n+        this fix is a no-op for users who haven't opted out.\"\"\"\n+        captured = self._capture_routing_agent(monkeypatch)\n+        with patch(\n+            \"hermes_cli.config.load_config\",\n+            return_value={\"approvals\": {\"mode\": \"manual\"}},\n+        ):\n+            agent = _make_codex_agent()\n+            with patch.object(\n+                agent, \"_spawn_background_review\", return_value=None\n+            ):\n+                agent.run_conversation(\"write something\")\n+        routing = captured[\"request_routing\"]\n+        assert routing.auto_approve_exec is False\n+        assert routing.auto_approve_apply_patch is False\n+\n+    def test_frozen_yolo_env_auto_approves_codex_server_requests(\n+        self, monkeypatch\n+    ):\n+        \"\"\"--yolo / HERMES_YOLO_MODE (frozen into _YOLO_MODE_FROZEN at import\n+        time — a prompt-injection-safe process-scoped bypass) should flow\n+        through to codex app-server routing so gateway/cron contexts do not\n+        fail closed when the user launched with yolo mode.\"\"\"\n+        import tools.approval as _approval\n+\n+        captured = self._capture_routing_agent(monkeypatch)\n+        monkeypatch.setattr(_approval, \"_YOLO_MODE_FROZEN\", True)\n+        with patch(\n+            \"hermes_cli.config.load_config\",\n+            return_value={\"approvals\": {\"mode\": \"manual\"}},\n+        ):\n+            agent = _make_codex_agent()\n+            with patch.object(\n+                agent, \"_spawn_background_review\", return_value=None\n+            ):\n+                agent.run_conversation(\"write something\")\n+        routing = captured[\"request_routing\"]\n+        assert routing.auto_approve_exec is True\n+        assert routing.auto_approve_apply_patch is True\n+\n+    def test_session_yolo_auto_approves_codex_server_requests(\n+        self, monkeypatch\n+    ):\n+        \"\"\"The /yolo session toggle should be honored at Codex session creation\n+        time, independent of the startup-time approvals config.\"\"\"\n+        captured = self._capture_routing_agent(monkeypatch)\n+        with patch(\n+            \"hermes_cli.config.load_config\",\n+            return_value={\"approvals\": {\"mode\": \"manual\"}},\n+        ):\n+            agent = _make_codex_agent()\n+            with patch(\n+                \"tools.approval.is_current_session_yolo_enabled\",\n+                return_value=True,\n+            ), patch.object(\n+                agent, \"_spawn_background_review\", return_value=None\n+            ):\n+                agent.run_conversation(\"write something\")\n+        routing = captured[\"request_routing\"]\n+        assert routing.auto_approve_exec is True\n+        assert routing.auto_approve_apply_patch is True\n+\n \n class TestReviewForkApiModeDowngrade:\n     \"\"\"When the parent agent runs on codex_app_server, the background"]},{"file":"tools/approval.py","hunks":["@@ -1770,6 +1770,27 @@ def _get_approval_mode() -> str:\n     return _normalize_approval_mode(mode)\n \n \n+def is_approval_bypass_active() -> bool:\n+    \"\"\"Return True when the user has opted out of Hermes approval prompts.\n+\n+    Collapses the canonical three-source bypass check used across the codebase\n+    into one place:\n+      - process-scoped ``--yolo`` / ``HERMES_YOLO_MODE`` (frozen at import time\n+        so a mid-process skill can't flip it — a prompt-injection escalation\n+        path; see ``_YOLO_MODE_FROZEN`` above),\n+      - the session-scoped gateway ``/yolo`` toggle,\n+      - ``approvals.mode: off`` in config.\n+\n+    This is the pure-bypass sub-expression only. Callers that also honor a\n+    hardline blocklist / permanent allowlist must check those separately.\n+    \"\"\"\n+    return (\n+        _YOLO_MODE_FROZEN\n+        or is_current_session_yolo_enabled()\n+        or _get_approval_mode() == \"off\"\n+    )\n+\n+\n def _get_approval_timeout() -> int:\n     \"\"\"Read the approval timeout from config. Defaults to 60 seconds.\"\"\"\n     try:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}