Files
framebridge/source/expanded/framebridge_expanded/autostart.py
T

885 lines
42 KiB
Python

"""Explicit user-service startup with host-bound encrypted systemd credentials."""
from __future__ import annotations
import argparse
import fcntl
import getpass
import hashlib
import io
import json
import os
from pathlib import Path
import pwd
import re
import resource
import secrets
import signal
import stat
import subprocess
import sys
import time
from .config import load_config, save_config, private_dir, state_path, default_config, validate
CREDENTIAL = "chatgpt-runtime-key"
_STOP = False
def _paths(state):
state = state_path(state)
directory = private_dir(private_dir(state) / "autostart")
name = "framebridge-autostart-" + hashlib.sha256(str(state).encode()).hexdigest()[:16] + ".service"
return state, directory, Path.home() / ".config/systemd/user" / name
def _read(path, maximum=131072):
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1 or info.st_mode & 0o077:
raise RuntimeError("Unsafe autostart file ownership or permissions.")
data = os.read(fd, maximum + 1)
if len(data) > maximum:
raise RuntimeError("Autostart file exceeds the size limit.")
return data
finally:
os.close(fd)
def _write(path, data, replace=False):
path = Path(path)
if path.exists() or path.is_symlink():
if not replace:
raise RuntimeError("Refusing to replace an existing autostart file.")
_read(path)
temporary = path.with_name("." + path.name + "." + secrets.token_hex(8))
fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
try:
with os.fdopen(fd, "wb") as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
if replace:
os.replace(temporary, path)
else:
os.link(temporary, path, follow_symlinks=False)
finally:
temporary.unlink(missing_ok=True)
def _json(path, value, replace=False):
_write(path, (json.dumps(value, sort_keys=True, indent=2) + "\n").encode(), replace)
def _command(args, *, data=None, timeout=35, check=True):
result = subprocess.run(args, input=data, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)
if check and result.returncode:
raise RuntimeError("Autostart command failed: " + Path(args[0]).name + ". No command output was exported.")
return result
def _systemctl(*args, check=True):
return _command(["systemctl", "--user", *args], check=check)
def _quote(value, *, command=False):
value = str(value)
if any(ord(ch) < 32 or ord(ch) == 127 for ch in value):
raise ValueError("Control characters are not allowed in service paths.")
if command:
value = value.replace("$", "$$")
return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("%", "%%") + '"'
def unit_text(state, encrypted, *, node=False):
module_path = Path(__file__).resolve().parents[1]
executable = [sys.executable, "-B", "-m", "framebridge_expanded.autostart", "_run", "--state", str(state)]
lines = ["# FrameBridge managed autostart v1", "[Unit]", "Description=FrameBridge profile and ChatGPT connection", "StartLimitIntervalSec=0", "ConditionPathExists=" + _quote(module_path / "framebridge_expanded/autostart.py"), "", "[Service]", "Type=simple", "UMask=0077", "LimitCORE=0", "Environment=" + _quote("PYTHONPATH=" + str(module_path)), "Environment=LANG=C.UTF-8", "Environment=LC_ALL=C.UTF-8"]
lines += ["ExecStart=" + " ".join(_quote(x, command=True) for x in executable), "Restart=on-failure", "RestartSec=15", "TimeoutStopSec=25", "KillMode=control-group", "StandardInput=null", "StandardOutput=null", "StandardError=null", "", "[Install]", "WantedBy=default.target", ""]
return "\n".join(lines)
def _linger(user):
directory = Path("/var/lib/systemd/linger")
try:
info = directory.stat()
except FileNotFoundError:
return False
if not stat.S_ISDIR(info.st_mode) or info.st_uid != 0 or directory.is_symlink():
raise RuntimeError("The systemd linger directory is unavailable or unsafe.")
try:
marker = (directory / user).lstat()
except FileNotFoundError:
return False
if not stat.S_ISREG(marker.st_mode) or marker.st_uid != 0:
raise RuntimeError("The systemd linger record is unsafe.")
return True
def _enabled_units():
result = _systemctl("list-unit-files", "--state=enabled", "--no-legend", "--no-pager")
return sorted(line.split()[0] for line in result.stdout.decode(errors="replace").splitlines() if line.split())
def _check_unit_directory(path):
for directory in [Path.home() / ".config", Path.home() / ".config/systemd", path.parent]:
if directory.is_symlink():
raise RuntimeError("User service directories must not be symbolic links.")
directory.mkdir(mode=0o700, exist_ok=True)
info = directory.stat()
if info.st_uid != os.getuid() or info.st_mode & 0o022:
raise RuntimeError("User service directories must be owned by this user and not writable by others.")
def _manifest(directory):
path = directory / "installation.json"
try:
value = json.loads(_read(path))
except FileNotFoundError:
return None
if not isinstance(value, dict) or value.get("schema") != 1:
raise RuntimeError("Unsupported autostart installation record.")
return value
def status(state):
state, directory, unit = _paths(state)
manifest = _manifest(directory)
properties = _systemctl("show", unit.name, "--property=ActiveState,SubState,UnitFileState", check=False)
values = dict(line.split("=", 1) for line in properties.stdout.decode(errors="replace").splitlines() if "=" in line)
from . import runtime, connection_background
result = {"configured": bool(manifest), "unit": unit.name, "service_active": values.get("ActiveState") == "active", "service_state": values.get("ActiveState", "unavailable"), "enabled": values.get("UnitFileState") == "enabled", "linger": _linger(pwd.getpwuid(os.getuid()).pw_name), "encrypted_credential_present": False, "runtime": _safe_runtime_status(runtime, state), "connection": connection_background.status(state)}
if manifest and not manifest.get("node"):
try:
cipher = _read(directory / "credential.cred")
result["encrypted_credential_present"] = hashlib.sha256(cipher).hexdigest() == manifest.get("credential_sha256")
except FileNotFoundError:
pass
try:
result["worker"] = json.loads(_read(directory / "worker-status.json"))
except FileNotFoundError:
result["worker"] = {"state": "not_started"}
result["boot_desired_enabled"] = boot_requested(state)
result["desired_connection"] = "not_configured" if not manifest else ("not_applicable" if manifest.get("node") else ("connected" if connection_requested(state) else "disconnected"))
return result
def _safe_runtime_status(runtime, state):
try:
return runtime.status(state)
except Exception:
return {"running": False, "ready": False, "status_unavailable": True}
def _configuration_action(state, action):
_, directory, _ = _paths(state)
fd = os.open(directory / "configuration.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1 or info.st_mode & 0o077:
raise RuntimeError("Unsafe autostart configuration lock.")
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise RuntimeError("Another autostart configuration is already in progress.") from None
return action(state)
finally:
os.close(fd)
def enable(state):
try:
return _configuration_action(state, _enable)
except (EOFError, KeyboardInterrupt):
print(flush=True)
return {"configured": False, "state": "cancelled"}
def disable(state):
return _configuration_action(state, _disable)
def _committed_connection_status(state):
try:
return status(state)
except (EOFError, KeyboardInterrupt):
# Enabling the service already committed. Cancelling only this status
# read must not suggest that the installed connection was cancelled.
return {"configured": True, "enabled": True, "interrupted_after_commit": True}
def configure_connection(state, public_settings, runtime_key, *, output=None):
"""Install this connection once, using only the supplied hidden in-memory key."""
if not isinstance(runtime_key, str) or not runtime_key or not runtime_key.isascii() or len(runtime_key) > 16384 or any(ord(ch) < 33 or ord(ch) > 126 for ch in runtime_key):
raise ValueError("Enter one nonempty MCP server access key.")
allowed = {"account_label", "tunnel_id", "control_plane_organization_id", "control_plane_url_path"}
if not isinstance(public_settings, dict) or set(public_settings) - allowed:
raise ValueError("Unsupported public connection settings.")
from .tunnel import TunnelSpec
settings = dict(public_settings)
spec = TunnelSpec(settings.get("tunnel_id", ""), settings.get("control_plane_organization_id", ""), settings.get("control_plane_url_path", ""))
label = settings.get("account_label", "")
if not isinstance(label, str) or len(label) > 320 or any(ord(ch) < 32 or ord(ch) == 127 for ch in label):
raise ValueError("Enter a valid account label.")
settings.update(spec.public_settings())
settings["account_label"] = label
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
try:
return _configuration_action(state, lambda selected: _configure_connection(selected, settings, runtime_key))
finally:
runtime_key = ""
def _connection_public_digest(config):
fields = {name: config.get(name, "") for name in ("account_label", "tunnel_id", "control_plane_organization_id", "control_plane_url_path", "role", "node_id")}
fields["mcp_port"] = config["ports"]["mcp"]
return hashlib.sha256(json.dumps(fields, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
def resume_connection(state):
"""Resume the verified saved connection without asking for its key again."""
return _configuration_action(state, _resume_connection)
def _resume_connection(state):
if os.geteuid() == 0:
raise RuntimeError("Run connect as the profile owner, not as root.")
state, directory, unit = _paths(state)
config = load_config(state)
record = _manifest(directory)
error = "The saved connection cannot be verified. Run fb connect chatgpt --replace."
if not record or record.get("node") or config["role"] == "node" or record.get("unit") != unit.name:
raise RuntimeError(error)
try:
valid_unit = hashlib.sha256(_read(unit)).hexdigest() == record.get("unit_sha256")
valid_cipher = hashlib.sha256(_read(directory / "credential.cred")).hexdigest() == record.get("credential_sha256")
bound = record.get("connection_settings_sha256")
if bound:
valid_settings = _connection_public_digest(config) == bound
else:
# Older releases only bound the complete profile when converting its desktop setting.
old_bound = record.get("changed_config_sha256")
valid_settings = bool(old_bound) and hashlib.sha256(_read(state / "config.json")).hexdigest() == old_bound
if record.get("headless_changed"):
valid_backup = hashlib.sha256(_read(directory / "config-before.json")).hexdigest() == record.get("config_before_sha256")
else:
valid_backup = True
except (OSError, ValueError):
raise RuntimeError(error) from None
if not (valid_unit and valid_cipher and valid_settings and valid_backup):
raise RuntimeError(error)
from .tunnel import TunnelSpec
TunnelSpec(config["tunnel_id"], config.get("control_plane_organization_id", ""), config.get("control_plane_url_path", ""))
user = pwd.getpwuid(os.getuid()).pw_name
if not _linger(user):
_command(["sudo", "-v"], timeout=180)
_command(["sudo", "-n", "--", "loginctl", "enable-linger", user])
was_manually_stopped = manual_stop_requested(state)
_connection_action(state, lambda _: _set_connection_requested(state, True))
_clear_manual_stop(state)
current = status(state)
local = current.get("runtime", {})
# A live managed worker owns local tools independently of its transport.
# Change its requested connection state instead of restarting those tools.
if current.get("service_active") and local.get("running") and local.get("ready") and current.get("worker", {}).get("state") not in {"failed", "stopped"} and not was_manually_stopped:
if not current.get("enabled") and boot_requested(state) is not False:
_systemctl("enable", unit.name)
return _committed_connection_status(state)
_systemctl("stop", unit.name)
from . import connection_background
stopped = connection_background.disconnect(state)
if stopped.get("ok") is False or stopped.get("alive") or connection_background.status(state).get("alive"):
raise RuntimeError("The previous connection is still stopping. Try connecting again shortly.")
_systemctl("daemon-reload")
if boot_requested(state) is False:
_systemctl("start", unit.name)
else:
_systemctl("enable", "--now", unit.name)
return _committed_connection_status(state)
def _optional_read(path):
try:
return _read(path)
except FileNotFoundError:
return None
def _configure_connection(state, settings, runtime_key):
if os.geteuid() == 0:
raise RuntimeError("Run connect as the profile owner, not as root.")
state, directory, unit = _paths(state)
config_path = state / "config.json"
original_config = _optional_read(config_path)
config = load_config(state) if original_config is not None else default_config()
if config["role"] == "node":
raise RuntimeError("Connect ChatGPT from the central profile.")
config.update(settings)
headless_before = config.get("headless", "auto")
changed_headless = headless_before in ("no", False)
if changed_headless:
config["headless"] = "auto"
validate(config)
config_bytes = (json.dumps(config, indent=2, sort_keys=True) + "\n").encode()
record_path = directory / "installation.json"
blob = directory / "credential.cred"
backup = directory / "config-before.json"
manual_stop = directory / "manual-stop.json"
intent = directory / "connection-intent.json"
targets = (config_path, unit, blob, record_path, backup, manual_stop, intent)
originals = {path: _optional_read(path) for path in targets}
previous = _manifest(directory)
if previous:
if previous.get("node") or previous.get("unit") != unit.name or originals[unit] is None or hashlib.sha256(originals[unit]).hexdigest() != previous.get("unit_sha256"):
raise RuntimeError("The existing service changed outside FrameBridge; nothing was replaced.")
if originals[blob] is None or hashlib.sha256(originals[blob]).hexdigest() != previous.get("credential_sha256"):
raise RuntimeError("The encrypted connection changed outside FrameBridge; nothing was replaced.")
if previous.get("headless_changed") and (originals[backup] is None or hashlib.sha256(originals[backup]).hexdigest() != previous.get("config_before_sha256")):
raise RuntimeError("The profile backup changed outside FrameBridge; nothing was replaced.")
elif any(originals[path] is not None for path in (unit, blob, backup)):
raise RuntimeError("Existing unowned connection files; nothing was replaced.")
_check_unit_directory(unit)
version = _command(["systemd-creds", "--version"]).stdout.decode(errors="replace")
match = re.match(r"systemd (\d+)", version)
if not match or int(match.group(1)) < 256:
raise RuntimeError("Protected connections require systemd 256 or later.")
_command(["sudo", "-v"], timeout=180)
user = pwd.getpwuid(os.getuid()).pw_name
linger_before = _linger(user)
units_before = _enabled_units()
properties = _systemctl("show", unit.name, "--property=ActiveState,UnitFileState", check=False)
values = dict(line.split("=", 1) for line in properties.stdout.decode(errors="replace").splitlines() if "=" in line)
was_enabled = values.get("UnitFileState") == "enabled"
was_active = values.get("ActiveState") in {"active", "activating", "reloading"}
try:
encrypted = _command(["sudo", "-n", "--", "systemd-creds", "encrypt", "--uid=" + str(os.getuid()), "--with-key=host", "--name=" + CREDENTIAL, "-", "-"], data=runtime_key.encode()).stdout
finally:
runtime_key = ""
if len(encrypted) < 80 or len(encrypted) > 131072:
raise RuntimeError("Connection protection did not complete.")
# Recheck the exact originals after authentication/encryption and before any stop.
if any(_optional_read(path) != data for path, data in originals.items()):
raise RuntimeError("The profile changed during connection setup; nothing was replaced.")
text = unit_text(state, blob).encode()
record = dict(previous) if previous else {"schema": 1, "unit": unit.name, "node": False, "linger_before": linger_before, "enabled_units_before": units_before, "headless_before": headless_before, "headless_changed": changed_headless, "config_before_sha256": hashlib.sha256(original_config).hexdigest() if original_config is not None else ""}
backup_bytes = originals[backup]
if changed_headless and not record.get("headless_changed"):
record.update(headless_before=headless_before, headless_changed=True, config_before_sha256=hashlib.sha256(original_config).hexdigest())
backup_bytes = original_config
elif not previous and changed_headless:
backup_bytes = original_config
if record.get("headless_changed"):
# Disabling changes only the desktop preference, preserving the newly selected connection.
restored_profile = dict(config)
restored_profile["headless"] = record["headless_before"]
backup_bytes = (json.dumps(restored_profile, indent=2, sort_keys=True) + "\n").encode()
record["config_before_sha256"] = hashlib.sha256(backup_bytes).hexdigest()
record["changed_config_sha256"] = hashlib.sha256(config_bytes).hexdigest()
record.update(unit_sha256=hashlib.sha256(text).hexdigest(), credential_sha256=hashlib.sha256(encrypted).hexdigest(), connection_settings_sha256=_connection_public_digest(config))
from . import connection_background
background_before = connection_background.status(state)
changed = {}
service_touched = False
manual_connection_stopped = False
def write(path, data):
changed[path] = data
_write(path, data, replace=originals.get(path) is not None)
try:
if previous:
service_touched = True
_systemctl("stop", unit.name)
disconnected = connection_background.disconnect(state)
if disconnected.get("ok") is False or disconnected.get("alive"):
raise RuntimeError("The previous connection could not be stopped safely; nothing was replaced.")
manual_connection_stopped = not previous and background_before.get("alive") is True
if connection_background.status(state).get("alive"):
raise RuntimeError("The previous connection is still stopping; nothing was replaced.")
write(blob, encrypted)
write(unit, text)
write(config_path, config_bytes)
if backup_bytes is not None and backup_bytes != originals[backup]:
write(backup, backup_bytes)
write(record_path, (json.dumps(record, sort_keys=True, indent=2) + "\n").encode())
write(intent, (json.dumps({"schema": 1, "connect": True, "revision": connection_request_revision(state) + 1}, sort_keys=True) + "\n").encode())
_command(["sudo", "-n", "--", "loginctl", "enable-linger", user])
_systemctl("daemon-reload")
service_touched = True
if boot_requested(state) is False:
_systemctl("start", unit.name)
else:
_systemctl("enable", "--now", unit.name)
except BaseException as error:
rollback_failed = False
if service_touched or unit in changed:
try:
rollback_failed = bool(_systemctl("disable", "--now", unit.name, check=False).returncode)
except Exception:
rollback_failed = True
for path, installed in reversed(list(changed.items())):
try:
current = _optional_read(path)
if current == originals[path]:
continue
if current != installed:
rollback_failed = True
continue
if originals[path] is None:
path.unlink()
else:
_write(path, originals[path], replace=True)
except Exception:
rollback_failed = True
try:
if originals[manual_stop] is not None and _optional_read(manual_stop) is None:
_write(manual_stop, originals[manual_stop])
_systemctl("daemon-reload")
if previous:
result = _systemctl("enable" if was_enabled else "disable", unit.name, check=False)
rollback_failed = rollback_failed or bool(result.returncode)
if was_active:
restarted = _systemctl("start", unit.name, check=False)
rollback_failed = rollback_failed or bool(restarted.returncode)
if not linger_before and _enabled_units() == units_before:
restored_linger = _command(["sudo", "-n", "--", "loginctl", "disable-linger", user], check=False)
rollback_failed = rollback_failed or bool(restored_linger.returncode)
except Exception:
rollback_failed = True
if rollback_failed:
raise RuntimeError("Connection setup did not complete, and restoration could not be fully verified. Inspect fb autostart status before retrying.") from None
if manual_connection_stopped:
raise RuntimeError("Connection setup did not complete. The previous manually started connection was stopped; reconnect it using its original key.") from None
raise
return _committed_connection_status(state)
def _enable(state, *, start_now=True, confirm=True):
if os.geteuid() == 0:
raise RuntimeError("Run autostart enable as the profile owner, not as root.")
state, directory, unit = _paths(state)
config = load_config(state)
if not confirm and config["role"] != "node":
raise RuntimeError("Only node startup can be initialized without the connection wizard.")
original_config = _read(state / "config.json")
existing = _manifest(directory)
if existing:
if existing.get("unit") != unit.name or hashlib.sha256(_read(unit)).hexdigest() != existing.get("unit_sha256"):
raise RuntimeError("The service changed outside FrameBridge; refusing to enable it.")
if not existing.get("node") and hashlib.sha256(_read(directory / "credential.cred")).hexdigest() != existing.get("credential_sha256"):
raise RuntimeError("The encrypted credential changed outside FrameBridge; refusing to enable it.")
user = pwd.getpwuid(os.getuid()).pw_name
if not _linger(user):
_command(["sudo", "-v"], timeout=180)
_command(["sudo", "-n", "--", "loginctl", "enable-linger", user])
_set_boot_requested(state, True)
_systemctl("daemon-reload")
if start_now:
_systemctl("enable", "--now", unit.name)
else:
_systemctl("enable", unit.name)
return status(state)
version = _command(["systemd-creds", "--version"]).stdout.decode(errors="replace")
match = re.match(r"systemd (\d+)", version)
if not match or int(match.group(1)) < 256:
raise RuntimeError("Encrypted user credentials require systemd 256 or later.")
node = config["role"] == "node"
if not node and not config.get("tunnel_id"):
raise RuntimeError("Save a tunnel with fb connect chatgpt before enabling automatic startup.")
print("FrameBridge will start automatically when this computer starts.")
print("Available tools are detected automatically.")
if not node:
print("Your connection key will be encrypted on this computer.")
original_headless = config.get("headless", "auto")
changed_headless = original_headless in ("no", False)
if confirm and input("Enable automatic startup? [y/N]: ").strip().lower() not in {"y", "yes"}:
return {"configured": False, "state": "cancelled"}
print("[1/3] Preparing automatic startup...", flush=True)
_check_unit_directory(unit)
if unit.exists() or unit.is_symlink() or (directory / "credential.cred").exists() or (directory / "credential.cred").is_symlink():
raise RuntimeError("Existing unowned service or encrypted credential; nothing was replaced.")
_command(["sudo", "-v"], timeout=180)
user = pwd.getpwuid(os.getuid()).pw_name
linger_before = _linger(user)
units_before = _enabled_units()
encrypted = b""
print("[2/3] Preparing this node..." if node else "[2/3] Protecting the connection key...", flush=True)
if not node:
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
runtime_key = getpass.getpass("Connection key (hidden): ")
if not runtime_key or not runtime_key.isascii() or len(runtime_key) > 16384 or any(ord(ch) < 33 or ord(ch) > 126 for ch in runtime_key):
raise ValueError("The runtime key must be nonempty and contain no whitespace or control characters.")
try:
encrypted = _command(["sudo", "-n", "--", "systemd-creds", "encrypt", "--uid=" + str(os.getuid()), "--with-key=host", "--name=" + CREDENTIAL, "-", "-"], data=runtime_key.encode()).stdout
finally:
runtime_key = ""
if len(encrypted) < 80 or len(encrypted) > 131072:
raise RuntimeError("Credential encryption returned an invalid result.")
blob = directory / "credential.cred"
text = unit_text(state, blob, node=node).encode()
record = {"schema": 1, "unit": unit.name, "unit_sha256": hashlib.sha256(text).hexdigest(), "credential_sha256": hashlib.sha256(encrypted).hexdigest() if encrypted else "", "node": node, "linger_before": linger_before, "enabled_units_before": units_before, "headless_before": original_headless, "headless_changed": changed_headless, "config_before_sha256": hashlib.sha256(original_config).hexdigest()}
created = []
try:
if encrypted:
_write(blob, encrypted); created.append(blob)
_write(unit, text); created.append(unit)
if changed_headless:
if _read(state / "config.json") != original_config:
raise RuntimeError("The profile changed during configuration; no profile settings were overwritten.")
_write(directory / "config-before.json", original_config)
created.append(directory / "config-before.json")
config["headless"] = "auto"
save_config(state, config)
record["changed_config_sha256"] = hashlib.sha256(_read(state / "config.json")).hexdigest()
_json(directory / "installation.json", record); created.append(directory / "installation.json")
print("[3/3] Enabling automatic startup...", flush=True)
_command(["sudo", "-n", "--", "loginctl", "enable-linger", user])
_systemctl("daemon-reload")
if start_now:
_systemctl("enable", "--now", unit.name)
else:
_systemctl("enable", unit.name)
except BaseException:
if unit in created:
_systemctl("disable", "--now", unit.name, check=False)
for path in reversed(created):
try: path.unlink()
except FileNotFoundError: pass
if changed_headless and record.get("changed_config_sha256") == hashlib.sha256(_read(state / "config.json")).hexdigest():
_write(state / "config.json", original_config, replace=True)
_systemctl("daemon-reload", check=False)
if not linger_before:
try:
if _enabled_units() == units_before:
_command(["sudo", "-n", "--", "loginctl", "disable-linger", user], check=False)
except Exception:
pass
raise
return status(state)
def _disable(state):
state, directory, unit = _paths(state)
record = _manifest(directory)
if not record:
return {"configured": False, "state": "already_disabled"}
if record.get("unit") != unit.name or hashlib.sha256(_read(unit)).hexdigest() != record.get("unit_sha256"):
raise RuntimeError("The service changed outside FrameBridge; refusing to remove it.")
blob = directory / "credential.cred"
if not record.get("node") and hashlib.sha256(_read(blob)).hexdigest() != record.get("credential_sha256"):
raise RuntimeError("The encrypted credential changed outside FrameBridge; refusing to remove it.")
backup = None
if record.get("headless_changed"):
backup = _read(directory / "config-before.json")
if hashlib.sha256(backup).hexdigest() != record.get("config_before_sha256"):
raise RuntimeError("The profile backup changed outside FrameBridge; refusing to remove it.")
owned_policy = [directory / "boot-policy.json", directory / "connection-intent.json"]
for policy in owned_policy:
if policy.exists() or policy.is_symlink():
_read(policy)
restore_linger = not record.get("linger_before") and [name for name in _enabled_units() if name != unit.name] == record.get("enabled_units_before")
if restore_linger:
_command(["sudo", "-v"], timeout=180)
_systemctl("disable", "--now", unit.name)
unit.unlink()
if not record.get("node"):
blob.unlink()
config_restored = False
if record.get("headless_changed") and hashlib.sha256(_read(state / "config.json")).hexdigest() == record.get("changed_config_sha256"):
_write(state / "config.json", backup, replace=True)
config_restored = True
_systemctl("daemon-reload")
linger_restored = False
if restore_linger and _enabled_units() == record.get("enabled_units_before"):
user = pwd.getpwuid(os.getuid()).pw_name
_command(["sudo", "-n", "--", "loginctl", "disable-linger", user])
linger_restored = True
if backup is not None:
(directory / "config-before.json").unlink()
for policy in owned_policy:
policy.unlink(missing_ok=True)
(directory / "installation.json").unlink()
return {"configured": False, "state": "disabled", "encrypted_credential_removed": True, "desktop_setting_restored": config_restored, "linger_restored": linger_restored}
def _credential(state):
# Debian 13 systemd 257 cannot load user encrypted credentials directly.
# Its systemd-creds CLI delegates owner-scoped decryption to the OS broker.
_, directory, _ = _paths(state)
record = _manifest(directory)
if not record or record.get("node"):
raise RuntimeError("The encrypted runtime credential is not configured.")
cipher = _read(directory / "credential.cred")
if hashlib.sha256(cipher).hexdigest() != record.get("credential_sha256"):
raise RuntimeError("The encrypted runtime credential changed outside FrameBridge.")
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
result = _command(["systemd-creds", "decrypt", "--user", "--name=" + CREDENTIAL, "-", "-"], data=cipher, timeout=15)
try:
key = result.stdout.decode("ascii")
except UnicodeDecodeError:
raise RuntimeError("The decrypted runtime credential is invalid.") from None
if not key or len(key) > 16384 or any(ord(ch) < 33 or ord(ch) == 127 for ch in key):
raise RuntimeError("The decrypted runtime credential is invalid.")
return key
def stop_for_current_boot(state):
# systemctl stop also cancels an already queued Restart=on-failure job.
# Keep the unit enabled and the encrypted credential for the next boot.
directory = _intent_directory(state)
if directory is None:
return {"configured": False, "state": "not_configured"}
_, _, unit = _paths(state)
record = _manifest(directory)
if record.get("unit") != unit.name or hashlib.sha256(_read(unit)).hexdigest() != record.get("unit_sha256"):
raise RuntimeError("The service changed outside FrameBridge; refusing to stop it.")
_systemctl("stop", unit.name)
return {"configured": True, "state": "stopped_for_current_boot", "autostart_configuration_preserved": True}
def _intent_directory(state):
directory = state_path(state) / "autostart"
if not directory.exists() and not directory.is_symlink():
return None
private_dir(directory)
return directory if _manifest(directory) else None
def mark_manual_stop(state):
directory = _intent_directory(state)
if directory is None:
return False
_json(directory / "manual-stop.json", {"requested": True, "boot_id": Path("/proc/sys/kernel/random/boot_id").read_text().strip()}, replace=True)
return True
def manual_stop_requested(state):
directory = _intent_directory(state)
if directory is None:
return False
try:
record = json.loads(_read(directory / "manual-stop.json"))
except FileNotFoundError:
return False
return isinstance(record, dict) and record.get("requested") is True and record.get("boot_id") == Path("/proc/sys/kernel/random/boot_id").read_text().strip()
def _clear_manual_stop(state):
directory = _intent_directory(state)
if directory is None:
return
path = directory / "manual-stop.json"
try:
_read(path)
except FileNotFoundError:
return
path.unlink()
def boot_requested(state):
"""Explicit boot policy; None retains the normal first-connect default."""
directory = _intent_directory(state)
if directory is None:
return None
try:
value = json.loads(_read(directory / "boot-policy.json"))
except FileNotFoundError:
return None
if type(value) is not dict or value.get("schema") != 1 or type(value.get("enabled")) is not bool:
raise RuntimeError("The saved boot policy cannot be verified.")
return value["enabled"]
def _set_boot_requested(state, enabled):
if type(enabled) is not bool:
raise ValueError("Boot policy must be true or false.")
directory = _intent_directory(state)
if directory is None:
return False
_json(directory / "boot-policy.json", {"schema": 1, "enabled": enabled}, replace=True)
return True
def connection_requested(state):
"""Desired transport state survives worker recovery and reboot until connect."""
directory = _intent_directory(state)
if directory is None:
return True
try:
value = json.loads(_read(directory / "connection-intent.json"))
except FileNotFoundError:
return True
if type(value) is not dict or value.get("schema") != 1 or type(value.get("connect")) is not bool:
raise RuntimeError("The saved connection intent cannot be verified.")
return value["connect"]
def connection_request_revision(state):
directory = _intent_directory(state)
if directory is None:
return 0
try:
value = json.loads(_read(directory / "connection-intent.json"))
except FileNotFoundError:
return 0
revision = value.get("revision", 0) if type(value) is dict else None
if type(revision) is not int or revision < 0:
raise RuntimeError("The saved connection intent cannot be verified.")
return revision
def _set_connection_requested(state, requested):
if type(requested) is not bool:
raise ValueError("Connection intent must be true or false.")
directory = _intent_directory(state)
if directory is None:
return False
_json(directory / "connection-intent.json", {"schema": 1, "connect": requested, "revision": connection_request_revision(state) + 1}, replace=True)
return True
def _connection_action(state, action):
"""Serialize the worker's transport startup with an explicit pause request."""
directory = _intent_directory(state)
if directory is None:
return action(state)
fd = os.open(directory / "connection-action.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1 or info.st_mode & 0o077:
raise RuntimeError("Unsafe connection action lock.")
deadline = time.monotonic() + 20
while True:
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
break
except BlockingIOError:
if time.monotonic() >= deadline:
raise RuntimeError("Another connection operation is still finishing. Try again shortly.")
time.sleep(0.05)
return action(state)
finally:
os.close(fd)
def _worker_status(directory, name):
_json(directory / "worker-status.json", {"state": name, "updated_at": time.time()}, replace=True)
def worker(state):
global _STOP
_STOP = False
state, directory, unit = _paths(state)
config = load_config(state)
from . import runtime, connection_background, tunnel
from .display_context import DisplayContextError
def stop(signum, frame):
global _STOP
_STOP = True
old = {s: signal.signal(s, stop) for s in (signal.SIGTERM, signal.SIGINT)}
lockfd = os.open(directory / "worker.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
info = os.fstat(lockfd)
if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_nlink != 1 or info.st_mode & 0o077:
os.close(lockfd)
raise RuntimeError("Unsafe autostart worker lock.")
fcntl.flock(lockfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
connected = False
attempted_connection = False
last_revision = None
next_connection_attempt = 0.0
try:
_clear_manual_stop(state)
while not _STOP:
if manual_stop_requested(state):
return 0
try:
current = runtime.start(state)
if manual_stop_requested(state):
return 0
except DisplayContextError:
if config.get("headless") in ("no", False):
_worker_status(directory, "waiting_for_graphical_login")
time.sleep(3)
continue
raise
if current.get("running") and current.get("ready"):
break
_worker_status(directory, "waiting_for_graphical_login" if config.get("headless") in ("no", False) else "waiting_for_local_services")
time.sleep(3)
if _STOP:
return 0
_worker_status(directory, "local_services_ready")
if config["role"] == "node":
while not _STOP:
if manual_stop_requested(state):
return 0
if not runtime.status(state).get("running"):
return 0 if manual_stop_requested(state) else 1
time.sleep(2)
return 0
while not _STOP:
if manual_stop_requested(state):
return 0
def synchronize_connection(_state):
nonlocal connected, attempted_connection, last_revision, next_connection_attempt
revision = connection_request_revision(state)
if revision != last_revision:
attempted_connection = False
next_connection_attempt = 0.0
last_revision = revision
current = connection_background.status(state)
connected = connected or current.get("alive") is True
if not connection_requested(state):
if current.get("alive"):
stopped = connection_background.disconnect(state)
if stopped.get("ok") is False or stopped.get("alive"):
raise RuntimeError("The paused connection is still stopping.")
_worker_status(directory, "disconnected")
return
if not current.get("alive") and (not attempted_connection or current.get("state") not in ("stopped", "closed")):
if time.monotonic() < next_connection_attempt:
_worker_status(directory, "reconnecting")
return
attempted_connection = True
next_connection_attempt = time.monotonic() + 15
key = _credential(state)
try:
spec = tunnel.TunnelSpec(config["tunnel_id"], config.get("control_plane_organization_id", ""), config.get("control_plane_url_path", ""))
binary = runtime.PREFIX / "original/geranas/FrameBridge-MCP-Private/bin/tunnel-client"
mcp = "http://127.0.0.1:" + str(config["ports"]["mcp"]) + "/mcp"
command = tunnel.build_command(binary, state, mcp, spec)
_worker_status(directory, "connecting")
connected = True
connection_background.start(command, key, state, spec.tunnel_id, mcp, output=io.StringIO(), wait_timeout=1)
finally:
key = ""
current = connection_background.status(state)
if not current.get("alive"):
_worker_status(directory, "disconnected" if current.get("state") in ("stopped", "closed") else "reconnecting")
else:
_worker_status(directory, "connected" if current.get("verified") else "reconnecting")
_connection_action(state, synchronize_connection)
if not runtime.status(state).get("running"):
return 0 if manual_stop_requested(state) else 1
time.sleep(2)
return 0
except Exception:
_worker_status(directory, "failed")
return 1
finally:
if connected:
connection_background.disconnect(state)
runtime.stop(state)
_worker_status(directory, "stopped")
os.close(lockfd)
for sig, handler in old.items():
signal.signal(sig, handler)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("action", choices=["_run"])
parser.add_argument("--state", required=True)
args = parser.parse_args()
return worker(args.state)
if __name__ == "__main__":
raise SystemExit(main())