The egress hook now has a deadline on every path. Read the breaking note below before upgrading if your onResult hook can be slow.
EgressRecord.withheld has always been documented as "the hook threw, or outran its deadline — the payload was never released". Only governedHooks enforced one. On govern.tool() and the LangChain adapters a hook could run indefinitely and then release:
tool() onResult sleeps 12000ms -> released "SECRET" after 12002ms
governTool onResult sleeps 12000ms -> released "SECRET" after 12002ms
The consequence was a hang rather than a leak. That is the safe direction for disclosure and the wrong one for availability, on exactly the seam where a slow check belongs: a classifier call, a remote policy lookup.
All three paths now take onResultTimeoutMs with the same 8 second default. On expiry the call throws the exported EgressTimeout and the record says withheld: true, identical to a hook that throws. A hook that settles afterwards is discarded, so a slow hook can never release a payload late.
There is no value that switches the deadline off. Zero, negative, NaN and Infinity are refused where the tool is wrapped, not on first call. An unbounded hook is the defect being closed, so it is not reachable by configuration. A hook that genuinely needs longer takes a larger number, which is visible in review.
Breaking
An egress hook slower than 8 seconds now withholds where it used to release late. Nothing changes for a hook that finishes inside it. If yours is meant to be slow, pass a larger onResultTimeoutMs or on_result_timeout_ms.
Python
on_result_timeout_ms is the same option with the same default, enforced on an async tool body by cancelling the hook. Python cannot interrupt running code, so the option on a synchronous body is refused with a TypeError rather than accepted and silently ignored, and a hook that blocks the event loop is documented as not preemptable.
Also
The warning on a governor with no name configured now says which route the placeholder actually closes. A policy reading context.actor cannot match an unconfigured governor, because the key is unset. A policy naming the placeholder as an Agent:: principal does match it, because that is the recorded subject when a call names no principal. The identity model page explains why the second route cannot be closed: the engine requires a principal, and a Cedar entity id is an arbitrary string, so no id can be made unnameable.
pip install "watchlight==0.9.1"
npm install @watchlight/sdk@0.9.1