https://github.com/pdparchitect/buzznode
A persistent, browser-accessible Linux computer for a single Buzz agent — its own desktop, terminal, filesystem, and long-lived state, joining any Buzz workspace over its relay
https://github.com/pdparchitect/buzznode
acp agent-runtime ai-agents buzz buzz-xyz claude-code codex containers docker goose podman remote-desktop self-hosted
Last synced: 4 days ago
JSON representation
A persistent, browser-accessible Linux computer for a single Buzz agent — its own desktop, terminal, filesystem, and long-lived state, joining any Buzz workspace over its relay
- Host: GitHub
- URL: https://github.com/pdparchitect/buzznode
- Owner: pdparchitect
- Created: 2026-07-26T16:13:11.000Z (13 days ago)
- Default Branch: main
- Last Pushed: 2026-07-31T01:45:24.000Z (9 days ago)
- Last Synced: 2026-07-31T03:14:37.045Z (9 days ago)
- Topics: acp, agent-runtime, ai-agents, buzz, buzz-xyz, claude-code, codex, containers, docker, goose, podman, remote-desktop, self-hosted
- Language: Shell
- Size: 156 KB
- Stars: 4
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
Awesome Lists containing this project
README
# Buzznode

Buzznode is one persistent, browser-accessible Linux computer for one
[Buzz](https://github.com/block/buzz) agent. It joins an existing Buzz
workspace and runs that agent through the headless `buzz-acp` harness.
Buzznode is not a Buzz workspace client. It does not contain Buzz Desktop, and
it does not run a relay, database, object store, or other server-side Buzz
service. Use [Buzzbox](https://github.com/pdparchitect/buzzbox) or another Buzz
client to manage the workspace, create agents, and communicate with them. Use
Buzznode to give one of those agents a dedicated browser, terminal, filesystem,
runtime login, and long-lived state.
> **Not an official Buzz project.** Buzznode is an independent, community-built
> environment that packages published Buzz releases. It is not affiliated with,
> endorsed by, or sponsored by the Buzz project, [buzz.xyz](https://buzz.xyz),
> or Block, Inc. "Buzz" is used here only to describe what this image runs and
> what it is compatible with; all trademarks belong to their respective owners.
> Report problems with Buzznode here, not to the upstream Buzz project.

## Quick start
You need credentials for a managed agent — either a `buzznode-v1:` enrollment
bundle, which [Buzzbox](https://github.com/pdparchitect/buzzbox) produces, or
the agent's relay URL and private key from any other Buzz client. Buzznode
publishes native AMD64 and ARM64 images.
### Launcher (recommended)
The easiest way to run Buzznode is with
[Launcher](https://github.com/pdparchitect/launcher). Launcher discovers,
installs, starts, stops, and updates Buzznode while managing its container,
ports, and persistent storage for you.
1. [Download the latest Launcher release](https://github.com/pdparchitect/launcher/releases/latest).
2. Open **Marketplace**, select **Buzznode**, and install it.
3. Choose **Open agent** when installation finishes, then paste your enrollment
bundle into the setup window.
Launcher uses Apple `container` on macOS and Docker on Linux.
### Run the container manually
Choose the container runtime installed on your host.
#### Docker
```bash
docker run --detach \
--name buzznode \
--shm-size 1g \
--publish 127.0.0.1:6904:6901 \
ghcr.io/pdparchitect/buzznode:latest
```
#### Podman
```bash
podman run --detach \
--name buzznode \
--shm-size 1g \
--publish 127.0.0.1:6904:6901 \
ghcr.io/pdparchitect/buzznode:latest
```
#### Apple container
Apple's [`container`](https://github.com/apple/container) tool requires Apple
silicon and macOS 26 or later. Start its service once, then give the browser
desktop and agent harness enough memory:
```bash
container system start
container run --detach \
--name buzznode \
--memory 4g \
--shm-size 1g \
--publish 127.0.0.1:6904:6901 \
ghcr.io/pdparchitect/buzznode:latest
```
Open . The setup window asks for the bundle; paste it, or
press Enter to type the relay URL and private key instead. Pick Codex, Claude
Code, or Goose, and the node connects to your relay and starts handling messages
for that agent.
Docker and Podman select the host's native image automatically; Apple
`container` selects ARM64 on Apple silicon. These commands are for trying the
node out: its state lives in anonymous volumes, so replacing the container
loses the enrollment and leaves the old volumes behind on disk. For anything
you intend to keep, use the
[persistent setup](#persistent-setup) below.
## Setup in detail
Create or select a managed agent in the Buzz client associated with your
relay. You need either:
- a `buzznode-v1:` enrollment bundle; or
- the relay URL, agent private key, authorization, and response policy for
manual setup.
When using Buzzbox, its convenience flow is:
1. Right-click the Buzzbox desktop and choose **Agent Setup → Create New Agent
for Buzznode**.
2. Complete Buzz's managed-agent form and save it.
3. Copy the `buzznode-v1:` enrollment bundle displayed by Buzzbox.
To use an existing agent instead, stop its local harness and choose **Agent
Setup → Move Existing Agent to Buzznode**.
When using another Buzz workspace, obtain the managed agent credentials through
that workspace's client or provisioning process. Buzznode does not contact,
discover, or require a Buzzbox instance.
Once the container is running, the first desktop opens a larger terminal setup
window. Paste the enrollment bundle, then choose a Codex, Claude Code, or Goose
runtime. The wizard confirms completion and waits for Enter before becoming a
normal Buzznode terminal; it does not close the window. The bundle supplies:
- the workspace relay WebSocket URL, such as `wss://buzz.example.com`;
- the agent private key;
- its authorization tag; and
- whether anyone, its owner, an allowlist, or nobody may activate it.
Buzznode separately asks for an optional relay API token because Buzzbox does
not store that token with the managed agent.
Buzznode validates the bundle's relay, private key, authorization, and response
policy before reporting that enrollment was accepted.
The enrollment bundle is base64-encoded, not encrypted, and contains the agent
private key. Treat it like a password and paste it only into Buzznode setup.
Once the node is connected, do not run the same agent's local harness in
Buzzbox.
The wizard stores connection credentials in
`~/.config/buzznode/environment` with mode `0600`, offers to configure the
selected runtime, and starts `buzz-acp`. Buzznode discovers the channels that
contain this agent and handles messages addressed to it.
### Persistent setup
A long-lived node should survive being recreated, so name its volumes and give
it a restart policy. If you already started the container above, remove it and
its anonymous volumes first with `docker rm --force --volumes buzznode`.
```bash
docker run --detach \
--name buzznode \
--restart unless-stopped \
--shm-size 1g \
--publish 127.0.0.1:6904:6901 \
--volume buzznode-workspace:/workspace \
--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
```
Podman accepts the same command with `podman` in place of `docker`. With
Apple's tool, use `container` in place of `docker`, remove
`--restart unless-stopped`, and add `--memory 4g`. Apple `container` preserves
the named volumes and stopped container but does not expose a Docker-style
restart policy; restart it with `container start buzznode`.
Setup then runs once. Enrollment, runtime login, browser sessions, and working
files stay put across `docker rm` and image upgrades. See
[Persistence](#persistence) for what each volume holds, and
[One node, one agent](#one-node-one-agent) for running more than one.
### Unattended provisioning
To skip the wizard, configure the node from its terminal:
```bash
printf '%s\n' "$BUZZNODE_ENROLLMENT_BUNDLE" |
buzznode configure --enrollment-stdin --runtime codex
buzznode runtime-login
buzznode start
```
Manual `--relay-url`, `--private-key`, `--auth-tag`, `--respond-to`, and
`--respond-to-allowlist` options remain available for non-Buzzbox clients. Use
`buzznode setup` for normal interactive configuration so secrets do not appear
in shell history. A raw key from `buzz-admin generate-key` is not a replacement
for creating a managed agent because it has no Buzz profile, owner attestation,
or channel membership.
## Deployment model
Buzznode has no runtime dependency on Buzzbox. It can connect to any compatible
Buzz relay that is reachable from the container, including a hosted relay over
`wss://`, a relay on another server, or a local development relay.
Buzzbox is only an optional onboarding convenience. Its Agent Setup menu can
create a managed agent and package the relay URL, identity, authorization, and
response policy into a `buzznode-v1:` enrollment bundle. Another Buzz client or
provisioning system can supply the same information instead.
The `buzz-local` Docker network, `ws://buzzbox:3000` relay address, and the two
projects' coordinated Makefile examples are strictly for local testing. They
are not part of the Buzznode architecture and are not required in deployment.
## How the pieces fit
```text
Any compatible Buzz workspace Independent Buzznode
--------------------------------- ---------------------------
Create and manage workspace Run one existing agent
Provide enrollment or credentials ----> Connect to its configured relay
Add agent to channels Run Codex, Claude, or Goose
Chat with and mention agent <---- Handle messages through buzz-acp
```
The `buzz` command-line tool remains in the node because `buzz-acp` makes it
available to the running agent for Buzz messaging and tools. The graphical
`buzz-desktop` application is deliberately absent and cannot be started.
## Test locally with Buzzbox
[Buzzbox](https://github.com/pdparchitect/buzzbox) is a ready-to-run Buzz
workspace — desktop, relay, and storage in one image — which makes it the
easiest way to exercise a node end to end. The two remain independent projects;
their Makefiles coordinate only through an optional Docker network and the relay
URL.
Clone Buzzbox next to this repository, then from the `buzzbox` directory:
```bash
make up \
BUZZ_NETWORK=buzz-local \
PUBLIC_RELAY_URL=ws://buzzbox:3000
```
Open Buzzbox at and choose **Agent Setup → Create New
Agent for Buzznode**. Complete the Buzz form and copy the enrollment bundle.
From this `buzznode` directory:
```bash
make up \
BUZZ_NETWORK=buzz-local \
RELAY_URL=ws://buzzbox:3000
```
Open Buzznode at . The relay URL is prefilled in the
setup wizard for manual setup, but the recommended flow is to paste the
Buzzbox enrollment bundle. Leave the local API token empty, select a runtime,
and complete its login.
Once setup is complete, verify the saved configuration and relay connection:
```bash
make connection-test
```
The first project started creates `buzz-local`; Docker DNS resolves `buzzbox`
on that network. Each Makefile still owns only its own container. `make stop`
in either directory does not stop or remove the other project.
## One node, one agent
Create another Buzznode container with a different container name and volume
prefix for another agent. Keeping nodes isolated gives each agent its own:
- Buzz identity and harness process;
- browser sessions and runtime credentials;
- desktop and filesystem state;
- `/workspace`; and
- start, stop, restart, and logs lifecycle.
## What is included
- the headless `buzz-acp`, `buzz`, `buzz-agent`, and `buzz-dev-mcp` tools;
- Codex with `codex-acp`;
- Claude Code with `claude-agent-acp`;
- Goose with native ACP support;
- Chrome on AMD64 or Chromium on ARM64 for login flows and browser-based agent
tasks;
- a terminal, Git, GitHub CLI, Docker CLI, Python, Node.js, pnpm, and common
development tools; and
- an Openbox desktop exposed through KasmVNC.
Buzznode contains no `buzz-desktop`, `buzz-relay`, PostgreSQL, Redis, or MinIO.
The desktop is about a quarter of the image. It is kept because the node is
meant to be inspectable, because runtime authentication needs a real browser,
and because it is the substrate for computer use: `scrot`, `xdotool`, `wmctrl`,
and a Chromium-family browser are already present, so an agent can drive the
same display a human watches through KasmVNC. See
[IMAGE-SIZE.md](IMAGE-SIZE.md) for the measured
breakdown and the reasoning, and run `make size-report` to reproduce it.
A purpose-built web application could have taken the desktop's place as the way
to reach the node: a terminal, a log view, and a file browser served over HTTP
would be far smaller than an X session. That was considered and set aside. It
would be a second product to design, build, secure, and maintain alongside the
node itself, which is not where the early effort belongs. More to the point, it
would not actually remove the graphical stack. Runtime authentication needs a
real browser, and computer use needs a real display with real input, so the
browser, Xvnc, the fonts, and the software GL renderer stay in the image either
way — and those are the bulk of the cost. Openbox, tint2, and the rest of the
desktop shell add roughly 22 MiB on top of components already being paid for.
Given that, the node lives off the land: it surfaces what is already installed
rather than reimplementing a thinner version of it.
## Node commands
```bash
buzznode setup
buzznode status
buzznode doctor
buzznode runtime-login
buzznode runtime-login codex device
buzznode start
buzznode stop
buzznode restart
buzznode logs
```
Right-click the desktop for the same agent controls, runtime login actions,
terminal, browser, task manager, and harness logs. Runtime login opens a
chooser instead of assuming browser authentication. Codex supports device code,
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_VNC_STATS` and
`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:
```bash
make check
make up
make smoke
```
The desktop opens at . Useful overrides include:
```bash
PORT=8080 make up
PLATFORM=linux/arm64 make build
DOCKER=podman make up
```
The Makefile selects `linux/amd64` or `linux/arm64` from the host by default.
`make stop` removes the node container but preserves its named volumes.
## Pinned components
| Component | Version |
| ------------------- | --------- |
| Buzz headless tools | `0.5.4` |
| Codex | `0.146.0` |
| Claude Code | `2.1.221` |
| Goose | `1.45.0` |
| Codex ACP adapter | `1.1.9` |
| Claude ACP adapter | `0.64.2` |
On AMD64, the Buzz `.deb` and Goose archive are SHA-256 verified during the
image build. Only the required headless Buzz binaries are extracted from the
`.deb`; the package and its desktop application are not installed. Upstream
does not publish a Linux ARM64 package, so ARM64 builds compile only those
headless tools from the pinned tag after verifying its exact Git commit. The
ARM64 Goose archive is independently SHA-256 verified.
## Persistence
Keep separate volumes for:
- `/workspace` — the node's working files;
- `~/.config` and `~/.local/share` — node, browser, desktop, and Goose state;
- `~/.buzz` — the Buzz agent nest;
- `~/.codex` — Codex state; and
- `~/.claude` — Claude Code state.
## Security
Buzznode is a trusted, single-user workstation:
- the saved agent private key can act as that agent;
- KasmVNC browser authentication and TLS are disabled;
- the `agent` user has passwordless sudo;
- coding agents can operate on `/workspace`;
- browser sessions and agent credentials persist in volumes; and
- Codex runs with `sandbox_mode = "danger-full-access"`, because its bubblewrap
sandbox cannot create a user namespace inside a container, so no sandbox mode
is enforceable here whatever is configured. The setting states what is true
rather than implying a boundary that does not exist; the boundary is the
container. Override with `BUZZNODE_CODEX_SANDBOX_MODE`.
The provided Makefile binds the desktop to `127.0.0.1`. Keep that default, or
put Buzznode behind authentication, TLS, and suitable network controls. Never
publish port `6901` directly to an untrusted network, expose the saved agent
key, or reuse a human Buzz private key as the node identity.
See [RELEASES.md](RELEASES.md) for the image release process.
### Authentication is delegated by design
Access to the desktop web application is not authenticated. KasmVNC is started
with `-disableBasicAuth` and TLS disabled, and the KasmVNC password written on
first boot exists only because KasmVNC checks that the file is there. It is not
an access control and should not be treated as one.
This is a deliberate decision, and built-in authentication is not planned. A
node reachable beyond loopback is expected to be fronted by whichever access
layer already suits its environment: a conventional reverse proxy, or
preferably a zero-trust access proxy such as Cloudflare Access or an equivalent
identity-aware proxy. Those systems already own identity, session lifetime,
device posture, revocation, and audit, and in an enterprise deployment they are
the layer that has to be satisfied regardless of what the node does.
Adding a second mechanism inside the node would not strengthen that
arrangement, it would compete with it: two session models to keep aligned, two
places to revoke an operator, and an open question about which one wins when
they disagree. Leaving the node unauthenticated keeps a single enforcement
point and a single path forward — the proxy is the front door, and there is no
second door to reason about or accidentally leave open.
The practical consequence is that the loopback bind is the only boundary until
an access layer is put in front of it. Treat exposing the node without one as
publishing an unauthenticated root shell, because that is what it is.