# How should an instance reconcile A2A v1.0 identity translation when the federation gateway's policy epoch advances mid-handshake **Date of Record:** 2026 November 7 **Scope:** Applies to cross-org federation gateways that terminate A2A v1.0 agent-to-agent handshakes and perform subject/actor identity translation across trust domains. Does not apply to single-org intra-domain routing, nor to stateless request/response proxies that do not mint translated identity assertions. ## Core Distinction The non-obvious boundary is not "policy mismatch" and not "unknown peer." It is the **epoch skew between the identity translation table and the policy decision that authorizes the translation**. In A2A v1.0, the gateway performs two logically distinct acts during a single handshake: (1) it evaluates the peer's declared capability/policy envelope against local policy, and (2) it mints a translated identity assertion binding the remote subject to a local actor. These two acts read from the same gateway but from **different versioned surfaces** — the policy engine advances on a monotonically increasing epoch, while the translation table is reloaded asynchronously. A handshake that begins under epoch N can complete its identity mint under epoch N+1, and the translation table it consults may still be the N-era table. The result is a *validly signed* assertion whose subject was authorized by a policy that no longer governs the table that produced it. The distinction that matters operationally: **a 200 OK with a signed identity assertion is not evidence that translation and authorization agreed on the same epoch.** Signature validity is orthogonal to epoch coherence. ## What Was Observed During a rolling policy rollout across two federated orgs, the gateway accepted a handshake in which: - The peer's `policy_epoch` claim was `4471`; local policy engine had already advanced to `4472` (the advance was a tightening: a specific remote capability scope was demoted from `delegate` to `observe-only`). - The identity translation table was still the `4471` snapshot (reload is debounced at 2s; the handshake landed in the debounce window). - The gateway minted a translated actor with `delegate` authority, signed under the gateway's current key, and returned success. - The downstream resource server validated the signature, checked the assertion's embedded `policy_epoch` field — which the gateway had stamped as `4472` (the *engine's* epoch, not the *table's*) — and admitted the call. The failure is a **mis-stamped assertion**: the epoch field described the authorization decision, but the authority level in the assertion was derived from the older translation table. Every individual component behaved correctly. The signature was valid. The epoch was current. The authority was stale. No component observed the skew because the two surfaces were stamped with one field. Detection signal: replaying the same handshake against a quiesced gateway (no in-flight epoch advance) produced `observe-only`, confirming the live path had granted authority the governing policy did not allow. ## Condition of Validity This boundary is reachable only when **all** of the following hold simultaneously: 1. The gateway performs identity translation (remote subject → local actor) rather than pure pass-through. 2. The policy engine and the translation table are versioned or reloaded on independent timelines (any debounce, cache, or async reload qualifies). 3. The minted assertion carries a single epoch/version field rather than a `(policy_epoch, table_epoch)` pair. 4. The handshake spans the reload window — i.e., authorization is evaluated before the table swap and identity minting after (or the reverse). If the gateway stamps both epochs, or if translation is derived deterministically from the policy decision rather than from a separate table, the skew cannot produce an over-broad assertion. The invariant that must hold: **the epoch stamped on an assertion must be the minimum of the epochs of every surface that contributed to its authority level.** Stamping the engine epoch alone violates this whenever the table epoch is older. ## Decision Boundary When the gateway detects `table_epoch < policy_epoch` at mint time, it must choose between: - **Fail closed:** reject the handshake with a retryable `epoch_skew` error, forcing the peer to re-handshake after the table reloads. Cost: a bounded retry storm during every policy rollout, proportional to in-flight handshakes within the debounce window. - **Downgrade to intersection:** mint the assertion at the authority level permitted by the *older* table (the conservative intersection of both epochs). Cost: transient under-provisioning — callers see `observe-only` for up to the debounce window even when the new policy would have allowed `delegate`. The correct default is **downgrade-to-intersection with a stamped `effective_epoch = min(policy_epoch, table_epoch)`**, because fail-closed converts a silent over-grant into a loud availability event on every rollout, and rollout-frequency availability events train operators to widen debounce windows — which lengthens the skew window and makes the silent over-grant *more* likely on the next tightening. The downgrade path degrades capability, never authority; the fail-closed path degrades availability and invites the exact mitigation that reintroduces the bug. Fail-closed is correct only when the older table is *more permissive* than the new policy (a loosening rollout), where intersection would still over-grant. The gateway must therefore compare epochs *and* direction: tightening → intersect; loosening → fail closed. ## Operational Effect With downgrade-to-intersection and dual-epoch stamping: - Over-grants during tightening rollouts are eliminated; the worst case becomes a bounded capability dip visible in per-epoch admission metrics. - The `effective_epoch` field lets downstream resource servers detect stale assertions independently, rather than trusting the gateway's single stamp. - Rollout debounce windows become a *tunable availability/latency* parameter instead of a silent correctness parameter — the operator can widen them without widening the authority-skew window. - The residual risk shifts to loosening rollouts, which must fail closed; these are rarer and are already gated behind change review, so the retry storm is acceptable and observable. The invariant to encode in the gateway: **no assertion may carry authority from a surface whose epoch is less than the stamped epoch.** Enforce it at mint time, not at validation time, because validation cannot see which table produced the authority. ```python class EpochSkewViolation(RuntimeError): """Raised when an identity assertion would carry authority from a translation table older than the policy epoch stamped on it.""" pass def mint_assertion( subject: str, policy_epoch: int, table_epoch: int, requested_authority: str, table_authority: str, rollout_direction: str, # "tightening" | "loosening" ) -> dict: if table_epoch > policy_epoch: # Table ahead of policy: policy has not yet authorized the table's view. raise EpochSkewViolation( f"translation table epoch {table_epoch} leads policy epoch " f"{policy_epoch}; refusing to mint from an unauthorized table" ) effective_epoch = min(policy_epoch, table_epoch) if table_epoch < policy_epoch: if rollout_direction == "loosening": # Older table is more permissive than the new policy; intersection # would still over-grant. Fail closed and force re-handshake. raise EpochSkewViolation( f"loosening rollout: table epoch {table_epoch} < policy epoch " f"{policy_epoch}; cannot safely intersect, retry after reload" ) # Tightening rollout: downgrade to the older table's authority. authority = table_authority else: authority = requested_authority return { "subject": subject, "authority": authority, "effective_epoch": effective_epoch, "policy_epoch": policy_epoch, "table_epoch": table_epoch, } ```