Fix private production pilot recovery, screen transfer and MCP dispatch
This commit is contained in:
1 parent
f6b14c5210
commit
db5f85c922
18 files changed
+1735
-66
No files matched your search
@@ -0,0 +1,316 @@
|
||||
"""Portable registration integration through the bundled FastMCP dispatcher.
|
||||
|
||||
Executors are bounded fixtures. No installed adapter, desktop or Fleet node is
|
||||
contacted; these checks establish dispatch and identity wiring, not acceptance.
|
||||
"""
|
||||
import ast
|
||||
import asyncio
|
||||
import base64
|
||||
from functools import partial, wraps
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from unittest import mock
|
||||
import uuid
|
||||
|
||||
WORK = Path(__file__).resolve().parents[1]
|
||||
VENDOR = WORK / "sources/original-geranas/FrameBridge-Fleet-Hub/vendor"
|
||||
sys.path.insert(0, str(WORK / "expanded"))
|
||||
sys.path.insert(0, str(VENDOR))
|
||||
import anyio
|
||||
from mcp.server.fastmcp import FastMCP, Image
|
||||
from mcp.types import ToolAnnotations
|
||||
from framebridge_expanded import tty_mcp
|
||||
from framebridge_expanded.real_screen.screen_frontdoor import register_framebridge_screen_tools
|
||||
from framebridge_expanded.real_screen.screen_presentation import register_screen_presentation
|
||||
from test_screen_frontdoor import Executor, NODE_ID, SESSION_ID
|
||||
|
||||
PORTABLE = WORK / "portable/front_door_mcp.py"
|
||||
|
||||
|
||||
class ChunkExecutor(Executor):
|
||||
"""Honor the typed route's optional larger chunk while retaining bindings."""
|
||||
def __call__(self, **kwargs):
|
||||
outer = super().__call__(**kwargs)
|
||||
request = json.loads(base64.b64decode(shlex.split(kwargs["script"])[-1]))
|
||||
if request["op"] == "read_chunk" and request.get("chunk_bytes"):
|
||||
size, index = request["chunk_bytes"], request["index"]
|
||||
result = json.loads(outer["stdout"])
|
||||
result["result"].update(
|
||||
total_chunks=math.ceil(len(self.data) / size),
|
||||
data=base64.b64encode(self.data[index * size:(index + 1) * size]).decode(),
|
||||
more=(index + 1) * size < len(self.data),
|
||||
)
|
||||
outer["stdout"] = json.dumps(result)
|
||||
return outer
|
||||
|
||||
|
||||
def portable(*, execute=None, screen_execute=None, threaded=True):
|
||||
"""Load the actual registration statements without bootstrap/profile effects."""
|
||||
tree = ast.parse(PORTABLE.read_text())
|
||||
names = {"_BlockingToolRegistrar", "_registered_operations", "framebridge_status",
|
||||
"framebridge_exec", "_screen_exec_request"}
|
||||
assignments = {"_blocking_tools", "_real_screen_client"}
|
||||
registrations = {"register_screen_presentation", "register_tty_tools"}
|
||||
selected = []
|
||||
for node in tree.body:
|
||||
if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names:
|
||||
selected.append(node)
|
||||
elif isinstance(node, ast.Assign) and any(
|
||||
isinstance(target, ast.Name) and target.id in assignments for target in node.targets):
|
||||
if not threaded and node.targets[0].id == "_blocking_tools":
|
||||
node.value = ast.Name(id="mcp", ctx=ast.Load())
|
||||
selected.append(node)
|
||||
elif (isinstance(node, ast.Expr) and isinstance(node.value, ast.Call)
|
||||
and isinstance(node.value.func, ast.Name) and node.value.func.id in registrations):
|
||||
selected.append(node)
|
||||
mcp = FastMCP("dispatch fixture")
|
||||
execute = execute or (lambda *args: {"ok": True})
|
||||
screen_execute = screen_execute or (lambda *args: {"ok": False})
|
||||
namespace = dict(mcp=mcp, anyio=anyio, partial=partial, wraps=wraps, inspect=inspect,
|
||||
ToolAnnotations=ToolAnnotations, Image=Image, socket=socket,
|
||||
ADAPTER_SOCKET=Path("/nonexistent/framebridge-test.sock"),
|
||||
PRODUCT_VERSION="test", GUIDE_SCHEMA=1,
|
||||
_framebridge_exec_geranas_impl=execute, expanded_execute_node=execute,
|
||||
expanded_execute_screen_node=screen_execute,
|
||||
register_framebridge_screen_tools=register_framebridge_screen_tools,
|
||||
resolve_profile_node=lambda target: target.upper(),
|
||||
register_screen_presentation=register_screen_presentation,
|
||||
register_tty_tools=tty_mcp.register_tools)
|
||||
exec(compile(ast.fix_missing_locations(ast.Module(body=selected, type_ignores=[])),
|
||||
str(PORTABLE), "exec"), namespace)
|
||||
return mcp, namespace
|
||||
|
||||
|
||||
def structured(reply):
|
||||
if isinstance(reply, tuple):
|
||||
content, value = reply
|
||||
if value is not None:
|
||||
return value
|
||||
reply = content
|
||||
if isinstance(reply, dict):
|
||||
return reply
|
||||
return json.loads(next(item.text for item in reply if item.type == "text"))
|
||||
|
||||
|
||||
async def until_set(event):
|
||||
async def poll():
|
||||
while not event.is_set():
|
||||
await asyncio.sleep(.001)
|
||||
await asyncio.wait_for(poll(), 1)
|
||||
|
||||
|
||||
class DispatchTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_actual_sdk_returns_status_while_exec_and_tty_capture_are_running(self):
|
||||
exec_started, capture_started, release = (threading.Event() for _ in range(3))
|
||||
fixture = ChunkExecutor()
|
||||
calls = []
|
||||
|
||||
def execute(*args):
|
||||
calls.append(args)
|
||||
exec_started.set()
|
||||
release.wait(2)
|
||||
return {"ok": True, "marker": args[0]}
|
||||
|
||||
def screen(alias, script, authorized, timeout_s, job_id, sha256, include_stdout):
|
||||
capture_started.set()
|
||||
release.wait(2)
|
||||
return fixture(target=alias, script=script, authorized=authorized,
|
||||
timeout_s=timeout_s, job_id=job_id, sha256=sha256,
|
||||
include_stdout=include_stdout)
|
||||
|
||||
mcp, _ = portable(execute=execute, screen_execute=screen)
|
||||
slow = asyncio.create_task(mcp.call_tool("framebridge_exec", {
|
||||
"script": "bounded-marker", "authorized": True}))
|
||||
capture = asyncio.create_task(mcp.call_tool("cua_tty_observe", {
|
||||
"session_id": SESSION_ID, "authorized": True}))
|
||||
try:
|
||||
await until_set(exec_started)
|
||||
await until_set(capture_started)
|
||||
status = structured(await asyncio.wait_for(mcp.call_tool("framebridge_status", {}), .3))
|
||||
self.assertTrue(status["ok"])
|
||||
self.assertFalse(slow.done())
|
||||
self.assertFalse(capture.done())
|
||||
finally:
|
||||
release.set()
|
||||
exec_result, capture_result = await asyncio.gather(slow, capture)
|
||||
self.assertEqual(structured(exec_result)["marker"], "bounded-marker")
|
||||
self.assertEqual(len(calls), 1)
|
||||
self.assertTrue(any(item.type == "image" for item in capture_result))
|
||||
|
||||
async def test_fast_exec_response_is_delivered_before_slow_exec_completes(self):
|
||||
started, release = threading.Event(), threading.Event()
|
||||
calls = []
|
||||
|
||||
def execute(*args):
|
||||
marker = args[0]
|
||||
calls.append(marker)
|
||||
if marker == "slow":
|
||||
started.set()
|
||||
release.wait(2)
|
||||
return {"ok": True, "marker": marker}
|
||||
|
||||
mcp, _ = portable(execute=execute)
|
||||
slow = asyncio.create_task(mcp.call_tool("framebridge_exec", {
|
||||
"script": "slow", "authorized": True}))
|
||||
try:
|
||||
await until_set(started)
|
||||
fast = structured(await asyncio.wait_for(mcp.call_tool("framebridge_exec", {
|
||||
"script": "fast", "authorized": True}), .3))
|
||||
self.assertEqual(fast["marker"], "fast")
|
||||
self.assertFalse(slow.done())
|
||||
finally:
|
||||
release.set()
|
||||
slow_result = await slow
|
||||
self.assertEqual(structured(slow_result)["marker"], "slow")
|
||||
self.assertCountEqual(calls, ["slow", "fast"])
|
||||
|
||||
async def test_full_portable_import_registers_tools_without_bootstrap_side_effects(self):
|
||||
import importlib.util
|
||||
from framebridge_expanded import fleet
|
||||
spec = importlib.util.spec_from_file_location("frontdoor_dispatch_test", PORTABLE)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with mock.patch.object(fleet, "bootstrap_original") as bootstrap:
|
||||
spec.loader.exec_module(module)
|
||||
bootstrap.assert_called_once_with()
|
||||
names = {tool.name for tool in await module.mcp.list_tools()}
|
||||
self.assertTrue(set(tty_mcp.TOOL_NAMES) <= names)
|
||||
self.assertTrue({"framebridge_exec", "framebridge_cua", "framebridge_screen_show"} <= names)
|
||||
self.assertFalse(inspect.iscoroutinefunction(module.framebridge_exec))
|
||||
|
||||
async def test_schemas_metadata_and_internal_sync_callbacks_are_preserved(self):
|
||||
mcp, namespace = portable()
|
||||
baseline, _ = portable(threaded=False)
|
||||
actual = {tool.name: tool.model_dump() for tool in await mcp.list_tools()}
|
||||
expected = {tool.name: tool.model_dump() for tool in await baseline.list_tools()}
|
||||
self.assertEqual(actual, expected)
|
||||
self.assertFalse(inspect.iscoroutinefunction(namespace["framebridge_exec"]))
|
||||
self.assertFalse(inspect.iscoroutinefunction(namespace["_screen_exec_request"]))
|
||||
self.assertIsInstance(namespace["framebridge_exec"]("marker", True), dict)
|
||||
|
||||
async def test_local_and_remote_tty_capture_use_identity_producing_transport(self):
|
||||
for target in ("LOCAL", "WORKER_1"):
|
||||
with self.subTest(target=target):
|
||||
fixture, calls = ChunkExecutor(), []
|
||||
|
||||
def screen(alias, script, authorized, timeout_s, job_id, sha256, include_stdout):
|
||||
calls.append(alias)
|
||||
return fixture(target=alias, script=script, authorized=authorized,
|
||||
timeout_s=timeout_s, job_id=job_id, sha256=sha256,
|
||||
include_stdout=include_stdout)
|
||||
|
||||
legacy = mock.Mock(side_effect=AssertionError("legacy EXEC must not capture"))
|
||||
mcp, _ = portable(execute=legacy, screen_execute=screen)
|
||||
result = await mcp.call_tool("cua_tty_observe", {
|
||||
"session_id": SESSION_ID, "authorized": True, "target": target})
|
||||
self.assertTrue(any(item.type == "image" for item in result))
|
||||
self.assertEqual(set(calls), {target})
|
||||
self.assertGreaterEqual(len(calls), 2)
|
||||
legacy.assert_not_called()
|
||||
|
||||
async def test_missing_uuid_and_wrong_reply_binding_still_fail_closed(self):
|
||||
for corruption, expected in (("uuid", "SCREEN_EXEC_NODE_ID_INVALID"),
|
||||
("hash", "SCREEN_EXEC_BINDING_INVALID")):
|
||||
with self.subTest(corruption=corruption):
|
||||
fixture = ChunkExecutor()
|
||||
|
||||
def screen(alias, script, authorized, timeout_s, job_id, sha256, include_stdout):
|
||||
result = fixture(target=alias, script=script, authorized=authorized,
|
||||
timeout_s=timeout_s, job_id=job_id, sha256=sha256,
|
||||
include_stdout=include_stdout)
|
||||
if corruption == "uuid":
|
||||
result.pop("node_id")
|
||||
else:
|
||||
result["mcp_script_sha256"] = "0" * 64
|
||||
return result
|
||||
|
||||
mcp, _ = portable(screen_execute=screen)
|
||||
result = structured(await mcp.call_tool("cua_tty_observe", {
|
||||
"session_id": SESSION_ID, "authorized": True}))
|
||||
self.assertFalse(result["ok"])
|
||||
self.assertEqual(result["error"], expected)
|
||||
|
||||
async def test_other_tty_rpcs_also_leave_status_responsive(self):
|
||||
started, release = threading.Event(), threading.Event()
|
||||
mcp, _ = portable()
|
||||
|
||||
def request(*args):
|
||||
started.set()
|
||||
release.wait(2)
|
||||
return {"ok": True}
|
||||
|
||||
with mock.patch.object(tty_mcp, "_remote_request", side_effect=request):
|
||||
slow = asyncio.create_task(mcp.call_tool("cua_tty_status", {
|
||||
"authorized": True, "target": "WORKER_1"}))
|
||||
try:
|
||||
await until_set(started)
|
||||
self.assertTrue(structured(await asyncio.wait_for(
|
||||
mcp.call_tool("framebridge_status", {}), .3))["ok"])
|
||||
self.assertFalse(slow.done())
|
||||
finally:
|
||||
release.set()
|
||||
await slow
|
||||
|
||||
async def test_anyio_cancellation_waits_for_single_worker_without_replay(self):
|
||||
started, release, finished = (threading.Event() for _ in range(3))
|
||||
mcp, namespace = portable()
|
||||
calls, scopes = [], []
|
||||
|
||||
@namespace["_blocking_tools"].tool()
|
||||
def bounded_operation() -> dict:
|
||||
calls.append(1)
|
||||
started.set()
|
||||
release.wait(2)
|
||||
finished.set()
|
||||
return {"ok": True}
|
||||
|
||||
async def dispatch():
|
||||
with anyio.CancelScope() as scope:
|
||||
scopes.append(scope)
|
||||
await mcp.call_tool("bounded_operation", {})
|
||||
|
||||
async with anyio.create_task_group() as group:
|
||||
group.start_soon(dispatch)
|
||||
await until_set(started)
|
||||
scopes[0].cancel()
|
||||
await anyio.sleep(.02)
|
||||
self.assertFalse(finished.is_set())
|
||||
release.set()
|
||||
self.assertTrue(finished.is_set())
|
||||
self.assertEqual(calls, [1])
|
||||
|
||||
|
||||
class AutomaticJobTests(unittest.TestCase):
|
||||
def test_identical_concurrent_auto_jobs_are_distinct_and_explicit_ids_unchanged(self):
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
tree = ast.parse(PORTABLE.read_text())
|
||||
function = next(node for node in tree.body if isinstance(node, ast.FunctionDef)
|
||||
and node.name == "_framebridge_exec_geranas_impl")
|
||||
namespace = dict(hashlib=hashlib, time=time, uuid=uuid,
|
||||
JOB_RE=re.compile(r"^[A-Za-z0-9._-]{1,80}$"),
|
||||
adapter_request=lambda payload: {"ok": True, "job_id": payload["job_id"]},
|
||||
sanitize_text=lambda text: text)
|
||||
exec(compile(ast.Module(body=[function], type_ignores=[]), str(PORTABLE), "exec"), namespace)
|
||||
execute = namespace["_framebridge_exec_geranas_impl"]
|
||||
with mock.patch.object(time, "strftime", return_value="20261028-000000"):
|
||||
with ThreadPoolExecutor(max_workers=8) as pool:
|
||||
results = list(pool.map(lambda _: execute("same-command", True), range(16)))
|
||||
self.assertEqual(len({result["job_id"] for result in results}), 16)
|
||||
self.assertTrue(all(namespace["JOB_RE"].fullmatch(result["job_id"]) for result in results))
|
||||
first = execute("same-command", True, job_id="owner-idempotent-id")
|
||||
second = execute("same-command", True, job_id="owner-idempotent-id")
|
||||
self.assertEqual(first["job_id"], "owner-idempotent-id")
|
||||
self.assertEqual(first["job_id"], second["job_id"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user