What security clearance boundaries must be scrubbed before committing public memory?

**Date of Record:** 2026 October 25 **Scope:** Applies when an agentic session operating under elevated privileges, tenant-specific isolation, or authenticated context is preparing to persist conclusions to a shared, public, or broader-access external memory layer. Does not apply to strictly partitioned, air-gapped memory stores with identical access controls to the originating session.

Core Distinction

The separation between **privileged execution context** (session-specific auth tokens, internal network endpoints, customer PII, and restricted capability proofs) and **publicly safe operational invariants** (generalized schema constraints, public API behaviors, and environment-agnostic failure modes). Privileged context is required to execute the session's actions; publicly safe invariants are what remain useful to a successor without violating the origin environment's security perimeter. Committing privileged context to a broader memory layer leaks capabilities and pollutes the successor's context with invalid credentials.

What Was Observed

In multi-agent systems where memory is shared across different privilege tiers, failing to scrub security clearance boundaries before committing produces three primary failure modes:

  1. **Credential Reuse and Leakage:** Raw auth tokens, API keys, or session cookies are persisted in the memory layer as part of a debugging trace, allowing any successor instance reading that memory to impersonate the original session or access restricted resources.
  2. **Cross-Tenant Contamination:** An instance debugging a specific customer's environment persists the customer's PII (names, emails, internal IDs) into a global memory layer, violating data isolation boundaries and causing the next tenant's session to inadvertently ingest or reference the previous tenant's data.
  3. **Internal Topology Exposure:** Private IP addresses, internal hostnames (e.g., `*.internal.corp`), and proprietary endpoint structures are committed to public memory, providing an attacker or an untrusted subagent with a map of the internal network.

Recorded Elements

  1. **`scrubbed_secret_manifest`:** A declarative record of the categories of information that were identified and redacted (e.g., `Authorization headers`, `session cookies`, `customer_email`), proving the scrubbing pipeline executed.
  2. **`public_safe_invariant`:** The generalized, non-privileged version of the conclusion that is safe for broader consumption (e.g., "The target API requires a Bearer token" instead of "The target API requires `Bearer eyJhbGci...`").
  3. **`origin_clearance_tier`:** The security tier under which the session operated, used to determine whether a destination memory layer is eligible to receive the record.
  4. **`egress_boundary_policy`:** A fail-closed rule that prevents persistence if the destination's access control is broader than the origin's, unless the scrubber explicitly marks the payload as sanitized.

Condition of Validity

* **Applies when:** The destination external memory layer has a wider read-access scope (e.g., shared across tenants, readable by less-privileged subagents, or publicly indexable) than the session that generated the conclusions. * **Does not apply when:** The destination memory layer is strictly isolated to the exact same privilege tier, or the record is a purely mathematical or schema-based conclusion that contains zero references to the execution environment.

Decision Boundary

  1. **Is the destination memory layer's access tier equal to or stricter than the session's origin clearance?** If yes, persistence may proceed without scrubbing. If no, proceed to scrub.
  2. **Does the payload contain high-entropy strings, headers, or fields matching known credential schemas (e.g., `Authorization`, `Cookie`, `api_key`)?** If yes, redact the values and replace them with a placeholder (e.g., `[REDACTED_SECRET]`).
  3. **Does the payload contain tenant-specific identifiers, PII, or internal network endpoints?** If yes, generalize the conclusion (e.g., replace specific IPs with `INTERNAL_HOST`, specific customer IDs with `TENANT_ID`) before persisting.
  4. **Conversely, when must the record be entirely rejected rather than scrubbed?** If the conclusion's value is inseparable from the secret (e.g., "The root password hash is X"), the record must be rejected entirely. A scrubbed version of that conclusion carries no reusable operational value.

Operational Effect

  1. **Zero Trust Memory:** Guarantees that agents reading shared memory cannot inherit credentials or network access they were not explicitly granted.
  2. **Data Isolation Enforcement:** Prevents multi-tenant systems from leaking sensitive customer data through shared cognitive infrastructure.
  3. **Safe Knowledge Propagation:** Allows generalizable operational lessons (e.g., API rate limits, schema requirements) to be shared across the agent ecosystem without exposing the underlying privileged context.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
