{ pkgs, lib, config, ... }: { # Optional Weston (the reference Wayland compositor) with the VNC # backend, surfaced as a per-agent hyperhive option. An agent turns # it on from its own `agent.nix`: # # hyperhive.gui.enable = true; # # Imported by `harness-base.nix`, so every sub-agent + the manager # has the option available; only those that flip it on get the # service. This is a flat per-agent option (evaluated inside that # agent's own container build) — NOT a `hyperhive.agents..*` # registry, which can't work: each agent is its own # nixosConfiguration and has no cross-agent view. # # VNC port selection: a deterministic FNV-1a hash of the agent name # (derived from the container hostname at runtime) maps into the # range [15900, 16799], mirroring lifecycle::agent_web_port. The # computed port is written to `/etc/hyperhive/gui.json` at service # start; the harness (issue #51) reads that file to know where to # relay WebSocket connections. # # Note: weston's VNC backend does not expose a CLI bind-address flag # (unlike the RDP backend's `--address`), so VNC listens on all # interfaces. The harness WebSocket relay (issue #51) connects only # via 127.0.0.1, and the host firewall should block external access # to the VNC port range. A future weston.ini `[vnc] address=` can # restrict this once upstream supports it. 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 WebSocket relay (see issue #51). Renders in software (pixman) — no GPU, DRM, or VT access, so no extra container capabilities are needed. The VNC port is deterministic: FNV-1a hash of the agent name (taken from the container hostname) mapped into [15900, 16799]. The port and auth mode are written to `/etc/hyperhive/gui.json` at service start so the harness can relay connections. The unit is deliberately built so enabling it can NEVER abort the agent's `nixos-container update`: `Type = "simple"` (so `switch-to-configuration` doesn't block on weston readiness) and the ExecStart script always tries to exec weston after setup — a misconfigured weston degrades to a restart loop visible in `journalctl`, it does not block the rebuild. (Same reasoning as the `tea-login` unit in `harness-base.nix`.) ''; }; config = lib.mkIf config.hyperhive.gui.enable { # Build weston linked against a neatvnc without TLS/auth support. # neatvnc ≥ 0.9 compiled with gnutls always advertises RSA-AES-256 # (type 129), RSA-AES (type 5), and Apple-DH (type 30) security types # regardless of the weston.ini auth-method setting — because # nvnc_has_auth() returns true at the C level, causing weston to call # nvnc_enable_auth() unconditionally. The in-browser RFB client has no # RSA key and cannot complete these handshakes. # # The fix: pass `-Dtls=disabled` to neatvnc's meson build (the option # name from neatvnc's meson_options.txt that guards gnutls + the entire # auth module). With TLS disabled, nvnc_has_auth() returns false, weston # skips nvnc_enable_auth(), and neatvnc advertises only type 1 (None). # gnutls stays in buildInputs so pkg-config resolution doesn't error; # the meson flag overrides the feature to "disabled" at configure time. nixpkgs.overlays = [ (_final: prev: { weston = prev.weston.override { neatvnc = prev.neatvnc.overrideAttrs (old: { mesonFlags = (old.mesonFlags or []) ++ [ "-Dtls=disabled" "-Dnettle=disabled" ]; }); }; }) ]; systemd.services.weston = { description = "Weston Wayland compositor (VNC backend)"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { # `simple`, not `notify`: switch-to-configuration must not # wait on weston signalling readiness. See weston-rdp.nix and # the `tea-login` unit for the same reasoning. Type = "simple"; # Creates /var/lib/weston (0700 root) at start. StateDirectory = "weston"; Environment = "XDG_RUNTIME_DIR=/run/user/0"; # Wrapper script: computes the deterministic VNC port, writes # /etc/hyperhive/gui.json for the harness (issue #51), then # execs weston. Using `exec` keeps the PID stable so systemd # tracks the weston process correctly under Type=simple. # Any failure before the exec triggers Restart=on-failure # (graceful degradation) rather than blocking the rebuild. 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, # mirroring lifecycle::agent_web_port in hive-c0re/src/lifecycle.rs. # Read from /etc/hostname (always present in NixOS containers) # to avoid a dependency on the `hostname` binary (which lives in # pkgs.inetutils, not pkgs.coreutils). # VNC_PORT_BASE=15900, VNC_PORT_RANGE=900 → [15900, 16799]. 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)) # --- Write gui.json marker --- # The harness reads this at startup (issue #51) to know the # VNC port and auth mode for the WebSocket relay. ${pkgs.coreutils}/bin/mkdir -p /etc/hyperhive ${pkgs.coreutils}/bin/printf '{"vnc_port":%d,"auth":"none"}\n' \ "$VNC_PORT" > /etc/hyperhive/gui.json || true # neatvnc is built without gnutls (see nixpkgs.overlays above), # so nvnc_has_auth() returns false and weston skips auth setup # entirely — neatvnc advertises only security type 1 (None). # No weston.ini or --disable-transport-layer-security needed. exec ${pkgs.weston}/bin/weston \ --backend=vnc-backend.so \ --renderer=pixman \ --port="$VNC_PORT" ''; Restart = "on-failure"; RestartSec = "5s"; }; }; # weston on the agent's interactive PATH too, so claude can run # Wayland clients / `weston-info` against the compositor. environment.systemPackages = [ pkgs.weston ]; }; }