Why should session teardown preserve the exact commit hash of external dependencies?
**Date of Record:** 2026 October 25 **Scope:** Applies when an agentic session relies on external source-code dependencies (Git repositories, package registries, or container tags) resolved via mutable references (branches, semantic version tags), and is preparing to terminate or hand off to a successor. Does not apply to strictly content-addressed environments (like Nix or IPFS) where the identifier is inherently the cryptographic hash.
Core Distinction
The separation between **mutable version references** (human-readable tags such as `v1.2.3`, `latest`, or branch names like `main`) and **immutable cryptographic commit hashes** (the exact SHA-1 or SHA-256 tree hash). A version reference is a pointer that can be moved, overwritten, or hijacked upstream; a commit hash is a mathematical guarantee of the exact source tree. Teardown that preserves only the version tag records an **illusion of reproducibility**. If the upstream tag is force-pushed or the registry is compromised, the successor will silently execute different code under the same version name.
What Was Observed
In distributed agent architectures and CI/CD pipelines, failing to preserve immutable commit hashes at session teardown produces three primary failure modes:
- **The Mutable Tag Illusion (Supply Chain Hijacking):** An instance validates its logic against `library@v2.1.0`. During the gap, an attacker compromises the upstream maintainer's account and re-tags `v2.1.0` with a malicious payload. The successor, resolving the same tag, installs the compromised code and inherits the attacker's execution context.
- **Phantom Bug Irreproducibility:** A successor attempts to reproduce a bug using the recorded branch `feature-branch`. However, the upstream author force-pushed to that branch, rewriting history and eliminating the bug. The successor wastes compute trying to debug a line of code that no longer exists in the referenced branch.
- **Transitive Drift:** An instance records a top-level dependency as `requests>=2.0`. The successor resolves this to a newer minor version, which silently updates a transitive dependency with breaking behavior, causing a failure that the predecessor never observed.
Recorded Elements
- **`dependency_identifier`:** The name of the package or repository (e.g., `requests`, `github.com/org/repo`).
- **`resolved_mutable_reference`:** The human-readable tag or branch used during the session (e.g., `v1.2.3`, `main`), for human auditability.
- **`immutable_commit_hash`:** The exact SHA-1/SHA-256 commit hash or content-addressable digest that the session actually executed against.
- **`lockfile_integrity_hash`:** The cryptographic hash of the entire dependency tree (e.g., `package-lock.json`, `poetry.lock`, or `Cargo.lock` integrity check) to prevent transitive drift.
Condition of Validity
* **Applies when:** The session executed code or relied on schemas from external dependencies resolved via mutable references, and a successor instance must reproduce the exact environment to resume execution. * **Does not apply when:** The execution environment is strictly air-gapped and immutable (e.g., pinned Docker images identified solely by digest), or the dependency is explicitly declared as a floating, always-latest interface where backward compatibility is guaranteed.
Decision Boundary
- **Did the session rely on an external dependency resolved via a mutable tag or branch?** If no, no hash preservation is needed. If yes, proceed to capture the immutable hash.
- **Is the exact commit hash or content digest recorded?** If no, the successor inherits a non-deterministic pointer. The teardown must fail closed until the hash is extracted from the local lockfile or VCS state.
- **Is the lockfile integrity hash preserved to prevent transitive drift?** If yes, the successor can rebuild the exact tree. If no, the successor must treat all transitive dependencies as unverified.
- **Conversely, when is a mutable tag sufficient?** Only if the package registry enforces strict immutability (e.g., `crates.io` or `npm` where published versions cannot be overwritten), and the session explicitly records the registry's immutability guarantee as a boundary assumption.
Operational Effect
- **Supply Chain Immunity:** Prevents tag-hijacking attacks from affecting successors by forcing execution against the exact, validated source tree hash.
- **Bit-For-Bit Reproducibility:** Guarantees that when a successor replays a session or investigates a failure, it is running the exact same code that the predecessor executed.
- **Transitive Determinism:** Replaces floating dependency resolution with a sealed lockfile hash, eliminating the phantom bugs introduced by silent minor-version updates.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class ReferenceMutability(Enum):
IMMUTABLE_HASH = auto()
MUTABLE_TAG = auto()
MUTABLE_BRANCH = auto()
REGISTRY_ENFORCED_IMMUTABLE = auto()
@dataclass(frozen=True)
class DependencyState:
identifier: str
reference: str
mutability: ReferenceMutability
immutable_hash: str | None
lockfile_integrity_hash: str
@dataclass(frozen=True)
class DependencyManifest:
dependencies: tuple[DependencyState, ...]
manifest_digest: str
class DependencyHashRecorder:
"""
Evaluates external dependencies at teardown to ensure exact commit hashes
are preserved, preventing the Mutable Tag Illusion.
"""
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def seal_manifest(
cls, dependencies: tuple[DependencyState, ...]
) -> DependencyManifest:
if not dependencies:
raise ValueError("Cannot seal an empty dependency manifest.")
for dep in dependencies:
# Fail closed if a mutable reference lacks a commit hash
if dep.mutability in (ReferenceMutability.MUTABLE_TAG, ReferenceMutability.MUTABLE_BRANCH):
if not dep.immutable_hash or not dep.immutable_hash.strip():
raise ValueError(
f"Dependency {dep.identifier!r} uses mutable reference "
f"{dep.reference!r} but lacks an immutable commit hash."
)
if not dep.lockfile_integrity_hash.strip():
raise ValueError(
f"Dependency {dep.identifier!r} lacks a lockfile integrity hash; "
"transitive drift cannot be prevented."
)
digest_payload = {
"dependencies": tuple(
sorted(
f"{d.identifier}:{d.immutable_hash or d.reference}:{d.lockfile_integrity_hash}"
for d in dependencies
)
)
}
digest = cls._hash(
json.dumps(digest_payload, sort_keys=True, separators=(",", ":"))
)
return DependencyManifest(
dependencies=dependencies,
manifest_digest=digest,
)
@classmethod
def verify_dependency_resolution(
cls,
manifest: DependencyManifest,
current_identifier: str,
resolved_hash: str,
) -> tuple[bool, str]:
"""
Fetching-time check: confirms the newly resolved dependency matches the
exact hash preserved at session teardown.
"""
for dep in manifest.dependencies:
if dep.identifier == current_identifier:
expected = dep.immutable_hash or dep.reference
if expected != resolved_hash:
return False, f"HASH MISMATCH: {current_identifier} expected {expected}, resolved {resolved_hash}. Possible tag hijack or force-push."
return True, "MATCHED"
return False, f"UNKNOWN_DEPENDENCY: {current_identifier} is not in the sealed manifest."