What invariants prevent negative-cache poisoning in agent discovery systems?

**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session uses a discovery system with negative caching (caching "not found" results to avoid repeated lookups), and the negative cache can be poisoned by an attacker or stale state, causing the agent to believe a real resource does not exist. Does not apply to discovery systems with no negative caching, or systems with cryptographic existence proofs.

Core Distinction

The separation between **positive cache** (cached "found" results) and **negative cache** (cached "not found" results). Positive cache poisoning causes the agent to serve wrong data; negative cache poisoning causes the agent to serve no data, hiding resources that actually exist. The latter is more insidious because the agent doesn't see an error — it silently fails to find valid resources.

What Was Observed

In discovery architectures with negative caching, failing to prevent poisoning produces three primary failure modes:

  1. **Resource Hiding Attack:** An attacker queries the discovery system for a sensitive resource before the legitimate owner does, causing a negative cache entry. The legitimate agent queries later, receives "not found," and does not access the resource.
  2. **Stale Negative Cache:** A resource is created after a legitimate "not found" response, but the negative cache TTL has not expired. The agent believes the resource still doesn't exist.
  3. **Distributed Denial via Cache:** An attacker floods the negative cache with entries for many resources, causing the discovery system to spend its cache budget on negative entries, evicting legitimate positive entries.

Recorded Elements

  1. **`negative_cache_ttl_signature`:** The exact TTL for negative entries, ideally much shorter than positive TTL, bounding the damage window.
  2. **`negative_cache_write_authorization_receipt`:** The proof that the principal who generated the negative cache entry was authorized to look up the resource (unauthenticated lookups should not populate the negative cache).
  3. **`negative_cache_revalidation_receipt`:** A cryptographic proof that stale negative entries are revalidated before being served, especially for critical resources.
  4. **`positive_creation_event_hash`:** When a resource is created, the creation event hash is published, so negative caches can be invalidated by tracking creation events.

Condition of Validity

* **Applies when:** The discovery system uses negative caching, adversarial actors can query the cache, and the cost of hiding real resources is material. * **Does not apply when:** Discovery systems use no negative caching, or they can prove non-existence cryptographically (e.g., Merkle-proof of absence in an immutable registry).

Decision Boundary

  1. **Is the lookup from an unauthenticated principal?** If yes, the negative result must NOT be cached (only authenticated lookups populate negative cache). If no, proceed.
  2. **Does the negative cache entry have a bounded TTL?** If no, resources can be hidden indefinitely. The cache must invalidate negative entries aggressively (e.g., TTL < 60s).
  3. **Has a `positive_creation_event_hash` been received since the negative cache write?** If yes, the negative entry must be invalidated immediately.
  4. **Conversely, when can an agent trust a negative cache result?** Only when the result came from an authenticated principal's lookup AND the entry is within TTL AND no creation event superseded it.

Operational Effect

  1. **Resource Hiding Prevention:** Blocks unauthenticated caching, preventing attackers from hiding resources.
  2. **Cache Freshness:** Short TTL and creation-event invalidation prevent stale negative entries.
  3. **Cache Budget Protection:** Reduces the volume of negative cache writes, preserving budget for positive entries.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json

class NegativeCacheVerdict(Enum):
    UNAUTHENTICATED_NO_CACHE = auto()
    CACHE_WRITE_ALLOWED = auto()
    CACHE_INVALIDATED = auto()
    CACHE_HIT_VALID = auto()

@dataclass(frozen=True)
class NegativeCacheEntry:
    resource_id: str
    write_epoch: float
    ttl_sec: float
    writer_was_authenticated: bool
    superseded_by_creation_event: bool

@dataclass(frozen=True)
class NegativeCachePolicy:
    current_epoch: float
    max_ttl_sec: float
    require_authentication: bool

@dataclass(frozen=True)
class NegativeCacheManifest:
    verdict: NegativeCacheVerdict
    directive: str
    manifest_digest: str

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

    @classmethod
    def evaluate_entry(
        cls, entry: NegativeCacheEntry, policy: NegativeCachePolicy
    ) -> NegativeCacheManifest:
        
        # Gate 1: Unauthenticated write prevention
        if policy.require_authentication and not entry.writer_was_authenticated:
            return NegativeCacheManifest(
                verdict=NegativeCacheVerdict.UNAUTHENTICATED_NO_CACHE,
                directive=f"REJECT: Unauthenticated lookup for {entry.resource_id}. Do not cache.",
                manifest_digest=cls._hash(f"UNAUTH:{entry.resource_id}")
            )

        # Gate 2: Creation event invalidation
        if entry.superseded_by_creation_event:
            return NegativeCacheManifest(
                verdict=NegativeCacheVerdict.CACHE_INVALIDATED,
                directive=f"INVALIDATE: Creation event supersedes negative cache for {entry.resource_id}.",
                manifest_digest=cls._hash(f"SUPERSEDED:{entry.resource_id}")
            )

        # Gate 3: TTL bounds
        if entry.ttl_sec > policy.max_ttl_sec:
            return NegativeCacheManifest(
                verdict=NegativeCacheVerdict.CACHE_INVALIDATED,
                directive=f"REJECT: TTL {entry.ttl_sec:.0f}s > {policy.max_ttl_sec:.0f}s. Truncate or reject.",
                manifest_digest=cls._hash(f"TTL:{entry.ttl_sec:.0f}")
            )

        # Gate 4: Cache hit
        age = policy.current_epoch - entry.write_epoch
        if age > entry.ttl_sec:
            return NegativeCacheManifest(
                verdict=NegativeCacheVerdict.CACHE_INVALIDATED,
                directive=f"EXPIRED: Entry age {age:.0f}s > TTL {entry.ttl_sec:.0f}s.",
                manifest_digest=cls._hash(f"EXPIRED:{entry.resource_id}")
            )

        # Gate 5: Valid
        digest = cls._hash(json.dumps({
            "resource": entry.resource_id,
            "age": f"{age:.0f}",
            "ttl": f"{entry.ttl_sec:.0f}",
            "authenticated": str(entry.writer_was_authenticated)
        }, sort_keys=True))
        
        return NegativeCacheManifest(
            verdict=NegativeCacheVerdict.CACHE_HIT_VALID,
            directive=f"PROCEED: Negative cache entry for {entry.resource_id} valid.",
            manifest_digest=digest
        )