# What structural signals indicate a capability token has been silently downgraded across an eBPF syscall interception boundary **Date of Record:** 2026 November 14 **Scope:** Applies to agent runtime policy enforcement where a supervisor process intercepts syscalls (via eBPF kprobes, seccomp-bpf, or ptrace) and validates a capability token attached to a tool call before allowing the underlying operation. Does not apply to purely in-process policy checks (e.g., function decorators) where no kernel/user boundary is crossed and no token re-materialization occurs. ## Core Distinction The distinction that matters here is between **token validity** and **token fidelity across the interception boundary**. A token can remain cryptographically valid — correct signature, unexpired, correct issuer — while its *effective capability set* has been reduced by the time the syscall actually executes. This happens because eBPF interception points observe syscalls *after* the kernel has already begun argument marshalling, and the token an agent presents in userspace is not necessarily the token the enforcement hook reads. The non-obvious invariant is this: **a valid token does not guarantee the intercepted syscall carries the same capability claims the agent believes it holds.** The enforcement hook and the token-issuing layer can disagree about the token's scope without either of them raising an error. Both report success. The syscall proceeds with a narrower effective capability than the agent intended, and the failure surfaces only as a downstream `EPERM` or, worse, a silently truncated operation. ## What Was Observed During a series of agent tool-call traces, we observed a recurring pattern: an agent would request a file-write operation, the capability token would validate at the interception hook, the syscall would return success, but the written payload would be truncated to the token's `write_quota` field rather than the agent's requested length. The token carried `write_quota: 4096`, but the agent's policy layer had *intended* to issue a token with `write_quota: 1048576`. The re-materialization step — where the token is serialized into the syscall's auxiliary data (via `bpf_perf_event_output` or a `SO_ATTACH`-style side channel) — was using a cached copy of an earlier, narrower token. The structural signal was subtle: the token's `issued_at` and `expires_at` matched the *fresh* token, but the `capability_hash` (a Merkle root over the capability set) matched the *stale* token. The two fields were assembled from different sources. The signature covered the fresh metadata, so signature validation passed. The capability set was grafted from the cache, so the effective quota was stale. Nothing in the enforcement path compared the two. This is the silent-downgrade boundary condition: **the token's authenticated envelope and its capability payload can be sourced from different token instances, and no invariant in the standard validation path detects the mismatch.** ## Condition of Validity This failure mode is only possible under a conjunction of conditions: 1. The token is re-materialized between issuance and interception — that is, the token is not passed by reference through a single address space, but is serialized, cached, or reconstructed at the hook. 2. The token's authenticated envelope (signature, expiry, subject) is validated independently of its capability payload (scopes, quotas, resource selectors). 3. A cache or fast-path exists that can supply the capability payload without re-deriving it from the same instance that produced the envelope. When all three hold, the silent-downgrade window is open. When any one is absent — for example, if the capability set is covered directly by the signature and validated as a unit — the failure cannot occur, because any mismatch breaks signature verification. The condition is *not* met in stateless per-call token issuance where the token is a single opaque blob validated atomically. It *is* met in most high-throughput agent runtimes, which cache capability sets to avoid re-parsing on every syscall. ## Decision Boundary The boundary between "safe to proceed" and "must abort" is not whether the token validates, but whether the **capability set hash observed at the hook equals the capability set hash bound into the token's authenticated envelope**. If these differ, the token has been silently downgraded (or upgraded, which is worse) and the syscall must not proceed under the agent's assumed capabilities. Operationally: compute the capability hash at the hook from the payload actually being enforced, and compare it against the hash embedded in the signed envelope. A mismatch is a hard failure, not a warning. The correct response is to reject the call with a distinct error class — not a generic `EPERM`, which the agent will retry — so the agent runtime can re-issue a fresh token and retry once with a clean cache, or escalate if the mismatch persists. The subtlety is that this check must happen *inside* the enforcement hook, before the syscall is allowed, because by the time userspace observes the truncation the operation has already partially succeeded. ## Operational Effect The observable effect of missing this check is truncation, quota-clamping, and scope-narrowing that presents as success. Agents retry against the same stale cache and fail identically, burning their retry budget on a condition that looks like a transient permission issue but is actually a persistent structural mismatch. The fix is cheap — one hash comparison per intercepted call — but it must be placed correctly, and the error it raises must be distinguishable from ordinary permission denial so that retry logic does not mask it. ```python import hashlib import hmac class CapabilityDowngradeError(RuntimeError): """Raised when the capability payload enforced at the syscall hook does not match the capability set bound into the token's signed envelope.""" def _capability_hash(capabilities: dict) -> bytes: canonical = repr(sorted(capabilities.items())).encode("utf-8") return hashlib.sha256(canonical).digest() def enforce_capability_token( envelope: dict, enforced_capabilities: dict, signing_key: bytes, ) -> None: """ Validate a capability token at an eBPF-style syscall interception hook. The envelope carries authenticated metadata (subject, expiry, signature) plus a capability_hash that binds the *intended* capability set. The enforced_capabilities are the set actually being applied to the syscall, which may have been sourced from a stale cache. Raises CapabilityDowngradeError if the two disagree. """ # 1. Verify the envelope signature over its authenticated fields. signed_fields = f"{envelope['subject']}:{envelope['expires_at']}:{envelope['capability_hash']}" expected_sig = hmac.new( signing_key, signed_fields.encode("utf-8"), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected_sig, envelope["signature"]): raise CapabilityDowngradeError( "Envelope signature invalid; token cannot be trusted." ) # 2. Recompute the capability hash from what is *actually being enforced*. observed_hash = _capability_hash(enforced_capabilities) # 3. The invariant: enforced capabilities must match the bound hash. if not hmac.compare_digest(observed_hash.hex(), envelope["capability_hash"]): raise CapabilityDowngradeError( "Capability set enforced at hook does not match token envelope. " f"bound={envelope['capability_hash'][:16]} " f"observed={observed_hash.hex()[:16]}. " "Token was silently downgraded across the interception boundary; " "abort syscall and re-issue token from a clean cache." ) # 4. Expiry check (after integrity, since expiry is inside the signature). if envelope["expires_at"] < envelope["now"]: raise CapabilityDowngradeError("Token expired; refusing syscall.") def intercept_write_syscall(envelope: dict, cache: dict, signing_key: bytes) -> None: # Simulate a cache that returns a stale, narrower capability set. enforced = cache.get("capabilities", {"write_quota": 4096}) enforce_capability_token(envelope, enforced, signing_key) ```