AID-H-003.002: CI/CD Release Gating, Model Artifact Signing & Secure Distribution

Version 1.20261015 · Actionable sub-technique

Apply mandatory automated acceptance criteria before promoting a model or deployment-bound model package. Verify origin, integrity, provenance, and the exact model-interface files and selectors that ship with the release. Production trusts only immutable, attested bytes from an internal mirror; a signed weight file does not authorize a different tokenizer, chat template, generation configuration, or parser.

Pillars: infra, model

Lifecycle: building, validation, operation

Scope boundary

Owns the final automated promotion, mirror-refresh, transfer, deployment, load, and reload decision and its enforcement. It independently consumes composition, integrity, source-tombstone, format, loader, and other policy-required evidence, signs and distributes approved immutable releases, and releases only the actual opened verified bytes to runtime.

AID-H-003.001: AID-H-003.002 gates and signs the completed model release; AID-H-003.001 admits the third-party software and build-tool population used to produce it. A release can fail promotion despite approved dependencies, and a signed release does not excuse unapproved build inputs.

AID-H-003.006: AID-H-003.002 makes and enforces the final release and runtime-load decision; AID-H-003.006 produces and independently verifies exact model composition, provenance, source-status, and loader evidence. Evidence helpers cannot create another promotion authority, and their successful results cannot replace the complete release policy or earn duplicate enforcement credit.

AID-M-002.002: AID-M-002.002 creates and verifies the whole-artifact digest and signature verdict consumed by release policy; AID-H-003.002 owns the final promotion, signing, mirroring, and distribution decision. A valid integrity verdict is necessary gate evidence but cannot promote a release by itself.

AID-H-003.008: AID-H-003.002 applies the currently approved signature and release policy; AID-H-003.008 makes the active signing and verification path crypto-agile across algorithm and policy migrations. Promotion semantics and cryptographic migration mechanics remain separate.

AID-H-003.010: AID-H-003.002 gates, signs, and distributes a rebuilt release artifact; AID-H-003.010 determines vulnerability applicability and remediation priority, then verifies rollout completion across the deployed population. A signed fixed release does not prove that every affected instance received it.

Implementation guidance

AID-H-003.002-G001

