Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions desktop/packaging/kubuntu/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# syntax=docker/dockerfile:1
# A headless Kubuntu desktop for AI agents, watched from w3bs.org/fleet.
#
# KWin (Wayland) drives a virtual display: the host's vkms device, a KMS display with no
# hardware behind it, so Mesa composites with OpenGL on the CPU (llvmpipe) and no GPU is
# needed. Plasma, Konsole and the Agentic Desktop Layer (`w3bs-desktop`, Enabled from the
# start, MCP on 127.0.0.1:7801) run on that KWin. TigerVNC's w0vncserver shares the session
# through the KDE portal on a Unix socket in /vnc, which the host's `w3bs fleet agent` relays
# to w3bs.org; no TCP port is opened.
#
# Built and run by `w3bs fleet desktop start`; see w3bs.org/docs/fleet-screen.md.

# Ubuntu packages TigerVNC 1.15, which has no Wayland server; w0vncserver arrived in 1.16.
FROM ubuntu:26.04 AS w0vncserver
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake pkg-config ca-certificates curl \
zlib1g-dev libpixman-1-dev libjpeg-turbo8-dev libglib2.0-dev libpipewire-0.3-dev uuid-dev \
libwayland-dev libwayland-bin wayland-protocols libxkbcommon-dev libpam0g-dev \
libx11-dev libxext-dev libxtst-dev libxdamage-dev libxfixes-dev libxrandr-dev libxi-dev libxcursor-dev
ARG TIGERVNC_VERSION=1.16.2
ARG TIGERVNC_SHA256=b107c0c8b8a962594281690366c24186e95c2ea4a169acbc0076aa62ed01f467
RUN curl -fsSL -o /tmp/tigervnc.tar.gz "https://github.com/TigerVNC/tigervnc/archive/refs/tags/v${TIGERVNC_VERSION}.tar.gz" \
&& echo "${TIGERVNC_SHA256} /tmp/tigervnc.tar.gz" | sha256sum -c - \
&& mkdir /src && tar xzf /tmp/tigervnc.tar.gz -C /src --strip-components=1
WORKDIR /build
RUN cmake /src -DCMAKE_BUILD_TYPE=Release -DBUILD_VIEWER=OFF -DENABLE_NLS=OFF -DENABLE_GNUTLS=OFF \
-DENABLE_NETTLE=OFF -DENABLE_H264=OFF -DENABLE_WAYLAND=ON -DINSTALL_SYSTEMD_UNITS=OFF \
&& make -j"$(nproc)" w0vncserver && install -m 0755 unix/w0vncserver/w0vncserver /usr/local/bin/

