What invariants must survive when an agent's capabilities are silently downgraded?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session's effective capabilities are reduced mid-session (revoked permissions, expired credentials, quota exhaustion, or policy changes) without explicit notification, causing the agent to continue operating on outdated capability assumptions. Does not apply to sessions with capability monitoring that actively surfaces changes, or sessions short enough that downgrade cannot occur mid-flight.
Core Distinction
The separation between **perceived capability** (what the agent believes it can do, based on session-start state) and **effective capability** (what the agent can actually do now, given current permissions and quotas). Relying on perceived capability creates the **Silent Downgrade Trap**: the agent continues to act as if it has full capabilities, and its actions fail mid-execution—sometimes catastrophically, if failures occur after partial side effects.
What Was Observed
In long-running or permission-sensitive agent architectures, failing to detect silent downgrade produces three primary failure modes:
- **Mid-Transaction Permission Failure:** The agent initiates a multi-step transaction that requires write access. Write permission is revoked after step 2. Step 3 fails with permission denied, but steps 1-2 are already committed, and the transaction is in an unrecoverable partial state.
- **Quota Exhaustion Ambush:** The agent's tool quota is silently reduced (e.g., from 1000 to 100 calls/hour). The agent plans a workflow needing 500 calls, executes 100, and fails the remaining 400 without warning.
- **Policy Change Blindness:** Policy changes revoke the agent's access to a data domain. The agent continues to request that data, fails repeatedly, and interprets each failure as a transient error, exhausting its retry budget.
Recorded Elements
- **`capability_monitoring_receipt`:** A periodic probe of the agent's effective capabilities (can it still call tool X? access data domain Y?), run at intervals so silent downgrades are detected within bounded latency.
- **`capability_downgrade_notification_signature`:** For voluntary downgrades by the policy authority, a cryptographic notification of the downgrade, sent to the agent before the downgrade takes effect.
- **`capability_delta_detection_receipt`:** When monitoring reveals a delta between perceived and effective capability, the cryptographic record of the delta and the timestamp of detection.
- **`graceful_downgrade_handling_receipt`:** For in-flight operations during downgrade, the exact state saved so operations can be resumed if capability is restored, or cleanly rolled back if not.
Condition of Validity
* **Applies when:** Capabilities can be silently downgraded mid-session, agent behavior depends on capability assumptions, and downgrade-caused failures have material consequences. * **Does not apply when:** Capabilities are immutable mid-session or downgrades are always explicitly notified.
Decision Boundary
- **Is the session long enough for capability changes to occur?** If no, standard behavior applies. If yes, proceed.
- **Is `capability_monitoring_receipt` being run periodically?** If yes, deltas will be detected. If no, silent downgrades are invisible.
- **Does `capability_delta_detection_receipt` show a delta?** If yes, the agent must adapt (halt, escalate, or gracefully hand off). If no, proceed with current capabilities.
- **Conversely, when can an agent continue without monitoring?** Only when the session is strictly bounded (completes before downgrade is possible) or all operations are strictly idempotent and can be safely retried.
Operational Effect
- **Mid-Transaction Failure Prevention:** Detection allows in-flight operations to halt before failure.
- **Quota Ambush Prevention:** Downgrades are detected before exhausting retry budgets.
- **Policy Change Visibility:** Policy changes surface as deltas, not as unexplained failures.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class DowngradeVerdict(Enum):
CAPABILITIES_STABLE = auto()
DOWNGRADE_DETECTED = auto()
NOTIFICATION_RECEIVED = auto()
NO_MONITORING = auto()
@dataclass(frozen=True)
class CapabilityProbe:
probe_id: str
perceived_capabilities: frozenset[str]
effective_capabilities: frozenset[str]
probe_epoch: float
downgrade_notification_received: bool
@dataclass(frozen=True)
class SessionContext:
session_start_epoch: float
current_epoch: float
monitoring_active: bool
@dataclass(frozen=True)
class DowngradePolicy:
require_monitoring: bool
@dataclass(frozen=True)
class DowngradeManifest:
verdict: DowngradeVerdict
lost_capabilities: frozenset[str]
directive: str
manifest_digest: str
class SilentDowngradeGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_probe(
cls, probe: CapabilityProbe, ctx: SessionContext, policy: DowngradePolicy
) -> DowngradeManifest:
# Gate 1: No monitoring
if not ctx.monitoring_active:
if policy.require_monitoring:
return DowngradeManifest(
verdict=DowngradeVerdict.NO_MONITORING,
lost_capabilities=frozenset(),
directive=f"HALT: Monitoring inactive, cannot detect silent downgrade.",
manifest_digest=cls._hash("NO_MONITORING")
)
digest = cls._hash(json.dumps({
"probe": probe.probe_id,
"monitoring": "OFF"
}, sort_keys=True))
return DowngradeManifest(
verdict=DowngradeVerdict.CAPABILITIES_STABLE,
lost_capabilities=frozenset(),
directive=f"PROCEED_WITH_RISK: Monitoring off, assuming capabilities stable.",
manifest_digest=digest
)
# Gate 2: Notification
if probe.downgrade_notification_received:
digest = cls._hash(json.dumps({
"probe": probe.probe_id,
"notification": "RECEIVED",
"effective": tuple(sorted(probe.effective_capabilities))
}, sort_keys=True))
return DowngradeManifest(
verdict=DowngradeVerdict.NOTIFICATION_RECEIVED,
lost_capabilities=probe.perceived_capabilities - probe.effective_capabilities,
directive=f"PROCEED: Downgrade notified, adapt to {len(probe.effective_capabilities)} capabilities.",
manifest_digest=digest
)
# Gate 3: Delta detection
lost = probe.perceived_capabilities - probe.effective_capabilities
if lost:
return DowngradeManifest(
verdict=DowngradeVerdict.DOWNGRADE_DETECTED,
lost_capabilities=lost,
directive=f"HALT: Silent downgrade detected, lost {sorted(lost)}.",
manifest_digest=cls._hash(f"DOWNGRADE:{sorted(lost)}")
)
# Gate 4: Stable
digest = cls._hash(json.dumps({
"probe": probe.probe_id,
"epoch": f"{probe.probe_epoch:.0f}",
"capabilities": tuple(sorted(probe.effective_capabilities))
}, sort_keys=True))
return DowngradeManifest(
verdict=DowngradeVerdict.CAPABILITIES_STABLE,
lost_capabilities=frozenset(),
directive=f"PROCEED: Capabilities stable at probe {probe.probe_id}.",
manifest_digest=digest
)