#!/usr/bin/python3 """Build the self-hosted FrameBridge website with the Python standard library.""" from pathlib import Path import html,json,re,shutil,sys BASE=Path(__file__).resolve().parent ROOT=BASE.parent DIST=BASE/'dist' META=json.loads((BASE/'release.json').read_text()) META.setdefault('git_url','/git/framebridge/framebridge') META.setdefault('git_clone_url','https://framebridge.betancourt.one/git/framebridge/framebridge.git') E=lambda value:html.escape(str(value),quote=True) READY=META.get('state')=='ready' if READY: assert re.fullmatch(r'[a-fA-F0-9]{64}',META.get('sha256','')),'Ready release requires the exact package SHA-256.' assert re.fullmatch(r'[a-fA-F0-9]{40}|[a-fA-F0-9]{64}',META.get('signing_fingerprint','')),'Ready release requires a public signing fingerprint.' assert META.get('source_archive_url') and META.get('package_signature_url') and META.get('size_bytes'),'Ready release requires source, signature and size metadata.' deps=META.get('dependency_sources',[]) assert len(deps)>=2 and all(item.get('url','').startswith('/downloads/') and re.fullmatch(r'[a-fA-F0-9]{64}',item.get('sha256','')) and item.get('size_bytes') for item in deps),'Ready release requires all dependency-source archives and their exact checksums.' for name in ['git_url','package_url','package_signature_url','checksums_url','checksums_signature_url','source_archive_url','signing_key_url']: value=META.get(name,'') assert not value or value.startswith('/') or value.startswith('https://framebridge.betancourt.one/'),'Only project-owned HTTPS or root-relative artifact URLs are allowed.' for path in ['assets','docs','download','blog/2026-10-26-opening-framebridge','community','support']: (DIST/path).mkdir(parents=True,exist_ok=True) for p in (BASE/'src').iterdir(): if p.is_file():shutil.copyfile(p,DIST/'assets'/p.name) for p in (BASE/'assets').iterdir(): if p.is_file():shutil.copyfile(p,DIST/'assets'/p.name) (DIST/'assets/release.json').write_text(json.dumps(META,indent=2)+'\n') def brand(): return 'framebridge' def header(current): items=[('Docs','/docs/','docs'),('Development','/blog/','blog'),('Source',META['git_url'],'source'),('Support','/support/','support'),('Download','/download/','download')] links=[] for title,url,key in items: links.append(''+E(title)+'') return '' def footer(): return '''''' def page(title,body,current='',light=False,description='FrameBridge connects your own ChatGPT account to Linux systems you control.'): return ''+E(title)+' · FrameBridge'+header(current)+body+footer()+'' def code(text): return '
'+E(text)+'
' release_label='Official release' if READY else 'Release preparation' home='''
Linux systems · Your ChatGPT account

Your computers.
A working conversation.

Bring commands, persistent terminals and a compatible desktop into your conversation with ChatGPT. Work on Linux systems you control, through one bridge.

GNU GPLv3 · Debian 13 · Manual connection to your own account

FRAMEBRIDGE / EXAMPLE WORKFLOW

$fb connect

Connect your own account through the guided terminal flow.

$fb status

See tools, connection and startup state in one place.

$fb console

Return to your work view: Bash and FBTERM activity.

Illustrative commands. This is not a screen capture.

Local executionPersistent terminalsGraphical + text toolsCentral + nodes

From a blinking cursor
to a connected workflow.

A conceptual look at independent tasks, consoles and connected computers. The software runs your tools on your systems; the ChatGPT model remains hosted.

Conceptual animation — not a screen recording.Original RC7 concept preview. Features and commands are documented for the current release below.

Start with one machine.
Extend when you need to.

A standalone computer can provide local tools. A central installation can also connect approved nodes.

Where work happens
Your ChatGPT accountFrameBridgeYour Linux systems

No phone pairing is required. Initial account consent and credentials stay in the normal browser or hidden terminal prompts.

  1. 01

    Install and connect

    Install the Debian package, then use the guided text interface to connect your own account. Advanced role setup is optional for ordinary standalone use.

  2. 02

    Choose the right tool

    Use FBEXEC for commands, persistent terminals for ongoing shell work, and graphical CUA when a compatible desktop is available.

  3. 03

    Keep the context

    Return to a live terminal session without rebuilding its shell state between calls. Running processes do not survive an operating-system reboot.

  4. 04

    Add approved nodes

    Use the central instance for ChatGPT transport and select enrolled nodes explicitly. Nodes do not need a second ChatGPT tunnel.

