diff --git a/tests/gateway/test_approve_deny_commands.py b/tests/gateway/test_approve_deny_commands.py index b1c192f1ac..ebe4d59172 100644 --- a/tests/gateway/test_approve_deny_commands.py +++ b/tests/gateway/test_approve_deny_commands.py @@ -173,6 +173,23 @@ class TestBlockingGatewayApproval: assert e1.event.is_set() assert e2.event.is_set() + def test_clear_session_denies_and_signals_all_entries(self): + """clear_session must wake blocked entries during boundary cleanup.""" + from tools.approval import clear_session, _ApprovalEntry, _gateway_queues + + session_key = "test-boundary-cleanup" + e1 = _ApprovalEntry({"command": "cmd1"}) + e2 = _ApprovalEntry({"command": "cmd2"}) + _gateway_queues[session_key] = [e1, e2] + + clear_session(session_key) + + assert e1.event.is_set() + assert e2.event.is_set() + assert e1.result == "deny" + assert e2.result == "deny" + assert session_key not in _gateway_queues + # ------------------------------------------------------------------ # /approve command diff --git a/tests/gateway/test_session_boundary_security_state.py b/tests/gateway/test_session_boundary_security_state.py index f7f4124951..00c1568de1 100644 --- a/tests/gateway/test_session_boundary_security_state.py +++ b/tests/gateway/test_session_boundary_security_state.py @@ -10,6 +10,7 @@ from gateway.platforms.base import MessageEvent from gateway.session import SessionEntry, SessionSource, build_session_key from tools import approval as approval_mod from tools.approval import ( + _ApprovalEntry, approve_session, enable_session_yolo, is_approved, @@ -214,3 +215,30 @@ def test_clear_session_boundary_security_state_is_scoped(): runner._clear_session_boundary_security_state("") assert is_approved(other_key, "recursive delete") is True assert other_key in runner._update_prompt_pending + + +def test_clear_session_boundary_security_state_wakes_blocked_approvals(): + """Boundary cleanup must cancel blocked approval waiters immediately.""" + from gateway.run import GatewayRunner + + runner = object.__new__(GatewayRunner) + runner._pending_approvals = {} + runner._update_prompt_pending = {} + + source = _make_source() + session_key = build_session_key(source) + other_key = "agent:main:telegram:dm:other-chat" + + target_entry = _ApprovalEntry({"command": "rm -rf /tmp/demo"}) + other_entry = _ApprovalEntry({"command": "rm -rf /tmp/other"}) + approval_mod._gateway_queues[session_key] = [target_entry] + approval_mod._gateway_queues[other_key] = [other_entry] + + runner._clear_session_boundary_security_state(session_key) + + assert target_entry.event.is_set() + assert target_entry.result == "deny" + assert other_entry.event.is_set() is False + assert other_entry.result is None + assert session_key not in approval_mod._gateway_queues + assert other_key in approval_mod._gateway_queues diff --git a/tools/approval.py b/tools/approval.py index 78fb481783..aa20a86aec 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -400,8 +400,8 @@ def unregister_gateway_notify(session_key: str) -> None: with _lock: _gateway_notify_cbs.pop(session_key, None) entries = _gateway_queues.pop(session_key, []) - for entry in entries: - entry.event.set() + for entry in entries: + entry.event.set() def resolve_gateway_approval(session_key: str, choice: str, @@ -475,7 +475,12 @@ def clear_session(session_key: str) -> None: _session_approved.pop(session_key, None) _session_yolo.discard(session_key) _pending.pop(session_key, None) - _gateway_queues.pop(session_key, None) + entries = _gateway_queues.pop(session_key, []) + for entry in entries: + # Session-boundary cleanup should cancel any blocked approval waits + # immediately so the old run can unwind instead of idling until timeout. + entry.result = "deny" + entry.event.set() def is_session_yolo_enabled(session_key: str) -> bool: