Skip to content

09 · Compliance, Privacy & Governance

Everything covered so far in Level 4 — alignment, safety, security, cost, latency — is about making an LLM system work well and safely. This module covers the layer that makes it operable under scrutiny: data retention policy, PII handling, audit trails, and the general shape of regulatory considerations that apply once an LLM system touches real user data at any meaningful scale. This is not legal advice, and specific regulatory obligations vary by jurisdiction and industry — the goal here is the engineering patterns that make compliance possible, so a legal or compliance team has something real to work with.

Data retention: knowing what you're keeping, and for how long

An LLM platform touches data at several distinct points — inputs, outputs, logs, traces (Level 3, module 9), eval samples (module 5), cached responses (module 6) — and each can silently accumulate sensitive data indefinitely if retention isn't designed deliberately:

from dataclasses import dataclass
from datetime import datetime, timedelta

@dataclass
class RetentionPolicy:
    data_category: str
    retention_days: int
    reason: str

retention_policies = [
    RetentionPolicy("raw_prompts_with_pii", 30, "Minimize PII exposure window"),
    RetentionPolicy("aggregated_metrics", 730, "No PII; useful for long-term trend analysis"),
    RetentionPolicy("eval_samples_anonymized", 365, "PII stripped before storage"),
    RetentionPolicy("audit_logs", 2555, "Common 7-year regulatory retention for audit trails"),
]

def is_expired(created_at: datetime, policy: RetentionPolicy) -> bool:
    return datetime.utcnow() - created_at > timedelta(days=policy.retention_days)

def purge_expired(records: list[dict], policy: RetentionPolicy) -> list[dict]:
    return [r for r in records if not is_expired(r["created_at"], policy)]

The pattern that matters: define retention per data category, not one blanket policy — raw prompts containing PII, anonymized aggregates, and audit logs have genuinely different retention needs, and a single global retention setting is either too short for audit purposes or too long for PII minimization.

PII detection and redaction

Detecting and redacting personally identifiable information before it's stored, logged, or sent to a third-party model provider reduces exposure at the source:

import re

PII_PATTERNS = {
    "email": re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"),
    "phone": re.compile(r"\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b"),
    "ssn": re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),
    "credit_card": re.compile(r"\b(?:\d[ -]*?){13,16}\b"),
}

def redact_pii(text: str) -> tuple[str, dict[str, int]]:
    found_counts = {}
    redacted = text
    for label, pattern in PII_PATTERNS.items():
        matches = pattern.findall(redacted)
        if matches:
            found_counts[label] = len(matches)
            redacted = pattern.sub(f"[REDACTED_{label.upper()}]", redacted)
    return redacted, found_counts

Regex-based detection catches well-structured PII (emails, phone numbers, SSNs) but misses unstructured PII (a name mentioned in prose, an address written informally) — for higher-stakes applications, pair regex with a dedicated PII-detection model or service, and treat regex detection as a fast first pass, not a complete solution. Apply redaction before data leaves your own boundary — before it's sent to a third-party provider or written to a log store, not after.

Audit trails for agentic actions

Once an LLM system takes real actions (Level 3's tool use, this level's security module), a complete, tamper-evident record of what happened and why is both a security control and, in regulated contexts, often a compliance requirement:

import json
import hashlib
from datetime import datetime

class AuditLog:
    def __init__(self, store):
        self.store = store
        self.last_hash = "0" * 64   # genesis hash

    def record(self, actor: str, action: str, details: dict):
        entry = {
            "timestamp": datetime.utcnow().isoformat(),
            "actor": actor,
            "action": action,
            "details": details,
            "prev_hash": self.last_hash,
        }
        entry_bytes = json.dumps(entry, sort_keys=True).encode()
        entry["hash"] = hashlib.sha256(entry_bytes + self.last_hash.encode()).hexdigest()
        self.last_hash = entry["hash"]
        self.store.append(entry)
        return entry

    def verify_chain(self, entries: list[dict]) -> bool:
        # each entry's hash depends on the previous entry's hash — tampering
        # with any past entry breaks every hash after it, making tampering detectable
        prev_hash = "0" * 64
        for entry in entries:
            expected = entry["hash"]
            entry_copy = {k: v for k, v in entry.items() if k != "hash"}
            entry_bytes = json.dumps(entry_copy, sort_keys=True).encode()
            actual = hashlib.sha256(entry_bytes + prev_hash.encode()).hexdigest()
            if actual != expected:
                return False
            prev_hash = expected
        return True

Log every consequential action an agent takes — not just the final outcome, but which tool was called, with what arguments, on whose authority, and what the model's stated reasoning was — because the audit trail is what lets you reconstruct why an agent did something months later, which is exactly what an incident review or regulatory inquiry needs.

Three governance principles recur across most privacy regulation (GDPR, CCPA, and sector-specific rules alike), independent of the exact legal text in any one jurisdiction:

  • Purpose limitation — data collected for one purpose (e.g., answering a support query) shouldn't be silently reused for another (e.g., training a model) without separate basis or consent.
  • Data minimization — send a model only the data actually needed for the task, not everything available "in case it helps" — a support agent summarizing a ticket doesn't need the user's full order history in its context unless the task requires it.
  • User rights — access, correction, and deletion rights mean your system needs a real mechanism to locate and remove a specific user's data across every store it touched (raw logs, caches, eval samples, vector indexes), not just the primary database.
def handle_deletion_request(user_id: str, data_stores: list):
    results = {}
    for store in data_stores:
        # every store that might hold user data needs to implement this —
        # primary DB, logs, cache, vector index, eval samples, everywhere
        deleted_count = store.delete_by_user_id(user_id)
        results[store.name] = deleted_count
    return results

Design for deletion from day one — retrofitting "find every place this user's data might exist" across logs, caches, vector stores, and eval snapshots after the fact is a much larger project than building each store with a user_id-indexed deletion path from the start.

How It Actually Works

The audit log's hash-chaining mechanism (each entry's hash depends on the previous entry's hash, forming a chain) is the same structural idea behind blockchains and Git commit history: it makes tampering with any past entry detectable, because changing one entry changes its hash, which invalidates every subsequent entry's hash in the chain. This doesn't prevent someone with write access from tampering — it makes tampering evident upon verification, which is the property audit trails actually need: not that data can never be altered, but that alteration can always be detected after the fact.

The reason PII minimization needs to happen before data leaves your boundary, not after, is that once data has been transmitted to a third-party provider, logged to a store you don't fully control, or used in a training run, deletion or correction becomes structurally harder or sometimes impossible — a model that has already been trained on data can't have that specific data "un-trained" out of its weights the way a database row can be deleted, since the influence is distributed across many parameters rather than stored at a retrievable address. This asymmetry (easy to prevent inclusion, hard to reverse it after the fact) is why data minimization and purpose limitation are enforced as upstream gates rather than downstream cleanup.

Cheat sheet

Concept Key fact
Retention policy Define per data category, not one global rule
PII redaction Apply before data leaves your boundary; regex + model for full coverage
Audit trail Hash-chained, tamper-evident log of every consequential action
Purpose limitation Don't silently repurpose data collected for one use into another
Data minimization Send models only what the task needs, not everything available
Deletion by design Every store needs a user_id-indexed deletion path from day one

Exercise

Implement redact_pii and run it against 20 synthetic sample prompts containing a mix of emails, phone numbers, and SSNs (never use real personal data — generate synthetic examples). Report the detection rate and any false positives. Then build the AuditLog class, record 10 sequential agent actions, verify the chain with verify_chain, and demonstrate that tampering with one historical entry causes verification to fail from that point forward.