FrameBridge 0.3.1 community source release

This commit is contained in:
FrameBridge Development committed 2026-10-26 22:09:18 -04:00
commit b23819fff1
1354 files changed
+343716

No files matched your search

@@ -0,0 +1,425 @@
#!/usr/bin/env python3
import hashlib
import json
import os
import re
import socket
import stat
import struct
import subprocess
import sys
from pathlib import Path
VERSION = 1
MAX_SCRIPT_BYTES = 262144
MAX_REQUEST_BYTES = 393216
JOB_RE = re.compile(r"^[A-Za-z0-9._-]{1,80}$")
HOME = Path.home()
FBROOT = HOME / ".local/share/framebridge/fbterm"
ENGINE = FBROOT / "bin/fbterm_engine.py"
ROOT = Path(
os.environ.get(
"FRAMEBRIDGE_TOOL_ROOT",
str(HOME / ".local/share/framebridge/tool-adapter")
)
)
SOCKET_PATH = ROOT / "adapter.sock"
INBOX = ROOT / "inbox"
PID_FILE = ROOT / "adapter.pid"
NATIVE_HOST = Path(os.environ.get("FRAMEBRIDGE_NATIVE_HOST", str(HOME / ".local/lib/framebridge-native/host/framebridge_host.py")))
def prepare_root():
ROOT.mkdir(parents=True, exist_ok=True)
INBOX.mkdir(parents=True, exist_ok=True)
os.chmod(ROOT, 0o700)
os.chmod(INBOX, 0o700)
def recv_request(conn):
data = bytearray()
while True:
chunk = conn.recv(65536)
if not chunk:
break
data.extend(chunk)
if len(data) > MAX_REQUEST_BYTES:
raise ValueError("REQUEST_TOO_LARGE")
if b"\n" in data:
break
raw = bytes(data).split(b"\n", 1)[0]
if not raw:
raise ValueError("EMPTY_REQUEST")
return json.loads(raw.decode("utf-8"))
def send_response(conn, obj):
raw = (
json.dumps(
obj,
ensure_ascii=True,
sort_keys=True,
separators=(",", ":"),
)
+ "\n"
).encode("utf-8")
try:
conn.sendall(raw)
except (BrokenPipeError, ConnectionResetError):
return False
return True
def peer_uid(conn):
fmt = "3i"
raw = conn.getsockopt(
socket.SOL_SOCKET,
socket.SO_PEERCRED,
struct.calcsize(fmt),
)
_pid, uid, _gid = struct.unpack(fmt, raw)
return uid
def last_result_final(stdout_path):
try:
lines = Path(stdout_path).read_text(
encoding="utf-8",
errors="replace",
).splitlines()
except OSError:
return None
for line in reversed(lines):
if line.startswith("RESULTADO_FINAL="):
return line.split("=", 1)[1].strip()[:2000]
return None
def engine_result(stdout):
for line in reversed(stdout.splitlines()):
line = line.strip()
if not line.startswith("{"):
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
if isinstance(obj, dict):
return obj
return None
def native_cua_roundtrip(payload, timeout_s=60):
if not NATIVE_HOST.is_file():
raise ValueError("FRAMEBRIDGE_NATIVE_HOST_NOT_FOUND")
raw = json.dumps(
payload,
ensure_ascii=True,
separators=(",", ":"),
).encode("utf-8")
frame = struct.pack("<I", len(raw)) + raw
try:
proc = subprocess.run(
[str(NATIVE_HOST)],
input=frame,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=timeout_s,
check=False,
)
except subprocess.TimeoutExpired:
raise ValueError("CUA_NATIVE_HOST_TIMEOUT")
out = proc.stdout or b""
if len(out) < 4:
raise ValueError("CUA_NATIVE_RESPONSE_HEADER_MISSING")
size = struct.unpack("<I", out[:4])[0]
if size <= 0 or size > 2_000_000:
raise ValueError("CUA_NATIVE_RESPONSE_SIZE_INVALID")
body = out[4:4 + size]
if len(body) != size:
raise ValueError("CUA_NATIVE_RESPONSE_TRUNCATED")
try:
response = json.loads(body.decode("utf-8"))
except Exception:
raise ValueError("CUA_NATIVE_RESPONSE_INVALID_JSON")
if not isinstance(response, dict):
raise ValueError("CUA_NATIVE_RESPONSE_NOT_OBJECT")
return response
def handle_cua_request(req, job_id):
timeout_s = int(req.get("timeout_s", 60))
if not 1 <= timeout_s <= 180:
raise ValueError("INVALID_CUA_TIMEOUT")
cua = req.get("cua")
if not isinstance(cua, dict):
raise ValueError("CUA_PAYLOAD_REQUIRED")
payload = dict(cua)
payload["op"] = "cua"
payload["id"] = job_id
payload["target"] = "ASUS"
payload["authorized"] = "YES"
response = native_cua_roundtrip(
payload,
timeout_s=timeout_s,
)
return {
"version": VERSION,
"ok": bool(response.get("executed")),
"job_id": job_id,
"mode": "CUA",
"action": str(payload.get("action", ""))[:32],
"channel": str(response.get("channel", "CUA-DIRECT")),
"exit_code": response.get("exit_code"),
"frame_output": str(response.get("output", "")),
"error": str(response.get("error", "")),
}
def handle(req):
if not isinstance(req, dict):
raise ValueError("REQUEST_NOT_OBJECT")
if req.get("version") != VERSION:
raise ValueError("UNSUPPORTED_VERSION")
job_id = str(req.get("job_id", ""))
if not JOB_RE.fullmatch(job_id):
raise ValueError("INVALID_JOB_ID")
mode = str(req.get("mode", "EXEC")).upper()
if mode != "EXEC":
raise ValueError("UNSUPPORTED_MODE")
if req.get("authorized") is not True:
raise ValueError("AUTHORIZED_TRUE_REQUIRED")
request_type = str(
req.get("request_type", "exec")
).lower().strip()
if request_type == "cua":
return handle_cua_request(req, job_id)
if request_type != "exec":
raise ValueError("UNSUPPORTED_REQUEST_TYPE")
script = req.get("script")
if not isinstance(script, str) or not script:
raise ValueError("SCRIPT_REQUIRED")
script_bytes = script.encode("utf-8")
if len(script_bytes) > MAX_SCRIPT_BYTES:
raise ValueError("SCRIPT_TOO_LARGE")
timeout_s = int(req.get("timeout_s", 120))
if not 1 <= timeout_s <= 600:
raise ValueError("INVALID_TIMEOUT")
digest = hashlib.sha256(script_bytes).hexdigest()
expected = req.get("sha256")
if expected is not None:
expected = str(expected).lower()
if digest != expected:
raise ValueError("SCRIPT_SHA256_MISMATCH")
command_path = INBOX / f"{job_id}.bash"
temp_path = INBOX / f".{job_id}.tmp"
temp_path.write_bytes(script_bytes)
os.chmod(temp_path, 0o600)
temp_path.replace(command_path)
proc = subprocess.run(
[
sys.executable,
str(ENGINE),
"exec",
job_id,
str(timeout_s),
str(command_path),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=timeout_s + 45,
check=False,
env={
**os.environ,
"FBTERM_ROOT": str(FBROOT),
"FBTERM_SESSION": os.environ.get(
"FBTERM_SESSION",
"framebridge-fbterm-r1",
),
"FBTERM_TITLE": os.environ.get(
"FBTERM_TITLE",
"FrameBridge Terminal R1",
),
},
)
result = engine_result(proc.stdout)
if result is None:
return {
"version": VERSION,
"ok": False,
"job_id": job_id,
"adapter_error": "ENGINE_RESULT_NOT_FOUND",
"engine_rc": proc.returncode,
"engine_stderr": proc.stderr[-2000:],
"command_sha256": digest,
}
stdout_path = result.get("STDOUT_PATH")
stderr_path = result.get("STDERR_PATH")
return {
"version": VERSION,
"ok": True,
"job_id": job_id,
"mode": mode,
"command_sha256": digest,
"engine_rc": proc.returncode,
"exit_code": result.get("EXIT"),
"result_final": (
last_result_final(stdout_path)
if stdout_path
else None
),
"stdout_path": stdout_path,
"stderr_path": stderr_path,
"stdout_sha256": result.get("STDOUT_SHA256"),
"stderr_sha256": result.get("STDERR_SHA256"),
"command_hash_gate": result.get(
"COMMAND_HASH_GATE"
),
"pty": result.get("PTY"),
"pane_pid": result.get("PANE_PID"),
"pane_tty": result.get("PANE_TTY"),
"shell_ready": result.get("SHELL_READY"),
}
def serve():
prepare_root()
if not ENGINE.is_file():
raise RuntimeError("FBTERM_ENGINE_NOT_FOUND")
if SOCKET_PATH.exists():
st = SOCKET_PATH.lstat()
if not stat.S_ISSOCK(st.st_mode):
raise RuntimeError("SOCKET_PATH_OCCUPIED")
SOCKET_PATH.unlink()
server = socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM,
)
server.bind(str(SOCKET_PATH))
os.chmod(SOCKET_PATH, 0o600)
server.listen(4)
PID_FILE.write_text(
str(os.getpid()) + "\n",
encoding="ascii",
)
os.chmod(PID_FILE, 0o600)
try:
while True:
conn, _ = server.accept()
with conn:
try:
if peer_uid(conn) != os.getuid():
raise PermissionError(
"PEER_UID_MISMATCH"
)
req = recv_request(conn)
resp = handle(req)
except Exception as exc:
resp = {
"version": VERSION,
"ok": False,
"adapter_error": (
f"{type(exc).__name__}:"
f"{exc}"
)[:2000],
}
send_response(conn, resp)
finally:
server.close()
try:
SOCKET_PATH.unlink()
except FileNotFoundError:
pass
if __name__ == "__main__":
action = sys.argv[1] if len(sys.argv) > 1 else ""
if action == "serve":
serve()
else:
print(
json.dumps(
{
"ok": False,
"error": "USAGE: serve",
},
sort_keys=True,
)
)
raise SystemExit(64)
@@ -0,0 +1,66 @@
#!/usr/bin/env python3
import json
import os
import socket
import sys
from pathlib import Path
HOME = Path.home()
ROOT = Path(
os.environ.get(
"FRAMEBRIDGE_TOOL_ROOT",
str(HOME / ".local/share/framebridge/tool-adapter")
)
)
SOCKET_PATH = ROOT / "adapter.sock"
if len(sys.argv) != 2:
print("USAGE: framebridge_tool_client.py REQUEST.json")
raise SystemExit(64)
request_path = Path(sys.argv[1])
request = json.loads(
request_path.read_text(encoding="utf-8")
)
raw = (
json.dumps(
request,
ensure_ascii=True,
separators=(",", ":"),
)
+ "\n"
).encode("utf-8")
sock = socket.socket(
socket.AF_UNIX,
socket.SOCK_STREAM,
)
sock.connect(str(SOCKET_PATH))
sock.sendall(raw)
sock.shutdown(socket.SHUT_WR)
data = bytearray()
while True:
chunk = sock.recv(65536)
if not chunk:
break
data.extend(chunk)
sock.close()
response = json.loads(
bytes(data).decode("utf-8")
)
print(
json.dumps(
response,
ensure_ascii=True,
sort_keys=True,
)
)