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
107 changes: 107 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Docker image

# Builds docker/ and publishes the agent image to Docker Hub.
# - Pull request touching docker/**: build (amd64) to VERIFY it compiles — no push.
# - Push to main touching docker/** (or manual dispatch): build multi-arch on
# native runners and push thunderockforge/forge-agent:latest.
#
# Requires repo secrets: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (a Docker Hub access token).
# Action versions use major tags for readability; pin to SHAs to match release.yml if desired.

on:
push:
branches: [main]
paths:
- 'docker/**'
- '.github/workflows/docker-image.yml'
pull_request:
paths:
- 'docker/**'
- '.github/workflows/docker-image.yml'
workflow_dispatch:

env:
IMAGE: docker.io/thunderockforge/forge-agent

jobs:
# ---- PR verification: prove the image builds, don't push ----
verify:
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build (amd64, no push)
uses: docker/build-push-action@v6
with:
context: ./docker
platforms: linux/amd64
push: false

# ---- main / manual: build each arch on its native runner, push by digest ----
build:
if: github.event_name != 'pull_request'
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
arch: amd64
runner: ubuntu-24.04
- platform: linux/arm64
arch: arm64
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: ./docker
platforms: ${{ matrix.platform }}
outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true
- name: Export digest
run: |
mkdir -p "${RUNNER_TEMP}/digests"
digest="${{ steps.build.outputs.digest }}"
touch "${RUNNER_TEMP}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digest-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

# ---- combine per-arch digests into one multi-arch :latest tag ----
merge:
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-24.04
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: digest-*
merge-multiple: true
- uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create and push manifest list (:latest)
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create -t ${{ env.IMAGE }}:latest \
$(printf '${{ env.IMAGE }}@sha256:%s ' *)
- name: Inspect
run: docker buildx imagetools inspect ${{ env.IMAGE }}:latest
136 changes: 116 additions & 20 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,24 +1,96 @@
# Forge — Docker agent image
# Pre-installs common dev tools so agents don't waste time on setup.
# Build: docker build -t forge-agent:latest docker/
# Size target: ~600 MB (multi-stage not needed; one fat layer is fine for a dev image)
# Pre-installs common dev tools + language toolchains so agents don't waste time on setup.
# Multi-stage: the `rust`, `go`, and `tools` builder stages build in parallel under BuildKit;
# the final stage (the shipped agent image) copies their artifacts in.
# Build: DOCKER_BUILDKIT=1 docker build -t thunderockforge/forge-agent:latest docker/

FROM ubuntu:22.04
ARG UBUNTU=ubuntu:22.04

# ===== builder: tools (lazygit, git-delta, uv + Python 3.11, ruff/black/isort/mypy, AWS CLI v2) =====
FROM ${UBUNTU} AS tools
ENV DEBIAN_FRONTEND=noninteractive \
UV_INSTALL_DIR=/opt/extra/bin \
UV_PYTHON_INSTALL_DIR=/opt/extra/uv/python \
UV_TOOL_DIR=/opt/extra/uv/tools \
UV_TOOL_BIN_DIR=/opt/extra/bin \
PATH=/opt/extra/bin:$PATH
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl unzip \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /opt/extra/bin
# lazygit + git-delta (arch-aware GitHub releases)
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in amd64) lg=x86_64; dl=x86_64-unknown-linux-gnu ;; \
arm64) lg=arm64; dl=aarch64-unknown-linux-gnu ;; \
*) echo "unsupported arch $arch"; exit 1 ;; esac; \
lgver="$(curl -fsSL https://api.github.com/repos/jesseduffield/lazygit/releases/latest | grep -Po '"tag_name": *"v\K[^"]*')"; \
curl -fsSL "https://github.com/jesseduffield/lazygit/releases/download/v${lgver}/lazygit_${lgver}_Linux_${lg}.tar.gz" -o /tmp/lazygit.tgz; \
tar -C /opt/extra/bin -xzf /tmp/lazygit.tgz lazygit; \
dver="$(curl -fsSL https://api.github.com/repos/dandavison/delta/releases/latest | grep -Po '"tag_name": *"\K[^"]*')"; \
curl -fsSL "https://github.com/dandavison/delta/releases/download/${dver}/delta-${dver}-${dl}.tar.gz" -o /tmp/delta.tgz; \
tar -C /tmp -xzf /tmp/delta.tgz; mv "/tmp/delta-${dver}-${dl}/delta" /opt/extra/bin/delta; \
rm -rf /tmp/lazygit.tgz /tmp/delta.tgz "/tmp/delta-${dver}-${dl}"
# uv + Python 3.11 + linters
RUN set -eux; \
curl -LsSf https://astral.sh/uv/install.sh | sh; \
command -v uv >/dev/null || cp "$HOME/.local/bin/uv" /opt/extra/bin/; \
uv python install 3.11; \
ln -sf "$(uv python find 3.11)" /opt/extra/bin/python3.11; \
uv tool install ruff; uv tool install black; uv tool install isort; uv tool install mypy
# AWS CLI v2
RUN set -eux; \
arch="$(uname -m)"; \
curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-${arch}.zip" -o /tmp/awscliv2.zip; \
cd /tmp; unzip -q awscliv2.zip; ./aws/install --install-dir /opt/extra/aws-cli --bin-dir /opt/extra/bin; \
cd /; rm -rf /tmp/aws /tmp/awscliv2.zip
RUN chmod -R a+rX /opt/extra

# ===== builder: rust (long pole) =====
FROM ${UBUNTU} AS rust
ENV DEBIAN_FRONTEND=noninteractive \
RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo PATH=/usr/local/cargo/bin:$PATH
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl build-essential pkg-config libssl-dev \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain 1.93.0 --profile default --no-modify-path; \
rustup component add clippy rust-analyzer rust-src; \
cargo install cargo-watch cargo-audit; \
cargo install --locked cargo-nextest; \
chmod -R a+rX "$CARGO_HOME" "$RUSTUP_HOME"

# ===== builder: go =====
FROM ${UBUNTU} AS go
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
arch="$(dpkg --print-architecture)"; \
case "$arch" in amd64) ga=amd64 ;; arm64) ga=arm64 ;; *) echo "unsupported $arch"; exit 1 ;; esac; \
GO_VERSION=1.24.4; \
curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ga}.tar.gz" -o /tmp/go.tgz; \
tar -C /usr/local -xzf /tmp/go.tgz; rm /tmp/go.tgz

