Add Remote controller capture IPC to the host and both SDKs

remote.focus lets a controller keyboard-capture lease ask core to enter
or leave remote focus; the reply acknowledges admission only. Core
reports the actual state with remote.focus events and forwards routed
transitions as remote.input events carrying the source keycode and, on
fresh presses, a canonical key name. A full focus mailbox revokes the
lease; revocation drains pending focus work. No native capture owner is
attached yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chase CichorzandClaude Opus 5.5 committed 2026-11-06 16:06:38 -05:00
1 parent 27017bef11
commit 2d5327adf4
10 files changed
+583 -10

No files matched your search

+44 -1
View File
@@ -141,6 +141,34 @@ def valid_remote_key_request(request):
return False
def valid_remote_captured_key(key):
"""Validate one controller capture transition from core, without desktop access."""
try:
_fields(key, ("code", "pressed"), ("name", "repeat"))
code, pressed = key["code"], key["pressed"]
name, repeat = key.get("name", ""), key.get("repeat", False)
if type(code) is not int or not 8 <= code <= 255 or type(pressed) is not bool or type(repeat) is not bool:
return False
if pressed and not repeat:
return valid_remote_key({"name": name, "pressed": True})
return name == "" and (pressed or not repeat)
except (ProtocolError, TypeError, ValueError):
return False
def valid_remote_focus_request(request):
"""Validate remote.focus parameters; capture remains owned by core."""
try:
_fields(request, ("lease", "epoch", "sequence", "remote"))
return (_token(request["lease"]) and _positive(request["epoch"])
and _positive(request["sequence"]) and type(request["remote"]) is bool)
except (ProtocolError, TypeError, ValueError, UnicodeError):
return False
REMOTE_FOCUS_REASONS = frozenset(("requested", "escape", "local", "unavailable"))
class SessionError(Exception):
def __init__(self, code, message):
super().__init__("Tentacle session error " + str(code))
@@ -330,9 +358,12 @@ class SessionClient:
waiter.put_nowait(value)
def _event(self, event):
_fields(event, ("sequence", "kind", "epoch"), ("action", "lease", "role", "grants", "reason", "speech"))
_fields(event, ("sequence", "kind", "epoch"), ("action", "lease", "role", "grants", "reason", "speech", "input", "focus"))
if not _positive(event["sequence"]) or not _positive(event["epoch"]):
raise ProtocolError("Invalid event sequence")
if (event["kind"] != "remote.input" and "input" in event
or event["kind"] != "remote.focus" and event.get("focus", "") != ""):
raise ProtocolError("Invalid session event")
if event["kind"] == "remote.action":
grants = event.get("grants")
role = event.get("role")
@@ -349,6 +380,18 @@ class SessionClient:
if ("speech" in event or not identity or event.get("role", "") != ""
or event.get("grants", []) != [] or event.get("reason") not in ("closed", "expired", "local", "replaced")):
raise ProtocolError("Invalid revoke event")
elif event["kind"] == "remote.input":
if (not _token(event.get("lease")) or event.get("action", "") != ""
or event.get("role", "") != "" or event.get("grants", []) != []
or event.get("reason", "") != "" or "speech" in event
or not valid_remote_captured_key(event.get("input"))):
raise ProtocolError("Invalid input event")
elif event["kind"] == "remote.focus":
if (not _token(event.get("lease")) or event.get("action", "") != ""
or event.get("role", "") != "" or event.get("grants", []) != []
or "speech" in event or event.get("focus") not in ("local", "remote")
or event.get("reason") not in REMOTE_FOCUS_REASONS):
raise ProtocolError("Invalid focus event")
elif event["kind"] == "remote.output":
if (not _token(event.get("lease")) or event.get("action", "") != ""
or event.get("role", "") != "" or event.get("grants", []) != []