Skip to main content

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

AnnotationA matching forbid yieldsUse it for
(none)Denya hard block
require_approvalDeny + human-approval gatehuman-in-the-loop — pause for a person
observeAllow + "would have denied"rolling out a new rule in monitor mode
quarantineQuarantinefreeze the agent (durable)
terminateTerminateend the current run
escalateDeny + SOC/HITL alerta 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.

Gate on the effect, not just the decision

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.