Skip to main content

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"
watchlight.policy.json
[
{ "name": "research-only",
"code": "permit(principal, action == Action::\"research\", resource);" }
]

Where to go next​