feat(#1906): non-root weston gui on a fixed vnc port
This commit is contained in:
parent
5caec9c1a9
commit
3d2e0ef561
5 changed files with 127 additions and 80 deletions
|
|
@ -253,18 +253,27 @@ compositor with the VNC backend, surfaced as
|
|||
`/screen/ws` WebSocket relay (`docs/web-ui/agent.md::Per-agent endpoints`)
|
||||
connects to the compositor at `127.0.0.1:<vnc_port>`.
|
||||
|
||||
- **Port allocation**: deterministic FNV-1a of the agent name
|
||||
(read from `/etc/hostname`, leading `h-` stripped) mapped into
|
||||
`[15900, 16799]`. Mirrors the agent web-UI port pattern from
|
||||
`docs/gotchas.md::Web UI ports collide on hash` — same FNV-1a
|
||||
constant, different range. The compositor's startup script writes
|
||||
`/etc/hyperhive/gui.json = {"vnc_port":N,"auth":"none","wayland_display":"wayland-0"}`
|
||||
so the harness reads the port at runtime; no nix-side / harness-side hash
|
||||
duplication.
|
||||
- **Port allocation**: a **fixed** port (`hyperhive.gui.vncPort`,
|
||||
default 5900). No per-agent hashing: network isolation is
|
||||
unconditional (each agent has its own netns — see
|
||||
`docs/network.md#container-isolation`), so the VNC port is
|
||||
container-local and can't collide across agents. The harness learns
|
||||
the port from the `HIVE_GUI_VNC_PORT` env var (set on the harness
|
||||
service when `gui.enable`) — no marker file, no runtime hash. (Unlike
|
||||
the agent **web-UI** port, which is still an FNV-1a hash because those
|
||||
listen on the shared host stack — see `Web UI ports collide on hash`.)
|
||||
- **Non-root, shared user session**: weston runs as the agent's own
|
||||
user (`hyperhive.user.name`, the same user hive-ag3nt runs as), not
|
||||
root, so the GUI and the agent share one session. The runtime dir is a
|
||||
fixed `/run/gui` (systemd `RuntimeDirectory=gui`, `0700`,
|
||||
`RuntimeDirectoryPreserve=yes` so it survives weston restarts for the
|
||||
wayland client sharing the `/run/gui/wayland-0` socket). Wayland
|
||||
clients in the agent's config (e.g. bitburner electron) must run as the
|
||||
same user with `XDG_RUNTIME_DIR=/run/gui`.
|
||||
- **Fixed Wayland socket name (`--socket=wayland-0`)**: weston is
|
||||
launched with `--socket=wayland-0` so the socket path is
|
||||
deterministic. `harness-base.nix` exports `WAYLAND_DISPLAY=wayland-0`
|
||||
and `XDG_RUNTIME_DIR=/run/user/0` as global system environment
|
||||
and `XDG_RUNTIME_DIR=/run/gui` as global system environment
|
||||
variables (gated on `hyperhive.gui.enable`) so every systemd service
|
||||
in the container inherits them. Without this, services starting
|
||||
Wayland clients could not find the compositor — libwayland falls
|
||||
|
|
|
|||
|
|
@ -355,9 +355,9 @@ shaped).
|
|||
land on the right pixel regardless of CSS scale.
|
||||
- `GET /screen/ws` — raw RFB byte relay: proxies WebSocket
|
||||
frames to the weston VNC server at `127.0.0.1:<vnc_port>`.
|
||||
Transparent to any RFB variant. VNC port comes from
|
||||
`/etc/hyperhive/gui.json` (written by the weston startup
|
||||
script in `weston-vnc.nix`).
|
||||
Transparent to any RFB variant. VNC port comes from the
|
||||
`HIVE_GUI_VNC_PORT` env var (a fixed port set on the harness
|
||||
service when `hyperhive.gui.enable`; see `weston-vnc.nix`).
|
||||
|
||||
Bus events (new vocabulary on `/events/stream`):
|
||||
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ struct AppState {
|
|||
files: TurnFiles,
|
||||
/// Prevents `/api/compact` from racing with an in-flight normal turn.
|
||||
turn_lock: TurnLock,
|
||||
/// VNC port read from `/etc/hyperhive/gui.json` at startup.
|
||||
/// `None` when the file is absent (gui not enabled for this agent).
|
||||
/// VNC port from the `HIVE_GUI_VNC_PORT` env var at startup.
|
||||
/// `None` when unset (gui not enabled for this agent).
|
||||
gui_vnc_port: Option<u16>,
|
||||
}
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ pub async fn serve(
|
|||
files: TurnFiles,
|
||||
turn_lock: TurnLock,
|
||||
) -> Result<()> {
|
||||
let gui_vnc_port = read_gui_json();
|
||||
let gui_vnc_port = read_gui_vnc_port();
|
||||
let static_dir: PathBuf = std::env::var_os("HIVE_STATIC_DIR")
|
||||
.map(PathBuf::from)
|
||||
.context(
|
||||
|
|
@ -277,12 +277,14 @@ async fn serve_icon() -> impl IntoResponse {
|
|||
([("content-type", "image/svg+xml")], body)
|
||||
}
|
||||
|
||||
/// Read `/etc/hyperhive/gui.json` and extract the `vnc_port` field.
|
||||
/// Returns `None` if the file is absent or unparseable — GUI not enabled.
|
||||
fn read_gui_json() -> Option<u16> {
|
||||
let text = std::fs::read_to_string("/etc/hyperhive/gui.json").ok()?;
|
||||
let val: serde_json::Value = serde_json::from_str(&text).ok()?;
|
||||
val["vnc_port"].as_u64().and_then(|p| u16::try_from(p).ok())
|
||||
/// The fixed VNC port weston bound, from the `HIVE_GUI_VNC_PORT` env var
|
||||
/// the harness service sets when gui is enabled (see weston-vnc.nix).
|
||||
/// `None` when unset (gui not enabled for this agent) or unparseable.
|
||||
/// The port is a fixed, container-local value — no per-agent hashing, no
|
||||
/// marker file — because network isolation is unconditional (each agent
|
||||
/// has its own netns, so the port can't collide across containers).
|
||||
fn read_gui_vnc_port() -> Option<u16> {
|
||||
std::env::var("HIVE_GUI_VNC_PORT").ok()?.parse().ok()
|
||||
}
|
||||
|
||||
/// WebSocket handler: upgrade then pump bytes between the WS client and
|
||||
|
|
|
|||
|
|
@ -1772,7 +1772,16 @@ in
|
|||
# bind-mounts and gateway upstream config stay in sync.
|
||||
HIVE_WEB_SOCKET = "/run/hive-agent/${userName}/web.sock";
|
||||
}
|
||||
// otelEnv;
|
||||
// otelEnv
|
||||
// lib.optionalAttrs config.hyperhive.gui.enable {
|
||||
# Tells the harness which fixed VNC port weston bound, and (by
|
||||
# its presence) that gui is enabled — the harness `/screen/ws`
|
||||
# relay reads this instead of a runtime marker file. The port is
|
||||
# container-local + fixed (network isolation is unconditional),
|
||||
# so the same value for every gui agent is fine. See
|
||||
# nix/templates/weston-vnc.nix::hyperhive.gui.vncPort.
|
||||
HIVE_GUI_VNC_PORT = toString config.hyperhive.gui.vncPort;
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = if otel.enable then "${otelExecStart}" else "${pkgs.hyperhive}/bin/${binary} serve";
|
||||
# Pin the journal identity to the binary name. Without this,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,27 @@
|
|||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
# GUI processes run as the agent's own non-root user — the same user
|
||||
# hive-ag3nt 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
|
||||
'';
|
||||
in
|
||||
{
|
||||
# Optional Weston (Wayland compositor) with the VNC backend,
|
||||
# surfaced as a per-agent `hyperhive.gui.enable` option. Imported
|
||||
|
|
@ -25,16 +46,49 @@
|
|||
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`.
|
||||
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 and the wayland client run as the agent's own non-root
|
||||
user (`hyperhive.user.name`), sharing one user session with a
|
||||
fixed `XDG_RUNTIME_DIR=/run/gui`.
|
||||
'';
|
||||
};
|
||||
|
||||
# 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.
|
||||
|
|
@ -53,59 +107,32 @@
|
|||
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" \
|
||||
# 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="$VNC_PORT" \
|
||||
--port=${toString config.hyperhive.gui.vncPort} \
|
||||
--socket=wayland-0 \
|
||||
--disable-transport-layer-security
|
||||
'';
|
||||
|
|
@ -127,7 +154,7 @@
|
|||
# shows a blank desktop.
|
||||
systemd.globalEnvironment = {
|
||||
WAYLAND_DISPLAY = "wayland-0";
|
||||
XDG_RUNTIME_DIR = "/run/user/0";
|
||||
XDG_RUNTIME_DIR = "/run/gui";
|
||||
};
|
||||
|
||||
# weston on the agent's interactive PATH so claude can run Wayland
|
||||
|
|
|
|||
Loading…
Reference in a new issue