Add Remote session foundations and feasibility checks

Add opt-in Tentacle protocol 1.1, explicit remote grants, lifecycle IPC and expiring core leases in both SDKs. Revoke access before update preparation, including failed updates.

Add transient secret entries, shared Braille cell export and isolated Xorg recovery experiments with focused tests and documented limitations. Production consent, desktop services and live interoperability remain pending.
This commit is contained in:
ak47 committed 2026-11-03 22:33:26 -05:00
1 parent a47253d697
commit 573d15d7b4
45 files changed
+3634 -80

No files matched your search

+256
View File
@@ -0,0 +1,256 @@
# Copyright (c) 2026 Storm Dragon
# SPDX-License-Identifier: GPL-3.0-or-later
"""Opt-in protocol 1.1 lifecycle transport. No desktop effects are provided."""
import os
import queue
import select
import threading
from . import MAX_FRAME, ProtocolError, _encode, _fields
REMOTE_GRANTS = frozenset(("output-observe", "remote-present", "keyboard-capture",
"keyboard-inject", "braille-stream", "clipboard-exchange"))
BASE_APIS = frozenset(("commands", "presentation", "interaction", "reader-settings"))
def valid_negotiation(apis, capabilities):
if (not isinstance(apis, list) or not isinstance(capabilities, list)
or any(not isinstance(value, str) for value in apis + capabilities)):
return False
a, c = set(apis), set(capabilities)
return (len(a) == len(apis) and len(c) == len(capabilities)
and {"commands", "session-events"} <= a
and a <= BASE_APIS | {"session-events", "remote-access"}
and c <= (BASE_APIS - {"commands"}) | REMOTE_GRANTS
and a & (BASE_APIS - {"commands"}) == c & (BASE_APIS - {"commands"})
and ("interaction" not in a or "presentation" in a)
and ("reader-settings" not in a or "interaction" in a)
and ("remote-access" in a) == bool(c & REMOTE_GRANTS))
def _positive(value):
return type(value) is int and 0 < value < 2**64
def _token(value):
return isinstance(value, str) and len(value.encode("utf-8")) == 32
class SessionError(Exception):
def __init__(self, code, message):
super().__init__("Tentacle session error " + str(code))
self.code, self.message = code, message
class SessionClient:
"""Single-use client passed as session_client= to serve.
Call from handlers or owned background workers, never from the receive loop.
next_event blocks until an event or session shutdown. Call timeout retires
the session; the host revokes its lease. Binary streams must have pipe FDs.
"""
def __init__(self):
self._lock = threading.Lock()
self._events = queue.Queue(32)
self._outbound = queue.Queue(32)
self._pending = {}
self._bytes = self._sequence = self._event_sequence = 0
self._started = self._active = False
self._stopped = threading.Event()
self._wake_read = self._wake_write = None
self._buffer = bytearray()
def _start(self, cancelled, input_stream, output_stream):
with self._lock:
if self._started:
raise ProtocolError("Session client already used")
self._started = True
self._cancelled = cancelled
self._input, self._output = input_stream.fileno(), output_stream.fileno()
self._wake_read, self._wake_write = os.pipe()
os.set_blocking(self._wake_write, False)
os.set_blocking(self._input, False)
os.set_blocking(self._output, False)
self._writer = threading.Thread(target=self._write_loop)
self._writer.start()
def _abort(self):
# Called under _lock too; never acquire it here.
self._stopped.set()
self._cancelled.set()
if self._wake_write is not None:
try:
os.write(self._wake_write, b"x")
except OSError:
pass
try:
self._outbound.put_nowait(None)
except queue.Full:
pass
def _finish(self):
self._abort()
self._writer.join()
with self._lock:
self._active = False
for waiter in self._pending.values():
waiter.put_nowait(SessionError(-32000, "Session closed"))
self._pending.clear()
while not self._events.empty():
self._events.get_nowait()
self._events.put_nowait(None)
def _close(self):
os.close(self._wake_read)
os.close(self._wake_write)
self._wake_read = self._wake_write = None
def _readline(self):
while not self._stopped.is_set():
end = self._buffer.find(b"\n")
if end >= 0:
frame = bytes(self._buffer[:end + 1])
del self._buffer[:end + 1]
if len(frame) > MAX_FRAME + 1:
raise ProtocolError("Frame limit")
return frame
if len(self._buffer) > MAX_FRAME:
raise ProtocolError("Frame limit")
ready, _, _ = select.select([self._input, self._wake_read], [], [])
if self._wake_read in ready:
return b""
try:
data = os.read(self._input, 4096)
except BlockingIOError:
continue
if not data:
if self._buffer:
raise ProtocolError("Unterminated frame")
return b""
self._buffer.extend(data)
return b""
def _enqueue_locked(self, frame):
if self._stopped.is_set():
raise SessionError(-32000, "Session closed")
if self._bytes + len(frame) > 256 * 1024:
self._abort()
raise ProtocolError("Session queue limit")
try:
self._outbound.put_nowait(frame)
except queue.Full:
self._abort()
raise ProtocolError("Session queue limit") from None
self._bytes += len(frame)
def _enqueue(self, frame):
with self._lock:
self._enqueue_locked(frame)
def _write_loop(self):
try:
while not self._stopped.is_set():
frame = self._outbound.get()
if frame is None:
return
position = 0
while position < len(frame) and not self._stopped.is_set():
ready, writable, _ = select.select([self._wake_read], [self._output], [])
if ready:
return
if writable:
try:
position += os.write(self._output, frame[position:])
except BlockingIOError:
continue
with self._lock:
self._bytes -= len(frame)
except OSError:
self._abort()
def call(self, method, params, timeout=2):
if not isinstance(method, str) or not 1 <= len(method.encode("utf-8")) <= 128 or len(_encode(params)) > 8192:
raise ProtocolError("Invalid session request")
if not 0 < timeout <= 2:
raise ValueError("Session call timeout must be in (0, 2]")
with self._lock:
if not self._active or self._stopped.is_set():
raise SessionError(-32000, "Session inactive")
if len(self._pending) >= 8:
raise ProtocolError("Pending call limit")
self._sequence += 1
request_id = "c:" + str(self._sequence)
waiter = queue.Queue(1)
self._pending[request_id] = waiter
try:
self._enqueue_locked(_encode({"jsonrpc": "2.0", "id": request_id,
"method": method, "params": params}) + b"\n")
except BaseException:
del self._pending[request_id]
raise
try:
value = waiter.get(timeout=timeout)
if isinstance(value, SessionError):
raise value
if self._stopped.is_set():
raise SessionError(-32000, "Session closed")
return value
except queue.Empty:
self._abort()
raise TimeoutError("Session call expired") from None
finally:
with self._lock:
self._pending.pop(request_id, None)
def next_event(self, timeout=None):
if self._stopped.is_set():
return None
return self._events.get(timeout=timeout)
def _reply(self, message):
_fields(message, ("jsonrpc", "id"), ("result", "error"))
if (message["jsonrpc"] != "2.0" or not isinstance(message["id"], str)
or ("result" in message) == ("error" in message)):
raise ProtocolError("Invalid session reply")
value = message.get("result")
if "error" in message:
error = message["error"]
_fields(error, ("code", "message"))
if type(error["code"]) is not int or not isinstance(error["message"], str) or len(error["message"].encode("utf-8")) > 1024:
raise ProtocolError("Invalid session error")
value = SessionError(error["code"], error["message"])
with self._lock:
waiter = self._pending.pop(message["id"], None)
if not self._active or waiter is None:
raise ProtocolError("Unexpected session reply")
waiter.put_nowait(value)
def _event(self, event):
_fields(event, ("sequence", "kind", "epoch"), ("action", "lease", "role", "grants", "reason"))
if not _positive(event["sequence"]) or not _positive(event["epoch"]):
raise ProtocolError("Invalid event sequence")
if event["kind"] == "remote.action":
grants = event.get("grants")
role = event.get("role")
if (not _token(event.get("action")) or event.get("lease", "") != "" or event.get("reason", "") != ""
or role not in ("controller", "controlled") or not isinstance(grants, list)
or not grants or any(not isinstance(grant, str) for grant in grants)
or len(set(grants)) != len(grants) or not set(grants) <= REMOTE_GRANTS
or role == "controller" and set(grants) & {"output-observe", "keyboard-inject"}
or role == "controlled" and set(grants) & {"remote-present", "keyboard-capture"}):
raise ProtocolError("Invalid action event")
elif event["kind"] == "remote.revoked":
if (not _token(event.get("lease")) or event.get("action", "") != "" or event.get("role", "") != ""
or event.get("grants", []) != [] or event.get("reason") not in ("closed", "expired", "local", "replaced")):
raise ProtocolError("Invalid revoke event")
else:
raise ProtocolError("Unknown session event")
with self._lock:
if not self._active or event["sequence"] != self._event_sequence + 1:
raise ProtocolError("Out-of-order session event")
self._event_sequence = event["sequence"]
try:
self._events.put_nowait(event)
except queue.Full:
self._abort()
raise ProtocolError("Event queue limit") from None