FROM ubuntu:26.04
ARG DESKTOP_VERSION=0.3.0
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8
RUN apt-get update && apt-get install -y --no-install-recommends \
kwin-wayland plasma-desktop plasma-workspace konsole breeze breeze-cursor-theme libkscreen-bin \
xdg-desktop-portal xdg-desktop-portal-kde pipewire wireplumber dbus-daemon flatpak \
libegl-mesa0 libgl1-mesa-dri libpipewire-0.3-0t64 libjpeg-turbo8 libpixman-1-0 libpam0g \
fonts-noto-core ca-certificates curl tini procps \
&& rm -rf /var/lib/apt/lists/*
# KWin asks for real-time scheduling, which a container's capability set does not grant.
RUN setcap -r /usr/bin/kwin_wayland 2>/dev/null || true
COPY --from=w0vncserver /usr/local/bin/w0vncserver /usr/local/bin/w0vncserver
RUN arch="$(uname -m)" \
&& base="https://github.com/profullstack/w3bs.org/releases/download/desktop-v${DESKTOP_VERSION}" \
&& file="w3bs-desktop-${DESKTOP_VERSION}-${arch}-unknown-linux-gnu.tar.gz" \
&& cd /tmp && curl -fsSLO "$base/$file" && curl -fsSLO "$base/SHA256SUMS" \
&& grep " $file\$" SHA256SUMS | sha256sum -c - \
&& tar xzf "$file" ./bin/w3bs-desktop && install -m 0755 bin/w3bs-desktop /usr/local/bin/ \
&& rm -rf /tmp/*
COPY session.sh /usr/local/bin/w3bs-kubuntu-session
RUN chmod 0755 /usr/local/bin/w3bs-kubuntu-session && mkdir -p /vnc /home/desktop && chmod 1777 /vnc /home/desktop
ENV HOME=/home/desktop
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/w3bs-kubuntu-session"]
83 changes: 83 additions & 0 deletions desktop/packaging/kubuntu/session.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/bin/sh
# The headless Kubuntu session inside the w3bs desktop container (see Dockerfile).
# Runs as the host user (docker run --user), with $HOME on a host volume.
# W3BS_DRM the KMS device KWin drives (default: the vkms card passed in)
# W3BS_GEOMETRY 1920x1080 by default
# W3BS_MCP 127.0.0.1:7801 by default (the desktop layer's MCP endpoint)
set -eu
GEOMETRY=${W3BS_GEOMETRY:-1920x1080}
export XDG_RUNTIME_DIR=/tmp/runtime-$(id -u)
export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus
export XDG_CURRENT_DESKTOP=KDE XDG_SESSION_TYPE=wayland XDG_SESSION_DESKTOP=KDE KDE_FULL_SESSION=true
export XDG_CONFIG_HOME=$HOME/.config XDG_DATA_HOME=$HOME/.local/share
# The host user has no passwd entry in the image, so name the shell Konsole should start.
export SHELL=/bin/bash
LOGS=$HOME/.local/state/w3bs-desktop/logs
mkdir -p -m 0700 "$XDG_RUNTIME_DIR" && mkdir -p "$LOGS" "$XDG_CONFIG_HOME/xdg-desktop-portal"
printf '[preferred]\ndefault=kde;gtk\n' > "$XDG_CONFIG_HOME/xdg-desktop-portal/kde-portals.conf"
rm -f /vnc/vnc.sock

say() { echo "w3bs-kubuntu: $*"; }
wait_for() { for _ in $(seq 1 100); do [ -e "$1" ] && return 0; sleep 0.1; done; say "timed out waiting for $1"; exit 1; }

# The display: the vkms card the host passed in (`w3bs fleet desktop start` finds it).
DRM=${W3BS_DRM:-}
if [ -z "$DRM" ]; then
for card in /dev/dri/card*; do
[ "$(basename "$(readlink -f "/sys/class/drm/$(basename "$card")/device")")" = vkms ] && DRM=$card && break
done
fi
[ -n "$DRM" ] || { say "no vkms display passed in; on the host run: sudo modprobe vkms"; exit 2; }

dbus-daemon --session --address="$DBUS_SESSION_BUS_ADDRESS" --nofork --nopidfile >"$LOGS/dbus.log" 2>&1 &
wait_for "$XDG_RUNTIME_DIR/bus"
pipewire >"$LOGS/pipewire.log" 2>&1 &
wireplumber >"$LOGS/wireplumber.log" 2>&1 &

# KWin hands its screencast and fake-input protocols only to clients it trusts by their desktop
# file. Ubuntu's KWin 6.6 does not trust the KDE portal inside this container (it does on
# Fedora's 6.7), so the portal, and with it w0vncserver and the desktop layer, would get no
# screen. Everything in this compositor is part of one agent desktop that the desktop layer
# already lets agents see and drive, so turn the check off here; it never touches the host.
export KWIN_WAYLAND_NO_PERMISSION_CHECKS=1
kbuildsycoca6 >"$LOGS/kbuildsycoca.log" 2>&1 || true

# KWin on the virtual display; with no GPU, Mesa renders OpenGL on the CPU.
KWIN_DRM_DEVICES=$DRM kwin_wayland --drm --socket wayland-0 >"$LOGS/kwin.log" 2>&1 &
KWIN=$!
wait_for "$XDG_RUNTIME_DIR/wayland-0"
export WAYLAND_DISPLAY=wayland-0 QT_QPA_PLATFORM=wayland
# vkms starts at 1024x768.
for _ in $(seq 1 20); do kscreen-doctor -o >/dev/null 2>&1 && break; sleep 0.5; done
OUTPUT=$(kscreen-doctor -j 2>/dev/null | sed -n 's/.*"name": *"\([^"]*\)".*/\1/p' | head -1)
[ -n "$OUTPUT" ] && kscreen-doctor "output.$OUTPUT.mode.$GEOMETRY@60" >"$LOGS/kscreen.log" 2>&1 \
|| say "could not set $GEOMETRY on ${OUTPUT:-the display}; see $LOGS/kscreen.log"

