Enforcement Effects
A Cedar decision is binary — Allow or Deny. Real governance needs more than
that: pause this for a human, let it through but record that a new rule would
have blocked it, quarantine the agent. Watchlight adds those without
forking Cedar — you annotate a forbid with an enforcement effect, and the
engine maps the resulting Deny to a richer decision. Every decision and output
below was produced by the published watchlight-engine wheel.
import json
import watchlight_engine
def govern(*policies):
eng = watchlight_engine.PolicyEngine()
for name, code in policies:
eng.add_policy(json.dumps({"name": name, "code": code}))
return eng
def decide(eng, action, principal="agent", resource="db", **context):
r = json.loads(eng.authorize(json.dumps({
"principal": principal, "action": action, "resource": resource,
"context": context,
})))
effect = (r.get("details") or {}).get("enforcement", {}).get("effect")
return r["decision"], effect # e.g. ("Deny", "require_approval")
An effect is declared with the @enforcement_effect("…") annotation directly
above a forbid:
@enforcement_effect("require_approval")
forbid(principal, action == Action::"delete", resource);
The effects
| Annotation | A matching forbid yields | Use it for |
|---|---|---|
| (none) | Deny | a hard block |
require_approval | Deny + human-approval gate | human-in-the-loop — pause for a person |
observe | Allow + "would have denied" | rolling out a new rule in monitor mode |
quarantine | Quarantine | freeze the agent (durable) |
terminate | Terminate | end the current run |
escalate | Deny + SOC/HITL alert | a loud deny that pages a human, async |
Human-in-the-loop (require_approval)
The headline effect: instead of blocking outright, pause the action for a
human. The engine returns a Deny whose enforcement effect is
require_approval — your app reads that and holds the action for approval
instead of failing it.
eng = govern(
("allow-all", 'permit(principal, action, resource);'),
("delete-needs-ok",
'@enforcement_effect("require_approval")\n'
'forbid(principal, action == Action::"delete", resource);'),
)
print(decide(eng, "read")) # ('Allow', None) — no gate
print(decide(eng, "delete")) # ('Deny', 'require_approval') — pause for a human
('Allow', None)
('Deny', 'require_approval')
The delete did not run — but it wasn't hard-denied either. The signal to
your application is the effect: require_approval means "hold this and ask a
person." In the SDK this collapses to a NeedsApproval result; on the governed
platform it also opens a tracked approval request operators action from a queue.
require_approval and escalate both return decision == "Deny". Read
details.enforcement.effect to tell a human-approval pause apart from a hard
deny — the helper above does exactly this.
Monitor mode (observe)
Roll out a new forbid without enforcing it yet: observe downgrades its
Deny to Allow and records that the policy would have blocked the action.
Flip the annotation off once you trust the rule.
eng = govern(("shadow-delete-ban",
'@enforcement_effect("observe")\n'
'forbid(principal, action == Action::"delete", resource);'))
print(decide(eng, "delete")) # ('Allow', 'observe') — allowed, but recorded as a would-block
Quarantine and terminate
Escalate beyond a plain deny — freeze the agent, or end the run:
eng = govern(("freeze",
'@enforcement_effect("quarantine")\n'
'forbid(principal, action == Action::"exfiltrate", resource);'))
print(decide(eng, "exfiltrate")) # ('Quarantine', 'quarantine')
eng = govern(("kill-run",
'@enforcement_effect("terminate")\n'
'forbid(principal, action == Action::"spawn_shell", resource);'))
print(decide(eng, "spawn_shell")) # ('Terminate', 'terminate')
Fail-closed interaction with conditions
Enforcement effects ride on top of the engine's fail-closed guarantee. If a
gated forbid can't be evaluated — for example it reads a context key the
caller omitted — the engine does not fall through to the human-approval path;
it denies. A guardrail whose applicability can't be confirmed always fails
closed. Condition gates on engine-supplied context.agent.* keys (which are
always present), or keep them unconditional.
What needs the governed platform
Two effects are containment actions across your fleet, so they belong to the
governed control plane (Watchlight Beacon), not the in-process engine:
revoke (withdraw an agent's identity trust) and sever_subtree (collapse a
whole sub-agent spawn tree). They emit signed governance events the platform
consumes. See DE vs Enterprise.
Next
- How Policy Works — the Cedar model these annotations sit on.
- Govern an agent — put these decisions in front of real tool calls, and attenuate a sub-agent's scope.