Skip to content
Provenance

SLSA Build Track Level 3 for Agent-Generated Artifacts

What SLSA Build Track Level 3 actually requires when the source-track author is an autonomous coding agent — hermetic builds, isolated builders, and signed provenance you can verify with slsa-verifier.

Advanced 12 min read Updated May 2026

SLSA Source Track answers who authored each line of code reaching production — including which autonomous agent and which model. SLSA Build Track answers a different question: how was the artifact built, by what builder, in what environment, from what inputs?

Build Track matters more, not less, when the upstream author is an LLM. Agent-generated source code is non-deterministic at authorship time; the build pipeline is the only point in the chain where determinism, hermeticity, and verifiable inputs are still achievable. Build Track 3 is where you re-establish trust after the agent has already done its non-deterministic work.

This article is the build-side companion to the source-side slsa-source-track.mdx. It covers what Build Track L3 actually requires, how to wire it for agent-generated artifacts, and how to verify it end-to-end with slsa-verifier.

SLSA Build Track Levels — Side by Side

The SLSA v1.0 specification breaks Build Track into three levels:

LevelProducer RequirementsVerifier Capability
L1Provenance exists, generated by some processVerify that provenance was produced
L2Provenance is signed by the build platform; runs on a hosted builderVerify the build platform identity
L3Build runs on a hardened, isolated builder; provenance is unforgeable by tenantsVerify the builder is L3-qualified and tenants cannot forge

L3 is the threshold where "the agent-authored code was built by something an attacker can't quietly substitute." It is also the threshold federal procurement (CNSA, FedRAMP 20x) increasingly requires for agent-touched software.

What L3 Requires When the Author Is an Agent

Build Track L3 adds two hard requirements on top of L2, and both acquire extra weight when the upstream code was generated by a coding agent:

1. Isolated build environment. The build platform must guarantee that builds run free from external influence — no cross-build interference, no access to other tenants' secrets, no persistence between runs. In practice this means a hosted builder on infrastructure the project does not administer. For agent code this matters because a self-hosted builder shared with the agent's runtime could let a prompt-injected agent corrupt the build. L3 forbids that topology.

2. Provenance is unforgeable by tenants. The signing key is held by the build platform's trusted control plane and is not exposed to user-defined build steps. Every provenance field is generated or verified by that control plane, so even a build step that has been agent-tampered cannot mint a provenance for a different artifact. This is what stops "agent generates malicious build script that signs the wrong SBOM."

Two related properties support L3 but are not themselves the L3 hard requirements:

Build inputs are fully declared (provenance completeness). Every source repo, dependency, and container base image should appear in the provenance — a property the Build Track builds up across L1–L3 rather than a standalone L3 gate. For agent runs this ideally includes the agent's policy pack and model fingerprint, so the build provenance can reach back to the agent identity certificate and prove which agent commits are baked in.

Hermetic builds (aspirational, not required at L3). The SLSA v1.0 spec explicitly notes that "Hermetic" — building with no network access — is not an L3 requirement; it is listed under future directions. Restricting build-time network access and pinning floating tags to digests is still strong hygiene, especially for agent-authored Dockerfiles that love FROM node:latest, but treat it as a higher bar you adopt on top of L3, not as something L3 itself mandates.

A Reference L3 Build for Agent-Authored Code

The two production L3 builders today are the SLSA GitHub Generator and Google Cloud Build with the slsa.dev/v1 provenance format. The GitHub generator is the most accessible. A minimal workflow:

name: build-and-attest
on:
  push:
    tags: ['v*']

permissions:
  id-token: write
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - id: agent-context
        name: Resolve agent commits in this build
        run: |
          # Find every commit between previous tag and HEAD
          # that was authored by a registered agent identity
          PREV_TAG=$(git describe --tags --abbrev=0 HEAD^)
          git log "$PREV_TAG"..HEAD --format='%H %ae' \
            | awk '/agents\.crashoverride\.com/' \
            > agent-commits.txt
          echo "agent_commit_count=$(wc -l < agent-commits.txt)" >> "$GITHUB_OUTPUT"

      - id: build
        name: Hermetic container build
        run: |
          docker buildx build \
            --no-cache \
            --pull \
            --provenance=mode=max \
            --sbom=true \
            --tag ghcr.io/crashoverride/api-gateway:${{ github.sha }} \
            --output type=registry,push=true \
            .
          DIGEST=$(docker buildx imagetools inspect \
            ghcr.io/crashoverride/api-gateway:${{ github.sha }} \
            --format '{{ .Manifest.Digest }}')
          echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"

  provenance:
    needs: build
    permissions:
      id-token: write
      packages: write
      actions: read
    uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
    with:
      image: ghcr.io/crashoverride/api-gateway
      digest: ${{ needs.build.outputs.digest }}
      registry-username: ${{ github.actor }}
    secrets:
      registry-password: ${{ secrets.GITHUB_TOKEN }}

