AID-H-031.001: AI-Generated Change Provenance & Risk Labeling
Pillars: infra, app
Lifecycle: building, validation
Scope boundary
Owns durable provenance and risk labels for AI-generated or AI-materially-modified development artifacts, including initiator, model or agent, context, validation status, and privileged-path impact. It emits routing facts but does not approve the change.
AID-H-031.004: AID-H-031.001 labels provenance and risk so the correct validation route can be selected; AID-H-031.004 consumes those labels and the complete evidence chain to authorize promotion. A provenance label is evidence, not approval.
Implementation guidance
- AID-H-031.001-G001: Derive and sign AI-change provenance from pinned Git commits and a separately signed generation event.
- AID-H-031.001-G002: Verify signed provenance, rehash the pinned Git change set, and derive a signed routing decision.
AID-H-031.001-G001
Concept:
Derive the changed-file set from two pinned Git commit IDs, hash the exact Git blobs rather than a caller-maintained file list, and bind it to a separately signed generation event from the coding-agent platform. Risk labels are computed from paths, not accepted from the agent.
# File: ci/build_generated_change_provenance.py
from __future__ import annotations
import hashlib
import json
import math
import os
import re
import subprocess
from pathlib import Path
COMMIT_RE = re.compile(r"^(?:[0-9a-f]{40}|[0-9a-f]{64})$")
RISK_PREFIXES = {
"identity": ("auth/", "identity/"),
"secrets": ("secrets/",),
"infra": ("infra/", "terraform/", "helm/"),
"deploy": ("deploy/", ".github/workflows/"),
"payments": ("payments/",),
"destructive_ops": ("migrations/", "operations/destructive/"),
}
def required_git_timeout() -> float:
value = float(os.environ["VERIFIED_H031_GIT_COMMAND_TIMEOUT_SECONDS"])
if not math.isfinite(value) or value <= 0: raise ValueError("Git command timeout must be finite and positive")
return value
GIT_COMMAND_TIMEOUT_SECONDS = required_git_timeout()
def git(*args: str) -> bytes:
return subprocess.check_output(["git", *args], timeout=GIT_COMMAND_TIMEOUT_SECONDS)
def resolve_commit(value: str) -> str:
if not COMMIT_RE.fullmatch(value):
raise ValueError("commit must be one full SHA-1 or SHA-256 object ID")
resolved = git("rev-parse", "--verify", value + "^{commit}").decode().strip()
if resolved != value:
raise ValueError("commit does not resolve exactly")
return resolved
def changed_blobs(base: str, head: str) -> list[dict]:
subprocess.run(["git", "merge-base", "--is-ancestor", base, head], check=True, timeout=GIT_COMMAND_TIMEOUT_SECONDS)
fields = git("diff", "--name-status", "--no-renames", "-z", base, head).split(b"\0")
if fields and fields[-1] == b"":
fields.pop()
if len(fields) % 2:
raise ValueError("unexpected git name-status output")
records = []
for index in range(0, len(fields), 2):
status = fields[index].decode("ascii")
path = fields[index + 1].decode("utf-8", "strict")
if status not in {"A", "M", "D", "T"} or not path or "\x00" in path:
raise ValueError("unsupported change status or path")
commit = base if status == "D" else head
blob = git("show", f"{commit}:{path}")
oid = git("rev-parse", f"{commit}:{path}").decode().strip()
records.append({
"path": path,
"status": status,
"git_blob_oid": oid,
"content_sha256": hashlib.sha256(blob).hexdigest(),
})
return sorted(records, key=lambda item: item["path"])
def derived_risks(records: list[dict]) -> list[str]:
paths = [item["path"] for item in records]
return sorted(
risk for risk, prefixes in RISK_PREFIXES.items()
if any(path.startswith(prefix) for path in paths for prefix in prefixes)
)
def canonical(value: object) -> bytes:
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
def build() -> dict:
base = resolve_commit(os.environ["AIDEFEND_BASE_COMMIT"])
head = resolve_commit(os.environ["AIDEFEND_HEAD_COMMIT"])
event_path = Path(os.environ["AIDEFEND_SIGNED_GENERATION_EVENT"])
event_raw = event_path.read_bytes()
event = json.loads(event_raw.decode("utf-8", errors="strict"))
if event.get("schema_version") != "aidefend.generation-event.v1":
raise ValueError("generation event schema differs")
if event.get("base_commit") != base or event.get("head_commit") != head:
raise ValueError("generation event commit binding differs")
records = changed_blobs(base, head)
if not records:
raise ValueError("change set is empty")
change_set_sha256 = hashlib.sha256(canonical(records)).hexdigest()
return {
"schema_version": "aidefend.generated-change-provenance.v2",
"generated_by": event["generated_by"],
"generator_id": event["generator_id"],
"model_id": event["model_id"],
"generation_event_sha256": hashlib.sha256(event_raw).hexdigest(),
"source_repo": event["source_repo"],
"base_commit": base,
"head_commit": head,
"ticket_id": event["ticket_id"],
"file_manifest": records,
"change_set_sha256": change_set_sha256,
"touched_risk_paths": derived_risks(records),
"ci_workflow_identity": os.environ["AIDEFEND_CI_WORKFLOW_IDENTITY"],
"ci_run_id": os.environ["AIDEFEND_CI_RUN_ID"],
}
output = Path(".aidefend/generated-change.json")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_bytes(canonical(build()) + b"\n")
# Verify a private snapshot, then parse/hash that same event and sign the derived provenance.
SNAPSHOT_DIR=$(mktemp -d)
trap 'rm -rf -- "$SNAPSHOT_DIR"' EXIT
EVENT_SNAPSHOT="$SNAPSHOT_DIR/generation-event.json"
install -m 0600 -- "$AIDEFEND_SIGNED_GENERATION_EVENT" "$EVENT_SNAPSHOT"
cosign verify-blob --bundle generation-event.sigstore.json \
--certificate-identity "$GENERATION_PLATFORM_IDENTITY" \
--certificate-oidc-issuer "$OIDC_ISSUER" "$EVENT_SNAPSHOT"
AIDEFEND_SIGNED_GENERATION_EVENT="$EVENT_SNAPSHOT" python ci/build_generated_change_provenance.py
cosign sign-blob --yes --bundle .aidefend/generated-change.sigstore.json \
.aidefend/generated-change.json
Action: Pin both signer identities in downstream policy and retain the event, provenance, bundles, and Git objects in append-only build evidence. Missing commits, omitted/unreadable blobs, signature failure, or an unbound generation event is insufficient_data and blocks routing.
Production implementation
At change creation, derive base/head commits and exact path/blob diff from Git, verify the separately signed generation event, bind model/provider, prompt/workflow and actor identities, classify policy risk and sign a closed provenance record without trusting author labels.
Independent verification
A separately credentialed verifier independently reads back or replays Git object graph, generation-event signature, change digest and signed provenance with amended history, mixed worktree, missing event, wrong model and signature fixtures, runs positive, negative, boundary, stale-policy, partial-population and dependency-failure fixtures, and reconciles the complete eligible population without trusting a component-supplied verdict.
AID-H-031.001-G002
Concept:
Routing verifies the provenance and policy signatures, recomputes the exact Git change manifest, and derives risk from paths again. It produces required checks; it never accepts a text file claiming those checks completed.
# File: ci/route_generated_change.py
from __future__ import annotations
import hashlib
import json
import os
import subprocess
COMMAND_TIMEOUT_SECONDS = int(os.environ["VERIFIED_COMMAND_TIMEOUT_SECONDS"])
from pathlib import Path
from ci.build_generated_change_provenance import changed_blobs, derived_risks
def verify_blob(path: Path, bundle: Path, identity: str, issuer: str) -> None:
subprocess.run([
"cosign", "verify-blob", "--bundle", str(bundle),
"--certificate-identity", identity,
"--certificate-oidc-issuer", issuer, str(path),
], check=True, timeout=COMMAND_TIMEOUT_SECONDS)
def route(provenance: dict, policy: dict) -> dict:
recomputed = changed_blobs(provenance["base_commit"], provenance["head_commit"])
digest = hashlib.sha256(
json.dumps(recomputed, sort_keys=True, separators=(",", ":")).encode()
).hexdigest()
if recomputed != provenance["file_manifest"] or digest != provenance["change_set_sha256"]:
raise PermissionError("provenance file-manifest binding differs")
risks = derived_risks(recomputed)
if risks != provenance["touched_risk_paths"]:
raise PermissionError("provenance risk labels differ")
matching = [
tier for tier in policy["tiers"]
if set(risks).issubset(set(tier["allowed_risk_paths"]))
]
if not matching:
raise PermissionError("no routing tier covers the derived risks")
selected = min(matching, key=lambda item: int(item["priority"]))
return {
"schema_version": "aidefend.generated-change-routing.v2",
"provenance_sha256": hashlib.sha256(
json.dumps(provenance, sort_keys=True, separators=(",", ":")).encode()
).hexdigest(),
"policy_version": policy["policy_version"],
"selected_tier": selected["id"],
"required_checks": sorted(set(selected["required_checks"])),
"required_review_roles": sorted(set(selected["required_review_roles"])),
"base_commit": provenance["base_commit"],
"head_commit": provenance["head_commit"],
"change_set_sha256": provenance["change_set_sha256"],
}
Action: Verify both bundles before parsing, require exact policy/provenance schemas, and sign the routing decision under the admission-policy identity. AID-H-031.002 and AID-H-031.003 consumes this decision and independently verifies actual check attestations; check names in a caller file never count as completion.
Production implementation
At pull-request or CI admission, verify provenance and generation-event signatures, recompute exact Git object/diff digests, reject extra/untracked or changed paths, derive risk from signed policy and emit a separate signed routing decision for required gates.
Independent verification
A separately credentialed verifier independently reads back or replays signatures, commits, diff/path population and route decision with forged, stale, amended, partial, extra-path, policy-drift and command-failure fixtures, runs positive, negative, boundary, stale-policy, partial-population and dependency-failure fixtures, and reconciles the complete eligible population without trusting a component-supplied verdict.
Open-source tools
- Open Policy Agent (OPA)
- OpenTelemetry
- Git
- in-toto
- slsa-verifier
- Cosign
- Kyverno
- OPA Gatekeeper (admission control)
- gitsign (commit-signature integrity when AI provenance metadata is embedded in the signed commit; label generation and validation remain external)
Commercial / hosted tools
- GitHub Enterprise Cloud
- GitHub Advanced Security
- GitLab Ultimate
- JFrog Artifactory
- JFrog Xray
- Sonatype Nexus Repository
- Amazon ECR
- Harness STO
Threat mappings
MITRE ATLAS
- N/A
MAESTRO
- N/A
OWASP LLM Top 10 2026
- N/A
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- N/A
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- N/A
Google Secure AI Framework 2.0 - Risks
- N/A
Databricks AI Security Framework 3.0
- Algorithms 5.1: Lack of tracking and reproducibility of experiments
- Governance 4.1: Lack of traceability and transparency of model assets