Files
hermes-agent/tui_gateway/contracts/server_requests.py
brooklyn! cc97da2888 fix(preview): refuse unfocused type and abort leftover keystrokes
After the locate click, type now refuses unless the located editable is
document.activeElement, so characters are not delivered as page input when
focus stayed on body. The keystroke loop checks an abort signal between
characters; preview.act cancel (timeout or interrupt) and a local Stop both
set it, so queued keystrokes stop. A printable press on body/html is refused
unless allow_shortcut is set.
2026-09-25 17:33:06 -05:00

232 lines
8.0 KiB
Python

"""Server→client requests: the backend asks the renderer a question (``server_requests.send``).
Every entry is one request method: the ``params`` the frame carries (``session_id`` is added by
the transport and declared on the shared base) and the ``result`` the client answers with. The
``request.cancel`` event that withdraws an open request lives here too.
"""
from __future__ import annotations
from pydantic import Field
from .base import JsonValue, Params, Payload, Result, WireEnum
from .registry import event, server_request
class ServerRequestParams(Params):
"""Every request frame's params start with the session the question belongs to."""
session_id: str
class ValueResult(Result):
"""The answer to any one-string prompt (sudo, secret, vault prompts, desktop bridges):
``''`` means skipped / declined."""
value: str
# ── clarify ───────────────────────────────────────────────────────────────────────────────────
class ClarifyQuestion(Params):
qid: str
question: str
choices: list[str] | None = None
multi_select: bool = False
class ClarifyRequestParams(ServerRequestParams):
"""Single question: ``question`` / ``choices`` (/ ``multi_select``); batch: ``questions``.
``answers`` rides only on a reconnect replay (locks the server already accepted)."""
question: str | None = None
choices: list[str] | None = None
multi_select: bool | None = None
questions: list[ClarifyQuestion] | None = None
answers: dict[str, str] | None = None
class ClarifyResult(Result):
"""Single: ``{answer}`` ('' = skip). Batch: ``{answers}`` for the whole set (early locks go through
the ``clarify.lock`` RPC); a response with neither is cancel-all."""
answer: str | None = None
answers: dict[str, str] | None = None
server_request("clarify", params=ClarifyRequestParams, result=ClarifyResult,
doc="The clarify tool: ask the user one question or a batch.")
# ── approval ──────────────────────────────────────────────────────────────────────────────────
class ApprovalChoice(WireEnum):
once = "once"
session = "session"
always = "always"
deny = "deny"
class ApprovalRequestParams(ServerRequestParams):
"""``tui_gateway/server.py::_approval_request_payload`` — the command is redacted server-side."""
request_id: str
command: str = ""
description: str = ""
choices: list[ApprovalChoice] = Field(default_factory=list)
allow_permanent: bool | None = None
allow_session: bool | None = None
smart_denied: bool | None = None
tool_name: str | None = None
session_id_hint: str | None = Field(default=None, alias="gateway_session_id")
# The approval queue entry carries tool-specific context the card may render; the closed set
# of keys is owned by tools/approval.py, so it stays open here.
model_config = Params.model_config | {"extra": "allow"}
class ApprovalResult(Result):
choice: ApprovalChoice
all: bool | None = None
server_request("approval", params=ApprovalRequestParams, result=ApprovalResult,
doc="A dangerous command awaits the user's decision.")
# ── one-string prompts ────────────────────────────────────────────────────────────────────────
class EmptyRequestParams(ServerRequestParams):
pass
class SudoRequestParams(ServerRequestParams):
"""Original command, redacted server-side before any password-injection rewrite."""
command: str = ""
server_request("sudo", params=SudoRequestParams, result=ValueResult,
doc="Masked sudo password for the terminal tool.")
class SecretRequestParams(ServerRequestParams):
env_var: str
prompt: str
metadata: dict[str, JsonValue] | None = None
server_request("secret", params=SecretRequestParams, result=ValueResult,
doc="Masked value for a named env var (skills / setup flows).")
class VaultUnlockRequestParams(ServerRequestParams):
backend: str
display_name: str
server_request("vault.unlock_prompt", params=VaultUnlockRequestParams, result=ValueResult,
doc="Master password to unlock an external password manager for this session.")
class VaultSaveLoginRequestParams(ServerRequestParams):
origin: str
site: str
server_request("vault.save_login", params=VaultSaveLoginRequestParams, result=ValueResult,
doc="Save a login for a site the agent is about to fill; the answer is JSON {identifier, password}.")
class VaultCodeRequestParams(ServerRequestParams):
site: str | None = None
hint: str | None = None
server_request("vault.code", params=VaultCodeRequestParams, result=ValueResult,
doc="A one-time / 2FA code the user reads from their device.")
# ── desktop GUI bridges ───────────────────────────────────────────────────────────────────────
class ReadRangeRequestParams(ServerRequestParams):
start: int | None = None
count: int | None = None
server_request("terminal.read", params=ReadRangeRequestParams, result=ValueResult,
doc="Read the visible in-app terminal buffer (JSON text answer).")
server_request("preview.read", params=ReadRangeRequestParams, result=ValueResult,
doc="Read the in-app browser preview's text (JSON text answer).")
server_request("window.read", params=EmptyRequestParams, result=ValueResult,
doc="Enumerate the native window below the app (JSON text answer).")
class PreviewActRequestParams(ServerRequestParams):
"""``tools/drive_preview_tool.py`` and ``tools/annotate_preview_tool.py`` field sets."""
action: str
ref: str | None = None
selector: str | None = None
text: str | None = None
key: str | None = None
submit: bool | None = None
full: bool | None = None
to: str | None = None
amount: int | None = None
max: int | None = None
allow_shortcut: bool | None = None
server_request("preview.act", params=PreviewActRequestParams, result=ValueResult,
doc="Click / type / scroll / annotate inside the in-app browser preview.")
class TourStep(Params):
selector: str | None = None
title: str | None = None
text: str | None = None
side: str | None = None
model_config = Params.model_config | {"extra": "allow"}
class TourRequestParams(ServerRequestParams):
"""``tools/tour_tool.py`` field set."""
action: str
surface: str | None = None
selector: str | None = None
title: str | None = None
text: str | None = None
side: str | None = None
steps: list[TourStep] | None = None
step_index: int | None = None
server_request("tour", params=TourRequestParams, result=ValueResult,
doc="Drive a guided tour highlight in the desktop renderer.")
# ── withdrawal ────────────────────────────────────────────────────────────────────────────────
class RequestCancelReason(WireEnum):
timeout = "timeout"
interrupted = "interrupted"
shutdown = "shutdown"
resolved = "resolved"
session_closed = "session_closed"
class RequestCancelPayload(Payload):
id: str
method: str
reason: str # a RequestCancelReason value; callers in tools/approval may pass their own wording
event("request.cancel", RequestCancelPayload,
doc="The backend withdrew an open server→client request; clear the matching card only.")