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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 123 additions & 10 deletions .github/workflows/voltage-agent-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@ name: Voltage images
# buzz-relay the upstream root Dockerfile (target runtime) → buzz-relay
#
# The changes job selects only the images whose inputs changed. Manual runs
# (workflow_dispatch) build all three.
# (workflow_dispatch) build all three, or only buzz-agent with agent_only.
#
# 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,
# and they run only on main, the only ref that the github-buzz-ecr-push role
# trusts. The AWS_ACCOUNT_ID secret keeps the account ID out of the
# repository and masks it in the logs.
#
# Engineering skills: the agent image ships voltagecloud/engineering-skills
# (internal) from that repository's master branch at build time. Only the
# publish job reads it, with a short-lived, read-only token from the
# voltage-agent-images GitHub App. The app's client ID and key are secrets of
# the `engineering-skills` environment, which only main may use, so pull
# requests never receive them; they build with an empty skills context.
# select-skills.sh prints counts only, never skill content.
on:
push:
branches: [main]
Expand Down Expand Up @@ -61,7 +69,12 @@ on:
- admin-web/**
- .github/workflows/voltage-agent-images.yml
# Publishes only when run on main; on other refs it builds and tests.
workflow_dispatch: {}
workflow_dispatch:
inputs:
agent_only:
description: "Build only buzz-agent, for example to ship new engineering skills"
type: boolean
default: false

# Pull requests share one group per ref and cancel superseded runs. Each main
# commit gets its own group: a queued main run that a newer push replaced
Expand All @@ -84,6 +97,7 @@ jobs:
outputs:
any: ${{ steps.select.outputs.any }}
build: ${{ steps.select.outputs.build }}
agent: ${{ steps.select.outputs.agent }}
merge: ${{ steps.select.outputs.merge }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
Expand Down Expand Up @@ -138,14 +152,15 @@ jobs:
- name: Build the image matrices
id: select
env:
ALL: ${{ github.event_name == 'workflow_dispatch' }}
ALL: ${{ github.event_name == 'workflow_dispatch' && !inputs.agent_only }}
AGENT_ONLY: ${{ github.event_name == 'workflow_dispatch' && inputs.agent_only }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
AGENT: ${{ steps.filter.outputs.agent }}
REAPER: ${{ steps.filter.outputs.reaper }}
RELAY: ${{ steps.filter.outputs.relay }}
run: |
set -euo pipefail
selected=()
if [ "$ALL" = true ] || [ "$AGENT" = true ]; then selected+=(buzz-agent); fi
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
images='{
Expand All @@ -162,6 +177,7 @@ jobs:
--args "${selected[@]}")
{
echo "any=$([ "${#selected[@]}" -gt 0 ] && echo true || echo false)"
echo "agent=$([[ " ${selected[*]} " == *" buzz-agent "* ]] && echo true || echo false)"
echo "build=$build"
echo "merge=$merge"
} >> "$GITHUB_OUTPUT"
Expand All @@ -186,12 +202,15 @@ jobs:
with:
dockerfile: voltage/images/buzz-sandbox-reaper/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
echo "==> $file"
docker buildx build --check -f "$file" .
docker buildx build --check --build-context engineering-skills="$RUNNER_TEMP/engineering-skills" -f "$file" .
done

# Pull requests, and runs on refs other than main: build and test, no push.
Expand Down Expand Up @@ -221,6 +240,9 @@ jobs:
buildkitd-config-inline: |
[worker.oci]
max-parallelism = 2
# Pull requests never read the internal skills repository.
- name: Empty engineering skills context
run: sh voltage/images/buzz-agent/select-skills.sh --none "$RUNNER_TEMP/engineering-skills"
- name: Build
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
Expand All @@ -229,6 +251,7 @@ jobs:
target: ${{ matrix.target }}
platforms: linux/${{ matrix.arch }}
build-args: ${{ matrix.image == 'buzz-relay' && env.RELAY_BUILD_ARGS || '' }}
build-contexts: engineering-skills=${{ runner.temp }}/engineering-skills
load: true
push: false
tags: ${{ env.IMAGE }}
Expand Down Expand Up @@ -261,14 +284,53 @@ jobs:
python3 test-codex-login.py "$IMAGE"
python3 -O test-profile.py "$IMAGE"
python3 test-cluster.py "$IMAGE"
python3 test-skills.py "$IMAGE"

# main: resolve engineering-skills master once, so every platform image of
# this run carries the same skills commit.
skills:
name: Resolve engineering skills
needs: changes
if: needs.changes.outputs.agent == 'true' && github.repository == 'voltagecloud/buzz' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 5
environment: engineering-skills
outputs:
revision: ${{ steps.resolve.outputs.revision }}
steps:
- name: Engineering skills token
id: token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ secrets.AGENT_IMAGE_APP_CLIENT_ID }}
private-key: ${{ secrets.AGENT_IMAGE_APP_PRIVATE_KEY }}
owner: voltagecloud
repositories: engineering-skills
permission-contents: read
- name: Resolve master
id: resolve
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
run: |
set -euo pipefail
revision=$(gh api repos/voltagecloud/engineering-skills/commits/master --jq .sha)
[[ "$revision" =~ ^[0-9a-f]{40}$ ]]
echo "revision=$revision" >> "$GITHUB_OUTPUT"
echo "engineering skills: master is $revision"

# main: build each platform on its native runner and push it by digest.
publish:
name: Publish ${{ matrix.image }} (${{ matrix.arch }})
needs: [changes, lint]
if: needs.changes.outputs.any == 'true' && github.repository == 'voltagecloud/buzz' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
# A skills failure fails only the agent image: the relay and reaper still
# publish. skills is skipped when this run does not build the agent.
needs: [changes, lint, skills]
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.lint.result == 'success' && needs.skills.result != 'cancelled' && needs.changes.outputs.any == 'true' && github.repository == 'voltagecloud/buzz' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
# Only the agent image needs the engineering-skills app secrets, which this
# main-only environment holds. The github-buzz-ecr-push role trusts its
# OIDC subject; the other images keep the main branch subject.
environment: ${{ matrix.image == 'buzz-agent' && 'engineering-skills' || '' }}
permissions:
contents: read
id-token: write # OIDC token for the github-buzz-ecr-push role
Expand All @@ -290,6 +352,48 @@ jobs:
buildkitd-config-inline: |
[worker.oci]
max-parallelism = 2
- name: Require the resolved skills commit
if: matrix.image == 'buzz-agent'
env:
REVISION: ${{ needs.skills.outputs.revision }}
run: |
[[ "$REVISION" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::engineering skills were not resolved (skills job: ${{ needs.skills.result }})"; exit 1; }
# Read-only, for engineering-skills alone, and revoked when the job ends.
- name: Engineering skills token
id: skills-token
if: matrix.image == 'buzz-agent'
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ secrets.AGENT_IMAGE_APP_CLIENT_ID }}
private-key: ${{ secrets.AGENT_IMAGE_APP_PRIVATE_KEY }}
owner: voltagecloud
repositories: engineering-skills
permission-contents: read
- name: Check out engineering skills
if: matrix.image == 'buzz-agent'
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: voltagecloud/engineering-skills
ref: ${{ needs.skills.outputs.revision }}
token: ${{ steps.skills-token.outputs.token }}
path: .engineering-skills
persist-credentials: false
- name: Select engineering skills
id: skills
env:
AGENT: ${{ matrix.image == 'buzz-agent' }}
EXPECTED: ${{ needs.skills.outputs.revision }}
run: |
set -euo pipefail
out="$RUNNER_TEMP/engineering-skills"
if [ "$AGENT" = true ]; then
sh voltage/images/buzz-agent/select-skills.sh .engineering-skills "$out"
rm -rf .engineering-skills
[ "$(cat "$out/REVISION")" = "$EXPECTED" ]
else
sh voltage/images/buzz-agent/select-skills.sh --none "$out"
fi
echo "revision=$(cat "$out/REVISION")" >> "$GITHUB_OUTPUT"
# Build and smoke-test before credentials exist; the push build below
# reuses this build's cache.
- name: Build
Expand All @@ -300,6 +404,7 @@ jobs:
target: ${{ matrix.target }}
platforms: linux/${{ matrix.arch }}
build-args: ${{ matrix.image == 'buzz-relay' && env.RELAY_BUILD_ARGS || '' }}
build-contexts: engineering-skills=${{ runner.temp }}/engineering-skills
load: true
push: false
tags: ${{ env.IMAGE }}
Expand All @@ -313,7 +418,11 @@ jobs:
run: bash voltage/images/test-relay-image.sh "$IMAGE"
- name: Image contract test
if: matrix.image == 'buzz-agent'
run: bash voltage/images/buzz-agent/test-image.sh "$IMAGE"
env:
REQUIRE_SKILLS: "1"
run: |
bash voltage/images/buzz-agent/test-image.sh "$IMAGE"
python3 voltage/images/buzz-agent/test-skills.py "$IMAGE"
- uses: aws-actions/configure-aws-credentials@e1253824e5c10ff9df46874f81ed3ec929e19cfd # v6.3.0
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-buzz-ecr-push
Expand All @@ -328,9 +437,11 @@ jobs:
target: ${{ matrix.target }}
platforms: linux/${{ matrix.arch }}
build-args: ${{ matrix.image == 'buzz-relay' && env.RELAY_BUILD_ARGS || '' }}
build-contexts: engineering-skills=${{ runner.temp }}/engineering-skills
labels: |
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.revision=${{ github.sha }}
cloud.voltage.engineering-skills.revision=${{ steps.skills.outputs.revision }}
outputs: type=image,name=${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-west-2.amazonaws.com/${{ matrix.repository }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
env:
Expand All @@ -350,7 +461,7 @@ jobs:
# A failed platform build fails only its own image's merge.
merge:
name: Merge ${{ matrix.image }} manifest
needs: [changes, publish]
needs: [changes, skills, publish]
if: ${{ !cancelled() && needs.publish.result != 'skipped' && needs.changes.outputs.any == 'true' && github.repository == 'voltagecloud/buzz' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
timeout-minutes: 15
Expand Down Expand Up @@ -380,7 +491,9 @@ jobs:
IMAGE: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-west-2.amazonaws.com/${{ matrix.repository }}
NAME: ${{ matrix.image }}
REPOSITORY: ${{ matrix.repository }}
TAG: ${{ github.sha }}
# An agent_only run republishes a commit that already has an image,
# so it tags the result with the skills commit as well.
TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.agent_only && format('{0}-skills-{1}', github.sha, needs.skills.outputs.revision) || github.sha }}
run: |
set -euo pipefail
if digest=$(aws ecr describe-images --repository-name "$REPOSITORY" \
Expand Down
6 changes: 4 additions & 2 deletions voltage/images/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ The voltage-terraform admission policy admits only digest-pinned images from `vo
Run from the repository root. The Rust build takes a long time; the Dockerfiles set `CARGO_BUILD_JOBS=2`.

```bash
docker buildx build --check -f voltage/images/buzz-agent/Dockerfile .
sh voltage/images/buzz-agent/select-skills.sh --none /tmp/engineering-skills
docker buildx build --check --build-context engineering-skills=/tmp/engineering-skills -f voltage/images/buzz-agent/Dockerfile .
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
docker buildx build --check -f voltage/images/buzz-sandbox-reaper/Dockerfile .

docker build --platform linux/amd64 -f voltage/images/buzz-sandbox-reaper/Dockerfile -t buzz-sandbox-reaper:local .
docker run --rm buzz-sandbox-reaper:local --help

docker build --platform linux/amd64 -f voltage/images/buzz-agent/Dockerfile -t buzz-agent:local .
docker build --platform linux/amd64 -f voltage/images/buzz-agent/Dockerfile \
--build-context engineering-skills=/tmp/engineering-skills -t buzz-agent:local .
bash voltage/images/buzz-agent/test-image.sh buzz-agent:local
```

Expand Down
6 changes: 6 additions & 0 deletions voltage/images/buzz-agent/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
# Build from the repository root so the image packages the checked-out Buzz
# source; Dockerfile.dockerignore next to this file selects the context.
FROM rust:1.95-alpine3.22@sha256:064dfc925d68d1a63f4fd2871bd7dc6e6ea56692989a487185855d62885d90aa AS source
RUN apk add --no-cache bash build-base cmake git musl-dev openssl-dev \

Check warning on line 5 in voltage/images/buzz-agent/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>`
openssl-libs-static perl pkgconf protoc python3 procps
WORKDIR /build
ENV CARGO_INCREMENTAL=0 CARGO_BUILD_JOBS=2
COPY . .
FROM source AS package-checks
RUN apk add --no-cache tini \

Check warning on line 11 in voltage/images/buzz-agent/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>`
&& tini -s -- cargo test --locked -p sprig -p buzz-acp -p buzz-agent -p buzz-dev-mcp -- --test-threads=1

FROM source AS builder
Expand All @@ -19,7 +19,7 @@

FROM node:22-trixie-slim@sha256:154ba2f4d6fec323d28e4f4bb86bba4677f1223391a1979cf521304e03a98dfa
LABEL maintainer="Voltage <hello@voltage.cloud>"
RUN apt-get update \

Check warning on line 22 in voltage/images/buzz-agent/Dockerfile

View workflow job for this annotation

GitHub Actions / Lint Dockerfiles

Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
&& apt-get install -y --no-install-recommends bash ca-certificates curl git gh gnupg openssh-client socat tini \
build-essential clang cmake pkg-config libssl-dev protobuf-compiler \
python3 python3-venv python3-pip golang-go postgresql-client sqlite3 \
Expand All @@ -33,7 +33,7 @@
# Release assets are public and pinned by checksum per architecture;
# credentials enter only at runtime.
ARG TARGETARCH
RUN case "$TARGETARCH" in \

Check warning on line 36 in voltage/images/buzz-agent/Dockerfile

View workflow job for this annotation

GitHub Actions / Lint Dockerfiles

Set the SHELL option -o pipefail before RUN with a pipe in it. If you are using /bin/sh in an alpine image or if your shell is symlinked to busybox then consider explicitly setting your SHELL to /bin/ash, or disable this check
amd64) via=x86_64 gitleaks=x64 sccache=x86_64 \
kubectl_sha=874d5e72dbb819f43cff16bcd1e4f8bac5b7f2361fe1e55049b0a6c676fb0cbf \
via_sha=9e5a7f9384cacda29061d814e9397e7b6d90a72ee4663775362978ac9b65d8b7 \
Expand Down Expand Up @@ -92,6 +92,12 @@
RUN install -d -m 0755 /etc/buzz-agent
COPY --chmod=0644 voltage/images/buzz-agent/subagents.md /etc/buzz-agent/subagents.md
COPY --chmod=0644 voltage/images/buzz-agent/cluster.md /etc/buzz-agent/cluster.md
# Voltage engineering skills from the `engineering-skills` build context, made by
# select-skills.sh: master of voltagecloud/engineering-skills on main, empty for
# pull requests and local builds. Root owns the copy, so the agent can read but
# not change it; profile.py links each skill into the harnesses.
# hadolint ignore=DL3022
COPY --from=engineering-skills . /opt/engineering-skills/
COPY --chmod=0755 voltage/images/buzz-agent/heavy-bin/ /opt/buzz-heavy/bin/
COPY --chmod=0755 voltage/images/buzz-agent/gh.sh /usr/local/bin/gh
COPY --chmod=0755 voltage/images/buzz-agent/via.sh /usr/local/bin/via
Expand Down
28 changes: 27 additions & 1 deletion voltage/images/buzz-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ The build context is the repository root. `Dockerfile.dockerignore` next to the
From the repository root:

```bash
docker build --platform linux/amd64 -f voltage/images/buzz-agent/Dockerfile -t buzz-agent:local .
sh voltage/images/buzz-agent/select-skills.sh --none /tmp/engineering-skills
docker build --platform linux/amd64 -f voltage/images/buzz-agent/Dockerfile \
--build-context engineering-skills=/tmp/engineering-skills -t buzz-agent:local .
cd voltage/images/buzz-agent
bash test-image.sh buzz-agent:local
bash test-harness.sh buzz-agent:local
Expand All @@ -42,13 +44,37 @@ python3 test-worker.py buzz-agent:local
python3 test-cache.py buzz-agent:local
python3 -O test-profile.py buzz-agent:local
python3 test-cluster.py buzz-agent:local
python3 test-skills.py buzz-agent:local
```

To build with the engineering skills, replace `--none` with a local checkout:
`sh voltage/images/buzz-agent/select-skills.sh ../engineering-skills /tmp/engineering-skills`.

`test-image.sh` also runs `test-review.py`; run the Python script directly for focused review checks.
These checks use synthetic tasks and accounts. Live GitHub, signing, `via`, cache, and staging behavior require end-to-end validation.
Run the bundled Rust suites with `docker build --platform linux/amd64 --target package-checks -f voltage/images/buzz-agent/Dockerfile .`.
Build and test `linux/arm64` on an arm64 host, or through `.github/workflows/voltage-agent-images.yml`, which builds each platform on a native runner.

## Engineering skills

The image ships [voltagecloud/engineering-skills](https://github.com/voltagecloud/engineering-skills) (internal) in `/opt/engineering-skills`, owned by root.
That repository is internal and this one is public, so the skills reach the build only as the `engineering-skills` [build context](https://docs.docker.com/build/building/context/#named-contexts).
`select-skills.sh` makes that context. It leaves out `walkthrough-video` and `cli-demo-video`, which need a browser, narration, and VHS, and writes the source commit to `REVISION`.

| Build | Skills |
| --- | --- |
| `main` (publish job) | `master` of engineering-skills at build time; the image label `cloud.voltage.engineering-skills.revision` records the commit |
| Pull requests, other refs, local builds | None (`select-skills.sh --none`) |

The publish job reads the repository with a short-lived token from the `voltage-agent-images` GitHub App: read-only contents, limited to engineering-skills.
The app's client ID and private key are the `AGENT_IMAGE_APP_CLIENT_ID` and `AGENT_IMAGE_APP_PRIVATE_KEY` secrets of the `engineering-skills` environment, which only `main` may use.
A skills change reaches agents with the next published agent image. To publish one without a code change, run the `Voltage images` workflow on `main` with `agent_only`. That image is tagged `<buzz commit>-skills-<engineering-skills commit>`, because the Buzz commit already has an image.
Each run resolves `master` once (the `skills` job), so both platform images carry the same skills commit.

At each session and worker start, `buzz-agent-profile` links every skill into `~/.claude/skills`, `$CODEX_HOME/skills`, and `~/.agents/skills`.
It manages only its own links: a skill the agent wrote keeps its name, and links to skills a newer image dropped are removed.
A linking failure prints a warning; the session still starts.

## Sandbox contract

Use an immutable registry digest in `spec.podTemplate.spec.containers[].image`.
Expand Down
Loading
Loading