Files
framebridge/source/portable/FRONT-DOOR-EXPANDED-PATCH.diff

253 lines
11 KiB
Diff

--- original/front_door_mcp.py
+++ portable/front_door_mcp.py
@@ -13,26 +13,28 @@
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from mcp.server.fastmcp import FastMCP, Image
+from framebridge_expanded.fleet import execute_node as expanded_execute_node, cua_node as expanded_cua_node, bootstrap_original
+bootstrap_original()
HOME = Path.home()
-ADAPTER_SOCKET = HOME / ".local/share/framebridge/tool-adapter/adapter.sock"
+ADAPTER_SOCKET = Path(os.environ.get("FRAMEBRIDGE_TOOL_ROOT", str(HOME / ".local/share/framebridge/tool-adapter"))) / "adapter.sock"
JOBS_ROOT = HOME / ".local/share/framebridge/fbterm/jobs"
-HUB_URL = os.environ.get("FRAMEBRIDGE_HUB_MCP_URL", "http://127.0.0.1:8877/mcp")
-BROKER_CONTROL = os.environ.get("FRAMEBRIDGE_BROKER_CONTROL", "http://127.0.0.1:28765")
-ASUS_NODE_ID = os.environ.get("FRAMEBRIDGE_ASUS_NODE_ID", "4b4157f9-3c98-4d24-bff4-840825d50951")
-GERAMEG_NODE_ID = os.environ.get("FRAMEBRIDGE_GERAMEG_NODE_ID", "76885f27-da2e-4cca-bbde-6ec981e91e68")
+HUB_URL = os.environ.get("FRAMEBRIDGE_HUB_MCP_URL", "http://127.0.0.1:20877/mcp")
+BROKER_CONTROL = os.environ.get("FRAMEBRIDGE_BROKER_CONTROL", "http://127.0.0.1:20878")
+ASUS_NODE_ID = os.environ.get("FRAMEBRIDGE_ASUS_NODE_ID", "")
+GERAMEG_NODE_ID = os.environ.get("FRAMEBRIDGE_GERAMEG_NODE_ID", "")
HOST = os.environ.get("FRAMEBRIDGE_FRONT_DOOR_HOST", "127.0.0.1")
-PORT = int(os.environ.get("FRAMEBRIDGE_FRONT_DOOR_PORT", "8865"))
+PORT = int(os.environ.get("FRAMEBRIDGE_FRONT_DOOR_PORT", "20865"))
JOB_RE = re.compile(r"^[A-Za-z0-9._-]{1,80}$")
SENSITIVE = re.compile(r"(?i)(secret|token|password|passwd|api[_-]?key|private[_-]?key|cookie|authorization|credential)")
KEY_BEGIN = re.compile(r"-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----")
KEY_END = re.compile(r"-----END [A-Z0-9 ]*PRIVATE KEY-----")
mcp = FastMCP(
- "FrameBridge GERANAS Front Door",
+ "FrameBridge Original Front Door",
instructions=(
- "Front Door privado de FrameBridge en GERANAS. "
- "framebridge_exec enruta por target: ASUS y GERAMEG mediante broker_outbound, GERANAS via FBTERM local, autorizado explicitamente; sin target mantiene ASUS. "
+ "Front Door original en el equipo receptor. "
+ "framebridge_exec defaults to LOCAL; other targets are confirmed Fleet aliases or node IDs. "
"Las seis herramientas Fleet son READ-only y se enrutan al Hub local."
),
host=HOST,
@@ -100,7 +102,7 @@
mode=oct(ADAPTER_SOCKET.stat().st_mode & 0o777) if ok else None
return {
"ok":ok,
- "host":"geranas",
+ "host":socket.gethostname(),
"adapter_socket":str(ADAPTER_SOCKET),
"socket_mode":mode,
"mcp_transport":"streamable-http",
@@ -158,6 +160,8 @@
if not JOB_RE.fullmatch(job_id):
return {"ok":False,"error":"INVALID_JOB_ID"}
+ if not ASUS_NODE_ID:
+ return {"ok":False,"error":"REMOTE_NODE_NOT_CONFIGURED"}
queued=broker_post(
"/fbpair/v1/control/admin-exec",
{
@@ -243,6 +247,8 @@
if not JOB_RE.fullmatch(job_id):
return {"ok":False,"error":"INVALID_JOB_ID"}
+ if not GERAMEG_NODE_ID:
+ return {"ok":False,"error":"REMOTE_NODE_NOT_CONFIGURED"}
queued=broker_post(
"/fbpair/v1/control/admin-exec",
{
@@ -310,7 +316,7 @@
sha256: str = "",
include_stdout: bool = False,
) -> dict:
- """Administrative/lab exec on GERANAS local FBTERM."""
+ """Historical alias for original local FBTERM execution on this receiver."""
return _framebridge_exec_geranas_impl(
script=script,
authorized=authorized,
@@ -371,13 +377,13 @@
def _cloud_dsm_rpc(payload: dict) -> dict:
- """Private GERANAS local socket; root-only approval never exposed through MCP."""
+ """Optional private local socket; root-only approval never exposed through MCP."""
import socket as _socket
import json as _json
sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_STREAM)
sock.settimeout(38)
try:
- sock.connect("/run/framebridge-dsm/cloud.sock")
+ sock.connect(os.environ.get("FRAMEBRIDGE_CLOUD_DSM_SOCKET", str(HOME / ".local/share/framebridge/dsm/cloud.sock")))
body = (_json.dumps(payload, separators=(",",":"),ensure_ascii=True) + "\n").encode("utf8")
if len(body)>12000:
return {"ok":False,"error":"CLOUD_REQUEST_TOO_LARGE","target":"CLOUD"}
@@ -428,49 +434,16 @@
job_id: str = "",
sha256: str = "",
include_stdout: bool = False,
- target: str = "ASUS",
+ target: str = "LOCAL",
) -> dict:
- """Generic administrative/lab exec. target=ASUS or GERAMEG uses broker; target=GERANAS uses local FBTERM. Legacy calls without target remain ASUS. CLOUD uses only typed JSON actions, never shell commands."""
- if target == "CLOUD":
- import json as _json
- try:
- payload = _json.loads(script)
- if not isinstance(payload,dict) or len(script)>2000:
- return {"ok":False,"error":"CLOUD_REQUIRES_JSON_ACTION"}
- except (TypeError,ValueError):
- return {"ok":False,"error":"CLOUD_ACCEPTS_ONLY_TYPED_JSON_ACTION_NOT_SHELL"}
- if payload.get("action")=="approve":
- return {"ok":False,"error":"CLOUD_OWNER_APPROVAL_LOCAL_TTY_ONLY"}
- return _cloud_dsm_rpc({"action":payload.get("action"),
- "params":payload.get("params",{}),
- "authorized":bool(authorized)})
- if target == "GERANAS":
- result = _framebridge_exec_geranas_impl(
- script=script,
- authorized=authorized,
- timeout_s=timeout_s,
- job_id=job_id,
- sha256=sha256,
- include_stdout=include_stdout,
- )
+ """Execute through original local FBTERM or a confirmed Fleet node alias."""
+ if target == "LOCAL":
+ result = _framebridge_exec_geranas_impl(script, authorized, timeout_s, job_id, sha256, include_stdout)
if isinstance(result, dict):
result = dict(result)
- result["target"] = "GERANAS"
+ result["target"] = "LOCAL"
return result
- if target not in ("ASUS", "GERAMEG"):
- return {"ok": False, "error": "UNSUPPORTED_TARGET", "allowed_targets": ["ASUS", "GERANAS", "GERAMEG", "CLOUD"]}
- exec_request = asus_exec_request if target == "ASUS" else gerameg_exec_request
- result=exec_request(
- script=script,
- authorized=authorized,
- timeout_s=timeout_s,
- job_id=job_id,
- sha256=sha256,
- )
- if include_stdout is not True and isinstance(result,dict):
- result=dict(result)
- result.pop("stdout",None)
- return result
+ return expanded_execute_node(target, script, authorized, timeout_s, job_id, sha256, include_stdout)
def _cua_frame_fields(frame_output: str) -> dict:
@@ -510,6 +483,54 @@
return result
+def local_cua_request(cua: dict, authorized: bool, timeout_s: int, job_id: str) -> dict:
+ if authorized is not True:
+ return {"ok":False,"error":"AUTHORIZED_TRUE_REQUIRED"}
+ if not isinstance(cua,dict):
+ return {"ok":False,"error":"CUA_PAYLOAD_REQUIRED"}
+ action=str(cua.get("action") or "").upper().strip()
+ if action not in {"OBSERVE","CLICK","DOUBLE_CLICK","TYPE","KEY","SCROLL","DRAG"}:
+ return {"ok":False,"error":"CUA_ACTION_UNSUPPORTED"}
+ try:
+ timeout_s=int(timeout_s)
+ except Exception:
+ return {"ok":False,"error":"INVALID_CUA_TIMEOUT"}
+ if not 1<=timeout_s<=180:
+ return {"ok":False,"error":"INVALID_CUA_TIMEOUT"}
+ if not job_id:
+ seed=json.dumps({"action":action,"time":time.time_ns()},sort_keys=True).encode()
+ digest=hashlib.sha256(seed).hexdigest()
+ job_id="cua-local-"+time.strftime("%Y%m%d-%H%M%S")+"-"+digest[:8]
+ if not JOB_RE.fullmatch(job_id):
+ return {"ok":False,"error":"INVALID_JOB_ID"}
+
+ result=adapter_request({
+ "version":1,"mode":"EXEC","request_type":"cua",
+ "authorized":True,"job_id":job_id,"timeout_s":timeout_s,"cua":cua,
+ })
+ if not isinstance(result,dict):
+ return {"ok":False,"error":"CUA_LOCAL_ADAPTER_RESULT_INVALID","target":"LOCAL"}
+ value=dict(result)
+ frame_output=str(value.pop("frame_output",""))
+ if frame_output:
+ value.update(_cua_frame_fields(frame_output))
+ value["target"]="LOCAL"
+ return value
+
+def dispatch_cua_request(cua: dict, authorized: bool, timeout_s: int, job_id: str, target: str = "LOCAL") -> dict:
+ """Reuse the original local CUA contract and route confirmed remote nodes."""
+ if target == "LOCAL":
+ return local_cua_request(cua, authorized, timeout_s, job_id)
+ result = expanded_cua_node(target, cua, authorized, timeout_s, job_id)
+ if not isinstance(result, dict):
+ return {"ok":False,"error":"NODE_CUA_RESPONSE_INVALID"}
+ result = dict(result)
+ frame_output = str(result.pop("frame_output", ""))
+ if frame_output:
+ result.update(_cua_frame_fields(frame_output))
+ return result
+
+
def asus_cua_request(cua: dict, authorized: bool, timeout_s: int, job_id: str) -> dict:
if authorized is not True:
return {"ok":False,"error":"AUTHORIZED_TRUE_REQUIRED"}
@@ -531,6 +552,8 @@
if not JOB_RE.fullmatch(job_id):
return {"ok":False,"error":"INVALID_JOB_ID"}
+ if not ASUS_NODE_ID:
+ return {"ok":False,"error":"REMOTE_NODE_NOT_CONFIGURED"}
queued=broker_post(
"/fbpair/v1/control/admin-cua",
{
@@ -606,8 +629,9 @@
expected_source_height: int = 0,
timeout_s: int = 60,
job_id: str = "",
+ target: str = "LOCAL",
):
- """CUA R8 on ASUS through GERANAS. Requires explicit per-use chat authorization."""
+ """Original CUA R8 on this receiver through its local Tool Adapter."""
if authorized is not True:
return {"ok":False,"error":"AUTHORIZED_TRUE_REQUIRED"}
action=str(action).upper().strip()
@@ -659,7 +683,7 @@
"button":str(button).lower(),"steps":int(steps),"duration_ms":int(duration_ms),
})
- result=asus_cua_request(payload,True,timeout_s,job_id)
+ result=dispatch_cua_request(payload,True,timeout_s,job_id,target)
if bool(observe_after) and action!="OBSERVE" and bool(result.get("ok")):
post=dict(payload)
post.clear()
@@ -667,7 +691,7 @@
if expected_window_id:
post["expected_window_id"]=str(expected_window_id)
post_id=(str(job_id or "cua")[:72]+"-post")[:80]
- post_result=asus_cua_request(post,True,timeout_s,post_id)
+ post_result=dispatch_cua_request(post,True,timeout_s,post_id,target)
result["post_ok"]=bool(post_result.get("ok"))
result["post_resultado_final"]=post_result.get("resultado_final")
for key_name in (
@@ -742,8 +766,8 @@
async def framebridge_fleet_node_service_status(node: str, service: str) -> dict:
return await hub_call("framebridge_fleet_node_service_status",{"node":node,"service":service})
-from gerameg_cua_frontdoor import register_tools as register_gerameg_cua_tools
-register_gerameg_cua_tools(mcp, gerameg_exec_request)
+from framebridge_expanded.tty_mcp import register_tools as register_tty_tools
+register_tty_tools(mcp, framebridge_exec)
if __name__ == "__main__":
mcp.run(transport="streamable-http")