#!/usr/bin/env python3 """Install the stable Framia CLI using Python 3.10+ (standard library only). Distributed as install_framia.py. No login, credentials, shell, sudo or PATH changes. HTTPS authenticates the stable pointer; its SHA-256 authenticates the manifest, which authenticates the binary. This is not a signature system. """ from __future__ import annotations import argparse from contextlib import contextmanager import errno import hashlib import io import json import os from pathlib import Path import platform import re import secrets import shlex import ssl import stat import subprocess import sys import threading import time import urllib.error import urllib.parse import urllib.request OFFICIAL_ORIGIN = "https://mcp-api.framia.pro" USER_AGENT = "Framia-Installer/1.0" MAX_MANIFEST = 64 * 1024 MAX_POINTER = 16 * 1024 MAX_BINARY = 256 * 1024 * 1024 MAX_VERSION_OUTPUT = 64 * 1024 VERSION_TIMEOUT = 10 TARGETS = ("darwin/amd64", "darwin/arm64", "linux/amd64", "linux/arm64", "windows/amd64") POINTER_FIELDS = {"schema_version", "version", "manifest_url", "manifest_sha256"} MANIFEST_FIELDS = {"schema_version", "product", "version", "revision", "environment", "public_origin", "mcp_url", "artifacts"} ARTIFACT_FIELDS = {"id", "kind", "target", "filename", "url", "sha256", "size"} RECORD_FIELDS = {"product", "version", "revision", "environment", "public_origin", "mcp_url", "target", "sha256", "size"} SEMVER = re.compile(r"(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?") class InstallError(Exception): """An expected, fail-closed installation failure.""" def _require(condition, message): if not condition: raise InstallError(message) def _fields(value, fields, label): _require(type(value) is dict and set(value) == fields, f"Invalid {label} fields") def _json(raw): def pairs(items): result = {} for key, value in items: _require(key not in result, "Duplicate JSON field") result[key] = value return result def constant(_value): raise InstallError("Non-finite JSON number") try: return json.loads(raw.decode("utf-8"), object_pairs_hook=pairs, parse_constant=constant) except (ValueError, UnicodeError, RecursionError) as exc: raise InstallError("Invalid UTF-8 JSON") from exc def _schema(value): _require(type(value) is int and value == 1, "Unsupported schema_version") def _version(value): _require(type(value) is str and len(value) <= 80, "Invalid release version") match = SEMVER.fullmatch(value) _require(match is not None, "Version must be canonical SemVer without build metadata") prerelease = match[4].split(".") if match[4] else [] _require(all(not (p.isdigit() and len(p) > 1 and p.startswith("0")) for p in prerelease), "Invalid numeric prerelease identifier") return tuple(int(match[i]) for i in (1, 2, 3)), prerelease def compare_versions(left, right): a, ap = _version(left) b, bp = _version(right) if a != b: return (a > b) - (a < b) if not ap or not bp: return (not ap) - (not bp) for x, y in zip(ap, bp): if x == y: continue if x.isdigit() and y.isdigit(): return (int(x) > int(y)) - (int(x) < int(y)) if x.isdigit() != y.isdigit(): return -1 if x.isdigit() else 1 return (x > y) - (x < y) return (len(ap) > len(bp)) - (len(ap) < len(bp)) def validate_origin(origin): _require(type(origin) is str and len(origin) <= 512 and origin.isascii(), "Invalid HTTPS origin") _require(not any(c.isspace() or ord(c) < 32 for c in origin) and not any(c in origin for c in "\\%?#@"), "Invalid HTTPS origin") try: parsed = urllib.parse.urlsplit(origin) host, port = parsed.hostname, parsed.port _require(parsed.scheme == "https" and host and not parsed.path and not parsed.query and not parsed.fragment, "Origin must be a canonical HTTPS origin without a path") _require(len(host) <= 253 and re.fullmatch(r"[a-z0-9]+(?:[.-][a-z0-9]+)+", host) and all(re.fullmatch(r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?", label) for label in host.split(".")), "Origin must use a canonical lowercase DNS hostname or IPv4 address") _require(port is None and origin == "https://" + host, "Origin must not include an explicit port") except ValueError as exc: raise InstallError("Invalid HTTPS origin") from exc return origin def _https_url(url): _require(type(url) is str and len(url) <= 2048 and url.isascii(), "Invalid HTTPS URL") try: parts = urllib.parse.urlsplit(url) origin = validate_origin("https://" + parts.netloc) except ValueError as exc: raise InstallError("Invalid HTTPS URL") from exc _require(url.startswith(origin + "/") and parts.scheme == "https", "Only HTTPS downloads are permitted") _require(not any(c.isspace() or ord(c) < 32 for c in url) and not any(c in url for c in "\\%?#@"), "Unsafe download URL") _require(all(p not in (".", "..") for p in parts.path.split("/")), "Unsafe download path") def _hash(value): _require(type(value) is str and re.fullmatch(r"[0-9a-f]{64}", value), "Invalid SHA-256") def _size(value): _require(type(value) is int and 0 < value <= MAX_BINARY, "Invalid artifact size (maximum 256 MiB)") def _provenance(value, origin): _require(value["product"] == "framia" and value["environment"] == "prod", "Only production Framia releases are installable") _version(value["version"]) _require(type(value["revision"]) is str and re.fullmatch(r"[0-9a-f]{40}", value["revision"]), "Invalid clean source revision") _require(value["public_origin"] == origin, "Manifest origin does not match selected origin") mcp_url = value["mcp_url"] _require(type(mcp_url) is str and mcp_url.endswith("/multimodal/mcp"), "Invalid production MCP URL") validate_origin(mcp_url.removesuffix("/multimodal/mcp")) if origin == OFFICIAL_ORIGIN: _require(mcp_url == OFFICIAL_ORIGIN + "/multimodal/mcp", "Manifest MCP URL does not match official production origin") def validate_pointer(pointer, origin): _fields(pointer, POINTER_FIELDS, "stable pointer") _schema(pointer["schema_version"]) _version(pointer["version"]) _hash(pointer["manifest_sha256"]) _require(pointer["manifest_url"] == f"{origin}/downloads/releases/{pointer['version']}/release.json", "Stable manifest URL/version mismatch") return pointer def validate_manifest(manifest, origin, version): _fields(manifest, MANIFEST_FIELDS, "manifest") _schema(manifest["schema_version"]) _provenance(manifest, origin) _require(manifest["version"] == version, "Manifest version differs from stable pointer") artifacts = manifest["artifacts"] _require(type(artifacts) is list and 3 <= len(artifacts) <= len(TARGETS) + 2, "CLI, plugin and installer artifacts are required") identities, filenames, kinds = set(), set(), set() for artifact in artifacts: _fields(artifact, ARTIFACT_FIELDS, "artifact") _require(all(type(artifact[key]) is str for key in ("id", "kind", "target", "filename", "url")), "Invalid artifact strings") target, kind = artifact["target"], artifact["kind"] if kind == "cli": _require(target in TARGETS and artifact["id"] == "cli-" + target.replace("/", "-"), "Unsupported or mismatched CLI target") else: _require((artifact["id"], kind, target) in (("plugin-codex", "plugin", "codex"), ("installer-python", "installer", "python3")), "Unsupported artifact identity") name = artifact["filename"] _require(re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,127}", name) and ".." not in name and not name.endswith("."), "Unsafe artifact filename") _require(name.split(".")[0].upper() not in {"CON", "PRN", "AUX", "NUL", *(f"COM{i}" for i in range(1, 10)), *(f"LPT{i}" for i in range(1, 10))}, "Reserved artifact filename") expected_name = ("framia-" + target.replace("/", "-") + (".exe" if target == "windows/amd64" else "") if kind == "cli" else "framia-plugin.zip" if kind == "plugin" else "install_framia.py") _require(name == expected_name, "Artifact filename does not match its identity") _require(artifact["url"] == f"{origin}/downloads/releases/{version}/{name}", "Artifact URL is not in the immutable release directory") _hash(artifact["sha256"]) _size(artifact["size"]) _require(artifact["id"] not in identities and name.casefold() not in filenames, "Duplicate artifact identity or filename") identities.add(artifact["id"]) filenames.add(name.casefold()) kinds.add(kind) _require(kinds == {"cli", "plugin", "installer"}, "Incomplete public release") return manifest def select_target(system=None, machine=None): system = (platform.system() if system is None else system).lower() machine = (platform.machine() if machine is None else machine).lower() arch = {"amd64": "amd64", "x86_64": "amd64", "arm64": "arm64", "aarch64": "arm64"}.get(machine) target = f"{system}/{arch}" _require(target in TARGETS, f"Unsupported platform: {system}/{machine}; no architecture fallback is allowed") return target class _NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): raise InstallError("HTTP redirects are not permitted") def fetch_https(url): """The public transport: verified TLS, no proxies, credentials or redirects.""" _https_url(url) opener = urllib.request.build_opener( urllib.request.ProxyHandler({}), _NoRedirect(), urllib.request.HTTPSHandler(context=ssl.create_default_context())) request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept-Encoding": "identity"}) try: return opener.open(request, timeout=30) except (urllib.error.URLError, OSError) as exc: raise InstallError("HTTPS download failed; no files were installed") from exc def _transfer(url, sink, limit, fetch, expected_hash=None, expected_size=None): _https_url(url) digest, total, start = hashlib.sha256(), 0, time.monotonic() try: with fetch(url) as response: _require(response.status == 200 and response.geturl() == url, "Download status or redirect rejected") headers = response.headers _require(headers.get("Content-Encoding", "identity").lower() == "identity", "Encoded downloads are not supported") length = headers.get("Content-Length") if length is not None: _require(re.fullmatch(r"[0-9]+", length) and len(length) <= 10, "Invalid Content-Length") length = int(length) _require(length <= limit and (expected_size is None or length == expected_size), "Download size mismatch") while True: _require(time.monotonic() - start <= 300, "Download exceeded time limit") chunk = response.read(min(64 * 1024, limit - total + 1)) if not chunk: break _require(type(chunk) is bytes, "Download did not return bytes") total += len(chunk) _require(total <= limit, "Download exceeds size limit") digest.update(chunk) sink.write(chunk) _require(length is None or total == length, "Truncated download") _require(expected_size is None or total == expected_size, "Artifact size mismatch") _require(expected_hash is None or digest.hexdigest() == expected_hash, "SHA-256 mismatch") except (urllib.error.URLError, OSError) as exc: raise InstallError("Download failed; existing binary was preserved") from exc return digest.hexdigest() def _download_json(url, limit, fetch, expected_hash=None): output = io.BytesIO() _transfer(url, output, limit, fetch, expected_hash) return _json(output.getvalue()) def _is_link(info): return stat.S_ISLNK(info.st_mode) or bool(getattr(info, "st_file_attributes", 0) & 0x400) def _regular(info): _require(stat.S_ISREG(info.st_mode) and not _is_link(info) and info.st_nlink == 1, "Destination must be a regular, non-symlink, non-hardlinked file") if os.name != "nt": _require(info.st_uid == os.getuid() and not info.st_mode & 0o022, "Install files must be owned by you and not writable by other users") def _lstat(path): try: return path.lstat() except FileNotFoundError: return None def _same_file(first, second): return first is not None and second is not None and os.path.samestat(first, second) def _directory(path, create=False): _require(".." not in path.parts, "Install directory cannot contain parent traversal") for item in (*reversed(path.parents), path): if create: try: item.mkdir(mode=0o700) except FileExistsError: pass info = item.lstat() _require(stat.S_ISDIR(info.st_mode) and not _is_link(info), "Install directory and its parents must not be symlinks or reparse points") if os.name != "nt": _require(info.st_uid == os.getuid() and not info.st_mode & 0o022, "Install directory must be user-owned and not writable by other users") return info def _open_regular(path, flags, create=False): before = _lstat(path) if before is None and create: try: fd = os.open(path, flags | os.O_CREAT | os.O_EXCL | getattr(os, "O_NOFOLLOW", 0), 0o600) try: _regular(os.fstat(fd)) return fd except BaseException: os.close(fd) raise except FileExistsError: before = path.lstat() _require(before is not None, "Install file disappeared") _regular(before) fd = os.open(path, flags | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)) try: after = os.fstat(fd) _regular(after) _require(_same_file(before, after), "Install file changed while opening") return fd except BaseException: os.close(fd) raise @contextmanager def _install_lock(directory, timeout=30): fd = _open_regular(directory / ".framia-install.lock", os.O_RDWR, create=True) acquired = False try: if os.name == "nt": import msvcrt if os.fstat(fd).st_size == 0: os.write(fd, b"\0") else: import fcntl deadline = time.monotonic() + timeout while True: try: if os.name == "nt": os.lseek(fd, 0, os.SEEK_SET) msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) else: fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) acquired = True break except OSError as exc: if exc.errno not in (errno.EACCES, errno.EAGAIN, errno.EDEADLK): raise _require(time.monotonic() < deadline, "Another Framia installer holds the install lock; retry later") time.sleep(0.05) _require(_same_file(os.fstat(fd), _lstat(directory / ".framia-install.lock")), "Install lock was replaced") yield finally: if acquired: if os.name == "nt": os.lseek(fd, 0, os.SEEK_SET) msvcrt.locking(fd, msvcrt.LK_UNLCK, 1) else: fcntl.flock(fd, fcntl.LOCK_UN) os.close(fd) # The lock file is deliberately persistent: unlinking would split the lock. @contextmanager def _staging(directory, suffix=""): path = directory / (".framia-stage-" + secrets.token_hex(16) + suffix) fd = os.open(path, os.O_RDWR | os.O_CREAT | os.O_EXCL, 0o600) identity = os.fstat(fd) try: with os.fdopen(fd, "w+b") as output: yield path, output finally: if _same_file(identity, _lstat(path)): path.unlink() def _file_hash(path): fd = _open_regular(path, os.O_RDONLY) with os.fdopen(fd, "rb") as source: before = os.fstat(source.fileno()) _require(before.st_size <= MAX_BINARY, "Existing binary exceeds 256 MiB; move it aside manually") digest, total = hashlib.sha256(), 0 while chunk := source.read(64 * 1024): total += len(chunk) _require(total <= MAX_BINARY, "Existing binary exceeds size limit") digest.update(chunk) after = os.fstat(source.fileno()) _require(total == before.st_size and before.st_mtime_ns == after.st_mtime_ns and before.st_ctime_ns == after.st_ctime_ns and _same_file(before, _lstat(path)), "Install file changed while hashing") return digest.hexdigest(), total, before def _record(manifest, artifact): return {**{key: manifest[key] for key in RECORD_FIELDS - {"target", "sha256", "size"}}, **{key: artifact[key] for key in ("target", "sha256", "size")}} def _records(path): if _lstat(path) is None: return [], False fd = _open_regular(path, os.O_RDONLY) with os.fdopen(fd, "rb") as source: raw = source.read(MAX_MANIFEST + 1) try: _require(len(raw) <= MAX_MANIFEST, "Install receipt exceeds size limit") receipt = _json(raw) _fields(receipt, {"schema_version", "installations"}, "install receipt") _schema(receipt["schema_version"]) records = receipt["installations"] _require(type(records) is list and 1 <= len(records) <= 2, "Invalid install receipt") for record in records: _fields(record, RECORD_FIELDS, "installation record") origin = validate_origin(record["public_origin"]) _provenance(record, origin) _require(record["target"] in TARGETS, "Invalid installed target") _hash(record["sha256"]) _size(record["size"]) return records, False except InstallError: return [], True def smoke_version(path): """Execute only a freshly hash-verified staging file, without user context.""" env = {key: os.environ[key] for key in ("SYSTEMROOT", "SystemRoot", "WINDIR", "windir") if key in os.environ} chunks, errors = [], [] process = subprocess.Popen([str(path), "version"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, env=env, cwd=path.parent, bufsize=0) def read_output(): try: remaining = MAX_VERSION_OUTPUT + 1 while remaining: chunk = process.stdout.read(min(4096, remaining)) if not chunk: break chunks.append(chunk) remaining -= len(chunk) except OSError as exc: errors.append(exc) reader = threading.Thread(target=read_output, daemon=True) try: reader.start() deadline = time.monotonic() + VERSION_TIMEOUT reader.join(VERSION_TIMEOUT) _require(not reader.is_alive(), "Binary version check timed out") output = b"".join(chunks) _require(not errors and len(output) <= MAX_VERSION_OUTPUT, "Binary version output exceeds limit or cannot be read") try: code = process.wait(timeout=max(0.01, deadline - time.monotonic())) except subprocess.TimeoutExpired as exc: raise InstallError("Binary version check timed out") from exc _require(code == 0, "Binary version check failed") return output finally: if process.poll() is None: process.kill() process.wait() reader.join(1) process.stdout.close() def _verify_version(raw, manifest): _require(type(raw) is bytes and len(raw) <= MAX_VERSION_OUTPUT, "Invalid binary version output") value = _json(raw) expected = {"name": "framia", "version": manifest["version"], "revision": manifest["revision"], "environment": "prod", "issuer": manifest["mcp_url"].removesuffix("/multimodal/mcp") + "/", "resource": manifest["mcp_url"], "transport": "mcp"} _require(value == expected, "Binary version/provenance does not match the verified production manifest") def _unchanged(path, snapshot): if snapshot is None: _require(_lstat(path) is None, "Destination appeared during installation") else: current = _file_hash(path) _require(current[:2] == snapshot[:2] and _same_file(current[2], snapshot[2]), "Destination changed during installation") def _sync_directory(directory): if os.name != "nt": fd = os.open(directory, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) try: os.fsync(fd) finally: os.close(fd) def default_install_dir(): if os.name == "nt": local = os.environ.get("LOCALAPPDATA") _require(local and Path(local).is_absolute(), "LOCALAPPDATA is unavailable; specify a user-local --install-dir") return Path(local) / "Framia" / "bin" return Path.home() / ".local" / "bin" def install(origin=OFFICIAL_ORIGIN, install_dir=None, *, version=None, yes_upgrade=False, allow_downgrade=False, replace=False, fetch=None, smoke=None, system=None, machine=None, lock_timeout=30): """Internal callable permits offline test transports; the CLI never does. A local receipt binds installed bytes to previously verified provenance. It is never used as authority to execute those bytes. Keep the previous record alongside the prepared record so interrupted atomic replacement is retryable. """ origin = validate_origin(origin) target = select_target(system, machine) if version is not None: _version(version) directory = (Path(install_dir).expanduser() if install_dir is not None else default_install_dir()).absolute() directory_identity = _directory(directory, create=True) destination = directory / ("framia.exe" if target.startswith("windows/") else "framia") receipt_path = directory / ".framia-install.json" fetch, smoke = fetch or fetch_https, smoke or smoke_version with _install_lock(directory, lock_timeout): existing = _file_hash(destination) if _lstat(destination) is not None else None records, damaged_receipt = _records(receipt_path) _require(not damaged_receipt or replace, "Unrecognized install receipt; inspect it, then use --replace explicitly") pointer = validate_pointer(_download_json(origin + "/downloads/stable.json", MAX_POINTER, fetch), origin) _require(version is None or version == pointer["version"], "--version must equal the current stable version; arbitrary release selection is not supported") manifest = validate_manifest(_download_json(pointer["manifest_url"], MAX_MANIFEST, fetch, pointer["manifest_sha256"]), origin, pointer["version"]) artifact = next((a for a in manifest["artifacts"] if a["kind"] == "cli" and a["target"] == target), None) _require(artifact is not None, f"This release has no CLI for {target}; no fallback is allowed") new_record = _record(manifest, artifact) previous = next((record for record in records if existing is not None and record["target"] == target and (record["sha256"], record["size"]) == existing[:2]), None) identical = existing is not None and existing[:2] == (artifact["sha256"], artifact["size"]) if identical and previous: _require(previous == new_record, "Manifest provenance changed for identical artifact bytes") if existing and not identical: if previous is None: _require(replace, "Existing binary is unrecognized or modified; inspect it, then use --replace explicitly") else: order = compare_versions(manifest["version"], previous["version"]) _require(order != 0 or previous["public_origin"] != origin, "Immutable release changed bytes under the same version; refusing replacement") _require(previous["public_origin"] == origin or replace, "Changing release origin requires --replace") _require(order >= 0 or allow_downgrade, "Installed version is newer; --allow-downgrade is required") _require(yes_upgrade, "Replacing an installed version requires --yes-upgrade") installations = [new_record] + ([previous] if previous and previous != new_record else []) receipt = (json.dumps({"schema_version": 1, "installations": installations}, sort_keys=True, indent=2) + "\n").encode() def publish_receipt(): with _staging(directory) as (receipt_stage, output): output.write(receipt) output.flush() os.fsync(output.fileno()) output.close() if _lstat(receipt_path) is not None: _regular(receipt_path.lstat()) os.replace(receipt_stage, receipt_path) _sync_directory(directory) if identical: _unchanged(destination, existing) _require(_same_file(directory_identity, _directory(directory)), "Install directory changed") if os.name != "nt": fd = _open_regular(destination, os.O_RDONLY) try: os.fchmod(fd, 0o700) finally: os.close(fd) if records != installations or damaged_receipt: publish_receipt() return destination, False with _staging(directory, ".exe" if target.startswith("windows/") else "") as (stage, output): _transfer(artifact["url"], output, artifact["size"], fetch, artifact["sha256"], artifact["size"]) output.flush() os.fsync(output.fileno()) if os.name != "nt": os.fchmod(output.fileno(), 0o700) output.close() stage_snapshot = _file_hash(stage) _require(stage_snapshot[:2] == (artifact["sha256"], artifact["size"]), "Staging file changed before version check") _verify_version(smoke(stage), manifest) _unchanged(stage, stage_snapshot) _unchanged(destination, existing) _require(_same_file(directory_identity, _directory(directory)), "Install directory changed") publish_receipt() os.replace(stage, destination) _sync_directory(directory) return destination, True def main(argv=None): parser = argparse.ArgumentParser(description=__doc__, prog="install_framia.py", epilog="Requires Python 3.10+. Installs only stable; never logs in or changes PATH.") parser.add_argument("--origin", default=OFFICIAL_ORIGIN, help="trusted canonical HTTPS release origin") parser.add_argument("--install-dir", help="user-owned directory (default: ~/.local/bin; Windows: LOCALAPPDATA/Framia/bin)") parser.add_argument("--version", help="require stable to select this exact version (not arbitrary historical selection)") parser.add_argument("--yes-upgrade", action="store_true", help="explicitly authorize replacing a known installed version") parser.add_argument("--allow-downgrade", action="store_true", help="also required when the known installed version is newer") parser.add_argument("--replace", action="store_true", help="explicitly replace an unrecognized/modified binary or change release origin") args = parser.parse_args(argv) try: _require(sys.version_info >= (3, 10), "Python 3.10+ is required") path, changed = install(**vars(args)) except (InstallError, OSError) as exc: print(f"Framia install failed: {exc}", file=sys.stderr) return 1 except KeyboardInterrupt: print("Framia installation cancelled.", file=sys.stderr) return 130 print(f"{'Installed' if changed else 'Already installed (verified SHA-256)'}: {path}") command = ("& '" + str(path).replace("'", "''") + "'" if os.name == "nt" else shlex.quote(str(path))) print(f"Log in when ready: {command} auth login") return 0 if __name__ == "__main__": raise SystemExit(main())