fix: keep api_server approval bridge and cron self-scheduling after presence strip
Widening the _presence() clearing from single-query to every unattended
context also cleared is_ask for platform=api_server. That surface answers
approvals through the /v1/runs bridge (approval.request ->
POST /v1/runs/{id}/approval), so a dangerous command that used to park in
waiting_for_approval became an instant BLOCK with no approval.request.
Restrict the clearing to single-query + cron, where nobody can answer.
Stripping HERMES_INTERACTIVE/HERMES_GATEWAY_SESSION/HERMES_EXEC_ASK from
the external worker env also made check_cronjob_requirements() False, so
the cronjob toolset vanished for every job on a managed-systemd gateway
even with cron.allow_agent_scheduling: true. Accept the existing
HERMES_CRON_SESSION marker (set by run_one_job's context) as well.
Review finding: _presence() over-widening broke the /v1/runs approval bridge; env strip hid the cronjob toolset in external workers.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
"""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 (or a programmatic
|
||||
platform session) nobody can answer the card, so ``_presence()`` must clear the trio and let
|
||||
the gate resolve from ``approvals.cron_mode`` / ``approvals.unattended_mode``.
|
||||
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 are NOT cleared: api_server answers via the
|
||||
``/v1/runs`` approval bridge, which needs ``is_ask`` intact.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
@@ -21,14 +22,8 @@ def leaked_presence(monkeypatch):
|
||||
monkeypatch.delenv("HERMES_SESSION_PLATFORM", raising=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"unattended_env",
|
||||
[{"HERMES_CRON_SESSION": "1"}, {"HERMES_SESSION_PLATFORM": "webhook"}],
|
||||
ids=["cron", "webhook"],
|
||||
)
|
||||
def test_unattended_context_clears_leaked_presence(monkeypatch, leaked_presence, unattended_env):
|
||||
for key, value in unattended_env.items():
|
||||
monkeypatch.setenv(key, value)
|
||||
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)
|
||||
|
||||
@@ -36,3 +31,11 @@ def test_unattended_context_clears_leaked_presence(monkeypatch, leaked_presence,
|
||||
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``;
|
||||
clearing ``is_ask`` there would turn every dangerous command into an instant BLOCK."""
|
||||
monkeypatch.setenv("HERMES_SESSION_PLATFORM", "api_server")
|
||||
_, _, _, is_ask = approval_mod._presence()
|
||||
assert is_ask is True
|
||||
|
||||
@@ -207,6 +207,15 @@ class TestCronjobRequirements:
|
||||
assert check_cronjob_requirements() is True
|
||||
|
||||
|
||||
def test_accepts_external_cron_worker_with_presence_vars_stripped(self, monkeypatch):
|
||||
"""``_launch_external_cron_worker`` strips the presence trio from the worker env; the
|
||||
cron session marker alone must keep ``cron.allow_agent_scheduling: true`` effective."""
|
||||
for v in ("HERMES_INTERACTIVE", "HERMES_GATEWAY_SESSION", "HERMES_EXEC_ASK"):
|
||||
monkeypatch.delenv(v, raising=False)
|
||||
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
|
||||
|
||||
assert check_cronjob_requirements() is True
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"var_name",
|
||||
["HERMES_INTERACTIVE", "HERMES_GATEWAY_SESSION", "HERMES_EXEC_ASK"],
|
||||
|
||||
@@ -869,16 +869,16 @@ def _human_decision(spec: _GateSpec, *, command: str, description: str,
|
||||
def _presence(approval_callback=None) -> tuple:
|
||||
"""``(approval_callback, is_cli, is_gateway, is_ask)`` for the current context.
|
||||
|
||||
Every unattended context (single-query ``-q``, cron, programmatic platforms) clears the
|
||||
presence trio: ``hermes chat -q`` exports HERMES_INTERACTIVE=1 for sudo prompts, a gateway
|
||||
sets HERMES_EXEC_ASK=1 at startup and passes its environ to every external cron worker
|
||||
(#110932), and a webhook session inherits that same HERMES_EXEC_ASK — in none of them can a
|
||||
human answer the card, so the gate must resolve from ``approvals.<ctx>_mode`` instead of
|
||||
parking on a pending approval."""
|
||||
Single-query ``-q`` and cron clear the presence trio: ``hermes chat -q`` exports
|
||||
HERMES_INTERACTIVE=1 for sudo prompts, and a gateway sets HERMES_EXEC_ASK=1 at startup and
|
||||
passes its environ to every external cron worker (#110932) — in neither can a human answer
|
||||
the card, so the gate must resolve from ``approvals.<ctx>_mode`` instead of parking on a
|
||||
pending approval. Unattended *platforms* keep ``is_ask``: api_server relies on it for the
|
||||
``/v1/runs`` approval bridge (``approval.request`` → ``POST /v1/runs/{id}/approval``)."""
|
||||
approval_callback = _resolve_cli_approval_callback(approval_callback)
|
||||
is_cli, is_gateway = _is_interactive_cli(), _is_gateway_approval_context()
|
||||
is_ask = env_var_enabled("HERMES_EXEC_ASK")
|
||||
if _unattended_contexts():
|
||||
if _is_single_query_approval_context() or _is_cron_approval_context():
|
||||
is_cli = is_gateway = is_ask = False
|
||||
return approval_callback, is_cli, is_gateway, is_ask
|
||||
|
||||
|
||||
@@ -1094,13 +1094,17 @@ Jobs run in a fresh session with no current-chat context, so prompts must be sel
|
||||
|
||||
|
||||
def check_cronjob_requirements() -> bool:
|
||||
"""Available in interactive CLI mode and gateway/messaging platforms (the scheduler is
|
||||
internal; no crontab needed). Flags must be explicitly truthy via ``env_var_enabled``."""
|
||||
from utils import env_var_enabled
|
||||
"""Available in interactive CLI mode, gateway/messaging platforms, and cron runs (the
|
||||
scheduler is internal; no crontab needed). Flags must be explicitly truthy via
|
||||
``env_var_enabled``. An external cron worker has the presence vars stripped from its env, so
|
||||
the cron session marker keeps ``cron.allow_agent_scheduling`` meaningful there."""
|
||||
from gateway.session_context import get_session_env
|
||||
from utils import env_var_enabled, is_truthy_value
|
||||
return (
|
||||
env_var_enabled("HERMES_INTERACTIVE")
|
||||
or env_var_enabled("HERMES_GATEWAY_SESSION")
|
||||
or env_var_enabled("HERMES_EXEC_ASK")
|
||||
or is_truthy_value(get_session_env("HERMES_CRON_SESSION", ""))
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user