# NousResearch/hermes-agent · Gateway approval routing fix for codex app-server

## Open now

- Nothing open on GitHub.

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.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #56534. Verify it addresses your case.
- **Recommended:** read PR #56534 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-56534`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #26530: codex app-server tool calls fail closed on gateway with no surfaceable approval prompt

## Prior & competing PRs

- **#56534: fix(codex-app-server): honor approvals.mode/yolo for gateway-context approval routing (salvage of #26533 by @simpolism, closes #26530)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/codex_runtime.py`, `tests/run_agent/test_codex_app_server_integration.py`, `tools/approval.py`
  - fix hunks:
  - `agent/codex_runtime.py`

    ```diff
    @@ -244,7 +244,10 @@ def run_codex_app_server_turn(
         Called from run_conversation() when agent.api_mode == "codex_app_server".
         Returns the same dict shape as the chat_completions path.
         """
    -    from agent.transports.codex_app_server_session import CodexAppServerSession
    +    from agent.transports.codex_app_server_session import (
    +        CodexAppServerSession,
    +        _ServerRequestRouting,
    +    )
     
         # Lazy session: one CodexAppServerSession per AIAgent instance.
         # Spawned on first turn, reused across turns, closed at AIAgent
    ```

    ```diff
    @@ -262,6 +265,27 @@ def run_codex_app_server_turn(
             except Exception:
                 approval_callback = None
     
    +        # Gateway / cron contexts have no UI to surface codex's approval
    +        # requests through, so codex app-server exec / apply_patch requests
    +        # fail closed (silently decline) by default. When the user has
    +        # explicitly opted out of Hermes approvals — via `approvals.mode: off`
    +        # in config, the /yolo session toggle, or --yolo / HERMES_YOLO_MODE —
    +        # honor that and let codex's own sandbox permission profile
    +        # (~/.codex/config.toml) be the policy gate instead of double-gating
    +        # with a missing Hermes UI. Defaults (manual/smart/unset) preserve the
    +        # current fail-closed behavior — this is a no-op for those users.
    +        auto_approve_requests = False
    +        try:
    +            from tools.approval import is_approval_bypass_active
    +
    +            auto_approve_requests = is_approval_bypass_active()
    +        except Exception:
    +            logger.debug(
    +                "codex app-server: approval-bypass lookup failed; "
    +                "keeping fail-closed default",
    +                exc_info=True,
    +            )
    +
             def _on_codex_event(note: dict) -> None:
                 # Bridge Codex app-server item/started notifications to Hermes
                 # tool-progress so gateways show verbose "running X" breadcrumbs
    ```

    ```diff
    @@ -281,6 +305,10 @@ def _on_codex_event(note: dict) -> None:
             agent._codex_session = CodexAppServerSession(
                 cwd=cwd,
                 approval_callback=approval_callback,
    +            request_routing=_ServerRequestRouting(
    +                auto_approve_exec=auto_approve_requests,
    +                auto_approve_apply_patch=auto_approve_requests,
    +            ),
                 on_event=_on_codex_event,
             )
     
    ```
  - `tests/run_agent/test_codex_app_server_integration.py`

    ```diff
    @@ -326,6 +326,136 @@ def fake_run_turn(self, user_input: str, **kwargs):
     
             assert captured["cwd"] == str(tmp_path)
     
    +    def _capture_routing_agent(self, monkeypatch):
    +        """Build a codex agent with a CodexAppServerSession stub that captures
    +        the request_routing passed at construction time, so we can assert how
    +        the gateway-context approval routing was resolved."""
    +        captured: dict = {}
    +
    +        def fake_init(self, **kwargs):
    +            captured.update(kwargs)
    +            self._thread_id = "thread-stub-1"
    +
    +        def fake_run_turn(self, user_input: str, **kwargs):
    +            return TurnResult(
    +                final_text="ok",
    +                projected_messages=[{"role": "assistant", "content": "ok"}],
    +                turn_id="turn-stub-1",
    +                thread_id="thread-stub-1",
    +            )
    +
    +        monkeypatch.setattr(CodexAppServerSession, "__init__", fake_init)
    +        monkeypatch.setattr(CodexAppServerSession, "run_turn", fake_run_turn)
    +        monkeypatch.setattr(
    +            CodexAppServerSession, "ensure_started", lambda self: "thread-stub-1"
    +        )
    +        return captured
    +
    +    def test_approvals_mode_off_auto_approves_codex_server_requests(
    +        self, monkeypatch
    +    ):
    +        """When the user disables Hermes approvals, codex app-server approval
    +        requests should not fail closed just because no interactive callback is
    +        wired (the typical gateway path). Codex's own sandbox permission
    +        profile remains the filesystem boundary."""
    +        captured = self._capture_routing_agent(monkeypatch)
    +        with patch(
    +            "hermes_cli.config.load_config",
    +            return_value={"approvals": {"mode": "off"}},
    +        ):
    +            agent = _make_codex_agent()
    +            with patch.object(
    +                agent, "_spawn_background_review", return_value=None
    +            ):
    +                agent.run_conversation("write something")
    +        routing = captured["request_routing"]
    +        assert routing.auto_approve_exec is True
    +        assert routing.auto_approve_apply_patch is True
    +
    +    def test_yaml_boolean_false_approval_mode_also_auto_approves(
    +        self, monkeypatch
    +    ):
    +        """YAML 1.1 parses unquoted `off` as False; match the normal approval
    +        subsystem's compatibility behavior for codex app-server routing too."""
    +        captured = self._capture_routing_agent(monkeypatch)
    +        with patch(
    +            "hermes_cli.config.load_config",
    +            return_value={"approvals": {"mode": False}},
    +        ):
    +            agent = _make_codex_agent()
    +            with patch.object(
    +                agent, "_spawn_background_review", return_value=None
    +            ):
    +                agent.run_conversation("write something")
    +        routing = captured["request_routing"]
    +        assert routing.auto_approve_exec is True
    +        assert routing.auto_approve_apply_patch is True
    +
    +    def test_manual_approvals_keep_codex_server_requests_fail_closed(
    +        self, monkeypatch
    +    ):
    +        """Default (manual) approvals must preserve the fail-closed behavior —
    +        this fix is a no-op for users who haven't opted out."""
    +        captured = self._capture_routing_agent(monkeypatch)
    +        with patch(
    +            "hermes_cli.config.load_config",
    +            return_value={"approvals": {"mode": "manual"}},
    +        ):
    +            agent = _make_codex_agent()
    +            with patch.object(
    +                agent, "_spawn_background_review", return_value=None
    +            ):
    +                agent.run_conversation("write something")
    +        routing = captured["request_routing"]
    +        assert routing.auto_approve_exec is False
    +        assert routing.auto_approve_apply_patch is False
    +
    +    def test_frozen_yolo_env_auto_approves_codex_server_requests(
    +        self, monkeypatch
    +    ):
    +        """--yolo / HERMES_YOLO_MODE (frozen into _YOLO_MODE_FROZEN at import
    +        time — a prompt-injection-safe process-scoped bypass) should flow
    +        through to codex app-server routing so gateway/cron contexts do not
    +        fail closed when the user launched with yolo mode."""
    +        import tools.approval as _approval
    +
    +        captured = self._capture_routing_agent(monkeypatch)
    +        monkeypatch.setattr(_approval, "_YOLO_MODE_FROZEN", True)
    +        with patch(
    +            "hermes_cli.config.load_config",
    +            return_value={"approvals": {"mode": "manual"}},
    +        ):
    +            agent = _make_codex_agent()
    +            with patch.object(
    +                agent, "_spawn_background_review", return_value=None
    +            ):
    +                agent.run_conversation("write something")
    +        routing = captured["request_routing"]
    +        assert routing.auto_approve_exec is True
    +        assert routing.auto_approve_apply_patch is True
    +
    +    def test_session_yolo_auto_approves_codex_server_requests(
    +        self, monkeypatch
    +    ):
    +        """The /yolo session toggle should be honored at Codex session creation
    +        time, independent of the startup-time approvals config."""
    +        captured = self._capture_routing_agent(monkeypatch)
    +        with patch(
    +            "hermes_cli.config.load_config",
    +            return_value={"approvals": {"mode": "manual"}},
    +        ):
    +            agent = _make_codex_agent()
    +            with patch(
    +                "tools.approval.is_current_session_yolo_enabled",
    +                return_value=True,
    +            ), patch.object(
    +                agent, "_spawn_background_review", return_value=None
    +            ):
    +                agent.run_conversation("write something")
    +        routing = captured["request_routing"]
    +        assert routing.auto_approve_exec is True
    +        assert routing.auto_approve_apply_patch is True
    +
     
     class TestReviewForkApiModeDowngrade:
         """When the parent agent runs on codex_app_server, the background
    ```
  - `tools/approval.py`

    ```diff
    @@ -1770,6 +1770,27 @@ def _get_approval_mode() -> str:
         return _normalize_approval_mode(mode)
     
     
    +def is_approval_bypass_active() -> bool:
    +    """Return True when the user has opted out of Hermes approval prompts.
    +
    +    Collapses the canonical three-source bypass check used across the codebase
    +    into one place:
    +      - process-scoped ``--yolo`` / ``HERMES_YOLO_MODE`` (frozen at import time
    +        so a mid-process skill can't flip it — a prompt-injection escalation
    +        path; see ``_YOLO_MODE_FROZEN`` above),
    +      - the session-scoped gateway ``/yolo`` toggle,
    +      - ``approvals.mode: off`` in config.
    +
    +    This is the pure-bypass sub-expression only. Callers that also honor a
    +    hardline blocklist / permanent allowlist must check those separately.
    +    """
    +    return (
    +        _YOLO_MODE_FROZEN
    +        or is_current_session_yolo_enabled()
    +        or _get_approval_mode() == "off"
    +    )
    +
    +
     def _get_approval_timeout() -> int:
         """Read the approval timeout from config. Defaults to 60 seconds."""
         try:
    ```
- **#26533: fix(codex-app-server): honor approvals.mode/yolo for gateway-context approval routing**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `0b8e81996`, `b23e1c307`, `#56534`

## Fix status

- Merged to main in PR #56534 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-01`.
- Subsystem: `gateway`
