{ pkgs, lib, config, ... }: { # Optional Weston (Wayland compositor) with the VNC backend, # surfaced as a per-agent `hyperhive.gui.enable` option. Imported # from harness-base.nix so every sub-agent + the manager sees the # option; only those that flip it on get the service. # # Port allocation, weston bind-address quirk, PAM service name, the # Type=simple choice, idle-time=0: all in # docs/gotchas.md::Weston VNC compositor. # Harness-side WebSocket relay shape: docs/web-ui/agent.md::Per-agent # endpoints (`/screen` + `/screen/ws`). options.hyperhive.gui.enable = lib.mkOption { type = lib.types.bool; default = false; description = '' Run Weston with the VNC backend as a systemd service, for in-browser GUI access via the harness `/screen/ws` WebSocket relay. Renders in software (pixman) — no GPU, DRM, or VT access, so no extra container capabilities are needed. The VNC port is a deterministic FNV-1a hash of the agent name mapped into `[15900, 16799]`, written to `/etc/hyperhive/gui.json` at service start so the harness can relay connections without a separate config flag. The unit is `Type = "simple"` so a misconfigured weston degrades to a restart loop instead of blocking `nixos-container update`. ''; }; config = lib.mkIf config.hyperhive.gui.enable { # neatvnc ≥ 0.9 always calls the PAM auth callback for Apple-DH # (type 30), regardless of weston.ini auth-method=none. # pam_permit.so accepts the browser's empty Apple-DH credentials. # Service name MUST be the literal `weston-remote-access` — that's # the string libweston passes to pam_start() in libweston/auth.c. security.pam.services."weston-remote-access".text = '' auth sufficient pam_permit.so account sufficient pam_permit.so session sufficient pam_permit.so ''; systemd.services.weston = { description = "Weston Wayland compositor (VNC backend)"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; StateDirectory = "weston"; Environment = "XDG_RUNTIME_DIR=/run/user/0"; # Wrapper script: computes the deterministic VNC port, writes # /etc/hyperhive/gui.json for the harness, then execs weston. # `exec` keeps the PID stable so systemd tracks the weston # process correctly under Type=simple. ExecStart = pkgs.writeShellScript "weston-vnc" '' mkdir -p /run/user/0 && chmod 700 /run/user/0 || true # --- Compute deterministic VNC port via FNV-1a --- # Agent name = container hostname with leading `h-` stripped. # Read from /etc/hostname (always present in NixOS containers) # to avoid depending on `hostname` (lives in pkgs.inetutils, # not pkgs.coreutils). RAW_HOST=$(${pkgs.coreutils}/bin/cat /etc/hostname) AGENT_NAME=$(${pkgs.coreutils}/bin/printf '%s' "$RAW_HOST" \ | ${pkgs.gnused}/bin/sed 's/^h-//') hash=2166136261 for byte in $(${pkgs.coreutils}/bin/printf '%s' "$AGENT_NAME" \ | ${pkgs.coreutils}/bin/od -An -tu1 \ | ${pkgs.coreutils}/bin/tr -s ' \n' ' '); do [ -n "$byte" ] || continue hash=$(( ((hash ^ byte) * 16777619) & 4294967295 )) done VNC_PORT=$((15900 + hash % 900)) # Marker file the harness reads at startup. ${pkgs.coreutils}/bin/mkdir -p /etc/hyperhive ${pkgs.coreutils}/bin/printf '{"vnc_port":%d,"auth":"none"}\n' \ "$VNC_PORT" > /etc/hyperhive/gui.json || true # --disable-transport-layer-security: skips the VeNCrypt TLS # wrapper so plain auth types (incl. Apple-DH type 30) are # advertised directly. [core] idle-time=0 disables the # compositor's 300s idle/lock screen. WESTON_INI=$(${pkgs.coreutils}/bin/mktemp /tmp/weston-XXXXXX.ini) ${pkgs.coreutils}/bin/printf '[core]\nidle-time=0\n\n[vnc]\nauth-method=none\n' > "$WESTON_INI" exec ${pkgs.weston}/bin/weston \ --config="$WESTON_INI" \ --backend=vnc-backend.so \ --renderer=pixman \ --port="$VNC_PORT" \ --disable-transport-layer-security ''; Restart = "on-failure"; RestartSec = "5s"; }; }; # weston on the agent's interactive PATH so claude can run Wayland # clients / weston-info against the compositor. environment.systemPackages = [ pkgs.weston ]; }; }