The split is critical: the build job produces the artifact; the provenance job — which runs on a separate, locked-down reusable workflow — generates and signs the provenance. The build job never touches the signing key. That is what unforgeable by tenants means in practice.

What the L3 Provenance Looks Like

The generated SLSA v1.0 provenance for an agent-built container:

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [{
    "name": "ghcr.io/crashoverride/api-gateway",
    "digest": { "sha256": "def456ghi789..." }
  }],
  "predicateType": "https://slsa.dev/provenance/v1",
  "predicate": {
    "buildDefinition": {
      "buildType": "https://slsa-framework.github.io/container-generator/buildtype/v1",
      "externalParameters": {
        "source": "git+https://github.com/crashoverride/api-gateway@refs/tags/v2.14.1",
        "image": "ghcr.io/crashoverride/api-gateway"
      },
      "internalParameters": {
        "GITHUB_EVENT_NAME": "push",
        "GITHUB_REF": "refs/tags/v2.14.1",
        "GITHUB_REPOSITORY_OWNER_ID": "147823891",
        "GITHUB_RUNNER_ENVIRONMENT": "github-hosted"
      },
      "resolvedDependencies": [{
        "uri": "git+https://github.com/crashoverride/api-gateway@refs/tags/v2.14.1",
        "digest": { "gitCommit": "def456ghi789..." }
      }]
    },
    "runDetails": {
      "builder": {
        "id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.1.0"
      },
      "metadata": {
        "invocationId": "https://github.com/crashoverride/api-gateway/actions/runs/9482839201/attempts/1",
        "startedOn": "2026-05-01T14:32:00Z",
        "finishedOn": "2026-05-01T14:38:42Z"
      }
    }
  }
}

Two fields earn their keep:

  • runDetails.builder.id is pinned to a tagged release of the reusable workflow. A verifier can confirm the build ran on the L3-qualified workflow, not a fork.
  • internalParameters.GITHUB_RUNNER_ENVIRONMENT proves the build ran on a GitHub-hosted runner (isolated). A self-hosted runner would show self-hosted here and fail L3 verification.

Verification with slsa-verifier

The verifier side is where Build Track L3 turns into a deployment gate:

# Install
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest

# Verify a container artifact
slsa-verifier verify-image \
  ghcr.io/crashoverride/api-gateway@sha256:def456ghi789... \
  --source-uri github.com/crashoverride/api-gateway \
  --source-tag v2.14.1 \
  --builder-id 'https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.1.0'

A successful run prints:

PASSED: SLSA verification passed

Wire it into your deployment pipeline:

#!/usr/bin/env bash
set -euo pipefail

IMAGE="ghcr.io/crashoverride/api-gateway@sha256:${DIGEST}"
EXPECTED_BUILDER='https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.1.0'

if ! slsa-verifier verify-image "$IMAGE" \
    --source-uri "github.com/crashoverride/api-gateway" \
    --source-tag "$RELEASE_TAG" \
    --builder-id "$EXPECTED_BUILDER"; then
  echo "FAIL: artifact does not have a verified SLSA L3 build provenance" >&2
  exit 1
fi

# Cross-check: does the provenance declare any resolved inputs at all?
PROV=$(cosign download attestation "$IMAGE" \
  | jq -r '.payload | @base64d | fromjson | .predicate')
RESOLVED_DEP_COUNT=$(echo "$PROV" \
  | jq -r '.buildDefinition.resolvedDependencies | length')

if [ "$RESOLVED_DEP_COUNT" -lt 1 ]; then
  echo "FAIL: provenance has no resolved dependencies" >&2
  exit 1
fi

kubectl set image deployment/api-gateway api-gateway="$IMAGE"

The shell gate now refuses any artifact that didn't run on the pinned L3 workflow, even if it's signed.