Execution

Run authorized shell tasks on the selected machine and inspect their recorded results.

FBEXEC

Terminals

Keep a real Bash session across calls. Inspect text, send input and return to ongoing work.

FBTERM / fb tty

Desktop and text

Discover compatible graphical CUA automatically. Use text-terminal tools when a graphical desktop is unavailable.

CUA / CUA-TTY

People and agents

The same command-line interface serves human operators and authorized ChatGPT agents, with discoverable usage guidance.

fb / framebridge

Independent FBEXEC tasks have been used together in the laboratory when their objectives and changes did not overlap. This is observed practice, not an isolation guarantee. Use one controller at a time for each shared graphical desktop.

Free software.
Verifiable releases.

Read the code, adapt it and contribute. Official downloads are identified by their published checksums and release signatures.

  • Version'''+E(META['version'])+'''
  • Platform'''+E(META['distribution'])+'''
  • LicenseGNU GPLv3 only
  • Channel'''+E(release_label)+'''

A small project.
An open invitation.

Good tools improve through use, careful fixes and clear documentation. Report a reproducible issue, improve the guide or propose a change in Git.

How to contribute
Opening FrameBridge: a shared interface for people and agents.Read the development note →
''' (DIST/'index.html').write_text(page('Your computers. A working conversation.',home)) notice='' if READY else '
Release preparation. Download and installation commands will appear here once the package, corresponding source and signing information have been finalized. No placeholder checksum or key should be trusted.
' facts='
Package
'+E(META['package_filename'])+'
Version
'+E(META['version'])+'
Platform
'+E(META['distribution'])+' · '+E(META['architecture'])+'
License
GNU GPLv3 only, with separate third-party notices
' if READY:facts+='
Package SHA-256
'+E(META['sha256'])+'
Signing-key fingerprint
'+E(META['signing_fingerprint'])+'
' facts+='
' manual='' apt='' if READY: def absolute(value): return value if value.startswith('https://') else META['site_url']+value manual_commands='''sudo apt install ca-certificates curl gnupg ( set -eu verify_dir="$(mktemp -d)" trap 'rm -rf "$verify_dir"' EXIT curl -fsSL "'''+absolute(META['signing_key_url'])+'''" -o "$verify_dir/project.asc" expected="'''+META['signing_fingerprint'].upper()+'''" actual="$(gpg --show-keys --with-colons "$verify_dir/project.asc" 2>/dev/null | awk -F: '$1=="fpr" {print $10; exit}')" test "$actual" = "$expected" || { printf '%s\\n' "Signing-key fingerprint mismatch."; exit 1; } gpg --batch --dearmor --output "$verify_dir/project.gpg" "$verify_dir/project.asc" curl -fsSL "'''+absolute(META['package_url'])+'''" -o "$verify_dir/'''+META['package_filename']+'''" curl -fsSL "'''+absolute(META['package_signature_url'])+'''" -o "$verify_dir/package.asc" printf '%s %s\\n' "'''+META['sha256']+'''" "$verify_dir/'''+META['package_filename']+'''" | sha256sum --check gpgv --keyring "$verify_dir/project.gpg" "$verify_dir/package.asc" "$verify_dir/'''+META['package_filename']+'''" sudo apt install "$verify_dir/'''+META['package_filename']+'''" )''' manual='
Download .debCorresponding source

Package signature · SHA256SUMS · Signed checksums

Download, verify and install

This complete terminal flow checks the expected signing-key fingerprint, package checksum and detached signature before asking APT to install the package and resolve its dependencies.

'+code(manual_commands) source_items=''.join('
  • '+E(item['label'])+'
    '+E(item['filename'])+'
    SHA-256: '+E(item['sha256'])+'
  • ' for item in META['dependency_sources']) manual+='

    Complete source materials

    The matching FrameBridge source archive above and the following dependency-source materials are distributed together for this release. Preserve their license notices when redistributing.

    ' apt_commands='''sudo apt install ca-certificates curl gnupg ( set -eu key_file="$(mktemp)" trap 'rm -f "$key_file"' EXIT curl -fsSL "'''+absolute(META['signing_key_url'])+'''" -o "$key_file" expected="'''+META['signing_fingerprint'].upper()+'''" actual="$(gpg --show-keys --with-colons "$key_file" 2>/dev/null | awk -F: '$1=="fpr" {print $10; exit}')" test "$actual" = "$expected" || { printf '%s\\n' "Signing-key fingerprint mismatch."; exit 1; } sudo install -d -m 0755 /etc/apt/keyrings sudo install -m 0644 "$key_file" /etc/apt/keyrings/framebridge.asc printf '%s\\n' "deb [signed-by=/etc/apt/keyrings/framebridge.asc] '''+META['apt_base']+''' trixie main" | sudo tee /etc/apt/sources.list.d/framebridge.list >/dev/null sudo apt update sudo apt install '''+META['package_name']+''' )''' apt='

    Install from the signed APT repository

    This project repository distributes FrameBridge; it is not a mirror of the full Debian archive. The signing key below is scoped to this repository with Signed-By.

    '+code(apt_commands)+'

    The fingerprint pins the key expected by this release page. Obtain it through this HTTPS site or another trusted project channel. Do not bypass APT signature errors.

    ' download='
    '+notice+manual+apt+'

    After installation

    Run the normal connection flow in a terminal on the computer you are connecting:

    '+code('fb connect\nfb status')+'

    Use your own account. Complete browser consent normally and enter credentials only at the hidden terminal prompts. Keep secrets out of chats, issue reports and copied command arguments.

    For diagnosis, use fb doctor. See the versioned command guide for advanced roles and operation details.

    ' (DIST/'download/index.html').write_text(page('Download and verify',download,'download')) community='''

    Start with the existing behavior

    Read the versioned guide, check the installed version and look for an existing issue. A reproducible report should name the command, expected result, actual result and relevant environment.

    Remove credentials and personal data from logs before sharing them. Never include passwords, access keys, private keys, tokens or cookies.

    Propose a focused change

    Use an issue to explain the problem or a pull request to propose a patch. Include the reason for the change and proportionate validation. Keep unrelated edits separate so they can be reviewed.

    '''+('

    Clone the project

    '+code('git clone '+META['git_clone_url']) if META.get('git_clone_url') else '')+'''

    Open the project in Git

    Official releases and free forks

    Maintainers review which changes become official FrameBridge releases. Published release checksums and signatures identify those artifacts.

    GNU GPLv3 also allows private modifications and independently distributed forks, subject to its terms. Contributing changes upstream is welcome; it is not a condition for exercising those rights. Modified builds should be clearly marked and must not be presented as official project releases.

    Read the GNU GPLv3 license. Third-party components retain their existing notices and licenses.

    Using an agent to contribute

    The command interface and MCP guide are designed for both people and authorized agents. Normal operation uses installed commands and tools. Editing or rebuilding the application is a development task that should follow the owner’s instructions.

    Project credits

    Programming: ChatGPT
    Original concept and project coordination: Gerardo Betancourt

    FrameBridge is an independent project. The credit describes how the project was developed and does not imply endorsement by OpenAI.

    ''' (DIST/'community/index.html').write_text(page('Contribute',community,'source')) post='''

    FrameBridge began with a practical goal: use a conversation with ChatGPT to do useful work on Linux computers we control. Commands, persistent terminals and a compatible desktop should fit into the same working context.

    Make the interface easier to understand

    The current development work brings everyday commands into a shorter vocabulary, keeps status observation separate from diagnosis, and explains the effects of connection and startup controls. The long command framebridge and its short alias fb use the same interface.

    The agent also needs a reliable starting point. A versioned guide and a brief MCP invitation are intended to help it discover how the installed software works, which computer receives an operation and what a command will change.

    Keep the evidence honest

    The preview video is a conceptual animation, not a recording of a live session. Real demonstrations and captures must remain distinguishable from illustrations. Independent tasks have been used together in the laboratory, but that experience does not establish automatic isolation or exclusive desktop control.

    Open source, with identifiable releases

    The project is being prepared for GNU GPLv3 distribution. Sources and package artifacts should be published together, with signatures and checksums identifying official releases. Community members remain free to adapt and redistribute the software under the license.

    The first contribution can be small: a clearer explanation, a reproducible issue or a focused fix. Review gives those changes a path into a later official version.

    Read the contribution guide →

    ''' (DIST/'blog/2026-10-26-opening-framebridge/index.html').write_text(page('Opening FrameBridge',post,'blog')) support='''

    What support can help with

    • Hosting and distribution: keeping the source code, documentation and release files available.
    • Maintenance: investigating reported problems and keeping the software usable.
    • Documentation: improving clear instructions for people and agents.
    • Development: building and reviewing focused improvements.

    Funding channel

    Funding details will be published here when a channel is available.

    Contributions are voluntary. FrameBridge remains free software under GNU GPLv3 only, whether or not you provide financial support.

    Support without a payment

    A useful bug report, a clearer explanation or a carefully scoped patch can be valuable contributions. You can also help by sharing honest demonstrations that distinguish real recordings from conceptual illustrations.

    Read the contribution guide →

    Review stays independent

    Financial support does not grant control over official releases, bypass code review or security checks, or imply a project endorsement. Official changes follow the same review and release process.

    Explore the source code →

    ''' (DIST/'support/index.html').write_text(page('Support FrameBridge',support,'support')) blog='''
    ''' (DIST/'blog/index.html').write_text(page('Development journal',blog,'blog')) guide_path=ROOT/'repository/source/docs/COMMAND-GUIDE.json' if not guide_path.exists(): guide_path=ROOT/'source/docs/COMMAND-GUIDE.json' guides={} guide_data={} if guide_path.exists(): guide_data=json.loads(guide_path.read_text()) guides=guide_data.get('topics',{}) assert isinstance(guides,dict) and all(isinstance(v,dict) for v in guides.values()) assert guide_data.get('version')==META['version'],'Guide and release versions must match.' (DIST/'assets/command-guide.json').write_text(json.dumps(guide_data,indent=2)+'\n') order=[x for x in ['start','commands','connection','lifecycle','nodes','terminals','screens','diagnostics','security'] if x in guides] order += [x for x in guides if x not in order] def docs_nav(current): entries='
  • Guide overview
  • ' for topic in order: entries+='
  • '+E(guides[topic].get('title',topic.title()))+'
  • ' return '' def paragraphs(body): if isinstance(body,list):body='\n\n'.join(str(x) for x in body) return ''.join('

    '+E(x).replace('\n','
    ')+'

    ' for x in str(body).split('\n\n') if x.strip()) for topic in order: guide=guides[topic] article='

    '+E(guide.get('title',topic))+'

    ' for section in guide.get('sections',[]): article+='

    '+E(section.get('heading',''))+'

    '+paragraphs(section.get('body','')) for command in section.get('commands',[]): article+=code(command if isinstance(command,str) else json.dumps(command,indent=2)) article+='

    This page is generated from the same versioned guide used by the CLI and MCP help tool. Use fb help '+E(topic)+' for the local reference.

    ' dest=DIST/'docs'/topic;dest.mkdir(parents=True,exist_ok=True) (dest/'index.html').write_text(page(guide.get('title',topic),'
    '+docs_nav(topic)+article+'
    ','docs',True)) hub='

    The command guide.

    One reference for human operators and authorized ChatGPT agents. The commands fb and framebridge use the same interface.

    Start with the normal connection flow. Use advanced setup only when you need to change roles or profile settings.

    '+code('fb connect\nfb status\nfb help')+'' if not guides:hub+='
    The shared command-guide export is being finalized. This staging page is not the complete published reference yet.
    ' else:hub+='

    Read the versioned guide data

    ' hub+='
    ' (DIST/'docs/index.html').write_text(page('Command guide','
    '+docs_nav('index')+hub+'
    ','docs',True)) (DIST/'404.html').write_text(page('Page not found','

    This path ends here.

    Return to FrameBridge or open the command guide.

    ')) (DIST/'robots.txt').write_text('User-agent: *\n'+('Allow: /\n' if READY else 'Disallow: /\n')) print(json.dumps({'web_root':str(DIST),'release_state':META['state'],'pages':len(list(DIST.rglob('*.html'))),'guide_topics':len(guides),'video_present':(DIST/'assets/framebridge-preview.mp4').is_file(),'ready_metadata_validated':READY},indent=2))