Add Debian, Ubuntu and Mint candidate profiles with scoped systemd 255 credentials

This commit is contained in:
FrameBridge compatibility build committed 2026-10-27 10:49:56 -04:00
1 parent b23819fff1
commit a48a20c936
28 files changed
+2527 -274

No files matched your search

+55 -19
View File
@@ -1,5 +1,6 @@
#!/usr/bin/python3
"""Archive the reviewed community source layout without runtime profiles or QA logs."""
import argparse
import calendar
import gzip
import hashlib
@@ -7,12 +8,15 @@ import io
import json
from pathlib import Path
import tarfile
import sys
from build_profiles import CANDIDATE_VERSION, SOURCE_VENDOR, target_profile, validate_vendor, vendor_versions
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"
EPOCH = calendar.timegm((2026, 10, 27, 0, 0, 0))
ARCHIVE_ROOT = "FrameBridge-" + CANDIDATE_VERSION
ARCHIVE_NAME = ARCHIVE_ROOT + "-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")
@@ -20,7 +24,7 @@ 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")
ARCHIVE_README = f"# FrameBridge {CANDIDATE_VERSION} compatibility candidate source\n\nThis source archive is a QA candidate, not a signed public release or evidence\nthat an OS/desktop lifecycle has been tested. Original signed 0.3.1 assets retain\ntheir separate immutable identity. Check TARGET-VENDOR-PROVENANCE.json for the\nnative ABI inputs actually included in this archive.\n\nThe offline builder requires Python 3, Debian dpkg/dpkg-deb and documented host\nbuild prerequisites. It writes source/build/<profile> and source/release/<profile>;\nit never installs a package, fetches dependencies, changes system Python or starts\nFrameBridge services. Run from this archive's top-level directory:\n\n python3 source/packaging/build_expanded.py --target debian13-amd64-py313\n python3 source/packaging/build_expanded.py --target lmde7-amd64-py313\n\nIf the cp312 input is recorded as included:\n\n python3 source/packaging/build_expanded.py --target ubuntu24.04-amd64-py312 --vendor-dir source/target-vendors/cp312\n python3 source/packaging/build_expanded.py --target mint22-amd64-py312 --vendor-dir source/target-vendors/cp312\n\nIf the cp314 input is recorded as included:\n\n python3 source/packaging/build_expanded.py --target ubuntu26.04-amd64-py314 --vendor-dir source/target-vendors/cp314\n\nThe default archive includes only the reviewed original cp313 vendor tree.\nThe archive producer can include reviewed alternate ABI trees using:\n\n python3 source/packaging/archive_sources.py --vendor-cp312 /reviewed/vendor-cp312 --vendor-cp314 /reviewed/vendor-cp314\n\nThose arguments must identify complete reviewed trees with the same 29 pinned\ndistribution names/versions as the original. The producer validates WHEEL/RECORD,\nCPython ABI, amd64 ELF and glibc baseline before including them. Local preparation\nmarkers and executable entry-point scripts are omitted. All included files and\ntheir hashes appear in SOURCE-ARCHIVE-MANIFEST.json alongside the finished archive;\nthe embedded provenance records input ABI identity without workstation paths.\n--check-vendor on build_expanded.py validates inputs without building a package.\nAn absent alternate tree must be supplied separately with equivalent provenance;\nchanging Depends cannot convert the original cp313 extensions into cp312/cp314.\n\nPackage output names contain the candidate version and a separate profile\nfolder. release-profile.json inside the installed prefix declares the exact\ncandidate profile. Build success and profile eligibility do not establish a\nvalidated installation, graphical control, real TTY or reboot lifecycle.\n\nOriginal FrameBridge material is GNU GPL version 3 only. Read LICENSE,\nLICENSING.md, COPYRIGHT.md, LICENSE-STATUS.md and OFFICIAL-RELEASES.md for scope,\nattribution, third-party terms and the official-release trust model.\n\nThe project source asset is accompanied by these separate unchanged dependency\nsource assets; they are not embedded or renamed by this helper:\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 name preserves an immutable shared asset's identity. Retain the R2\nsource-notice supplements and both dependency indices under licensing/third-party.\nMatching package versions do not constitute an independent source rebuild of the\nretained tunnel executable; its exact own-source revision is not asserted.\n\nThe blog directory contains licensing policies and the post template. Draft\narticles, private runtime profiles, QA logs, build outputs and Git history are\noutside the source allowlist. Only source/evidence/BASELINE.json is included from\ntechnical evidence. Release signatures and publication status are separate\nrecords; this candidate archiver does not sign or publish anything.\n".encode("utf-8")
GPL_SHA256 = "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"
def sha(path):
@@ -33,7 +37,7 @@ def source_files():
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":
if any(part in {"__pycache__", ".git", "build", "release", "qa"} for part in relative.parts) or path.suffix in {".pyc", ".log"}:
return
if path.is_file():
selected[relative.as_posix()] = path
@@ -75,22 +79,54 @@ def source_files():
raise RuntimeError("Required third-party source/notice input missing: " + name)
return selected
def main():
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--vendor-cp312", type=Path, help="Reviewed cp312 vendor input to include")
parser.add_argument("--vendor-cp314", type=Path, help="Reviewed cp314 vendor input to include")
args = parser.parse_args(argv)
names = source_files()
release = BASE / "release"
release.mkdir(exist_ok=True)
expected_versions = vendor_versions(BASE / SOURCE_VENDOR)
provenance = {"schema": 1, "version": CANDIDATE_VERSION, "validation_status": "candidate",
"vendors": {"cp313": {"path": "source/" + SOURCE_VENDOR.as_posix(),
**validate_vendor(BASE / SOURCE_VENDOR, target_profile("debian13-amd64-py313"), expected_versions)}}}
for abi, directory, profile_id in (("cp312", args.vendor_cp312, "ubuntu24.04-amd64-py312"),
("cp314", args.vendor_cp314, "ubuntu26.04-amd64-py314")):
if directory is None:
continue
evidence = validate_vendor(directory, target_profile(profile_id), expected_versions)
archive_path = "source/target-vendors/" + abi
provenance["vendors"][abi] = {"path": archive_path, **evidence}
for path in sorted(directory.rglob("*")):
relative = path.relative_to(directory)
if path.is_symlink():
raise RuntimeError("Alternate vendor source symlink rejected: " + str(relative))
if (not path.is_file() or any(part in {"__pycache__", ".git", "build", "release", "qa", "bin"} for part in relative.parts)
or path.suffix in {".pyc", ".log"} or path.name == "FB-ABI-PREPARED.json"):
continue
name = archive_path + "/" + relative.as_posix()
if name in names:
raise RuntimeError("Archive input path collision: " + name)
names[name] = path
provenance["omitted_vendor_abis"] = sorted({"cp312", "cp314"} - set(provenance["vendors"]))
if provenance["omitted_vendor_abis"]:
print("Candidate source excludes alternate vendor ABIs: " + ", ".join(provenance["omitted_vendor_abis"]), file=sys.stderr)
generated = {ARCHIVE_README_NAME: ARCHIVE_README,
"TARGET-VENDOR-PROVENANCE.json": (json.dumps(provenance, indent=2, sort_keys=True) + "\n").encode()}
release = BASE / "release" / "candidate-source"
release.mkdir(parents=True, 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, data in sorted(generated.items()):
info = tarfile.TarInfo(ARCHIVE_ROOT + "/" + relative)
info.uid = info.gid = 0
info.uname = info.gname = ""
info.mtime = EPOCH
info.mode = 0o644
info.size = len(data)
archive.addfile(info, io.BytesIO(data))
for relative, path in sorted(names.items()):
info = archive.gettarinfo(str(path), arcname=ARCHIVE_ROOT + "/" + relative)
info.uid = info.gid = 0
@@ -99,10 +135,10 @@ def main():
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,
manifest = {"status": "COMPATIBILITY_CANDIDATE_BUILD", "version": CANDIDATE_VERSION, "vendor_inputs": provenance, "archive": output.name,
"bytes": output.stat().st_size, "sha256": sha(output),
"files": [{"path": ARCHIVE_README_NAME,
"sha256": hashlib.sha256(ARCHIVE_README).hexdigest(), "generated": True}]
"files": [{"path": name, "sha256": hashlib.sha256(data).hexdigest(), "generated": True}
for name, data in sorted(generated.items())]
+ [{"path": relative, "sha256": sha(path)}
for relative, path in sorted(names.items())],
"draft_blog_articles_included": False,
@@ -111,7 +147,7 @@ def main():
"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,
"sha256": manifest["sha256"], "files": len(names) + len(generated),
"status": manifest["status"]}))
if __name__ == "__main__":
+58 -14
View File
@@ -1,20 +1,25 @@
#!/usr/bin/python3
"""Reproducible Debian package builder; never installs or starts the payload."""
import argparse
import ast
import calendar
import gzip
import hashlib
import json
import os
import re
from pathlib import Path
import shutil
import subprocess
from build_profiles import CANDIDATE_VERSION, PROFILES, SOURCE_VENDOR, VENDOR_RELATIVE, target_profile, validate_vendor, vendor_versions
BASE = Path(__file__).resolve().parents[1]
ROOT = BASE.parent
VERSION = "0.3.1"
VERSION = CANDIDATE_VERSION
PUBLIC_FILENAME = "FrameBridge-0.3.1.deb"
GPL_SHA256 = "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986"
EPOCH = calendar.timegm((2026, 10, 26, 0, 0, 0))
EPOCH = calendar.timegm((2026, 10, 27, 0, 0, 0))
PACKAGE = "framebridge-original-expanded"
RELEASE = BASE / "release"
STAGE = BASE / "build/package-root"
@@ -92,7 +97,7 @@ def install_legal_material(docs):
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"
"FrameBridge Original Expanded — compatibility candidate " + VERSION + "\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"
@@ -105,8 +110,8 @@ def install_legal_material(docs):
"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.", "",
lines = ["# Source availability for FrameBridge " + VERSION, "",
"Candidate source must be retained with its build identity; no public archive is asserted.", "",
"The release source set also includes these separate assets:", ""]
for item in DEPENDENCY_SOURCE_ASSETS:
lines.extend(["- " + item["filename"],
@@ -122,32 +127,70 @@ def install_legal_material(docs):
"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",
"schema_version": 1, "version": VERSION, "status": "COMPATIBILITY_CANDIDATE_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",
"source_archive": "CANDIDATE_SOURCE_REQUIRED_BEFORE_PUBLICATION",
"dependency_source_archives": DEPENDENCY_SOURCE_ASSETS
}, indent=2) + "\n")
def main():
def main(argv=None):
global VERSION, PUBLIC_FILENAME, RELEASE, STAGE, PREFIX
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--target", required=True, choices=sorted(PROFILES))
parser.add_argument("--version", default=CANDIDATE_VERSION)
parser.add_argument("--vendor-dir", type=Path, help="Reviewed target-ABI Fleet Hub vendor tree; never installed globally")
parser.add_argument("--check-vendor", action="store_true", help="Validate inputs without staging or building a package")
args = parser.parse_args(argv)
if not re.fullmatch(r"[0-9][0-9A-Za-z.+:~\-]*", args.version) or args.version == "0.3.1":
parser.error("A distinct valid Debian candidate version is required")
subprocess.run(["dpkg", "--validate-version", args.version], check=True)
VERSION = args.version
version_assignments = [node for node in ast.parse((BASE / "expanded/framebridge_expanded/__init__.py").read_text()).body
if isinstance(node, ast.Assign) and any(isinstance(item, ast.Name) and item.id == "__version__" for item in node.targets)]
if len(version_assignments) != 1 or ast.literal_eval(version_assignments[0].value) != VERSION:
raise RuntimeError("Candidate package version differs from application __version__")
profile = target_profile(args.target)
profile["package_version"] = VERSION
original_vendor = BASE / SOURCE_VENDOR
vendor = args.vendor_dir.resolve() if args.vendor_dir else original_vendor
if args.vendor_dir is None and profile["python_abi"] != "cp313":
parser.error("This target requires --vendor-dir with reviewed " + profile["python_abi"] + " wheels")
vendor_evidence = validate_vendor(vendor, profile, vendor_versions(original_vendor))
profile["vendor"] = vendor_evidence
PUBLIC_FILENAME = "FrameBridge-" + VERSION + "-" + args.target + ".deb"
RELEASE = BASE / "release" / args.target
STAGE = BASE / "build" / args.target / "package-root"
PREFIX = STAGE / "usr/lib/framebridge-expanded"
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 sha(BASE / "sources" / relative) != expected:
raise RuntimeError("Original source changed: " + relative)
if args.check_vendor:
print(json.dumps(profile, sort_keys=True))
return
if STAGE.exists():
shutil.rmtree(STAGE)
PREFIX.mkdir(parents=True)
RELEASE.mkdir(exist_ok=True)
RELEASE.mkdir(parents=True, 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
if not source.is_dir():
raise RuntimeError("Required public source snapshot missing: " + name)
shutil.copytree(source, PREFIX / "original" / dest, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
staged_vendor = PREFIX / VENDOR_RELATIVE
shutil.rmtree(staged_vendor)
shutil.copytree(vendor, staged_vendor, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "bin", "FB-ABI-PREPARED.json"))
if validate_vendor(staged_vendor, profile, vendor_versions(original_vendor)) != vendor_evidence:
raise RuntimeError("Staged vendor tree differs from reviewed input")
(PREFIX / "release-profile.json").write_text(json.dumps(profile, indent=2, sort_keys=True) + "\n")
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"):
for name in ("framebridge", "framebridge-original", "framebridge-native-expanded", "framebridge-screen-access", "framebridge-credential-helper"):
shutil.copy2(BASE / "packaging" / name, PREFIX / "bin" / name)
(PREFIX / "bin" / name).chmod(0o755)
rules = STAGE / "usr/lib/udev/rules.d"
@@ -193,7 +236,7 @@ def main():
"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"
"Depends: python3 (>= " + profile["python_minor"] + "), python3 (<< 3." + str(int(profile["python_minor"].split(".")[1]) + 1) + "), 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"
@@ -230,12 +273,13 @@ def main():
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 / "RELEASE-PROFILE.json").write_text(json.dumps(profile, indent=2, sort_keys=True) + "\n")
(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"}))
print(json.dumps({"package": str(output), "bytes": output.stat().st_size, "sha256": sha(output), "payload_entries": len(manifest), "profile_id": args.target, "validation_status": "candidate", "public_filename": PUBLIC_FILENAME, "status": "COMPATIBILITY_CANDIDATE_BUILD"}))
if __name__ == "__main__":
main()
+193
View File
@@ -0,0 +1,193 @@
"""Explicit Debian-family candidate targets and offline vendor validation.
This module never installs dependencies or imports the target's native modules.
Profiles describe eligible build inputs, not completed OS/desktop validation.
"""
import base64
import csv
from email.parser import Parser
import hashlib
from pathlib import Path
import re
CANDIDATE_VERSION = "0.3.2~compat.20261027.1"
VENDOR_RELATIVE = Path("original/geranas/FrameBridge-Fleet-Hub/vendor")
SOURCE_VENDOR = Path("sources/original-geranas/FrameBridge-Fleet-Hub/vendor")
GLIBC_BASELINE = (2, 34)
PROFILES = {
"debian13-amd64-py313": {"os_id": "debian", "os_version": "13", "python_minor": "3.13"},
"ubuntu24.04-amd64-py312": {"os_id": "ubuntu", "os_version": "24.04", "python_minor": "3.12"},
"mint22-amd64-py312": {"os_id": "linuxmint", "os_version": "22", "python_minor": "3.12"},
"ubuntu26.04-amd64-py314": {"os_id": "ubuntu", "os_version": "26.04", "python_minor": "3.14"},
"lmde7-amd64-py313": {"os_id": "linuxmint", "os_version": "7", "python_minor": "3.13"},
}
def target_profile(profile_id):
profile = dict(PROFILES[profile_id])
profile.update(schema=1, profile_id=profile_id, architecture="amd64",
python_abi="cp" + profile["python_minor"].replace(".", ""),
validation_status="candidate", glibc_minimum="2.34")
return profile
def _normalized(name):
return re.sub(r"[-_.]+", "-", name).lower()
def distribution_metadata(directory):
directory = Path(directory)
if not directory.is_dir() or directory.is_symlink():
raise RuntimeError("Vendor input must be a real directory: " + str(directory))
for path in directory.rglob("*"):
if path.is_symlink():
raise RuntimeError("Vendor symlinks are not supported: " + str(path))
result = {}
for info in sorted(directory.glob("*.dist-info")):
metadata = info / "METADATA"
if not metadata.is_file():
raise RuntimeError("Missing vendor METADATA: " + str(info))
msg = Parser().parsestr(metadata.read_text(encoding="utf-8"))
name, version = msg.get("Name"), msg.get("Version")
if not name or not version or _normalized(name) in result:
raise RuntimeError("Missing or duplicate vendor distribution identity: " + str(info))
result[_normalized(name)] = (version, info, msg)
if not result:
raise RuntimeError("Vendor input contains no distribution metadata")
return result
def vendor_versions(directory):
return {name: value[0] for name, value in distribution_metadata(directory).items()}
def _python_allowed(requirement, minor):
# Fail closed on unsupported specifier syntax rather than silently omitting it.
version = tuple(map(int, minor.split("."))) + (0,)
for part in requirement.split(","):
match = re.fullmatch(r"\s*(>=|<=|!=|==|>|<)\s*(\d+(?:\.\d+){0,2})(\.\*)?\s*", part)
if not match:
raise RuntimeError("Unsupported Requires-Python specifier: " + requirement)
op, number, wildcard = match.groups()
components = tuple(map(int, number.split(".")))
wanted = components + (0,) * (3 - len(components))
if wildcard:
if op not in ("==", "!="):
raise RuntimeError("Invalid Requires-Python wildcard: " + requirement)
equal = version[:len(components)] == components
allowed = equal if op == "==" else not equal
else:
allowed = {">=": version >= wanted, "<=": version <= wanted,
">": version > wanted, "<": version < wanted,
"==": version == wanted, "!=": version != wanted}[op]
if not allowed:
return False
return True
def _wheel_tag_allowed(tag, profile):
parts = tag.split("-")
if len(parts) != 3:
return False
interpreters, abis, platforms = [part.split(".") for part in parts]
target = profile["python_abi"]
target_number = int(target[2:])
for interpreter in interpreters:
for abi in abis:
if abi == "none" and interpreter in ("py3", target):
python_ok = True
elif abi == target and interpreter == target:
python_ok = True
elif abi == "abi3" and re.fullmatch(r"cp3\d+", interpreter):
python_ok = int(interpreter[2:]) <= target_number
else:
python_ok = False
if not python_ok:
continue
for platform in platforms:
if platform == "any" and abi == "none":
return True
match = re.fullmatch(r"manylinux_(\d+)_(\d+)_x86_64", platform)
legacy = {"manylinux1_x86_64": (2, 5), "manylinux2010_x86_64": (2, 12),
"manylinux2014_x86_64": (2, 17)}
baseline = tuple(map(int, match.groups())) if match else legacy.get(platform)
if baseline and baseline <= GLIBC_BASELINE and abi != "none":
return True
return False
def validate_vendor(directory, profile, expected_versions=None):
directory = Path(directory)
distributions = distribution_metadata(directory)
versions = {name: value[0] for name, value in distributions.items()}
if expected_versions is not None and versions != expected_versions:
raise RuntimeError("Vendor distribution names/versions differ from reviewed source inputs")
owners = {}
native_owners = set()
packages = []
for name, (version, info, metadata) in distributions.items():
requires_python = metadata.get("Requires-Python")
if requires_python and not _python_allowed(requires_python, profile["python_minor"]):
raise RuntimeError("Vendor Requires-Python rejects target: " + name)
wheel, record = info / "WHEEL", info / "RECORD"
if not wheel.is_file() or not record.is_file():
raise RuntimeError("Missing vendor WHEEL/RECORD: " + name)
wheel_metadata = Parser().parsestr(wheel.read_text(encoding="utf-8"))
tags = wheel_metadata.get_all("Tag", [])
if not tags or not any(_wheel_tag_allowed(tag, profile) for tag in tags):
raise RuntimeError("Vendor wheel ABI/platform rejects target: " + name + " " + repr(tags))
if (wheel_metadata.get("Root-Is-Purelib", "").lower() == "false"
and any(_wheel_tag_allowed(tag, profile) and not tag.endswith("-any") for tag in tags)):
native_owners.add(name)
for row in csv.reader(record.read_text(encoding="utf-8").splitlines()):
if row:
owners.setdefault(row[0], []).append(name)
relative = Path(row[0])
if relative.is_absolute():
raise RuntimeError("Absolute vendor RECORD path: " + row[0])
# Wheel entry-point scripts are not shipped in this module tree.
if ".." in relative.parts or "bin" in relative.parts or "__pycache__" in relative.parts:
continue
file = directory / relative
if len(row) < 3 or not file.is_file():
raise RuntimeError("Missing or malformed vendor RECORD file: " + row[0])
if not row[1] and file != record:
raise RuntimeError("Vendor RECORD digest missing: " + row[0])
if row[1]:
algorithm, separator, encoded = row[1].partition("=")
wanted = base64.urlsafe_b64encode(hashlib.sha256(file.read_bytes()).digest()).decode().rstrip("=")
if algorithm != "sha256" or not separator or wanted != encoded:
raise RuntimeError("Vendor RECORD digest mismatch: " + row[0])
if row[2] and int(row[2]) != file.stat().st_size:
raise RuntimeError("Vendor RECORD size mismatch: " + row[0])
packages.append({"name": name, "version": version, "wheel_tags": tags})
native = []
for path in sorted(directory.rglob("*.so")):
relative = path.relative_to(directory).as_posix()
if len(owners.get(relative, [])) != 1:
raise RuntimeError("Native vendor file is not owned by one RECORD: " + relative)
if owners[relative][0] not in native_owners:
raise RuntimeError("Native vendor file lacks matching native WHEEL metadata: " + relative)
match = re.search(r"\.cpython-(\d+)-x86_64-linux-gnu\.so$", path.name)
if match:
if "cp" + match.group(1) != profile["python_abi"]:
raise RuntimeError("Native vendor extension ABI rejects target: " + relative)
elif not path.name.endswith(".abi3.so"):
raise RuntimeError("Native vendor extension has unrecognized ABI suffix: " + relative)
data = path.read_bytes()
if len(data) < 20 or data[:6] != b"\x7fELF\x02\x01" or int.from_bytes(data[18:20], "little") != 62:
raise RuntimeError("Native vendor extension is not ELF amd64: " + relative)
needed = [tuple(map(int, values)) for values in re.findall(rb"GLIBC_(\d+)\.(\d+)", data)]
if needed and max(needed) > GLIBC_BASELINE:
raise RuntimeError("Native vendor extension exceeds glibc 2.34 baseline: " + relative)
native.append(relative)
required = ("_cffi_backend.cpython-", "pydantic_core/_pydantic_core.cpython-", "rpds/rpds.cpython-")
if any(not any(path.startswith(prefix) for path in native) for prefix in required):
raise RuntimeError("Required cffi/pydantic_core/rpds native vendor extensions missing")
digest = hashlib.sha256()
for path in sorted(directory.rglob("*")):
if path.is_file() and "__pycache__" not in path.parts and path.suffix != ".pyc" and "bin" not in path.relative_to(directory).parts and path.name != "FB-ABI-PREPARED.json":
digest.update(path.relative_to(directory).as_posix().encode() + b"\0")
digest.update(hashlib.sha256(path.read_bytes()).digest())
return {"vendor_sha256": digest.hexdigest(), "packages": packages, "native_extensions": native}
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
# Installed root-owned; never run a privileged helper from an owner checkout.
exec /usr/bin/python3 -I -S /usr/lib/framebridge-expanded/expanded/framebridge_expanded/credential_helper.py "$@"
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/python3
"""Generate installed, MCP-export and web-guide inputs from the shared contract."""
from pathlib import Path
import json
import sys
SOURCE=Path(__file__).resolve().parents[1]
sys.path.insert(0,str(SOURCE/'expanded'))
from framebridge_expanded.guide import export_guide, GUIDE_SCHEMA
from framebridge_expanded import __version__
def markdown(data, title):
lines=['# '+title,'','Version: '+data['version']+' | Guide schema: '+str(GUIDE_SCHEMA),
'','This reference is generated from the shared CLI/MCP guide. Use the matching installed version.',
'','## Contents','']
for i,(name,entry) in enumerate(data['topics'].items(),1):
lines.append(str(i)+'. ['+entry['title']+'](#'+str(i)+'-'+name+')')
for i,(name,entry) in enumerate(data['topics'].items(),1):
lines.extend(['','<a id="'+str(i)+'-'+name+'"></a>','','## '+str(i)+'. '+entry['title'],''])
for section in entry['sections']:
lines.extend(['### '+section['heading'],'',section['body'],''])
if section['commands']:
lines.extend(['~~~sh',*section['commands'],'~~~',''])
return '\n'.join(lines).rstrip()+'\n'
def roff(text):
return text.replace('\\','\\e').replace('-','\\-')
def man(data):
lines=['.TH FRAMEBRIDGE 1 "2026-10-27" "FrameBridge '+__version__+'" "User Commands"',
'.SH NAME','framebridge, fb \\- use local Linux tools and enrolled nodes with ChatGPT',
'.SH SYNOPSIS','.B framebridge',
'[\\-\\-json] [\\-\\-state\\-dir DIRECTORY] COMMAND [ARGUMENTS]',
'.SH DESCRIPTION',
'FrameBridge provides one command-line interface for human operators and authorized agents.',
'The names fb and framebridge execute the same program.',
'The tools run on the selected Linux computers; the ChatGPT model remains hosted.']
for entry in data['topics'].values():
lines.extend(['.SH '+entry['title'].upper()])
for section in entry['sections']:
lines.extend(['.SS '+roff(section['heading']),roff(section['body'])])
if section['commands']:
lines.extend(['.PP','.nf',*(roff(x) for x in section['commands']),'.fi'])
lines.extend(['.SH AUTHORS','Programming: ChatGPT.',
'Concept and project coordination: Gerardo Betancourt.',
'.SH LICENSE','GNU GPL version 3 only, with the applicable third-party license notices.',
'See the installed copyright and source license files.',
'.SH SEE ALSO','.BR systemctl (1), .BR dpkg (1), .BR apt (8)'])
return '\n'.join(lines)+'\n'
def main():
data=export_guide();doc=SOURCE/'docs';doc.mkdir(exist_ok=True)
(doc/'COMMAND-GUIDE.json').write_text(json.dumps(data,indent=2,ensure_ascii=False)+'\n')
ref=markdown(data,'FrameBridge — command guide')
(doc/'COMMAND-REFERENCE.md').write_text(ref)
(SOURCE.parent/'docs/COMMAND-REFERENCE.md').write_text(ref)
(doc/'DEBIAN-TUTORIAL.md').write_text(markdown(data,'FrameBridge — Debian, Ubuntu and Linux Mint installation and first use'))
(doc/'framebridge.1').write_text(man(data))
print(json.dumps({'version':data['version'],'guide_schema':GUIDE_SCHEMA,'topics':len(data['topics']),'guide_sha256':data['guide_sha256'],'generated_files':5}))
if __name__=='__main__':main()