FrameBridge 0.3.1 community source release
This commit is contained in:
commit
b23819fff1
1354 files changed
+343716
No files matched your search
Executable
+258
@@ -0,0 +1,258 @@
|
||||
#!/usr/bin/python3
|
||||
"""Manual launcher for the captured FrameBridge Original programs."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import signal
|
||||
import stat
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
PREFIX = Path(__file__).resolve().parents[1]
|
||||
REAL_HOME = Path.home()
|
||||
DEFAULT_STATE = Path(os.environ.get("XDG_STATE_HOME", str(REAL_HOME / ".local/state"))) / "framebridge-original"
|
||||
LAYOUT = json.loads((PREFIX / "layout.json").read_text())
|
||||
CORE_COMMANDS = ["bash", "tmux", "tee", "flock", "awk", "sha256sum", "timeout",
|
||||
"gnome-terminal", "wmctrl"]
|
||||
|
||||
def settings(args):
|
||||
state = Path(args.state_dir).expanduser().absolute()
|
||||
home = state / "home"
|
||||
runtime_base = Path("/run/user") / str(os.getuid())
|
||||
if not runtime_base.is_dir() or runtime_base.stat().st_uid != os.getuid():
|
||||
runtime_base = Path("/tmp")
|
||||
runtime = runtime_base / ("fbo-" + str(os.getuid()) + "-" + hashlib.sha256(str(state).encode()).hexdigest()[:16])
|
||||
env = os.environ.copy()
|
||||
authority = env.get("XAUTHORITY")
|
||||
if not authority and (REAL_HOME / ".Xauthority").is_file():
|
||||
authority = str(REAL_HOME / ".Xauthority")
|
||||
env.update({
|
||||
"HOME": str(home), "XDG_CONFIG_HOME": str(home / ".config"),
|
||||
"XDG_DATA_HOME": str(home / ".local/share"),
|
||||
"XDG_STATE_HOME": str(home / ".local/state"), "XDG_CACHE_HOME": str(home / ".cache"),
|
||||
"XDG_RUNTIME_DIR": str(runtime), "TMPDIR": str(runtime / "tmp"),
|
||||
"TMUX_TMPDIR": str(runtime / "tmux"), "PYTHONDONTWRITEBYTECODE": "1",
|
||||
"FBTERM_ROOT": str(home / ".local/share/framebridge/fbterm"),
|
||||
"FRAMEBRIDGE_FBTERM_ROOT_OVERRIDE": str(home / ".local/share/framebridge/fbterm"),
|
||||
"FBTERM_SESSION": "framebridge-original-fbterm",
|
||||
"FBTERM_TITLE": "FrameBridge Original",
|
||||
"FRAMEBRIDGE_FBTERM_ROOT": str(home / ".local/share/framebridge/fbterm"),
|
||||
"FRAMEBRIDGE_FBTERM_ENGINE": str(PREFIX / LAYOUT["fbterm_bin"] / "fbterm_engine.py"),
|
||||
"FRAMEBRIDGE_FBTERM_SESSION": "framebridge-original-fbterm",
|
||||
"FRAMEBRIDGE_FBTERM_TITLE": "FrameBridge Original",
|
||||
"FRAMEBRIDGE_EXEC_SESSION": "framebridge-original",
|
||||
"FRAMEBRIDGE_RESCUE_SESSION": "framebridge-original-rescue",
|
||||
"FRAMEBRIDGE_UI_STATE_DIR": str(home / ".local/share/framebridge/ui-state"),
|
||||
"FRAMEBRIDGE_PROGRESS_DIR": str(home / ".local/state/framebridge/progress"),
|
||||
"FRAMEBRIDGE_PROGRESS_LOG": str(home / ".local/state/framebridge/progress/framebridge-progress.log"),
|
||||
"FRAMEBRIDGE_TOOL_ROOT": str(runtime / "adapter"),
|
||||
"FRAMEBRIDGE_NATIVE_HOST": str(PREFIX / LAYOUT["native_host"]),
|
||||
"FRAMEBRIDGE_FRONT_DOOR_HOST": "127.0.0.1",
|
||||
"FRAMEBRIDGE_FRONT_DOOR_PORT": str(args.port),
|
||||
"FRAMEBRIDGE_FLEET_HUB_HOST": "127.0.0.1",
|
||||
"FRAMEBRIDGE_FLEET_HUB_PORT": str(args.hub_port),
|
||||
"FRAMEBRIDGE_HUB_MCP_URL": "http://127.0.0.1:" + str(args.hub_port) + "/mcp",
|
||||
"FRAMEBRIDGE_BROKER_CONTROL": "http://127.0.0.1:" + str(args.broker_port),
|
||||
"FRAMEBRIDGE_ASUS_NODE_ID": "", "FRAMEBRIDGE_GERAMEG_NODE_ID": "",
|
||||
"FRAMEBRIDGE_CLOUD_DSM_SOCKET": str(runtime / "cloud-unconfigured.sock"),
|
||||
"FRAMEBRIDGE_NODE_REGISTRY": str(home / ".config/framebridge/nodes.json"),
|
||||
"FRAMEBRIDGE_ACCOUNT_BINDING": str(home / ".config/framebridge/account-binding.json"),
|
||||
"FRAMEBRIDGE_PAIRED_DISCOVERY": str(home / ".config/framebridge/paired-nodes.json"),
|
||||
"FRAMEBRIDGE_ENABLE_GERAMEG_CUA_TTY": "0",
|
||||
"PYTHONPATH": os.pathsep.join(str(PREFIX / p) for p in [
|
||||
"original/geranas/FrameBridge-Fleet-Hub/vendor",
|
||||
"original/geranas/FrameBridge-Fleet-Hub/app", "portable"]),
|
||||
})
|
||||
env.pop("TMUX", None)
|
||||
env.pop("TMUX_PANE", None)
|
||||
if authority:
|
||||
env["XAUTHORITY"] = authority
|
||||
return state, home, env
|
||||
|
||||
def prepare(args):
|
||||
state, home, env = settings(args)
|
||||
runtime = Path(env["XDG_RUNTIME_DIR"])
|
||||
try:
|
||||
runtime.mkdir(mode=0o700)
|
||||
except FileExistsError:
|
||||
pass
|
||||
st = runtime.lstat()
|
||||
if not stat.S_ISDIR(st.st_mode) or st.st_uid != os.getuid() or stat.S_IMODE(st.st_mode) != 0o700:
|
||||
raise RuntimeError("Runtime directory must be private and owned by this user: " + str(runtime))
|
||||
for p in [runtime, runtime / "tmp", runtime / "tmux", runtime / "adapter", state, home, state / "run", state / "tmp", state / "tmux",
|
||||
state / "logs", home / ".config/framebridge",
|
||||
home / ".local/share/framebridge/fbterm",
|
||||
home / ".local/share/framebridge/tool-adapter",
|
||||
home / ".local/lib/framebridge-native",
|
||||
home / "Descargas/framebridge_v0_1"]:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
p.chmod(0o700)
|
||||
links = {
|
||||
home / ".local/share/framebridge/fbterm/bin": PREFIX / LAYOUT["fbterm_bin"],
|
||||
home / ".local/share/framebridge/tool-adapter/bin": PREFIX / LAYOUT["adapter_bin"],
|
||||
home / ".local/lib/framebridge-native/host": PREFIX / LAYOUT["native_host_dir"],
|
||||
home / "Descargas/framebridge_v0_1/host": PREFIX / LAYOUT["native_host_dir"],
|
||||
}
|
||||
for dst, src in links.items():
|
||||
if dst.is_symlink():
|
||||
if dst.resolve() != src.resolve():
|
||||
raise RuntimeError("Unexpected link: " + str(dst))
|
||||
elif dst.exists():
|
||||
raise RuntimeError("Runtime program path already exists: " + str(dst))
|
||||
else:
|
||||
dst.symlink_to(src, target_is_directory=True)
|
||||
registry = home / ".config/framebridge/nodes.json"
|
||||
if not registry.exists():
|
||||
registry.write_text(json.dumps({"nodes": {}}, indent=2) + "\n")
|
||||
registry.chmod(0o600)
|
||||
return state, home, env
|
||||
|
||||
def birth(pid):
|
||||
try:
|
||||
fields = Path("/proc/" + str(pid) + "/stat").read_text().rsplit(") ", 1)[1].split()
|
||||
if fields[0] in {"Z", "X"}:
|
||||
return None
|
||||
return fields[19]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
def live(record):
|
||||
expected = record.get("birth")
|
||||
return expected is not None and birth(record["pid"]) == expected
|
||||
|
||||
def load_record(state):
|
||||
p = state / "processes.json"
|
||||
if not p.exists():
|
||||
return {"processes": []}
|
||||
return json.loads(p.read_text())
|
||||
|
||||
def stop(args):
|
||||
state, home, env = settings(args)
|
||||
records = load_record(state)
|
||||
for rec in reversed(records.get("processes", [])):
|
||||
if live(rec):
|
||||
os.killpg(rec["pid"], signal.SIGTERM)
|
||||
deadline = time.monotonic() + 5
|
||||
while any(live(r) for r in records.get("processes", [])) and time.monotonic() < deadline:
|
||||
time.sleep(.1)
|
||||
for rec in records.get("processes", []):
|
||||
if live(rec):
|
||||
os.killpg(rec["pid"], signal.SIGKILL)
|
||||
if Path(env["TMUX_TMPDIR"]).is_dir() and shutil.which("tmux"):
|
||||
subprocess.run(["tmux", "kill-server"], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
(state / "processes.json").unlink(missing_ok=True)
|
||||
print("FrameBridge Original detenido. Estado: " + str(state))
|
||||
|
||||
def start(args):
|
||||
missing = [name for name in CORE_COMMANDS if not shutil.which(name)]
|
||||
if missing:
|
||||
raise RuntimeError("Faltan comandos para ejecutar FrameBridge Original: " + ", ".join(missing))
|
||||
state, home, env = prepare(args)
|
||||
records = load_record(state)
|
||||
if any(live(r) for r in records.get("processes", [])):
|
||||
raise RuntimeError("Este perfil ya está ejecutándose; usa status o stop.")
|
||||
for port in (args.port, args.hub_port):
|
||||
with socket.socket() as probe:
|
||||
probe.bind(("127.0.0.1", port))
|
||||
commands = [
|
||||
("adapter", [sys.executable, str(PREFIX / LAYOUT["adapter"]), "serve"]),
|
||||
("fleet", [sys.executable, str(PREFIX / "original/geranas/FrameBridge-Fleet-Hub/app/fleet_hub_mcp_server.py")]),
|
||||
("mcp", [sys.executable, str(PREFIX / "portable/front_door_mcp.py")]),
|
||||
]
|
||||
record = {"state_dir": str(state), "mcp_url": "http://127.0.0.1:" + str(args.port) + "/mcp",
|
||||
"port": args.port, "hub_port": args.hub_port, "processes": []}
|
||||
try:
|
||||
for role, command in commands:
|
||||
log = (state / "logs" / (role + ".log")).open("ab")
|
||||
proc = subprocess.Popen(command, env=env, cwd=home, stdin=subprocess.DEVNULL,
|
||||
stdout=log, stderr=subprocess.STDOUT, start_new_session=True, umask=0o077)
|
||||
log.close()
|
||||
record["processes"].append({"role": role, "pid": proc.pid, "birth": birth(proc.pid)})
|
||||
(state / "processes.json").write_text(json.dumps(record, indent=2) + "\n")
|
||||
(state / "processes.json").chmod(0o600)
|
||||
deadline = time.monotonic() + 15
|
||||
while time.monotonic() < deadline:
|
||||
if not all(live(r) for r in record["processes"]):
|
||||
raise RuntimeError("Falló un proceso. Consulta los logs del perfil: " + str(state / "logs"))
|
||||
ready = (Path(env["FRAMEBRIDGE_TOOL_ROOT"]) / "adapter.sock").is_socket()
|
||||
for port in (args.port, args.hub_port):
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=.2):
|
||||
pass
|
||||
except OSError:
|
||||
ready = False
|
||||
if ready:
|
||||
print("FrameBridge Original iniciado: " + record["mcp_url"])
|
||||
print("Conexión a ChatGPT: manual; usa tunnel --help y las instrucciones del paquete.")
|
||||
print("Perfil local: " + str(state))
|
||||
return
|
||||
time.sleep(.15)
|
||||
raise RuntimeError("No se completó el arranque; consulta los logs del perfil.")
|
||||
except BaseException:
|
||||
stop(args)
|
||||
raise
|
||||
|
||||
def status(args):
|
||||
state, home, env = settings(args)
|
||||
record = load_record(state)
|
||||
print(json.dumps({"state_dir": str(state), "mcp_url": record.get("mcp_url"),
|
||||
"processes": [{"role": x["role"], "pid": x["pid"], "running": live(x)}
|
||||
for x in record.get("processes", [])]}, indent=2))
|
||||
|
||||
def doctor(args):
|
||||
state, home, env = settings(args)
|
||||
required = CORE_COMMANDS
|
||||
gui = ["xdotool", "xwininfo", "xclip", "import", "convert", "identify", "xterm"]
|
||||
imports = subprocess.run([sys.executable, "-c", "import mcp,pydantic_core,cryptography"], env=env, capture_output=True)
|
||||
report = {"python": sys.version.split()[0], "python_abi_expected": "3.13",
|
||||
"mcp_imports_ok": imports.returncode == 0,
|
||||
"core_commands": {x: bool(shutil.which(x)) for x in required},
|
||||
"cua_commands": {x: bool(shutil.which(x)) for x in gui},
|
||||
"display": os.environ.get("DISPLAY", ""),
|
||||
"tunnel_sha256": hashlib.sha256((PREFIX / "original/geranas/FrameBridge-MCP-Private/bin/tunnel-client").read_bytes()).hexdigest()}
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0 if sys.version_info[:2] == (3, 13) and imports.returncode == 0 and all(report["core_commands"].values()) else 1
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="FrameBridge Original: ejecución local, CUA y conexión manual a ChatGPT.")
|
||||
parser.add_argument("--state-dir", default=str(DEFAULT_STATE))
|
||||
parser.add_argument("--port", type=int, default=19865)
|
||||
parser.add_argument("--hub-port", type=int, default=19877)
|
||||
parser.add_argument("--broker-port", type=int, default=19878)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
for name in ["start", "stop", "status", "doctor"]:
|
||||
sub.add_parser(name)
|
||||
tunnel = sub.add_parser("tunnel", help="Ejecuta el cliente de túnel original; configuración manual independiente.")
|
||||
tunnel.add_argument("arguments", nargs=argparse.REMAINDER)
|
||||
args = parser.parse_args()
|
||||
if args.command == "tunnel":
|
||||
state, home, env = prepare(args)
|
||||
rest = args.arguments
|
||||
if rest[:1] == ["--"]:
|
||||
rest = rest[1:]
|
||||
explicit_refs = {x[4:] for x in rest if x.startswith("env:")}
|
||||
for x in rest:
|
||||
if "=env:" in x:
|
||||
explicit_refs.add(x.split("=env:", 1)[1])
|
||||
for key in list(env):
|
||||
if (key.startswith(("TUNNEL_CLIENT_", "CONTROL_PLANE_", "OPENAI_", "MCP_", "HEALTH_", "LOG_", "CLOUDFLARED_", "HARPOON_"))
|
||||
or key in {"TUNNEL_ID", "TUNNEL_BIN", "CRED_FILE", "OPEN_WEB_UI", "PID_FILE",
|
||||
"ALLOW_REMOTE_UI", "ADMIN_UI_LOG_BUFFER_EVENTS", "PROXY_CHECK_INTERVAL"}):
|
||||
if key not in explicit_refs:
|
||||
env.pop(key, None)
|
||||
os.execve(str(PREFIX / "original/geranas/FrameBridge-MCP-Private/bin/tunnel-client"),
|
||||
["tunnel-client"] + rest, env)
|
||||
result = globals()[args.command](args)
|
||||
return result or 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except (OSError, ValueError, RuntimeError) as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
Reference in new issue
Block a user