NousResearch/hermes-agent

Gateway approval routing fix for codex app-server

Cluster 26530 · gateway. Data as of

Nothing open on GitHub. Fix merged in #56534 on 1 Jul 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #56534. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #56534. That is separate from the fix event.
Still open
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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #56534 fix(codex-app-server): honor approvals.mode/yolo for gateway-context approval routing (salvage of #26533 by @simpolism, closes #26530)

    merged to main

    files: agent/codex_runtime.py, tests/run_agent/test_codex_app_server_integration.py, tools/approval.py

    Show the diff
    • agent/codex_runtime.py
      @@ -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
      @@ -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
      @@ -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
      @@ -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
      @@ -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:

1 PR closed without merging

From the close comments: merged via another change 1

  • #26533 fix(codex-app-server): honor approvals.mode/yolo for gateway-context approval routing · merged via another change → 0b8e819, b23e1c3, #56534
Closed issues · 1
  • Closed#26530codex app-server tool calls fail closed on gateway with no surfaceable approval prompt
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item 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.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
gateway
Merged PRs
#56534
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
1 Jul 2026

For your agent

The same facts, typed.