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,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"
+118
View File
@@ -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()
+241
View File
@@ -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()
+98
View File
@@ -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()
+10
View File
@@ -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())
+10
View File
@@ -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()
+258
View File
@@ -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 "$@"