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
34 changes: 34 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
**
!go.mod
!go.sum
!cmd/
!cmd/forge-gate/
!cmd/forge-gate/**
!internal/
!internal/buildinfo/
!internal/buildinfo/**
!internal/configjson/
!internal/configjson/**
!internal/gate/
!internal/gate/**
!internal/githubdelivery/
!internal/githubdelivery/**
!internal/processtree/
!internal/processtree/**
!internal/protocol/
!internal/protocol/**
!internal/store/
!internal/store/**

# Explicitly document excluded local and release material.
.git
dist
release
.env
.env.*
*secret*
*.db
gate.json
state
worktrees
evidence
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ permissions:
contents: read

jobs:
oci-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: OCI release helper self-tests
run: python3 scripts/oci-release-self-test.py
- name: Verify OCI Gate contract
run: scripts/oci-gate-contract.sh
- name: Native OCI Gate E2E
run: scripts/oci-gate-e2e.sh v999.0.0 "$GITHUB_SHA"

test:
runs-on: ubuntu-latest
steps:
Expand Down
158 changes: 156 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ concurrency:
cancel-in-progress: false

jobs:
publish-linux:
prepare-linux:
runs-on: ubuntu-latest
permissions:
contents: write
contents: read
id-token: write
attestations: write
steps:
Expand Down Expand Up @@ -99,6 +99,160 @@ jobs:
with:
subject-checksums: release/SHA256SUMS
sbom-path: release/agent-forge_${{ steps.identity.outputs.version }}_linux.spdx.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: agent-forge-linux-${{ github.sha }}
path: release/*
if-no-files-found: error
retention-days: 1

publish-oci-gate:
needs: prepare-linux
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.ref }}
fetch-depth: 0
persist-credentials: false
- name: Validate exact annotated release tag
id: identity
shell: bash
run: |
set -euo pipefail
version="$GITHUB_REF_NAME"
commit="$(git rev-parse HEAD)"
scripts/validate-release-tag.sh "$version" "$commit"
printf 'version=%s\n' "$version" >>"$GITHUB_OUTPUT"
printf 'commit=%s\n' "$commit" >>"$GITHUB_OUTPUT"
- name: Require existing public GHCR package
env:
GITHUB_TOKEN: ${{ github.token }}
run: python3 scripts/ghcr-package-public.py
- name: Probe exact GHCR tag
id: tag
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ github.token }}
run: python3 scripts/ghcr-tag-state.py 0k-lab/agent-forge-gate "${{ steps.identity.outputs.version }}" >>"$GITHUB_OUTPUT"
- name: Refuse an existing stable tag
if: steps.tag.outputs.state != 'absent'
run: |
echo "OCI tag already exists; stable tags are never resumed or rerun. Correct the release and use the next patch version." >&2
exit 1
- name: Use isolated authenticated Docker config
run: |
set -euo pipefail
install -d -m 0700 "$RUNNER_TEMP/agent-forge-oci-auth"
printf 'DOCKER_CONFIG=%s\n' "$RUNNER_TEMP/agent-forge-oci-auth" >>"$GITHUB_ENV"
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Build and push multi-architecture Gate image
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8
with:
context: .
file: Dockerfile.gate
platforms: linux/amd64,linux/arm64
push: true
pull: true
no-cache: true
sbom: true
provenance: mode=max
tags: ghcr.io/0k-lab/agent-forge-gate:${{ steps.identity.outputs.version }}
build-args: |
VERSION=${{ steps.identity.outputs.version }}
COMMIT=${{ steps.identity.outputs.commit }}
- name: Attest registry image provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-name: ghcr.io/0k-lab/agent-forge-gate
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Remove GHCR credentials
run: |
set -euo pipefail
[ "$DOCKER_CONFIG" = "$RUNNER_TEMP/agent-forge-oci-auth" ]
docker logout ghcr.io
rm -rf -- "$RUNNER_TEMP/agent-forge-oci-auth"
install -d -m 0700 "$RUNNER_TEMP/agent-forge-oci-anonymous"
printf 'DOCKER_CONFIG=%s\n' "$RUNNER_TEMP/agent-forge-oci-anonymous" >>"$GITHUB_ENV"
- name: Anonymously verify exact index
id: image
env:
IMAGE: ghcr.io/0k-lab/agent-forge-gate
VERSION: ${{ steps.identity.outputs.version }}
EXPECTED_DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
docker buildx imagetools inspect --raw "$IMAGE:$VERSION" >"$RUNNER_TEMP/agent-forge-gate-index.json"
python3 scripts/verify-oci-gate-release.py \
"$RUNNER_TEMP/agent-forge-gate-index.json" "$EXPECTED_DIGEST" >>"$GITHUB_OUTPUT"
printf 'digest=%s\n' "$EXPECTED_DIGEST" >>"$GITHUB_OUTPUT"
- name: Anonymously validate both runtime images
env:
IMAGE: ghcr.io/0k-lab/agent-forge-gate
VERSION: ${{ steps.identity.outputs.version }}
COMMIT: ${{ steps.identity.outputs.commit }}
run: |
set -euo pipefail
for architecture in amd64 arm64; do
case "$architecture" in
amd64) digest="${{ steps.image.outputs.amd64_digest }}" ;;
arm64) digest="${{ steps.image.outputs.arm64_digest }}" ;;
esac
reference="$IMAGE@$digest"
docker pull --platform "linux/$architecture" "$reference"
[ "$(docker image inspect --format '{{.Architecture}}' "$reference")" = "$architecture" ]
[ "$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.source"}}' "$reference")" = https://github.com/0k-lab/agent-forge ]
[ "$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.version"}}' "$reference")" = "$VERSION" ]
[ "$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$reference")" = "$COMMIT" ]
[ "$(docker image inspect --format '{{.Config.User}}' "$reference")" = 65532:65532 ]
[ "$(docker image inspect --format '{{json .Config.Entrypoint}}' "$reference")" = '["/usr/local/bin/forge-gate"]' ]
[ "$(docker image inspect --format '{{json .Config.Cmd}}' "$reference")" = '["-config","/etc/agent-forge/gate.json"]' ]
[ "$(docker run --rm --platform "linux/$architecture" "$reference" --version)" = "forge-gate $VERSION $COMMIT" ]
done
docker buildx imagetools inspect --raw "$IMAGE:$VERSION" >"$RUNNER_TEMP/agent-forge-gate-index-recheck.json"
python3 scripts/verify-oci-gate-release.py \
"$RUNNER_TEMP/agent-forge-gate-index-recheck.json" "${{ steps.build.outputs.digest }}" >/dev/null

publish-linux:
needs: [prepare-linux, publish-oci-gate]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.ref }}
fetch-depth: 0
persist-credentials: false
- name: Validate exact annotated release tag
id: identity
shell: bash
run: |
set -euo pipefail
version="$GITHUB_REF_NAME"
commit="$(git rev-parse HEAD)"
scripts/validate-release-tag.sh "$version" "$commit"
printf 'version=%s\n' "$version" >>"$GITHUB_OUTPUT"
printf 'commit=%s\n' "$commit" >>"$GITHUB_OUTPUT"
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: agent-forge-linux-${{ github.sha }}
path: release
- name: Verify downloaded Linux release
run: python3 scripts/verify-linux-release.py "${{ steps.identity.outputs.version }}" release
- name: Publish verified GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
Expand Down
42 changes: 42 additions & 0 deletions Dockerfile.gate
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
FROM golang:1.24.4-bookworm@sha256:10f549dc8489597aa7ed2b62008199bb96717f52a8e8434ea035d5b44368f8a6 AS build

ARG VERSION
ARG COMMIT
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/forge-gate ./cmd/forge-gate
COPY internal/buildinfo ./internal/buildinfo
COPY internal/configjson ./internal/configjson
COPY internal/gate ./internal/gate
COPY internal/githubdelivery ./internal/githubdelivery
COPY internal/processtree ./internal/processtree
COPY internal/protocol ./internal/protocol
COPY internal/store ./internal/store
RUN printf '%s\n' "$VERSION" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' \
&& printf '%s\n' "$COMMIT" | grep -Eq '^[0-9a-f]{40}$' \
&& CGO_ENABLED=0 GOFLAGS=-mod=readonly GOENV=off GOWORK=off GOEXPERIMENT= GOFIPS140=off GOTOOLCHAIN=go1.24.4 \
go build -trimpath -buildvcs=false \
-ldflags="-s -w -buildid= -X agent-forge/internal/buildinfo.Version=${VERSION} -X agent-forge/internal/buildinfo.Commit=${COMMIT}" \
-o /out/forge-gate ./cmd/forge-gate

FROM debian:bookworm-slim@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171

ARG VERSION
ARG COMMIT
RUN apt-get update \
&& apt-get install --yes --no-install-recommends ca-certificates git \
&& rm -rf /var/lib/apt/lists/* \
&& rm -rf /var/cache/apt/* \
&& printf 'agent-forge:x:65532:\n' >>/etc/group \
&& printf 'agent-forge:x:65532:65532:Agent Forge:/var/lib/agent-forge:/usr/sbin/nologin\n' >>/etc/passwd \
&& install -d -o 65532 -g 65532 -m 0700 /var/lib/agent-forge/state /var/lib/agent-forge/repositories \
&& install -d -o 0 -g 0 -m 0755 /etc/agent-forge
COPY --from=build /out/forge-gate /usr/local/bin/forge-gate
LABEL org.opencontainers.image.source="https://github.com/0k-lab/agent-forge" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${COMMIT}"
USER 65532:65532
EXPOSE 18080
ENTRYPOINT ["/usr/local/bin/forge-gate"]
CMD ["-config", "/etc/agent-forge/gate.json"]
54 changes: 52 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A minimal Go vertical slice: submit a job to **forge-gate**, persist it in SQLit
- **Gate** owns repository registrations and authorization, public-source clone/fetch/reuse, prepared local repository paths, Worker pools and authenticated slots, submission-time lifecycle/execution policy resolution, leases, exact GitHub App publication, CI observation/merge, and authoritative SQLite state.
- **Worker** consumes Gate-prepared local repositories, edits, runs only instructed checks, commits locally, and returns the candidate SHA/evidence. External repository URLs and GitHub credentials never cross the Gate/Worker boundary; Worker never clones, pushes, or writes external APIs.
- **Plugin** uses the strict NDJSON [plugin protocol v1](docs/plugin-protocol-v1.md). The reference plugin implements `text`; `forge-codex-plugin` implements `workspace_edit`, invokes `CODEX_BIN` (default `codex`) with bounded output and a timeout, obtains the actual-diff commit subject through Codex structured output, and never reports business success or commits.
- This MVP deliberately has no control panel, Docker, reviewer, mTLS, PostgreSQL, or plugin marketplace. Its only browser UI is a read-only debug viewer. GitHub delivery is optional.
- This MVP deliberately has no control panel, universal Worker image, reviewer, mTLS, PostgreSQL, or plugin marketplace. Its only browser UI is a read-only debug viewer. GitHub delivery is optional.

## Build and test

Expand Down Expand Up @@ -46,7 +46,57 @@ The canonical matrix is:

Every archive contains a `VERSION` file, and each runtime binary supports `--version`. CI runs `scripts/release-artifacts-e2e.sh` and uploads the verified full matrix as short-lived workflow artifacts.

A pushed annotated `vMAJOR.MINOR.PATCH` tag starts `.github/workflows/release.yml`; prerelease suffixes, lightweight tags, and commits outside `origin/main` are rejected. The tag pipeline selects the six Linux archives, writes a Linux-only `SHA256SUMS`, generates an SPDX JSON SBOM with pinned Syft, and creates GitHub build-provenance and SBOM attestations through OIDC. It then uploads the exact verified asset set to a draft GitHub Release, verifies every remote SHA-256 digest, and publishes the draft. Existing releases and assets are never replaced. If upload fails after draft creation, the partial draft is deliberately retained and a rerun refuses it; an operator must inspect it and explicitly decide whether to delete it before retrying. macOS is distributed through a Homebrew Formula and bottles rather than as GitHub Release assets; Cask and Apple notarization are not part of this CLI release path.
## OCI Gate image

Gate is published separately as the publicly pullable multi-architecture image `ghcr.io/0k-lab/agent-forge-gate:vMAJOR.MINOR.PATCH`. The release workflow publishes only that exact version tag—never `latest`, major, or minor tags—and refuses any version tag that already exists. Failed stable tags are never resumed or rerun; correct the release and use the next patch version. This is workflow policy, not registry-enforced immutability. Pinning the resolved index digest remains the strongest production reference, for example `ghcr.io/0k-lab/agent-forge-gate@sha256:<index-digest>`.

### One-time GHCR bootstrap before the first stable OCI release

Use a GitHub token with `write:packages` and permission to publish for `0k-lab/agent-forge`. Keep it in `GHCR_TOKEN`, never in argv or the image. From a clean checkout of the exact reviewed `main` commit:

```sh
export GHCR_USER='<github-user>'
export GHCR_TOKEN='<write-packages-token>'
COMMIT=$(git rev-parse HEAD)
printf '%s' "$GHCR_TOKEN" | docker login ghcr.io --username "$GHCR_USER" --password-stdin
docker buildx build --file Dockerfile.gate \
--platform linux/amd64,linux/arm64 --pull --no-cache --sbom=true --provenance=mode=max \
--build-arg VERSION=v0.0.0 --build-arg COMMIT="$COMMIT" \
--tag "ghcr.io/0k-lab/agent-forge-gate:bootstrap-$COMMIT" --push .
docker logout ghcr.io
unset GHCR_TOKEN
```

The pinned Dockerfile bases and source label create the package and link it to this repository. Confirm the repository link and make the package public in GitHub Package settings. Then use a fresh Docker config with no GHCR credentials to verify an anonymous pull of the bootstrap image:

```sh
DOCKER_CONFIG=$(mktemp -d)
export DOCKER_CONFIG
docker pull "ghcr.io/0k-lab/agent-forge-gate:bootstrap-$COMMIT"
rm -rf "$DOCKER_CONFIG"
unset DOCKER_CONFIG
```

Confirm the package metadata reports public visibility; only then create the stable git tag. After the first stable image is published and verified, the bootstrap package version may be removed through GitHub Package settings.

The stable-tag workflow only reads package metadata and fails before any image write when the package is absent or nonpublic. It does not and cannot change package visibility through GitHub's supported REST API. GHCR has no registry-enforced create-only or immutable tag operation: repository Actions concurrency is the single-writer control, the workflow checks absence through an authenticated registry request, and it verifies the exact index digest again after descriptor-bound runtime checks. Consumers that require cryptographic identity must pin the digest rather than trusting a mutable tag.

The image runs as UID/GID `65532:65532`. Mount Gate config read-only, mount `/var/lib/agent-forge/state` writable, and supply tokens through environment variables or mounted secret files referenced by the runtime environment. The image contains Gate and Git for supported public-source/delivery operation; it does not contain Worker, CLI, or plugin binaries, and no universal Worker image is published.

```sh
docker run --rm --read-only --cap-drop=ALL \
--security-opt=no-new-privileges --tmpfs /tmp:rw,nosuid,nodev,noexec \
-p 18080:18080 \
--mount type=bind,src="$PWD/gate.json",dst=/etc/agent-forge/gate.json,readonly \
--mount type=bind,src="$PWD/state",dst=/var/lib/agent-forge/state \
--mount type=bind,src="$PWD/repositories",dst=/var/lib/agent-forge/repositories \
-e FORGE_OWNER_TOKEN -e FORGE_WORKER_TOKEN \
ghcr.io/0k-lab/agent-forge-gate:v0.1.0
```

Create the host state and repositories directories owned by `65532:65532`; config should listen on `0.0.0.0:18080`, place SQLite under the mounted state directory, and use the mounted repositories directory for public-source storage. Keep secret values out of the image and config.

A pushed annotated `vMAJOR.MINOR.PATCH` tag starts `.github/workflows/release.yml`; prerelease suffixes, lightweight tags, and commits outside `origin/main` are rejected. The tag pipeline selects the six Linux archives, writes a Linux-only `SHA256SUMS`, generates an SPDX JSON SBOM with pinned Syft, creates GitHub build-provenance and SBOM attestations through OIDC, and completes privileged disposable acceptance before any OCI push. It uploads those exact prepared assets as a short-lived workflow artifact, publishes and anonymously validates the Gate image, then downloads and revalidates the prepared assets before GitHub Release publication. Existing releases and assets are never replaced. If upload fails after draft creation, the partial draft is deliberately retained and the stable version is not rerun; an operator must inspect it, and any corrected release uses the next patch version. macOS is distributed through a Homebrew Formula and bottles rather than as GitHub Release assets; Cask and Apple notarization are not part of this CLI release path.

## Linux install and upgrade

Expand Down
Loading
Loading