add new tool: to_captcha

This commit is contained in:
VladislavIlin7 2026-04-21 23:32:09 +03:00
parent 50589232d6
commit f1f32d8366
14 changed files with 1008 additions and 130 deletions

View file

@ -1922,6 +1922,7 @@ class HermesCLI:
platform="cli",
session_db=self._session_db,
clarify_callback=self._clarify_callback,
captcha_callback=self._captcha_callback,
reasoning_callback=(
self._stream_reasoning_delta if (self.streaming_enabled and self.show_reasoning)
else self._on_reasoning if (self.show_reasoning or self.verbose)
@ -5113,6 +5114,40 @@ class HermesCLI:
"Use your best judgement to make the choice and proceed."
)
def _captcha_callback(self, payload):
"""Prompt the user to complete a paused CAPTCHA flow in the live browser."""
import time as _time
timeout = int((payload.get("verification") or {}).get("max_wait_seconds", 900))
response_queue = queue.Queue()
self._captcha_state = {
"payload": payload,
"response_queue": response_queue,
}
self._captcha_deadline = _time.monotonic() + timeout
self._invalidate()
last_refresh = _time.monotonic()
while True:
try:
result = response_queue.get(timeout=1)
self._captcha_deadline = 0
return result
except queue.Empty:
remaining = self._captcha_deadline - _time.monotonic()
if remaining <= 0:
break
now = _time.monotonic()
if now - last_refresh >= 5.0:
last_refresh = now
self._invalidate()
self._captcha_state = None
self._captcha_deadline = 0
self._invalidate()
_cprint(f"\n{_DIM}(captcha wait timed out after {timeout}s){_RST}")
return {"action": "timeout", "user_response": ""}
def _sudo_password_callback(self) -> str:
"""
Prompt for sudo password through the prompt_toolkit UI.
@ -5812,6 +5847,8 @@ class HermesCLI:
return [("class:sudo-prompt", f"🔑 {state_suffix}")]
if self._approval_state:
return [("class:prompt-working", f"{state_suffix}")]
if self._captcha_state:
return [("class:prompt-working", f"🧩 {state_suffix}")]
if self._clarify_freetext:
return [("class:clarify-selected", f"{state_suffix}")]
if self._clarify_state:
@ -5878,6 +5915,7 @@ class HermesCLI:
sudo_widget,
secret_widget,
approval_widget,
captcha_widget,
clarify_widget,
spinner_widget,
spacer,
@ -5900,6 +5938,7 @@ class HermesCLI:
sudo_widget,
secret_widget,
approval_widget,
captcha_widget,
clarify_widget,
spinner_widget,
spacer,
@ -5983,6 +6022,10 @@ class HermesCLI:
self._approval_deadline = 0
self._approval_lock = threading.Lock() # serialize concurrent approval prompts (delegation race fix)
# CAPTCHA / human verification prompt state
self._captcha_state = None # dict with payload + response_queue
self._captcha_deadline = 0
# Slash command loading state
self._command_running = False
self._command_status = ""
@ -6058,6 +6101,23 @@ class HermesCLI:
event.app.invalidate()
return
# --- CAPTCHA prompt: accept ready/done/abort style input ---
if self._captcha_state:
text = event.app.current_buffer.text.strip()
normalized = text.lower()
if normalized in {"abort", "cancel", "stop"}:
action = "abort"
elif text:
action = "ready"
else:
return
self._captcha_state["response_queue"].put({"action": action, "user_response": text})
self._captcha_state = None
self._captcha_deadline = 0
event.app.current_buffer.reset()
event.app.invalidate()
return
# --- Clarify freetext mode: user typed their own answer ---
if self._clarify_freetext and self._clarify_state:
text = event.app.current_buffer.text.strip()
@ -6194,7 +6254,7 @@ class HermesCLI:
# Buffer.auto_up/auto_down handle both: cursor movement when multi-line,
# history browsing when on the first/last line (or single-line input).
_normal_input = Condition(
lambda: not self._clarify_state and not self._approval_state and not self._sudo_state and not self._secret_state
lambda: not self._clarify_state and not self._approval_state and not self._captcha_state and not self._sudo_state and not self._secret_state
)
@kb.add('up', filter=_normal_input)
@ -6261,6 +6321,14 @@ class HermesCLI:
event.app.invalidate()
return
if self._captcha_state:
self._captcha_state["response_queue"].put({"action": "timeout", "user_response": ""})
self._captcha_state = None
self._captcha_deadline = 0
event.app.current_buffer.reset()
event.app.invalidate()
return
# Cancel clarify prompt
if self._clarify_state:
self._clarify_state["response_queue"].put(
@ -6334,7 +6402,7 @@ class HermesCLI:
# Guard: don't START recording during agent run or interactive prompts
if cli_ref._agent_running:
return
if cli_ref._clarify_state or cli_ref._sudo_state or cli_ref._approval_state:
if cli_ref._clarify_state or cli_ref._sudo_state or cli_ref._approval_state or cli_ref._captcha_state:
return
# Guard: don't start while a previous stop/transcribe cycle is
# still running — recorder.stop() holds AudioRecorder._lock and
@ -6554,6 +6622,8 @@ class HermesCLI:
return "type secret (hidden), Enter to skip"
if cli_ref._approval_state:
return ""
if cli_ref._captcha_state:
return "type ready/done after you solve the challenge, or abort to cancel"
if cli_ref._clarify_freetext:
return "type your answer here and press Enter"
if cli_ref._clarify_state:
@ -6597,6 +6667,13 @@ class HermesCLI:
('class:clarify-countdown', f' ({remaining}s)'),
]
if cli_ref._captcha_state:
remaining = max(0, int(cli_ref._captcha_deadline - _time.monotonic()))
return [
('class:hint', " complete the challenge in the browser, then type 'ready'"),
('class:clarify-countdown', f' ({remaining}s)'),
]
if cli_ref._clarify_state:
remaining = max(0, int(cli_ref._clarify_deadline - _time.monotonic()))
countdown = f' ({remaining}s)' if cli_ref._clarify_deadline else ''
@ -6619,7 +6696,7 @@ class HermesCLI:
return []
def get_hint_height():
if cli_ref._sudo_state or cli_ref._secret_state or cli_ref._approval_state or cli_ref._clarify_state or cli_ref._command_running:
if cli_ref._sudo_state or cli_ref._secret_state or cli_ref._approval_state or cli_ref._captcha_state or cli_ref._clarify_state or cli_ref._command_running:
return 1
# Keep a 1-line spacer while agent runs so output doesn't push
# right up against the top rule of the input area
@ -6644,7 +6721,7 @@ class HermesCLI:
height=get_hint_height,
)
# --- Clarify tool: dynamic display widget for questions + choices ---
# --- Interactive panels: CAPTCHA + clarify ---
def _panel_box_width(title: str, content_lines: list[str], min_width: int = 46, max_width: int = 76) -> int:
"""Choose a stable panel width wide enough for the title and content."""
@ -6672,6 +6749,45 @@ class HermesCLI:
def _append_blank_panel_line(lines, border_style: str, box_width: int) -> None:
lines.append((border_style, "" + (" " * box_width) + "\n"))
def _get_captcha_display():
state = cli_ref._captcha_state
if not state:
return []
payload = state.get("payload") or {}
title = "Manual CAPTCHA Required"
browser_view_url = payload.get("browser_view_url") or "Browser view URL is not configured."
body_lines = [
payload.get("instructions") or "Open the live browser and complete the verification challenge.",
f"Type: {payload.get('captcha_type', 'unknown')}",
f"Task ID: {payload.get('task_id', '')}",
f"Browser: {browser_view_url}",
"When the challenge disappears, type 'ready' or 'done' and press Enter.",
"Type 'abort' to stop this browser task.",
]
box_width = _panel_box_width(title, body_lines, min_width=56, max_width=94)
inner_text_width = max(8, box_width - 2)
lines = []
lines.append(('class:captcha-border', '╭─ '))
lines.append(('class:captcha-title', title))
lines.append(('class:captcha-border', ' ' + ('' * max(0, box_width - len(title) - 3)) + '\n'))
_append_blank_panel_line(lines, 'class:captcha-border', box_width)
for text in body_lines:
style = 'class:captcha-link' if text.startswith("Browser: ") else 'class:captcha-text'
for wrapped in _wrap_panel_text(text, inner_text_width):
_append_panel_line(lines, 'class:captcha-border', style, wrapped, box_width)
_append_blank_panel_line(lines, 'class:captcha-border', box_width)
lines.append(('class:captcha-border', '' + ('' * box_width) + '\n'))
return lines
captcha_widget = ConditionalContainer(
Window(
FormattedTextControl(_get_captcha_display),
wrap_lines=True,
),
filter=Condition(lambda: cli_ref._captcha_state is not None),
)
def _get_clarify_display():
"""Build styled text for the clarify question/choices panel."""
state = cli_ref._clarify_state
@ -6897,6 +7013,7 @@ class HermesCLI:
sudo_widget=sudo_widget,
secret_widget=secret_widget,
approval_widget=approval_widget,
captcha_widget=captcha_widget,
clarify_widget=clarify_widget,
spinner_widget=spinner_widget,
spacer=spacer,
@ -6954,6 +7071,11 @@ class HermesCLI:
'approval-cmd': '#AAAAAA italic',
'approval-choice': '#AAAAAA',
'approval-selected': '#FFD700 bold',
# CAPTCHA panel
'captcha-border': '#CD7F32',
'captcha-title': '#FFBF00 bold',
'captcha-text': '#FFF8DC',
'captcha-link': '#87CEEB underline',
# Voice mode
'voice-prompt': '#87CEEB',
'voice-recording': '#FF4444 bold',