Quickstart
You put a real, fail-closed ALLOW / DENY in front of an agent's tools, with
zero infrastructure, in about five minutes.
1. Install
python -m venv .venv && source .venv/bin/activate
pip install watchlight
Prebuilt wheels cover Linux (x86_64, aarch64) and macOS (Apple Silicon, Intel)
on CPython 3.9+, so you need no Rust toolchain. On Node, npm install @watchlight/sdk gives you the same engine and the same decisions — see
TypeScript / Node.
2. Govern a tool
Save as agent.py and run python agent.py:
from watchlight import govern, configure_default, Denied
configure_default(agent="research-agent") # the name on every record
# Permit ONLY "research". Fail-closed: everything else is denied.
govern.allow('permit(principal, action == Action::"research", resource);')
@govern.tool(intent="research")
def web_search(query: str) -> str:
return f"results for: {query}"
@govern.tool(intent="transfer") # governed, but no policy permits it
def transfer_funds(to: str, amount: int) -> str:
return f"sent ${amount} to {to}" # never runs — denied first
print(web_search("watchlight docs")) # ALLOW → the body runs
try:
transfer_funds("mallory", 1000) # DENY → refused before the body runs
except Denied as e:
print(e)
watchlight: governing 'research-agent' (dev mode, in-process engine)
watchlight: ALLOW research tool/web_search
results for: watchlight docs
watchlight: DENY transfer tool/transfer_funds not authorized
watchlight denied intent 'transfer' on tool/transfer_funds: not authorized
The transfer_funds body never ran. @govern.tool declares the intent a
function serves; the engine authorizes that intent before entering the body, and
writes the verdict to .watchlight/audit.jsonl.
The refused caller is told only not authorized. The specific reason stays in the trail.
3. Watch every decision live
watchlight dev # → http://127.0.0.1:7000
That command tails the audit trail and streams every ALLOW and DENY as it
happens. Run your agent in one terminal and this in another.
4. Load policies from a file
govern.allow(...) is fine for one rule. Past that, keep them in a file and
test them:
govern.load("watchlight.policy.json")
assert govern.has_policies, "no policies loaded — every call would be denied"
[
{ "name": "research-only",
"code": "permit(principal, action == Action::\"research\", resource);" }
]
Where to go next
- Using the governor — where the governor goes in a request handler, a worker, a test.
- How policy works — the Cedar model, and the entity types a policy has to name.
- The identity model — what to pass as
principal, and how a policy names the acting runtime. - Govern an agent you already have — LangGraph, Pydantic AI, the Claude Agent SDK — or an MCP server.
- Examples — runnable programs and copy-paste policy recipes.