FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
@@ -0,0 +1,147 @@
|
||||
"""Seven local/remote MCP tools for the real persistent FrameBridge CUA-TTY."""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import uuid
|
||||
import zlib
|
||||
|
||||
from . import tty
|
||||
|
||||
TOOL_NAMES = (
|
||||
"cua_tty_status", "cua_tty_open", "cua_tty_observe",
|
||||
"cua_tty_key_send", "cua_tty_text_type", "cua_tty_resize", "cua_tty_close",
|
||||
)
|
||||
|
||||
|
||||
def _exec_json(execute, target, arguments):
|
||||
script = "python3 -m framebridge_expanded.tty " + " ".join(shlex.quote(str(x)) for x in arguments)
|
||||
response = execute(script=script, authorized=True, timeout_s=30,
|
||||
job_id="tty-rpc-" + uuid.uuid4().hex[:24],
|
||||
sha256=hashlib.sha256(script.encode()).hexdigest(),
|
||||
include_stdout=True, target=target)
|
||||
if type(response) is not dict or response.get("ok") is not True:
|
||||
return {"ok": False, "error": "TTY_REMOTE_EXEC_FAILED"}
|
||||
if response.get("stdout_truncated"):
|
||||
return {"ok": False, "error": "TTY_REMOTE_RESPONSE_TRUNCATED"}
|
||||
output = response.get("stdout", "")
|
||||
if type(output) is not str:
|
||||
return {"ok": False, "error": "TTY_REMOTE_RESPONSE_INVALID"}
|
||||
try:
|
||||
value = json.loads(output)
|
||||
except (ValueError, TypeError):
|
||||
return {"ok": False, "error": "TTY_REMOTE_RESPONSE_INVALID"}
|
||||
return value if type(value) is dict else {"ok": False, "error": "TTY_REMOTE_RESPONSE_INVALID"}
|
||||
|
||||
|
||||
def _remote_request(execute, target, payload):
|
||||
encoded = base64.b64encode(json.dumps(payload, ensure_ascii=True, separators=(",", ":")).encode()).decode("ascii")
|
||||
first = _exec_json(execute, target, ["request", "--payload-b64", encoded, "--wire"])
|
||||
if first.get("ok") is not True:
|
||||
return first
|
||||
try:
|
||||
ticket = first["ticket"]
|
||||
digest = first["sha256"]
|
||||
length = first["bytes"]
|
||||
count = first["chunks"]
|
||||
if (first.get("wire") != "zlib-base64-v1" or type(ticket) is not str
|
||||
or re.fullmatch(r"[a-f0-9]{32}", ticket) is None
|
||||
or type(digest) is not str or re.fullmatch(r"[a-f0-9]{64}", digest) is None
|
||||
or type(length) is not int or not 0 < length <= 16777216
|
||||
or type(count) is not int or not 1 <= count <= 20000):
|
||||
raise ValueError()
|
||||
chunks = []
|
||||
for index in range(count):
|
||||
part = first if index == 0 else _exec_json(
|
||||
execute, target, ["read", "--ticket", ticket, "--index", index])
|
||||
if (part.get("ok") is not True or part.get("ticket") != ticket
|
||||
or part.get("index") != index or part.get("chunks") != count
|
||||
or part.get("sha256") != digest or type(part.get("data")) is not str
|
||||
or len(part["data"]) > 1800):
|
||||
raise ValueError()
|
||||
chunks.append(part["data"])
|
||||
compressed = base64.b64decode("".join(chunks), validate=True)
|
||||
decoder = zlib.decompressobj()
|
||||
raw = decoder.decompress(compressed, length + 1)
|
||||
if (len(raw) != length or not decoder.eof or decoder.unused_data
|
||||
or decoder.unconsumed_tail or hashlib.sha256(raw).hexdigest() != digest):
|
||||
raise ValueError()
|
||||
result = json.loads(raw)
|
||||
return result if type(result) is dict else {"ok": False, "error": "TTY_REMOTE_RESPONSE_INVALID"}
|
||||
except (KeyError, ValueError, TypeError, zlib.error):
|
||||
return {"ok": False, "error": "TTY_REMOTE_RESPONSE_INVALID"}
|
||||
|
||||
|
||||
def register_tools(mcp, exec_request, *, screen_client=None, image_factory=None):
|
||||
if screen_client is None:
|
||||
from .real_screen.screen_frontdoor import ScreenClient, resolve_profile_node
|
||||
if image_factory is None:
|
||||
from mcp.server.fastmcp import Image
|
||||
image_factory = Image
|
||||
screen_client = ScreenClient(exec_request, image_factory, resolve_node=resolve_profile_node)
|
||||
"""Register tools; exec_request is the existing authorized fleet executor."""
|
||||
def call(payload, target):
|
||||
if payload.get("authorized") is not True:
|
||||
return {"ok": False, "error": "AUTHORIZED_TRUE_REQUIRED"}
|
||||
if type(target) is not str or not target.strip():
|
||||
return {"ok": False, "error": "TTY_TARGET_INVALID"}
|
||||
target = target.strip()
|
||||
if target.upper() == "LOCAL":
|
||||
result = tty.request(os.environ.get("FRAMEBRIDGE_EXPANDED_STATE"), payload)
|
||||
target = "LOCAL"
|
||||
else:
|
||||
result = _remote_request(exec_request, target, payload)
|
||||
result = dict(result)
|
||||
result["target"] = target
|
||||
return result
|
||||
|
||||
@mcp.tool()
|
||||
def cua_tty_status(authorized: bool = False, target: str = "LOCAL") -> dict:
|
||||
"""Report the actual CUA-TTY daemon, Bash sessions and configurable resource limits."""
|
||||
return call({"op": "status", "authorized": authorized}, target)
|
||||
|
||||
@mcp.tool()
|
||||
def cua_tty_open(authorized: bool = False, program: str = "bash",
|
||||
rows: int = 24, cols: int = 80, target: str = "LOCAL") -> dict:
|
||||
"""Open a real persistent Bash console in the profile's private tmux server; no X11 required."""
|
||||
return call({"op": "open", "authorized": authorized, "program": program,
|
||||
"rows": rows, "cols": cols}, target)
|
||||
|
||||
@mcp.tool()
|
||||
def cua_tty_observe(session_id: str, authorized: bool = False,
|
||||
target: str = "LOCAL"):
|
||||
"""Capture the entire actual active screen and report whether this session is visible."""
|
||||
return screen_client.call("observe", target, authorized, session_id=session_id)
|
||||
|
||||
@mcp.tool()
|
||||
def cua_tty_key_send(session_id: str, key: str, authorized: bool = False,
|
||||
target: str = "LOCAL", capture_id: str = ""):
|
||||
"""Send a key only to the visible TTY target bound to a real capture; use graphical CUA for X11."""
|
||||
return screen_client.call("key", target, authorized, session_id=session_id,
|
||||
capture_id=capture_id, key=key)
|
||||
|
||||
@mcp.tool()
|
||||
def cua_tty_text_type(session_id: str, text: str, authorized: bool = False,
|
||||
target: str = "LOCAL", capture_id: str = ""):
|
||||
"""Type only into the visible TTY target bound to a real capture; never into a hidden session."""
|
||||
return screen_client.call("text", target, authorized, session_id=session_id,
|
||||
capture_id=capture_id, text=text)
|
||||
|
||||
@mcp.tool()
|
||||
def cua_tty_resize(session_id: str, rows: int, cols: int,
|
||||
authorized: bool = False, target: str = "LOCAL") -> dict:
|
||||
"""Resize the real PTY/tmux console within the limits reported by cua_tty_status."""
|
||||
return call({"op": "resize", "authorized": authorized, "session_id": session_id,
|
||||
"rows": rows, "cols": cols}, target)
|
||||
|
||||
@mcp.tool()
|
||||
def cua_tty_close(session_id: str, authorized: bool = False,
|
||||
target: str = "LOCAL") -> dict:
|
||||
"""Close only the selected console owned by this runtime and verify tmux cleanup."""
|
||||
return call({"op": "close", "authorized": authorized, "session_id": session_id}, target)
|
||||
|
||||
return TOOL_NAMES
|
||||
Reference in new issue
Block a user