From 47c6214fa4204b1c5431fdfe149fc2af26eb3c76 Mon Sep 17 00:00:00 2001 From: pdparchitect Date: Thu, 30 Jul 2026 00:14:23 +0000 Subject: [PATCH] - chore: update version to 0.5.2 and modify changelog, README, and Dockerfile accordingly - Add initial Buzznode desktop environment and related scripts --- CHANGELOG.md | 54 +++ Dockerfile | 379 +++++------------- Makefile | 46 ++- README.md | 43 +- VERSION | 2 +- cortile/cortile-config.toml | 139 ------- cortile/cortilectl | 122 ------ gtk/Buzznode/gtk-3.0/settings.ini | 2 - gtk/generate-resource-overlay.py | 97 ----- init.sh | 276 ------------- kasm/custom.css | 47 --- kasm/patch.sh | 30 -- openbox/autostart | 82 ---- openbox/rc.xml | 216 ---------- openbox/theme/bullet.xbm | 5 - openbox/theme/close.xbm | 4 - openbox/theme/desk.xbm | 4 - openbox/theme/desk_toggled.xbm | 4 - openbox/theme/iconify.xbm | 4 - openbox/theme/max.xbm | 4 - openbox/theme/max_disabled.xbm | 4 - openbox/theme/max_toggled.xbm | 4 - openbox/theme/shade.xbm | 4 - openbox/theme/themerc | 150 ------- .../etc/bash.bashrc.d/buzznode-prompt.sh | 0 .../etc/desktop/session.d/10-buzznode-harness | 17 + .../desktop/startup.d/05-agent-runtime-trust | 81 ++++ {openbox => overlay/etc/xdg/openbox}/menu.xml | 4 +- {tint2 => overlay/etc/xdg/tint2}/tint2rc | 8 +- {browser => overlay/opt/browser}/index.html | 0 .../usr/local/bin}/agent-runtime-login | 0 {shell => overlay/usr/local/bin}/buzznode | 4 +- .../usr/local/bin/buzznode-greeting | 4 - .../usr/local/bin/desktop-panel-status | 0 overlay/usr/local/bin/desktop-welcome | 24 ++ .../share/backgrounds/desktop-wallpaper.svg | 9 + .../usr/share/kasmvnc/www/assets}/favicon.svg | 0 .../usr/share/themes/Desktop}/gtk-3.0/gtk.css | 0 shell/chromium | 38 -- tests/smoke-container.sh | 2 +- tests/test-agent-runtime-login.sh | 12 +- tests/test-buzznode.sh | 32 +- tests/test-desktop-theme.sh | 161 +++----- wallpaper/buzz-grid.svg | 4 - 44 files changed, 430 insertions(+), 1692 deletions(-) delete mode 100644 cortile/cortile-config.toml delete mode 100644 cortile/cortilectl delete mode 100644 gtk/Buzznode/gtk-3.0/settings.ini delete mode 100644 gtk/generate-resource-overlay.py delete mode 100755 init.sh delete mode 100644 kasm/custom.css delete mode 100644 kasm/patch.sh delete mode 100755 openbox/autostart delete mode 100644 openbox/rc.xml delete mode 100644 openbox/theme/bullet.xbm delete mode 100644 openbox/theme/close.xbm delete mode 100644 openbox/theme/desk.xbm delete mode 100644 openbox/theme/desk_toggled.xbm delete mode 100644 openbox/theme/iconify.xbm delete mode 100644 openbox/theme/max.xbm delete mode 100644 openbox/theme/max_disabled.xbm delete mode 100644 openbox/theme/max_toggled.xbm delete mode 100644 openbox/theme/shade.xbm delete mode 100644 openbox/theme/themerc rename shell/bashrc => overlay/etc/bash.bashrc.d/buzznode-prompt.sh (100%) create mode 100755 overlay/etc/desktop/session.d/10-buzznode-harness create mode 100755 overlay/etc/desktop/startup.d/05-agent-runtime-trust rename {openbox => overlay/etc/xdg/openbox}/menu.xml (95%) rename {tint2 => overlay/etc/xdg/tint2}/tint2rc (95%) rename {browser => overlay/opt/browser}/index.html (100%) rename {shell => overlay/usr/local/bin}/agent-runtime-login (100%) rename {shell => overlay/usr/local/bin}/buzznode (99%) rename shell/welcome => overlay/usr/local/bin/buzznode-greeting (97%) rename shell/buzznode-panel-status => overlay/usr/local/bin/desktop-panel-status (100%) create mode 100755 overlay/usr/local/bin/desktop-welcome create mode 100644 overlay/usr/share/backgrounds/desktop-wallpaper.svg rename {kasm => overlay/usr/share/kasmvnc/www/assets}/favicon.svg (100%) rename {gtk/Buzznode => overlay/usr/share/themes/Desktop}/gtk-3.0/gtk.css (100%) delete mode 100755 shell/chromium delete mode 100644 wallpaper/buzz-grid.svg 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 - - - - - - - - root-menu - - - - - root-menu - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - root-menu - - - - - - - - - - - - - - - - - - - - - - - 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 @@ - kitty --title "Buzznode" --override remember_window_size=no --override initial_window_width=100c --override initial_window_height=30c -e welcome + desktop-welcome @@ -103,7 +103,7 @@ - kitty -e ranger /var/log/buzznode + kitty -e ranger /var/log/launcher-desktop 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 '