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
11 changes: 11 additions & 0 deletions docs/fleet-screen.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ Docker, `w0vncserver`, the real /fleet page in Chromium.
KDE's own `krfb` 26.08 does not work for this: it never opens its port unless started as
`krfb-virtualmonitor`, which shares a new, empty monitor instead of the desktop.

### W3BS OS (1.2.7 and later)

A W3BS OS desktop shares its screen out of the box: every W3BS DE session runs wayvnc on a socket
only its user can open (`/run/user/<uid>/w3bs-vnc.sock`, no TCP port), and the machine boots
straight to a locked desktop, so the page shows the lock screen and, with control, you type the
password there. Install with `--fleet-token fj_...` to join at first boot, or join later:

```sh
w3bs fleet join fj_... --screen control --vnc unix:/run/user/$(id -u)/w3bs-vnc.sock
```

### Other desktops (not yet tested by us)

- **GNOME (Wayland):** `w0vncserver` uses the same portal and should work; GNOME shows its
Expand Down
9 changes: 9 additions & 0 deletions os/debian/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ come with it, and the **Agentic Desktop Layer** (`w3bs-desktop`, glibc build in
`w3bs-de autostart` starts its broker and tray, and agent access stays OFF until the user turns it
on.

**Login and fleet screen (1.2.7).** At boot greetd's `initial_session` logs the first user in on
its own with `W3BS_LOCK_AT_START=1`: `w3bs-hyprland` runs hyprlock as soon as Hyprland is up, so
the console shows a lock screen, never an open desktop (`--no-autologin` keeps the tuigreet
greeter). Every Hyprland session also runs wayvnc on `$XDG_RUNTIME_DIR/w3bs-vnc.sock` (0700, no TCP
port; restarted when it exits, which it does when the lock screen goes away). `w3bs fleet screen on
--control --vnc unix:/run/user/<uid>/w3bs-vnc.sock` relays it to w3bs.org/fleet, where the owner
sees the lock screen and can type the password. `--fleet-token fj_...` does that join at first boot
(`w3bs-fleet-join.service`, lingering user service of the first user).

## Tests

`test/dtest.py` boots the ISO in QEMU (KVM when available) and checks the live system, an
Expand Down
65 changes: 64 additions & 1 deletion os/debian/config/includes.chroot/usr/local/sbin/w3bs-os-install
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ machine needs the internet (DHCP and IPv6 are automatic; --ip for a static addre
--ip ADDR/PREFIX --gateway GW [--dns "A B"] static IPv4 instead of DHCP
--ip6 ADDR/PREFIX --gateway6 GW6 static IPv6 as well (often fe80::1)
--iface NAME the interface for --ip/--ip6 (default: the first wired one)
--no-autologin a desktop shows the greeter at boot (default: the first user is logged
in on its own, straight to the lock screen, so the screen can be
watched and unlocked from w3bs.org/fleet)
--fleet-token TOKEN join your fleet on w3bs.org/fleet at first boot (the fj_... token from
that page), sharing the desktop's screen there (view and control)
--serial-console also a login on ttyS0 and kernel messages there
--no-reboot do not offer to reboot at the end
--list-disks print the disks the installer would offer and exit
Expand Down Expand Up @@ -144,6 +149,7 @@ wired_interfaces() {
DEFAULTS="" DISK="" YES="" ENCRYPT=1 HOSTNAME_="" DOMAIN="" TIMEZONE="" KEYMAP="" USERS="" FULLNAME=""
PASSWORD_FILE="" ROOT_PASSWORD_FILE="" PASSPHRASE_FILE="" NO_REBOOT="" SERIAL=""
STATIC_IP="" STATIC_GW="" STATIC_DNS="" STATIC_IP6="" STATIC_GW6="" NET_IFACE="" DESKTOP=""
AUTOLOGIN=1 FLEET_TOKEN=""
while [ $# -gt 0 ]; do
case "$1" in
--defaults) DEFAULTS=1 ;;
Expand All @@ -167,6 +173,8 @@ while [ $# -gt 0 ]; do
--gateway6) STATIC_GW6=${2:-}; shift ;;
--iface) NET_IFACE=${2:-}; shift ;;
--serial-console) SERIAL=1 ;;
--no-autologin) AUTOLOGIN="" ;;
--fleet-token) FLEET_TOKEN=${2:-}; shift ;;
--no-reboot) NO_REBOOT=1 ;;
--desktop) DESKTOP=${2:-}; shift ;;
--server) DESKTOP=none ;;
Expand Down Expand Up @@ -270,6 +278,8 @@ USERS=$(echo $USERS)
for u in $USERS; do [[ "$u" =~ ^[a-z_][a-z0-9_-]{0,30}$ ]] && [ "$u" != root ] || fail "invalid user name $u"; done
[ -n "$FULLNAME" ] || ask FULLNAME "Full name" "W3BS User"
case "$FULLNAME" in *:*) fail "the full name cannot contain ':'" ;; esac
case "$FLEET_TOKEN" in ''|fj_*) ;; *) fail "--fleet-token takes the fj_... token from w3bs.org/fleet" ;; esac
FIRST_USER=${USERS%% *}

