Compare commits

..
1 Commits
Author SHA1 Message Date
gregandClaude Opus 5.5 c75190d8c4 feat(builder): research, build and a worker for the AI builder
Validate game templates / validate (push) Successful in 19s
Validate game templates / boot-test (push) Skipped
Validate game templates / publish (push) Skipped
- research: searches SearXNG, reads the best pages through the reader, has the
  model list the server's variants, then checks every Docker image and Steam
  app it named for real before an admin sees it.
- build: first boot of the variant's images (skipping ones that need root),
  a draft by the model, then the draft deployed with the node-agent's
  BootTest like CI does; crashes, error lines and saves outside /server go
  back to the model, three boots at most.
- worker: polls the panel's builder queue (outbound only), runs research or
  build in a fresh clone, streams the output as the job log, and opens the
  result as a pull request from the bot account.
- Dockerfile.builder, a workflow publishing gbr-builder:latest, and
  BUILDER.md. Everything about where it runs comes from environment
  variables; deployment files live outside this public repo.

draft now keeps its conversation in a drafter, so build can send boot
failures back to the same model session. The boot-test runner comment no
longer names its VM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:51:29 -04:00
8 changed files with 32 additions and 106 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
name: Builder image name: Builder image
# Publishes the AI builder worker (gbrgames worker) used on the builder VM; watchtower there picks it up. # Publishes the AI builder worker image (gbrgames worker, see BUILDER.md).
on: on:
push: push:
branches: ["main"] branches: ["main"]
+1 -1
View File
@@ -62,7 +62,7 @@ jobs:
boot-test: boot-test:
needs: validate needs: validate
if: needs.validate.outputs.games != '' if: needs.validate.outputs.games != ''
# A dedicated runner on VM 116 whose game containers can reach the internet but not the LAN # A dedicated runner whose game containers can reach the internet but not private networks
runs-on: game-test runs-on: game-test
steps: steps:
- name: Checkout Code - name: Checkout Code
+27 -54
View File
@@ -1,15 +1,15 @@
# AI builder (gbrgames worker) # AI builder (gbrgames worker)
The panel's **AI builder** (Admin → Games → AI Config Generator) is run by a worker on our own hardware, The panel's **AI builder** (Admin → Games → AI Config Generator) is run by a worker we host ourselves.
not on the panel's droplet and not in CI: The panel only keeps a queue; every result is a pull request to this repo, which CI validates and
boot-tests like any other.
``` ```
Panel (droplet) Builder VM (home lab) Panel Builder worker (gbrgames worker, Docker)
─────────────── ───────────────────────────────────────────── ───── ─────────────────────────────────────────
admin starts a job ──► queued ◄─poll── gbrgames worker (Docker) admin starts a job ──► queued ◄─poll── research: web search + page reader + model
progress / log / variants ◄─post── research: SearXNG + page reader + Ollama (3090) progress / log / variants ◄─post── build: first boot → draft → boot with the node-agent
"Review draft PR" build: first boot → draft → boot with the node-agent "Review draft PR" → branch draft/<id>-<job> + PR as the bot ──► CI (validate + boot test)
→ branch draft/<id>-<job> + PR as gbr-bot ──► CI (validate + boot test)
``` ```
1. **Research** (`gbrgames research -name N`): searches the web, reads the best pages, and has the model list 1. **Research** (`gbrgames research -name N`): searches the web, reads the best pages, and has the model list
@@ -21,68 +21,41 @@ progress / log / variants ◄─post── research: SearXNG + page rea
draft by the model (`draft`, rules in `AUTHORING.md`), then the draft is deployed with the node-agent's draft by the model (`draft`, rules in `AUTHORING.md`), then the draft is deployed with the node-agent's
`BootTest`, the same check CI runs. Failures (crash, error lines, saves written outside `/server`) go `BootTest`, the same check CI runs. Failures (crash, error lines, saves written outside `/server`) go
back to the model, three boots at most. back to the model, three boots at most.
4. The worker pushes `games/<id>/` to a branch and opens a pull request as **gbr-bot**. A draft that 4. The worker pushes `games/<id>/` to a branch and opens a pull request as the bot account. A draft that
validates but never booted still gets a PR, titled `WIP:`, with what's left listed. Nothing reaches validates but never booted still gets a PR, titled `WIP:`, with what's left listed. Nothing reaches
players until a person merges it and enables the game. players until a person merges it and enables the game.
The worker only connects out (to the panel, Gitea, Ollama, SearXNG); nothing needs to reach the VM. The worker only connects out (to the panel, Gitea, the model and the search services).
## The VM ## Requirements
A Proxmox VM of its own, so builds never compete with CI boot tests on VM 126: Debian 13, 4+ cores A Docker host with the isolated `gbr-boottest` network (`RUNNER.md`, step 1), so servers booted by the
(`host` CPU type), 24 GB RAM (ARK-sized servers need ~16 GB), 150 GB disk (SteamCMD downloads), Docker. builder can't reach private networks. Enough memory for the largest server it builds (~16 GB for ARK) and
disk for SteamCMD downloads. The image `gbr-builder:latest` is published by
`.gitea/workflows/builder-image.yml` whenever `cmd/` changes on main.
```bash ## Configuration
# as root
curl -fsSL https://get.docker.com | sh
git clone https://www.gitea.gbreed.com/gbreed-public/gbr-games.git /opt/gbr-games
```
**Isolated network:** game servers booted here must not reach the LAN. Same setup as the runner All through environment variables; nothing about where it runs lives in this repo.
(`RUNNER.md`, step 1):
```bash | Variable | What |
mkdir -p /opt/gbr-boottest | --- | --- |
cp /opt/gbr-games/scripts/runner/setup-boottest-network.sh /opt/gbr-boottest/ | `PANEL_URL` | The panel the worker takes jobs from |
chmod +x /opt/gbr-boottest/setup-boottest-network.sh | `BUILDER_TOKEN` | Shared secret the panel checks on `/api/v1/builder/**` (same value in the panel's env) |
cp /opt/gbr-games/scripts/runner/gbr-boottest-network.service /etc/systemd/system/ | `GITEA_TOKEN` | The bot account's token (repository read/write on this repo): pushes draft branches, opens PRs |
systemctl daemon-reload && systemctl enable --now gbr-boottest-network | `OLLAMA_URL` | Ollama for the model (or `MODEL_PROVIDER=gemini` with `GEMINI_API_KEY`) |
``` | `SEARXNG_URL`, `READER_URL` | A SearXNG instance (JSON API) and a page-to-text reader (`READER_URL/<page url>`) |
| `MODEL`, `POLL_INTERVAL`, `WORKER_NAME`, `WORK_DIR` | Optional: model name, poll interval (20s), name shown in the panel, work folder |
**Worker:**
```bash
mkdir -p /opt/gbr-builder/work && cd /opt/gbr-builder
cp /opt/gbr-games/scripts/builder/docker-compose.yml .
cp /opt/gbr-games/scripts/builder/env.example .env && chmod 600 .env # then fill it in
docker compose up -d && docker logs -f gbr-builder
```
`gbr-builder:latest` is published by `.gitea/workflows/builder-image.yml` whenever `cmd/` changes on
main; watchtower on the VM updates the worker within five minutes.
## Accounts and secrets
| Where | Name | What |
| --- | --- | --- |
| Builder `.env` | `GITEA_TOKEN` | Token of the **gbr-bot** Gitea user (Write on `gbr-games`, scope repository read/write) |
| Builder `.env` + panel env | `BUILDER_TOKEN` | Shared secret the panel checks on `/api/v1/builder/**`; any long random string |
| Builder `.env` | `OLLAMA_URL`, `SEARXNG_URL`, `READER_URL` | The 3090's Ollama, SearXNG, and the page reader |
## Running a build by hand ## Running a build by hand
On any machine with Docker and the `gbr-boottest` network (the builder VM, or the runner): On any machine with Docker and the `gbr-boottest` network, with `OLLAMA_URL`, `SEARXNG_URL` and
`READER_URL` set:
```bash ```bash
export OLLAMA_URL=http://192.168.4.30:11434 SEARXNG_URL=http://192.168.4.38:8080 READER_URL=http://192.168.4.38:3000
go run ./cmd/gbrgames research -name Valheim -out research.json go run ./cmd/gbrgames research -name Valheim -out research.json
# pick a variant from research.json into v.json, then: # pick a variant from research.json into v.json, then:
go run ./cmd/gbrgames build -name Valheim -variant @v.json -out build.json go run ./cmd/gbrgames build -name Valheim -variant @v.json -out build.json
``` ```
The draft lands in `games/<id>/`, first-boot reports in `firstboot-out/`. The draft lands in `games/<id>/`, first-boot reports in `firstboot-out/`.
## Housekeeping
Like the runner: a weekly `docker system prune -af --filter "until=168h"` keeps images and SteamCMD
downloads in check. Each job removes its clone; boot tests remove their containers and volumes.
+1 -1
View File
@@ -1,4 +1,4 @@
# The AI builder worker (gbrgames worker), run on our own builder VM; see BUILDER.md. # The AI builder worker (gbrgames worker); see BUILDER.md.
FROM golang:1.24-alpine AS build FROM golang:1.24-alpine AS build
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
+1 -1
View File
@@ -327,7 +327,7 @@ func (t *teeWriter) Write(p []byte) (int, error) {
// buildSummary is the pull request's description: where the facts came from and what was really checked. // buildSummary is the pull request's description: where the facts came from and what was really checked.
func buildSummary(o buildOptions, res *buildResult) string { func buildSummary(o buildOptions, res *buildResult) string {
var b strings.Builder var b strings.Builder
fmt.Fprintf(&b, "Drafted by the GBR builder (AI on our own hardware). **Review before merging**: check the settings, "+ fmt.Fprintf(&b, "Drafted by the GBR AI builder. **Review before merging**: check the settings, "+
"ports and descriptions against the game's documentation.\n\n") "ports and descriptions against the game's documentation.\n\n")
fmt.Fprintf(&b, "- Game: **%s**", o.name) fmt.Fprintf(&b, "- Game: **%s**", o.name)
if o.variant.Title != "" { if o.variant.Title != "" {
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"time" "time"
) )
// worker runs the AI builder on our own hardware. It asks the panel for queued jobs (it only connects out, // worker runs the AI builder. It asks the panel for queued jobs (it only connects out,
// like node-agents do), runs research or build in a fresh clone of this repo, streams the output to the // like node-agents do), runs research or build in a fresh clone of this repo, streams the output to the
// panel as the job's log, and finishes a build with a pull request opened as the bot account. CI then // panel as the job's log, and finishes a build with a pull request opened as the bot account. CI then
// validates and boot-tests that pull request like any other. // validates and boot-tests that pull request like any other.
-28
View File
@@ -1,28 +0,0 @@
# The AI builder worker on the builder VM (BUILDER.md). It polls the panel for jobs, so nothing needs to
# reach this VM from outside. Game servers it boots run in the isolated gbr-boottest network.
services:
gbr-builder:
image: www.gitea.gbreed.com/gbreed-public/gbr-builder:latest
container_name: gbr-builder
restart: unless-stopped
env_file: .env
environment:
WORK_DIR: /work
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# Job clones and first-boot reports (servers themselves use Docker volumes)
- /opt/gbr-builder/work:/work
labels:
- com.centurylinklabs.watchtower.enable=true
watchtower:
image: containrrr/watchtower:1.7.1
container_name: gbr-builder-watchtower
restart: unless-stopped
environment:
WATCHTOWER_LABEL_ENABLE: "true"
WATCHTOWER_POLL_INTERVAL: "300"
WATCHTOWER_CLEANUP: "true"
DOCKER_API_VERSION: "1.44"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
-19
View File
@@ -1,19 +0,0 @@
# Copy to /opt/gbr-builder/.env and fill in. Never commit the real file.
# The panel the worker takes jobs from, and the token both sides share (panel env BUILDER_TOKEN)
PANEL_URL=https://api.gbrhosting.com
BUILDER_TOKEN=
# The gbr-bot account's token (repository read/write on gbreed-public/gbr-games): pushes draft branches, opens PRs
GITEA_TOKEN=
# The model on the 3090, and the search + page reader the research step uses
OLLAMA_URL=http://192.168.4.30:11434
SEARXNG_URL=http://192.168.4.38:8080
READER_URL=http://192.168.4.38:3000
# Optional
# MODEL=qwen3.8:27b
# MODEL_PROVIDER=ollama # or gemini, with GEMINI_API_KEY
# POLL_INTERVAL=20s
# WORKER_NAME=gamebuilder