Fix private production pilot recovery, screen transfer and MCP dispatch

This commit is contained in:
Gerardo Betancourt committed 2026-10-27 23:54:41 -04:00
1 parent f6b14c5210
commit db5f85c922
18 files changed
+1735 -66

No files matched your search

@@ -32,7 +32,7 @@ import zlib
MAX_PIXELS = 4096 * 2160
MAX_FRAMEBUFFER_BYTES = 128 * 1024 * 1024
MAX_IMAGE_BYTES = 32 * 1024 * 1024
MAX_CHUNK_BYTES = 2400
MAX_CHUNK_BYTES = 45000
CAPTURE_ID = re.compile(r"[0-9a-f]{32}\Z")
SESSION_ID = re.compile(r"[A-Za-z0-9_-]{1,64}\Z")
DISPLAY_ID = re.compile(r":[0-9]{1,5}(?:\.[0-9]{1,3})?\Z")
@@ -13,12 +13,17 @@ import math
import re
import shlex
import struct
import time
import uuid
from typing import Callable
CHUNK_BYTES = 2100
MAX_IMAGE_BYTES = 32 * 1024 * 1024
MAX_STDOUT_BYTES = 4096
# Only the explicit typed screen transport preserves this larger stdout budget.
EXPANDED_STDOUT_BYTES = 65536
EXPANDED_CHUNK_BYTES = 45000
TRANSFER_TIMEOUT_SECONDS = 45
RPC_COMMAND = '/usr/bin/python3 -B -m framebridge_expanded.real_screen.screen_rpc '
HEX_ID = re.compile(r"[0-9a-f]{32}\Z")
SHA = re.compile(r"[0-9a-f]{64}\Z")
@@ -28,14 +33,25 @@ ERROR = re.compile(r"[A-Z0-9_]{1,100}\Z")
READ = {"readOnlyHint": True, "destructiveHint": False,
"idempotentHint": False, "openWorldHint": False}
CHANGE = {**READ, "readOnlyHint": False}
# These explicit backend checks run before an input capture is consumed. Generic
# dispatch failures do not prove whether input was sent, even with valid framing.
INPUT_REJECTION_CODES = {
"REQUEST_INVALID", "OPERATION_INVALID", "UNKNOWN_REQUEST_FIELDS",
"SESSION_ID_INVALID", "KEY_INVALID", "TEXT_INVALID", "CAPTURE_ID_INVALID",
"CAPTURE_RECORD_INVALID", "CAPTURE_RECORD_MISMATCH",
"USE_GRAPHICAL_CUA_FOR_X11_INPUT", "VISIBLE_INPUT_UNAVAILABLE",
"CAPTURE_ALREADY_USED_OBSERVE_AGAIN", "SESSION_NOT_VISIBLE",
}
class ScreenError(RuntimeError):
"""A bounded public error suitable for the MCP response."""
def __init__(self, code, details=None):
def __init__(self, code, details=None, *, backend_rejection=False):
super().__init__(code)
self.details = details or {}
# Local proof, never accepted from a remote JSON field.
self.backend_rejection = backend_rejection
def require(ok, code):
@@ -62,7 +78,7 @@ class _AuthenticatedReply(dict):
_IDENTITY_FIELDS = ("capture_id", "node", "sha256", "session_id", "bytes",
"width", "height", "source", "screen_type",
"full_screen", "authentic_pixels")
"full_screen", "authentic_pixels", "max_chunk_bytes")
def __init__(self, payload, owner, node, node_id):
super().__init__(payload)
@@ -80,23 +96,51 @@ class _AuthenticatedReply(dict):
class ScreenClient:
"""Transport only; execution policy and node resolution stay in the host."""
"""Transport only; execution policy and node resolution stay in the host.
The monotonic transfer deadline bounds admission of RPCs and successful
publication. It is cooperative: the executor can spend its existing bounded
transport grace on the final in-flight RPC. Late replies never become a
successful image, and no further chunks are requested after the deadline.
"""
def __init__(self, exec_request: Callable, image_factory: Callable,
resolve_node: Callable | None = None):
resolve_node: Callable | None = None, *,
stdout_limit_bytes: int = MAX_STDOUT_BYTES,
transfer_timeout_s: int = TRANSFER_TIMEOUT_SECONDS):
if (type(stdout_limit_bytes) is not int or
stdout_limit_bytes not in (MAX_STDOUT_BYTES, EXPANDED_STDOUT_BYTES)):
raise ValueError("unsupported screen stdout budget")
if type(transfer_timeout_s) is not int or not 1 <= transfer_timeout_s <= 120:
raise ValueError("screen transfer timeout must be 1..120 seconds")
self.stdout_limit_bytes = stdout_limit_bytes
self.chunk_bytes = (EXPANDED_CHUNK_BYTES
if stdout_limit_bytes == EXPANDED_STDOUT_BYTES else CHUNK_BYTES)
self.transfer_timeout_s = transfer_timeout_s
self.exec_request = exec_request
self.image_factory = image_factory
self.resolve_node = resolve_node or (lambda node: node.upper())
self._reply_owner = object()
def remote(self, node, request):
def remote(self, node, request, *, deadline=None, submitted=None):
timeout_s = 30
if deadline is not None:
remaining = deadline - time.monotonic()
# The authenticated EXEC protocol accepts whole seconds only. Rounding
# down prevents any individual request exceeding the remaining budget.
require(remaining >= 1, "SCREEN_TRANSFER_TIMEOUT")
timeout_s = min(timeout_s, int(remaining))
payload = base64.b64encode(encode(request)).decode("ascii")
script = RPC_COMMAND + shlex.quote(payload) + "\n"
digest = hashlib.sha256(script.encode("utf-8")).hexdigest()
job_id = "cua-screen-" + uuid.uuid4().hex
outer = self.exec_request(script=script, authorized=True, timeout_s=30,
if submitted is not None:
submitted["attempted"] = True
outer = self.exec_request(script=script, authorized=True, timeout_s=timeout_s,
job_id=job_id, sha256=digest,
include_stdout=True, target=node)
if deadline is not None:
require(time.monotonic() < deadline, "SCREEN_TRANSFER_TIMEOUT")
require(type(outer) is dict, "SCREEN_EXEC_FAILED")
require(type(outer.get("exit_code")) in (int, str) and
str(outer["exit_code"]) in ("0", "1"), "SCREEN_EXEC_FAILED")
@@ -109,7 +153,7 @@ class ScreenClient:
except (ValueError, AttributeError):
raise ScreenError("SCREEN_EXEC_NODE_ID_INVALID") from None
stdout = outer.get("stdout")
require(type(stdout) is str and len(stdout.encode("utf-8")) <= MAX_STDOUT_BYTES and
require(type(stdout) is str and len(stdout.encode("utf-8")) <= self.stdout_limit_bytes and
outer.get("stdout_truncated") is not True, "SCREEN_STDOUT_INVALID")
try:
envelope = json.loads(stdout)
@@ -129,12 +173,19 @@ class ScreenClient:
if valid_reference(code.get("cause"), ERROR):
details["cause"] = code["cause"]
code = code.get("code")
raise ScreenError(code if valid_reference(code, ERROR) else "SCREEN_BACKEND_FAILED", details)
valid_code = valid_reference(code, ERROR)
raise ScreenError(code if valid_code else "SCREEN_BACKEND_FAILED", details,
backend_rejection=(valid_code and outer.get("ok") is False
and str(outer["exit_code"]) == "1"
and (code in INPUT_REJECTION_CODES
or details.get("performed") is False)))
require(outer.get("ok") is True and str(outer["exit_code"]) == "0", "SCREEN_EXEC_FAILED")
require(type(envelope.get("result")) is dict, "SCREEN_RESPONSE_INVALID")
return _AuthenticatedReply(envelope["result"], self._reply_owner, node, node_id)
def image(self, node, manifest):
def image(self, node, manifest, *, deadline=None):
if deadline is None:
deadline = time.monotonic() + self.transfer_timeout_s
capture_id = manifest.get("capture_id")
total, digest = manifest.get("bytes"), manifest.get("sha256")
require(valid_reference(capture_id, HEX_ID) and valid_reference(digest, SHA),
@@ -151,11 +202,23 @@ class ScreenClient:
"SCREEN_PIXEL_SOURCE_INVALID")
require(manifest.get("full_screen") is True and manifest.get("authentic_pixels") is True,
"SCREEN_PIXEL_PROVENANCE_INVALID")
# Capability belongs to this authenticated manifest, never a shared cache.
# A new central must retain the old request schema for a frozen receiver.
chunk_bytes = CHUNK_BYTES
if "max_chunk_bytes" in manifest:
capability = manifest["max_chunk_bytes"]
require(type(capability) is int and 1 <= capability <= EXPANDED_CHUNK_BYTES,
"SCREEN_TRANSFER_CAPABILITY_INVALID")
chunk_bytes = min(self.chunk_bytes, capability)
parts = []
count = math.ceil(total / CHUNK_BYTES)
count = math.ceil(total / chunk_bytes)
for index in range(count):
part = self.remote(node, {"op": "read_chunk", "capture_id": capture_id,
"index": index, "expected_sha256": digest})
request = {"op": "read_chunk", "capture_id": capture_id,
"index": index, "expected_sha256": digest}
# Omit the extension entirely on constrained legacy transports.
if chunk_bytes != CHUNK_BYTES:
request["chunk_bytes"] = chunk_bytes
part = self.remote(node, request, deadline=deadline)
require(isinstance(part, _AuthenticatedReply) and
part.bound_to(self._reply_owner, node, manifest.node_id),
"SCREEN_CHUNK_NODE_MISMATCH")
@@ -167,12 +230,12 @@ class ScreenClient:
type(part.get("more")) is bool and part["more"] == (index + 1 < count),
"SCREEN_CHUNK_MANIFEST_INVALID")
text = part.get("data")
require(type(text) is str and len(text) <= 2800, "SCREEN_CHUNK_INVALID")
require(type(text) is str and len(text) <= 4 * math.ceil(chunk_bytes / 3), "SCREEN_CHUNK_INVALID")
try:
data = base64.b64decode(text, validate=True)
except (ValueError, UnicodeError, binascii.Error):
raise ScreenError("SCREEN_CHUNK_INVALID") from None
require(len(data) == min(CHUNK_BYTES, total - index * CHUNK_BYTES),
require(len(data) == min(chunk_bytes, total - index * chunk_bytes),
"SCREEN_CHUNK_LENGTH_INVALID")
parts.append(data)
data = b"".join(parts)
@@ -181,10 +244,15 @@ class ScreenClient:
require(len(data) >= 33 and data[:8] == b"\x89PNG\r\n\x1a\n" and
data[12:16] == b"IHDR" and struct.unpack(">II", data[16:24]) == (width, height),
"SCREEN_PNG_INVALID")
require(time.monotonic() < deadline, "SCREEN_TRANSFER_TIMEOUT")
return self.image_factory(data=data, format="png")
def call(self, op, node, authorized, **kwargs):
requested_node = node
action_confirmed = False
input_reply_received = False
submitted = {}
deadline = time.monotonic() + self.transfer_timeout_s
try:
require(authorized is True, "AUTHORIZED_TRUE_REQUIRED")
require(valid_reference(node, NODE), "SCREEN_NODE_INVALID")
@@ -202,11 +270,20 @@ class ScreenClient:
if op == "text":
require(type(kwargs.get("text")) is str and 0 < len(kwargs["text"].encode("utf-8")) <= 4096,
"SCREEN_TEXT_INVALID")
manifest = self.remote(node, {"op": op, **kwargs})
manifest = self.remote(node, {"op": op, **kwargs}, deadline=deadline,
submitted=submitted)
input_reply_received = op in ("key", "text")
action = manifest.get("action")
summary = action.get("result") if type(action) is dict else None
action_confirmed = (op in ("key", "text") and type(action) is dict and
action.get("kind") == op and
action.get("previous_capture_id") == kwargs["capture_id"] and
type(summary) is dict and summary.get("performed") is True)
if op == "read_capture":
require(manifest.get("capture_id") == kwargs["capture_id"],
"SCREEN_CAPTURE_REFERENCE_MISMATCH")
image = self.image(node, manifest)
image = self.image(node, manifest, deadline=deadline)
require(time.monotonic() < deadline, "SCREEN_TRANSFER_TIMEOUT")
metadata = {"ok": True, "node": node, "requested_node": requested_node,
"node_id": manifest.node_id, "hostname": manifest["node"],
"result": dict(manifest, transport_node=node, node_id=manifest.node_id)}
@@ -217,9 +294,21 @@ class ScreenClient:
metadata["result"]["authentic_pixels"] = True
return [metadata, image]
except ScreenError as exc:
if op in ("key", "text") and (input_reply_received or
(submitted.get("attempted") and not exc.backend_rejection)):
exc.details.setdefault("observe_again", True)
if action_confirmed:
exc.details.update(performed=True, capture_consumed=True)
exc.details.setdefault("capture_id", kwargs["capture_id"])
return {"ok": False, "node": requested_node, "error": str(exc), **exc.details}
except Exception:
return {"ok": False, "node": requested_node, "error": "SCREEN_BACKEND_UNAVAILABLE"}
details = {}
if op in ("key", "text") and submitted.get("attempted"):
details.update(observe_again=True, capture_id=kwargs["capture_id"])
if action_confirmed:
details.update(performed=True, capture_consumed=True)
return {"ok": False, "node": requested_node,
"error": "SCREEN_BACKEND_UNAVAILABLE", **details}
def resolve_profile_node(alias):
@@ -233,12 +322,16 @@ def resolve_profile_node(alias):
def register_framebridge_screen_tools(mcp, exec_request, *, image_factory=None,
resolve_node=None):
resolve_node=None,
stdout_limit_bytes=MAX_STDOUT_BYTES,
transfer_timeout_s=TRANSFER_TIMEOUT_SECONDS):
"""Register real image tools. The host supplies its existing executor."""
if image_factory is None:
from mcp.server.fastmcp import Image
image_factory = Image
client = ScreenClient(exec_request, image_factory, resolve_node)
client = ScreenClient(exec_request, image_factory, resolve_node,
stdout_limit_bytes=stdout_limit_bytes,
transfer_timeout_s=transfer_timeout_s)
@mcp.tool(annotations=dict(READ))
def framebridge_screen_observe(node: str, authorized: bool,
@@ -13,7 +13,7 @@ import stat
import sys
import time
from .screen_capture import CaptureError, capture_screen, read_chunk
from .screen_capture import CaptureError, capture_screen, read_chunk, MAX_CHUNK_BYTES
from .screen_control import ControlError, act, bind_screen, snapshot_binding
MAX_REQUEST = 16384
@@ -89,7 +89,10 @@ def write_record(path: Path, value, *, replace=False):
def public_manifest(manifest):
# Internal path is unnecessary to use or understand a captured screen.
return {k: v for k, v in manifest.items() if k not in {'path', '_action_consumed'}}
result = {k: v for k, v in manifest.items() if k not in {'path', '_action_consumed'}}
# An old receiver omits this capability and only accepts the original schema.
result['max_chunk_bytes'] = MAX_CHUNK_BYTES
return result
def observe(directory: Path, requested_session_id=None):
@@ -138,7 +141,7 @@ def validate_request(request):
allowed = {
'observe': {'op', 'session_id'},
'status': {'op'},
'read_chunk': {'op', 'capture_id', 'index', 'expected_sha256'},
'read_chunk': {'op', 'capture_id', 'index', 'expected_sha256', 'chunk_bytes'},
'read_capture': {'op', 'capture_id'},
'key': {'op', 'capture_id', 'session_id', 'key'},
'text': {'op', 'capture_id', 'session_id', 'text'},
@@ -149,6 +152,9 @@ def validate_request(request):
sid = request['session_id']
require(type(sid) is str and len(sid) <= 96 and re.fullmatch(r'[A-Za-z0-9_-]+', sid), 'SESSION_ID_INVALID')
if op == 'read_chunk':
if 'chunk_bytes' in request:
require(type(request['chunk_bytes']) is int and
1 <= request['chunk_bytes'] <= MAX_CHUNK_BYTES, 'CHUNK_BOUNDS_INVALID')
require(type(request.get('index')) is int and request['index'] >= 0, 'CHUNK_INDEX_INVALID')
require(type(request.get('expected_sha256')) is str and HEX_SHA.fullmatch(request['expected_sha256']), 'EXPECTED_SHA256_REQUIRED')
if op == 'key':
@@ -179,7 +185,7 @@ def dispatch(request, directory=None):
if op == 'read_chunk':
require(manifest['sha256'] == request['expected_sha256'], 'CAPTURE_SHA256_MISMATCH')
chunk = read_chunk(directory, manifest['capture_id'], request['index'],
chunk_bytes=2100, expected_sha256=request['expected_sha256'])
chunk_bytes=request.get('chunk_bytes', 2100), expected_sha256=request['expected_sha256'])
return {'ok': True, 'result': chunk}
if manifest.get('screen_type') == 'x11':
raise ScreenRpcError('USE_GRAPHICAL_CUA_FOR_X11_INPUT')
@@ -17,7 +17,10 @@ import signal
import subprocess
import time
from .screen_frontdoor import RPC_COMMAND, MAX_STDOUT_BYTES
from .screen_frontdoor import RPC_COMMAND, EXPANDED_STDOUT_BYTES
# This typed route bypasses the legacy 4096-byte administrative sanitizer.
MAX_STDOUT_BYTES = EXPANDED_STDOUT_BYTES
from .screen_rpc import MAX_REQUEST, encode, validate_request, ScreenRpcError
REQUEST_TYPE = "screen_rpc"