fix(approval): fail closed at once when no human can see the approval prompt
A one-shot run (hermes chat --query-file ... -Q, stdin closed, no window)
still installs the prompt_toolkit approval callback but never starts the
prompt UI. The protected instruction-file gate (AGENTS.md / CLAUDE.md /
SOUL.md ...) took the registered callback for a reachable human and waited
out approvals.timeout on a panel painted nowhere. With the profile value of
300000 s the stage job hung for good.
- Gate: in a one-shot run, or when only an automatic approver
(subagent_auto_approve) is attached, return BLOCKED / APPROVAL REQUIRED
immediately instead of prompting. Human consent is still required every
time; yolo and approvals.single_query_mode still do not open this gate.
- One-shot exit prints an "approval_required: {json}" line on stderr
(session_id, tool_call_id, request_id, targets, reason, applied=false)
so the launcher can tell "blocked, needs a human" from "still working".
- execute_code: an approval that arrives after the calling script stopped
waiting (300 s reply timeout, script run timeout, retired kernel cell)
is not applied.
- Same invisible wait removed from the computer_use and memory-write
prompts in a one-shot run. The one-shot marker is no longer inherited by
child processes.
- BLOCKED text no longer breaks on a target path containing braces.
Long-running tools are untouched: no general tool timeout was shortened.
Kart: turkservis-20261007-1421-2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwSpve9HuXMy7dHf4RDMzX
This commit is contained in:
1 parent
aa498f4cbb
commit
0d8c521a3c
11 files changed
+884
-14
No files matched your search
@@ -716,6 +716,36 @@ def _call(tool_name, args):
|
||||
# RPC server (runs in a thread inside the parent process)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# How long the generated sandbox client waits for one tool-call reply
|
||||
# (``_sock.settimeout(300)`` / the file transport's 300s poll deadline).
|
||||
_RPC_REPLY_TIMEOUT_SECONDS = 300
|
||||
|
||||
|
||||
def _bind_rpc_reply_deadline(call_start: float, stop_event=None):
|
||||
"""Tell the approval gates when this call's requester stops waiting.
|
||||
|
||||
The requester is gone at the reply timeout, or earlier when the script
|
||||
itself ended (run timeout, interrupt): ``stop_event`` is set then.
|
||||
"""
|
||||
try:
|
||||
from tools.approval import set_approval_reply_deadline
|
||||
gone = stop_event.is_set if stop_event is not None else None
|
||||
return (True, set_approval_reply_deadline(
|
||||
call_start + _RPC_REPLY_TIMEOUT_SECONDS, gone))
|
||||
except Exception:
|
||||
return (False, None)
|
||||
|
||||
|
||||
def _restore_rpc_reply_deadline(previous) -> None:
|
||||
if not previous or not previous[0]:
|
||||
return
|
||||
try:
|
||||
from tools.approval import restore_approval_reply_deadline
|
||||
restore_approval_reply_deadline(previous[1])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Terminal parameters that must not be used from ephemeral sandbox scripts
|
||||
_TERMINAL_BLOCKED_PARAMS = {"background", "pty", "notify", "notify_on_complete", "watch_patterns"}
|
||||
|
||||
@@ -826,12 +856,18 @@ def _rpc_server_loop(
|
||||
# Dispatch through the standard tool handler.
|
||||
# Suppress stdout/stderr from internal tool handlers so
|
||||
# their status prints don't leak into the CLI spinner.
|
||||
# The sandbox client stops waiting for this reply after
|
||||
# _RPC_REPLY_TIMEOUT_SECONDS; an approval granted later
|
||||
# must not apply a write the script already saw fail.
|
||||
_prev_deadline = _bind_rpc_reply_deadline(call_start, stop_event)
|
||||
try:
|
||||
with thread_scoped_silence():
|
||||
result = dispatch(tool_name, tool_args)
|
||||
except Exception as exc:
|
||||
logger.error("Tool call failed in sandbox: %s", exc, exc_info=True)
|
||||
result = tool_error(str(exc))
|
||||
finally:
|
||||
_restore_rpc_reply_deadline(_prev_deadline)
|
||||
|
||||
tool_call_counter[0] += 1
|
||||
call_duration = time.monotonic() - call_start
|
||||
@@ -1101,6 +1137,7 @@ def _rpc_poll_loop(
|
||||
tool_args.pop(param, None)
|
||||
|
||||
# Dispatch through the standard tool handler
|
||||
_prev_deadline = _bind_rpc_reply_deadline(call_start, stop_event)
|
||||
try:
|
||||
with thread_scoped_silence():
|
||||
tool_result = handle_function_call(
|
||||
@@ -1110,6 +1147,8 @@ def _rpc_poll_loop(
|
||||
logger.error("Tool call failed in remote sandbox: %s",
|
||||
exc, exc_info=True)
|
||||
tool_result = tool_error(str(exc))
|
||||
finally:
|
||||
_restore_rpc_reply_deadline(_prev_deadline)
|
||||
|
||||
tool_call_counter[0] += 1
|
||||
call_duration = time.monotonic() - call_start
|
||||
|
||||
Reference in new issue
Block a user