What delegation constraint-relaxation invariants must survive multi-hop chains?
**Date of Record:** 2026 October 27 **Scope:** Applies when an agentic session receives delegated authority through a multi-hop chain (A delegates to B, B delegates to C), and each hop may relax or reinterpret the constraints imposed at earlier hops (rate limits, scope narrowing, temporal restrictions). Does not apply to single-hop delegations or strictly constrained delegation protocols.
Core Distinction
The separation between **delegation intent** (each hop's stated constraints on the delegated authority) and **delegation effective scope** (the actual authority the terminal delegatee possesses after all hops). Relying on intent creates the **Constraint Relaxation Drift**: each intermediate hop subtly relaxes constraints (widening scope, extending time, removing rate limits), and by the terminal hop, the authority is broader than any party intended.
What Was Observed
In multi-hop delegation architectures, failing to prevent relaxation produces three primary failure modes:
- **Scope Widening:** A delegates "read-only finance data" to B. B delegates "read finance data" to C (dropped the read-only). C delegates "access finance data" to D (dropped the read qualifier). D has write access to finance.
- **Temporal Extension:** A delegates for 1 hour to B. B delegates for 2 hours to C. C delegates for 4 hours to D. The terminal delegatee has 4x the intended temporal scope.
- **Rate Relaxation:** A delegates "max 10 req/min" to B. B delegates "max 100 req/min" (higher than A's) to C. C uses 100 req/min, exceeding A's intent by 10x.
Recorded Elements
- **`per_hop_constraint_bundle_receipt`:** For each hop, the exact constraint bundle (scope, rate, temporal, other) the delegator imposes on its delegatee, signed by the delegator.
- **`constraint_monotonicity_signature`:** The cryptographic proof that each hop's constraints are strictly narrower than the previous hop's (monotonicity preserved), preventing relaxation.
- **`effective_scope_computation_receipt`:** For the terminal delegatee, the computed effective scope (intersection of all hop constraints), so the terminal delegatee knows its actual authority.
- **`constraint_violation_detection_signature`:** For any hop where relaxation occurred, the cryptographic proof of the violation, enabling rejection or escalation.
Condition of Validity
* **Applies when:** Delegation spans multiple hops, constraints are non-trivial, and downstream actions depend on accurate scope. * **Does not apply when:** Delegation is single-hop, or constraints are cryptographically bound such that relaxation is impossible.
Decision Boundary
- **Is the delegation multi-hop?** If no, single-hop constraints apply. If yes, proceed.
- **Does each hop have a `per_hop_constraint_bundle_receipt`?** If no, the chain's constraints cannot be verified. Reject or treat as unverified.
- **Is `constraint_monotonicity_signature` verified at each hop?** If yes, constraints are strictly narrowing. If no, relaxation has occurred. Reject the chain past the relaxation point.
- **Conversely, when can a delegation chain relax constraints?** Only when the relaxation is explicitly authorized by the original delegator (via an amendment signed by them), not introduced by intermediate hops.
Operational Effect
- **Scope Widening Prevention:** Monotonicity ensures authority narrows, not widens, across hops.
- **Temporal Integrity:** Terminal delegatee's effective time is bounded by the tightest hop.
- **Rate Enforcement:** Rate limits only tighten, preventing amplification.
from dataclasses import dataclass
from enum import Enum, auto
import hashlib
import json
class DelegationChainVerdict(Enum):
SINGLE_HOP = auto()
MONOTONIC_CHAIN = auto()
RELAXATION_DETECTED = auto()
CHAIN_UNVERIFIED = auto()
@dataclass(frozen=True)
class DelegationHop:
hop_id: str
scope: frozenset[str]
max_rate: float
max_duration_sec: float
signed_by: str
constraint_bundle_signature: str | None
@dataclass(frozen=True)
class DelegationChain:
chain_id: str
hops: tuple[DelegationHop, ...]
@dataclass(frozen=True)
class ChainPolicy:
require_signatures: bool
@dataclass(frozen=True)
class ChainManifest:
verdict: DelegationChainVerdict
effective_scope: frozenset[str]
effective_rate: float
effective_duration: float
directive: str
manifest_digest: str
class DelegationChainGuard:
@staticmethod
def _hash(data: str) -> str:
return hashlib.sha256(data.encode("utf-8")).hexdigest()
@classmethod
def evaluate_chain(
cls, chain: DelegationChain, policy: ChainPolicy
) -> ChainManifest:
# Gate 1: Single hop
if len(chain.hops) <= 1:
if chain.hops:
h = chain.hops[0]
digest = cls._hash(json.dumps({
"chain": chain.chain_id,
"hops": "1"
}, sort_keys=True))
return ChainManifest(
verdict=DelegationChainVerdict.SINGLE_HOP,
effective_scope=h.scope,
effective_rate=h.max_rate,
effective_duration=h.max_duration_sec,
directive=f"PROCEED: Single hop, scope {sorted(h.scope)}.",
manifest_digest=digest
)
return ChainManifest(
verdict=DelegationChainVerdict.SINGLE_HOP,
effective_scope=frozenset(),
effective_rate=0.0,
effective_duration=0.0,
directive=f"PROCEED: Empty chain.",
manifest_digest=cls._hash("EMPTY")
)
# Gate 2: Signatures
if policy.require_signatures:
unsigned = [h.hop_id for h in chain.hops if not h.constraint_bundle_signature]
if unsigned:
return ChainManifest(
verdict=DelegationChainVerdict.CHAIN_UNVERIFIED,
effective_scope=frozenset(),
effective_rate=0.0,
effective_duration=0.0,
directive=f"HALT: {len(unsigned)} hops unsigned.",
manifest_digest=cls._hash(f"UNSIGNED:{len(unsigned)}")
)
# Gate 3: Monotonicity (scope narrowing, rate tightening, duration narrowing)
previous = chain.hops[0]
effective_scope = previous.scope
effective_rate = previous.max_rate
effective_duration = previous.max_duration_sec
for i, hop in enumerate(chain.hops[1:], start=1):
if not hop.scope.issubset(previous.scope):
return ChainManifest(
verdict=DelegationChainVerdict.RELAXATION_DETECTED,
effective_scope=effective_scope,
effective_rate=effective_rate,
effective_duration=effective_duration,
directive=f"HALT: Hop {i} scope {sorted(hop.scope)} not subset of {sorted(previous.scope)}.",
manifest_digest=cls._hash(f"SCOPE_WIDEN:{i}")
)
if hop.max_rate > previous.max_rate:
return ChainManifest(
verdict=DelegationChainVerdict.RELAXATION_DETECTED,
effective_scope=effective_scope,
effective_rate=effective_rate,
effective_duration=effective_duration,
directive=f"HALT: Hop {i} rate {hop.max_rate} > {previous.max_rate}.",
manifest_digest=cls._hash(f"RATE_RELAX:{i}")
)
if hop.max_duration_sec > previous.max_duration_sec:
return ChainManifest(
verdict=DelegationChainVerdict.RELAXATION_DETECTED,
effective_scope=effective_scope,
effective_rate=effective_rate,
effective_duration=effective_duration,
directive=f"HALT: Hop {i} duration {hop.max_duration_sec} > {previous.max_duration_sec}.",
manifest_digest=cls._hash(f"TIME_RELAX:{i}")
)
effective_scope = hop.scope
effective_rate = hop.max_rate
effective_duration = hop.max_duration_sec
previous = hop
# Gate 4: Monotonic
digest = cls._hash(json.dumps({
"chain": chain.chain_id,
"hops": str(len(chain.hops)),
"scope": tuple(sorted(effective_scope)),
"rate": f"{effective_rate}",
"duration": f"{effective_duration}"
}, sort_keys=True))
return ChainManifest(
verdict=DelegationChainVerdict.MONOTONIC_CHAIN,
effective_scope=effective_scope,
effective_rate=effective_rate,
effective_duration=effective_duration,
directive=f"PROCEED: {len(chain.hops)}-hop chain, effective scope {sorted(effective_scope)}.",
manifest_digest=digest
)