{ 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. Also records the # fixed Wayland socket name (`wayland-0`) so other tooling can # read it without inspecting the env-var injection below. ${pkgs.coreutils}/bin/mkdir -p /etc/hyperhive ${pkgs.coreutils}/bin/printf '{"vnc_port":%d,"auth":"none","wayland_display":"wayland-0"}\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" # --socket=wayland-0: pin the compositor's Wayland socket name # to `wayland-0` (weston default is to pick any free name such # as `wayland-1`). Pinning lets the WAYLAND_DISPLAY=wayland-0 # global env injection below (see systemd.globalEnvironment) take # effect unconditionally — any Wayland client launched by any # systemd service in this container automatically connects to # this compositor instead of failing or starting a second # isolated display (fixes double-screen: VNC showing blank # weston desktop while services render on a different seat). exec ${pkgs.weston}/bin/weston \ --config="$WESTON_INI" \ --backend=vnc-backend.so \ --renderer=pixman \ --port="$VNC_PORT" \ --socket=wayland-0 \ --disable-transport-layer-security ''; Restart = "on-failure"; RestartSec = "5s"; }; }; # Expose the compositor's socket to every systemd service in the # container so Wayland clients (e.g. bitburner started via # `systemd.services.*` in agent.nix) can find the compositor # without per-service wiring. `systemd.globalEnvironment` is the # correct path — it sets DefaultEnvironment in systemd.conf, # reaching all units started by PID 1. `environment.variables` # goes to /etc/environment (PAM sessions only) and is NOT visible # to systemd service units. Without these vars a service starting # a Wayland client would either fail to connect (libwayland falls # back to a headless display) or open a second compositor — VNC # shows a blank desktop. systemd.globalEnvironment = { WAYLAND_DISPLAY = "wayland-0"; XDG_RUNTIME_DIR = "/run/user/0"; }; # weston on the agent's interactive PATH so claude can run Wayland # clients / weston-info against the compositor. environment.systemPackages = [ pkgs.weston ]; }; }