diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19f9bc4..a2cb650 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,60 @@ All notable changes to Buzznode are documented here, following
## [Unreleased]
+## [0.5.0] - 2026-07-30
+
+### Changed
+
+- Update the Buzz headless tools to `0.5.2` from upstream commit `3e48f1b`.
+- Re-cut the desktop-base release. 0.4.0 moved Buzznode onto
+ `launcher-image-base-desktop` but was never built and run end to end; this is
+ the first version verified by booting the image and confirming the setup
+ wizard opens over the Buzz wallpaper.
+
+## [0.4.0] - 2026-07-29
+
+### Changed
+
+- Build on the published Launcher desktop base
+ (`ghcr.io/pdparchitect/launcher-image-base-desktop`) instead of assembling
+ Ubuntu, Node, KasmVNC, Openbox, and the browser here. The Dockerfile keeps
+ only what is actually Buzznode: the headless Buzz tools and the coding-agent
+ runtimes.
+- **Breaking.** The desktop account is the base's `agent`, homed at
+ `/home/agent`. Volume targets move from `/home/buzznode/...` to
+ `/home/agent/...`; an existing node must remount its volumes at the new paths
+ or be re-enrolled from a fresh set. The Launcher catalog manifest is updated.
+- **Breaking.** `BUZZNODE_RESOLUTION` and `BUZZNODE_VNC_STATS` are replaced by
+ the base's `DESKTOP_RESOLUTION` and `DESKTOP_VNC_STATS`. Every other
+ `BUZZNODE_*` variable is unchanged.
+- Declare ports the way the other Launcher products do: the desktop's `6901` is
+ inherited from the base rather than redeclared, and this image adds no
+ `EXPOSE` or `HEALTHCHECK` of its own.
+- The agent harness log moves from `/var/log/buzznode` to the base's
+ `/var/log/launcher-desktop`.
+- Product files are installed through `overlay/`, which is copied over the
+ base's defaults, rather than through per-file `COPY` instructions. The
+ entrypoint is the base's: the setup wizard and the node greeting are both
+ reached through `desktop-welcome`, and the harness starts from
+ `/etc/desktop/session.d/10-buzznode-harness`.
+
+### Removed
+
+- The Openbox, Cortile, KasmVNC, GTK-theme, and browser-wrapper sources, along
+ with `init.sh`. All of them are the desktop base's now. What remains is the
+ Buzz wallpaper, favicon, landing page, accent colours, root menu, and the
+ panel entry that opens `buzznode status`.
+
+### Fixed
+
+- The wallpaper is a full-canvas SVG pattern rather than a 37px tile. The base
+ applies wallpapers with `feh --bg-fill`, which would have scaled the old tile
+ into a single enormous dot. It also carries no XML comment: the imlib2 loader
+ feh uses rejects any SVG containing one, and the desktop then comes up with no
+ wallpaper at all. `make check` guards both.
+
+- Refresh the Launcher catalogue screenshot from the rebuilt desktop.
+
## [0.3.0] - 2026-07-28
### Changed
diff --git a/Dockerfile b/Dockerfile
index e0cc8d9..c30bea5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,17 +2,24 @@
#
# Buzznode - a browser-accessible computer for one Buzz agent.
#
+# The Openbox/KasmVNC desktop, its browser, and the Ubuntu/Node foundation
+# under it all come from the Launcher desktop base. Nothing about the desktop
+# is configured here: branding and product programs are installed through
+# overlay/, which is copied over the base's defaults.
+#
# Buzznode connects to an existing relay and deliberately contains neither the
# Buzz Desktop client nor local relay/backing services.
+ARG DESKTOP_IMAGE=ghcr.io/pdparchitect/launcher-image-base-desktop:0.1.0
+
# Upstream publishes a Linux package only for AMD64. Extract its headless tools
# there; on ARM64, build the same immutable tag and exact commit from source.
FROM rust:1.95-bookworm AS buzz-tools
ARG TARGETARCH
-ARG BUZZ_VERSION=0.5.0
-ARG BUZZ_DEB_SHA256=9674cf098eca88333e8d895ec9d0a5c56c796fbc358fe1087b645890b8e2faca
-ARG BUZZ_SOURCE_SHA=4a977c588a540be38bd8ddb268cd24437bac8165
+ARG BUZZ_VERSION=0.5.2
+ARG BUZZ_DEB_SHA256=3f022bc31ed579e045946e6acab8483639bcb94e62c1e70f67b97b22f8f879c5
+ARG BUZZ_SOURCE_SHA=3e48f1b2365d326ee1c9582448d86a99b44ecd5d
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl git pkg-config && \
@@ -54,53 +61,53 @@ RUN set -eux; \
fi; \
strip /out/*
-# ═══════════════════════════════════════════════════════════════════
-# Stage: core - shared runtime/tooling baseline for agent workloads.
-# ═══════════════════════════════════════════════════════════════════
-FROM ubuntu:24.04 AS core
+FROM ${DESKTOP_IMAGE}
+
+USER root
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ARG TARGETARCH
-ENV DEBIAN_FRONTEND=noninteractive
-
-RUN arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
- case "$arch" in \
- amd64|arm64) ;; \
- *) echo "Buzznode does not support linux/$arch" >&2; exit 1 ;; \
- esac
-# Core tools for the node and its coding-agent runtimes.
+# The Python data stack the agent runtimes are expected to have on hand. The
+# desktop base carries plain python3; these are the libraries on top of it.
RUN apt-get update && apt-get install -y --no-install-recommends \
- bash coreutils curl git openssh-client jq socat wget ca-certificates sudo \
- tar zip unzip file procps openssl gnupg \
- dnsutils iproute2 haveged \
- sqlite3 \
- python3 python3-pip python-is-python3 \
- python3-numpy python3-pandas python3-scipy python3-requests \
- ipython3 \
- vim ripgrep git-lfs \
- && rm -rf /var/lib/apt/lists/*
-
-# Node.js 24, matching the current Buzz development toolchain.
-RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
- apt-get install -y --no-install-recommends nodejs && \
- rm -rf /var/lib/apt/lists/* && \
- node --version && npm --version
+ python3-numpy python3-pandas python3-scipy python3-requests ipython3 \
+ && rm -rf /var/lib/apt/lists/* && \
+ ln -sf /usr/bin/ipython3 /usr/bin/ipython
-RUN corepack enable && corepack prepare pnpm@10.13.1 --activate && \
- pnpm --version
+# Docker and GitHub CLIs remain available for coding-agent workflows. Buzznode
+# does not require a host Docker socket.
+RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor \
+ -o /usr/share/keyrings/docker-archive-keyring.gpg && \
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu noble stable" \
+ > /etc/apt/sources.list.d/docker.list && \
+ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
+ -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
+ > /etc/apt/sources.list.d/github-cli.list && \
+ apt-get update && \
+ apt-get install -y --no-install-recommends docker-ce-cli gh && \
+ rm -rf /var/lib/apt/lists/*
-# Coding CLIs and the ACP adapters that make them discoverable by Buzz.
+# Coding CLIs and the ACP adapters that make them discoverable by Buzz. Node
+# and npm come from the runtime layer in the base chain.
+#
+# npm's cache follows HOME, which the desktop base points at the session user's
+# home. Without an explicit cache directory this root-run install leaves
+# /home/agent/.npm owned by root, and then kitty cannot start and the session
+# opens with no terminal at all.
ARG CODEX_VERSION=0.145.0
ARG CLAUDE_CODE_VERSION=2.1.220
ARG CODEX_ACP_VERSION=1.1.7
ARG CLAUDE_ACP_VERSION=0.62.0
+ENV npm_config_cache=/tmp/npm-cache
RUN npm install -g \
- "@openai/codex@${CODEX_VERSION}" \
- "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
- "@agentclientprotocol/codex-acp@${CODEX_ACP_VERSION}" \
- "@agentclientprotocol/claude-agent-acp@${CLAUDE_ACP_VERSION}" && \
+ "@openai/codex@${CODEX_VERSION}" \
+ "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
+ "@agentclientprotocol/codex-acp@${CODEX_ACP_VERSION}" \
+ "@agentclientprotocol/claude-agent-acp@${CLAUDE_ACP_VERSION}" && \
+ rm -rf /tmp/npm-cache && \
codex --version && \
claude --version && \
codex-acp --version && \
@@ -128,19 +135,11 @@ RUN arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
goose --version && \
goose acp --help >/dev/null
-# Mike Farah yq.
-ARG YQ_VERSION=4.44.6
-RUN arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
- curl -fsSL "https://github.com/mikefarah/yq/releases/download/v${YQ_VERSION}/yq_linux_${arch}" \
- -o /usr/local/bin/yq && \
- chmod +x /usr/local/bin/yq && \
- yq --version
-
-# Copy only the headless Buzz tools. The builder extracts the verified upstream
+# Only the headless Buzz tools. The builder extracts the verified upstream
# package on AMD64 and builds the same pinned source tag on ARM64.
-ARG BUZZ_VERSION=0.5.0
-ARG BUZZ_DEB_SHA256=9674cf098eca88333e8d895ec9d0a5c56c796fbc358fe1087b645890b8e2faca
-ARG BUZZ_SOURCE_SHA=4a977c588a540be38bd8ddb268cd24437bac8165
+ARG BUZZ_VERSION=0.5.2
+ARG BUZZ_SOURCE_SHA=3e48f1b2365d326ee1c9582448d86a99b44ecd5d
+ARG BUZZ_SOURCE_URL=https://github.com/block/buzz
COPY --from=buzz-tools /out/ /usr/local/bin/
RUN for binary in buzz buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr; do \
test -x "/usr/local/bin/$binary"; \
@@ -152,246 +151,48 @@ RUN for binary in buzz buzz-acp buzz-agent buzz-dev-mcp git-credential-nostr; do
test ! -e /usr/bin/buzz-desktop; \
test ! -e /usr/local/bin/buzz-desktop
-# Required directories.
-RUN mkdir -p /data /outputs /workspace /var/log/buzznode
-
-# Alias ipython to ipython3 and pip to pip3 for consistency.
-RUN ln -sf /usr/bin/ipython3 /usr/bin/ipython && \
- ln -sf /usr/bin/pip3 /usr/bin/pip
-
-# ═══════════════════════════════════════════════════════════════════
-# Stage: base - desktop UI substrate layered on top of core.
-# ═══════════════════════════════════════════════════════════════════
-FROM core AS base
-
-# KasmVNC supplies its own X server (Xvnc), so the `xorg` metapackage is not
-# installed: it would add xserver-xorg-core, input/video drivers, keyboard-
-# configuration, and udev/systemd for hardware this container never has.
-# `x11-xserver-utils` is skipped for the same reason - its only consumer would
-# be the xrdb call in KasmVNC's generated xstartup, and xstartup is replaced
-# below with `exec openbox-session`. Together they cost ~90 MiB.
-# xauth, xkb-data, and x11-xkb-utils are listed explicitly even though
-# kasmvncserver depends on them, so an autoremove can never take them out.
-# xfonts-base supplies the core font path Xvnc is started with.
-RUN apt-get update && apt-get install -y --no-install-recommends \
- xdg-utils ssl-cert \
- xauth xkb-data x11-xkb-utils xfonts-base \
- xterm dbus-x11 x11-utils \
- scrot \
- openbox obconf tint2 kitty ranger feh picom htop xdotool wmctrl \
- fonts-noto fonts-noto-color-emoji \
- libnss3 libatk1.0-0t64 libatk-bridge2.0-0t64 libcups2t64 libdrm2 \
- libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 \
- libpango-1.0-0 libasound2t64 libxshmfence1 \
- && rm -rf /var/lib/apt/lists/*
-
-# Cortile provides optional dynamic tiling on top of Openbox.
-ARG CORTILE_VERSION=2.5.2
-RUN set -eux; \
- arch="$(dpkg --print-architecture)"; \
- case "$arch" in \
- amd64) cortile_arch=amd64 ;; \
- arm64) cortile_arch=arm64 ;; \
- *) echo "Unsupported Cortile architecture: $arch" >&2; exit 1 ;; \
- esac; \
- tmp_dir="$(mktemp -d)"; \
- curl -fsSL \
- "https://github.com/leukipp/cortile/releases/download/v${CORTILE_VERSION}/cortile_${CORTILE_VERSION}_linux_${cortile_arch}.tar.gz" \
- | tar -xz -C "$tmp_dir"; \
- install -m 0755 "$tmp_dir/cortile" /usr/local/bin/cortile; \
- rm -rf "$tmp_dir"
-
-# KasmVNC exposes the desktop in a browser.
-ARG KASMVNC_VERSION=1.4.0
-RUN arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
- curl -fsSL \
- "https://github.com/kasmtech/KasmVNC/releases/download/v${KASMVNC_VERSION}/kasmvncserver_noble_${KASMVNC_VERSION}_${arch}.deb" \
- -o /tmp/kasmvnc.deb && \
- apt-get update && \
- apt-get install -y --no-install-recommends /tmp/kasmvnc.deb && \
- rm -f /tmp/kasmvnc.deb && \
- rm -rf /var/lib/apt/lists/*
-
-# Docker and GitHub CLIs remain available for coding-agent workflows. Buzznode
-# does not require a host Docker socket.
-RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor \
- -o /usr/share/keyrings/docker-archive-keyring.gpg && \
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu noble stable" \
- > /etc/apt/sources.list.d/docker.list && \
- curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
- -o /usr/share/keyrings/githubcli-archive-keyring.gpg && \
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
- > /etc/apt/sources.list.d/github-cli.list && \
- apt-get update && \
- apt-get install -y --no-install-recommends docker-ce-cli gh && \
- rm -rf /var/lib/apt/lists/*
-
-# Google does not publish Chrome for Linux ARM64. Keep Chrome on AMD64 and use
-# Debian's signed Chromium package on ARM64 behind the same Buzznode launcher.
-RUN set -eux; \
- arch="${TARGETARCH:-$(dpkg --print-architecture)}"; \
- if [ "$arch" = "amd64" ]; then \
- curl -fsSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb \
- -o /tmp/browser.deb; \
- apt-get update; \
- apt-get install -y --no-install-recommends /tmp/browser.deb; \
- rm -f /tmp/browser.deb; \
- else \
- mkdir -p /etc/apt/keyrings; \
- curl -fsSL https://ftp-master.debian.org/keys/archive-key-12.asc \
- -o /etc/apt/keyrings/debian-archive-key-12.asc; \
- printf '%s\n' \
- 'deb [arch=arm64 signed-by=/etc/apt/keyrings/debian-archive-key-12.asc] https://deb.debian.org/debian bookworm main' \
- > /etc/apt/sources.list.d/debian-bookworm.list; \
- printf '%s\n' \
- 'Package: *' \
- 'Pin: release n=bookworm' \
- 'Pin-Priority: 100' \
- > /etc/apt/preferences.d/debian-bookworm; \
- apt-get update; \
- apt-get install -y --no-install-recommends chromium; \
- rm -f \
- /etc/apt/keyrings/debian-archive-key-12.asc \
- /etc/apt/preferences.d/debian-bookworm \
- /etc/apt/sources.list.d/debian-bookworm.list; \
- fi; \
- rm -rf /var/lib/apt/lists/*
-
-# ═══════════════════════════════════════════════════════════════════
-# Stage: buzznode - one persistent desktop connected to an existing Buzz relay.
-# ═══════════════════════════════════════════════════════════════════
-FROM base AS buzznode
-
-RUN if id -u agent >/dev/null 2>&1; then \
- usermod -d /home/buzznode -m agent; \
- elif id -u ubuntu >/dev/null 2>&1; then \
- usermod -l agent -d /home/buzznode -m ubuntu && groupmod -n agent ubuntu; \
- else \
- groupadd --system agent && \
- useradd --system --create-home --home-dir /home/buzznode \
- --gid agent --shell /bin/bash agent; \
- fi && \
- mkdir -p \
- /home/buzznode/.vnc \
- /home/buzznode/.config/buzznode \
- /home/buzznode/.local/share/applications \
- /home/buzznode/.buzz \
- /home/buzznode/.codex \
- /home/buzznode/.claude \
- /workspace \
- /var/log/buzznode && \
- chown -R agent:agent \
- /home/buzznode \
- /workspace \
- /var/log/buzznode
-
-ENV HOME=/home/buzznode \
- BROWSER=chromium
-ENV GTK_THEME=Buzznode
-# Chrome and Chromium ask GTK for embedded symbolic window-control resources.
-# Overlay only those four resources so their custom frames use the exact
-# Openbox glyph masks.
-ENV G_RESOURCE_OVERLAYS=/org/gtk/libgtk=/usr/share/buzznode/gtk-overlay
-
-# Browser popup menus come from Linux's native color pipeline rather than
-# extension-theme colors. A GTK system theme therefore styles the menus,
-# dialogs, toolbar, tabs, and omnibox as one coherent near-black surface.
-COPY gtk/Buzznode /usr/share/themes/Buzznode
-COPY gtk/generate-resource-overlay.py /tmp/generate-gtk-resource-overlay.py
-COPY openbox/theme /tmp/openbox-theme
-# Generate real symbolic PNGs directly from the Openbox XBM source assets.
-RUN python3 /tmp/generate-gtk-resource-overlay.py \
- /tmp/openbox-theme /usr/share/buzznode/gtk-overlay && \
- rm -rf /tmp/generate-gtk-resource-overlay.py /tmp/openbox-theme
-
-RUN echo "agent ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/agent && \
- chmod 0440 /etc/sudoers.d/agent && \
- touch /home/buzznode/.sudo_as_admin_successful /home/buzznode/.hushlogin && \
- chown agent:agent \
- /home/buzznode/.sudo_as_admin_successful \
- /home/buzznode/.hushlogin
-
-# KasmVNC UI customisation.
-COPY kasm/custom.css /usr/share/kasmvnc/www/assets/custom.css
-COPY kasm/favicon.svg /usr/share/kasmvnc/www/assets/favicon.svg
-COPY kasm/patch.sh /tmp/kasm-patch.sh
-RUN chmod +x /tmp/kasm-patch.sh && /tmp/kasm-patch.sh && rm /tmp/kasm-patch.sh
-
-# Match the Buzz website's chartreuse background and subtle dot grid.
-COPY wallpaper/buzz-grid.svg /usr/share/backgrounds/buzz-grid.svg
-
-# Browser preferences. `system_theme: 1` selects GTK on Linux; unlike an
-# extension theme, it also reaches native menus and other popup surfaces.
-RUN for config_dir in google-chrome chromium; do \
- mkdir -p "/home/buzznode/.config/$config_dir/Default"; \
- printf '{\n "browser": {\n "has_seen_welcome_page": true,\n "check_default_browser": false\n },\n "bookmark_bar": { "show_on_all_tabs": false },\n "distribution": {\n "skip_first_run_ui": true,\n "show_welcome_page": false,\n "import_bookmarks": false,\n "make_chrome_default_for_user": false,\n "suppress_first_run_default_browser_prompt": true\n },\n "extensions": {\n "theme": {\n "system_theme": 1\n }\n }\n}' \
- > "/home/buzznode/.config/$config_dir/Default/Preferences"; \
- touch "/home/buzznode/.config/$config_dir/First Run"; \
- done && \
- chown -R agent:agent \
- /home/buzznode/.config/google-chrome \
- /home/buzznode/.config/chromium
-
-# Suppress the default-browser prompt via managed policy. Do not set
-# BrowserThemeColor here: that policy overrides the GTK system theme.
+# These paths are volumes, so a rebuilt image resumes enrolled and signed in.
+# Creating them now means the entrypoint's ownership pass has something to
+# normalise on a brand-new volume.
RUN mkdir -p \
- /etc/opt/chrome/policies/managed \
- /etc/chromium/policies/managed && \
- printf '{\n "DefaultBrowserSettingEnabled": false,\n "BrowserSignin": 0,\n "HomepageLocation": "file:///opt/browser/index.html",\n "HomepageIsNewTabPage": false,\n "ShowHomeButton": true\n}\n' \
- > /etc/opt/chrome/policies/managed/buzznode-policy.json && \
- cp /etc/opt/chrome/policies/managed/buzznode-policy.json \
- /etc/chromium/policies/managed/buzznode-policy.json
-
-COPY openbox/rc.xml /etc/xdg/openbox/rc.xml
-COPY openbox/menu.xml /etc/xdg/openbox/menu.xml
-COPY openbox/autostart /etc/xdg/openbox/autostart
-COPY openbox/theme /usr/share/themes/Triste-Crimson/openbox-3
-COPY cortile/cortilectl /usr/local/bin/cortilectl
-COPY shell/welcome /usr/local/bin/welcome
-COPY shell/chromium /usr/local/bin/chromium
-COPY shell/buzznode /usr/local/bin/buzznode
-COPY shell/buzznode-panel-status /usr/local/bin/buzznode-panel-status
-COPY shell/agent-runtime-login /usr/local/bin/agent-runtime-login
-RUN mkdir -p /etc/bash.bashrc.d
-COPY shell/bashrc /etc/bash.bashrc.d/buzznode-prompt.sh
-COPY browser /opt/browser
-COPY tint2/tint2rc /etc/xdg/tint2/tint2rc
-RUN chmod +x \
- /etc/xdg/openbox/autostart \
- /usr/local/bin/cortilectl \
- /usr/local/bin/welcome \
- /usr/local/bin/chromium \
+ /home/agent/.config/buzznode \
+ /home/agent/.buzz \
+ /home/agent/.codex \
+ /home/agent/.claude && \
+ rm -rf /home/agent/.cache /home/agent/.npm && \
+ chown -R agent:agent /home/agent
+
+# Product branding and the programs that drive the node. Everything under
+# overlay/ is installed over the base's defaults, so a file here wins without
+# the base knowing this product exists.
+COPY overlay /
+RUN chmod 0755 \
+ /etc/desktop/session.d/10-buzznode-harness \
+ /etc/desktop/startup.d/05-agent-runtime-trust \
+ /usr/local/bin/agent-runtime-login \
/usr/local/bin/buzznode \
- /usr/local/bin/buzznode-panel-status \
- /usr/local/bin/agent-runtime-login && \
- echo '[ -d /etc/bash.bashrc.d ] && for f in /etc/bash.bashrc.d/*.sh; do . "$f"; done' \
- >> /etc/bash.bashrc
-
-RUN mkdir -p /usr/share/xsessions && \
- printf '[Desktop Entry]\nName=Openbox\nExec=openbox-session\nType=Application\n' \
- > /usr/share/xsessions/openbox.desktop
-
-USER agent
-
-RUN mkdir -p "$HOME/.config/cortile"
-COPY --chown=agent:agent cortile/cortile-config.toml /home/buzznode/.config/cortile/config.toml
-
-RUN printf '#!/bin/bash\nexec openbox-session\n' > "$HOME/.vnc/xstartup" && \
- chmod +x "$HOME/.vnc/xstartup" && \
- touch "$HOME/.vnc/.de-was-selected" && \
- printf 'network:\n ssl:\n require_ssl: false\n websocket_port: 6901\n' \
- > "$HOME/.vnc/kasmvnc.yaml"
-
-USER root
-
-COPY init.sh /init
-RUN chmod +x /init
-
-EXPOSE 6901
+ /usr/local/bin/buzznode-greeting \
+ /usr/local/bin/desktop-panel-status \
+ /usr/local/bin/desktop-welcome
+
+# Rebrand the KasmVNC client. The base already patched it, so this replaces the
+# brand rather than injecting the asset links a second time.
+RUN kasm-patch "Buzznode"
+
+# DESKTOP_PERSISTENT_PATHS is the base entrypoint's contract: these are created
+# and ownership-normalised before the session starts, and they are the paths
+# declared as volumes below.
+ENV DESKTOP_TITLE="Buzznode" \
+ DESKTOP_PERSISTENT_PATHS="/home/agent/.config /home/agent/.local/share /home/agent/.buzz /home/agent/.codex /home/agent/.claude"
+
+LABEL org.opencontainers.image.title="Buzznode" \
+ org.opencontainers.image.description="A browser-accessible computer for one Buzz agent, in a Launcher-managed desktop" \
+ org.opencontainers.image.source="https://github.com/pdparchitect/buzznode" \
+ dev.pdparchitect.launcher.upstream.source="${BUZZ_SOURCE_URL}" \
+ dev.pdparchitect.launcher.upstream.version="${BUZZ_VERSION}" \
+ dev.pdparchitect.launcher.upstream.revision="${BUZZ_SOURCE_SHA}"
+
+# The desktop's 6901 comes from the base, like every other Launcher product,
+# and this image publishes nothing else.
WORKDIR /workspace
-VOLUME ["/workspace", "/home/buzznode/.config", "/home/buzznode/.local/share", "/home/buzznode/.buzz", "/home/buzznode/.codex", "/home/buzznode/.claude"]
-
-HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=6 \
- CMD curl -fsS http://127.0.0.1:6901/ >/dev/null || exit 1
-
-ENTRYPOINT ["/init"]
+VOLUME ["/workspace", "/home/agent/.config", "/home/agent/.local/share", "/home/agent/.buzz", "/home/agent/.codex", "/home/agent/.claude"]
diff --git a/Makefile b/Makefile
index fbc1036..f95cdc2 100644
--- a/Makefile
+++ b/Makefile
@@ -9,9 +9,10 @@ NATIVE_ARCH := $(shell uname -m | sed \
-e 's/^aarch64$$/arm64/')
PLATFORM ?= linux/$(NATIVE_ARCH)
TARGETARCH ?= $(word 2,$(subst /, ,$(PLATFORM)))
-BUZZ_VERSION ?= 0.5.0
-BUZZ_DEB_SHA256 ?= 9674cf098eca88333e8d895ec9d0a5c56c796fbc358fe1087b645890b8e2faca
-BUZZ_SOURCE_SHA ?= 4a977c588a540be38bd8ddb268cd24437bac8165
+BUZZ_VERSION ?= 0.5.2
+BUZZ_DEB_SHA256 ?= 3f022bc31ed579e045946e6acab8483639bcb94e62c1e70f67b97b22f8f879c5
+BUZZ_SOURCE_SHA ?= 3e48f1b2365d326ee1c9582448d86a99b44ecd5d
+DESKTOP_IMAGE ?= ghcr.io/pdparchitect/launcher-image-base-desktop:0.1.0
CODEX_VERSION ?= 0.145.0
CLAUDE_CODE_VERSION ?= 2.1.220
CODEX_ACP_VERSION ?= 1.1.7
@@ -59,13 +60,20 @@ help:
@echo " BUZZ_NETWORK=buzz-local VNC_STATS=true"
check:
- bash -n init.sh openbox/autostart shell/agent-runtime-login shell/buzznode \
- shell/buzznode-panel-status shell/chromium shell/welcome \
+ bash -n \
+ overlay/etc/desktop/session.d/10-buzznode-harness \
+ overlay/etc/desktop/startup.d/05-agent-runtime-trust \
+ overlay/usr/local/bin/agent-runtime-login \
+ overlay/usr/local/bin/buzznode \
+ overlay/usr/local/bin/buzznode-greeting \
+ overlay/usr/local/bin/desktop-panel-status \
+ overlay/usr/local/bin/desktop-welcome \
tests/test-agent-runtime-login.sh tests/test-buzznode.sh \
tests/test-desktop-theme.sh tests/smoke-container.sh
bash tests/test-agent-runtime-login.sh
bash tests/test-buzznode.sh
bash tests/test-desktop-theme.sh
+ @grep -q "^ARG DESKTOP_IMAGE=$(DESKTOP_IMAGE)$$" Dockerfile
@grep -q "^ARG BUZZ_VERSION=$(BUZZ_VERSION)$$" Dockerfile
@grep -q "^ARG BUZZ_DEB_SHA256=$(BUZZ_DEB_SHA256)$$" Dockerfile
@grep -q "^ARG BUZZ_SOURCE_SHA=$(BUZZ_SOURCE_SHA)$$" Dockerfile
@@ -76,20 +84,18 @@ check:
@grep -q "^ARG GOOSE_VERSION=$(GOOSE_VERSION)$$" Dockerfile
@grep -q "^ARG GOOSE_AMD64_SHA256=$(GOOSE_AMD64_SHA256)$$" Dockerfile
@grep -q "^ARG GOOSE_ARM64_SHA256=$(GOOSE_ARM64_SHA256)$$" Dockerfile
- @grep -q 'window.handle.width: 0' openbox/theme/themerc
- @grep -q 'window.client.padding.width: 6' openbox/theme/themerc
- @grep -q 'window.client.padding.height: 6' openbox/theme/themerc
- @grep -q 'cd /workspace' shell/bashrc
- @grep -q 'BUZZNODE_CODEX_SANDBOX_MODE' init.sh
+ @grep -q 'RUN kasm-patch "Buzznode"' Dockerfile
+ @grep -q 'cd /workspace' overlay/etc/bash.bashrc.d/buzznode-prompt.sh
+ @grep -q 'BUZZNODE_CODEX_SANDBOX_MODE' overlay/etc/desktop/startup.d/05-agent-runtime-trust
@grep -q 'BUZZNODE_CODEX_SANDBOX_MODE' README.md
- @grep -q '\[ -n "$${PS1:-}" \]' shell/bashrc
- @grep -q '' openbox/rc.xml
+ @grep -q '\[ -n "$${PS1:-}" \]' overlay/etc/bash.bashrc.d/buzznode-prompt.sh
@echo "Buzznode metadata, setup CLI, and shell syntax are valid."
build:
$(DOCKER) build \
--platform "$(PLATFORM)" \
--build-arg "TARGETARCH=$(TARGETARCH)" \
+ --build-arg "DESKTOP_IMAGE=$(DESKTOP_IMAGE)" \
--build-arg "BUZZ_VERSION=$(BUZZ_VERSION)" \
--build-arg "BUZZ_DEB_SHA256=$(BUZZ_DEB_SHA256)" \
--build-arg "BUZZ_SOURCE_SHA=$(BUZZ_SOURCE_SHA)" \
@@ -128,14 +134,14 @@ run: network
$(NETWORK_ARG) \
--publish "$(BIND_ADDRESS):$(PORT):6901" \
$(RELAY_ENV) \
- --env "BUZZNODE_RESOLUTION=$(RESOLUTION)" \
- --env "BUZZNODE_VNC_STATS=$(VNC_STATS)" \
+ --env "DESKTOP_RESOLUTION=$(RESOLUTION)" \
+ --env "DESKTOP_VNC_STATS=$(VNC_STATS)" \
--volume "$(VOLUME_PREFIX)-workspace:/workspace" \
- --volume "$(VOLUME_PREFIX)-config:/home/buzznode/.config" \
- --volume "$(VOLUME_PREFIX)-data:/home/buzznode/.local/share" \
- --volume "$(VOLUME_PREFIX)-nest:/home/buzznode/.buzz" \
- --volume "$(VOLUME_PREFIX)-codex:/home/buzznode/.codex" \
- --volume "$(VOLUME_PREFIX)-claude:/home/buzznode/.claude" \
+ --volume "$(VOLUME_PREFIX)-config:/home/agent/.config" \
+ --volume "$(VOLUME_PREFIX)-data:/home/agent/.local/share" \
+ --volume "$(VOLUME_PREFIX)-nest:/home/agent/.buzz" \
+ --volume "$(VOLUME_PREFIX)-codex:/home/agent/.codex" \
+ --volume "$(VOLUME_PREFIX)-claude:/home/agent/.claude" \
"$(IMAGE)"; \
fi
@$(MAKE) --no-print-directory url
@@ -185,7 +191,7 @@ logs:
vnc-log:
$(DOCKER) exec "$(CONTAINER)" \
- bash -c 'tail --lines=200 --follow /home/buzznode/.vnc/*:1.log'
+ bash -c 'tail --lines=200 --follow /home/agent/.vnc/*:1.log'
status:
@$(DOCKER) ps --all \
diff --git a/README.md b/README.md
index cb6a3c5..8d19eb3 100644
--- a/README.md
+++ b/README.md
@@ -141,11 +141,11 @@ docker run --detach \
--shm-size 1g \
--publish 127.0.0.1:6904:6901 \
--volume buzznode-workspace:/workspace \
- --volume buzznode-config:/home/buzznode/.config \
- --volume buzznode-data:/home/buzznode/.local/share \
- --volume buzznode-nest:/home/buzznode/.buzz \
- --volume buzznode-codex:/home/buzznode/.codex \
- --volume buzznode-claude:/home/buzznode/.claude \
+ --volume buzznode-config:/home/agent/.config \
+ --volume buzznode-data:/home/agent/.local/share \
+ --volume buzznode-nest:/home/agent/.buzz \
+ --volume buzznode-codex:/home/agent/.codex \
+ --volume buzznode-claude:/home/agent/.claude \
ghcr.io/pdparchitect/buzznode:latest
```
@@ -317,6 +317,37 @@ desktop browser, or API-key authentication. Claude Code supports Claude
subscription, Anthropic Console, long-lived setup-token, or organization SSO
flows.
+## Relationship to the Launcher desktop base
+
+Buzznode is a product image on top of
+`ghcr.io/pdparchitect/launcher-image-base-desktop`, the same substrate the
+other Launcher desktops use. The base supplies Ubuntu, Node, KasmVNC, Openbox,
+Cortile, tint2, kitty, the browser, the GTK theme, the `agent` account, and the
+entrypoint. This repository supplies only the node.
+
+That split is what the source layout reflects:
+
+| Path | What it is |
+| -------------------------------- | ----------------------------------------------- |
+| `Dockerfile` | The headless Buzz tools and the agent runtimes |
+| `overlay/` | Files copied over the base's defaults |
+| `overlay/usr/local/bin/buzznode` | The node CLI |
+| `overlay/etc/desktop/session.d/` | Programs the session runs once it has a display |
+
+Two consequences are worth knowing:
+
+- The desktop user is `agent`, homed at `/home/agent`, and the agent harness
+ log lives in `/var/log/launcher-desktop`.
+- Desktop-level settings use the base's names — `DESKTOP_RESOLUTION`,
+ `DESKTOP_VNC_STATS`, `DESKTOP_TITLE`. Node-level settings keep their
+ `BUZZNODE_*` and `BUZZ_*` names.
+
+To build against a different base, override `DESKTOP_IMAGE`:
+
+```bash
+make up DESKTOP_IMAGE=ghcr.io/pdparchitect/launcher-image-base-desktop:0.2.0
+```
+
## Build locally
From this directory:
@@ -343,7 +374,7 @@ The Makefile selects `linux/amd64` or `linux/arm64` from the host by default.
| Component | Version |
| ------------------- | --------- |
-| Buzz headless tools | `0.5.0` |
+| Buzz headless tools | `0.5.2` |
| Codex | `0.145.0` |
| Claude Code | `2.1.220` |
| Goose | `1.44.0` |
diff --git a/VERSION b/VERSION
index 0d91a54..8f0916f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.3.0
+0.5.0
diff --git a/cortile/cortile-config.toml b/cortile/cortile-config.toml
deleted file mode 100644
index 5bf9dee..0000000
--- a/cortile/cortile-config.toml
+++ /dev/null
@@ -1,139 +0,0 @@
-################################################################################
-# Cortile config for Pantalk desktop example #
-# https://github.com/leukipp/cortile/blob/main/config.toml #
-################################################################################
-
-#################################### Tiling ####################################
-
-# Start in floating mode; user enables tiling via right-click menu or keyboard.
-tiling_enabled = false
-
-# Default tiling layout when enabled.
-tiling_layout = "vertical-right"
-
-# Layouts available when cycling with next/previous.
-# @note "maximized" is intentionally excluded - it causes every window
-# to appear maximized and prevents genuine tiling after manual maximize
-tiling_cycle = [
- "vertical-left",
- "vertical-right",
- "horizontal-top",
- "horizontal-bottom",
-]
-
-# Show overlay briefly when layout changes (ms, 0 = disabled).
-tiling_gui = 1200
-
-# Systray menu entries (action name from [keys], display label).
-tiling_icon = [
- ["toggle", "Toggle Tiling"],
- ["", ""],
- ["cycle_next", "Next Layout"],
- ["cycle_previous", "Previous Layout"],
- ["", ""],
- ["reset", "Reset"],
- ["", ""],
- ["exit", "Exit"],
-]
-
-#################################### Window ####################################
-
-# Regex to ignore windows from tiling (WM_CLASS, WM_NAME).
-window_ignore = [
- ["tint2.*", ""],
- ["nm.*", ""],
- ["gcr.*", ""],
- ["polkit.*", ""],
- ["wrapper.*", ""],
-]
-
-window_masters_max = 1
-window_slaves_max = 3
-window_gap_size = 8
-window_focus_delay = 0
-window_decoration = true
-
-################################## Proportion ##################################
-
-proportion_step = 0.05
-proportion_min = 0.2
-
-##################################### Edge #####################################
-
-# Openbox already reserves 8px on all screen edges;
-# Cortile sees the reduced workspace, so no extra edge margin needed.
-edge_margin = [0, 0, 0, 0]
-edge_margin_primary = [0, 0, 0, 0]
-edge_corner_size = 10
-edge_center_size = 100
-
-################################################################################
-[colors]
-################################################################################
-
-gui_text = [255, 255, 255, 255]
-gui_background = [30, 30, 40, 230]
-gui_client_slave = [58, 58, 78, 255]
-gui_client_master = [98, 98, 128, 255]
-icon_background = [0, 0, 0, 0]
-icon_foreground = [255, 255, 255, 255]
-
-################################################################################
-[keys]
-################################################################################
-
-enable = "Control-Shift-Home"
-disable = "Control-Shift-End"
-toggle = "Control-Shift-T"
-decoration = "Control-Shift-D"
-restore = "Control-Shift-R"
-reset = "Control-Shift-BackSpace"
-cycle_next = "Control-Shift-Next"
-cycle_previous = "Control-Shift-Prior"
-layout_vertical_left = "Control-Shift-Left"
-layout_vertical_right = "Control-Shift-Right"
-layout_horizontal_top = "Control-Shift-Up"
-layout_horizontal_bottom = "Control-Shift-Down"
-# @note maximized layout is intentionally unbound to prevent accidental activation
-layout_maximized = ""
-layout_fullscreen = "Control-Shift-Return"
-slave_increase = "Control-Shift-plus"
-slave_decrease = "Control-Shift-minus"
-master_increase = "Control-Shift-KP_Add"
-master_decrease = "Control-Shift-KP_Subtract"
-window_next = "Control-Shift-KP_2"
-window_previous = "Control-Shift-KP_8"
-screen_next = "Control-Shift-KP_9"
-screen_previous = "Control-Shift-KP_7"
-master_make = "Control-Shift-KP_5"
-master_make_next = "Control-Shift-KP_6"
-master_make_previous = "Control-Shift-KP_4"
-proportion_increase = "Control-Shift-KP_3"
-proportion_decrease = "Control-Shift-KP_1"
-mod_screens = "Mod1"
-mod_workspaces = "Mod4"
-
-################################################################################
-[corners]
-################################################################################
-
-top_left = ""
-top_center = ""
-top_right = ""
-center_right = ""
-bottom_right = ""
-bottom_center = ""
-bottom_left = ""
-center_left = ""
-
-################################################################################
-[systray]
-################################################################################
-
-click_left = ""
-click_middle = "toggle"
-click_right = ""
-scroll_up = "cycle_previous"
-scroll_down = "cycle_next"
-scroll_left = "proportion_decrease"
-scroll_right = "proportion_increase"
diff --git a/cortile/cortilectl b/cortile/cortilectl
deleted file mode 100644
index 338c6bf..0000000
--- a/cortile/cortilectl
+++ /dev/null
@@ -1,122 +0,0 @@
-#!/bin/bash
-# cortilectl - thin wrapper to manage cortile from Openbox menus.
-# Usage: cortilectl {start|stop|on|off|toggle|next-layout|prev-layout}
-
-set -euo pipefail
-
-# Ensure DISPLAY is set (Openbox menu inherits it, but autostart may not).
-export DISPLAY="${DISPLAY:-:1}"
-
-log_file="/tmp/cortile.log"
-
-is_running() {
- pgrep -u "$(id -u)" -x cortile >/dev/null 2>&1
-}
-
-start_cortile() {
- if ! is_running; then
- nohup cortile -v >>"$log_file" 2>&1 &
- disown
- # Wait for cortile to connect to X and start listening.
- local i=0
- while [ $i -lt 20 ]; do
- if is_running; then
- break
- fi
- sleep 0.25
- i=$((i + 1))
- done
-
- # Give Cortile a brief moment to finish grabbing keybindings after process start.
- if is_running; then
- sleep 0.75
- fi
- fi
-}
-
-unmaximize_all() {
- # @note strip _NET_WM_STATE maximized hints from every window so Cortile
- # and Openbox both see them as normal-sized; without this, maximized
- # windows resist any tiling resize
- local wids
- if command -v wmctrl >/dev/null 2>&1; then
- wmctrl -l | awk '{print $1}' | while read -r wid; do
- wmctrl -i -r "$wid" -b remove,maximized_vert,maximized_horz 2>/dev/null || true
- done
- else
- # Fallback using xprop (always available in X11 installs).
- wids=$(xprop -root _NET_CLIENT_LIST 2>/dev/null \
- | grep -o '0x[0-9a-f]\+' || true)
- for wid in $wids; do
- # Read current state, strip maximized atoms, rewrite.
- local cur
- cur=$(xprop -id "$wid" _NET_WM_STATE 2>/dev/null \
- | sed 's/.*= //' | tr ',' '\n' \
- | grep -iv 'maximized' | tr '\n' ',' \
- | sed 's/,$//' || true)
- if [ -n "$cur" ]; then
- xprop -id "$wid" -f _NET_WM_STATE 32a -set _NET_WM_STATE "$cur" 2>/dev/null || true
- else
- xprop -id "$wid" -remove _NET_WM_STATE 2>/dev/null || true
- fi
- done
- fi
-}
-
-reset_cortile_layout() {
- # @note send Cortile's reset key to clear any stuck internal layout state
- # (e.g. "maximized" mode), then force the default vertical-right layout
- sleep 0.1
- xdotool key --clearmodifiers "ctrl+shift+BackSpace" 2>>"$log_file" || true
- sleep 0.1
- xdotool key --clearmodifiers "ctrl+shift+Right" 2>>"$log_file" || true
-}
-
-send_key() {
- # @note openbox holds a keyboard grab while closing the menu that triggered
- # this command; a short pause lets it release the grab so the synthetic
- # keypress reaches cortile's global key listener on the first attempt
- sleep 0.3
- # Use xdotool to simulate the Cortile keybinding.
- xdotool key --clearmodifiers "$1" 2>>"$log_file" || true
-}
-
-mode="${1:-toggle}"
-
-case "$mode" in
- start)
- start_cortile
- ;;
- stop)
- pkill -u "$(id -u)" -x cortile 2>/dev/null || true
- ;;
- on|enable)
- start_cortile
- unmaximize_all
- send_key "ctrl+shift+Home"
- reset_cortile_layout
- ;;
- off|disable)
- if is_running; then
- send_key "ctrl+shift+End"
- fi
- ;;
- toggle)
- start_cortile
- unmaximize_all
- send_key "ctrl+shift+t"
- reset_cortile_layout
- ;;
- next-layout)
- start_cortile
- send_key "ctrl+shift+Next"
- ;;
- prev-layout)
- start_cortile
- send_key "ctrl+shift+Prior"
- ;;
- *)
- echo "Usage: cortilectl {start|stop|on|off|toggle|next-layout|prev-layout}" >&2
- exit 2
- ;;
-esac
diff --git a/gtk/Buzznode/gtk-3.0/settings.ini b/gtk/Buzznode/gtk-3.0/settings.ini
deleted file mode 100644
index 699fa7a..0000000
--- a/gtk/Buzznode/gtk-3.0/settings.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[Settings]
-gtk-font-name = Noto Sans 9
diff --git a/gtk/generate-resource-overlay.py b/gtk/generate-resource-overlay.py
deleted file mode 100644
index 9c2429e..0000000
--- a/gtk/generate-resource-overlay.py
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/usr/bin/env python3
-"""Generate GTK symbolic PNG resources from the Openbox XBM controls."""
-
-from __future__ import annotations
-
-import argparse
-import re
-import struct
-import zlib
-from pathlib import Path
-
-
-CONTROL_SOURCES = {
- "window-minimize-symbolic.symbolic.png": "iconify.xbm",
- "window-maximize-symbolic.symbolic.png": "max.xbm",
- "window-restore-symbolic.symbolic.png": "max_toggled.xbm",
- "window-close-symbolic.symbolic.png": "close.xbm",
-}
-PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
-
-
-def parse_xbm(path: Path) -> tuple[int, int, bytes]:
- source = path.read_text(encoding="utf-8")
- width_match = re.search(r"#define\s+\w+_width\s+(\d+)", source)
- height_match = re.search(r"#define\s+\w+_height\s+(\d+)", source)
- bits_match = re.search(r"\{([^}]*)\}", source, re.DOTALL)
- if not width_match or not height_match or not bits_match:
- raise ValueError(f"Invalid XBM control: {path}")
-
- width = int(width_match.group(1))
- height = int(height_match.group(1))
- data = bytes(
- int(value, 16)
- for value in re.findall(r"0x([0-9a-fA-F]+)", bits_match.group(1))
- )
- expected_size = ((width + 7) // 8) * height
- if len(data) != expected_size:
- raise ValueError(
- f"{path} contains {len(data)} bytes; expected {expected_size}"
- )
- return width, height, data
-
-
-def png_chunk(kind: bytes, data: bytes) -> bytes:
- return (
- struct.pack(">I", len(data))
- + kind
- + data
- + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)
- )
-
-
-def render_symbolic_png(width: int, height: int, bits: bytes) -> bytes:
- canvas_size = 16
- offset_x = (canvas_size - width) // 2
- offset_y = (canvas_size - height) // 2
- bytes_per_row = (width + 7) // 8
- rows = []
-
- for canvas_y in range(canvas_size):
- row = bytearray([0])
- for canvas_x in range(canvas_size):
- source_x = canvas_x - offset_x
- source_y = canvas_y - offset_y
- visible = False
- if 0 <= source_x < width and 0 <= source_y < height:
- source_byte = bits[source_y * bytes_per_row + source_x // 8]
- visible = bool(source_byte & (1 << (source_x % 8)))
- row.extend((0, 0, 0, 255 if visible else 0))
- rows.append(bytes(row))
-
- header = struct.pack(">IIBBBBB", canvas_size, canvas_size, 8, 6, 0, 0, 0)
- return (
- PNG_SIGNATURE
- + png_chunk(b"IHDR", header)
- + png_chunk(b"IDAT", zlib.compress(b"".join(rows), level=9))
- + png_chunk(b"IEND", b"")
- )
-
-
-def main() -> None:
- parser = argparse.ArgumentParser()
- parser.add_argument("openbox_theme", type=Path)
- parser.add_argument("overlay_root", type=Path)
- args = parser.parse_args()
-
- output_dir = args.overlay_root / "icons/16x16/status"
- output_dir.mkdir(parents=True, exist_ok=True)
- for output_name, source_name in CONTROL_SOURCES.items():
- width, height, bits = parse_xbm(args.openbox_theme / source_name)
- (output_dir / output_name).write_bytes(
- render_symbolic_png(width, height, bits)
- )
-
-
-if __name__ == "__main__":
- main()
diff --git a/init.sh b/init.sh
deleted file mode 100755
index c8562d2..0000000
--- a/init.sh
+++ /dev/null
@@ -1,276 +0,0 @@
-#!/bin/bash
-# Buzznode container entrypoint.
-# Starts one persistent browser-accessible desktop that connects to an external
-# Buzz relay. Buzznode deliberately runs no relay or backing data services.
-
-set -euo pipefail
-
-export HOME=/home/buzznode
-export XDG_CONFIG_HOME="$HOME/.config"
-export XDG_DATA_HOME="$HOME/.local/share"
-
-agent_uid="$(id -u agent)"
-export XDG_RUNTIME_DIR="/run/user/${agent_uid}"
-
-resolution="${BUZZNODE_RESOLUTION:-1920x1080}"
-if [[ ! "$resolution" =~ ^[0-9]{3,5}x[0-9]{3,5}$ ]]; then
- echo "[buzznode] invalid BUZZNODE_RESOLUTION: $resolution" >&2
- exit 1
-fi
-
-width="${resolution%x*}"
-height="${resolution#*x}"
-
-mkdir -p \
- "$HOME/.vnc" \
- "$HOME/.config/buzznode" \
- "$HOME/.local/share/applications" \
- "$HOME/.buzz" \
- "$HOME/.codex" \
- "$HOME/.claude" \
- "$XDG_RUNTIME_DIR" \
- /workspace \
- /var/log/buzznode \
- /tmp/.X11-unix
-
-# Keep the durable workspace and the agent's home available as Ranger
-# bookmarks without replacing any bookmarks the user has already assigned.
-ranger_data_dir="$XDG_DATA_HOME/ranger"
-ranger_bookmarks="$ranger_data_dir/bookmarks"
-mkdir -p "$ranger_data_dir"
-touch "$ranger_bookmarks"
-if ! grep -q '^W:' "$ranger_bookmarks"; then
- printf 'W:/workspace\n' >> "$ranger_bookmarks"
-fi
-if ! grep -q '^H:' "$ranger_bookmarks"; then
- printf 'H:%s\n' "$HOME" >> "$ranger_bookmarks"
-fi
-
-# Ownership only needs normalizing once per volume lifetime. Recursing the home
-# directory and the workspace on every boot walks the browser profile, the agent
-# nest, and every checked-out repository, which becomes minutes of startup
-# latency once they hold real data. Anything created later is created by the
-# agent user already.
-persistent_paths=(
- "$HOME"
- /workspace
-)
-ownership_stamp="$HOME/.config/buzznode/.ownership-normalized"
-
-chown agent:agent \
- "${persistent_paths[@]}" \
- "$XDG_RUNTIME_DIR" \
- /var/log/buzznode
-
-if [ ! -e "$ownership_stamp" ]; then
- chown -R agent:agent "${persistent_paths[@]}" /var/log/buzznode
- touch "$ownership_stamp"
- chown agent:agent "$ownership_stamp"
- echo "[buzznode] normalized ownership of the persistent volumes"
-fi
-
-chmod 700 "$XDG_RUNTIME_DIR" "$HOME/.config/buzznode"
-chmod 1777 /tmp/.X11-unix
-
-if getent group ssl-cert >/dev/null 2>&1; then
- usermod -a -G ssl-cert agent
-fi
-
-# Codex and Claude Code each prompt once per directory before working in it
-# ("Do you trust the contents of this directory?"). `buzznode launch` runs the
-# harness unattended - it does `cd /workspace` and execs buzz-acp with
-# codex-acp, backgrounded into a log file - so there is nobody present to
-# answer, and the agent would sit on the prompt with the reason buried in the
-# log. codex-acp consults trust_level, so record the decision at boot instead.
-#
-# This grants no access the harness is not already started with. Set
-# BUZZNODE_TRUST_WORKSPACE=false to leave both prompts in place.
-harness_workdir="${BUZZNODE_HARNESS_WORKDIR:-/workspace}"
-codex_config="$HOME/.codex/config.toml"
-
-# Codex sandboxes the commands it runs with bubblewrap, and warns when it has to
-# fall back to its bundled copy. Neither copy can work here: the container blocks
-# unprivileged user namespaces, so bwrap cannot create one, and installing the
-# distro package only adds a second binary that fails the same way. Declare the
-# mode that matches reality rather than leaving a config that implies an
-# isolation boundary which is not there - the boundary is the container itself.
-#
-# Set BUZZNODE_CODEX_SANDBOX_MODE to read-only or workspace-write to choose a
-# different mode, or to an empty value to leave the setting out entirely.
-codex_sandbox_mode="${BUZZNODE_CODEX_SANDBOX_MODE-danger-full-access}"
-if [ -n "$codex_sandbox_mode" ] &&
- ! grep -Eq '^sandbox_mode *=' "$codex_config" 2>/dev/null; then
- codex_sandbox_tmp="$(mktemp)"
- # Prepended, not appended: sandbox_mode is a top-level key, and TOML assigns
- # any key following a [table] header to that table. The trust block below
- # writes [projects."..."] tables, so appending would quietly turn this into a
- # per-project setting instead of a global one.
- {
- printf 'sandbox_mode = "%s"\n\n' "$codex_sandbox_mode"
- [ -s "$codex_config" ] && cat "$codex_config"
- } > "$codex_sandbox_tmp"
- install -m 0600 -o agent -g agent "$codex_sandbox_tmp" "$codex_config"
- rm -f "$codex_sandbox_tmp"
- echo "[buzznode] set Codex sandbox_mode=$codex_sandbox_mode" \
- "(no usable bubblewrap in a container)"
-fi
-
-if [ "${BUZZNODE_TRUST_WORKSPACE:-true}" = "true" ]; then
- if ! grep -Fq "[projects.\"$harness_workdir\"]" "$codex_config" 2>/dev/null; then
- printf '\n[projects."%s"]\ntrust_level = "trusted"\n' \
- "$harness_workdir" >> "$codex_config"
- chown agent:agent "$codex_config"
- echo "[buzznode] recorded $harness_workdir as trusted for Codex"
- fi
-
- claude_config="$HOME/.claude.json"
- if ! jq -e --arg dir "$harness_workdir" \
- '.projects[$dir].hasTrustDialogAccepted == true' \
- "$claude_config" >/dev/null 2>&1; then
- [ -s "$claude_config" ] || echo '{}' > "$claude_config"
- claude_trust_tmp="$(mktemp)"
- # Leave the file untouched if it is not valid JSON rather than
- # replacing a config the operator may have hand-written.
- if jq --arg dir "$harness_workdir" \
- '.projects[$dir].hasTrustDialogAccepted = true' \
- "$claude_config" > "$claude_trust_tmp" 2>/dev/null; then
- install -m 0600 -o agent -g agent \
- "$claude_trust_tmp" "$claude_config"
- echo "[buzznode] recorded $harness_workdir as trusted for Claude Code"
- fi
- rm -f "$claude_trust_tmp"
- fi
-fi
-
-# Use a GPU only when the host exposes a render node *and* the desktop user can
-# open it; otherwise keep software rendering. A passed-through node is normally
-# root:render 0660 and the host's render group does not exist in this image, so
-# presence alone does not mean usable. Announcing hw3d in that case leaves Xvnc
-# and Chrome retrying against a device they cannot open.
-gpu_node=""
-gpu_node_blocked=""
-for node in /dev/dri/renderD*; do
- [ -e "$node" ] || continue
- printf -v node_q '%q' "$node"
- if su -s /bin/bash -c "test -r $node_q && test -w $node_q" agent; then
- gpu_node="$node"
- break
- fi
- gpu_node_blocked="$node"
-done
-
-if [ -n "$gpu_node" ]; then
- gpu_config=" gpu:
- hw3d: true
- drinode: $gpu_node"
- echo "[buzznode] GPU acceleration enabled via $gpu_node"
-else
- gpu_config=" gpu:
- hw3d: false"
- if [ -n "$gpu_node_blocked" ]; then
- echo "[buzznode] $gpu_node_blocked is not readable by the agent user;" \
- "using software rendering"
- else
- echo "[buzznode] no GPU render node found; using software rendering"
- fi
-fi
-
-cat > "$HOME/.vnc/kasmvnc.yaml" <> "$HOME/.vnc/kasmvnc.yaml" <<'YAML'
-
-logging:
- log_writer_name: EncodeManager
- log_dest: logfile
- level: 100
-YAML
- echo "[buzznode] KasmVNC encoder statistics enabled"
-fi
-
-cat > "$HOME/.vnc/xstartup" <<'XSTARTUP'
-#!/bin/bash
-exec openbox-session
-XSTARTUP
-chmod +x "$HOME/.vnc/xstartup"
-touch "$HOME/.vnc/.de-was-selected"
-chown -R agent:agent "$HOME/.vnc"
-
-# KasmVNC checks these even while browser authentication and TLS are disabled.
-su -s /bin/bash -c '
- openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
- -keyout "$HOME/.vnc/self.pem" \
- -out "$HOME/.vnc/self.pem" \
- -subj "/CN=buzznode" >/dev/null 2>&1
- printf "buzznode\nbuzznode\n" | kasmvncpasswd -u agent -wo >/dev/null 2>&1 || true
-' agent
-
-# shellcheck disable=SC2329
-cleanup() {
- echo "[buzznode] stopping"
- su -s /bin/bash -c 'kasmvncserver -kill :1 >/dev/null 2>&1 || true' agent
- pkill -TERM -u agent -f '(^|/)buzz-acp($| )' 2>/dev/null || true
-}
-trap cleanup EXIT INT TERM
-
-su -s /bin/bash -c 'kasmvncserver -kill :1 >/dev/null 2>&1 || true' agent
-rm -f /tmp/.X1-lock /tmp/.X11-unix/X1
-
-su -s /bin/bash -c "
- export HOME='$HOME'
- export DISPLAY=:1
- export XDG_CONFIG_HOME='$XDG_CONFIG_HOME'
- export XDG_DATA_HOME='$XDG_DATA_HOME'
- export XDG_RUNTIME_DIR='$XDG_RUNTIME_DIR'
- exec kasmvncserver :1 \
- -disableBasicAuth \
- -interface 0.0.0.0 \
- -websocketPort 6901 \
- -publicIP 127.0.0.1 \
- -geometry '$resolution' \
- -depth 24 \
- -httpd /usr/share/kasmvnc/www \
- -BlacklistThreshold 0 \
- -FreeKeyMappings
-" agent >>/var/log/buzznode/kasmvnc.log 2>&1 &
-
-for attempt in $(seq 1 40); do
- if curl -fsS http://127.0.0.1:6901/ >/dev/null 2>&1; then
- echo "[buzznode] desktop ready at http://localhost:6901"
- break
- fi
- if [ "$attempt" -eq 40 ]; then
- echo "[buzznode] KasmVNC did not become ready" >&2
- tail -n 100 /var/log/buzznode/kasmvnc.log >&2 || true
- exit 1
- fi
- sleep 1
-done
-
-while curl -fsS http://127.0.0.1:6901/ >/dev/null 2>&1; do
- sleep 5
-done
-
-echo "[buzznode] browser environment stopped unexpectedly" >&2
-exit 1
diff --git a/kasm/custom.css b/kasm/custom.css
deleted file mode 100644
index 3e98ac4..0000000
--- a/kasm/custom.css
+++ /dev/null
@@ -1,47 +0,0 @@
-/* KasmVNC UI overrides - hide branding, sidebar, black background. */
-.noVNC_logo {
- display: none !important;
-}
-body,
-#noVNC_container,
-.noVNC_container {
- background-color: #000 !important;
- background-image: none !important;
-}
-#noVNC_control_bar {
- display: none !important;
-}
-#noVNC_control_bar_hint {
- display: none !important;
-}
-
-/* Hide loading / transition / status screens. */
-#noVNC_transition {
- display: none !important;
- background: #000 !important;
- background-image: none !important;
-}
-#noVNC_transition_text {
- display: none !important;
-}
-.noVNC_spinner,
-.noVNC_spinner::before,
-.noVNC_spinner::after {
- display: none !important;
-}
-#noVNC_status {
- display: none !important;
-}
-#noVNC_connect_dlg {
- display: none !important;
-}
-.noVNC_connect_layer {
- display: none !important;
-}
-
-/* Hide version / fallback error branding. */
-.noVNC_version_wrapper,
-.noVNC_version,
-.noVNC_version_separator {
- display: none !important;
-}
diff --git a/kasm/patch.sh b/kasm/patch.sh
deleted file mode 100644
index 20d5e29..0000000
--- a/kasm/patch.sh
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/bin/bash
-# Patch KasmVNC web assets to remove branding and apply customisations.
-# Run once after installing the kasmvnc .deb package.
-set -euo pipefail
-
-WWW=/usr/share/kasmvnc/www
-
-# 1. Inject custom assets, rebrand the title, and replace upstream icon links.
-find "$WWW" -maxdepth 1 -name '*.html' -exec sed -i \
- -e 's|[^<]*|Buzznode|' \
- -e 's|]*rel="icon"[^>]*>||g' \
- -e 's|]*rel="apple-touch-icon"[^>]*>||g' \
- -e 's|||' \
- {} +
-
-# 2. Replace the "KasmVNC" brand string and keep the browser title fixed.
-# KasmVNC otherwise replaces it after connecting with the VNC desktop name,
-# which contains Docker's generated hostname.
-find "$WWW/assets" -name 'ui-*.js' -exec sed -i \
- -e 's|"KasmVNC"|"Buzznode"|g' \
- -e 's|document.title=r.detail.name+" - "+ox|document.title=ox|g' \
- {} +
-
-if grep -ERq 'document\.title=[[:alnum:]_$]+\.detail\.name\+" - "\+' \
- "$WWW/assets"/ui-*.js; then
- echo "[kasm-patch] dynamic VNC desktop title was not removed" >&2
- exit 1
-fi
-
-echo "[kasm-patch] KasmVNC UI patched successfully"
diff --git a/openbox/autostart b/openbox/autostart
deleted file mode 100755
index 8872311..0000000
--- a/openbox/autostart
+++ /dev/null
@@ -1,82 +0,0 @@
-#!/bin/bash
-# Openbox autostart - runs when the browser desktop session begins.
-
-set -e
-
-if [ ! -f "$HOME/.config/mimeapps.list" ]; then
- mkdir -p "$HOME/.local/share/applications" "$HOME/.config"
- cat > "$HOME/.local/share/applications/chromium-buzznode.desktop" <<'CHROMEDESKTOP'
-[Desktop Entry]
-Name=Browser
-Exec=chromium %u
-Type=Application
-MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;
-NoDisplay=true
-CHROMEDESKTOP
- cat > "$HOME/.config/mimeapps.list" <<'MIMEAPPS'
-[Default Applications]
-x-scheme-handler/http=chromium-buzznode.desktop
-x-scheme-handler/https=chromium-buzznode.desktop
-text/html=chromium-buzznode.desktop
-MIMEAPPS
- xdg-settings set default-web-browser chromium-buzznode.desktop 2>/dev/null || true
-fi
-export BROWSER=chromium
-
-# Kitty is tuned for a software-rendered remote display.
-mkdir -p "$HOME/.config/kitty"
-cat > "$HOME/.config/kitty/kitty.conf" <<'KITTYCONF'
-detect_urls yes
-open_url_with chromium
-url_color #5599ff
-cursor_shape block
-shell_integration no-cursor
-confirm_os_window_close 0
-map ctrl+c copy_or_interrupt
-map ctrl+v paste_from_clipboard
-window_padding_width 8
-repaint_delay 40
-input_delay 8
-sync_to_monitor no
-cursor_blink_interval 0
-mouse_hide_wait 0
-disable_ligatures always
-resize_debounce_time 0.1
-KITTYCONF
-
-wallpaper="/usr/share/backgrounds/buzz-grid.svg"
-rm -f "$HOME/.fehbg" "$HOME/.wallpaper"
-(
- last_res=""
- while true; do
- cur_res="$(xdpyinfo -display "${DISPLAY:-:1}" 2>/dev/null \
- | awk '/dimensions/{print $2}')"
- if [ -n "$cur_res" ] && [ "$cur_res" != "$last_res" ]; then
- feh --no-fehbg --bg-tile "$wallpaper"
- last_res="$cur_res"
- fi
- sleep 2
- done
-) &
-
-# The image owns the panel layout. Load it explicitly so Tint2 does not prefer
-# a stale tint2rc copied into the persistent user config on an earlier run.
-tint2 -c /etc/xdg/tint2/tint2rc &
-cortilectl start &
-
-# A configured node starts its headless agent harness and opens the node
-# terminal. A new node opens the terminal-first setup wizard.
-if buzznode configured; then
- buzznode start
- kitty --title "Buzznode" \
- --override remember_window_size=no \
- --override initial_window_width=100c \
- --override initial_window_height=30c \
- -e welcome &
-else
- kitty --title "Set up Buzznode" \
- --override remember_window_size=no \
- --override initial_window_width=100c \
- --override initial_window_height=44c \
- -e bash -lc 'buzznode setup; exec bash' &
-fi
diff --git a/openbox/rc.xml b/openbox/rc.xml
deleted file mode 100644
index 1eca311..0000000
--- a/openbox/rc.xml
+++ /dev/null
@@ -1,216 +0,0 @@
-
-
-
- 10
- 20
-
-
- yes
- no
- yes
- no
- 200
- no
-
-
- Smart
-
yes
-
-
- 8
- 8
- 52
- 8
-
-
- Never
-
-
- Triste-Crimson
- LIMC
- yes
- no
-
- Noto Sans
- 9
- Bold
- Normal
-
-
- Noto Sans
- 9
- Normal
- Normal
-
-
- Noto Sans
- 9
- Bold
- Normal
-
-
- Noto Sans
- 9
- Normal
- Normal
-
-
- Noto Sans
- 9
- Bold
- Normal
-
-
- Noto Sans
- 9
- Normal
- Normal
-
-
-
- 1
- 1
-
- Desktop
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- kitty
-
-
-
-
- kitty
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- no
-
-
-
diff --git a/openbox/theme/bullet.xbm b/openbox/theme/bullet.xbm
deleted file mode 100644
index 976273b..0000000
--- a/openbox/theme/bullet.xbm
+++ /dev/null
@@ -1,5 +0,0 @@
-#define bullet_width 10
-#define bullet_height 10
-static unsigned char bullet_bits[] = {
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x30, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
diff --git a/openbox/theme/close.xbm b/openbox/theme/close.xbm
deleted file mode 100644
index 3e327e3..0000000
--- a/openbox/theme/close.xbm
+++ /dev/null
@@ -1,4 +0,0 @@
-#define desk_width 6
-#define desk_height 6
-static unsigned char desk_bits[] = {
- 0x33, 0x33, 0x00, 0x00, 0x33, 0x33 };
diff --git a/openbox/theme/desk.xbm b/openbox/theme/desk.xbm
deleted file mode 100644
index 9598c82..0000000
--- a/openbox/theme/desk.xbm
+++ /dev/null
@@ -1,4 +0,0 @@
-#define desk_width 6
-#define desk_height 6
-static unsigned char desk_bits[] = {
- 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f };
diff --git a/openbox/theme/desk_toggled.xbm b/openbox/theme/desk_toggled.xbm
deleted file mode 100644
index 8daa5de..0000000
--- a/openbox/theme/desk_toggled.xbm
+++ /dev/null
@@ -1,4 +0,0 @@
-#define desk_width 6
-#define desk_height 6
-static unsigned char desk_bits[] = {
- 0x3f, 0x3f, 0x33, 0x33, 0x3f, 0x3f };
diff --git a/openbox/theme/iconify.xbm b/openbox/theme/iconify.xbm
deleted file mode 100644
index 6799d2e..0000000
--- a/openbox/theme/iconify.xbm
+++ /dev/null
@@ -1,4 +0,0 @@
-#define iconify_width 6
-#define iconify_height 6
-static unsigned char iconify_bits[] = {
- 0x00, 0x00, 0x00, 0x00, 0x0c, 0x0c };
diff --git a/openbox/theme/max.xbm b/openbox/theme/max.xbm
deleted file mode 100644
index 4426c02..0000000
--- a/openbox/theme/max.xbm
+++ /dev/null
@@ -1,4 +0,0 @@
-#define max7_width 6
-#define max7_height 6
-static unsigned char max7_bits[] = {
- 0x33, 0x33, 0x00, 0x00, 0x30, 0x30 };
diff --git a/openbox/theme/max_disabled.xbm b/openbox/theme/max_disabled.xbm
deleted file mode 100644
index a6eab63..0000000
--- a/openbox/theme/max_disabled.xbm
+++ /dev/null
@@ -1,4 +0,0 @@
-#define max_disabled_width 6
-#define max_disabled_height 6
-static unsigned char max_disabled_bits[] = {
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
diff --git a/openbox/theme/max_toggled.xbm b/openbox/theme/max_toggled.xbm
deleted file mode 100644
index e0f5022..0000000
--- a/openbox/theme/max_toggled.xbm
+++ /dev/null
@@ -1,4 +0,0 @@
-#define iconify2_width 6
-#define iconify2_height 6
-static unsigned char iconify2_bits[] = {
- 0x03, 0x03, 0x00, 0x00, 0x33, 0x33 };
diff --git a/openbox/theme/shade.xbm b/openbox/theme/shade.xbm
deleted file mode 100644
index 7438e1f..0000000
--- a/openbox/theme/shade.xbm
+++ /dev/null
@@ -1,4 +0,0 @@
-#define shade_width 6
-#define shade_height 6
-static unsigned char shade_bits[] = {
- 0x0c, 0x0c, 0x00, 0x00, 0x00, 0x00 };
diff --git a/openbox/theme/themerc b/openbox/theme/themerc
deleted file mode 100644
index 3d7e186..0000000
--- a/openbox/theme/themerc
+++ /dev/null
@@ -1,150 +0,0 @@
-# Arc Openbox theme
-# Copyright (C) 2015 Dino Duratović
-#
-# Inspired by and made for horst3180's Arc GTK theme
-# https://github.com/horst3180/Arc-theme
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-### WINDOW
-border.width: 1
-
-# Grab margin. Openbox has no separate resize-sensitivity setting: the areas you
-# can grab are exactly the areas it draws, and with the bottom handle gone the
-# frame offered 1px to grab at the bottom against a 28px titlebar - the bottom
-# corners were nearly unhittable. Client padding is the one knob that adds frame
-# without widening the visible border: it inserts frame around the client and
-# paints it in the frame background, so nothing new is drawn while the grabbable
-# ring goes from 1px to 7px. Measured via _NET_FRAME_EXTENTS: 1,1,28,1 at 0
-# becomes 7,7,34,7 at 6.
-window.client.padding.width: 6
-window.client.padding.height: 6
-# No bottom handle. It renders as a second line under the client area with a
-# resize grip boxed off at each end, which reads as clutter. Resizing stays
-# available through the window edges and corners and through Alt+right-drag
-# anywhere on the frame (see rc.xml).
-window.handle.width: 0
-
-padding.width: 6
-padding.height: 5
-
-window.active.border.color: #0b0b0b
-window.inactive.border.color: #070707
-window.active.title.separator.color: #000000
-window.inactive.title.separator.color: #000000
-window.active.client.color: #000000
-window.inactive.client.color: #000000
-
-window.active.label.text.color: #D3DAE3
-window.inactive.label.text.color: #7F8388
-
-window.active.button.unpressed.image.color: #D3DAE3
-window.active.button.pressed.image.color: #DC143C
-window.active.button.disabled.image.color: #000000
-window.active.button.hover.image.color: #afb8c5
-window.active.button.toggled.unpressed.image.color: #D3DAE3
-window.active.button.toggled.pressed.image.color: #DC143C
-window.active.button.toggled.hover.image.color: #afb8c5
-window.inactive.button.unpressed.image.color: #1F2328
-window.inactive.button.pressed.image.color: #DC143C
-window.inactive.button.disabled.image.color: #000000
-window.inactive.button.hover.image.color: #afb8c5
-window.inactive.button.toggled.unpressed.image.color: #1F2328
-window.inactive.button.toggled.pressed.image.color: #DC143C
-window.inactive.button.toggled.hover.image.color: #afb8c5
-
-window.active.title.bg: flat solid
-window.active.title.bg.color: #000000
-window.active.label.bg: flat solid
-window.active.label.bg.color: #000000
-window.active.handle.bg: flat solid
-window.active.handle.bg.color: #000000
-window.active.grip.bg: flat solid
-window.active.grip.bg.color: #000000
-window.inactive.title.bg: flat solid
-window.inactive.title.bg.color: #000000
-window.inactive.label.bg: flat solid
-window.inactive.label.bg.color: #000000
-window.inactive.handle.bg: flat solid
-window.inactive.handle.bg.color: #000000
-window.inactive.grip.bg: flat solid
-window.inactive.grip.bg.color: #000000
-
-window.active.button.unpressed.bg: parentrelative
-window.active.button.pressed.bg: parentrelative
-window.active.button.hover.bg: parentrelative
-window.active.button.disabled.bg: parentrelative
-window.active.button.toggled.unpressed.bg: parentrelative
-window.active.button.toggled.pressed.bg: parentrelative
-window.active.button.toggled.hover.bg: parentrelative
-window.inactive.button.unpressed.bg: parentrelative
-window.inactive.button.pressed.bg: parentrelative
-window.inactive.button.hover.bg: parentrelative
-window.inactive.button.disabled.bg: parentrelative
-window.inactive.button.toggled.unpressed.bg: parentrelative
-window.inactive.button.toggled.pressed.bg: parentrelative
-window.inactive.button.toggled.hover.bg: parentrelative
-
-window.label.text.justify: center
-
-#window.active.label.text.font: text shadow
-#window.inactive.label.text.font: text shadow
-
-### MENU
-menu.border.width: 8
-menu.separator.width: 1
-menu.separator.padding.width: 6
-menu.separator.padding.height: 4
-
-menu.overlap.x: 0
-menu.overlap.y: 0
-
-menu.border.color: #000000
-menu.separator.color: #222222
-
-menu.title.text.color: #ffffff
-menu.items.text.color: #a8adb5
-menu.items.disabled.text.color: #76797F
-menu.items.active.text.color: #000000
-menu.items.active.disabled.text.color: #aeb0b6
-
-menu.items.bg: flat solid
-menu.items.bg.color: #000000
-menu.items.active.bg: flat solid
-menu.items.active.bg.color: #ffffff
-menu.title.bg: flat solid
-menu.title.bg.color: #000000
-
-menu.title.text.justify: center
-
-#menu.items.font: text shadow
-#menu.title.text.font: text shadow
-
-### OSD
-osd.border.width: 1
-
-osd.border.color: #000000
-
-osd.label.text.color: #D3DAE3
-
-osd.bg: flat solid
-osd.bg.color: #000000
-osd.label.bg: flat solid
-osd.label.bg.color: #000000
-osd.hilight.bg: flat solid
-osd.hilight.bg.color: #DC143C
-osd.unhilight.bg: flat solid
-osd.unhilight.bg.color: #000000
-
-#osd.label.text.font: text shadow
diff --git a/shell/bashrc b/overlay/etc/bash.bashrc.d/buzznode-prompt.sh
similarity index 100%
rename from shell/bashrc
rename to overlay/etc/bash.bashrc.d/buzznode-prompt.sh
diff --git a/overlay/etc/desktop/session.d/10-buzznode-harness b/overlay/etc/desktop/session.d/10-buzznode-harness
new file mode 100755
index 0000000..9e209f8
--- /dev/null
+++ b/overlay/etc/desktop/session.d/10-buzznode-harness
@@ -0,0 +1,17 @@
+#!/bin/bash
+# Start the agent harness for a node that has already been enrolled.
+#
+# This is session.d rather than startup.d because enrollment happens inside the
+# session: a brand-new node has no identity until somebody completes the setup
+# wizard desktop-welcome opens, and there is nothing to start until then. On
+# every later boot the node is configured, and the harness comes up with the
+# desktop.
+#
+# `buzznode start` returns once the harness is backgrounded, so this does not
+# hold up the rest of the session.
+
+set -euo pipefail
+
+if buzznode configured >/dev/null 2>&1; then
+ buzznode start
+fi
diff --git a/overlay/etc/desktop/startup.d/05-agent-runtime-trust b/overlay/etc/desktop/startup.d/05-agent-runtime-trust
new file mode 100755
index 0000000..5f7b71f
--- /dev/null
+++ b/overlay/etc/desktop/startup.d/05-agent-runtime-trust
@@ -0,0 +1,81 @@
+#!/bin/bash
+# Record the workspace as trusted for the coding agent runtimes.
+#
+# Codex and Claude Code each prompt once per directory before working in it
+# ("Do you trust the contents of this directory?"). `buzznode launch` runs the
+# harness unattended - it does `cd /workspace` and execs buzz-acp with
+# codex-acp, backgrounded into a log file - so there is nobody present to
+# answer, and the agent would sit on the prompt with the reason buried in the
+# log. codex-acp consults trust_level, so record the decision at boot instead.
+#
+# This grants no access the harness is not already started with. Set
+# BUZZNODE_TRUST_WORKSPACE=false to leave both prompts in place.
+#
+# Runs from the desktop base entrypoint before the session starts. The base
+# passes the account the desktop will run as in DESKTOP_RUNTIME_USER.
+
+set -euo pipefail
+
+runtime_user="${DESKTOP_RUNTIME_USER:-agent}"
+home_dir="/home/agent"
+harness_workdir="${BUZZNODE_HARNESS_WORKDIR:-/workspace}"
+codex_config="$home_dir/.codex/config.toml"
+
+install_as_runtime_user() {
+ install -m 0600 -o "$runtime_user" -g "$runtime_user" "$1" "$2"
+}
+
+# Codex sandboxes the commands it runs with bubblewrap, and warns when it has to
+# fall back to its bundled copy. Neither copy can work here: the container blocks
+# unprivileged user namespaces, so bwrap cannot create one, and installing the
+# distro package only adds a second binary that fails the same way. Declare the
+# mode that matches reality rather than leaving a config that implies an
+# isolation boundary which is not there - the boundary is the container itself.
+#
+# Set BUZZNODE_CODEX_SANDBOX_MODE to read-only or workspace-write to choose a
+# different mode, or to an empty value to leave the setting out entirely.
+codex_sandbox_mode="${BUZZNODE_CODEX_SANDBOX_MODE-danger-full-access}"
+if [ -n "$codex_sandbox_mode" ] &&
+ ! grep -Eq '^sandbox_mode *=' "$codex_config" 2>/dev/null; then
+ codex_sandbox_tmp="$(mktemp)"
+ # Prepended, not appended: sandbox_mode is a top-level key, and TOML assigns
+ # any key following a [table] header to that table. The trust block below
+ # writes [projects."..."] tables, so appending would quietly turn this into a
+ # per-project setting instead of a global one.
+ {
+ printf 'sandbox_mode = "%s"\n\n' "$codex_sandbox_mode"
+ [ -s "$codex_config" ] && cat "$codex_config"
+ } > "$codex_sandbox_tmp"
+ install_as_runtime_user "$codex_sandbox_tmp" "$codex_config"
+ rm -f "$codex_sandbox_tmp"
+ echo "[buzznode] set Codex sandbox_mode=$codex_sandbox_mode" \
+ "(no usable bubblewrap in a container)"
+fi
+
+if [ "${BUZZNODE_TRUST_WORKSPACE:-true}" != "true" ]; then
+ exit 0
+fi
+
+if ! grep -Fq "[projects.\"$harness_workdir\"]" "$codex_config" 2>/dev/null; then
+ printf '\n[projects."%s"]\ntrust_level = "trusted"\n' \
+ "$harness_workdir" >> "$codex_config"
+ chown "$runtime_user:$runtime_user" "$codex_config"
+ echo "[buzznode] recorded $harness_workdir as trusted for Codex"
+fi
+
+claude_config="$home_dir/.claude.json"
+if ! jq -e --arg dir "$harness_workdir" \
+ '.projects[$dir].hasTrustDialogAccepted == true' \
+ "$claude_config" >/dev/null 2>&1; then
+ [ -s "$claude_config" ] || echo '{}' > "$claude_config"
+ claude_trust_tmp="$(mktemp)"
+ # Leave the file untouched if it is not valid JSON rather than replacing a
+ # config the operator may have hand-written.
+ if jq --arg dir "$harness_workdir" \
+ '.projects[$dir].hasTrustDialogAccepted = true' \
+ "$claude_config" > "$claude_trust_tmp" 2>/dev/null; then
+ install_as_runtime_user "$claude_trust_tmp" "$claude_config"
+ echo "[buzznode] recorded $harness_workdir as trusted for Claude Code"
+ fi
+ rm -f "$claude_trust_tmp"
+fi
diff --git a/openbox/menu.xml b/overlay/etc/xdg/openbox/menu.xml
similarity index 95%
rename from openbox/menu.xml
rename to overlay/etc/xdg/openbox/menu.xml
index b481089..7cb03aa 100644
--- a/openbox/menu.xml
+++ b/overlay/etc/xdg/openbox/menu.xml
@@ -3,7 +3,7 @@
diff --git a/tint2/tint2rc b/overlay/etc/xdg/tint2/tint2rc
similarity index 95%
rename from tint2/tint2rc
rename to overlay/etc/xdg/tint2/tint2rc
index 7b3439a..9229ae1 100644
--- a/tint2/tint2rc
+++ b/overlay/etc/xdg/tint2/tint2rc
@@ -167,9 +167,13 @@ systray_monitor = 1
systray_name_filter =
#-------------------------------------
-# Buzznode status
+# Buzznode status.
+#
+# This file exists only for the two lines the base's panel cannot carry: the
+# tooltip and the click action that opens `buzznode status`. Everything else is
+# the base's tint2rc, which tint2 gives no way to include or extend.
execp = new
-execp_command = buzznode-panel-status
+execp_command = desktop-panel-status
execp_interval = 5
execp_continuous = 0
execp_has_icon = 0
diff --git a/browser/index.html b/overlay/opt/browser/index.html
similarity index 100%
rename from browser/index.html
rename to overlay/opt/browser/index.html
diff --git a/shell/agent-runtime-login b/overlay/usr/local/bin/agent-runtime-login
similarity index 100%
rename from shell/agent-runtime-login
rename to overlay/usr/local/bin/agent-runtime-login
diff --git a/shell/buzznode b/overlay/usr/local/bin/buzznode
similarity index 99%
rename from shell/buzznode
rename to overlay/usr/local/bin/buzznode
index 3184a68..f386e4a 100755
--- a/shell/buzznode
+++ b/overlay/usr/local/bin/buzznode
@@ -6,7 +6,7 @@ set -euo pipefail
config_dir="${BUZZNODE_CONFIG_DIR:-$HOME/.config/buzznode}"
environment_file="$config_dir/environment"
runtime_file="$config_dir/runtime"
-harness_log="${BUZZNODE_HARNESS_LOG:-/var/log/buzznode/buzz-acp.log}"
+harness_log="${BUZZNODE_HARNESS_LOG:-/var/log/launcher-desktop/buzz-acp.log}"
style_reset=""
style_bold=""
@@ -422,7 +422,7 @@ setup_command() {
current_relay="$(effective_relay_url)"
clear
- welcome --no-shell
+ buzznode-greeting
print_heading "Connect this Buzznode"
print_info "This node runs one agent from an existing Buzz workspace."
print_info "In Buzzbox, choose Agent Setup → Create New Agent for Buzznode."
diff --git a/shell/welcome b/overlay/usr/local/bin/buzznode-greeting
similarity index 97%
rename from shell/welcome
rename to overlay/usr/local/bin/buzznode-greeting
index b665421..bd5d962 100755
--- a/shell/welcome
+++ b/overlay/usr/local/bin/buzznode-greeting
@@ -38,7 +38,3 @@ ${D} buzznode status Show connection and runtime state
File Manager opens /workspace. Press 'W for Workspace or 'H for Home.${R}
EOF
-
-if [ "${1:-}" != "--no-shell" ]; then
- exec bash -l
-fi
diff --git a/shell/buzznode-panel-status b/overlay/usr/local/bin/desktop-panel-status
similarity index 100%
rename from shell/buzznode-panel-status
rename to overlay/usr/local/bin/desktop-panel-status
diff --git a/overlay/usr/local/bin/desktop-welcome b/overlay/usr/local/bin/desktop-welcome
new file mode 100755
index 0000000..a94f97c
--- /dev/null
+++ b/overlay/usr/local/bin/desktop-welcome
@@ -0,0 +1,24 @@
+#!/bin/bash
+# The terminal the session opens with.
+#
+# A new node has no identity, so the first terminal is the setup wizard - it is
+# the only thing on this desktop that can do anything useful yet. Once the node
+# is enrolled the terminal is a terminal, behind the node's status greeting.
+
+set -euo pipefail
+
+if buzznode configured >/dev/null 2>&1; then
+ exec kitty \
+ --title "${DESKTOP_TITLE:-Buzznode}" \
+ --override remember_window_size=no \
+ --override initial_window_width=100c \
+ --override initial_window_height=30c \
+ -e bash -lc 'buzznode-greeting; exec bash'
+fi
+
+exec kitty \
+ --title "Set up Buzznode" \
+ --override remember_window_size=no \
+ --override initial_window_width=100c \
+ --override initial_window_height=44c \
+ -e bash -lc 'buzznode setup; exec bash'
diff --git a/overlay/usr/share/backgrounds/desktop-wallpaper.svg b/overlay/usr/share/backgrounds/desktop-wallpaper.svg
new file mode 100644
index 0000000..65d25e6
--- /dev/null
+++ b/overlay/usr/share/backgrounds/desktop-wallpaper.svg
@@ -0,0 +1,9 @@
+
diff --git a/kasm/favicon.svg b/overlay/usr/share/kasmvnc/www/assets/favicon.svg
similarity index 100%
rename from kasm/favicon.svg
rename to overlay/usr/share/kasmvnc/www/assets/favicon.svg
diff --git a/gtk/Buzznode/gtk-3.0/gtk.css b/overlay/usr/share/themes/Desktop/gtk-3.0/gtk.css
similarity index 100%
rename from gtk/Buzznode/gtk-3.0/gtk.css
rename to overlay/usr/share/themes/Desktop/gtk-3.0/gtk.css
diff --git a/shell/chromium b/shell/chromium
deleted file mode 100755
index 627a1cd..0000000
--- a/shell/chromium
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/bash
-# chromium - wrapper that launches the available Chromium-family browser.
-# Placed in /usr/local/bin to override any system chromium and provide a
-# single entry point for menus, xdg-open, kitty, and CLI tools.
-
-# Only use the GPU when a render node is present *and* this user can open it.
-# With one the browser can composite and rasterize on the GPU, which removes
-# the largest source of repaint work on this desktop. A passed-through node is
-# normally root:render 0660 and this image has no matching group, so testing
-# for presence alone points Chrome's GPU process at a device it cannot open.
-gpu_args=(--disable-gpu --disable-software-rasterizer)
-for node in /dev/dri/renderD*; do
- if [ -r "$node" ] && [ -w "$node" ]; then
- gpu_args=(--ignore-gpu-blocklist --enable-gpu-rasterization)
- break
- fi
-done
-
-if [ -x /opt/google/chrome/google-chrome ]; then
- browser=/opt/google/chrome/google-chrome
-elif [ -x /usr/bin/chromium ]; then
- browser=/usr/bin/chromium
-else
- echo "No supported Chromium-family browser is installed." >&2
- exit 1
-fi
-
-exec "$browser" \
- --no-sandbox \
- --test-type \
- "${gpu_args[@]}" \
- --disable-dev-shm-usage \
- --no-first-run \
- --no-default-browser-check \
- --disable-infobars \
- --force-dark-mode \
- --enable-features=WebContentsForceDark \
- "$@"
diff --git a/tests/smoke-container.sh b/tests/smoke-container.sh
index 0148948..9936e4a 100644
--- a/tests/smoke-container.sh
+++ b/tests/smoke-container.sh
@@ -48,7 +48,7 @@ fi
"$docker" exec --detach \
--user agent \
--env DISPLAY=:1 \
- --env HOME=/home/buzznode \
+ --env HOME=/home/agent \
"$container" \
chromium file:///opt/browser/index.html
diff --git a/tests/test-agent-runtime-login.sh b/tests/test-agent-runtime-login.sh
index f206b35..22ee327 100755
--- a/tests/test-agent-runtime-login.sh
+++ b/tests/test-agent-runtime-login.sh
@@ -6,7 +6,7 @@ project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
temporary_dir="$(mktemp -d)"
trap 'rm -rf "$temporary_dir"' EXIT
-helper="$project_dir/shell/agent-runtime-login"
+helper="$project_dir/overlay/usr/local/bin/agent-runtime-login"
runtime_log="$temporary_dir/runtime.log"
export runtime_log
@@ -77,11 +77,11 @@ fi
# codex-acp consults the same trust_level. `buzznode launch` starts the harness
# unattended in the workspace, so that prompt has to be settled at boot or the
# agent stops with the reason buried in its log.
-grep -Fq 'BUZZNODE_TRUST_WORKSPACE' "$project_dir/init.sh"
-grep -Fq 'trust_level = "trusted"' "$project_dir/init.sh"
-grep -Fq 'hasTrustDialogAccepted' "$project_dir/init.sh"
+grep -Fq 'BUZZNODE_TRUST_WORKSPACE' "$project_dir/overlay/etc/desktop/startup.d/05-agent-runtime-trust"
+grep -Fq 'trust_level = "trusted"' "$project_dir/overlay/etc/desktop/startup.d/05-agent-runtime-trust"
+grep -Fq 'hasTrustDialogAccepted' "$project_dir/overlay/etc/desktop/startup.d/05-agent-runtime-trust"
# The directory trusted at boot must be the one the harness is launched in.
-grep -Fq 'BUZZNODE_HARNESS_WORKDIR:-/workspace' "$project_dir/init.sh"
-grep -Fq 'cd /workspace' "$project_dir/shell/buzznode"
+grep -Fq 'BUZZNODE_HARNESS_WORKDIR:-/workspace' "$project_dir/overlay/etc/desktop/startup.d/05-agent-runtime-trust"
+grep -Fq 'cd /workspace' "$project_dir/overlay/usr/local/bin/buzznode"
echo "Agent runtime login tests passed."
diff --git a/tests/test-buzznode.sh b/tests/test-buzznode.sh
index 7c023ed..472cc63 100755
--- a/tests/test-buzznode.sh
+++ b/tests/test-buzznode.sh
@@ -10,7 +10,7 @@ export HOME="$temporary_dir/home"
export BUZZNODE_CONFIG_DIR="$HOME/.config/buzznode"
mkdir -p "$HOME"
-cli="$project_dir/shell/buzznode"
+cli="$project_dir/overlay/usr/local/bin/buzznode"
token='token with spaces and $shell characters'
private_key='0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'
auth_tag='{"kind":"owner delegation","value":"$secret"}'
@@ -56,7 +56,7 @@ if grep -Fq $'\033[' <<<"$plain_status_output"; then
exit 1
fi
-panel_status="$project_dir/shell/buzznode-panel-status"
+panel_status="$project_dir/overlay/usr/local/bin/desktop-panel-status"
mock_bin="$temporary_dir/bin"
mkdir -p "$mock_bin"
ln -s "$cli" "$mock_bin/buzznode"
@@ -72,21 +72,29 @@ grep -Fq 'Set up Buzznode' <<<"$unconfigured_panel_output"
configured_panel_output="$(PATH="$mock_bin:$PATH" "$panel_status")"
grep -Eq '(running|stopped)' <<<"$configured_panel_output"
+overlay_dir="$project_dir/overlay"
normalized_menu="$(
- tr '\n\t' ' ' < "$project_dir/openbox/menu.xml" | tr -s ' '
+ tr '\n\t' ' ' < "$overlay_dir/etc/xdg/openbox/menu.xml" | tr -s ' '
)"
grep -Fq 'buzznode setup; exec bash' <<<"$normalized_menu"
-grep -Fq "buzznode setup; exec bash" "$project_dir/openbox/autostart"
-grep -Fq 'tint2 -c /etc/xdg/tint2/tint2rc' \
- "$project_dir/openbox/autostart"
grep -Fq 'kitty --title "Agent Harness Log" -e buzznode logs' \
<<<"$normalized_menu"
-grep -Fq 'panel_items = PTSEC' "$project_dir/tint2/tint2rc"
-grep -Fq 'execp_command = buzznode-panel-status' "$project_dir/tint2/tint2rc"
-grep -Fq 'buzznode status; exec bash' "$project_dir/tint2/tint2rc"
-grep -Fq 'assets/favicon.svg' "$project_dir/kasm/patch.sh"
-grep -Fq 'COPY kasm/favicon.svg /usr/share/kasmvnc/www/assets/favicon.svg' \
- "$project_dir/Dockerfile"
+
+# The session entry points the desktop base calls into. The wizard opens for an
+# unenrolled node and the harness only starts once there is an identity, so
+# both branches have to be present.
+welcome="$overlay_dir/usr/local/bin/desktop-welcome"
+grep -Fq 'buzznode setup; exec bash' "$welcome"
+grep -Fq 'buzznode-greeting; exec bash' "$welcome"
+grep -Fq 'buzznode start' \
+ "$overlay_dir/etc/desktop/session.d/10-buzznode-harness"
+
+# This image ships a tint2rc only to add the tooltip and the click action.
+grep -Fq 'panel_items = PTSEC' "$overlay_dir/etc/xdg/tint2/tint2rc"
+grep -Fq 'execp_command = desktop-panel-status' \
+ "$overlay_dir/etc/xdg/tint2/tint2rc"
+grep -Fq 'buzznode status; exec bash' "$overlay_dir/etc/xdg/tint2/tint2rc"
+test -s "$overlay_dir/usr/share/kasmvnc/www/assets/favicon.svg"
if "$cli" configure --relay-url 'https://not-a-websocket.example.com' \
--private-key "$private_key" >/dev/null 2>&1; then
diff --git a/tests/test-desktop-theme.sh b/tests/test-desktop-theme.sh
index 85767a4..06d757d 100644
--- a/tests/test-desktop-theme.sh
+++ b/tests/test-desktop-theme.sh
@@ -1,125 +1,72 @@
#!/bin/bash
+# Buzznode's share of the desktop appearance.
+#
+# The desktop base owns the GTK theme machinery, the Openbox theme, the panel
+# layout, the window-control resource overlay, and the Chrome policy - all of
+# that is asserted in the base's own tests. What is checked here is only what
+# this image still installs over it: the Buzz accent colours, the browser
+# landing page, and the KasmVNC brand.
set -euo pipefail
project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
-overlay_test_dir="$(mktemp -d)"
-trap 'rm -rf "$overlay_test_dir"' EXIT
+overlay_dir="$project_dir/overlay"
+gtk_css="$overlay_dir/usr/share/themes/Desktop/gtk-3.0/gtk.css"
+landing_page="$overlay_dir/opt/browser/index.html"
-grep -Fq 'background: #000;' "$project_dir/browser/index.html"
-grep -Fq 'Buzznode Browser' "$project_dir/browser/index.html"
-grep -Fq '██████╗ ██╗ ██╗███████╗███████╗███╗' \
- "$project_dir/browser/index.html"
-if grep -Fq '
Buzznode
' "$project_dir/browser/index.html"; then
+# The browser landing page.
+grep -Fq 'background: #000;' "$landing_page"
+grep -Fq '██████╗ ██╗ ██╗███████╗███████╗' "$landing_page" # the shared BUZZ wordmark
+if grep -Fq '
Buzznode
' "$landing_page"; then
echo "The obsolete browser welcome card is still present." >&2
exit 1
fi
-grep -Fq 'ENV GTK_THEME=Buzznode' "$project_dir/Dockerfile"
-grep -Fq \
- 'ENV G_RESOURCE_OVERLAYS=/org/gtk/libgtk=/usr/share/buzznode/gtk-overlay' \
- "$project_dir/Dockerfile"
-grep -Fq \
- 'COPY gtk/Buzznode /usr/share/themes/Buzznode' \
- "$project_dir/Dockerfile"
-grep -Fq \
- 'COPY gtk/generate-resource-overlay.py /tmp/generate-gtk-resource-overlay.py' \
- "$project_dir/Dockerfile"
+# The Buzz accents. These two declarations are the only reason this image ships
+# a gtk.css at all - everything else in the file is the base's.
+grep -Fq 'caret-color: #d7d72e;' "$gtk_css"
+grep -Fq '@define-color theme_selected_bg_color #2b2b0b;' "$gtk_css"
-python3 "$project_dir/gtk/generate-resource-overlay.py" \
- "$project_dir/openbox/theme" "$overlay_test_dir"
-for control in minimize maximize restore close; do
- control_path="$overlay_test_dir/icons/16x16/status/window-${control}-symbolic.symbolic.png"
- test -s "$control_path"
- python3 -c \
- 'import pathlib, sys; assert pathlib.Path(sys.argv[1]).read_bytes().startswith(b"\x89PNG\r\n\x1a\n")' \
- "$control_path"
-done
-
-grep -Fq '"system_theme": 1' "$project_dir/Dockerfile"
-grep -Fq 'for config_dir in google-chrome chromium' \
- "$project_dir/Dockerfile"
-grep -Fq '/etc/chromium/policies/managed/buzznode-policy.json' \
- "$project_dir/Dockerfile"
-grep -Fq 'browser=/opt/google/chrome/google-chrome' \
- "$project_dir/shell/chromium"
-grep -Fq 'browser=/usr/bin/chromium' \
- "$project_dir/shell/chromium"
-grep -Fq 'exec "$browser"' \
- "$project_dir/shell/chromium"
-grep -Fq 'popover.background.menu' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'background-color: #020303;' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'window.background.csd decoration' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'decoration:not(:backdrop)' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'headerbar.header-bar.titlebar' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'border-radius: 0;' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'padding-right: 4px;' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'button.titlebutton:backdrop' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'caret-color: #d7d72e;' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fq 'color: #dc143c;' \
- "$project_dir/gtk/Buzznode/gtk-3.0/gtk.css"
-grep -Fxq 'gtk-font-name = Noto Sans 9' \
- "$project_dir/gtk/Buzznode/gtk-3.0/settings.ini"
-test "$(grep -Fc 'Noto Sans' "$project_dir/openbox/rc.xml")" -eq 6
-test "$(grep -Fc '9' "$project_dir/openbox/rc.xml")" -eq 6
+# The brand and the wallpaper drop-in the base resolves at session start.
+grep -Fq 'RUN kasm-patch "Buzznode"' "$project_dir/Dockerfile"
+test -s "$overlay_dir/usr/share/kasmvnc/www/assets/favicon.svg"
+test -s "$overlay_dir/usr/share/backgrounds/desktop-wallpaper.svg"
-if grep -Fq '"BrowserThemeColor"' "$project_dir/Dockerfile"; then
- echo "BrowserThemeColor still overrides the GTK Chrome theme." >&2
- exit 1
-fi
-if grep -Fq -- '--pack-extension=' "$project_dir/Dockerfile"; then
- echo "The obsolete Chrome extension theme is still packaged." >&2
- exit 1
-fi
+# The wallpaper must cover a full canvas. The base applies it with
+# `feh --bg-fill`, which would scale a bare 37px tile into one enormous dot.
+wallpaper="$overlay_dir/usr/share/backgrounds/desktop-wallpaper.svg"
+grep -Fq 'patternUnits="userSpaceOnUse"' "$wallpaper"
-grep -Fq 'amd64|arm64)' "$project_dir/Dockerfile"
-grep -Fq 'yq_linux_${arch}' "$project_dir/Dockerfile"
-grep -Fq 'kasmvncserver_noble_${KASMVNC_VERSION}_${arch}.deb' \
- "$project_dir/Dockerfile"
-grep -Fq 'google-chrome-stable_current_amd64.deb' \
- "$project_dir/Dockerfile"
-grep -Fq "'deb [arch=arm64 signed-by=/etc/apt/keyrings/debian-archive-key-12.asc] https://deb.debian.org/debian bookworm main'" \
- "$project_dir/Dockerfile"
-grep -Fq 'apt-get install -y --no-install-recommends chromium' \
- "$project_dir/Dockerfile"
-grep -Fq 'git clone --branch "v${BUZZ_VERSION}" --depth 1' \
- "$project_dir/Dockerfile"
-grep -Fq 'FROM rust:1.95-bookworm AS buzz-tools' \
- "$project_dir/Dockerfile"
-grep -Fq 'test "$(git rev-parse HEAD)" = "$BUZZ_SOURCE_SHA"' \
- "$project_dir/Dockerfile"
-grep -Fq 'arm64) goose_arch=aarch64' "$project_dir/Dockerfile"
-if grep -Fq 'Buzznode currently supports linux/amd64 only' \
- "$project_dir/Dockerfile"; then
- echo "The Dockerfile still rejects ARM64 builds." >&2
+# ...and it must contain no XML comment. The imlib2 SVG loader feh uses rejects
+# any file with one - anywhere, not just before the root element - reporting
+# "No Imlib2 loader for that file format". The desktop then comes up with no
+# wallpaper at all, which is easy to miss and easy to reintroduce by
+# documenting the file in the obvious place.
+if grep -Fq '