# ===== final: the shipped agent image (run with --user 1000:1000) =====
FROM ${UBUNTU}
ENV DEBIAN_FRONTEND=noninteractive \
LANG=C.UTF-8 \
LC_ALL=C.UTF-8

# Core build tools + languages
# Core tools + languages + dev extras
RUN apt-get update && apt-get install -y --no-install-recommends \
# Basics
ca-certificates curl wget git openssh-client gnupg \
# Build essentials
build-essential pkg-config \
build-essential pkg-config libssl-dev \
# Python
python3 python3-pip python3-venv \
# Shells & utilities
bash zsh jq ripgrep fd-find fzf tree unzip less \
bash zsh jq ripgrep fd-find fzf tree unzip less bat \
# Dev extras
htop direnv cmake clang postgresql-client silversearcher-ag \
# Process inspection
procps \
# Networking
Expand All @@ -27,6 +99,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
nano \
&& rm -rf /var/lib/apt/lists/*

# Make fd/bat available under their common names (Ubuntu ships fdfind/batcat)
RUN ln -sf "$(command -v fdfind)" /usr/local/bin/fd 2>/dev/null || true; \
ln -sf "$(command -v batcat)" /usr/local/bin/bat 2>/dev/null || true

# Add apt repos: NodeSource (Node 22 LTS) and GitHub CLI
RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
Expand All @@ -40,36 +116,56 @@ RUN mkdir -p /etc/apt/keyrings \

# Install Node.js, npm, and GitHub CLI in one layer
RUN apt-get update && apt-get install -y --no-install-recommends \
nodejs \
git-lfs \
gh \
nodejs git-lfs gh \
&& rm -rf /var/lib/apt/lists/* \
&& npm install -g npm@10

# Make fd and fdfind available as 'fd'
RUN ln -sf "$(command -v fdfind)" /usr/local/bin/fd 2>/dev/null || true

# AI agent CLIs — must be present so Docker-mode tasks can execute them
RUN npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli opencode-ai

# Antigravity CLI (agy) — distributed as a Go binary via the official installer
# (not on npm). The installer's `--dir` flag drops the binary straight into a
# system path on PATH, so the non-root `agent` user that runs containers resolves
# it. A failed fetch makes the build step fail, surfacing the break at image build
# time rather than at task launch.
# Antigravity CLI (agy) — Go binary via the official installer; --dir drops it on PATH
RUN curl -fsSL https://antigravity.google/cli/install.sh | bash -s -- --dir /usr/local/bin \
&& agy --version

# Default git config so commits work out of the box inside containers
RUN git config --system init.defaultBranch main \
&& git config --system advice.detachedHead false

# Language toolchains + extra CLIs from the parallel builder stages
COPY --from=tools /opt/extra /opt/extra
COPY --from=rust /usr/local/cargo /usr/local/cargo
COPY --from=rust /usr/local/rustup /usr/local/rustup
COPY --from=go /usr/local/go /usr/local/go
ENV RUSTUP_HOME=/usr/local/rustup CARGO_HOME=/usr/local/cargo \
UV_INSTALL_DIR=/opt/extra/bin \
UV_PYTHON_INSTALL_DIR=/opt/extra/uv/python \
UV_TOOL_DIR=/opt/extra/uv/tools \
UV_TOOL_BIN_DIR=/opt/extra/bin \
PATH=/usr/local/go/bin:/usr/local/cargo/bin:/opt/extra/bin:$PATH

# Non-root user — use --user 1000:1000 in docker run as needed
RUN groupadd --gid 1000 agent \
&& useradd --uid 1000 --gid agent --shell /bin/bash --create-home agent

# Set a reasonable default shell
# Curated dotfiles (no secrets, no baked git identity) + vim-gitgutter
COPY --chown=1000:1000 dotfiles/bashrc /home/agent/.bashrc
COPY --chown=1000:1000 dotfiles/gitconfig /home/agent/.gitconfig
COPY --chown=1000:1000 dotfiles/vimrc /home/agent/.vimrc
RUN set -eux; \
git clone --depth=1 https://github.com/airblade/vim-gitgutter \
/home/agent/.vim/pack/plugins/start/vim-gitgutter; \
chown -R 1000:1000 /home/agent/.vim

# GSD (get-shit-done) — Claude Code workflow layer, baked into the agent skeleton.
# Forge seeds /home/agent/.claude + .gsd into each task's per-agent $HOME at
# container start (see electron/ipc/pty.ts). Config is secret-free.
COPY --chown=1000:1000 dotfiles/gsd-defaults.json /home/agent/.gsd/defaults.json
RUN set -eux; \
HOME=/home/agent npx -y get-shit-done-cc@latest --global --yes; \
rm -rf /home/agent/.npm; \
chown -R 1000:1000 /home/agent/.claude /home/agent/.gsd; \
chmod -R a+rX /home/agent/.claude /home/agent/.gsd

ENV SHELL=/bin/bash
WORKDIR /app

CMD ["bash"]
32 changes: 32 additions & 0 deletions docker/dotfiles/bashrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ~/.bashrc — personal agent sandbox. Curated from ashutosh_setup.
# Secrets and Adobe-specific logic intentionally omitted.

bind '"\e[A": previous-history' 2>/dev/null
bind '"\e[B": next-history' 2>/dev/null
bind '"\eOA": previous-history' 2>/dev/null
bind '"\eOB": next-history' 2>/dev/null

export HISTFILE=$HOME/.bash_history
export HISTSIZE=10000000
export HISTFILESIZE=10000000
export HISTCONTROL=ignoreboth:erasedups
export HISTTIMEFORMAT='%F %T '
shopt -s histappend cmdhist checkwinsize globstar
PROMPT_COMMAND='history -a; history -n'

alias grep='grep --color=auto'
alias ls='ls --color=auto'
alias ll='ls -lh --color=auto'
alias la='ls -lAh --color=auto'
alias lg='lazygit'
alias fvim='vim "$(fzf)"'
alias fcat="fzf --preview 'bat --color=always --style=numbers {}'"

for _f in \
/usr/share/doc/fzf/examples/key-bindings.bash \
/usr/share/fzf/key-bindings.bash \
/usr/share/doc/fzf/examples/completion.bash \
/usr/share/fzf/completion.bash; do
[ -f "$_f" ] && source "$_f"
done
unset _f
35 changes: 35 additions & 0 deletions docker/dotfiles/gitconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[alias]
co = checkout
a = add
b = branch
c = commit
d = diff
f = fetch
g = grep
l = log
m = merge
p = pull
r = remote
s = status
w = whatchanged
lg = log --graph
lo = log --oneline
lp = log --patch
lfp = log --first-parent
lt = log --topo-order
ll = log --graph --topo-order --abbrev-commit --date=short --decorate --all --boundary --pretty=format:'%Cgreen%ad %Cred%h%Creset -%C(yellow)%d%Creset %s %Cblue[%cn]%Creset %Cblue%G?%Creset'
unstage = reset HEAD --
slast = rev-parse @~
head = log --name-status HEAD^..HEAD
[core]
editor = vim
pager = delta
[interactive]
diffFilter = delta --color-only
[delta]
navigate = true
dark = true
[merge]
conflictStyle = zdiff3
[pull]
rebase = true
Loading
Loading