start_gateway() sets HERMES_EXEC_ASK=1 process-wide, so an api_server/webhook turn inside a gateway still reads as 'ask' in _presence() even though _is_gateway_approval_context() already excluded the platform. The unattended branch then never runs: a smart ESCALATE (or any manual-mode dangerous command) falls through to the interactive transport, and on a session with no gateway notify callback — non-streaming /v1/chat/completions, webhook, msgraph_webhook — the card parks as pending_approval that no client can ever answer, while approvals.unattended_mode is never consulted (#100532). Drop is_ask for unattended-platform sessions without a registered notify callback, so the unattended branch resolves instantly from approvals.unattended_mode before the guardian call runs. /v1/runs and streaming chat completions register their callback and keep the ask path.
52 lines
2.5 KiB
Python
52 lines
2.5 KiB
Python
"""Unattended approval contexts never resolve as interactive (#110932).
|
|
|
|
A gateway sets HERMES_EXEC_ASK=1 at startup and hands its environ to every external cron
|
|
worker; interactive launches export HERMES_INTERACTIVE=1. Inside cron nobody can answer the
|
|
card, so ``_presence()`` must clear the trio and let the gate resolve from
|
|
``approvals.cron_mode``. Unattended platforms keep ``is_ask`` only while their session has a
|
|
registered approval notifier: api_server answers via the ``/v1/runs`` approval bridge, which
|
|
registers one; without a notifier (non-streaming /v1/chat/completions) the card can never be
|
|
answered, so the gate resolves from ``approvals.unattended_mode`` instead (#100532).
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from tools import approval as approval_mod
|
|
|
|
|
|
@pytest.fixture
|
|
def leaked_presence(monkeypatch):
|
|
monkeypatch.setenv("HERMES_INTERACTIVE", "1")
|
|
monkeypatch.setenv("HERMES_EXEC_ASK", "1")
|
|
monkeypatch.setenv("HERMES_GATEWAY_SESSION", "1")
|
|
monkeypatch.delenv("HERMES_CRON_SESSION", raising=False)
|
|
monkeypatch.delenv("HERMES_SINGLE_QUERY_SESSION", raising=False)
|
|
monkeypatch.delenv("HERMES_SESSION_PLATFORM", raising=False)
|
|
|
|
|
|
def test_cron_context_clears_leaked_presence(monkeypatch, leaked_presence):
|
|
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
|
|
_, is_cli, is_gateway, is_ask = approval_mod._presence()
|
|
assert (is_cli, is_gateway, is_ask) == (False, False, False)
|
|
|
|
|
|
def test_interactive_session_keeps_presence(monkeypatch, leaked_presence):
|
|
_, is_cli, is_gateway, is_ask = approval_mod._presence()
|
|
assert (is_cli, is_gateway, is_ask) == (True, True, True)
|
|
|
|
|
|
def test_api_server_platform_keeps_exec_ask_for_runs_approval_bridge(monkeypatch, leaked_presence):
|
|
"""api_server resolves approvals via ``approval.request`` → ``POST /v1/runs/{id}/approval``;
|
|
that bridge registers a notify callback for the session, and only then does ``is_ask``
|
|
survive — clearing it with a live bridge would turn every bridged dangerous command into
|
|
an instant BLOCK. Without a notifier the gate falls to ``approvals.unattended_mode``
|
|
(#100532; covered in test_approval.py)."""
|
|
monkeypatch.setenv("HERMES_SESSION_PLATFORM", "api_server")
|
|
monkeypatch.setenv("HERMES_SESSION_KEY", "test-runs-bridge-presence")
|
|
approval_mod.register_gateway_notify("test-runs-bridge-presence", lambda data: None)
|
|
try:
|
|
_, _, _, is_ask = approval_mod._presence()
|
|
assert is_ask is True
|
|
finally:
|
|
approval_mod.unregister_gateway_notify("test-runs-bridge-presence")
|