Store in internal registry; require signatures and digest pinning for promotion.
Production implementation: promote an immutable OCI artifact and verify it after push
set -euo pipefail
umask 077
: "$OCI_REPOSITORY"
: "$RELEASE_ID"
: "$MODEL_TARBALL"
: "$MODEL_SBOM"
case "$OCI_REPOSITORY" in */*) ;; *) echo "full internal registry/repository required" >&2; exit 2 ;; esac
test -s "$MODEL_TARBALL"
test -s "$MODEL_SBOM"
TAG_REF="$OCI_REPOSITORY:staging-$RELEASE_ID"
RESULT=$(oras push --format json \
  --artifact-type application/vnd.aidefend.model.v1 \
  "$TAG_REF" \
  "$MODEL_TARBALL:application/vnd.aidefend.model.layer.v1+tar" \
  "$MODEL_SBOM:application/vnd.aidefend.model.sbom.v2+json")
DIGEST=$(printf "%s" "$RESULT" | jq -er '.digest | select(test("^sha256:[a-f0-9]{64}$"))')
IMMUTABLE_REF="$OCI_REPOSITORY@$DIGEST"
oras manifest fetch "$IMMUTABLE_REF" > release/manifest.json
cosign sign --yes "$IMMUTABLE_REF"
cosign verify \
  --certificate-identity "$(cat /opt/aidefend/trust/model-release.identity)" \
  --certificate-oidc-issuer "$(cat /opt/aidefend/trust/model-release.issuer)" \
  "$IMMUTABLE_REF" > release/signature-verification.json
printf "%s\n" "$IMMUTABLE_REF" > release/immutable-model-ref.txt

Action: Promote and deploy only immutable-model-ref.txt after independently re-resolving the manifest and verifying its signature and required attestations. Tags are publication aliases, never deployment identity. Retain the pushed and fetched manifests, layer digests, signer identity, transparency evidence, registry/repository, and release-policy digest.

Before you begin

Apply AID-H-003.002-G001 only when a model, adapter, dataset, container, configuration, or release bundle moves into a trusted registry, deployment ring, or runtime.

AID-H-003.002-G002

Use reproducible, safe packaging and verify on deploy.
Transfer and release preflight

This standalone archive verifier prepares integrity evidence for a transferred candidate. It does not authorize a runtime load or serving-reference switch. At load/reload use AID-H-003.002-G003; its canonical path coordinates the signed AID-H-003.006-G001/G002 composition producers and performs one protected capture plus one verified reconstruction. Do not run this optional preflight extraction as an additional mandatory runtime copy.

Concept

Build from a private, read-only export staging directory. Keep the manifest and detached signature outside the archive trust boundary. Before a loader can see the release, verify the manifest signature and archive digest, reject non-canonical or non-regular members, reconstruct a new versioned directory, and require the signed manifest population to equal the archive population exactly.

Build a deterministic archive and external manifest
# File: model_release/build_package_manifest.py
from __future__ import annotations

import argparse
import hashlib
import json
import os
import stat
import tarfile
from pathlib import Path, PurePosixPath

from model_sbom.interface_population import load_population_policy, require_member, require_population


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    descriptor = os.open(
        path, os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
    )
    with os.fdopen(descriptor, "rb") as handle:
        if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
            raise ValueError(f"not a regular file: {path}")
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def require_external(root: Path, candidate: Path) -> None:
    try:
        candidate.resolve(strict=False).relative_to(root)
    except ValueError:
        return
    raise ValueError(f"release evidence must be outside model_dir: {candidate}")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--model-dir", type=Path, required=True)
    parser.add_argument("--archive", type=Path, required=True)
    parser.add_argument("--manifest", type=Path, required=True)
    for name in ("loader-policy", "loader-bundle", "release-policy", "release-bundle"):
        parser.add_argument("--" + name, type=Path, required=True)
    args = parser.parse_args()
    population_policy = load_population_policy(args.loader_policy, args.loader_bundle, args.release_policy, args.release_bundle)
    root = args.model_dir.resolve(strict=True)
    if not root.is_dir() or root.is_symlink():
        raise ValueError("model_dir must be a real directory")
    require_external(root, args.archive)
    require_external(root, args.manifest)

    members = []
    for path in sorted(root.rglob("*")):
        status = path.lstat()
        if stat.S_ISDIR(status.st_mode):
            continue
        if stat.S_ISLNK(status.st_mode) or not stat.S_ISREG(status.st_mode):
            raise ValueError(f"symlink or special member is forbidden: {path}")
        name = PurePosixPath(path.relative_to(root).as_posix())
        if name.is_absolute() or ".." in name.parts or name.as_posix() in {"", "."}:
            raise ValueError(f"non-canonical member path: {name}")
        require_member(name.as_posix(), population_policy.document)
        members.append({
            "path": name.as_posix(),
            "size": status.st_size,
            "sha256": sha256_file(path),
            "source": path,
        })
    if not members:
        raise ValueError("model export is empty")

    require_population([row["path"] for row in members], population_policy.document)
    args.archive.parent.mkdir(parents=True, exist_ok=True)
    archive_tmp = args.archive.with_suffix(args.archive.suffix + ".tmp")
    with tarfile.open(archive_tmp, "w", format=tarfile.PAX_FORMAT) as package:
        for member in members:
            status = member["source"].lstat()
            if not stat.S_ISREG(status.st_mode) or status.st_size != member["size"]:
                raise ValueError(f"source changed during packaging: {member['path']}")
            info = tarfile.TarInfo(member["path"])
            info.size = member["size"]
            info.mode, info.uid, info.gid, info.mtime = 0o400, 0, 0, 0
            descriptor = os.open(
                member["source"],
                os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0),
            )
            with os.fdopen(descriptor, "rb") as source:
                package.addfile(info, source)
    archive_tmp.replace(args.archive)

    manifest = {
        "schema_version": "aidefend.model-release-manifest.v2",
        "model_dir": root.name,
        "archive_sha256": sha256_file(args.archive),
        "loader_policy_sha256": population_policy.sha256,
        "files": [
            {key: member[key] for key in ("path", "size", "sha256")}
            for member in members
        ],
    }
    args.manifest.parent.mkdir(parents=True, exist_ok=True)
    temporary = args.manifest.with_suffix(args.manifest.suffix + ".tmp")
    temporary.write_text(
        json.dumps(manifest, sort_keys=True, separators=(",", ":"), allow_nan=False) + "\n",
        encoding="utf-8",
    )
    temporary.replace(args.manifest)


if __name__ == "__main__":
    main()
python -m model_release.build_package_manifest \
  --model-dir model_export \
  --archive artifacts/model.tar \
  --manifest evidence/release-manifest.json \
  --loader-policy policy/model-loader-policy.json \
  --loader-bundle policy/model-loader-policy.sigstore.json \
  --release-policy policy/model-release-trust.json \
  --release-bundle policy/model-release-trust.sigstore.json
cosign sign-blob --yes --key env://MODEL_RELEASE_SIGNING_KEY \
  --bundle evidence/release-manifest.sig evidence/release-manifest.json
Verify before extraction and load
# File: model_release/verify_before_load.py
from __future__ import annotations

import argparse
import hashlib
import json
import os
import re
import shutil
import stat
import subprocess
import tarfile
import tempfile
from pathlib import Path, PurePosixPath

from model_sbom.interface_population import load_population_policy, require_population

SHA256 = re.compile(r"^[a-f0-9]{64}$")


class ModelPackageRejected(Exception):
    pass


def regular_bytes(path: Path) -> bytes:
    descriptor = os.open(
        path, os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
    )
    with os.fdopen(descriptor, "rb") as handle:
        if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
            raise ModelPackageRejected(f"not a regular file: {path}")
        value = handle.read()
    if not value:
        raise ModelPackageRejected(f"empty release evidence: {path}")
    return value


def unique_object(pairs):
    value = {}
    for key, item in pairs:
        if key in value:
            raise ModelPackageRejected(f"duplicate JSON key: {key}")
        value[key] = item
    return value


def signed_manifest(
    manifest_path: Path, bundle_path: Path, key: Path, timeout: float,
) -> tuple[dict, str]:
    payload, bundle = regular_bytes(manifest_path), regular_bytes(bundle_path)
    with tempfile.TemporaryDirectory(prefix="aidefend-model-evidence-") as stage_name:
        stage = Path(stage_name)
        snapshot, signature = stage / "manifest.json", stage / "manifest.sig"
        snapshot.write_bytes(payload)
        signature.write_bytes(bundle)
        subprocess.run(
            ["cosign", "verify-blob", "--key", str(key), "--bundle", str(signature), str(snapshot)],
            check=True, capture_output=True, text=True, timeout=timeout,
        )
        if snapshot.read_bytes() != payload:
            raise ModelPackageRejected("verified manifest snapshot changed")
    value = json.loads(
        payload.decode("utf-8", errors="strict"),
        object_pairs_hook=unique_object,
        parse_constant=lambda item: (_ for _ in ()).throw(
            ModelPackageRejected(f"non-finite JSON number: {item}")
        ),
    )
    if not isinstance(value, dict) or set(value) != {
        "schema_version", "model_dir", "archive_sha256", "files", "loader_policy_sha256",
    } or value["schema_version"] != "aidefend.model-release-manifest.v2":
        raise ModelPackageRejected("release manifest schema differs")
    return value, hashlib.sha256(payload).hexdigest()


def canonical(value: object) -> PurePosixPath:
    if not isinstance(value, str) or "\\" in value:
        raise ModelPackageRejected("member path is not canonical POSIX")
    path = PurePosixPath(value)
    if path.is_absolute() or path.as_posix() != value or value in {"", "."} or ".." in path.parts:
        raise ModelPackageRejected(f"unsafe member path: {value!r}")
    return path


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--archive", type=Path, required=True)
    parser.add_argument("--manifest", type=Path, required=True)
    parser.add_argument("--bundle", type=Path, required=True)
    parser.add_argument("--key", type=Path, required=True)
    parser.add_argument("--output", type=Path, required=True)
    parser.add_argument("--max-files", type=int, required=True)
    parser.add_argument("--max-total-bytes", type=int, required=True)
    parser.add_argument("--verification-timeout", type=float, required=True)
    for name in ("loader-policy", "loader-bundle", "release-policy", "release-bundle"):
        parser.add_argument("--" + name, type=Path, required=True)
    args = parser.parse_args()
    population_policy = load_population_policy(args.loader_policy, args.loader_bundle, args.release_policy, args.release_bundle)
    if args.max_files < 1 or args.max_total_bytes < 1 or args.verification_timeout <= 0:
        raise ModelPackageRejected("deployment limits are invalid")
    if args.output.exists() or args.output.is_symlink():
        raise ModelPackageRejected("output must be a new versioned directory")

    manifest, manifest_sha256 = signed_manifest(
        args.manifest, args.bundle, args.key, args.verification_timeout
    )
    if manifest["loader_policy_sha256"] != population_policy.sha256:
        raise ModelPackageRejected("preflight and current signed interface policies differ")
    if not SHA256.fullmatch(str(manifest["archive_sha256"])):
        raise ModelPackageRejected("archive digest is malformed")
    entries = manifest["files"]
    if not isinstance(entries, list) or not entries or len(entries) > args.max_files:
        raise ModelPackageRejected("manifest population is empty or exceeds policy")
    expected, total_bytes = {}, 0
    for entry in entries:
        if not isinstance(entry, dict) or set(entry) != {"path", "size", "sha256"}:
            raise ModelPackageRejected("manifest member schema differs")
        name, size = canonical(entry["path"]).as_posix(), entry["size"]
        if (
            isinstance(size, bool) or not isinstance(size, int) or size < 0
            or not SHA256.fullmatch(str(entry["sha256"])) or name in expected
        ):
            raise ModelPackageRejected("invalid or duplicate manifest member")
        total_bytes += size
        expected[name] = entry
    require_population(expected, population_policy.document)
    if total_bytes > args.max_total_bytes:
        raise ModelPackageRejected("manifest size exceeds deployment policy")

    args.output.parent.mkdir(parents=True, exist_ok=True)
    stage = Path(tempfile.mkdtemp(prefix=args.output.name + ".", dir=args.output.parent))
    observed = set()
    try:
        descriptor = os.open(
            args.archive,
            os.O_RDONLY | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0),
        )
        with os.fdopen(descriptor, "rb") as archive_handle:
            if not stat.S_ISREG(os.fstat(archive_handle.fileno()).st_mode):
                raise ModelPackageRejected("archive is not a regular file")
            archive_digest = hashlib.sha256()
            for chunk in iter(lambda: archive_handle.read(1024 * 1024), b""):
                archive_digest.update(chunk)
            if archive_digest.hexdigest() != manifest["archive_sha256"]:
                raise ModelPackageRejected("archive digest differs from signed manifest")
            archive_handle.seek(0)
            with tarfile.open(fileobj=archive_handle, mode="r:*") as package:
                for member in package:
                    name = canonical(member.name).as_posix()
                    if not member.isreg() or name not in expected or name in observed:
                        raise ModelPackageRejected(f"unexpected, duplicate, or non-file member: {name}")
                    entry = expected[name]
                    if member.size != entry["size"]:
                        raise ModelPackageRejected(f"member size differs: {name}")
                    source = package.extractfile(member)
                    if source is None:
                        raise ModelPackageRejected(f"member cannot be read: {name}")
                    target = stage.joinpath(*PurePosixPath(name).parts)
                    target.parent.mkdir(parents=True, exist_ok=True)
                    member_digest = hashlib.sha256()
                    with target.open("xb") as destination:
                        for chunk in iter(lambda: source.read(1024 * 1024), b""):
                            member_digest.update(chunk)
                            destination.write(chunk)
                    if member_digest.hexdigest() != entry["sha256"]:
                        raise ModelPackageRejected(f"member digest differs: {name}")
                    target.chmod(0o400)
                    observed.add(name)
        if observed != set(expected):
            raise ModelPackageRejected("archive has missing or extra members")
        os.replace(stage, args.output)
    except Exception:
        shutil.rmtree(stage, ignore_errors=True)
        raise
    print(json.dumps({
        "outcome": "PASS",
        "manifest_sha256": manifest_sha256,
        "archive_sha256": manifest["archive_sha256"],
        "file_count": len(observed),
        "output": str(args.output),
    }, sort_keys=True))


if __name__ == "__main__":
    main()
python -m model_release.verify_before_load \
  --archive artifacts/model.tar \
  --manifest evidence/release-manifest.json \
  --loader-policy policy/model-loader-policy.json \
  --loader-bundle policy/model-loader-policy.sigstore.json \
  --release-policy policy/model-release-trust.json \
  --release-bundle policy/model-release-trust.sigstore.json \
  --bundle evidence/release-manifest.sig \
  --key keys/model-release.pub \
  --output /models/releases/support-agent-20260721 \
  --max-files "$MODEL_PACKAGE_MAX_FILES" \
  --max-total-bytes "$MODEL_PACKAGE_MAX_TOTAL_BYTES" \
  --verification-timeout "$MODEL_PACKAGE_VERIFY_TIMEOUT_SECONDS"

Verification: Run negative fixtures for an unsigned manifest, a changed archive, duplicate JSON keys, traversal paths, symlinks, special members, an unlisted extra file, a missing file, digest mismatch, and policy limits. Every case must reject before a loader can observe the versioned output. An integrity-only result cannot authorize a serving-reference switch. Supply independently verified package evidence to AID-H-003.002-G003, which checks the complete current release policy before exposing the private artifact/tree. Never load from transfer or unverified extraction staging.

Install model_sbom.interface_population from AID-H-003.006-G001. Preflight reads the same signed loader-policy v3 as packaging and the canonical gate, binds its digest into model-release-manifest v2, and rejects a missing required interface member or a policy change. Review and regenerate v1 manifests before adoption; an integrity-only preflight still cannot authorize a load.

Run these module commands from the installed package root used by AID-H-003.006. Do not execute the .py file path directly when sibling namespaces are not installed; the canonical package layout requires no source-root PYTHONPATH override.

AID-H-003.002-G003

Enforce the final load decision over one captured immutable artifact and its verified complete interface, record ALLOW or DENY independently, and expose only that same snapshot/tree to the installed loader.
Canonical release and runtime-load gate

Install this coordinator and its G006-G009 evidence modules in the approved gate image. Release approval, loader policy, signer roots and evidence callbacks are deployment configuration, never model or request inputs. Use one gate-owned read-only capture; for packages, the composition module checks and reconstructs the exact signed member population once. Integrity and format helpers do not create another authorization or another large artifact copy.

# File: runtime/verify_model_artifact.py
from __future__ import annotations

import hashlib
import math
import os
import re
import stat
import subprocess
import tempfile
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path

from model_sbom.admit_model_artifact import strict_json_object, load_release_policy, verified_json_bytes, gate_snapshot
from model_sbom.runtime_guard import runtime_verify
from runtime.verify_model_loader_policy import verify_model, load_loader_policy
from supply_chain.verify_tombstones import check as check_tombstones

FIELDS = {"schema_version", "model_id", "release_id", "artifact_sha256", "sbom_attestation_ref", "not_after", "policy_version"}
SHA256 = re.compile(r"^[a-f0-9]{64}$")
OCI_DIGEST = re.compile(r"^oci://[^@\s]+@sha256:[a-f0-9]{64}$")
# Mount these deployment-authority inputs read-only, outside the model workspace.
VERIFY_KEY = Path("/opt/aidefend/trust/model-release.pub")

def release_manifest(manifest_path: Path, signature_path: Path) -> tuple[dict, str]:
    timeout = float(os.environ["VERIFIED_H003_MODEL_RELEASE_VERIFY_TIMEOUT_SECONDS"])
    if not math.isfinite(timeout) or timeout <= 0:
        raise ValueError("verification timeout must come from a verified positive policy value")
    raw, signature = manifest_path.read_bytes(), signature_path.read_bytes()
    with tempfile.TemporaryDirectory(prefix="model-release-manifest-") as directory:
        snapshot, bundle = Path(directory)/"release.json", Path(directory)/"release.bundle"
        snapshot.write_bytes(raw); bundle.write_bytes(signature)
        subprocess.run(["cosign", "verify-blob", "--key", str(VERIFY_KEY), "--bundle", str(bundle), str(snapshot)],
                       check=True, capture_output=True, text=True, timeout=timeout)
        if snapshot.read_bytes() != raw:
            raise PermissionError("verified manifest changed")
    value = strict_json_object(raw, "release manifest")
    if set(value) != FIELDS or value["schema_version"] != "aidefend.model-release.v1":
        raise ValueError("release manifest schema differs")
    if not SHA256.fullmatch(value["artifact_sha256"]) or not OCI_DIGEST.fullmatch(value["sbom_attestation_ref"]):
        raise ValueError("release digest or attestation reference differs")
    if any(not isinstance(value[k],str) or not value[k] for k in ("model_id","release_id","policy_version")):
        raise ValueError("release identity is incomplete")
    expiry = datetime.fromisoformat(value["not_after"].replace("Z", "+00:00"))
    if expiry.tzinfo is None or expiry <= datetime.now(timezone.utc):
        raise PermissionError("release approval expired")
    return value, hashlib.sha256(raw).hexdigest()

def record_decision(event, append_and_readback, evidence_args):
    trust, trust_raw = load_release_policy(evidence_args.release_policy, evidence_args.release_policy_bundle)
    if event.get("composition", {}).get("release_policy_sha256", hashlib.sha256(trust_raw).hexdigest()) != hashlib.sha256(trust_raw).hexdigest():
        raise PermissionError("release trust policy changed")
    payload_path, bundle_path = append_and_readback(event)
    recorded, _raw = verified_json_bytes(payload_path, bundle_path,
        trust["admission_receipt_identity"], trust["admission_receipt_issuer"], "load decision")
    if recorded != event: raise PermissionError("independent decision readback differs")


def bounded_error(exc: Exception, stage: str) -> dict:
    chain, seen, current = [], set(), exc
    while current is not None and id(current) not in seen and len(chain) < 6:
        seen.add(id(current))
        message = " ".join(str(current).split())[:256]
        chain.append({"type": type(current).__name__, "message": message})
        current = current.__cause__
    return {"stage": stage, "root_type": chain[-1]["type"], "root_message": chain[-1]["message"], "chain": chain}


@contextmanager
def verify_for_load(artifact: Path, manifest_path: Path, signature_path: Path, *,
                    evidence_args, tombstone_policy: Path, tombstone_bundle: Path,
                    version_store, loader_policy: Path, loader_bundle: Path,
                    loader_name: str, loader_kwargs: dict, append_and_readback,
                    verdict_path: Path | None = None, verdict_bundle: Path | None = None):
    """Only this gate exposes its private verified descriptor/tree to a loader."""
    event = {"schema_version": "aidefend.model-load-decision.v2", "control": "AID-H-003.002",
             "decision": "DENY", "reason": "verification_not_complete"}
    stage = "release_manifest"
    try:
        manifest, manifest_sha = release_manifest(manifest_path, signature_path)
        event.update(model_id=manifest["model_id"], release_id=manifest["release_id"], manifest_sha256=manifest_sha)
        stage = "loader_policy"
        policy, _release, _policy_raw, _release_raw = load_loader_policy(loader_policy, loader_bundle,
            evidence_args.release_policy, evidence_args.release_policy_bundle)
        if policy["required_loader_flags"].get(loader_name) != loader_kwargs:
            raise PermissionError("actual loader options differ from signed policy")
        # One gate-owned read-only capture; member reconstruction hashes during
        # its only extraction pass. No helper creates another artifact copy.
        stage = "artifact_capture"
        with gate_snapshot(artifact, bounds=policy["artifact_limits"]) as captured:
            if captured.sha256 != manifest["artifact_sha256"]:
                raise PermissionError("actual captured bytes differ from approved release")
            stage = "composition"
            composition = runtime_verify(evidence_args, artifact=captured)
            if (composition["artifact_digest"] != "sha256:" + captured.sha256
                    or composition["release_policy_version"] != manifest["policy_version"]):
                raise PermissionError("composition evidence belongs to another release")
            stage = "tombstone"
            tombstone = check_tombstones(composition["source_uri"], captured.sha256,
                tombstone_policy, tombstone_bundle, version_store)
            stage = "loader"
            loader = verify_model(loader_policy, loader_bundle,
                evidence_args.release_policy, evidence_args.release_policy_bundle,
                loader_name, loader_kwargs, verdict_path, verdict_bundle,
                artifact=captured, composition=composition)
            if (loader["artifact_digest"] != "sha256:" + captured.sha256
                    or loader["release_policy_sha256"] != composition["release_policy_sha256"]):
                raise PermissionError("loader evidence differs from captured release")
            captured.assert_unchanged()
            event.update(artifact_sha256=captured.sha256, artifact_bytes=captured.size,
                         composition=composition, tombstone=tombstone, loader=loader,
                         decision="ALLOW", reason="complete_release_policy_verified")
            stage = "decision_readback"
            record_decision(event, append_and_readback, evidence_args)
            # Exit verification's exception handler before executing the caller's
            # loader, so a loader failure is not mislabeled a policy denial.
            approved = True
            try:
                yield captured, event
            finally:
                captured.assert_unchanged()
    except Exception as exc:
        if not locals().get("approved", False):
            detail = bounded_error(exc, stage)
            event.update(decision="DENY", reason=stage + ":" + detail["root_type"], error=detail)
            try: record_decision(event, append_and_readback, evidence_args)
            except Exception as evidence_error:
                raise PermissionError("load denied; independent denial recording unavailable") from evidence_error
        raise
Load through the installed adapter

Select a loader from the fixed registry in the deployment image. A complete Transformers release uses the verified local tree containing weights, tokenizer, config and generation files. Bind any installed tool/reasoning parser to its signed package configuration. A true single-file model can use the separate single-file loader without claiming package completeness.

# This registry is installed in the approved deployment image, never request data.
def load_safetensors_single(captured, *, trust_remote_code: bool):
    from safetensors.torch import load_file
    if trust_remote_code is not False or captured.member_root is not None:
        raise PermissionError("single-file loader scope differs")
    return load_file(captured.path, device="cpu")


def load_transformers_package(captured, *, trust_remote_code: bool, local_files_only: bool, use_safetensors: bool):
    from transformers import AutoModelForCausalLM, AutoTokenizer
    if (captured.member_root is None or trust_remote_code is not False
            or local_files_only is not True or use_safetensors is not True):
        raise PermissionError("complete local package loader scope differs")
    root = captured.member_root
    tokenizer = AutoTokenizer.from_pretrained(root, trust_remote_code=False, local_files_only=True)
    model = AutoModelForCausalLM.from_pretrained(root, trust_remote_code=False,
                                              local_files_only=True, use_safetensors=True)
    # This adapter reads the exact config/tokenizer/generation files in the
    # reconstructed package. Install any tool/reasoning parser adapter in the
    # approved image and select only its signed package configuration here.
    return model, tokenizer


APPROVED_LOADERS = {"safetensors-single": load_safetensors_single,
                    "transformers-safetensors-package": load_transformers_package}
configured_loader = APPROVED_LOADERS[configured_gate_inputs["loader_name"]]
with verify_for_load(artifact_path, release_manifest_path, release_bundle_path,
                     **configured_gate_inputs) as (captured, decision):
    model = configured_loader(captured, **configured_gate_inputs["loader_kwargs"])
    # Keep this context open through the serving lifetime when the loader maps
    # tensors or lazily reads interface files. Closing it removes the private tree.
    serve(model)
Verify the complete path

Exercise the actual package and SBOM producers, real model parsers and independent signed receipt readback. Reject mismatched container formats, altered or omitted members, symlinks, traversal, unsupported parser inputs, expired approval, tombstones, policy drift and unavailable evidence before handing bytes to a loader. Append signed ALLOW or DENY decision evidence; inability to persist/read it is an error and never an allow. ALLOW authorizes the load and does not itself prove the loader completed execution. The model-load-decision schema is v2; composition/runtime/loader verification receipts are v4, and their old schemas require semantic review.

Signed DENY records retain a bounded stage, explicit-cause chain, root type and normalized root message (six causes, 256 characters each). This distinguishes missing/extra population, digest mismatch, expired policy and replay failures while keeping sensitive paths in the restricted audit store. Failure to retain and independently read back a denial keeps the load blocked.

AID-H-003.002-G004

Enforce a mandatory acceptance criteria checklist for production promotion.
Acceptance Criteria (automated)
  1. Mirrored Only: Artifact must originate from internal mirror/registry; prod pipelines are blocked from public hubs.
  2. Signature & Provenance: Valid cosign signature bound to CI OIDC (--certificate-identity, --certificate-oidc-issuer) and present in Rekor transparency log; SLSA/in-toto provenance required.
  3. Model SBOM + Attestation present and verified (per AID-H-003.006): Admission verifies issuer/subject, digest binding, format allow-list, unsafe deserialization ban, loader policy, trust_remote_code flag, and license.
  4. Secure Loader Policy: Enforce trust_remote_code=false, offline loading, and approved model formats by default. Pickle/joblib/legacy .ckpt, TorchScript/mobile model files, custom loaders, custom operators, or any trust_remote_code=true exception require named risk approval and a signed model-loader-analysis sandbox verdict from AID-I-001.005 before promotion.
  5. Offline Loading: Enforce offline model loading (e.g., TRANSFORMERS_OFFLINE=1, local_files_only=True).
  6. Content-Addressed: Use immutable sha256: digests, never floating tags (e.g., latest/main).
  7. Curation Re-verification + Tombstones: Mirror refresh re-verifies external namespace owner and redirect status; if deleted/owner-changed, mark source tombstoned and quarantine until re-onboarded.
Admission Controllers

Use Kyverno (verifyImages/verifyAttestations) or Sigstore Policy Controller to enforce signature and attestation policies cluster-wide.

Open-source tools

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-003.002: CI/CD Release Gating, Model Artifact Signing & Secure Distribution | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version