Composing Build Track L3 with Agent Source Provenance

Where this becomes powerful is when you join the build provenance to the agent source provenance. The build provenance lists resolvedDependencies[0].digest.gitCommit. That commit's signature, by the agent identity workflow described in our cryptographic provenance article, carries the agent's model and policy hashes. A combined verifier:

# 1. Verify the build was L3
slsa-verifier verify-image "$IMAGE" \
  --source-uri "github.com/crashoverride/api-gateway" \
  --source-tag "$RELEASE_TAG" \
  --builder-id "$EXPECTED_BUILDER" || exit 1

# 2. Pull the source commit from build provenance
COMMIT=$(slsa-verifier verify-image "$IMAGE" \
  --source-uri "github.com/crashoverride/api-gateway" \
  --source-tag "$RELEASE_TAG" \
  --builder-id "$EXPECTED_BUILDER" \
  --print-provenance \
  | jq -r '.predicate.buildDefinition.resolvedDependencies[0].digest.gitCommit')

# 3. For every agent-authored commit reachable from $COMMIT,
#    verify the agent identity signature
for HASH in $(git log "$COMMIT" --format='%H' --grep '^Agent-Authored:'); do
  cosign verify-blob \
    --certificate-identity-regexp '^agent:.*' \
    --certificate-oidc-issuer https://agents.crashoverride.com \
    --signature "$HASH.sig" \
    "$HASH.commit-msg" || {
      echo "FAIL: agent commit $HASH lacks valid agent identity signature" >&2
      exit 1
    }
done

This is the chain regulators are starting to ask for: from a running container in production back to a specific agent run, with both build and source provenance signed and verifiable.

The Hermeticity Problem with Agent-Generated Build Files

Agents love to write Dockerfiles like:

FROM node:latest
RUN apt-get update && apt-get install -y curl
RUN curl https://example.com/install.sh | bash
COPY . .
RUN npm install

Every line breaks hermeticity. node:latest is a floating tag. apt-get update reaches the network unpinned. curl | bash is uncontrolled remote code. npm install resolves dependencies at build time without a lockfile guarantee. None of this fails SLSA L3 verification on its own — L3 does not mandate hermeticity — but it is exactly the higher-bar hygiene worth enforcing on agent-authored builds.

A pre-merge gate that rejects agent-authored Dockerfiles that violate hermeticity:

# Run this in CI on every PR with agent-authored changes to Dockerfile
ERRORS=0

if grep -E '^FROM .*:latest' Dockerfile; then
  echo "Dockerfile uses floating :latest tag — pin to digest"
  ERRORS=$((ERRORS+1))
fi

if grep -E '^RUN curl .* \| (bash|sh)' Dockerfile; then
  echo "Dockerfile pipes curl to shell — fetch and verify instead"
  ERRORS=$((ERRORS+1))
fi

if grep -E '^FROM [^@]+$' Dockerfile | grep -v '@sha256:'; then
  echo "Base image not pinned to sha256 digest"
  ERRORS=$((ERRORS+1))
fi

exit "$ERRORS"

Run this in the same PR check that flags agent commits. Agents are great at writing code that works; they are mediocre at writing code that builds hermetically. The gate is what stops the difference from leaking into production.

What This Means in Practice

For a team adopting Build Track L3 on agent-authored code:

  • Move builds to a hosted, isolated runner. Stop using self-hosted GitHub Actions runners for any artifact that ships to production.
  • Adopt the SLSA reusable workflow generators. The pinned @v2.1.0 reusable workflow is the L3 boundary; do not fork it.
  • Pin every base image to a sha256 digest. Add a CI gate that fails PRs introducing floating tags.
  • Enforce slsa-verifier at the deployment gate. Unsigned or unverified artifacts must not deploy.
  • Join build provenance to source provenance. The chain from running container → build run → source commit → agent identity is what compliance auditors will ask for.

Build Track L3 is achievable today on GitHub Actions for most teams in under a sprint. The harder work is the cultural shift: treating agent-authored Dockerfiles and CI configs with the same suspicion you'd apply to a pull request from an unknown contributor.

Sources

This article is part of the Provenance knowledge series (4 articles) Browse all Provenance articles →
Related Use Case

Software Compliance — Your last compliance vendor

Don't fake the evidence. Trust it.

Explore Use Case →