Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,19 @@ def __init__(
_agent_cfg = _load_agent_config()
except Exception:
_agent_cfg = {}
_approvals_cfg = _agent_cfg.get("approvals", {})
if not isinstance(_approvals_cfg, dict):
_approvals_cfg = {}
try:
from tools.approval import _normalize_approval_mode
_approval_mode = _normalize_approval_mode(
_approvals_cfg.get("mode", "manual")
)
except Exception:
_approval_mode = str(
_approvals_cfg.get("mode", "manual")
).lower()
self._codex_app_server_auto_approve_requests = _approval_mode == "off"
try:
self._tool_guardrails = ToolCallGuardrailController(
ToolCallGuardrailConfig.from_mapping(
Expand Down Expand Up @@ -15748,7 +15761,10 @@ def _run_codex_app_server_turn(
Called from run_conversation() when self.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
Expand All @@ -15763,9 +15779,36 @@ def _run_codex_app_server_turn(
approval_callback = _get_approval_callback()
except Exception:
approval_callback = None
# Gateway 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 HERMES_YOLO_MODE=1 — honor that and let codex's own
# sandbox permission profile be the policy gate instead of
# double-gating with a missing Hermes UI.
_auto_approve_requests = bool(
self._codex_app_server_auto_approve_requests
)
try:
from tools.approval import is_current_session_yolo_enabled
_auto_approve_requests = (
_auto_approve_requests
or is_current_session_yolo_enabled()
)
except Exception:
pass
_auto_approve_requests = (
_auto_approve_requests
or os.getenv("HERMES_YOLO_MODE", "").lower() in {"1", "true", "yes", "on"}
)
self._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,
),
)

# NOTE: the user message is ALREADY appended to messages by the
Expand Down
162 changes: 162 additions & 0 deletions tests/run_agent/test_codex_app_server_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,168 @@ def test_chat_completions_loop_is_not_entered(self, fake_session):
agent.run_conversation("hi")
assert not client_mock.chat.completions.create.called

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 (typical gateway path). Codex's own sandbox permission profile
remains the filesystem boundary.
"""
captured = {}

def fake_init(self, **kwargs):
captured.update(kwargs)

def fake_run_turn(self, user_input: str, **kwargs):
return TurnResult(final_text="ok", thread_id="th", turn_id="tu")

monkeypatch.setattr(CodexAppServerSession, "__init__", fake_init)
monkeypatch.setattr(CodexAppServerSession, "run_turn", fake_run_turn)
monkeypatch.setattr(
CodexAppServerSession, "ensure_started", lambda self: "th"
)

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 parses unquoted `off` as False; match the normal approval
subsystem's compatibility behavior for codex app-server routing too.
"""
captured = {}

def fake_init(self, **kwargs):
captured.update(kwargs)

monkeypatch.setattr(CodexAppServerSession, "__init__", fake_init)
monkeypatch.setattr(
CodexAppServerSession,
"run_turn",
lambda self, user_input, **kwargs: TurnResult(
final_text="ok", thread_id="th", turn_id="tu"
),
)
monkeypatch.setattr(
CodexAppServerSession, "ensure_started", lambda self: "th"
)

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):
captured = {}

def fake_init(self, **kwargs):
captured.update(kwargs)

def fake_run_turn(self, user_input: str, **kwargs):
return TurnResult(final_text="ok", thread_id="th", turn_id="tu")

monkeypatch.setattr(CodexAppServerSession, "__init__", fake_init)
monkeypatch.setattr(CodexAppServerSession, "run_turn", fake_run_turn)
monkeypatch.setattr(
CodexAppServerSession, "ensure_started", lambda self: "th"
)

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_hermes_yolo_env_auto_approves_codex_server_requests(self, monkeypatch):
"""HERMES_YOLO_MODE should flow through to codex app-server routing
so gateway/cron contexts do not fail closed when the user explicitly
enabled yolo mode outside the CLI slash-command path.
"""
captured = {}

def fake_init(self, **kwargs):
captured.update(kwargs)

def fake_run_turn(self, user_input: str, **kwargs):
return TurnResult(final_text="ok", thread_id="th", turn_id="tu")

monkeypatch.setattr(CodexAppServerSession, "__init__", fake_init)
monkeypatch.setattr(CodexAppServerSession, "run_turn", fake_run_turn)
monkeypatch.setattr(
CodexAppServerSession, "ensure_started", lambda self: "th"
)
monkeypatch.setenv("HERMES_YOLO_MODE", "1")

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 = {}

def fake_init(self, **kwargs):
captured.update(kwargs)

def fake_run_turn(self, user_input: str, **kwargs):
return TurnResult(final_text="ok", thread_id="th", turn_id="tu")

monkeypatch.setattr(CodexAppServerSession, "__init__", fake_init)
monkeypatch.setattr(CodexAppServerSession, "run_turn", fake_run_turn)
monkeypatch.setattr(
CodexAppServerSession, "ensure_started", lambda self: "th"
)

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
Expand Down
Loading