hyperhive/nix/templates/weston-vnc.nix
iris 8a97277f20 fix(#540): pin weston socket + inject WAYLAND_DISPLAY globally
Services started by systemd in a gui-enabled container didn't have
WAYLAND_DISPLAY set, so Wayland clients couldn't find the compositor.
libwayland would fall back to a headless display or error out, leaving
apps running invisibly while the VNC session showed a blank weston
desktop (the double-screen problem).

Fix in weston-vnc.nix:
- Pass --socket=wayland-0 to weston so the socket name is
  deterministic (weston normally picks any free wayland-N name).
- Set WAYLAND_DISPLAY=wayland-0 and XDG_RUNTIME_DIR=/run/user/0 as
  global environment.variables gated on hyperhive.gui.enable, so
  every service in the container inherits them automatically.
- Update gui.json to include wayland_display for tooling that reads it.

Update docs/gotchas.md with the rationale and pointer to #540.
2026-05-31 22:59:46 +02:00

134 lines
6.1 KiB
Nix

{
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 environment.variables) 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. Closes #540 (double-screen: VNC shows 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 process in the container
# so Wayland clients started by any systemd service (e.g. bitburner
# launched via a `systemd.services.*` declaration in agent.nix) can
# find the compositor without per-service wiring. Without these vars
# a service starting a Wayland client would either fail to connect
# (libwayland falls back to creating a headless display) or open a
# second compositor entirely — both cause the VNC session to show a
# blank desktop while apps appear to "work" elsewhere (#540).
environment.variables = {
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 ];
};
}