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
170 changes: 170 additions & 0 deletions .github/workflows/build-images.yml
Comment thread
ascerra marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Build Images

on:
push:
branches: [main]
paths:
- "images/**/Containerfile"
workflow_dispatch:
inputs:
image:
description: >-
Which image to build (directory name under images/, e.g. "code").
Leave empty to build all images that have a Containerfile.
required: false
type: string

env:
REGISTRY: ghcr.io
IMAGE_PREFIX: ghcr.io/${{ github.repository_owner }}/fullsend

permissions:
contents: read
packages: write

jobs:
discover:
name: Discover images
runs-on: ubuntu-latest
outputs:
images: ${{ steps.find.outputs.images }}
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Find Containerfiles
id: find
env:
INPUT_IMAGE: ${{ inputs.image }}
run: |
set -euo pipefail

if [[ -n "${INPUT_IMAGE}" ]]; then
if [[ ! "${INPUT_IMAGE}" =~ ^[a-zA-Z0-9_-]+$ ]]; then
echo "::error::Invalid image name — only alphanumeric, hyphens, and underscores allowed"
exit 1
fi
if [[ ! -f "images/${INPUT_IMAGE}/Containerfile" ]]; then
echo "::error::No Containerfile found at images/${INPUT_IMAGE}/Containerfile"
exit 1
fi
echo "images=[\"${INPUT_IMAGE}\"]" >> "${GITHUB_OUTPUT}"
else
images="[]"
for cf in images/*/Containerfile; do
dir="$(basename "$(dirname "${cf}")")"
images="$(echo "${images}" | jq -c --arg d "${dir}" '. + [$d]')"
done
echo "images=${images}" >> "${GITHUB_OUTPUT}"
fi

- name: Show discovered images
env:
IMAGES: ${{ steps.find.outputs.images }}
run: echo "Will build:${IMAGES}"

build-base:
name: Build base sandbox
runs-on: ubuntu-latest
needs: discover
outputs:
base-image: ${{ steps.tag.outputs.base_image }}
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Compute base image tag
id: tag
run: echo "base_image=${{ env.IMAGE_PREFIX }}-sandbox:${{ github.sha }}" >> "${GITHUB_OUTPUT}"

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Base image metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.IMAGE_PREFIX }}-sandbox
tags: |
type=sha,prefix=
type=raw,value=latest

- name: Build and push base sandbox
uses: docker/build-push-action@v7
with:
context: images/sandbox
file: images/sandbox/Containerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max

build-agent:
name: Build ${{ matrix.image }}
runs-on: ubuntu-latest
needs: [discover, build-base]
if: needs.discover.outputs.images != '[]'
strategy:
matrix:
image: ${{ fromJSON(needs.discover.outputs.images) }}
exclude:
- image: sandbox
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Image metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.IMAGE_PREFIX }}-${{ matrix.image }}
tags: |
type=sha,prefix=
type=raw,value=latest

- name: Build and push
uses: docker/build-push-action@v7
with:
context: images/${{ matrix.image }}
file: images/${{ matrix.image }}/Containerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
build-args: |
BASE_IMAGE=${{ needs.build-base.outputs.base-image }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Summary
env:
IMAGE_NAME: ${{ matrix.image }}
FULL_TAG: ${{ env.IMAGE_PREFIX }}-${{ matrix.image }}:latest
BASE_USED: ${{ needs.build-base.outputs.base-image }}
run: |
{
echo "### Built \`${IMAGE_NAME}\` image"
echo ""
echo "**Registry:** \`${FULL_TAG}\`"
echo "**Base image:** \`${BASE_USED}\`"
echo ""
echo "Reference this in your harness YAML:"
echo "\`\`\`yaml"
echo "image: ${FULL_TAG}"
echo "\`\`\`"
} >> "${GITHUB_STEP_SUMMARY}"
10 changes: 10 additions & 0 deletions e2e/admin/admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,21 @@ func verifyInstalled(t *testing.T, env *e2eEnv, orgCfg *config.OrgConfig, enable
".github/actions/fullsend/action.yml",
".github/scripts/setup-agent-env.sh",
"agents/triage.md",
"agents/code.md",
"harness/triage.yaml",
"harness/code.yaml",
"policies/triage.yaml",
"policies/code.yaml",
"env/triage.env",
"env/code-agent.env",
"env/gcp-vertex.env",
"scripts/validate-triage.sh",
"scripts/scan-secrets",
"scripts/pre-code.sh",
"scripts/post-code.sh",
"scripts/reconcile-repos.sh",
"skills/code-implementation/SKILL.md",
"templates/shim-workflow.yaml",
"CODEOWNERS",
} {
_, err := env.client.GetFileContent(ctx, testOrg, forge.ConfigRepoName, path)
Expand Down
113 changes: 113 additions & 0 deletions images/code/Containerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Containerfile — sandbox image for the fullsend code agent.
#
# Extends the base fullsend sandbox image with tools needed by the code agent:
# - Go (compile + test target repos written in Go)
# - gitleaks (secret scanner, SHA256-verified)
# - pre-commit + gitlint (runs repo-defined hooks and validates commit messages)
#
# The base image already provides Claude Code, rsync, tirith, and LLM Guard.
# See images/sandbox/Containerfile in fullsend-ai/fullsend for the base definition.
#
# This image is built AUTOMATICALLY by .github/workflows/build-images.yml
# on every push to main that touches images/code/Containerfile.
# You can also trigger it manually via workflow_dispatch.
#
# Published to: ghcr.io/fullsend-ai/fullsend-code:latest
#
# The harness references this image:
# image: ghcr.io/fullsend-ai/fullsend-code:latest

# CI passes --build-arg BASE_IMAGE=... from the build-base job output.
# The default lets local builds work when the sandbox image is already pulled.
ARG BASE_IMAGE=ghcr.io/fullsend-ai/fullsend-sandbox:latest
FROM ${BASE_IMAGE}

USER root

# ---------------------------------------------------------------------------
# CA certificates — git 2.43 on Ubuntu Noble is linked against
# libcurl-gnutls which does NOT read SSL_CERT_FILE. It needs either
# GIT_SSL_CAINFO env or http.sslCAInfo git config to find the CA bundle.
#
# The OpenShell sandbox does TLS termination for network policy and
# injects its own CA at /etc/openshell-tls/ca-bundle.pem (NVIDIA/OpenShell#790).
# The env file (code-agent.env) sets GIT_SSL_CAINFO dynamically at runtime.
# The git system config below is a build-time fallback for the system bundle.
RUN if command -v apt-get >/dev/null 2>&1; then \
apt-get update -qq && apt-get install -y -qq ca-certificates && rm -rf /var/lib/apt/lists/*; \
elif command -v apk >/dev/null 2>&1; then \
apk add --no-cache ca-certificates; \
elif command -v dnf >/dev/null 2>&1; then \
dnf install -y ca-certificates && dnf clean all; \
fi \
&& update-ca-certificates 2>/dev/null || true
# Set the system CA bundle as git's default sslCAInfo. At runtime, the
# env file overrides this with the OpenShell bundle if present.
RUN git config --system http.sslCAInfo /etc/ssl/certs/ca-certificates.crt 2>/dev/null || true

# ---------------------------------------------------------------------------
# Go toolchain — needed to compile and run tests in Go-based target repos.
# Without this the agent falls back to manual code review (no `go build`,
# `go test`, or `go vet`), wasting ~4 tool calls trying to find Go.
#
# Pinned version + SHA256 checksum for supply chain safety.
# To update: get the latest linux-amd64 archive + sha256 from https://go.dev/dl/?mode=json
ARG GO_VERSION=1.24.13
ARG GO_SHA256=1fc94b57134d51669c72173ad5d49fd62afb0f1db9bf3f798fd98ee423f8d730

RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" \
-o /tmp/go.tar.gz \
&& echo "${GO_SHA256} /tmp/go.tar.gz" | sha256sum -c - \
&& tar -C /usr/local -xzf /tmp/go.tar.gz \
&& rm /tmp/go.tar.gz

ENV PATH="/usr/local/go/bin:${PATH}" \
GOPATH="/sandbox/go" \
GOMODCACHE="/sandbox/go/pkg/mod"

# ---------------------------------------------------------------------------
# gitleaks — secret scanner used by scripts/scan-secrets.
# When gitleaks is on PATH, the scan-secrets script uses it directly
# with zero download latency. Without it, scan-secrets falls back to
# downloading and verifying at runtime (slower, needs network).
#
# To update: bump GITLEAKS_VERSION and GITLEAKS_SHA256 from:
# https://github.com/gitleaks/gitleaks/releases/download/v<VERSION>/gitleaks_<VERSION>_checksums.txt
ARG GITLEAKS_VERSION=8.30.1
ARG GITLEAKS_SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb

RUN curl -fsSL \
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
-o /tmp/gitleaks.tar.gz \
&& echo "${GITLEAKS_SHA256} /tmp/gitleaks.tar.gz" | sha256sum -c - \
&& tar xzf /tmp/gitleaks.tar.gz -C /usr/local/bin gitleaks \
&& chmod +x /usr/local/bin/gitleaks \
&& rm /tmp/gitleaks.tar.gz

# ---------------------------------------------------------------------------
# pre-commit + gitlint — pre-commit runs repo-defined hooks (formatting,
# linting, etc.) on changed files before committing. gitlint validates
# commit message format. Baking them into the image avoids pip installs
# on every run and ensures they're available even if pip is restricted.
ARG PRECOMMIT_VERSION=4.5.1
ARG GITLINT_VERSION=0.19.1
RUN pip install "pre-commit==${PRECOMMIT_VERSION}" "gitlint-core==${GITLINT_VERSION}" 2>/dev/null \
|| pip3 install "pre-commit==${PRECOMMIT_VERSION}" "gitlint-core==${GITLINT_VERSION}"
# The venv's bin dir (/sandbox/.venv/bin) is not on PATH inside the
# sandbox Bash shell. Symlink the binaries into /usr/local/bin so the
# agent can find them without PATH manipulation.
Comment thread
ascerra marked this conversation as resolved.
RUN for bin in pre-commit gitlint; do \
if [ -x "/sandbox/.venv/bin/$bin" ]; then \
ln -sf "/sandbox/.venv/bin/$bin" "/usr/local/bin/$bin"; \
fi; \
done

# ---------------------------------------------------------------------------
# scan-secrets — gitleaks wrapper used by the code-implementation skill.
# Baked into the image so the agent has it at /usr/local/bin without
# needing host_files or network access. /usr is read-only in the
# sandbox policy, so the agent cannot tamper with it at runtime.
COPY scan-secrets /usr/local/bin/scan-secrets
RUN chmod +x /usr/local/bin/scan-secrets

USER sandbox
Loading
Loading