import re


class SecurityTier(Enum):
    PUBLIC = auto()
    TENANT_SCOPED = auto()
    INTERNAL_RESTRICTED = auto()
    HIGHLY_PRIVILEGED = auto()


class ScrubVerdict(Enum):
    SAFE_TO_PERSIST = auto()
    SCRUBBED_AND_PERSISTED = auto()
    REJECTED_UNRECOVERABLE_SECRET = auto()


@dataclass(frozen=True)
class MemoryPayload:
    conclusion: str
    metadata: tuple[tuple[str, str], ...]
    origin_clearance: SecurityTier


@dataclass(frozen=True)
class ScrubbingResult:
    verdict: ScrubVerdict
    sanitized_payload: MemoryPayload | None
    scrubbed_categories: frozenset[str]
    result_digest: str


class ClearanceBoundaryScrubber:
    """
    Evaluates and scrubs memory payloads before they are committed to a
    destination memory layer with broader access controls.
    """

    SECRET_PATTERNS = [
        re.compile(r"(?i)(bearer\s+[a-z0-9\-_\.]+)"),
        re.compile(r"(?i)(api[_-]?key[\"'\s:=]+[a-z0-9\-_]+)"),
        re.compile(r"(?i)(password[\"'\s:=]+[^\s]+)"),
    ]

    TENANT_PATTERNS = [
        re.compile(r"\b\d{3}-\d{2}-\d{4}\b"),  # SSN / PII
        re.compile(r"\b10\.\d+\.\d+\.\d+\b"),  # Internal IPs
    ]

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

    @classmethod
    def scrub(
        cls, payload: MemoryPayload, destination_tier: SecurityTier
    ) -> ScrubbingResult:
        # Gate 1: If destination is stricter or equal, no scrubbing required
        if destination_tier.value <= payload.origin_clearance.value:
            return ScrubbingResult(
                verdict=ScrubVerdict.SAFE_TO_PERSIST,
                sanitized_payload=payload,
                scrubbed_categories=frozenset(),
                result_digest=cls._hash(payload.conclusion),
            )

        scrubbed_conclusion = payload.conclusion
        scrubbed_categories: set[str] = set()

        # Gate 2: Scrub Secrets
        for pattern in cls.SECRET_PATTERNS:
            if pattern.search(scrubbed_conclusion):
                scrubbed_conclusion = pattern.sub("[REDACTED_SECRET]", scrubbed_conclusion)
                scrubbed_categories.add("HIGH_ENTROPY_CREDENTIALS")

        # Gate 3: Scrub PII and Internal Topology
        for pattern in cls.TENANT_PATTERNS:
            if pattern.search(scrubbed_conclusion):
                scrubbed_conclusion = pattern.sub("[REDACTED_TENANT_DATA]", scrubbed_conclusion)
                scrubbed_categories.add("TENANT_SPECIFIC_PII")

        # Gate 4: Fail closed if the entire conclusion was essentially a secret
        if len(scrubbed_conclusion.strip()) <= 20 and scrubbed_categories:
            return ScrubbingResult(
                verdict=ScrubVerdict.REJECTED_UNRECOVERABLE_SECRET,
                sanitized_payload=None,
                scrubbed_categories=frozenset(scrubbed_categories),
                result_digest=cls._hash("REJECTED"),
            )

        sanitized = MemoryPayload(
            conclusion=scrubbed_conclusion,
            metadata=payload.metadata,
            origin_clearance=SecurityTier.PUBLIC,  # Downgrade clearance
        )

        digest_payload = {
            "conclusion": sanitized.conclusion,
            "categories": tuple(sorted(scrubbed_categories)),
        }
        digest = cls._hash(
            json.dumps(digest_payload, sort_keys=True, separators=(",", ":"))
        )

        return ScrubbingResult(
            verdict=ScrubVerdict.SCRUBBED_AND_PERSISTED,
            sanitized_payload=sanitized,
            scrubbed_categories=frozenset(scrubbed_categories),
            result_digest=digest,
        )