diff --git a/README.md b/README.md index ac56b5a..694e96f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Run a task on your own machine, return to the same Bash terminal, inspect an X11 desktop, or select an enrolled computer. ChatGPT remains hosted; FrameBridge runs the tools on your Linux computers. -> **Development candidate: `0.3.2~compat.20261027.2`.** This revision is being validated. It is not a signed public release. The separately signed 0.3.1 release remains a different artifact. Read the [candidate scope and validation record](docs/LINUX-COMPATIBILITY-CANDIDATE.md) before choosing a package. +> **Development candidate: `0.3.2~compat.20261027.3`.** This revision is being validated. It is not a signed public release. The separately signed 0.3.1 release remains a different artifact. Read the [candidate scope and validation record](docs/LINUX-COMPATIBILITY-CANDIDATE.md) before choosing a package. ## Start here @@ -26,14 +26,14 @@ The compatibility candidate has separate native Python builds; it is not one int | Ubuntu 24.04 | amd64 / x86_64 | 3.12 | `ubuntu24.04-amd64-py312` | | Linux Mint 22.x | amd64 / x86_64 | 3.12 | `mint22-amd64-py312` | -The preceding `.1` candidate passed VM tests on these systems, including Cinnamon on Mint 22.3, and a Debian production pilot. This `.2` revision needs its own validation. Ubuntu 26.04 and LMDE 7 builds remain experimental; other distributions, older releases and CPU architectures are outside this candidate's scope. +The preceding `.1` candidate passed VM tests on these systems, including Cinnamon on Mint 22.3, and a Debian production pilot. This `.3` revision needs its own validation. Ubuntu 26.04 and LMDE 7 builds remain experimental; other distributions, older releases and CPU architectures are outside this candidate's scope. Verify official releases with the independently trusted project key as described in [Official releases](OFFICIAL-RELEASES.md). A candidate's recorded SHA-256 identifies its reviewed file; it is not a release signature. Candidate packages are for reviewed pilots. For example, with the **Mint** candidate downloaded into the current directory: ~~~bash -sudo apt install ./FrameBridge-0.3.2~compat.20261027.2-mint22-amd64-py312.deb +sudo apt install ./FrameBridge-0.3.2~compat.20261027.3-mint22-amd64-py312.deb ~~~ Use the corresponding profile filename on Debian or Ubuntu. Review APT's proposed changes for the actual computer. Installation places the software on disk; it does not establish a ChatGPT connection or replace the system Python. diff --git a/docs/COMMAND-REFERENCE.md b/docs/COMMAND-REFERENCE.md index d6efdb6..4a9a9e4 100644 --- a/docs/COMMAND-REFERENCE.md +++ b/docs/COMMAND-REFERENCE.md @@ -1,6 +1,6 @@ # FrameBridge — command guide -Version: 0.3.2~compat.20261027.2 | Guide schema: 1 +Version: 0.3.2~compat.20261027.3 | Guide schema: 1 This reference is generated from the shared CLI/MCP guide. Use the matching installed version. @@ -36,7 +36,7 @@ fb --json status Install the .deb built for your exact Debian 13, Ubuntu 24.04/26.04, Linux Mint 22.x or LMDE 7 amd64 profile. These compatibility candidates still require installation and lifecycle validation on each target. Select the artifact from the matching profile folder and keep the distribution system Python. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings. ~~~sh -sudo apt install ./framebridge-expanded_0.3.2~compat.20261027.2_amd64.deb +sudo apt install ./framebridge-expanded_0.3.2~compat.20261027.3_amd64.deb fb connect ~~~ diff --git a/source/docs/COMMAND-GUIDE.json b/source/docs/COMMAND-GUIDE.json index 53141ff..3c4ed04 100644 --- a/source/docs/COMMAND-GUIDE.json +++ b/source/docs/COMMAND-GUIDE.json @@ -1,12 +1,12 @@ { "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topics": { "start": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "start", "title": "Start with FrameBridge", @@ -24,7 +24,7 @@ "heading": "Install and connect", "body": "Install the .deb built for your exact Debian 13, Ubuntu 24.04/26.04, Linux Mint 22.x or LMDE 7 amd64 profile. These compatibility candidates still require installation and lifecycle validation on each target. Select the artifact from the matching profile folder and keep the distribution system Python. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings.", "commands": [ - "sudo apt install ./framebridge-expanded_0.3.2~compat.20261027.2_amd64.deb", + "sudo apt install ./framebridge-expanded_0.3.2~compat.20261027.3_amd64.deb", "fb connect" ] }, @@ -64,12 +64,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "4a85d9721ac3f758837a57126484b6cca56a598088f90365de2796f0b8d13766" + "guide_sha256": "b5be618bfb62d102ebec222370882da129c37e92044ebe86cd9203def5c0b272" }, "commands": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "commands", "title": "Command reference", @@ -136,12 +136,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "e8680e094c647dc991d7bc9e4800d3978502dad81cf101f10d072be88dc4e9a1" + "guide_sha256": "043a13ee822b03113ca877351986ee4d25628fc31bf623a9f23134c1980545c0" }, "connection": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "connection", "title": "Connect and disconnect ChatGPT", @@ -190,12 +190,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "0c8c16baf4d28e9b37154dad049193aeae252813b18eba3159cf437918eb3416" + "guide_sha256": "3a6218c30256375c32bc80b34609f75b8465e901a2c7012389e6b5cdff995af3" }, "lifecycle": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "lifecycle", "title": "Services, connection intent and boot policy", @@ -256,12 +256,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "0e3d6c96084e40b07d7373f3f7dbe22aa7659e59ec7cff57383026918dd0948c" + "guide_sha256": "a7dd26f8acc2ffbadb47f01a9fe24b1b547853f576b7b8805e01ab9d40087cd4" }, "nodes": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "nodes", "title": "Central and node operation", @@ -310,12 +310,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "a0a317d230abb52bd673bb82453c0c17d73d31e30b1766e4247ef80ada02aa2a" + "guide_sha256": "a535fb417b3dff1c6ad022a83576ea9e001a2f2f78e99a2ca0d146747fe1300c" }, "terminals": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "terminals", "title": "Persistent Bash terminals", @@ -376,12 +376,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "e9ab7535894f07a7aa309d0f4f02d36d37c65143006f8bc67b9f12a1e00f0f5a" + "guide_sha256": "d6854b43a419e32ba16a3707d1481607d0073017daa971597bbeeee885e22f8c" }, "screens": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "screens", "title": "Authentic screen observation and input", @@ -426,12 +426,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "3d37bc0fac316fa50275f1a9f38f15e87aef435593c694aebaacf4666734e1fd" + "guide_sha256": "0b43cec434d78c75e277487cd28346c689e6f8111e0d99daf67e03904380651b" }, "diagnostics": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "diagnostics", "title": "Status and diagnosis", @@ -476,12 +476,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "9cdab418287a3fcf283258782d117813bb45d23183101e5b7ee8eeb162cd38b4" + "guide_sha256": "67ddeb5dc3314452f3378099b5427bed6e3a426547c322294b29f8935302bc5e" }, "security": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "security", "title": "Authorization, distribution and safe operation", @@ -516,12 +516,12 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "64ecf0b4e25dec7faffae90ef3d48ce732f5ec2c7fb815815a625ae124230c9e" + "guide_sha256": "995ee955506bd5ac91b27363c6833d2f6872555d7b6fbb03471b5af18bbf9661" }, "compatibility": { "ok": true, "product": "FrameBridge", - "version": "0.3.2~compat.20261027.2", + "version": "0.3.2~compat.20261027.3", "guide_schema": 1, "topic": "compatibility", "title": "Compatibility with earlier commands", @@ -564,8 +564,8 @@ ], "read_only": true, "capabilities_verified": false, - "guide_sha256": "f622466edc45196d42390129d8a32424e1b0e64b7751688790061b1e8b5941d2" + "guide_sha256": "1a31c25cf96ca3f02f74984eba740dade767816537e3fac9edee9d7cebf86717" } }, - "guide_sha256": "f83c632796c7cdb3489683b1c4584f0fedec11f4aeb9321430f1a709888d6965" + "guide_sha256": "05db1724919b95443b00670ef3365512c0ac30bbd15462819c3a2236e7df7a25" } diff --git a/source/docs/COMMAND-REFERENCE.md b/source/docs/COMMAND-REFERENCE.md index d6efdb6..4a9a9e4 100644 --- a/source/docs/COMMAND-REFERENCE.md +++ b/source/docs/COMMAND-REFERENCE.md @@ -1,6 +1,6 @@ # FrameBridge — command guide -Version: 0.3.2~compat.20261027.2 | Guide schema: 1 +Version: 0.3.2~compat.20261027.3 | Guide schema: 1 This reference is generated from the shared CLI/MCP guide. Use the matching installed version. @@ -36,7 +36,7 @@ fb --json status Install the .deb built for your exact Debian 13, Ubuntu 24.04/26.04, Linux Mint 22.x or LMDE 7 amd64 profile. These compatibility candidates still require installation and lifecycle validation on each target. Select the artifact from the matching profile folder and keep the distribution system Python. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings. ~~~sh -sudo apt install ./framebridge-expanded_0.3.2~compat.20261027.2_amd64.deb +sudo apt install ./framebridge-expanded_0.3.2~compat.20261027.3_amd64.deb fb connect ~~~ diff --git a/source/docs/DEBIAN-TUTORIAL.md b/source/docs/DEBIAN-TUTORIAL.md index cd17009..036dc10 100644 --- a/source/docs/DEBIAN-TUTORIAL.md +++ b/source/docs/DEBIAN-TUTORIAL.md @@ -1,6 +1,6 @@ # FrameBridge — Debian, Ubuntu and Linux Mint installation and first use -Version: 0.3.2~compat.20261027.2 | Guide schema: 1 +Version: 0.3.2~compat.20261027.3 | Guide schema: 1 This reference is generated from the shared CLI/MCP guide. Use the matching installed version. @@ -36,7 +36,7 @@ fb --json status Install the .deb built for your exact Debian 13, Ubuntu 24.04/26.04, Linux Mint 22.x or LMDE 7 amd64 profile. These compatibility candidates still require installation and lifecycle validation on each target. Select the artifact from the matching profile folder and keep the distribution system Python. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings. ~~~sh -sudo apt install ./framebridge-expanded_0.3.2~compat.20261027.2_amd64.deb +sudo apt install ./framebridge-expanded_0.3.2~compat.20261027.3_amd64.deb fb connect ~~~ diff --git a/source/docs/framebridge.1 b/source/docs/framebridge.1 index 43b3684..7529401 100644 --- a/source/docs/framebridge.1 +++ b/source/docs/framebridge.1 @@ -1,4 +1,4 @@ -.TH FRAMEBRIDGE 1 "2026-10-27" "FrameBridge 0.3.2~compat.20261027.2" "User Commands" +.TH FRAMEBRIDGE 1 "2026-10-27" "FrameBridge 0.3.2~compat.20261027.3" "User Commands" .SH NAME framebridge, fb \- use local Linux tools and enrolled nodes with ChatGPT .SH SYNOPSIS @@ -21,7 +21,7 @@ fb \-\-json status Install the .deb built for your exact Debian 13, Ubuntu 24.04/26.04, Linux Mint 22.x or LMDE 7 amd64 profile. These compatibility candidates still require installation and lifecycle validation on each target. Select the artifact from the matching profile folder and keep the distribution system Python. Normal first connection does not require a separate setup, doctor or start sequence. Run connect in a normal interactive terminal; advanced setup is optional for central/node roles and public settings. .PP .nf -sudo apt install ./framebridge\-expanded_0.3.2~compat.20261027.2_amd64.deb +sudo apt install ./framebridge\-expanded_0.3.2~compat.20261027.3_amd64.deb fb connect .fi .SS Updating and removal diff --git a/source/expanded/framebridge_expanded/__init__.py b/source/expanded/framebridge_expanded/__init__.py index 71bce87..75fb0ab 100644 --- a/source/expanded/framebridge_expanded/__init__.py +++ b/source/expanded/framebridge_expanded/__init__.py @@ -1 +1 @@ -__version__ = "0.3.2~compat.20261027.2" +__version__ = "0.3.2~compat.20261027.3" diff --git a/source/expanded/framebridge_expanded/fleet.py b/source/expanded/framebridge_expanded/fleet.py index 6a17b8f..5e0719b 100644 --- a/source/expanded/framebridge_expanded/fleet.py +++ b/source/expanded/framebridge_expanded/fleet.py @@ -333,7 +333,8 @@ def main() -> None: streamable_http_path="/mcp") hub = FleetHub(discover_nodes, node_read) register_fleet_read_tools(server, hub) - server.run(transport="streamable-http") + from .http_server import run_streamable_mcp + run_streamable_mcp(server) if __name__ == "__main__": diff --git a/source/expanded/framebridge_expanded/http_server.py b/source/expanded/framebridge_expanded/http_server.py new file mode 100644 index 0000000..6505337 --- /dev/null +++ b/source/expanded/framebridge_expanded/http_server.py @@ -0,0 +1,22 @@ +"""Run FrameBridge's HTTP MCP endpoints without optional WebSocket detection. + +The FastMCP app owns its routes, middleware and session-manager lifespan. +Other FrameBridge transports do not use this helper. +""" + + +def run_streamable_mcp(server): + import anyio + import uvicorn + + async def serve(): + config = uvicorn.Config( + server.streamable_http_app(), + host=server.settings.host, + port=server.settings.port, + log_level=server.settings.log_level.lower(), + ws="none", + ) + await uvicorn.Server(config).serve() + + anyio.run(serve) diff --git a/source/packaging/build_profiles.py b/source/packaging/build_profiles.py index 5672836..bf82d8b 100644 --- a/source/packaging/build_profiles.py +++ b/source/packaging/build_profiles.py @@ -11,7 +11,7 @@ from pathlib import Path import re -CANDIDATE_VERSION = "0.3.2~compat.20261027.2" +CANDIDATE_VERSION = "0.3.2~compat.20261027.3" VENDOR_RELATIVE = Path("original/geranas/FrameBridge-Fleet-Hub/vendor") SOURCE_VENDOR = Path("sources/original-geranas/FrameBridge-Fleet-Hub/vendor") GLIBC_BASELINE = (2, 34) diff --git a/source/portable/front_door_mcp.py b/source/portable/front_door_mcp.py index 10d92f2..de3d4c8 100644 --- a/source/portable/front_door_mcp.py +++ b/source/portable/front_door_mcp.py @@ -838,4 +838,5 @@ from framebridge_expanded.tty_mcp import register_tools as register_tty_tools register_tty_tools(mcp, framebridge_exec) if __name__ == "__main__": - mcp.run(transport="streamable-http") + from framebridge_expanded.http_server import run_streamable_mcp + run_streamable_mcp(mcp) diff --git a/source/tests/test_http_transport.py b/source/tests/test_http_transport.py new file mode 100644 index 0000000..2770561 --- /dev/null +++ b/source/tests/test_http_transport.py @@ -0,0 +1,145 @@ +"""Actual HTTP MCP startup is independent of ambient WebSocket libraries.""" +import json +import os +from pathlib import Path +import socket +import subprocess +import sys +import tempfile +import time +import unittest +import urllib.error +import urllib.request + +from framebridge_expanded import __version__ + +SOURCE = Path(__file__).resolve().parents[1] + + +class HttpMcpTransportTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix='fb-http-transport-') + self.addCleanup(self.temporary.cleanup) + self.directory = Path(self.temporary.name) + self.ambient = self.directory/'ambient' + package = self.ambient/'websockets' + package.mkdir(parents=True) + self.marker = self.directory/'unwanted-websocket-import' + (package/'__init__.py').write_text( + "import os\nfrom pathlib import Path\n" + "Path(os.environ['FB_TEST_WS_MARKER']).write_text('imported')\n" + "raise RuntimeError('INCOMPATIBLE_AMBIENT_WEBSOCKETS')\n") + home = self.directory/'home' + home.mkdir(mode=0o700) + self.env = dict(os.environ, HOME=str(home), PYTHONNOUSERSITE='1', + PYTHONDONTWRITEBYTECODE='1', + FB_TEST_WS_MARKER=str(self.marker), + FRAMEBRIDGE_EXPANDED_STATE=str(home/'state'), + FRAMEBRIDGE_STATE_DIR=str(home/'state'), + FRAMEBRIDGE_TOOL_ROOT=str(home/'adapter'), + FRAMEBRIDGE_BROKER_SOURCE=str(SOURCE/'sources/original-geranas/FrameBridge-Broker-RC1/src'), + FRAMEBRIDGE_FLEET_SOURCE=str(SOURCE/'sources/original-geranas/FrameBridge-Fleet-Hub/app')) + # Match the child search path exported by runtime.environment(), with an + # intentionally incompatible optional library taking precedence. + self.env['PYTHONPATH'] = os.pathsep.join(map(str, [ + self.ambient, SOURCE/'expanded', SOURCE/'portable', + SOURCE/'sources/original-geranas/FrameBridge-Fleet-Hub/vendor'])) + + def test_default_websocket_detection_would_load_the_incompatible_library(self): + code = "from mcp.server.fastmcp import FastMCP; import uvicorn; uvicorn.Config(FastMCP('negative-control').streamable_http_app()).load()" + result = subprocess.run([sys.executable, '-c', code], env=self.env, + capture_output=True, text=True, timeout=15) + self.assertNotEqual(0, result.returncode) + self.assertIn('INCOMPATIBLE_AMBIENT_WEBSOCKETS', result.stderr) + self.assertTrue(self.marker.exists()) + + def start(self, name): + with socket.socket() as reservation: + reservation.bind(('127.0.0.1', 0)) + self.port = reservation.getsockname()[1] + if name == 'fleet': + command = [sys.executable, '-m', 'framebridge_expanded.fleet'] + self.env['FRAMEBRIDGE_FLEET_HUB_PORT'] = str(self.port) + else: + command = [sys.executable, str(SOURCE/'portable/front_door_mcp.py')] + self.env['FRAMEBRIDGE_FRONT_DOOR_PORT'] = str(self.port) + log = (self.directory/(name+'.log')).open('w') + self.process = subprocess.Popen(command, env=self.env, cwd=self.directory, + stdin=subprocess.DEVNULL, stdout=log, + stderr=log, start_new_session=True) + log.close() + self.addCleanup(self.stop) + self.session = None + self.protocol = None + deadline = time.monotonic()+12 + while time.monotonic() < deadline: + if self.process.poll() is not None: + self.fail('HTTP component exited: '+(self.directory/(name+'.log')).read_text()[-2000:]) + try: + reply, headers, _ = self.rpc({'jsonrpc':'2.0', 'id':1, + 'method':'initialize', 'params':{'protocolVersion':'2025-11-25', + 'capabilities':{}, 'clientInfo':{'name':'framebridge-http-qa','version':'1'}}}) + break + except urllib.error.URLError: + time.sleep(.05) + else: + self.fail('HTTP component did not become ready') + self.assertIn('result', reply) + self.assertIn('serverInfo', reply['result']) + self.assertTrue(headers.get('Content-Type', '').startswith('text/event-stream')) + self.session = headers.get('Mcp-Session-Id') + self.assertTrue(self.session) + self.protocol = reply['result']['protocolVersion'] + _, _, code = self.rpc({'jsonrpc':'2.0', 'method':'notifications/initialized'}) + self.assertEqual(202, code) + + def stop(self): + if self.process.poll() is None: + self.process.terminate() + try: + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + self.process.kill() + self.process.wait(timeout=5) + + def rpc(self, value): + headers = {'Content-Type':'application/json', + 'Accept':'application/json, text/event-stream'} + if self.session: + headers['Mcp-Session-Id'] = self.session + if self.protocol: + headers['Mcp-Protocol-Version'] = self.protocol + request = urllib.request.Request('http://127.0.0.1:'+str(self.port)+'/mcp', + data=json.dumps(value).encode(), headers=headers, method='POST') + with urllib.request.urlopen(request, timeout=3) as response: + raw = response.read().decode() + if raw and response.headers.get('Content-Type','').startswith('text/event-stream'): + raw = next(line[5:].strip() for line in raw.splitlines() if line.startswith('data:')) + return json.loads(raw) if raw else None, response.headers, response.status + + def test_fleet_initializes_and_lists_tools_without_websocket_import(self): + self.start('fleet') + result, _, status = self.rpc({'jsonrpc':'2.0','id':2,'method':'tools/list'}) + self.assertEqual(200, status) + self.assertEqual(6, len(result['result']['tools'])) + self.assertFalse(self.marker.exists()) + + def test_actual_frontdoor_help_and_execution_denial_over_streamable_http(self): + self.start('frontdoor') + result, _, status = self.rpc({'jsonrpc':'2.0','id':2,'method':'tools/list'}) + self.assertEqual(200, status) + names = {tool['name'] for tool in result['result']['tools']} + self.assertTrue({'framebridge_help','framebridge_exec'}.issubset(names)) + reply, _, _ = self.rpc({'jsonrpc':'2.0','id':3,'method':'tools/call', + 'params':{'name':'framebridge_help','arguments':{'topic':'start'}}}) + self.assertIn(__version__, json.dumps(reply['result'])) + denied, _, _ = self.rpc({'jsonrpc':'2.0','id':4,'method':'tools/call', + 'params':{'name':'framebridge_exec', + 'arguments':{'script':'true','authorized':False}}}) + self.assertIn('AUTHORIZED_TRUE_REQUIRED', json.dumps(denied['result'])) + self.assertFalse((Path(self.env['FRAMEBRIDGE_TOOL_ROOT'])/'adapter.sock').exists()) + self.assertFalse(self.marker.exists()) + + +if __name__ == '__main__': + unittest.main()