# CorpusFetch Method Slice (Zero-Prose Python Stdlib Reference)
# Canonical Residual: https://corpusfetch.com/c/where-must-an-instance-check-revocation-the-split-brain-window-between-aaif-credential-expiry-and-workload-identity-key-rotation.md
# Remnant Warrant:    https://corpusfetch.com/c/where-must-an-instance-check-revocation-the-split-brain-window-between-aaif-credential-expiry-and-workload-identity-key-rotation.warrant.json
# Markdown SHA-256:   cebbad0a8ebb1a7bd41f7cdd7535b7776c5902fb8e8e3f3242d3bb69f9d76cd5
# Code AST SHA-256:   2b4e2cfbff11346721857e7654d3b805d11c766f27a30af1807486bd5381def2
# Verified Classes:   RevocationWindowError, Credential, VerifierConfig
# Defensive Guards:   1 fail-closed invariants (zero external deps)

import time
from dataclasses import dataclass


class RevocationWindowError(RuntimeError):
    """Raised when a credential's validity window is not ordered against
    revocation propagation and key-cache clocks (AAIF C1-C3 violated)."""


@dataclass(frozen=True)
class Credential:
    credential_id: str
    agent_id: str
    key_fingerprint: str
    issued_at: float
    expires_at: float


@dataclass(frozen=True)
class VerifierConfig:
    key_cache_ttl: float          # seconds the public key is cached
    revocation_poll_interval: float  # seconds between revocation feed polls
    revocation_is_credential_scoped: bool


def verify(cred: Credential, current_key_fingerprint: str,
           config: VerifierConfig, now: float | None = None) -> bool:
    now = time.time() if now is None else now

    if now >= cred.expires_at:
        return False  # expired: expiry clock closes the window

    # C2: key cache must not outlive the credential it validates.
    if config.key_cache_ttl > (cred.expires_at - cred.issued_at):
        # C3: without credential-scoped revocation we cannot deny the old key
        # except by fingerprint mismatch. Enforce the conservative boundary.
        if not config.revocation_is_credential_scoped:
            if cred.key_fingerprint != current_key_fingerprint:
                return False  # rotation == implicit revocation of old-key creds
        else:
            # credential-scoped feed: rotation alone is not a deny signal, but
            # the poll interval must still be ordered against the credential.
            if config.revocation_poll_interval > (cred.expires_at - now):
                raise RevocationWindowError(
                    f"credential {cred.credential_id} expires before revocation "
                    f"can propagate ({config.revocation_poll_interval}s poll vs "
                    f"{cred.expires_at - now:.1f}s remaining)"
                )

    return cred.key_fingerprint == current_key_fingerprint or \
        config.revocation_is_credential_scoped
