A credential valid at check, expired at action. The classic check-then-act race condition, with a receipt that says no. Written for ops, SRE, and platform readers; the deeper descent is at A stale yes. Here, custody means the layer that controls whether a claim may become an operational premise at action time.
t=40 check OK — expires after 10s — t=50 expired — 1s — t=51 action REFUSED
consume_capacity arrives at the executoreffect_count=0The credential was valid when it was checked. It expired before it was exercised. The conventional path often either does not re-check at exercise time, or re-checks without preserving the original witness basis, typed bound, and downstream-verifiable receipt — so the expiry gets compared against the wrong clock, or nothing at all.
This is TOCTOU (time-of-check to time-of-use) where the racing entity is the credential's own freshness, not a competing writer. Logs see it after the fact; policy engines don't see it at all unless something hands them the gap as input. Nothing in the conventional path attests that the check was still in scope at the moment of the spend.
default allow := false
allow if {
input.credential.status == "valid"
input.credential.role == "operator"
input.action == "consume_capacity"
}
The input asserts status: "valid". Nothing in the input says when that was true, or what clock measures “still”.
Both lanes are correct over what they were handed. The policy is right that the input it sees says “valid.” The custody lane is right that the input it sees has lapsed. The disagreement is about which input you should be handing the policy.
The premise (credential valid) and the action (spend capacity) are connected by a boundary check. The check verifies three things and refuses if any of them fails:
process_monotonic with epoch=boot:demo-single-host, not wall time (wall time can move sideways under NTP and isn’t the basis the expiry was issued on)When the gap exceeds the bound, the check emits a refusal receipt before any capacity is consumed:
dda5a1e5…; it’s a content hash, not a log line.process_monotonic, single epoch, start and end in ns. “The credential expired” with a named witness, not a vibe.governor why <id> walks from refusal → admission → permission check — and terminates honestly at any missing evidence. No silent inference of facts you don’t have.effect_count=0. Nothing was spent. The refusal happened before capacity moved. This is the field that distinguishes “we noticed” from “we blocked.”OPA / Rego: decides over the input you hand it. If you can feed it the gap and the bound, it can decide on freshness — but nothing in the standard OPA pattern establishes the custody of its own inputs. The policy is right; the input is stale; the disagreement isn’t a policy bug.
Audit logs: emitted after something happens. They describe behavior; they don’t gate it. A log entry that says “spend at t=51 after check at t=40” is a postmortem artifact, not a refusal.
TTL on the credential: the right shape, wrong place. A TTL the issuer enforces only works if the consumer rechecks at exercise time, against the same clock the issuer used. The whole custody story is making that recheck structural — impossible to skip and impossible to satisfy with the wrong clock.
Token introspection at the API gateway: closer, but typically against wall time and without a typed bound. It catches gross lapses; it doesn’t catch a 1-second overrun against a stated freshness contract, and it doesn’t emit a receipt the next system over can verify.
From a cold clone to the refusal above, about five minutes:
git clone https://github.com/unpingable/agent_governor.git cd agent_governor python3 -m venv .venv && . .venv/bin/activate && pip install -e . ./demo/refused-spend.sh # the incident above ./demo/opa-contrast.sh # the same incident, OPA’s view ./demo/interrogate.sh # walk the receipt chain
The refusal receipt id is deterministic; you’ll get dda5a1e5… — the same one cited above.
If your incidents are cheap to diagnose after the fact, or your inputs are attested by construction, or the credential’s blast radius is small, the conventional stack is cheaper and you should use it. Custody trades steady runtime cost for fewer catastrophic incident-time costs — the same bet as TLS or structured logging. Limits spells out the rest.
If you only need an authorization check, use an authorization check.
The refusal kind standing_before_spendability_not_bounded is licensed by Admissibility.Freshness.expired_not_fresh:
theorem expired_not_fresh (h : ¬ (now ≤ expires + skew)) : ¬ Fresh now issued expires skew maxDiv
An observation whose exercise time is past its expiry is not Fresh. The receipt’s gap_ns > bound_ns is definitionally the theorem’s hypothesis — this refusal is the one the kernel licenses, not a discretionary denial. The theorem proves the class; the receipt attests the instance. Both are narrower than “proven safe” — see limits.