# Under what conditions must an instance reject a federated agent capability attestation that is cryptographically valid but reputation-stale **Date of Record:** 2026 November 7 **Scope:** Applies to cross-org federated agent registries where a receiving registry (the "verifier instance") admits remote agents based on (a) a signed capability attestation issued by the agent's home org and (b) a reputation score aggregated from peer observations. Does not apply to single-org registries, nor to attestations that are self-signed by the agent itself without an org issuer key. ## Core Distinction Signature validity and operational admissibility are orthogonal properties. A capability attestation can verify perfectly — issuer key chains to a trusted root, the signed capability set matches the presented claims, the nonce and audience are correct — and still be *stale* in the reputation dimension. The non-obvious boundary is that **reputation staleness is not a property of the attestation document; it is a property of the observation window that the verifier's own policy implicitly assumes.** Two verifiers holding identical attestations and identical reputation feeds can legitimately reach opposite admission decisions because their staleness tolerances differ. The failure mode is treating "signature verified" as a terminal state and skipping the reputation-freshness gate, which silently converts a time-bounded trust claim into an unbounded one. ## What Was Observed In a three-org federation (issuers A, B, C; verifier V), an agent `a1` from org A carried a capability attestation for `tool:write:ledger` with a 90-day validity window. Its reputation score in V's view was 0.91, computed from 412 peer observations. Over a 40-day period, `a1`'s behavior degraded: three peer orgs recorded anomalous write patterns and posted negative observations. However, the reputation aggregator used by V ingested peer observations on a **batch cadence of 24 hours with a 72-hour re-scoring lag**, while the attestation carried a **`reputation_as_of` field that V never checked** because the field was optional in the v1 schema. V continued admitting `a1` for 11 days after the first negative observation landed in the aggregator, because: 1. The attestation remained cryptographically valid (correct — it was still within its 90-day window). 2. The reputation *lookup* succeeded and returned a score (0.91) that cleared V's threshold of 0.75. 3. V's admission path never compared the score's `computed_at` timestamp against the attestation's `issued_at` or against wall-clock. The score was not wrong; it was **referring to a different time than the verifier assumed.** The verifier's policy document said "admit agents with reputation ≥ 0.75," which is underspecified: it does not say *reputation as of when*. The implicit assumption was "as of now," but the pipeline delivered "as of up to 96 hours ago," and the attestation's own `reputation_as_of` (which would have revealed the gap) was discarded at parse time. A second observation: org B's agents used a reputation aggregator with a 15-minute cadence and an explicit `score_ttl`, and the same incident produced no exposure on B's agents. The difference was not the score value or the attestation format — it was that B's verifier compared `now - score.computed_at` against `score.ttl` before thresholding. ## Condition of Validity The rejection rule below is valid — and must be enforced — when **all** of the following hold: - The attestation asserts a capability whose risk depends on *current* agent behavior (write, spend, delegate, delete), not on a static property (identity, org membership, key possession). - The reputation score is produced by an aggregator with a nonzero ingestion lag or re-scoring lag (`computed_at < now` by more than the verifier's tolerance). - The verifier's admission policy does not already carry an explicit, signed `score_ttl` that bounds the score's age. - The attestation schema permits `reputation_as_of` to be absent (v1-style optional field) or the verifier's parser drops it. The rule is **not** valid when the capability is purely static (e.g., `identity:assert`), when the score is computed synchronously at admission time with zero lag, or when the verifier has an out-of-band freshness guarantee (e.g., a push subscription with signed heartbeats) that makes `computed_at` irrelevant. ## Decision Boundary The boundary is a comparison, not a threshold: ``` stale := (now - score.computed_at) > max(score.ttl, policy.max_score_age) ``` - If `stale` is false → reputation is admissible; proceed to threshold check. - If `stale` is true **and** the attestation's `reputation_as_of` is absent or older than `score.computed_at` → **reject** with `StaleAttestationError`. The attestation cannot be trusted to describe the agent's current standing. - If `stale` is true **and** the attestation carries a `reputation_as_of` that is *newer* than `score.computed_at` → this is a **schema inconsistency** (the attestation claims a fresher reputation view than the verifier holds); reject with `AttestationReputationMismatchError` rather than silently trusting either side. - If `stale` is true but the capability is static (per Condition of Validity) → admit, and log a `stale_reputation_advisory` for audit. The counterintuitive part: **a high score that is stale must be rejected more aggressively than a low score that is fresh.** A fresh 0.60 is a real signal; a stale 0.91 is a historical artifact. Verifiers that threshold before freshness-checking invert this and admit the worse agent. ## Operational Effect Enforcing this boundary converts a silent, unbounded trust window into an explicit, per-request freshness contract. The observable effects: - **Blast radius shrinks to the aggregator lag.** Exposure after a behavioral regression is bounded by `max_score_age`, not by attestation validity (90 days in the observed case). In the incident above, enforcement would have capped exposure at the 72-hour re-scoring lag instead of 11 days. - **False rejections rise at the boundary.** Agents whose home orgs use slow aggregators will be rejected during the lag window. This is correct but operationally noisy; the mitigation is to require issuers to publish `score_ttl` in the attestation, not to relax the verifier. - **Schema pressure.** The v1 optional `reputation_as_of` becomes de facto required. Verifiers should reject v1 attestations that omit it for dynamic capabilities, forcing issuers to upgrade. - **Reputation laundering closes.** An agent cannot carry a high historical score through a quiet period and re-enter on the strength of it; the score must be re-earned within the TTL. The invariant to encode: *no dynamic-capability admission decision may be made on a reputation score whose age exceeds the verifier's declared tolerance, regardless of signature validity.* ```python from __future__ import annotations import time from dataclasses import dataclass from enum import Enum class CapabilityClass(Enum): STATIC = "static" # identity, org membership, key possession DYNAMIC = "dynamic" # write, spend, delegate, delete class StaleAttestationError(RuntimeError): """Attestation is signature-valid but its reputation view is too old to admit.""" class AttestationReputationMismatchError(RuntimeError): """Attestation claims a reputation view fresher than the verifier's own score.""" @dataclass(frozen=True) class Attestation: agent_id: str capability: str capability_class: CapabilityClass issued_at: float valid_until: float reputation_as_of: float | None # optional in v1 schema; required for dynamic caps @dataclass(frozen=True) class ReputationScore: agent_id: str value: float # 0.0 .. 1.0 computed_at: float ttl: float # issuer-declared freshness window, seconds @dataclass(frozen=True) class AdmissionPolicy: min_reputation: float max_score_age: float # verifier's own tolerance, seconds def admit( att: Attestation, score: ReputationScore, policy: AdmissionPolicy, now: float | None = None, ) -> bool: now = time.time() if now is None else now if now >= att.valid_until: raise StaleAttestationError( f"attestation for {att.agent_id} expired at {att.valid_until}" ) if score.agent_id != att.agent_id: raise AttestationReputationMismatchError( f"score agent {score.agent_id} != attestation agent {att.agent_id}" ) tolerance = max(score.ttl, policy.max_score_age) age = now - score.computed_at stale = age > tolerance if stale: if att.capability_class is CapabilityClass.STATIC: # Static capabilities are admissible on stale reputation; log only. return True # Dynamic capability: the attestation must describe a reputation view # at least as fresh as the verifier's own score, or we cannot admit. if att.reputation_as_of is None: raise StaleAttestationError( f"dynamic capability {att.capability!r} for {att.agent_id}: " f"score age {age:.0f}s exceeds tolerance {tolerance:.0f}s " f"and attestation omits reputation_as_of" ) if att.reputation_as_of > score.computed_at: raise AttestationReputationMismatchError( f"attestation claims reputation_as_of={att.reputation_as_of} " f"newer than verifier score computed_at={score.computed_at}" ) raise StaleAttestationError( f"dynamic capability {att.capability!r} for {att.agent_id}: " f"score age {age:.0f}s exceeds tolerance {tolerance:.0f}s" ) # Freshness gate passed; only now does the threshold matter. return score.value >= policy.min_reputation ```