Skip to content
Merged
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
17 changes: 17 additions & 0 deletions tests/gateway/test_approve_deny_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,23 @@ def test_unregister_signals_all_entries(self):
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
Expand Down
28 changes: 28 additions & 0 deletions tests/gateway/test_session_boundary_security_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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,
Expand Down Expand Up @@ -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
11 changes: 8 additions & 3 deletions tools/approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
Loading