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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions .github/workflows/voltage-agent-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ name: Voltage images
# buzz-agent voltage/images/buzz-agent/Dockerfile → voltage-buzz-agent-staging
# buzz-sandbox-reaper voltage/images/buzz-sandbox-reaper/Dockerfile → voltage-buzz-sandbox-reaper-staging
# buzz-relay the upstream root Dockerfile (target runtime) → buzz-relay
# paseo-daemon voltage/images/paseo-daemon/Dockerfile → voltage-paseo-daemon-staging
#
# The changes job selects only the images whose inputs changed. Manual runs
# (workflow_dispatch) build all three, or only buzz-agent with agent_only.
# (workflow_dispatch) build all of them, or only buzz-agent with agent_only.
# Paseo publishes to staging ECR after provisioning in voltage-terraform #1029.
#
# Trust model: this repository and its logs are public. Pull requests run
# without AWS access. Only the publish and merge jobs request an OIDC token,
Expand Down Expand Up @@ -131,6 +133,15 @@ jobs:
- 'examples/**'
- 'voltage/images/buzz-sandbox-reaper/**'
- '.github/workflows/voltage-agent-images.yml'
paseo:
- '.cargo/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'crates/**'
- 'examples/**'
- 'voltage/images/paseo-daemon/**'
- '.github/workflows/voltage-agent-images.yml'
relay:
- 'Dockerfile'
- '.dockerignore'
Expand All @@ -157,16 +168,19 @@ jobs:
AGENT: ${{ steps.filter.outputs.agent }}
REAPER: ${{ steps.filter.outputs.reaper }}
RELAY: ${{ steps.filter.outputs.relay }}
PASEO: ${{ steps.filter.outputs.paseo }}
run: |
set -euo pipefail
selected=()
if [ "$ALL" = true ] || [ "$AGENT_ONLY" = true ] || [ "$AGENT" = true ]; then selected+=(buzz-agent); fi
if [ "$ALL" = true ] || [ "$REAPER" = true ]; then selected+=(buzz-sandbox-reaper); fi
if [ "$ALL" = true ] || [ "$RELAY" = true ]; then selected+=(buzz-relay); fi
if [ "$ALL" = true ] || [ "$PASEO" = true ]; then selected+=(paseo-daemon); fi
images='{
"buzz-agent": {"file": "voltage/images/buzz-agent/Dockerfile", "target": "", "repository": "voltage-buzz-agent-staging"},
"buzz-sandbox-reaper": {"file": "voltage/images/buzz-sandbox-reaper/Dockerfile", "target": "", "repository": "voltage-buzz-sandbox-reaper-staging"},
"buzz-relay": {"file": "Dockerfile", "target": "runtime", "repository": "buzz-relay"}
"buzz-relay": {"file": "Dockerfile", "target": "runtime", "repository": "buzz-relay"},
"paseo-daemon": {"file": "voltage/images/paseo-daemon/Dockerfile", "target": "", "repository": "voltage-paseo-daemon-staging"}
}'
platforms='[{"arch": "amd64", "runner": "ubuntu-latest"}, {"arch": "arm64", "runner": "ubuntu-24.04-arm"}]'
build=$(jq -cn --argjson images "$images" --argjson platforms "$platforms" \
Expand Down Expand Up @@ -202,13 +216,18 @@ jobs:
with:
dockerfile: voltage/images/buzz-sandbox-reaper/Dockerfile
failure-threshold: error
- name: Hadolint paseo-daemon
uses: hadolint/hadolint-action@06be81baf89a55ffd0e24b8f04a4185738dd3387 # v3.5.0
with:
dockerfile: voltage/images/paseo-daemon/Dockerfile
failure-threshold: error
# --check resolves every COPY --from source, so the agent image needs its
# engineering-skills context; the other images ignore it.
- name: BuildKit checks
run: |
set -euo pipefail
sh voltage/images/buzz-agent/select-skills.sh --none "$RUNNER_TEMP/engineering-skills"
for file in voltage/images/buzz-agent/Dockerfile voltage/images/buzz-sandbox-reaper/Dockerfile Dockerfile; do
for file in voltage/images/buzz-agent/Dockerfile voltage/images/buzz-sandbox-reaper/Dockerfile voltage/images/paseo-daemon/Dockerfile Dockerfile; do
echo "==> $file"
docker buildx build --check --build-context engineering-skills="$RUNNER_TEMP/engineering-skills" -f "$file" .
done
Expand Down Expand Up @@ -264,6 +283,9 @@ jobs:
- name: Relay smoke test
if: matrix.image == 'buzz-relay'
run: bash voltage/images/test-relay-image.sh "$IMAGE"
- name: Paseo daemon contract test
if: matrix.image == 'paseo-daemon'
run: bash voltage/images/paseo-daemon/test-image.sh "$IMAGE"
- name: Image contract tests
if: matrix.image == 'buzz-agent'
run: |
Expand Down Expand Up @@ -417,6 +439,9 @@ jobs:
- name: Relay smoke test
if: matrix.image == 'buzz-relay'
run: bash voltage/images/test-relay-image.sh "$IMAGE"
- name: Paseo daemon contract test
if: matrix.image == 'paseo-daemon'
run: bash voltage/images/paseo-daemon/test-image.sh "$IMAGE"
- name: Image contract test
if: matrix.image == 'buzz-agent'
env:
Expand Down
7 changes: 4 additions & 3 deletions voltage/images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,20 @@ The same workflow also builds the Buzz relay image from the upstream root `Docke
| [buzz-agent](buzz-agent/README.md) | Remote developer agent Sandbox: the `sprig` harness, Codex and Claude ACP adapters, and developer tools. It also runs as the `buzz-agent-gate` DaemonSet. | `voltage-buzz-agent-staging` |
| [buzz-sandbox-reaper](buzz-sandbox-reaper/Dockerfile) | In-cluster reaper for ended and orphaned agent Sandboxes (`buzz-sandbox-reaper reconcile`) | `voltage-buzz-sandbox-reaper-staging` |
| [buzz-relay](#the-relay-image) | Buzz relay, admin, and pair relay binaries with the web bundles, from the root `Dockerfile` | `buzz-relay` |
| [paseo-daemon](paseo-daemon/README.md) | Paseo daemon that runs a developer's personal assistant in front of Buzz remote agents | `voltage-paseo-daemon-staging` |

Each image compiles the Buzz crates of the commit it is built from.
The build context is the repository root.
For the two agent images, BuildKit reads the `Dockerfile.dockerignore` next to each Dockerfile instead of the root `.dockerignore`.
These files keep only the Cargo workspace and, for `buzz-agent`, the files that its Dockerfile copies from `voltage/images/buzz-agent`.
For the agent images and `paseo-daemon`, BuildKit reads the `Dockerfile.dockerignore` next to each Dockerfile instead of the root `.dockerignore`.
These files keep only the Cargo workspace and the files that each Dockerfile copies from its own directory.

## Publishing

[`.github/workflows/voltage-agent-images.yml`](../../.github/workflows/voltage-agent-images.yml) runs when a change touches the files that an image reads.
Its first job selects only the images whose inputs changed; manual runs build all of them.

- Pull requests: hadolint and BuildKit checks, then a build of each selected image for `linux/amd64` and `linux/arm64` on native runners, without a push.
The workflow tests the agent image on both platforms, runs the reaper binary, and smoke-tests the relay image with [`test-relay-image.sh`](test-relay-image.sh).
The workflow tests the agent image on both platforms, runs the reaper binary, smoke-tests the relay image with [`test-relay-image.sh`](test-relay-image.sh), and runs [`paseo-daemon/test-image.sh`](paseo-daemon/test-image.sh).
These jobs have no AWS access.
- `main`: each platform builds on its native runner and is pushed by digest.
A merge job then creates one manifest list per image, tagged with the full commit SHA.
Expand Down
82 changes: 82 additions & 0 deletions voltage/images/paseo-daemon/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# syntax=docker/dockerfile:1.7
# Headless Paseo daemon (paseo.sh) that runs one developer's personal
# assistant (PA) in front of their Buzz agents. The daemon is the unmodified
# upstream image. This image adds the `buzz` CLI, the PA agent CLIs, the PA
# instructions, and a wrapper that writes Paseo's config from the environment.
# See README.md next to this file.
# Build from the repository root; Dockerfile.dockerignore next to this file
# selects the context.
FROM rust:1.95-alpine3.22@sha256:064dfc925d68d1a63f4fd2871bd7dc6e6ea56692989a487185855d62885d90aa AS builder
RUN apk add --no-cache build-base cmake git musl-dev perl pkgconf

Check warning on line 10 in voltage/images/paseo-daemon/Dockerfile

View workflow job for this annotation

GitHub Actions / Lint Dockerfiles

Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`
WORKDIR /build
ENV CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2
# Only the Cargo workspace: a change to the wrapper or the docs does not
# rebuild the CLI.
COPY .cargo .cargo
COPY Cargo.toml Cargo.lock rust-toolchain.toml ./
COPY crates crates
COPY examples examples
COPY scripts scripts
COPY test-fixtures test-fixtures
# The musl target links statically, so the binary runs on the Debian base.
# The target directory is removed in the same layer to keep the cache small.
RUN cargo build --locked --release -p buzz-cli --bin buzz \
&& install -D -m 0755 -s target/release/buzz /out/buzz \
&& rm -rf target

# ghcr.io/getpaseo/paseo:0.15.3 (linux/amd64 and linux/arm64)
FROM ghcr.io/getpaseo/paseo@sha256:57048c43a2c2988434273c7f8a4b25aa585bca6c1720493ae52e2fa91497b99a
LABEL maintainer="Voltage <hello@voltage.cloud>"
USER root
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# Codex is the PA fallback when only CODEX_ACCESS_TOKEN is present. The
# version is pinned by package-lock.json, the same as in the buzz-agent image.
WORKDIR /opt/paseo-buzz/runtimes
COPY voltage/images/paseo-daemon/package.json voltage/images/paseo-daemon/package-lock.json ./
RUN npm ci --omit=dev --no-audit --no-fund \
&& npm cache clean --force \
&& rm -rf /root/.npm \
&& ln -s /opt/paseo-buzz/runtimes/node_modules/.bin/codex /usr/local/bin/codex \
&& HOME=/tmp codex --version | grep -qx 'codex-cli 0.159.3'
# The Claude Code CLI is the binary that the daemon's Claude Agent SDK bundles
# for this platform (Claude Code 2.1.246 with SDK 0.3.246 in Paseo 0.15.3), so
# the base digest pins it and it matches the SDK. Paseo's claude provider
# runs `claude` from PATH.
RUN shopt -s nullglob \
&& binaries=(/usr/local/lib/node_modules/@getpaseo/server/node_modules/@anthropic-ai/claude-agent-sdk-linux-*/claude) \
&& if [ "${#binaries[@]}" -ne 1 ]; then echo "expected one bundled Claude Code binary, found: ${binaries[*]}" >&2; exit 1; fi \
&& ln -s "${binaries[0]}" /usr/local/bin/claude \
&& HOME=/tmp claude --version | grep -qx '2.1.246 (Claude Code)'
# Host tools off (BUZZ_PASEO_HOST_TOOLS=off): the wrapper links the daemon's
# terminal worker directory to the empty, root-owned no-terminal-worker
# directory, and puts host-tools-off first on PATH, where `git` always fails.
RUN install -d -o root -g root -m 0555 /opt/paseo-buzz/no-terminal-worker \
&& install -d -o root -g root -m 0755 /opt/paseo-buzz/host-tools-off /etc/paseo-buzz \
&& printf '%s\n' '#!/bin/sh' \
'echo "git is not available: host tools are off (BUZZ_PASEO_HOST_TOOLS=off)" >&2' \
'exit 1' >/opt/paseo-buzz/host-tools-off/git \
&& chmod 0755 /opt/paseo-buzz/host-tools-off/git
COPY --from=builder /out/buzz /usr/local/bin/buzz
COPY voltage/images/paseo-daemon/recorder /opt/paseo-buzz/recorder
COPY --chmod=0644 voltage/images/paseo-daemon/assistant.md /etc/paseo-buzz/assistant.md
COPY --chmod=0755 voltage/images/paseo-daemon/entrypoint.sh /usr/local/bin/paseo-buzz-entrypoint
# Relay-only and TLS-only. The listener is loopback, for the in-container
# CLI, the agent MCP server, and the health check. Clients, including the web
# app at app.paseo.sh, reach the daemon only through the relay. The daemon's
# own web UI and the speech models are off (dictation downloads about 631 MB
# at first start). The wrapper refuses to start without TLS. Claude Code does
# not update itself or send telemetry.
ENV PASEO_LISTEN=127.0.0.1:6767 \
PASEO_RELAY_ENABLED=true \
PASEO_RELAY_ENDPOINT=buzz.voltage.cloud:443 \
PASEO_RELAY_USE_TLS=true \
PASEO_RELAY_PUBLIC_USE_TLS=true \
PASEO_WEB_UI_ENABLED=false \
PASEO_DICTATION_ENABLED=false \
BUZZ_RELAY_URL=wss://buzz.voltage.cloud \
BUZZ_PASEO_HOST_TOOLS=off \
DISABLE_AUTOUPDATER=1 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
USER 1000:1000
WORKDIR /home/paseo
ENTRYPOINT ["/usr/local/bin/paseo-buzz-entrypoint"]
91 changes: 91 additions & 0 deletions voltage/images/paseo-daemon/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Build outputs
target/
**/target/
desktop/src-tauri/target/

