What retention invariants must survive when an agent's memory is copied to a forensic archive?

**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session's memory is copied to a forensic archive for legal, compliance, or incident investigation, and the archive must preserve the memory's integrity, chain of custody, and access control across potentially decades of retention. Does not apply to operational backups or snapshots used for recovery.

Core Distinction

The separation between **live memory** (the agent's active state, subject to ongoing mutation) and **archived forensic copy** (an immutable snapshot with cryptographic integrity, chain of custody, and retention policy). Relying on simple file copies creates the **Archive Integrity Failure**: the archive is copied without cryptographic proof of integrity, the chain of custody is undocumented, and years later, auditors cannot prove the archive was not tampered with.

What Was Observed

In forensic retention architectures, failing to preserve retention invariants produces three primary failure modes:

  1. **Untrusted Archive:** The archive is copied without signing, and an attacker modifies it years later. No one can detect the modification.
  2. **Broken Chain of Custody:** The archive is transferred between systems, but the transfer is not logged or signed. Auditors cannot prove the archive was never accessed by unauthorized parties.
  3. **Retention Policy Drift:** The archive is supposed to be retained for 7 years, but a subsequent operator changes the policy to 2 years and deletes the archive. No cryptographic proof of the original retention policy exists.

Recorded Elements

  1. **`archive_integrity_signature`:** A cryptographic signature over the archive contents, generated at creation time and verified on every access.
  2. **`chain_of_custody_log`:** A cryptographic hash chain of every transfer, access, or operation on the archive, allowing the successor to verify no unauthorized access occurred.
  3. **`retention_policy_receipt`:** The cryptographic proof of the retention policy (duration, access controls, disposal rules) as of archive creation, immutable for the archive's lifetime.
  4. **`access_authorization_manifest`:** The list of principals authorized to access the archive, bound at creation time and enforced at every access.

Condition of Validity

* **Applies when:** The archive is used for legal, compliance, or forensic purposes where integrity and chain of custody must be provable over long retention periods. * **Does not apply when:** The archive is used for routine operational purposes (backup, restore) where cryptographic integrity is not legally required.

Decision Boundary

  1. **Is this a forensic archive (legal/compliance) or operational backup?** If operational, standard backup applies. If forensic, proceed.
  2. **Is the `archive_integrity_signature` present and verifiable?** If no, the archive cannot be proven unmodified. Reject it.
  3. **Is the `chain_of_custody_log` continuous and signed?** If no, the chain of custody is broken. The archive is not forensically admissible.
  4. **Conversely, when can an archive be accepted without full forensic rigor?** Only when the archive is explicitly marked as `NON_FORENSIC` (e.g., a warm-copy for recovery) and downstream consumers are informed that integrity is not guaranteed.

Operational Effect

  1. **Integrity Proof:** Enables auditors to verify years later that the archive has not been modified.
  2. **Chain of Custody:** Provides cryptographic proof of every access and transfer, satisfying forensic admissibility requirements.
  3. **Policy Immutability:** Locks the retention policy at creation time, preventing later operators from shortening the retention period.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class ArchiveVerdict(Enum):
    FORENSIC_ARCHIVE_VERIFIED = auto()
    OPERATIONAL_ARCHIVE = auto()
    INTEGRITY_FAILED = auto()
    CUSTODY_BROKEN = auto()

@dataclass(frozen=True)
class ForensicArchiveState:
    archive_integrity_signature: str | None
    chain_of_custody_log_hash: str | None
    retention_policy_receipt: str | None
    current_integrity_hash: str
    expected_integrity_hash: str

@dataclass(frozen=True)
class ArchivePolicy:
    require_forensic_rigor: bool

@dataclass(frozen=True)
class ArchiveManifest:
    verdict: ArchiveVerdict
    directive: str
    manifest_digest: str

class ForensicRetentionGuard:
    @staticmethod
    def _hash(data: str) -> str:
        return hashlib.sha256(data.encode("utf-8")).hexdigest()

    @classmethod
    def evaluate_archive(
        cls, state: ForensicArchiveState, policy: ArchivePolicy
    ) -> ArchiveManifest:
        
        # Gate 1: Operational archive
        if not policy.require_forensic_rigor:
            digest = cls._hash(json.dumps({
                "integrity": state.current_integrity_hash,
                "mode": "OPERATIONAL"
            }, sort_keys=True))
            return ArchiveManifest(
                verdict=ArchiveVerdict.OPERATIONAL_ARCHIVE,
                directive="PROCEED: Operational archive, forensic rigor not required.",
                manifest_digest=digest
            )

        # Gate 2: Integrity signature
        if not state.archive_integrity_signature:
            return ArchiveManifest(
                verdict=ArchiveVerdict.INTEGRITY_FAILED,
                directive="HALT: No integrity signature. Cannot verify archive is unmodified.",
                manifest_digest=cls._hash("NO_SIG")
            )

        # Gate 3: Custody log
        if not state.chain_of_custody_log_hash:
            return ArchiveManifest(
                verdict=ArchiveVerdict.CUSTODY_BROKEN,
                directive="HALT: No chain of custody log. Archive is not forensically admissible.",
                manifest_digest=cls._hash("NO_CUSTODY")
            )

        # Gate 4: Retention policy
        if not state.retention_policy_receipt:
            return ArchiveManifest(
                verdict=ArchiveVerdict.INTEGRITY_FAILED,
                directive="HALT: No retention policy receipt. Cannot verify retention constraints.",
                manifest_digest=cls._hash("NO_RETENTION")
            )

        # Gate 5: Integrity match
        if state.current_integrity_hash != state.expected_integrity_hash:
            return ArchiveManifest(
                verdict=ArchiveVerdict.INTEGRITY_FAILED,
                directive="HALT: Archive contents do not match integrity hash. Tampered or corrupted.",
                manifest_digest=cls._hash("HASH_MISMATCH")
            )

        # Gate 6: Verified
        digest = cls._hash(json.dumps({
            "integrity_sig": state.archive_integrity_signature,
            "custody": state.chain_of_custody_log_hash,
            "retention": state.retention_policy_receipt,
            "content": state.current_integrity_hash
        }, sort_keys=True))
        
        return ArchiveManifest(
            verdict=ArchiveVerdict.FORENSIC_ARCHIVE_VERIFIED,
            directive="PROCEED: Forensic archive verified with integrity, custody, and retention.",
            manifest_digest=digest
        )