Developer Edition Examples
Runnable, copy-paste examples using the in-process engine. Every policy and decision below was produced by the published engine — the outputs are real, not illustrative.
These show the policy layer directly. To govern real tool calls with a
decorator (@govern.tool), see the Quickstart; for
end-to-end, self-contained scripts — a research agent, framework plugins, and an
MCP server — see the
examples/ gallery.
Each example uses this tiny helper so the policy logic stays front and center:
import json
import watchlight_engine
def new_engine(*policies):
eng = watchlight_engine.PolicyEngine()
for name, code in policies:
eng.add_policy(json.dumps({"name": name, "code": code}))
return eng
def decide(eng, principal, action, resource, **context):
resp = json.loads(eng.authorize(json.dumps({
"principal": principal,
"action": action,
"resource": resource,
"context": context,
})))
return resp["decision"]
The engine is fail-closed: with no policy that permits a request, the answer
is always Deny.
1. Role-based access (RBAC)
Grant a principal broad authority; everyone else is denied by default.
eng = new_engine(
("admin-all", 'permit(principal == User::"admin", action, resource);'),
)
decide(eng, "admin", "delete", "customer-record") # -> "Allow"
decide(eng, "alice", "delete", "customer-record") # -> "Deny"
2. Action-scoped permissions
Permit only specific actions, regardless of principal. A read-only grant lets
read through and denies write.
eng = new_engine(
("read-only", 'permit(principal, action == Action::"read", resource);'),
)
decide(eng, "reporting-agent", "read", "ledger") # -> "Allow"
decide(eng, "reporting-agent", "write", "ledger") # -> "Deny"
3. Resource-scoped permissions
Restrict a grant to a specific resource.
eng = new_engine(
("public-only", 'permit(principal, action, resource == Resource::"public-doc");'),
)
decide(eng, "agent", "read", "public-doc") # -> "Allow"
decide(eng, "agent", "read", "secret-doc") # -> "Deny"
4. Attribute-based access — numeric limits (ABAC)
when conditions read the request context, so a decision can depend on runtime
values. Here, a transfer is allowed only under a threshold.
eng = new_engine(
("under-limit", 'permit(principal, action, resource) when { context.amount <= 1000 };'),
)
decide(eng, "payments-agent", "transfer", "acct", amount=500) # -> "Allow"
decide(eng, "payments-agent", "transfer", "acct", amount=5000) # -> "Deny"
5. Attribute-based access — path patterns
Cedar's like operator matches string patterns — useful for path or namespace
scoping.
eng = new_engine(
("public-paths", 'permit(principal, action, resource) when { context.path like "public/*" };'),
)
decide(eng, "agent", "read", "file", path="public/readme") # -> "Allow"
decide(eng, "agent", "read", "file", path="secret/key") # -> "Deny"
6. Context conditions — require MFA and tier
Combine conditions with &&. This grant requires both a completed MFA and a gold
tier.
eng = new_engine(
("mfa-and-tier",
'permit(principal, action, resource) when { context.mfa == true && context.tier == "gold" };'),
)
decide(eng, "ops-agent", "read", "secret", mfa=True, tier="gold") # -> "Allow"
decide(eng, "ops-agent", "read", "secret", mfa=True, tier="silver") # -> "Deny"
7. Quarantine an agent with unless
Permit an agent normally, unless it has been flagged — a clean way to express "this agent is quarantined, revoke its authority" without deleting the grant. This mirrors how the platform sidelines a misbehaving agent.
eng = new_engine(
("agent-unless-quarantined",
'permit(principal == User::"agent", action, resource) unless { context.quarantined == true };'),
)
decide(eng, "agent", "read", "data", quarantined=False) # -> "Allow"
decide(eng, "agent", "read", "data", quarantined=True) # -> "Deny"
8. Explicit denials win (deny-overrides)
A forbid rule always beats a permit. This is how you express a hard stop that
no grant can override.
eng = new_engine(
("admin-all", 'permit(principal == User::"admin", action, resource);'),
("no-deletes", 'forbid(principal, action == Action::"delete", resource);'),
)
decide(eng, "admin", "read", "customer-record") # -> "Allow"
decide(eng, "admin", "delete", "customer-record") # -> "Deny" (forbid wins)
9. Gate an agent tool call
Put the decision in front of the action so it is refused before it runs — the core of governed execution.
eng = new_engine(
("agent-can-read",
'permit(principal == User::"research-agent", action == Action::"read", resource);'),
)
def governed(principal, action, resource, run):
if decide(eng, principal, action, resource) != "Allow":
raise PermissionError(f"Denied by policy: {principal} may not {action} {resource}")
return run()
# Allowed — the read happens.
governed("research-agent", "read", "public-dataset", lambda: fetch("public-dataset"))
# Denied — PermissionError is raised before `transfer_funds` is ever called.
governed("research-agent", "transfer", "treasury", lambda: transfer_funds("treasury"))
Where to go next
- Cedar policy syntax — the full policy
language: entity types,
when/unless, operators likelikeandin. - Govern a real agent — put these decisions in front of a live agent's tool calls, or govern a whole framework agent in-process.
examples/gallery — end-to-end runnable scripts: a research agent, framework plugins, an MCP server.- Developer Edition vs Enterprise — when to graduate to the governed control plane.