# Native non-relay UI builds (not part of the public image)
desktop/
mobile/

# Node artifacts (built inside the image; host outputs would invalidate cache)
node_modules/
**/node_modules/
web/dist/

# VCS, IDE, scratch
.git/
.github/
.gitignore
.gitattributes
.vscode/
.idea/
.scratch/

# Local environment / secrets — never want these in the build context
.env
.env.*
!.env.example
*.pem
*.key
secrets/

# OS junk
.DS_Store
Thumbs.db

# CI artifacts and tooling that the image doesn't need
.cache/
coverage/
dist/
playwright-report/
test-results/
bin/
sidecars/

# Docs the image doesn't ship. examples/ stays — examples/countdown-bot is
# a Cargo workspace member, so cargo-chef needs its manifest. Markdown files
# inside crates (Cargo.toml `readme = ...`) likewise need to be present to
# avoid manifest warnings; the savings from excluding them are negligible.
docs/

# Old Dockerfile noise (this Dockerfile is THE source of truth now)
docker-compose.yml
docker-compose.*.yml
prometheus.yml

# --- Voltage agent images ---------------------------------------------------
# The lines above copy the root .dockerignore. The lines below also exclude
# what `cargo build --locked` does not read. Keep the workspace manifest and
# lockfile, .cargo/, rust-toolchain.toml, every root workspace member (crates/,
# examples/), and scripts/ and test-fixtures/, which crates include at
# compile time.
web/
apps/
admin-web/
benchmarks/
deploy/
migrations/
perf/
PLANS/
patches/
schema/
script/
.agents/
.claude/
.codex/
.config/
.goose/
.hermit/
.intersect/
.release/
**/__pycache__/
voltage/
# This image also packages the wrapper, the PA instructions, and the PA
# runtime pins. The other files in voltage/images/paseo-daemon (tests, docs)
# stay out of the context.
!voltage/images/paseo-daemon/entrypoint.sh
!voltage/images/paseo-daemon/assistant.md
!voltage/images/paseo-daemon/package.json
!voltage/images/paseo-daemon/package-lock.json

!voltage/images/paseo-daemon/recorder/**
Loading
Loading