Skip to main content

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.

Governing an agent's tools?

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