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,3 @@
|
||||
# Inactive until an administrator explicitly enables a named user.
|
||||
ACTION=="add", SUBSYSTEM=="graphics", KERNEL=="fb0", TEST=="/etc/framebridge-expanded/screen-access.json", RUN+="/usr/lib/framebridge-expanded/bin/framebridge-screen-access apply-device"
|
||||
ACTION=="change", SUBSYSTEM=="graphics", KERNEL=="fb0", TEST=="/etc/framebridge-expanded/screen-access.json", RUN+="/usr/lib/framebridge-expanded/bin/framebridge-screen-access apply-device"
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/python3
|
||||
"""Archive the reviewed community source layout without runtime profiles or QA logs."""
|
||||
import calendar
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import tarfile
|
||||
|
||||
BASE = Path(__file__).resolve().parents[1]
|
||||
ROOT = BASE.parent
|
||||
EPOCH = calendar.timegm((2026, 10, 26, 0, 0, 0))
|
||||
ARCHIVE_ROOT = "FrameBridge-0.3.1"
|
||||
ARCHIVE_NAME = "FrameBridge-0.3.1-source.tar.gz"
|
||||
SOURCE_FOLDERS = ("sources", "expanded", "portable", "packaging", "docs", "tests")
|
||||
ROOT_FILES = ("README.md", "LICENSE", "LICENSING.md", "COPYRIGHT.md",
|
||||
"LICENSE-STATUS.md", "OFFICIAL-RELEASES.md", "CONTRIBUTING.md", "SECURITY.md", "RELEASE-NOTES.md")
|
||||
ROOT_FOLDERS = ("docs", "provenance", "licensing")
|
||||
BLOG_POLICY_FILES = ("README.md", "LICENSING.md", "POST-TEMPLATE.md", "LICENSE-CC-BY-SA-4.0.txt")
|
||||
ARCHIVE_README_NAME = "README-SOURCE-ARCHIVE.md"
|
||||
DEPENDENCY_SOURCE_ASSETS = [{'filename': 'FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz', 'bytes': 82606998, 'sha256': '64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a'}, {'filename': 'FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz', 'bytes': 26941745, 'sha256': '6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9'}]
|
||||
ARCHIVE_README = "# FrameBridge 0.3.1 source archive\n\nThis archive preserves the repository layout needed by the offline builder.\nFrom this directory, run python3 source/packaging/build_expanded.py to build\nFrameBridge-0.3.1.deb. The builder writes source/build and source/release;\nit does not install or start FrameBridge. Debian's dpkg-deb and the documented\nbuild prerequisites must already be available.\n\nOriginal FrameBridge material is GNU GPL version 3 only. Read LICENSE,\nLICENSING.md, COPYRIGHT.md, LICENSE-STATUS.md and OFFICIAL-RELEASES.md for\nscope, attribution, third-party terms and the official-release trust model.\n\nThe project source asset is accompanied by these separate dependency assets:\n\n- FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz\n 82,606,998 bytes\n SHA-256 64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a\n- FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz\n 26,941,745 bytes\n SHA-256 6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9\n\nThe Preview1 filename deliberately preserves the identity of an immutable\nshared asset. Retain licensing/third-party/R2-SUPPLEMENTS.json and its referenced\nsource notices with the dependency sources. The source indices and\nSOURCE-AVAILABILITY.md there record scope and provenance. Do not substitute\nunrelated archives. The retained tunnel executable is not claimed to have been\nindependently rebuilt from an identified own-source revision.\n\nThe blog directory contains licensing policies and the post template.\nEditorial article drafts are omitted because they are not source build inputs.\n\nOnly source/evidence/BASELINE.json is included from technical evidence. Runtime\nprofiles, private QA logs, generated build/release files and Git history are\noutside the source archive allowlist.\n\nA completed archive or builder run is not a declaration of public readiness.\nRelease checksums, signatures and publication status are recorded separately.\n".encode("utf-8")
|
||||
GPL_SHA256 = "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
def source_files():
|
||||
selected = {}
|
||||
|
||||
def add(path):
|
||||
relative = path.relative_to(ROOT)
|
||||
if path.is_symlink():
|
||||
raise RuntimeError("Source archives must not include symbolic links: " + str(relative))
|
||||
if "__pycache__" in relative.parts or path.suffix == ".pyc":
|
||||
return
|
||||
if path.is_file():
|
||||
selected[relative.as_posix()] = path
|
||||
|
||||
for folder in SOURCE_FOLDERS:
|
||||
directory = BASE / folder
|
||||
if not directory.is_dir() or directory.is_symlink():
|
||||
raise RuntimeError("Required source directory missing: " + folder)
|
||||
for path in directory.rglob("*"):
|
||||
add(path)
|
||||
# The builder needs this input; no other evidence, QA log, or diff is selected.
|
||||
baseline = BASE / "evidence/BASELINE.json"
|
||||
if not baseline.is_file():
|
||||
raise RuntimeError("Required build baseline missing.")
|
||||
add(baseline)
|
||||
for name in ROOT_FILES:
|
||||
path = ROOT / name
|
||||
if not path.is_file():
|
||||
raise RuntimeError("Required repository input missing: " + name)
|
||||
add(path)
|
||||
for name in ROOT_FOLDERS:
|
||||
directory = ROOT / name
|
||||
if not directory.is_dir() or directory.is_symlink():
|
||||
raise RuntimeError("Required repository directory missing: " + name)
|
||||
for path in directory.rglob("*"):
|
||||
add(path)
|
||||
# Preserve policy references, but do not publish draft blog articles implicitly.
|
||||
for name in BLOG_POLICY_FILES:
|
||||
path = ROOT / "blog" / name
|
||||
if not path.is_file():
|
||||
raise RuntimeError("Required blog licensing policy input missing: " + name)
|
||||
add(path)
|
||||
if sha(ROOT / "LICENSE") != GPL_SHA256:
|
||||
raise RuntimeError("The GPLv3 text differs from the recorded official license.")
|
||||
required_notices = ROOT / "licensing/third-party"
|
||||
for name in ("THIRD-PARTY-NOTICES.md", "DEPENDENCY-SOURCES.json", "SOURCE-AVAILABILITY.md", "R2-SUPPLEMENTS.json", "NATIVE-DEPENDENCY-TERMS.json", "NATIVE-DEPENDENCY-TERMS.md", "TUNNEL-DEPENDENCY-SOURCES.json"):
|
||||
path = required_notices / name
|
||||
if not path.is_file() or not path.stat().st_size:
|
||||
raise RuntimeError("Required third-party source/notice input missing: " + name)
|
||||
return selected
|
||||
|
||||
def main():
|
||||
names = source_files()
|
||||
release = BASE / "release"
|
||||
release.mkdir(exist_ok=True)
|
||||
output = release / ARCHIVE_NAME
|
||||
with output.open("wb") as raw:
|
||||
with gzip.GzipFile(filename="", mode="wb", fileobj=raw,
|
||||
mtime=EPOCH, compresslevel=6) as zipped:
|
||||
with tarfile.open(fileobj=zipped, mode="w", format=tarfile.GNU_FORMAT) as archive:
|
||||
info = tarfile.TarInfo(ARCHIVE_ROOT + "/" + ARCHIVE_README_NAME)
|
||||
info.uid = info.gid = 0
|
||||
info.uname = info.gname = ""
|
||||
info.mtime = EPOCH
|
||||
info.mode = 0o644
|
||||
info.size = len(ARCHIVE_README)
|
||||
archive.addfile(info, io.BytesIO(ARCHIVE_README))
|
||||
for relative, path in sorted(names.items()):
|
||||
info = archive.gettarinfo(str(path), arcname=ARCHIVE_ROOT + "/" + relative)
|
||||
info.uid = info.gid = 0
|
||||
info.uname = info.gname = ""
|
||||
info.mtime = EPOCH
|
||||
info.mode = 0o755 if path.stat().st_mode & 0o111 else 0o644
|
||||
with path.open("rb") as stream:
|
||||
archive.addfile(info, stream)
|
||||
manifest = {"status": "COMMUNITY_RELEASE_BUILD", "archive": output.name,
|
||||
"bytes": output.stat().st_size, "sha256": sha(output),
|
||||
"files": [{"path": ARCHIVE_README_NAME,
|
||||
"sha256": hashlib.sha256(ARCHIVE_README).hexdigest(), "generated": True}]
|
||||
+ [{"path": relative, "sha256": sha(path)}
|
||||
for relative, path in sorted(names.items())],
|
||||
"draft_blog_articles_included": False,
|
||||
"evidence_allowlist": ["source/evidence/BASELINE.json"],
|
||||
"dependency_sources": DEPENDENCY_SOURCE_ASSETS[0]["filename"],
|
||||
"dependency_source_archives": DEPENDENCY_SOURCE_ASSETS}
|
||||
(release / "SOURCE-ARCHIVE-MANIFEST.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
print(json.dumps({"archive": str(output), "bytes": output.stat().st_size,
|
||||
"sha256": manifest["sha256"], "files": len(names) + 1,
|
||||
"status": manifest["status"]}))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+241
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/python3
|
||||
"""Reproducible Debian package builder; never installs or starts the payload."""
|
||||
import calendar
|
||||
import gzip
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
BASE = Path(__file__).resolve().parents[1]
|
||||
ROOT = BASE.parent
|
||||
VERSION = "0.3.1"
|
||||
PUBLIC_FILENAME = "FrameBridge-0.3.1.deb"
|
||||
GPL_SHA256 = "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"
|
||||
EPOCH = calendar.timegm((2026, 10, 26, 0, 0, 0))
|
||||
PACKAGE = "framebridge-original-expanded"
|
||||
RELEASE = BASE / "release"
|
||||
STAGE = BASE / "build/package-root"
|
||||
PREFIX = STAGE / "usr/lib/framebridge-expanded"
|
||||
|
||||
DEPENDENCY_SOURCE_ASSETS = [{'filename': 'FrameBridge-0.3.1-Community-Preview1-dependency-sources.tar.gz', 'bytes': 82606998, 'sha256': '64540a85bd65eac3f394d8c0a967cacbc00e112676d57d644dcd1bb691c1926a'}, {'filename': 'FrameBridge-0.3.1-tunnel-client-dependency-sources.tar.gz', 'bytes': 26941745, 'sha256': '6445d4f0e773aba833fe5c610fbae54102b92de2ce636438f769f21e890c2bd9'}]
|
||||
|
||||
def sha(path):
|
||||
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
# These are release inputs, not files inferred from installed runtime profiles.
|
||||
LEGAL_FILES = ("LICENSE", "LICENSING.md", "COPYRIGHT.md", "LICENSE-STATUS.md", "OFFICIAL-RELEASES.md")
|
||||
THIRD_PARTY_REQUIRED = ("THIRD-PARTY-NOTICES.md", "DEPENDENCY-SOURCES.json", "SOURCE-AVAILABILITY.md", "R2-SUPPLEMENTS.json", "NATIVE-DEPENDENCY-TERMS.json", "NATIVE-DEPENDENCY-TERMS.md", "TUNNEL-DEPENDENCY-SOURCES.json")
|
||||
BLOG_POLICY_FILES = ("README.md", "LICENSING.md", "POST-TEMPLATE.md", "LICENSE-CC-BY-SA-4.0.txt")
|
||||
|
||||
def regular_files(directory):
|
||||
for path in sorted(directory.rglob("*")):
|
||||
if path.is_symlink():
|
||||
raise RuntimeError("Legal input must not be a symbolic link: " + str(path.relative_to(ROOT)))
|
||||
if path.is_file():
|
||||
yield path
|
||||
|
||||
def legal_preflight():
|
||||
for relative in LEGAL_FILES:
|
||||
path = ROOT / relative
|
||||
if not path.is_file() or path.is_symlink():
|
||||
raise RuntimeError("Required project legal input missing: " + relative)
|
||||
if sha(ROOT / "LICENSE") != GPL_SHA256:
|
||||
raise RuntimeError("The GPLv3 legal text differs from the recorded official text.")
|
||||
directory = ROOT / "licensing/third-party"
|
||||
for name in THIRD_PARTY_REQUIRED:
|
||||
path = directory / name
|
||||
if not path.is_file() or path.is_symlink() or not path.stat().st_size:
|
||||
raise RuntimeError("Required third-party legal input missing: licensing/third-party/" + name)
|
||||
index = json.loads((directory / "DEPENDENCY-SOURCES.json").read_text())
|
||||
if not isinstance(index, (dict, list)):
|
||||
raise RuntimeError("Dependency source index must be a JSON object or array.")
|
||||
if not isinstance(index, dict) or index.get("archive") != DEPENDENCY_SOURCE_ASSETS[0]:
|
||||
raise RuntimeError("The immutable Python/Rust dependency source identity changed.")
|
||||
native = json.loads((directory / "TUNNEL-DEPENDENCY-SOURCES.json").read_text())
|
||||
expected = DEPENDENCY_SOURCE_ASSETS[1]
|
||||
if (native.get("archive"), native.get("size_bytes"), native.get("sha256")) != (expected["filename"], expected["bytes"], expected["sha256"]):
|
||||
raise RuntimeError("The tunnel dependency source identity changed.")
|
||||
licenses = directory / "licenses"
|
||||
if not licenses.is_dir() or not any(regular_files(licenses)):
|
||||
raise RuntimeError("Third-party license texts are missing.")
|
||||
supplement = licenses / "wit-bindgen-rt-0.39.0-LICENSE-MIT.txt"
|
||||
if not supplement.is_file() or sha(supplement) != "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3":
|
||||
raise RuntimeError("The pinned wit-bindgen source notice supplement is missing or changed.")
|
||||
for name in BLOG_POLICY_FILES:
|
||||
if not (ROOT / "blog" / name).is_file():
|
||||
raise RuntimeError("Required referenced blog policy input missing: blog/" + name)
|
||||
# Do not accidentally ship local workstation paths in newly assembled notices.
|
||||
private_roots = (str(ROOT.resolve()), str(BASE.resolve()), str(Path.home()))
|
||||
inputs = [ROOT / name for name in LEGAL_FILES] + list(regular_files(ROOT / "licensing"))
|
||||
for path in inputs:
|
||||
data = path.read_bytes()
|
||||
for private_root in private_roots:
|
||||
if private_root.encode() in data:
|
||||
raise RuntimeError("Local path found in legal input: " + str(path.relative_to(ROOT)))
|
||||
return index
|
||||
|
||||
def install_legal_material(docs):
|
||||
for relative in LEGAL_FILES:
|
||||
shutil.copy2(ROOT / relative, docs / relative)
|
||||
for directory in ("licensing", "docs", "provenance"):
|
||||
source = ROOT / directory
|
||||
for path in regular_files(source):
|
||||
destination = docs / directory / path.relative_to(source)
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(path, destination)
|
||||
(docs / "blog").mkdir()
|
||||
for name in BLOG_POLICY_FILES:
|
||||
shutil.copy2(ROOT / "blog" / name, docs / "blog" / name)
|
||||
shutil.copy2(ROOT / "RELEASE-NOTES.md", docs / "RELEASE-NOTES.md")
|
||||
(docs / "copyright").write_text(
|
||||
"FrameBridge Original Expanded — Community Release 0.3.1\n\n"
|
||||
"Original FrameBridge material: GNU GPL version 3 only (GPL-3.0-only).\n"
|
||||
"The complete license is included in LICENSE. COPYRIGHT.md and LICENSING.md\n"
|
||||
"identify attribution, scope and third-party exceptions.\n\n"
|
||||
"Third-party material retains its applicable copyrights and terms.\n"
|
||||
"See licensing/third-party/THIRD-PARTY-NOTICES.md, retained native terms\n"
|
||||
"and the original notices alongside vendored components.\n\n"
|
||||
"Official release signatures and public readiness are recorded separately.\n")
|
||||
(docs / "THIRD-PARTY-NOTICES.md").write_text(
|
||||
"# Included third-party notices\n\n"
|
||||
"The assembled notices and license texts are included under\n"
|
||||
"[licensing/third-party](licensing/third-party/THIRD-PARTY-NOTICES.md).\n"
|
||||
"Original vendored notices are preserved with their components.\n")
|
||||
lines = ["# Source availability for FrameBridge 0.3.1", "",
|
||||
"Project source archive: FrameBridge-0.3.1-source.tar.gz.", "",
|
||||
"The release source set also includes these separate assets:", ""]
|
||||
for item in DEPENDENCY_SOURCE_ASSETS:
|
||||
lines.extend(["- " + item["filename"],
|
||||
" Bytes: " + str(item["bytes"]),
|
||||
" SHA-256: " + item["sha256"]])
|
||||
lines.extend(["", "The Preview1 name deliberately identifies an immutable shared asset.",
|
||||
"Retain the R2 source-notice supplement under licensing/third-party.",
|
||||
"The tunnel dependency asset records exact dependency archives and",
|
||||
"retained notices; it does not assert an independently rebuilt tunnel",
|
||||
"executable or an identified exact own-source revision.", "",
|
||||
"See licensing/third-party/SOURCE-AVAILABILITY.md and both dependency",
|
||||
"source indices for scope and provenance. Publication readiness and",
|
||||
"signature status are external release records, not builder claims.", ""])
|
||||
(docs / "SOURCE-AVAILABILITY.md").write_text("\n".join(lines))
|
||||
(docs / "COMMUNITY-RELEASE-STATUS.json").write_text(json.dumps({
|
||||
"schema_version": 1, "version": VERSION, "status": "COMMUNITY_RELEASE_BUILD",
|
||||
"certified_as_RC7": False,
|
||||
"public_readiness": "EXTERNAL_RELEASE_RECORD",
|
||||
"signature_status": "EXTERNAL_RELEASE_RECORD",
|
||||
"vm_lifecycle_validation": "NOT_ASSERTED_BY_BUILDER",
|
||||
"source_archive": "FrameBridge-0.3.1-source.tar.gz",
|
||||
"dependency_source_archives": DEPENDENCY_SOURCE_ASSETS
|
||||
}, indent=2) + "\n")
|
||||
|
||||
def main():
|
||||
legal_preflight()
|
||||
baseline = json.loads((BASE / "evidence/BASELINE.json").read_text())
|
||||
for relative, expected in baseline["source_hashes"].items():
|
||||
assert sha(BASE / "sources" / relative) == expected, "Original source changed: " + relative
|
||||
if STAGE.exists():
|
||||
shutil.rmtree(STAGE)
|
||||
PREFIX.mkdir(parents=True)
|
||||
RELEASE.mkdir(exist_ok=True)
|
||||
for name, dest in [("original-asus", "asus"), ("original-geranas", "geranas"), ("original-gerameg", "gerameg")]:
|
||||
source = BASE / "sources" / name
|
||||
assert source.is_dir(), "Required public source snapshot missing: " + name
|
||||
shutil.copytree(source, PREFIX / "original" / dest, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
||||
for source in ("portable", "expanded", "docs"):
|
||||
shutil.copytree(BASE / source, PREFIX / source, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
||||
(PREFIX / "bin").mkdir()
|
||||
for name in ("framebridge", "framebridge-original", "framebridge-native-expanded", "framebridge-screen-access"):
|
||||
shutil.copy2(BASE / "packaging" / name, PREFIX / "bin" / name)
|
||||
(PREFIX / "bin" / name).chmod(0o755)
|
||||
rules = STAGE / "usr/lib/udev/rules.d"
|
||||
rules.mkdir(parents=True)
|
||||
shutil.copy2(BASE / "packaging/70-framebridge-screen.rules", rules / "70-framebridge-screen.rules")
|
||||
layout = {
|
||||
"adapter": "original/asus/tool-adapter/bin/framebridge_tool_adapter.py",
|
||||
"adapter_bin": "original/asus/tool-adapter/bin",
|
||||
"fbterm_bin": "original/asus/fbterm/bin",
|
||||
"native_host": "original/asus/framebridge-native/host/framebridge_host.py",
|
||||
"native_host_dir": "original/asus/framebridge-native/host"
|
||||
}
|
||||
(PREFIX / "layout.json").write_text(json.dumps(layout, indent=2) + "\n")
|
||||
bindir = STAGE / "usr/bin"
|
||||
bindir.mkdir(parents=True)
|
||||
for name in ("framebridge", "fb", "framebridge-expanded"):
|
||||
(bindir / name).symlink_to("../lib/framebridge-expanded/bin/framebridge")
|
||||
applications = STAGE / "usr/share/applications"
|
||||
applications.mkdir(parents=True)
|
||||
(applications / "framebridge-console.desktop").write_text(
|
||||
"[Desktop Entry]\nVersion=1.0\nType=Application\nName=FrameBridge Console\n"
|
||||
"Comment=Open the persistent local Bash and FBTERM activity console\n"
|
||||
"Exec=/usr/bin/fb console\nIcon=utilities-terminal\nTerminal=false\n"
|
||||
"Categories=System;TerminalEmulator;\nStartupWMClass=Gnome-terminal\n")
|
||||
docs = STAGE / "usr/share/doc" / PACKAGE
|
||||
docs.mkdir(parents=True)
|
||||
for file in sorted((BASE / "docs").glob("*")):
|
||||
if file.is_file():
|
||||
shutil.copy2(file, docs / file.name)
|
||||
readme = (BASE / "docs/DEBIAN-TUTORIAL.md").read_bytes()
|
||||
(docs / "README.Debian.gz").write_bytes(gzip.compress(readme, mtime=EPOCH))
|
||||
install_legal_material(docs)
|
||||
man = STAGE / "usr/share/man/man1"
|
||||
man.mkdir(parents=True)
|
||||
man_source = (BASE / "docs/framebridge.1").read_bytes() if (BASE / "docs/framebridge.1").exists() else b".TH FRAMEBRIDGE 1\n.SH NAME\nframebridge - FrameBridge Original Expanded\n.SH SYNOPSIS\nframebridge [--state-dir PATH] setup|start|stop|status|doctor\n.SH DESCRIPTION\nRun framebridge tutorial for the Debian installation and usage guide.\n"
|
||||
(man / "framebridge.1.gz").write_bytes(gzip.compress(man_source, mtime=EPOCH))
|
||||
(man / "fb.1.gz").symlink_to("framebridge.1.gz")
|
||||
control = STAGE / "DEBIAN"
|
||||
control.mkdir()
|
||||
installed = sum(p.stat().st_size for p in STAGE.rglob("*") if p.is_file() and not p.is_symlink())
|
||||
(control / "control").write_text(
|
||||
"Package: " + PACKAGE + "\nVersion: " + VERSION + "\nSection: utils\nPriority: optional\nArchitecture: amd64\n"
|
||||
"Maintainer: FrameBridge <framebridge@localhost>\n"
|
||||
"Homepage: https://framebridge.betancourt.one/\n"
|
||||
"Installed-Size: " + str((installed + 1023) // 1024) + "\n"
|
||||
"Depends: python3 (>= 3.13), python3 (<< 3.14), libc6 (>= 2.34), libgcc-s1, python3-cryptography, sudo, libpam-systemd, dbus-user-session, udev, libx11-6, ca-certificates, iproute2, procps, systemd, bash, tmux, coreutils, util-linux, mawk | gawk, gnome-terminal, wmctrl, xdotool, x11-utils, xclip, imagemagick, xterm, xvfb, openbox, dbus-x11, xauth, x11-xkb-utils, fonts-dejavu-core\n"
|
||||
"Description: FrameBridge with standalone, central and node modes\n"
|
||||
" Original execution and desktop programs, a real persistent terminal,\n"
|
||||
" authenticated node enrollment, and a manual ChatGPT tunnel wizard.\n"
|
||||
" Includes real active-screen capture, saved original PNG viewing,\n"
|
||||
" per-console sudo capability, explicit read-only framebuffer access,\n"
|
||||
" a shared graphical/text console, and an English Debian tutorial.\n"
|
||||
" Optional virtual displays are explicitly identified as virtual.\n"
|
||||
" No phone or Owner Device pairing. Services start after connection setup.\n"
|
||||
" Installing the package alone does not activate a connection.\n")
|
||||
(control / "prerm").write_text(
|
||||
"#!/bin/sh\nset -eu\n"
|
||||
"case \"$1\" in\n"
|
||||
" remove|deconfigure)\n"
|
||||
" /usr/bin/python3 -I -S /usr/lib/framebridge-expanded/expanded/framebridge_expanded/uninstall.py --all-users\n"
|
||||
" if [ -e /etc/framebridge-expanded/screen-access.json ] || [ -L /etc/framebridge-expanded/screen-access.json ]; then\n"
|
||||
" /usr/lib/framebridge-expanded/bin/framebridge-screen-access disable\n"
|
||||
" fi\n"
|
||||
" ;;\nesac\n")
|
||||
(control / "prerm").chmod(0o755)
|
||||
rows = []
|
||||
for path in sorted(STAGE.rglob("*")):
|
||||
if path.is_file() and not path.is_symlink() and control not in path.parents:
|
||||
rows.append(hashlib.md5(path.read_bytes()).hexdigest() + " " + str(path.relative_to(STAGE)))
|
||||
(control / "md5sums").write_text("\n".join(rows) + "\n")
|
||||
for path in STAGE.rglob("*"):
|
||||
if path.is_symlink():
|
||||
continue
|
||||
if path.is_dir():
|
||||
path.chmod(0o755)
|
||||
elif os.access(path, os.X_OK):
|
||||
path.chmod(0o755)
|
||||
else:
|
||||
path.chmod(0o644)
|
||||
os.utime(path, (EPOCH, EPOCH), follow_symlinks=False)
|
||||
manifest = [{"path": str(p.relative_to(STAGE)), "sha256": sha(p), "bytes": p.stat().st_size}
|
||||
for p in sorted(STAGE.rglob("*")) if p.is_file() and not p.is_symlink()]
|
||||
(RELEASE / "PACKAGE-MANIFEST.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
||||
output = RELEASE / ("framebridge-expanded_" + VERSION + "_amd64.deb")
|
||||
env = dict(os.environ, SOURCE_DATE_EPOCH=str(EPOCH))
|
||||
subprocess.run(["dpkg-deb", "--root-owner-group", "-Zxz", "-z6", "--build", str(STAGE), str(output)], check=True, env=env)
|
||||
shutil.copy2(output, RELEASE / PUBLIC_FILENAME)
|
||||
print(json.dumps({"package": str(output), "bytes": output.stat().st_size, "sha256": sha(output), "payload_entries": len(manifest), "public_filename": PUBLIC_FILENAME, "status": "COMMUNITY_RELEASE_BUILD"}))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+98
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/python3
|
||||
"""Build the original snapshot on GERAMEG; does not install or start software."""
|
||||
import calendar
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import time
|
||||
|
||||
BASE = Path(__file__).resolve().parents[1]
|
||||
VERSION = "0.1.0~original.20261021.1"
|
||||
EPOCH = calendar.timegm((2026, 10, 21, 0, 0, 0))
|
||||
RELEASE = BASE / "release"
|
||||
STAGE = BASE / "build/package-root"
|
||||
PREFIX = STAGE / "usr/lib/framebridge-original"
|
||||
|
||||
def sha(p):
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
def main():
|
||||
assert (BASE / "portable/front_door_mcp.py").is_file(), "portable source missing"
|
||||
assert (BASE / "sources/original-asus/SOURCE-MANIFEST.json").is_file(), "ASUS snapshot missing"
|
||||
assert (BASE / "sources/original-geranas/MANIFEST.json").is_file(), "GERANAS snapshot missing"
|
||||
if STAGE.exists():
|
||||
shutil.rmtree(STAGE)
|
||||
RELEASE.mkdir(exist_ok=True)
|
||||
PREFIX.mkdir(parents=True)
|
||||
shutil.copytree(BASE / "sources/original-geranas", PREFIX / "original/geranas")
|
||||
shutil.copytree(BASE / "sources/original-asus", PREFIX / "original/asus")
|
||||
shutil.copytree(BASE / "portable", PREFIX / "portable", ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
||||
(PREFIX / "bin").mkdir()
|
||||
shutil.copy2(BASE / "packaging/framebridge-original", PREFIX / "bin/framebridge-original")
|
||||
(PREFIX / "bin/framebridge-original").chmod(0o755)
|
||||
layout = {
|
||||
"adapter": "original/asus/tool-adapter/bin/framebridge_tool_adapter.py",
|
||||
"adapter_bin": "original/asus/tool-adapter/bin",
|
||||
"fbterm_bin": "original/asus/fbterm/bin",
|
||||
"native_host": "original/asus/framebridge-native/host/framebridge_host.py",
|
||||
"native_host_dir": "original/asus/framebridge-native/host",
|
||||
}
|
||||
(PREFIX / "layout.json").write_text(json.dumps(layout, indent=2) + "\n")
|
||||
bindir = STAGE / "usr/bin"
|
||||
bindir.mkdir()
|
||||
(bindir / "framebridge-original").symlink_to("../lib/framebridge-original/bin/framebridge-original")
|
||||
docs = STAGE / "usr/share/doc/framebridge-original"
|
||||
docs.mkdir(parents=True)
|
||||
for p in [BASE / "README-ORIGINAL.md", BASE / "evidencia-mt/CONEXION-MANUAL.md"]:
|
||||
if p.exists():
|
||||
shutil.copy2(p, docs / p.name)
|
||||
for p in (BASE / "portable").glob("*.diff"):
|
||||
shutil.copy2(p, docs / p.name)
|
||||
control = STAGE / "DEBIAN"
|
||||
control.mkdir()
|
||||
installed = sum(p.stat().st_size for p in STAGE.rglob("*") if p.is_file() and not p.is_symlink())
|
||||
(control / "control").write_text(
|
||||
"Package: framebridge-original\n"
|
||||
"Version: " + VERSION + "\n"
|
||||
"Section: utils\nPriority: optional\nArchitecture: amd64\n"
|
||||
"Maintainer: FrameBridge <framebridge@localhost>\n"
|
||||
"Installed-Size: " + str((installed + 1023) // 1024) + "\n"
|
||||
"Depends: python3 (>= 3.13), python3 (<< 3.14), libc6 (>= 2.34), libgcc-s1, bash, tmux, coreutils, util-linux, mawk | gawk, gnome-terminal, wmctrl, xdotool, x11-utils, xclip, imagemagick, xterm\n"
|
||||
"Description: FrameBridge Original with manual ChatGPT connection\n"
|
||||
" Exact ASUS executor and native desktop programs with GERANAS MCP dependencies\n"
|
||||
" and tunnel client. A separate profile and explicit manual startup avoid\n"
|
||||
" collision with existing FrameBridge installations. No phone pairing or\n"
|
||||
" Owner Device is required by this local executable path.\n")
|
||||
lines = []
|
||||
for p in sorted(STAGE.rglob("*")):
|
||||
if p.is_file() and not p.is_symlink() and control not in p.parents:
|
||||
lines.append(hashlib.md5(p.read_bytes()).hexdigest() + " " + str(p.relative_to(STAGE)))
|
||||
(control / "md5sums").write_text("\n".join(lines) + "\n")
|
||||
manifest = [{"path": str(p.relative_to(STAGE)), "sha256": sha(p), "bytes": p.stat().st_size}
|
||||
for p in sorted(STAGE.rglob("*")) if p.is_file() and not p.is_symlink()]
|
||||
(RELEASE / "PACKAGE-MANIFEST.json").write_text(json.dumps({
|
||||
"package": "framebridge-original", "version": VERSION, "architecture": "amd64",
|
||||
"source_host": "GERAMEG", "files": manifest}, indent=2) + "\n")
|
||||
for p in STAGE.rglob("*"):
|
||||
if not p.is_symlink():
|
||||
p.chmod(0o755 if p.is_dir() or p.stat().st_mode & 0o111 else 0o644)
|
||||
for p in sorted(STAGE.rglob("*"), reverse=True):
|
||||
os.utime(p, (EPOCH, EPOCH), follow_symlinks=False)
|
||||
STAGE.chmod(0o755)
|
||||
os.utime(STAGE, (EPOCH, EPOCH))
|
||||
output = RELEASE / ("framebridge-original_" + VERSION + "_amd64.deb")
|
||||
env = os.environ.copy()
|
||||
env["SOURCE_DATE_EPOCH"] = str(EPOCH)
|
||||
subprocess.run(["dpkg-deb", "--root-owner-group", "--uniform-compression", "-Zxz", "-z6",
|
||||
"--build", str(STAGE), str(output)], check=True, env=env)
|
||||
shutil.copyfile(output, RELEASE / "framebridge.deb")
|
||||
paths = [output, RELEASE / "framebridge.deb", RELEASE / "PACKAGE-MANIFEST.json"]
|
||||
(RELEASE / "SHA256SUMS.txt").write_text("".join(sha(p) + " " + p.name + "\n" for p in paths))
|
||||
print(json.dumps({"package": str(output), "bytes": output.stat().st_size, "sha256": sha(output),
|
||||
"files": len(manifest)}, indent=2))
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/python3
|
||||
"""FrameBridge Expanded command-line entry point."""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
prefix = Path(__file__).resolve().parents[1]
|
||||
os.environ.setdefault("FRAMEBRIDGE_EXPANDED_PREFIX", str(prefix))
|
||||
sys.path.insert(0, str(prefix / "expanded"))
|
||||
from framebridge_expanded.cli import main
|
||||
raise SystemExit(main())
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/python3
|
||||
"""Private captured-host entry point with installed-prefix discovery."""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
prefix = Path(__file__).resolve().parents[1]
|
||||
os.environ.setdefault("FRAMEBRIDGE_EXPANDED_PREFIX", str(prefix))
|
||||
sys.path.insert(0, str(prefix / "expanded"))
|
||||
from framebridge_expanded.native_host import main
|
||||
main()
|
||||
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)
|
||||
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
exec /usr/bin/python3 -I -S /usr/lib/framebridge-expanded/expanded/framebridge_expanded/screen_access.py "$@"
|
||||
Reference in new issue
Block a user