216 lines
9.5 KiB
Nix
216 lines
9.5 KiB
Nix
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
# GUI processes run as the agent's own non-root user — the same user
|
|
# hive-agent runs as (declared + home-chowned by harness-base.nix) — so
|
|
# weston, the wayland client, and the agent share one user session.
|
|
# `hyperhive.user.name` is set per-agent by the meta-flake renderer.
|
|
userName = config.hyperhive.user.name;
|
|
|
|
# Static weston config. `[core] idle-time=0` disables the 300s idle /
|
|
# lock screen; `[vnc] auth-method=none` + the `--disable-transport-
|
|
# layer-security` flag below advertise plain auth types directly. A
|
|
# store file (not a runtime mktemp) so weston's ExecStart is a direct
|
|
# exec — no wrapper script, which keeps the journal SyslogIdentifier
|
|
# clean (`weston`, not a store-path basename).
|
|
westonIni = pkgs.writeText "weston.ini" ''
|
|
[core]
|
|
idle-time=0
|
|
|
|
[vnc]
|
|
auth-method=none
|
|
|
|
[output]
|
|
name=VNC-1
|
|
mode=1280x720
|
|
'';
|
|
in
|
|
{
|
|
# Optional Weston (Wayland compositor) with the VNC backend,
|
|
# surfaced as a per-agent `hyperhive.gui.enable` option. Imported
|
|
# from ./default.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.
|
|
|
|
Weston binds a fixed VNC port (`hyperhive.gui.vncPort`) on the
|
|
container's own loopback. Network isolation is unconditional
|
|
(each agent has its own netns), so a fixed port can't collide
|
|
across containers — no per-agent hashing needed. The harness
|
|
learns the port from the `HIVE_GUI_VNC_PORT` env var (set by the
|
|
harness service when gui is enabled). The unit is
|
|
`Type = "simple"` so a misconfigured weston degrades to a restart
|
|
loop instead of blocking `nixos-container update`.
|
|
|
|
Weston, the wayland client and the agent harness run as the
|
|
agent's own non-root user (`hyperhive.user.name`), sharing one
|
|
session: a fixed `XDG_RUNTIME_DIR=/run/gui`, one wayland display,
|
|
and one D-Bus session bus at `/run/gui/bus` (gui-dbus.service),
|
|
so GUI clients need no private `dbus-run-session`.
|
|
'';
|
|
};
|
|
|
|
# Fixed VNC port weston binds inside the container. Safe to be the
|
|
# same for every agent because network isolation is unconditional
|
|
# (private netns per container — see hive-network.nix), so the port
|
|
# is container-local and can't collide. Internal: the harness reads
|
|
# the value via the `HIVE_GUI_VNC_PORT` env var the harness service
|
|
# injects from this option, not directly.
|
|
options.hyperhive.gui.vncPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 5900;
|
|
internal = true;
|
|
description = ''
|
|
VNC port weston binds inside the container (default 5900, the
|
|
standard VNC port). Container-local, so the same value for every
|
|
agent is fine. Surfaced to the harness as `HIVE_GUI_VNC_PORT`.
|
|
'';
|
|
};
|
|
|
|
config = lib.mkIf config.hyperhive.gui.enable {
|
|
# The GUI must run non-root: weston + the wayland client share the
|
|
# agent's own user session. `user.name` is the agent name for every
|
|
# spawned agent; only a misconfigured root-named agent would trip this.
|
|
assertions = [
|
|
{
|
|
assertion = userName != "root";
|
|
message = "hyperhive.gui.enable requires a non-root hyperhive.user.name (the GUI runs as that user).";
|
|
}
|
|
];
|
|
|
|
# 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
|
|
'';
|
|
|
|
# One shared D-Bus *session* bus for the whole GUI session, bound at
|
|
# /run/gui/bus and owned by the agent user. Wayland GUI clients
|
|
# (chromium / electron via ozone) refuse to map a toplevel without a
|
|
# reachable session bus ("Failed to connect to the bus" -> binds
|
|
# xdg_wm_base then destroys it = invisible window, though CDP still
|
|
# works). Running ONE persistent bus here -- instead of each client
|
|
# wrapping itself in `dbus-run-session` (a private throwaway bus per
|
|
# process) -- keeps weston, the agent harness and the GUI client in a
|
|
# single session: one user, one XDG_RUNTIME_DIR, one wayland display,
|
|
# one bus. The address is exported via globalEnvironment below so
|
|
# every unit in the container inherits it.
|
|
systemd.services.gui-dbus = {
|
|
description = "Shared D-Bus session bus for the GUI session";
|
|
wantedBy = [ "multi-user.target" ];
|
|
before = [ "weston.service" ];
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
User = userName;
|
|
Group = userName;
|
|
# Share /run/gui with weston; ordered `before` weston so this
|
|
# creates + chowns the dir first. Preserve across restarts so
|
|
# weston + clients don't lose the dir holding their sockets.
|
|
RuntimeDirectory = "gui";
|
|
RuntimeDirectoryMode = "0700";
|
|
RuntimeDirectoryPreserve = "yes";
|
|
Environment = "XDG_RUNTIME_DIR=/run/gui";
|
|
# dbus-daemon unlinks a stale path socket before binding, so a
|
|
# restart re-binds /run/gui/bus cleanly.
|
|
ExecStart = "${pkgs.dbus}/bin/dbus-daemon --session --nofork --nopidfile --address=unix:path=/run/gui/bus";
|
|
SyslogIdentifier = "gui-dbus";
|
|
Restart = "on-failure";
|
|
RestartSec = "2s";
|
|
};
|
|
};
|
|
|
|
systemd.services.weston = {
|
|
description = "Weston Wayland compositor (VNC backend)";
|
|
after = [
|
|
"network.target"
|
|
"gui-dbus.service"
|
|
];
|
|
wants = [ "gui-dbus.service" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
StateDirectory = "weston";
|
|
# Run as the agent's own user; share a fixed runtime dir at
|
|
# /run/gui (RuntimeDirectory creates+chowns it). Preserve it
|
|
# across weston restarts so the wayland client sharing the
|
|
# /run/gui/wayland-0 socket doesn't lose the dir under it. 0700
|
|
# because a wayland XDG_RUNTIME_DIR must not be group/world-accessible.
|
|
User = userName;
|
|
Group = userName;
|
|
RuntimeDirectory = "gui";
|
|
RuntimeDirectoryMode = "0700";
|
|
RuntimeDirectoryPreserve = "yes";
|
|
Environment = "XDG_RUNTIME_DIR=/run/gui";
|
|
# Direct exec (no wrapper script): fixed `--port`, static config.
|
|
# `--socket=wayland-0` pins the compositor's Wayland socket name
|
|
# (weston otherwise picks any free name like `wayland-1`), so the
|
|
# `WAYLAND_DISPLAY=wayland-0` globalEnvironment injection below
|
|
# reaches every wayland client in the container deterministically
|
|
# (fixes double-screen: VNC showing a blank weston desktop while a
|
|
# client renders on a different seat). `--disable-transport-layer-
|
|
# security` skips the VeNCrypt TLS wrapper so plain auth types
|
|
# (incl. Apple-DH type 30) are advertised directly.
|
|
ExecStart = ''
|
|
${pkgs.weston}/bin/weston \
|
|
--config=${westonIni} \
|
|
--backend=vnc-backend.so \
|
|
--renderer=pixman \
|
|
--port=${toString config.hyperhive.gui.vncPort} \
|
|
--socket=wayland-0 \
|
|
--disable-transport-layer-security
|
|
'';
|
|
# ExecStart is already a direct `weston` exec (basename is clean), but
|
|
# pin the identity explicitly so it can't drift if a wrapper is ever
|
|
# introduced (sweep per the systemd SyslogIdentifier convention).
|
|
SyslogIdentifier = "weston";
|
|
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. DBUS_SESSION_BUS_ADDRESS points every unit
|
|
# at the one shared session bus (gui-dbus.service above) so GUI
|
|
# clients use it instead of spawning a private `dbus-run-session`.
|
|
systemd.globalEnvironment = {
|
|
WAYLAND_DISPLAY = "wayland-0";
|
|
XDG_RUNTIME_DIR = "/run/gui";
|
|
DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/gui/bus";
|
|
};
|
|
|
|
# weston on the agent's interactive PATH so claude can run Wayland
|
|
# clients / weston-info against the compositor.
|
|
environment.systemPackages = [ pkgs.weston ];
|
|
};
|
|
}
|