if [ -z "$DISK" ]; then
if [ -n "$DEFAULTS" ]; then DISK=$(largest_disk) || true
Expand Down Expand Up @@ -323,6 +333,8 @@ $(lsblk -no NAME,SIZE,FSTYPE "$DISK" 2>/dev/null | sed 's/^/
Users $USERS (zsh, sudo)
Desktop $( [ "$DESKTOP" = hyprland ] && echo "W3BS DE $(cat /usr/share/w3bs-os/de/share/VERSION 2>/dev/null) on Hyprland 0.55 (Debian backports): greetd login, Firefox, TronBrowser" || echo "none (server)" )
Agent access $( [ "$DESKTOP" = hyprland ] && echo "Agentic Desktop Layer installed, agent access OFF until you turn it on in the tray" || echo "-" )
Login $( [ "$DESKTOP" != hyprland ] && echo "-" || { [ -n "$AUTOLOGIN" ] && echo "$FIRST_USER logged in at boot, desktop locked (--no-autologin: greeter)" || echo "greeter"; } )
Fleet $( [ -n "$FLEET_TOKEN" ] && echo "joins w3bs.org/fleet at first boot as $FIRST_USER$( [ "$DESKTOP" = hyprland ] && echo ", screen shared (view and control)" )" || echo "not joined (w3bs fleet join later)" )
SSH on (password logins for users; root by key only)
Dev tools w3bs CLI, curl, wget, git, mise (node, bun, pnpm), readm3, hqterm + hqsh, threatcrush
Box setup cli-tools root-ubuntu.sh at first boot: nginx pages, firewall, oh-my-zsh, tmux
Expand Down Expand Up @@ -503,7 +515,7 @@ EOF
greetd tuigreet waybar foot fuzzel sway-notification-center swaybg swayidle swaylock grim slurp wl-clipboard \
xwayland brightnessctl pavucontrol fonts-dejavu fonts-noto-core fonts-font-awesome thunar libnotify-bin \
pipewire pipewire-pulse wireplumber firefox-esr qt6-wayland polkitd xdg-desktop-portal-gtk sway \
libgl1-mesa-dri dbus-user-session dbus-bin >>"$LOG" 2>&1 \
libgl1-mesa-dri dbus-user-session dbus-bin wayvnc >>"$LOG" 2>&1 \
|| fail "cannot install the desktop packages (see $LOG)"
# The desktop is W3BS DE (w3bs.org/de): its files live in each user's home and update with
# `w3bs-de update`; the copy on this ISO (same release) is kept in /usr/share/w3bs-os/de for
Expand Down Expand Up @@ -566,6 +578,19 @@ vt = 1
command = "tuigreet --time --remember --asterisks --greeting 'W3BS OS · W3BS DE · log in as $_names (user name, then password)' --cmd /usr/local/bin/w3bs-hyprland"
user = "_greetd"
EOF
# The first user is logged in at boot, straight to the lock screen (w3bs-hyprland runs
# hyprlock when W3BS_LOCK_AT_START is set, and shares the session over VNC on a socket only
# that user can open): the console and w3bs.org/fleet show a lock screen, not a text greeter.
if [ -n "$AUTOLOGIN" ]; then
cat >>"$TARGET/etc/greetd/config.toml" <<EOF

# Boot: log $FIRST_USER in on its own, straight to a locked W3BS DE. Delete this section to
# always show the greeter (w3bs-os-install --no-autologin).
[initial_session]
command = "env W3BS_LOCK_AT_START=1 /usr/local/bin/w3bs-hyprland"
user = "$FIRST_USER"
EOF
fi
in_target systemctl disable [email protected] >>"$LOG" 2>&1
in_target systemctl enable greetd.service >>"$LOG" 2>&1 || fail "cannot enable greetd"
in_target systemctl set-default graphical.target >>"$LOG" 2>&1
Expand Down Expand Up @@ -642,6 +667,44 @@ TimeoutStartSec=3600
WantedBy=multi-user.target
EOF
in_target systemctl enable w3bs-first-boot.service >>"$LOG" 2>&1
if [ -n "$FLEET_TOKEN" ]; then
# The fleet agent is a user service of the first user (lingering, so it runs without a
# login); on a desktop it relays the screen from the session's VNC socket.
install -d -m 0700 "$TARGET/etc/w3bs-os"
( umask 077; printf '%s\n' "$FLEET_TOKEN" >"$TARGET/etc/w3bs-os/fleet-token" )
_screen=""
[ "$DESKTOP" = hyprland ] && _screen='--screen control --vnc unix:/run/user/$uid/w3bs-vnc.sock'
cat >"$TARGET/usr/local/sbin/w3bs-os-fleet-join" <<EOF
#!/bin/sh
# W3BS OS first boot: join w3bs.org/fleet with the token given to the installer, then forget it.
u=$FIRST_USER
uid=\$(id -u "\$u") || exit 1
tok=\$(head -n 1 /etc/w3bs-os/fleet-token) || exit 1
loginctl enable-linger "\$u"
for i in \$(seq 1 60); do [ -S "/run/user/\$uid/bus" ] && break; sleep 1; done
runuser -l "\$u" -c "XDG_RUNTIME_DIR=/run/user/\$uid DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/\$uid/bus ~/.local/bin/w3bs fleet join \$tok --non-interactive $_screen" || exit 1
rm -f /etc/w3bs-os/fleet-token
EOF
chmod 0755 "$TARGET/usr/local/sbin/w3bs-os-fleet-join"
cat >"$TARGET/etc/systemd/system/w3bs-fleet-join.service" <<EOF
[Unit]
Description=W3BS OS first boot: join w3bs.org/fleet
Wants=network-online.target
After=network-online.target w3bs-first-boot.service
ConditionPathExists=/etc/w3bs-os/fleet-token

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/w3bs-os-fleet-join
StandardOutput=append:/var/log/w3bs-first-boot.log
StandardError=append:/var/log/w3bs-first-boot.log
TimeoutStartSec=600

[Install]
WantedBy=multi-user.target
EOF
in_target systemctl enable w3bs-fleet-join.service >>"$LOG" 2>&1
fi

step "Installing developer tools (mise: node, bun, pnpm; readm3; hqterm; threatcrush)"
cat >"$TARGET/etc/profile.d/w3bs-dev.sh" <<'EOF'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,42 @@ if [ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/bus" ]; then
systemctl --user import-environment XDG_CURRENT_DESKTOP XDG_SESSION_TYPE 2>/dev/null
fi

rt=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
# Once this session's Hyprland is up: lock it if greetd logged us in on its own (initial_session
# sets W3BS_LOCK_AT_START, so the console shows a lock screen, not an open desktop), and share it
# over VNC on a socket only this user can open ($rt is 0700), which `w3bs fleet screen` relays to
# w3bs.org/fleet. Nothing listens on a TCP port.
session_helpers() {
mark=$rt/w3bs-session.mark
: >"$mark"
(
i=0
while [ "$i" -lt 90 ]; do
i=$((i + 1)); sleep 1
sig=$(find "$rt/hypr" -mindepth 1 -maxdepth 1 -type d -newer "$mark" 2>/dev/null | head -n 1)
[ -n "$sig" ] && [ -S "$sig/.socket.sock" ] || continue
export HYPRLAND_INSTANCE_SIGNATURE="${sig##*/}"
sock=$(cd "$rt" && ls -t wayland-[0-9] 2>/dev/null | head -n 1)
[ -n "$sock" ] && export WAYLAND_DISPLAY="$sock"
if [ "${W3BS_LOCK_AT_START:-}" = 1 ]; then
# A plain Wayland client: no `hyprctl dispatch` (its argument is Lua in a Lua config).
hyprlock --config "$HOME/.local/share/w3bs-de/default/hypr/hyprlock.conf" >>"$log" 2>&1 &
fi
# wayvnc exits when the output it captures changes (the lock screen going away does it):
# start it again for as long as this Hyprland is running.
if command -v wayvnc >/dev/null 2>&1; then
while [ -S "$sig/.socket.sock" ]; do
rm -f "$rt/w3bs-vnc.sock"
wayvnc --unix-socket "$rt/w3bs-vnc.sock" >>"$log" 2>&1
sleep 1
done
rm -f "$rt/w3bs-vnc.sock"
fi
exit 0
done
) &
}

gpu() {
for c in /sys/class/drm/card[0-9]*; do
[ -e "$c/device" ] || continue
Expand Down Expand Up @@ -109,6 +145,7 @@ EOF
run() {
start=$(date +%s)
{ echo "=== $(date) $*"; echo "vm=${vm:-none} drivers=$(gpu)"; } >>"$log"
[ -z "${W3BS_SESSION_CMD:-}" ] && session_helpers
env "$@" $bus ${W3BS_SESSION_CMD:-/usr/local/bin/w3bs-de session} >>"$log" 2>&1
rc=$?
# Copy Hyprland's own log (it lives under the runtime dir, gone at logout).
Expand Down
13 changes: 10 additions & 3 deletions os/debian/test/dtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ def check(name, ok, detail=""):
vm.expect(r"[Pp]assphrase|unlock disk", 300); time.sleep(2); vm.send("w3bs-luks-pass\n")
vm.expect(r"\S+ login: ", 600); time.sleep(3)
if a.desktop:
# The greeter (tuigreet) on the screen: user name, then password.
time.sleep(8); vm.screenshot(os.path.join(a.out, f"greeter-{a.firmware}.png"))
vm.sendkeys("w3bs\n"); time.sleep(2); vm.sendkeys("w3bs-test-pass\n"); time.sleep(70)
# The first user is logged in at boot, straight to the lock screen (hyprlock): the
# password unlocks it.
time.sleep(90); vm.screenshot(os.path.join(a.out, f"locked-{a.firmware}.png"))
vm.sendkeys("w3bs-test-pass\n"); time.sleep(15)
vm.screenshot(os.path.join(a.out, f"desktop-{a.firmware}.png"))
vm.send("w3bs\n"); vm.expect(r"[Pp]assword: ", 60); vm.send("w3bs-test-pass\n")
time.sleep(15); vm.send("PS1='> '; PROMPT='> '\n"); time.sleep(2)
Expand Down Expand Up @@ -125,6 +126,12 @@ def check(name, ok, detail=""):
check("Waybar", re.search(r"WAYBAR=\d", o) is not None, o)
check("notification center (swaync)", re.search(r"SWAYNC=\d", o) is not None, o)
check("Hyprland config has no errors", "CFGERR=0" in o, o)
rc, o = vm.sh("grep -c '^user = \"w3bs\"' /etc/greetd/config.toml | sed 's/^/AUTOLOGIN=/'; "
"grep -c 'Unlocked' ~/.local/state/w3bs-os/hyprland-session.log | sed 's/^/UNLOCKED=/'; "
"test -S /run/user/1000/w3bs-vnc.sock && echo VNCSOCK=1; stat -c VNCMODE=%a /run/user/1000", 60)
check("autologin at boot (greetd initial_session)", "AUTOLOGIN=1" in o, o)
check("boots to the lock screen, unlocked from the keyboard", re.search(r"UNLOCKED=[1-9]", o) is not None, o)
check("VNC on the session's private socket (fleet screen)", "VNCSOCK=1" in o and "VNCMODE=700" in o, o)
rc, o = vm.sh("test -x ~/.local/opt/hqterm-desktop/app/hqterm-desktop -o -x ~/.local/opt/hqterm-desktop/hqterm-desktop.AppImage && echo HQDESK=1; grep -c '^Exec' ~/.local/share/applications/hqterm.desktop | sed 's/^/HQENTRY=/'", 60)
check("hqterm desktop app + launcher entry", "HQDESK=1" in o and "HQENTRY=1" in o, o)
rc, o = vm.sh("w3bs-de doctor; echo DOCTOR=$?", 120)
Expand Down
Loading