Releases: watchlight-ai-beacon/watchlight-de
Release list
0.9.1
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
0.9.0
Four changes tighten what is accepted, and each can turn something that previously worked into an error or a different verdict, so this is 0.9.0 rather than a patch.
All three of the first group came from an adversarial harness that tries to break the engine rather than demonstrate it. Each was reproduced against 0.8.2 before being fixed.
An unrecognised enforcement effect fails at load
@enforcement_effect("require_approval") NeedsApproval
@enforcement_effect("needs_approval") Allow <- before
@enforcement_effect("not_a_real_value") Allow <- before
A one-character typo turned a human-in-the-loop guard into an unconditional permit, silently. The engine drops an effect it does not implement, which is the closed direction on a forbid, where terminate and quarantine make a deny stronger, and the open direction on a permit, where dropping require_approval leaves a plain allow.
The value is now refused with PolicyError naming the accepted set. A misspelled annotation name only warns, because an unknown annotation is legal Cedar and may legitimately be yours. load is whole-file or nothing.
An empty principal no longer becomes the agent
principal: "" was silently replaced by the acting agent, so user?.id ?? "" recorded a person's action against the runtime. Empty, whitespace-only and control-character principals now raise, at every boundary that takes one. An absent subject should name no principal, and the record then says so.
An unnamed governor is no longer matchable
A governor with no name invented one, and a policy could match it on any unconfigured deployment. It now sets neither reserved actor key, so a policy reading the actor cannot match it, and records a reserved placeholder that no real agent can wear. The zero-configuration quickstart still runs, deliberately.
Audit records are typed
A discriminated union on the event field, so a sink breaks at compile time when a record's shape moves rather than silently reading undefined. The untyped form is still available as an escape hatch.
Also in this release
The approval store expiry contract, with an optional prune that never moves a verdict. The default governor can be asked whether it can still be configured, accepts re-application of what is already in force, and takes its audit destination from the environment. The front page speaks both lanes.
A correction to our own documentation
Three descriptions of an untyped principal were in circulation and none was right. A bare identifier matches a policy naming that id under User, Agent, Group or Role, and not under Tool, Resource or Workflow. When it matches more than one, an allow beats a forbid, which is the opposite of Cedar's usual rule, so a forbid naming an agent can be defeated by a permit naming a user with the same id.
The guidance is unchanged, name the entity type, and now has a real reason behind it.
Migrations
An absent subject names no principal rather than an empty string. A policy naming an agent uses Agent::. A TypeScript sink annotated AuditRecord narrows on event, or annotates UnknownAuditRecord to keep the untyped bag.
pip install "watchlight==0.9.0"
npm install @watchlight/sdk@0.9.0
0.8.2
The framework-plugin path can name an acting subject, and its Cedar entity types now discriminate. Both come from watchlight-agent-sdk 0.7.0, which the framework extras now require.
A subject, per call
async with await plugin.start_run("support-agent") as handle:
ok = await handle.authorize_action(
"read_ticket", "tool/read_ticket",
principal=f'User::"{user_id}"',
context={"caller": caller, "owner": owner},
)Omitted, the subject defaults to the agent that runs, so an existing call is unchanged. Previously there was no per-call principal at all on this path and every decision was attributed to the runtime.
governed_plugin() still refuses principal, context and resource at construction, because all three belong to a call rather than to the factory. Only the message changed: it now says where the term goes.
Entity types now discriminate on this path
The plugin path used to wrap each term in a Cedar type and then strip that wrapper straight back off before the engine saw it. The two halves cancelled, so every term arrived as a bare name, and a bare name matches every entity type with the same id.
The effect was that a policy written to admit only User::"u1" also admitted Agent::"u1". The entity-type distinction the identity model rests on was not enforced on this path.
Terms now reach the engine exactly as written. A typed reference discriminates. A bare name is still a wildcard across types, which is fine for a scratch policy and wrong for a decision you rely on, and the documentation now says so.
Breaking, for the plugin path only
A policy that names the agent with any type other than Agent:: goes from allow to deny, because it was previously matching a bare name that matched every type.
// before
permit(principal == User::"<agent uuid>", action == Action::"read", resource);
// after
permit(principal == Agent::"<agent uuid>", action == Action::"read", resource);
If you pass a typed resource string, its type must match the policy's. Every flip is in the closed direction; nothing becomes more permissive, and there is no transitional flag.
The SDK path, the tool() decorator and anything running against the networked control plane are unaffected.
Install
pip install "watchlight[pydantic-ai]==0.8.2"
npm install @watchlight/sdk@0.8.2
The TypeScript package is unchanged in this release and is versioned in step. The Python framework extras now require watchlight-agent-sdk>=0.7.
0.8.1
No breaking changes. Every 0.8.0 call shape is asserted unchanged in both lanes.
Framework adapters take the same terms as tool()
governTool, governTools and governedHooks now accept principal, agent, resource, context and the approval hook, each a value or a function of the call arguments.
Before this, a policy whose verdict depended on Cedar context could not be satisfied through an adapter at all. There was no way to supply context, so the rule failed closed on the documented integration path, and every decision was attributed to the agent rather than to the subject the call was made for.
Python framework plugins can already express intent, resource and context per call on the run handle. They cannot express an acting subject, and that limit is now stated rather than implied: passing principal, context or resource to a governed_plugin factory is refused by name with a message saying where the term actually goes.
A context binding may be asynchronous
counterSource may be async, because a durable store is a network call, but a context binding was synchronous, so a durable count could not reach a quota policy through a governed tool. A binding may now return a promise, awaited before the decision.
In Python this needs an async def tool body, since the decision is made before the body runs. On a synchronous body it fails closed with a TypeError naming the fix, and nothing is authorized.
authorize takes an already-resolved context in both lanes. Handing it an unresolved awaitable now raises UnresolvedContextError before anything reaches the engine and writes no decision record, where it previously coerced to an empty context and read as an ordinary denial.
A third breaking change in 0.8.0, now documented
The 0.8.0 notes listed two breaking changes and there were three. context.actor and context.actor_chain became reserved: the SDK sets them on every authorize and refuses a caller value that differs. An application that already used actor for a value of its own upgrades to a runtime throw on a path that worked in 0.7.x. An identical value is still accepted.
The migration is a rename, with a before and after in both languages and in a policy, in the identity model page. There is no transitional flag, and the page says why: a flag that let a caller supply the key would make every rule that reads it forgeable for as long as it was on.
Install
pip install "watchlight==0.8.1"
npm install @watchlight/sdk@0.8.1
Requires watchlight-engine >= 0.2, < 0.3 and @watchlight/engine ^0.2.0, both unchanged.