diff --git a/.github/workflows/govulncheck.yaml b/.github/workflows/govulncheck.yaml
index 6422d54dc1..4b5cff069b 100644
--- a/.github/workflows/govulncheck.yaml
+++ b/.github/workflows/govulncheck.yaml
@@ -16,7 +16,8 @@ name: govulncheck
on:
push:
branches:
- - main
+ - giantswarm
+ pull_request:
schedule:
- cron: "37 4 * * 1"
diff --git a/.github/workflows/helm-e2e.yaml b/.github/workflows/helm-e2e.yaml
index dfc5df2757..ec7646477c 100644
--- a/.github/workflows/helm-e2e.yaml
+++ b/.github/workflows/helm-e2e.yaml
@@ -16,7 +16,7 @@ name: helm-e2e
on:
pull_request:
push:
- branches: [main]
+ branches: [giantswarm]
permissions:
contents: read
jobs:
diff --git a/.github/workflows/pr-workflow.yaml b/.github/workflows/pr-workflow.yaml
index b0a1e30690..687b32d63b 100644
--- a/.github/workflows/pr-workflow.yaml
+++ b/.github/workflows/pr-workflow.yaml
@@ -16,7 +16,7 @@ name: pr-workflow
on:
pull_request:
push:
- branches: [main]
+ branches: [giantswarm]
schedule:
# Weekly run on the default branch keeps the micro-VM asset cache warm (GitHub
# evicts caches idle for 7 days). The push-to-main run populates the cache that
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
new file mode 100644
index 0000000000..0cb0f1fdeb
--- /dev/null
+++ b/.github/workflows/publish.yaml
@@ -0,0 +1,251 @@
+# Copyright 2026 The Agent Substrate Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Fork (giantswarm/substrate): publishes the Giant Swarm line of Agent
+# Substrate to ghcr.io/giantswarm/substrate — every push to the `giantswarm`
+# branch as a dev build, every `v*` tag as a release (FORK.md, "Publishing").
+# What is published: the six images the agent platform runs (ateapi,
+# atecontroller, atelet, atenet, podcertcontroller, ateom-gvisor; multi-arch,
+# built with ko like upstream's release.yaml), a digest-true mirror of the
+# agentgateway image the chart deploys, and the charts substrate-crds and
+# substrate with their image defaults stamped to this registry. Every own
+# image is scanned before the charts that reference it are pushed.
+name: publish
+
+on:
+ push:
+ branches:
+ - giantswarm
+ tags:
+ - "v*"
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Version to publish (default: the tag without "v", or a dev version derived from the commit)'
+ required: false
+
+permissions:
+ contents: read
+ packages: write
+
+env:
+ REGISTRY: ghcr.io/${{ github.repository }}
+ UPSTREAM_REPO: kagent-dev/substrate
+ COMPONENTS: ateapi atecontroller atelet atenet podcertcontroller ateom-gvisor
+
+jobs:
+ version:
+ runs-on: ubuntu-latest
+ outputs:
+ version: ${{ steps.version.outputs.version }}
+ steps:
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
+ with:
+ fetch-depth: 0
+ fetch-tags: true
+ - name: Resolve the version
+ id: version
+ env:
+ INPUT_VERSION: ${{ inputs.version }}
+ run: |
+ set -o errexit -o nounset -o pipefail
+ if [ -n "${INPUT_VERSION}" ]; then
+ version="${INPUT_VERSION}"
+ elif [ "${GITHUB_REF_TYPE}" = tag ]; then
+ version="${GITHUB_REF_NAME#v}"
+ else
+ # Dev version in the schema giantswarm's gitsemver emits today:
+ # -dev....h
+ # base = the next patch of the pin, the nearest upstream release tag
+ # reachable from the branch (the line's own tags carry a
+ # pre-release suffix and are excluded). Consumers select the channel
+ # with a Flux semverFilter on the branch part
+ # (`.*-dev\.giantswarm\..*`); the timestamp keeps semver order across
+ # rebuilds. When gitsemver moves to the RFC schema
+ # `-btc`, change this
+ # block and the consumers' filter together.
+ git fetch --quiet "https://github.com/${UPSTREAM_REPO}.git" 'refs/tags/v*:refs/tags/v*'
+ pin=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*-*' HEAD)
+ IFS=. read -r major minor patch <<<"${pin#v}"
+ base="${major}.${minor}.$((patch + 1))"
+ branch=$(printf '%s' "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
+ stamp=$(TZ=UTC git show -s --format=%cd --date=format-local:%Y-%m-%d.%H-%M-%S "${GITHUB_SHA}")
+ version="${base}-dev.${branch}.${stamp}.h${GITHUB_SHA:0:7}"
+ echo "pin ${pin} -> dev base ${base}"
+ fi
+ if ! [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
+ echo "::error::'${version}' is not a semver version"
+ exit 1
+ fi
+ echo "version=${version}" | tee -a "$GITHUB_OUTPUT"
+
+ images:
+ needs: version
+ runs-on: ubuntu-latest
+ env:
+ VERSION: ${{ needs.version.outputs.version }}
+ steps:
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
+ - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
+ with:
+ go-version-file: go.mod
+ - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - uses: imjasonh/setup-crane@feee3b6bb0d4c68370f256a4502498c9227e5c6b # v0.7
+ - name: Build and push the component images
+ # ko cross-compiles both platforms (no QEMU) onto the distroless base
+ # .ko.yaml pins; --image-refs records the pushed reference with its
+ # digest for the scan and the ledger.
+ run: |
+ set -o errexit -o nounset -o pipefail
+ : > image-refs.txt
+ for component in ${COMPONENTS}; do
+ KO_DOCKER_REPO="${REGISTRY}/${component}" ./hack/run-tool.sh ko build \
+ --bare --platform linux/amd64,linux/arm64 \
+ --tags "${VERSION}" --image-refs "ref-${component}.txt" \
+ "./cmd/${component}"
+ cat "ref-${component}.txt" >> image-refs.txt
+ done
+ - name: Mirror the agentgateway image the chart deploys
+ # atenet-router and atenet-egress run upstream's agentgateway build
+ # (charts/substrate/values.yaml images.agentgateway), an image this
+ # repository does not build. Copied by digest, under its upstream tag,
+ # so the chart published below deploys only images of this registry.
+ run: |
+ set -o errexit -o nounset -o pipefail
+ src=$(sed -n 's/^ agentgateway: //p' charts/substrate/values.yaml)
+ test -n "${src}"
+ dst="${REGISTRY}/agentgateway:${src##*:}"
+ crane copy "${src}" "${dst}"
+ echo "${dst}@$(crane digest "${dst}")" >> image-refs.txt
+ - name: Record the digests
+ run: |
+ {
+ echo "## Images ${VERSION}"
+ echo
+ echo '| Image | Digest |'
+ echo '|---|---|'
+ sed -E 's/^([^@]+)@(sha256:[0-9a-f]+)$/| `\1` | `\2` |/' image-refs.txt
+ echo
+ } >> "$GITHUB_STEP_SUMMARY"
+ cat image-refs.txt
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: image-refs
+ path: image-refs.txt
+ if-no-files-found: error
+
+ scan:
+ needs:
+ - version
+ - images
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ image:
+ - ateapi
+ - atecontroller
+ - atelet
+ - atenet
+ - podcertcontroller
+ - ateom-gvisor
+ - agentgateway
+ env:
+ # The trivy database mirrors, tried in order (ghcr rate-limits anonymous pulls).
+ TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2
+ steps:
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
+ - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: image-refs
+ - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Pick the image by digest
+ id: ref
+ run: |
+ set -o errexit -o nounset -o pipefail
+ ref=$(grep -E "^${REGISTRY}/${{ matrix.image }}[:@]" image-refs.txt | tail -1)
+ test -n "${ref}"
+ echo "ref=${ref}" | tee -a "$GITHUB_OUTPUT"
+ - name: Scan
+ # Own images gate the charts: a fixable HIGH or CRITICAL finding fails
+ # the publish (fix it — bump the module, or time-box it in .trivyignore
+ # with an expiry when upstream has no fix). The mirrored agentgateway
+ # image is upstream's build and reports only: findings there go to
+ # kagent-dev/substrate.
+ uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
+ with:
+ image-ref: ${{ steps.ref.outputs.ref }}
+ format: table
+ severity: CRITICAL,HIGH
+ ignore-unfixed: true
+ trivyignores: .trivyignore
+ exit-code: ${{ matrix.image == 'agentgateway' && '0' || '1' }}
+
+ charts:
+ needs:
+ - version
+ - scan
+ runs-on: ubuntu-latest
+ env:
+ VERSION: ${{ needs.version.outputs.version }}
+ steps:
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
+ - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
+ - name: Package and push the charts
+ # The chart's image defaults are stamped to this registry and version
+ # (image.tag explicitly, although the helper falls back to
+ # .Chart.AppVersion: `helm show values` is documentation); the checks
+ # make a missed anchor fail the job instead of shipping upstream's
+ # defaults. The chart version is the artifact version, as upstream does.
+ run: |
+ set -o errexit -o nounset -o pipefail
+ values=charts/substrate/values.yaml
+ sed -i \
+ -e "s|^ registry: ghcr.io/kagent-dev/substrate$| registry: ${REGISTRY}|" \
+ -e "s|^ tag: \"\"$| tag: \"${VERSION}\"|" \
+ -e "s|^ agentgateway: ghcr.io/kagent-dev/substrate/agentgateway:| agentgateway: ${REGISTRY}/agentgateway:|" \
+ "${values}"
+ grep -q "^ registry: ${REGISTRY}$" "${values}"
+ grep -q "^ tag: \"${VERSION}\"$" "${values}"
+ grep -q "^ agentgateway: ${REGISTRY}/agentgateway:" "${values}"
+ if grep -n "kagent-dev/substrate" "${values}"; then
+ echo "::error::${values} still references upstream's registry after stamping"
+ exit 1
+ fi
+
+ mkdir -p dist
+ for chart in substrate-crds substrate; do
+ helm package "charts/${chart}" --destination dist --version "${VERSION}" --app-version "${VERSION}"
+ done
+ echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin
+ {
+ echo "## Charts ${VERSION}"
+ echo
+ echo '| Chart | Digest |'
+ echo '|---|---|'
+ } >> "$GITHUB_STEP_SUMMARY"
+ for chart in substrate-crds substrate; do
+ out=$(helm push "dist/${chart}-${VERSION}.tgz" "oci://${REGISTRY}/helm" 2>&1)
+ echo "${out}"
+ digest=$(sed -n 's/^Digest: //p' <<<"${out}")
+ echo "| \`oci://${REGISTRY}/helm/${chart}:${VERSION}\` | \`${digest}\` |" >> "$GITHUB_STEP_SUMMARY"
+ done
diff --git a/.github/workflows/sync-upstream.yaml b/.github/workflows/sync-upstream.yaml
new file mode 100644
index 0000000000..121f9675f7
--- /dev/null
+++ b/.github/workflows/sync-upstream.yaml
@@ -0,0 +1,193 @@
+# Copyright 2026 The Agent Substrate Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Fork (giantswarm/substrate): keeps the Giant Swarm line of Agent Substrate
+# in step with upstream (FORK.md, "Re-pin"). Weekly and on demand it
+# 1. mirrors upstream `main` into this fork's `main` — kagent-dev/substrate
+# rewrites its main when it rebases onto agent-substrate/substrate, so the
+# mirror is a forced update of the one branch nobody edits here;
+# 2. probes whether the carried patches of the `giantswarm` branch still
+# rebase onto upstream `main` and names the first one that would conflict;
+# 3. on `workflow_dispatch` with a `pin`, re-pins the branch: rebases the
+# carried patches onto the new upstream tag (a patch upstream has merged
+# falls away), builds and tests, and pushes the rebased branch — or, when
+# a patch conflicts, pushes what applied before it and opens a pull
+# request that names the conflicting patch and the ones behind it.
+# Pushes use the taylorbot token: a push with GITHUB_TOKEN triggers no other
+# workflow, and the push to `giantswarm` is what publishes the dev build.
+name: sync-upstream
+
+on:
+ schedule:
+ - cron: "23 5 * * 1"
+ workflow_dispatch:
+ inputs:
+ pin:
+ description: "Upstream release tag (or commit) to re-pin the giantswarm branch onto, e.g. v0.0.27. Empty: mirror main and probe the rebase only."
+ required: false
+ dry_run:
+ description: "Do everything except push"
+ type: boolean
+ default: false
+
+permissions:
+ contents: read
+
+concurrency:
+ group: sync-upstream
+ cancel-in-progress: false
+
+env:
+ LINE_BRANCH: giantswarm
+ UPSTREAM_REPO: kagent-dev/substrate
+
+jobs:
+ sync:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
+ with:
+ ref: giantswarm
+ fetch-depth: 0
+ fetch-tags: true
+ token: ${{ secrets.TAYLORBOT_GITHUB_ACTION }}
+ - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
+ with:
+ go-version-file: go.mod
+ - name: Fetch upstream
+ run: |
+ set -o errexit -o nounset -o pipefail
+ git remote add upstream "https://github.com/${UPSTREAM_REPO}.git"
+ git fetch --quiet upstream main 'refs/tags/v*:refs/tags/v*'
+ git config user.name taylorbot
+ git config user.email dev@giantswarm.io
+
+ - name: Mirror upstream main
+ env:
+ DRY_RUN: ${{ inputs.dry_run }}
+ run: |
+ set -o errexit -o nounset -o pipefail
+ fork=$(git rev-parse origin/main)
+ up=$(git rev-parse upstream/main)
+ if [ "${fork}" = "${up}" ]; then
+ echo "- main: at upstream \`${up:0:12}\`" >> "$GITHUB_STEP_SUMMARY"
+ exit 0
+ fi
+ kind=fast-forward
+ git merge-base --is-ancestor "${fork}" "${up}" || kind="rewrite — upstream rebased its main"
+ echo "- main: \`${fork:0:12}\` → \`${up:0:12}\` (${kind})" >> "$GITHUB_STEP_SUMMARY"
+ if [ "${DRY_RUN}" != true ]; then
+ git push --force-with-lease="refs/heads/main:${fork}" origin "${up}:refs/heads/main"
+ fi
+
+ - name: Resolve the pin and the carried patches
+ id: line
+ env:
+ INPUT_PIN: ${{ inputs.pin }}
+ run: |
+ set -o errexit -o nounset -o pipefail
+ # The pin is the nearest upstream release tag reachable from the
+ # branch; the line's own tags carry a pre-release suffix and are excluded.
+ current=$(git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*-*' HEAD)
+ new="${INPUT_PIN:-${current}}"
+ if ! git rev-parse --verify --quiet "${new}^{commit}" >/dev/null; then
+ echo "::error::pin '${new}' is not a known upstream tag or commit"
+ exit 1
+ fi
+ {
+ echo "current=${current}"
+ echo "new=${new}"
+ echo "head=$(git rev-parse HEAD)"
+ } >> "$GITHUB_OUTPUT"
+ {
+ echo "- pin: \`${current}\` → \`${new}\`"
+ echo "- carried patches on \`${LINE_BRANCH}\`:"
+ git log --reverse --format=' - `%h` %s' "${current}..HEAD"
+ } >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Probe the rebase onto upstream main
+ if: steps.line.outputs.new == steps.line.outputs.current
+ env:
+ CURRENT: ${{ steps.line.outputs.current }}
+ HEAD_SHA: ${{ steps.line.outputs.head }}
+ run: |
+ set -o errexit -o nounset -o pipefail
+ up=$(git rev-parse --short upstream/main)
+ if git rebase --quiet --onto upstream/main "${CURRENT}" >/dev/null 2>&1; then
+ echo "- probe: the carried patches rebase cleanly onto upstream main \`${up}\`" >> "$GITHUB_STEP_SUMMARY"
+ else
+ conflict=$(git rev-parse --short REBASE_HEAD 2>/dev/null || echo unknown)
+ git rebase --abort
+ subject=$(git log -1 --format=%s "${conflict}" 2>/dev/null || true)
+ echo "- probe: rebasing onto upstream main \`${up}\` conflicts at \`${conflict}\` ${subject}" >> "$GITHUB_STEP_SUMMARY"
+ echo "::warning::carried patch ${conflict} (${subject}) conflicts with upstream main; the next re-pin needs a hand"
+ fi
+ git checkout --quiet "${HEAD_SHA}"
+
+ - name: Re-pin
+ if: steps.line.outputs.new != steps.line.outputs.current
+ env:
+ CURRENT: ${{ steps.line.outputs.current }}
+ NEW: ${{ steps.line.outputs.new }}
+ HEAD_SHA: ${{ steps.line.outputs.head }}
+ DRY_RUN: ${{ inputs.dry_run }}
+ GH_TOKEN: ${{ secrets.TAYLORBOT_GITHUB_ACTION }}
+ run: |
+ set -o errexit -o nounset -o pipefail
+ if git rebase --onto "${NEW}" "${CURRENT}"; then
+ echo "- re-pin: the carried patches rebased onto \`${NEW}\`" >> "$GITHUB_STEP_SUMMARY"
+ git log --reverse --format=' - `%h` %s' "${NEW}..HEAD" >> "$GITHUB_STEP_SUMMARY"
+ go build ./...
+ go test ./...
+ if [ "${DRY_RUN}" != true ]; then
+ git push --force-with-lease="refs/heads/${LINE_BRANCH}:${HEAD_SHA}" origin "HEAD:refs/heads/${LINE_BRANCH}"
+ echo "- pushed \`${LINE_BRANCH}\` (\`${HEAD_SHA:0:12}\` → \`$(git rev-parse --short HEAD)\`); the publish workflow builds the dev build" >> "$GITHUB_STEP_SUMMARY"
+ fi
+ exit 0
+ fi
+
+ # A carried patch conflicts: replay the patches before it onto the new
+ # pin, push that as a hand-over branch and open a pull request that
+ # names the rest. The pull request is a hand-over, not a merge — the
+ # line is a rebased branch (FORK.md, "Re-pin").
+ conflict=$(git rev-parse REBASE_HEAD)
+ git rebase --abort
+ branch="sync/$(date -u +%Y%m%d)-${NEW}"
+ git branch --force "${branch}" "${conflict}^"
+ git checkout --quiet "${branch}"
+ git rebase --quiet --onto "${NEW}" "${CURRENT}"
+ {
+ echo "Re-pin of \`${LINE_BRANCH}\` from \`${CURRENT}\` onto \`${NEW}\` stopped at a conflicting carried patch."
+ echo
+ echo "Replayed on this branch:"
+ git log --reverse --format='- `%h` %s' "${NEW}..HEAD"
+ echo
+ echo "Conflicting: \`$(git rev-parse --short "${conflict}")\` $(git log -1 --format=%s "${conflict}")"
+ echo
+ echo "Not replayed yet (in order):"
+ git log --reverse --format='- `%h` %s' "${conflict}^..${HEAD_SHA}"
+ echo
+ echo "To finish (FORK.md, \"Re-pin\"): check this branch out, \`git cherry-pick -x\` the conflicting patch and the ones behind it, resolve, run \`go build ./... && go test ./...\`, then update the line with \`git push --force-with-lease origin HEAD:${LINE_BRANCH}\` and close this pull request. Do not merge it: the line is a rebased branch, a merge would fold the old pin back in."
+ } > "${RUNNER_TEMP}/body.md"
+ {
+ echo "- re-pin: conflict at \`$(git rev-parse --short "${conflict}")\` $(git log -1 --format=%s "${conflict}")"
+ } >> "$GITHUB_STEP_SUMMARY"
+ if [ "${DRY_RUN}" != true ]; then
+ git push --force origin "HEAD:refs/heads/${branch}"
+ gh pr create --base "${LINE_BRANCH}" --head "${branch}" \
+ --title "sync: re-pin ${LINE_BRANCH} onto ${NEW} (hand-over: a carried patch conflicts)" \
+ --body-file "${RUNNER_TEMP}/body.md" >> "$GITHUB_STEP_SUMMARY"
+ fi
+ echo "::error::carried patch $(git rev-parse --short "${conflict}") conflicts with ${NEW}; hand-over branch ${branch}"
+ exit 1
diff --git a/.trivyignore b/.trivyignore
new file mode 100644
index 0000000000..7139a3e2d6
--- /dev/null
+++ b/.trivyignore
@@ -0,0 +1,21 @@
+# Copyright 2026 The Agent Substrate Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Fork (giantswarm/substrate): time-boxed acceptances of image-scan findings
+# the line cannot fix itself (publish.yaml scans every own image; a FIXABLE
+# HIGH/CRITICAL finding is never listed here — bump the module, upstream first).
+# One finding per line, with an expiry and a reason; an expired entry fails the
+# scan again and is re-triaged, never just extended:
+#
+# CVE-2026-NNNNN exp:2026-12-31 # : why no fix exists upstream, what to track, tracked in /#NN
diff --git a/CODEOWNERS b/CODEOWNERS
new file mode 100644
index 0000000000..316dd43b2b
--- /dev/null
+++ b/CODEOWNERS
@@ -0,0 +1,2 @@
+# generated by giantswarm/github actions - changes will be overwritten
+* @giantswarm/team-bumblebee
diff --git a/FORK.md b/FORK.md
new file mode 100644
index 0000000000..19cfbf143a
--- /dev/null
+++ b/FORK.md
@@ -0,0 +1,167 @@
+# The Giant Swarm line of Agent Substrate
+
+This repository is Team Bumblebee's fork of [kagent-dev/substrate](https://github.com/kagent-dev/substrate)
+(itself a fork of [agent-substrate/substrate](https://github.com/agent-substrate/substrate)). The Giant Swarm
+Agent Platform's kagent (API v2) runs every agent as a Substrate actor; this fork is where the platform's
+Substrate is pinned, built, scanned and published. It answers one question from one place: **which Substrate
+are we running, and why does it differ from upstream?**
+
+Tracking (both upstream repositories and this fork have issues disabled): the line
+[giantswarm/giantswarm#37757](https://github.com/giantswarm/giantswarm/issues/37757), upstream engagement
+[giantswarm/giantswarm#37742](https://github.com/giantswarm/giantswarm/issues/37742) (Substrate rows), the
+kagent line built the same way [giantswarm/giantswarm#37010](https://github.com/giantswarm/giantswarm/issues/37010)
+(`giantswarm/kagent-upstream`), the epic [giantswarm/giantswarm#37705](https://github.com/giantswarm/giantswarm/issues/37705).
+
+## Branches
+
+| Branch | What it is | Who moves it |
+|---|---|---|
+| `main` | A pure mirror of upstream `main`. Upstream rebases its `main` onto agent-substrate (no release tag is an ancestor of it), so the mirror is a **forced** update. Never edited, never the target of a pull request. | the sync workflow (taylorbot) only — the `protect-main` ruleset admits the `bots` team and nobody else |
+| `giantswarm` (default) | **The line**: the upstream release tag the platform's kagent pins ("the pin") + cherry-picked upstream fixes + this fork's own files. Every change of the fork's own files is a pull request against it. | pull requests (`run-tests` and `govulncheck` required); the sync workflow and repository admins may force-push it for a re-pin |
+| `fork/` | pull-request branches against `giantswarm` | anyone in the team |
+| `sync/-` | hand-over branches the sync workflow opens when a re-pin conflicts | the sync workflow; a human finishes them |
+
+## Pin
+
+| | |
+|---|---|
+| Upstream tag | **v0.0.26** (2026-09-06; tag commit `0cb6a535`) |
+| Why this one | `giantswarm/kagent-upstream` pins `github.com/kagent-dev/substrate v0.0.26` in `go/go.mod` (the `replace` of `github.com/agent-substrate/substrate`): the ate-api gRPC contract between kagent's client and Substrate's server must match. |
+| When it moves | only together with kagent's pin, proven in agentlab first (`agentlab configure --defaults --chart-branch poc/kagent-main && agentlab up` and the proofs) — see "Re-pin". Not on a schedule. |
+| Derived how | `git describe --tags --abbrev=0 --match 'v[0-9]*' --exclude '*-*' giantswarm` with upstream's tags fetched; the line's own tags carry a pre-release suffix and are excluded. The workflows compute it, nothing records it twice. |
+
+## Carried patches
+
+Everything on `giantswarm` that is not in the pin (`git log v0.0.26..giantswarm`):
+
+| Patch | Purpose | Fork commit | Upstream |
+|---|---|---|---|
+| Grant atelet cluster-wide read access to sandbox configs | atelet's sandbox-asset prewarm degraded on the second test cluster without the RBAC ([#37742](https://github.com/giantswarm/giantswarm/issues/37742) row 10) | `74b45f9e` (`git cherry-pick -x a7505e9c`) | [kagent-dev/substrate#33](https://github.com/kagent-dev/substrate/pull/33), merged 2026-09-08, not in v0.0.26 — falls away at the re-pin onto the first tag that contains it |
+| Fork infrastructure: this file, the README pointer, `CODEOWNERS`, `.github/workflows/publish.yaml`, `.github/workflows/sync-upstream.yaml`, `.trivyignore`, and the branch triggers of `pr-workflow.yaml`, `helm-e2e.yaml`, `govulncheck.yaml` (`main` → `giantswarm`, govulncheck also on pull requests) | the line's CI, publishing and sync | the `giantswarm` branch history | not for upstream |
+
+Nothing in the line changes Substrate's behaviour beyond what upstream has already merged. Giant Swarm
+specific wiring lives elsewhere: the CA/JWT pool bootstrap (`kubectl-ate admin make-ca-pool`/`make-jwt-pool`
+and the `ate-api-authentication` ConfigMap) is created by [agentlab](https://github.com/giantswarm/agentlab)
+and by meta chart 4.0; the `WorkerPool` the platform's Harnesses run on comes with the kagent chart
+(`kagent.substrateWorkerPool`); feature gates, Kyverno exceptions and network policies are cluster
+configuration.
+
+Dropped at the bootstrap of the line (2026-09-10): the three June commits of the old fork `main` — `6564754e`
+enable websockets (upstream has it: `cmd/atenet/internal/router/xds.go` `UpgradeConfigs`), `5169cdd4`
+ActorTemplate env refs (upstream kagent-dev/substrate#20, merged), `63ea2b0e` a dispatch-only release workflow
+that never ran (upstream ships `release.yaml`) — and the eight stale June branches whose patches upstream has
+merged.
+
+## Re-pin
+
+The re-pin moves the line onto a new upstream release tag and replays the carried patches; a patch upstream has
+merged falls away by itself (`git rebase` drops already-applied patches). It is the one sanctioned rewrite of
+`giantswarm`.
+
+1. kagent first: the new pin is whatever `giantswarm/kagent-upstream`'s `go/go.mod` `replace` names after its own
+ re-pin. Do not move Substrate ahead of kagent — the ate-api contract is versioned by that pin.
+2. Run **Actions → sync-upstream → Run workflow** with `pin` = the tag (for example `v0.0.27`). The workflow
+ mirrors `main`, rebases the carried patches onto the tag, runs `go build ./... && go test ./...`, and
+ force-pushes `giantswarm`. The push runs upstream's suites (`pr-workflow`, `helm-e2e`, `govulncheck`) and
+ `publish` builds the dev build.
+ - On a conflict it pushes `sync/-` (the new tag + the patches that applied before the conflict) and
+ opens a pull request that names the conflicting patch and the ones behind it. Finish it by hand: check the
+ branch out, `git cherry-pick -x` the rest, resolve, test, `git push --force-with-lease origin HEAD:giantswarm`,
+ close the pull request. **Do not merge it** — the line is a rebased branch; a merge would fold the old pin back in.
+ - `dry_run: true` does everything except the pushes; the run summary shows the outcome.
+3. Update this file (pin, carried patches) in a pull request, and the Substrate rows of #37742.
+4. Move the consumers to the new dev version (see "Consumers"), prove it in agentlab, then let the meta chart's
+ pin and kagent-upstream follow.
+
+The weekly run (Mondays 05:23 UTC) does not re-pin: it mirrors `main` and **probes** whether the carried patches
+still rebase onto upstream `main`, naming the first patch that would conflict in the run summary, so the next
+re-pin is never a surprise.
+
+Manual equivalent (a workstation, upstream as a remote):
+
+```sh
+git fetch upstream main --tags
+git checkout giantswarm
+git rebase --onto v0.0.27 v0.0.26 # new pin, old pin
+go build ./... && go test ./...
+git push --force-with-lease origin giantswarm
+```
+
+## Publishing
+
+`publish.yaml` publishes to `ghcr.io/giantswarm/substrate` on every push to `giantswarm` and on every `v*` tag;
+nothing is ever pushed by hand.
+
+| Artifact | Name |
+|---|---|
+| Control plane and node images | `ghcr.io/giantswarm/substrate/{ateapi,atecontroller,atelet,atenet,podcertcontroller}:` — linux/amd64 + linux/arm64, built with ko from `./cmd/` on the distroless base `.ko.yaml` pins |
+| Worker image | `ghcr.io/giantswarm/substrate/ateom-gvisor:` — the `WorkerPool.spec.workerImage` of the platform's pool |
+| agentgateway | `ghcr.io/giantswarm/substrate/agentgateway:` — a digest-true `crane copy` of upstream's `images.agentgateway` (atenet-router and atenet-egress run it; this repository does not build it) |
+| Charts | `oci://ghcr.io/giantswarm/substrate/helm/substrate-crds:`, `oci://ghcr.io/giantswarm/substrate/helm/substrate:` — `image.registry`, `image.tag` and `images.agentgateway` stamped to this registry; `version` = `appVersion` = the image tag |
+
+Not published from here: `ateom-microvm` and the demo images (the platform runs gVisor workers only),
+`kubectl-ate` binaries (use upstream's release), PyPI packages. Third-party images stay as upstream pins them
+(`postgres`, `rustfs`, `amazon/aws-cli`, `coredns`, `busybox`).
+
+**Versions.**
+
+- Dev build, on every push to `giantswarm`: `-dev.giantswarm...h`
+ (for the pin v0.0.26: `0.0.27-dev.giantswarm.…`), the schema the kagent line uses — base = the pin's patch + 1,
+ branch lowercased to `[a-z0-9-]`, committer date in UTC, so a rebuild of the same commit yields the same version
+ and versions sort chronologically within the branch. Consumers that follow the channel use a Flux
+ `OCIRepository` with `semver: ">=0.0.27-0 <0.1.0-0"` and `semverFilter: ".*-dev\.giantswarm\..*"`; exact pins
+ name the full string.
+- Release, on a tag `vX.Y.Z-gs.N` where `X.Y.Z` is upstream's **next** version (the dev base) and `N` counts the
+ line's releases of that pin: `v0.0.27-gs.1`. Ordering by semver: `0.0.27-dev.… < 0.0.27-gs.1 < 0.0.27`, so a dev
+ build never outranks a release, a fork release never outranks the upstream version it anticipates, and the
+ switch to an upstream tag one day is a range change, not a rename. A fleet consumer follows
+ `semverFilter: ".*-gs\..*"`.
+- `workflow_dispatch` with a `version` input publishes that string (for a one-off).
+
+**Digests.** Every run writes an `Images`/`Charts` table with the digest of each pushed artifact to its summary
+and uploads them as the `image-refs` artifact; the platform pins by tag and verifies by digest from there. Release
+digests are recorded here:
+
+| Release | Pin | Images and charts |
+|---|---|---|
+| none yet | | |
+
+**Scans.** Every own image is scanned with Trivy (HIGH and CRITICAL, fixable only) after the push and before the
+charts that reference it are published. A fixable finding fails the publish: bump the module (upstream first) or,
+when upstream has no fix, add a time-boxed entry to `.trivyignore` (`CVE-… exp:YYYY-MM-DD # reason, tracking
+issue`) — an expired entry fails again and is re-triaged, not extended. The mirrored agentgateway image is
+scanned report-only; its findings belong upstream. `govulncheck` covers the Go module graph on every push, pull
+request and weekly.
+
+## Consumers
+
+| Consumer | Where the pin lives | Selects |
+|---|---|---|
+| [agentlab](https://github.com/giantswarm/agentlab) | `internal/lab/substrate.go` (`substrateChartsRepo`, `substrateImageRegistry`, `substrateVersion`) | an exact dev version or release; installs `substrate-crds` + `substrate` and preloads the worker image |
+| agent-platform meta chart, branch `poc/kagent-main` | `helm/agent-platform/values.yaml` `kagent.substrateWorkerPool.workerImage` | the `ateom-gvisor` image at an exact version (the `WorkerPool` the kagent chart renders) |
+| [giantswarm/kagent-upstream](https://github.com/giantswarm/kagent-upstream) | `Makefile` `SUBSTRATE_REPO ?= oci://ghcr.io/giantswarm/substrate/helm`, `SUBSTRATE_VERSION` | the `substrate`/`substrate-crds` chart dependencies of the kagent charts (off in the platform, which installs Substrate as cluster infrastructure) |
+
+## Assets that are not images
+
+- **gVisor `runsc`**: the chart's `SandboxConfig gvisor-default` (`charts/substrate/templates/sandboxconfig-gvisor.yaml`)
+ names `gs://gvisor/releases/release/20260803/{x86_64,aarch64}/gvisor.tar.bz2`; atelet downloads the release
+ tarball at prewarm. Unchanged from upstream and not mirrored; a cluster needs egress to
+ `storage.googleapis.com` from the atelet pods (or an override of `spec.assets` on the SandboxConfig).
+- **micro-VM assets** (kata, cloud-hypervisor, virtiofsd): assembled by `hack/microvm-assets/assemble.sh` for the
+ e2e suites; the platform does not run the micro-VM sandbox class.
+
+## Contributing
+
+- **Upstream first.** Every behavioural change is a pull request to
+ [kagent-dev/substrate](https://github.com/kagent-dev/substrate) (issues are disabled there; discussion goes
+ through the pull request or the kagent community channels) with DCO sign-off (`git commit -s`); pull-request
+ workflows from forks wait for a maintainer's approval. The line carries the same change as a
+ `git cherry-pick -x` of the upstream commit (or, before merge, of your pull-request branch) until an upstream
+ release contains it, with a row in #37742.
+- **Fork-only changes** (workflows, this file): a pull request from `fork/` against `giantswarm`.
+- **Experiments**: your own personal fork. Branches here exist to become pull requests.
+- **What CI runs on a pull request**: upstream's `pr-workflow` (unit, root-gated and e2e suites on kind, gVisor and
+ micro-VM lanes), `helm-e2e` (the charts on kind) and `govulncheck`; `run-tests` and `govulncheck` are required.
+ `publish` runs only on the branch and on tags.
+- **Do not** dispatch upstream's `release.yaml` here (it is the mirror's file; it would push to this registry under
+ an arbitrary tag and try to push charts to upstream's), and do not push tags other than `vX.Y.Z-gs.N` releases.
diff --git a/README.md b/README.md
index dfb13f7fe9..0ae046289e 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,10 @@
+> **Giant Swarm line.** This repository is Team Bumblebee's fork of
+> [kagent-dev/substrate](https://github.com/kagent-dev/substrate) for the Giant
+> Swarm Agent Platform: `main` mirrors upstream, the `giantswarm` branch is what
+> the platform runs and publishes (`ghcr.io/giantswarm/substrate`). Pin,
+> carried patches, published artifacts, the re-pin procedure and how to
+> contribute: [FORK.md](FORK.md).
+
# Agent Substrate
[](https://opensource.org/licenses/Apache-2.0)