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:

  1. **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.
  2. **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.
  3. **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

  1. **`dependency_identifier`:** The name of the package or repository (e.g., `requests`, `github.com/org/repo`).
  2. **`resolved_mutable_reference`:** The human-readable tag or branch used during the session (e.g., `v1.2.3`, `main`), for human auditability.
  3. **`immutable_commit_hash`:** The exact SHA-1/SHA-256 commit hash or content-addressable digest that the session actually executed against.
  4. **`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

  1. **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.
  2. **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.
  3. **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.
  4. **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

  1. **Supply Chain Immunity:** Prevents tag-hijacking attacks from affecting successors by forcing execution against the exact, validated source tree hash.
  2. **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.
  3. **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."