/usr/lib/*/libexec/xdg-desktop-portal-kde >"$LOGS/xdg-desktop-portal-kde.log" 2>&1 &
sleep 1
/usr/libexec/xdg-desktop-portal -r >"$LOGS/xdg-desktop-portal.log" 2>&1 &
sleep 1
# Nobody can click a consent dialog on a headless desktop: grant remote control and screenshots
# up front, the way KDE intends for unattended machines. w0vncserver and the desktop layer each
# open their own portal session against this grant.
flatpak permission-set kde-authorized remote-desktop "" yes
flatpak permission-set screenshot screenshot "" yes

plasmashell >"$LOGS/plasmashell.log" 2>&1 &
konsole >"$LOGS/konsole.log" 2>&1 &

# The VNC server: a Unix socket only, owner-only (0600), no password (the socket is the lock).
w0vncserver -rfbport -1 -rfbunixpath /vnc/vnc.sock -SecurityTypes None -AlwaysShared \
-desktop "w3bs desktop" >"$LOGS/w0vncserver.log" 2>&1 &
W0=$!

# The Agentic Desktop Layer, Enabled from the start: this desktop exists for agents.
w3bs-desktop daemon --adapter auto --enabled-by-default --mcp-http "${W3BS_MCP:-127.0.0.1:7801}" \
>"$LOGS/w3bs-desktop.log" 2>&1 &
say "up: KWin on $DRM, VNC on /vnc/vnc.sock, desktop layer MCP on http://${W3BS_MCP:-127.0.0.1:7801}/mcp"

# Stay up while the compositor and the VNC server live (docker restarts the container
# otherwise); tini reaps everything else, including w0vncserver's per-viewer children.
while kill -0 "$KWIN" 2>/dev/null && kill -0 "$W0" 2>/dev/null; do sleep 5; done
say "KWin or w0vncserver exited; see $LOGS"
exit 1
12 changes: 9 additions & 3 deletions desktop/tests/rig/fleet-screen.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#
# desktop/tests/rig/fleet-screen.sh [SCRATCH_DIR]
# W3BS_BIN=path/to/w3bs KEEP=1 desktop/tests/rig/fleet-screen.sh
# VNC_UNIX=1 desktop/tests/rig/fleet-screen.sh # VNC on a Unix socket, as `w3bs fleet desktop` does
#
# Checks: the page shows the remote desktop; in view mode, input the page sends is dropped by
# the agent (the page's own switch is forced on); after `w3bs fleet screen on --control`,
Expand All @@ -21,6 +22,11 @@ NAME=${NAME:-adl-fleet-screen}
IMAGE=${IMAGE:-fedora:44}
PORT=${PORT:-3377}
VNC_PORT=${VNC_PORT:-5937}
if [ "${VNC_UNIX:-0}" = 1 ]; then
VNC_LISTEN="-rfbport -1 -rfbunixpath /rig/vnc.sock"; VNC_ADDR=unix:/rig/vnc.sock
else
VNC_LISTEN="-localhost -rfbport $VNC_PORT"; VNC_ADDR=127.0.0.1:$VNC_PORT
fi
W3BS_BIN=${W3BS_BIN:-${CARGO_TARGET_DIR:-$REPO/package-manager/target}/debug/w3bs}
RENDER=${RENDER:-$(ls /dev/dri/renderD* 2>/dev/null | head -1)}
[ -n "$RENDER" ] || { echo "no /dev/dri/renderD* on this host; KWin cannot screencast without OpenGL" >&2; exit 2; }
Expand Down Expand Up @@ -88,10 +94,10 @@ sleep 2
mkdir -p /root/.config/tigervnc
echo '$VNC_PASSWORD' | vncpasswd -f > /root/.config/tigervnc/passwd
chmod 600 /root/.config/tigervnc/passwd
setsid w0vncserver -localhost -rfbport $VNC_PORT -SecurityTypes VncAuth \\
setsid w0vncserver $VNC_LISTEN -SecurityTypes VncAuth \\
-PasswordFile /root/.config/tigervnc/passwd -AlwaysShared >\$L/w0vncserver.log 2>&1 &
sleep 1
w3bs fleet join $TOKEN --api http://localhost:$PORT --no-service --screen view --vnc 127.0.0.1:$VNC_PORT
w3bs fleet join $TOKEN --api http://localhost:$PORT --no-service --screen view --vnc $VNC_ADDR
setsid w3bs fleet agent >\$L/agent.log 2>&1 &
EOF
chmod +x "$RIG"/*.sh
Expand All @@ -110,7 +116,7 @@ for i in $(seq 1 20); do grep -q "input messages dropped" "$RIG/logs/agent.log"
grep -q "input messages dropped (view only)" "$RIG/logs/agent.log" && ok "agent: $(grep -m1 'input messages dropped' "$RIG/logs/agent.log" | sed 's/^w3bs fleet agent: //')" || bad "agent log: $(tail -5 "$RIG/logs/agent.log")"

echo "== control: w3bs fleet screen on --control, then typing lands"
docker exec "$NAME" sh -c ". /rig/env.sh; w3bs fleet screen on --control --vnc 127.0.0.1:$VNC_PORT" >"$RIG/logs/screen-on.log" 2>&1 && ok "$(head -1 "$RIG/logs/screen-on.log")" || bad "screen on: $(cat "$RIG/logs/screen-on.log")"
docker exec "$NAME" sh -c ". /rig/env.sh; w3bs fleet screen on --control --vnc $VNC_ADDR" >"$RIG/logs/screen-on.log" 2>&1 && ok "$(head -1 "$RIG/logs/screen-on.log")" || bad "screen on: $(cat "$RIG/logs/screen-on.log")"
for i in $(seq 1 90); do machines | grep -q '"mode":"control"' && machines | grep -q '"screen_listening":true' && break; sleep 0.5; done
machines | grep -q '"mode":"control"' && ok "agent picked up control mode without a restart" || bad "mode: $(machines | head -c 400)"
(cd "$REPO" && node "$HERE/fleet-screen-browser.mjs" control "$RIG/state.json" "$RIG/shots" "typed-through-w3bs") >"$RIG/logs/browser-control.log" 2>&1 \
Expand Down
52 changes: 50 additions & 2 deletions docs/fleet-screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ running the [Agentic Desktop Layer](/desktop), a machine in another room), open
**w3bs.org/fleet**, pick the machine and press **View screen**. The page shows its desktop live
in the browser (noVNC), view only unless you allow control.

**A server with no desktop at all?** One command gives it one: see
[A headless desktop for AI agents](#a-headless-desktop-for-ai-agents).

## How it works

```
browser (noVNC) --wss--> w3bs.org relay <--wss-- w3bs fleet agent --tcp--> local VNC server
browser (noVNC) --wss--> w3bs.org relay <--wss-- w3bs fleet agent --> local VNC server
(TCP or a Unix socket)
```

- **Nothing on the machine listens to the network.** The fleet agent (`w3bs fleet agent`, the
Expand All @@ -31,14 +35,58 @@ browser (noVNC) --wss--> w3bs.org relay <--wss-- w3bs fleet agent --tcp--> local
- **The VNC password stays in your browser.** VNC authentication is a challenge and response;
w3bs.org relays the response and never sees the password.

## Turn it on
## A headless desktop for AI agents

On a Linux server with Docker and no desktop, after `w3bs fleet join`:

```sh
w3bs fleet desktop start # view only from /fleet
w3bs fleet desktop start --control # also mouse and keyboard from /fleet
```

That is all. It runs a **Kubuntu desktop in a Docker container** (`w3bs-desktop`) and shares its
screen on /fleet: press **View screen**. The first start builds the image (a few minutes);
after that it starts in seconds, and Docker restarts it with the machine.

What is in the container:

- **KDE Plasma** (KWin, Plasma shell, Konsole) from Ubuntu 26.04's packages.
- **No GPU needed.** KWin drives the kernel's `vkms` virtual display and Mesa renders OpenGL on
the CPU. `start` loads `vkms` (asking `sudo` once) and keeps it loaded across reboots.
- **The [Agentic Desktop Layer](/desktop)**, enabled from the start, with its MCP endpoint on
`http://127.0.0.1:7802/mcp` (not 7801, so it never clashes with a desktop layer on the host).
- **TigerVNC's `w0vncserver`**, built from source in the image (Ubuntu's TigerVNC is too old to
have it), listening only on a Unix socket in `~/.local/share/w3bs/desktop/vnc/` that only
your user can open. No TCP port, no VNC password to manage.
- Its home directory is `~/.local/share/w3bs/desktop/home` on the host, kept across restarts.

Give an AI agent the desktop:

```sh
w3bs fleet desktop agent claude # registers it, prints the `claude mcp add ...` line
w3bs fleet desktop agent watcher --preset observe # look, never touch
```

Run the printed line on the same machine; the agent then sees and drives the Kubuntu desktop
through MCP, and you watch it on /fleet.

```sh
w3bs fleet desktop status # running? shared? image current?
w3bs fleet desktop logs # container log; session logs are under home/.local/state
w3bs fleet desktop start --rebuild # rebuild the image (after a w3bs update changes it)
w3bs fleet desktop start --geometry 2560x1440
w3bs fleet desktop stop # remove the container and stop sharing; home is kept
```

## Turn it on (your own VNC server)

On the machine, after it joined your fleet:

```sh
w3bs fleet screen on # view only, VNC server on 127.0.0.1:5900
w3bs fleet screen on --control # also allow mouse and keyboard from the page
w3bs fleet screen on --vnc 127.0.0.1:5901
w3bs fleet screen on --vnc unix:/run/user/1000/vnc.sock # a Unix socket
w3bs fleet screen # show the setting and whether the VNC server answers
w3bs fleet screen off # stop; open sessions end within one report (30 s)
```
Expand Down
8 changes: 4 additions & 4 deletions package-manager/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package-manager/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "3"
members = ["crates/w3bs-schema", "crates/w3bs-core", "crates/w3bs-cli", "crates/w3bs-publish"]

[workspace.package]
version = "0.1.6"
version = "0.1.7"
edition = "2024"
license = "MIT OR Apache-2.0"
repository = "https://github.com/profullstack/w3bs.org"
Expand Down
Loading
Loading