Watchlight — Developer Edition
Govern an AI agent in five minutes. One install, zero infrastructure, same API as production.
Watchlight is an Agent Runtime Governance Control Plane — it puts a policy decision point in front of every action your AI agents take, authorizing tool calls and recording a tamper-evident, value-free audit trail.
The Developer Edition is the free, open front door to it. It runs the real
authorization engine in-process, so you can add a governed ALLOW / DENY
to your agent on your own laptop — no server, no database, no signup. It's for
evaluating the model and shipping governed agents; the code you write here is the
code you run in production — going to production is pointing the same code at the
running control plane, not a rewrite.
For enterprises, Watchlight provides the wider Agent Runtime Governance Control Plane: signed, tamper-evident lineage; multi-tenant isolation; drift & anomaly detection → automatic quarantine; and fleet-wide revocation across every agent and environment. → watchlight.ai
How the pieces fit together
your code · agents · tools
┌─────────────┬──────────────────┬──────────────┬─────────────┐
│ @govern.tool│ LangGraph / │ MCP client │ custom app │
│ (decorator) │ Pydantic AI / │ (any tool) │ │
│ │ Claude Agent │ │ │
└──────┬──────┴────────┬─────────┴──────┬───────┴──────┬──────┘
│ │ │ │
watchlight watchlight.<fw> watchlight-mcp watchlight
(govern) .governed_plugin (PEP · MCP spec) SDK
└───────────────┴───────┬────────┴──────────────┘
▼
┌───────────────────────────────┐
│ Watchlight engine · Cedar │ the REAL engine,
│ in-process · zero infra │ a compiled wheel
└───────────────┬───────────────┘
PERMIT ─ forward │ ─ DENY (blocked before execution)
▼
value-free audit → .watchlight/audit.jsonl
▼
watchlight dev · http://localhost:7000
Production = the SAME code, pointed at the governed control plane
(signed audit · multi-tenant · drift→quarantine · fleet revocation).
| Package | You use it for |
|---|---|
watchlight |
the govern decorator + the watchlight dev dashboard |
watchlight[langgraph|pydantic-ai|claude-agent] |
govern an existing framework agent |
watchlight-agent-sdk |
the lifecycle SDK — InProcessClient, sessions, preflight, local lineage (imports as watchlight_core) |
watchlight-mcp |
govern an MCP server (a policy enforcement point) |
watchlight-engine |
the compiled in-process engine (pulled in automatically) |
watchlight[all] |
one install for the whole DE — SDK + every plugin + the MCP PEP; runs every example in the docs |
Just want everything?
pip install "watchlight[all]"pulls the SDK, all framework plugins, and the MCP PEP in one go — so every example on docs.watchlight.ai/de runs with a single install. Note the SDK's module name iswatchlight_core(there is nowatchlight-corepackage on PyPI).
Runnable, self-contained examples for every one of these are in
examples/ — start with
examples/governed_research_agent.py.
Quickstart
The
governdecorator,watchlight dev, and the framework + MCP integrations below all work today. Full guide: Developer Edition docs.
Prerequisites: Python 3.9+. Prebuilt wheels ship for Linux, macOS, and Windows — no Rust toolchain, no build step, no account.
pip install watchlight
Prefer an isolated environment? Install into a virtual environment instead:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install watchlight
Save this as agent.py and run python agent.py:
# agent.py — a complete, runnable program (copy, paste, run).
from watchlight import govern, Denied
# Permit ONLY the "research" intent. 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)
$ python agent.py
watchlight: governing 'my-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
That DENY line — in your own terminal, in under five minutes, with no
account — is the product. The transfer_funds body never ran.
TypeScript / Node
Same governance, in your Node app — no Python sidecar.
@watchlight/sdk runs the same
compiled engine in-process (WebAssembly).
Prerequisites: Node ≥ 18. @watchlight/sdk pulls in the compiled engine
(@watchlight/engine) automatically — no native toolchain.
npm install @watchlight/sdk
Then, in an ES-module / TypeScript file (await at top level needs "type": "module" or a .mjs file):
// agent.ts — the same DENY line, in Node.
import { govern, Denied } from "@watchlight/sdk";
// Permit ONLY the "research" intent. Fail-closed: everything else is denied.
govern.allow('permit(principal, action == Action::"research", resource);');
async function webSearch(query: string) { return `results for: ${query}`; }
async function transferFunds(to: string, amount: number) { return `sent $${amount} to ${to}`; } // never runs
const search = govern.tool(webSearch, { intent: "research" });
const transfer = govern.tool(transferFunds, { intent: "transfer" }); // no policy permits it
console.log(await search("watchlight docs")); // ALLOW → the body runs
try {
await transfer("mallory", 1000); // DENY → refused before the body runs
} catch (e) {
if (e instanceof Denied) console.log(e.message);
}
watchlight: governing 'my-agent' (dev mode, in-process engine)
watchlight: ALLOW research tool/webSearch
results for: watchlight docs
watchlight: DENY transfer tool/transferFunds not authorized
watchlight denied intent 'transfer' on tool/transferFunds: not authorized
It mirrors the Python package feature-for-feature:
- Runtime context, per-user, human-in-the-loop:
govern.tool(fn, { intent, principal?, resource?, context?, onNeedsApproval?, onResult? })— runtime facts into Cedarcontext.*, per-callprincipal, and a three-stateAllow/Deny/NeedsApprovalverdict with a single-use approval token. - Govern what a tool returns:
onResult(result, { intent, resource, principal, decisionId, obligations? })(Pythonon_result) runs after the body and before the caller sees the result — sanitize, screen, honour the decision's obligations, or re-authorize on its classification; a returned value replaces the payload, a throw withholds it (fail-closed). Writes a value-freeegressaudit record joined to the decision bydecision_id. - Obligations on an
Allow: a permit annotated@obligate_redact("ssn"),@obligate_max_items("25"),@obligate_log_values("false")(or any@obligate_<name>("raw")) yieldsd.obligations—{ redact, maxItems, logValues, extra }(Pythonresult["obligations"]:redact/max_items/log_values/extra, the last as{name: [values]}) — constraints your code oronResultmust honour. Several carriers merge to the strictest reading; only anAllowcarries them;DenyandNeedsApprovalnever do; an unreadable obligation fails closed (AuthorizeError). Needs engine >= 0.2.0. See the allow-but-redact pattern. - Frameworks:
governedHooks()for the Claude Agent SDK;governTool()for LangChain / LangGraph.js. - Data minimization:
govern.sanitize(text, { resource, decisionId, known? })— strip PII before an agent reads a document: structured detectors (email, phone, SSN, card, IBAN, IPv4, API key, labelled passport / date of birth), an app-suppliedknowndictionary (KNOWN; simple case-insensitive match — Unicode case folding differs between lanes), and opt-inPERSON/ADDRESSheuristics. Pass thedecisionIdfromauthorizeand thesanitizationaudit line joins the decision ondecision_id. - Content screening:
govern.screen(text, { resource, decisionId? })— flag or redact prompt-injection shapes in what a read returns, before it reaches the model; with thedecisionIdthescreeningaudit line joins the decision. - Attenuation & graduation:
govern.scope().attenuate();scope.toToken()/govern.scopeFromToken()carry an attenuated scope to a worker process (HMAC integrity; the receiving engine re-proves the subset); every decision returns adecisionIdto join to your records;WATCHLIGHT_APDP_URLgraduates the same code to the control plane.
Full API + runnable examples: ts/ · npm: @watchlight/sdk (glue,
Apache-2.0) + @watchlight/engine (the compiled engine). Docs:
docs.watchlight.ai/de/typescript.
Already using a framework? Govern it in-process
Bring your existing LangGraph, Pydantic AI, or Claude Agent SDK agent under governance with zero infrastructure — the same plugin you ship to production, wired to the in-process engine:
pip install 'watchlight[langgraph]' # or [pydantic-ai], [claude-agent]
from watchlight.langgraph import governed_plugin # .pydantic_ai / .claude_agent
plugin = governed_plugin("watchlight.policy.json") # in-process, zero infra
async with await plugin.start_run("research-agent") as handle:
if not await handle.authorize_action("read", "tool/web_search"):
raise PermissionError("denied before it executed")
... # your tool runs, every action governed + recorded to .watchlight/audit.jsonl
Going to production is one environment variable, not a rewrite — set
WATCHLIGHT_APDP_URL and the identical code authorizes against a running policy
service. Runnable examples for all three frameworks are in
examples/.
Watch every decision live — watchlight dev
A zero-dependency local dashboard that tails your value-free audit trail and shows every governance decision as it happens — the ALLOWs, and the DENYs that stopped a tool before it ran.
watchlight dev # → http://127.0.0.1:7000
Run your governed agent in another terminal and watch the decisions stream in. It shows only this process — fleet-wide lineage, signed audit, and drift→quarantine are the governed control plane (Enterprise).
On an ephemeral host, keep the trail: pass an audit_sink and every record —
decisions, sanitizations, attenuations — is also handed to your code with exactly
the fields the file line carries (the file stays on). The sink is fire-and-forget
and can never block or change a decision; a failure is reported once.
govern = Watchlight(agent="my-agent", audit_sink=lambda record: my_store.insert(record))
Reference sinks — a Postgres row, an OTLP log record, a webhook — are in
examples/patterns/audit-sink.md.
The trail is also an input: govern.counters(...) folds it into a number for a
quota policy — decisions for exactly this principal (and intent / resource) in
the last window, from the record timestamps — so context.reads_this_hour < 100
has something to compare against. Streams the local file (bounded, 64 MiB by
default); malformed lines are skipped and counted, never echoed.
c = govern.counters(principal='User::"u1"', intent="read", window="1h") # {"count": 7, "window": {...}, ...}
govern.authorize(action="read", principal='User::"u1"', context={"reads_this_hour": c["count"]})
The quotas pattern has the policy, the tool binding, and the exact counting rules.
Test your policies before they gate real actions
A policy is the only thing standing between an agent and a real action, so unit-test
it like any other code. Golden fixtures assert the expected verdict
(Allow / Deny / NeedsApproval) for a (principal, action, resource, context);
a wrong expectation fails the suite. Run it in CI.
from watchlight import govern
govern.load("watchlight.policy.json")
report = govern.test([
{"name": "under limit allows", "action": "book",
"context": {"amount": 200, "limit": 500, "refundable": True}, "expect": "Allow"},
{"name": "over limit denies", "action": "book",
"context": {"amount": 800, "limit": 500, "refundable": True}, "expect": "Deny"},
{"name": "big wire needs a human", "action": "wire",
"context": {"amount": 5000}, "expect": "NeedsApproval"},
])
assert report["failed"] == 0, report
govern.test(...) (Node: await govern.test([...])) drives the engine's decision
core directly, so it never writes the audit trail and holds zero decision logic —
every verdict is the engine's. Set "approved": true on a fixture to mint a
single-use token and assert the human-confirmed NeedsApproval → Allow downgrade;
set "obligations": {"redact": ["ssn"]} to also assert the obligations an Allow
carries (exact match; {} asserts none).
Or from CI, with the CLI — a suite.json of { policyFile?, policies?, tests: [...] },
exit 1 on any failure:
watchlight policy test suite.json # Python
npx watchlight policy test suite.json # Node
Patterns — advanced policies for high-stakes decisions
Past the quickstart, the interesting question is what to write in the policy.
The governance patterns library is a set of
copy-paste recipes for the decisions people reach for the Developer Edition to
govern — spending money, deleting things, messaging the outside world, moving
data, killing a runaway agent. Each is a problem shape: a policy, the code that
governs the tool, and tests that prove the verdicts. Every policy is run through
the real engine by check.sh, so what a pattern
claims and what the engine does can't drift.
The advanced policy JSON — each a runnable { policies, tests } suite with Cedar
context conditions and @enforcement_effect gates — lives under
examples/patterns/suites/:
| Pattern | The high-stakes question | Verified by |
|---|---|---|
| Money-bounded agent | Spend this much, on this, now — or does a human decide? | money-bounded-agent.suite.json |
| Destructive actions | Delete / drop / deploy: require a human; make some things undeletable. | destructive-actions.suite.json |
| External messaging | May the agent message outside — allowlisted destinations only, with review? | external-messaging.suite.json |
| Data egress | May this classification of data cross this boundary? | data-egress.suite.json |
| Egress after read | Govern what a tool returns — decide on the result's classification after the fetch. | egress-after-read.suite.json |
| Kill-switch / quarantine | Stop a suspect agent cold — a hard boundary that beats every grant. | kill-switch.suite.json |
| Per-user attribution | Attribute the decision to the acting end-user, and scope policy to them. | per-user-attribution.suite.json |
| PII before read | Strip PII from a document before the agent ever sees it; read only through the sanitizing path. | pii-before-read.suite.json + pii-before-read.mjs |
| Screen before model | Catch prompt-injection shapes in what a read returns before the model reads it. | screen-before-model.mjs |
| Sub-agent confinement | A spawned agent can only ever do less than its parent — never more. | subagent-confinement.mjs |
| Audit sink | Ship the value-free trail to a store you already run, without touching a decision. | audit-sink.mjs |
| Quotas | This many reads per hour, writes per day — a counter from the audit trail in context. |
quotas.suite.json |
Patterns whose guarantee is not a policy verdict — sanitization, content
screening, scope attenuation, the audit sink — are verified by a Node script under
examples/patterns/scripts/ instead. Run every
suite and script at once:
examples/patterns/check.sh # every pattern must have a suite or script; runs them all
Govern an MCP server
Put a policy enforcement point (PEP) in front of any
MCP server (spec 2026-07-28). The MCP PEP
authorizes every governed call — tools/call, resources/read,
resources/subscribe, prompts/get — in-process before it reaches the
server, so a denied call never executes.
pip install watchlight-mcp
import watchlight_mcp
watchlight_mcp.serve(
listen_addr="127.0.0.1:9700",
upstream_url="http://localhost:3000/mcp", # the MCP server you're governing
upstream_server="github",
policy_files=["examples/mcp.policy.json"],
audit_path=".watchlight/audit.jsonl", # ← the file `watchlight dev` tails
)
Point your MCP client at http://127.0.0.1:9700/mcp instead of the server. A
self-contained, self-demonstrating example (it fires an allowed and a denied
call and proves the denied one never ran) is in
examples/governed_mcp_server.py.
For a stdio-launched server use serve_stdio(...); to run non-blocking and
hot-reload policies use serve_background(...). Pass tls_cert=/tls_key= to
terminate HTTPS on the listener, and upstream_ca= to trust a private
https:// upstream. See the MCP server guide.
Watch the MCP decisions live
watchlight dev tails .watchlight/audit.jsonl — so give the PEP the same
audit_path (above) and run the console beside it, from the same directory:
# terminal 1 — the governed MCP server, auditing to the file the console tails
python examples/governed_mcp_server.py
# terminal 2 — the live console
watchlight dev # → http://127.0.0.1:7000
Every tools/call decision streams in — the tool, the upstream it fronts, and
the reason a call was denied. (The example prints the exact
watchlight dev --audit … command for its own audit file.)
What runs locally
| Capability | Developer Edition (free / open) | Enterprise |
|---|---|---|
| Policy engine | in-process Cedar, policies from a local .cedar file |
a running, scaled policy service |
| Sub-agent scope attenuation | engine-side strict-subset validation | same, server-side |
| Scope across processes | HMAC scope token — integrity within one trust domain, not attestation (a secret holder can mint any scope, root included); the receiving engine re-proves the subset | independently attestable scopes |
| Content screening | rule-based, in-process, value-free: govern.sanitize (structured PII) + govern.screen (prompt-injection / output-leak shapes); not ML classification |
a running guardrails service (ML classifiers, NER) |
| Audit | local JSONL, greppable, value-free | a signed, tamper-evident audit service |
| Dashboard | watchlight dev → localhost:7000 (policies + execution lineage) |
the full operator console |
Everything the Developer Edition removes is infrastructure, never a guarantee. Fail-closed semantics, engine-side attenuation, explicit scopes, and value-free audit are identical in every mode.
Open source, and the compiled engine
Everything you write against is open and Apache-2.0 — read it, audit it, fork it:
watchlight— thegoverndecorator,govern.scopeattenuation, the CLI, and thewatchlight devdashboard- the framework plugins —
watchlight-langgraph,watchlight-pydantic-ai,watchlight-claude-agent - the MCP PEP's transport layer, and every example in this repo
The decision engine ships as a compiled wheel — watchlight-engine (the Cedar authorization pipeline) and the watchlight-mcp runtime — both free to use, including in production and commercially — for up to 25 governed agents per organization (a commercial license is needed only above that, or to re-offer the engine itself as a hosted authorization service). The engine source is the part Watchlight sells; the code you integrate with is not.
You don't have to trust a black box to trust the decisions:
- The policy language is open. Decisions are standard Cedar — an open, formally-specified language; the same policy yields the same decision, deterministically.
- The integration layer is open. The SDK, plugins, CLI, and PEP transport are all readable here, so you can see exactly what the engine is asked and what it returns.
- Every decision is on disk. Each
ALLOW/DENYis appended, value-free, to.watchlight/audit.jsonl— inspect the engine's behaviour on your own machine, tool by tool.
Want the engine source or an air-gapped build? That's Enterprise — email sales@watchlight.ai.
A note on identity
The Developer Edition authorizes the principal you assert — the agent you construct the governor with, or the Watchlight-Agent-Id a governed MCP request carries. It does not cryptographically prove the caller: on your own machine, running both sides, that's the right trade — zero setup, no IdP, no signup. Bind any non-loopback listener behind something that authenticates the caller (a reverse proxy doing mTLS/OIDC, or the Enterprise plane).
Identity hardens as you grow, without changing your policies:
- Developer Edition — the principal is asserted (cooperative, local-dev).
- Next — an optional signed session token binds the principal to a key your process holds, so a prompt-injected sub-agent can't rewrite a header to escalate — still no external infrastructure.
- Enterprise — identity is attested: federated (OIDC) and workload (mTLS) identity, cryptographically verified across the fleet.
Only how strongly the principal is proven changes between these — the policies you write do not.
Developer Edition vs Enterprise
The Developer Edition is the real engine, free and in-process; Enterprise points the same code — no rewrite — at the governed control plane, adding signed tamper-evident lineage, multi-tenant isolation, drift→quarantine, and fleet-wide revocation across every agent and environment.
License
The Developer-Edition SDK, the framework plugins, this repository, and the
watchlight dev dashboard are Apache-2.0 — use, fork, and ship them freely.
The authorization engine (watchlight-engine) and the MCP runtime
(watchlight-mcp) ship as compiled wheels under the Watchlight Developer
Edition license; they are free to use, including in production and
commercially, for up to 25 governed agents per organization — a commercial
license is needed only above that, or to re-offer the engine itself as a hosted
authorization service.
Want the engine source, an air-gapped build, or to govern a fleet in production? That's the Enterprise plane — email sales@watchlight.ai.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file watchlight-0.7.1.tar.gz.
File metadata
- Download URL: watchlight-0.7.1.tar.gz
- Upload date:
- Size: 102.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a43f12810e0eb9a4a1923282197aab0767feec006316079188297ad3fd69166
|
|
| MD5 |
7da8a94b21acc1aee352373cdb9e1132
|
|
| BLAKE2b-256 |
5dcabc2b644a361392537b1a191cded4db17223dab3855adde17b243c853031b
|
Provenance
The following attestation bundles were made for watchlight-0.7.1.tar.gz:
Publisher:
publish.yml on watchlight-ai-beacon/watchlight-de
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
watchlight-0.7.1.tar.gz -
Subject digest:
0a43f12810e0eb9a4a1923282197aab0767feec006316079188297ad3fd69166 - Sigstore transparency entry: 2717381876
- Sigstore integration time:
-
Permalink:
watchlight-ai-beacon/watchlight-de@331ee47160ae8d055d05738cfd885ced8be62de3 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/watchlight-ai-beacon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@331ee47160ae8d055d05738cfd885ced8be62de3 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file watchlight-0.7.1-py3-none-any.whl.
File metadata
- Download URL: watchlight-0.7.1-py3-none-any.whl
- Upload date:
- Size: 74.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a72c780985a7e90329b2d4ffa0960abcd18670fab33c2aecacb212a648905d5
|
|
| MD5 |
ed0939a5a8c7b13cc7b8b264a1aede6c
|
|
| BLAKE2b-256 |
5eec360f239db06788599646fe6675555bd045b0a702e92fbf765b5fe7cc7472
|
Provenance
The following attestation bundles were made for watchlight-0.7.1-py3-none-any.whl:
Publisher:
publish.yml on watchlight-ai-beacon/watchlight-de
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
watchlight-0.7.1-py3-none-any.whl -
Subject digest:
5a72c780985a7e90329b2d4ffa0960abcd18670fab33c2aecacb212a648905d5 - Sigstore transparency entry: 2717383736
- Sigstore integration time:
-
Permalink:
watchlight-ai-beacon/watchlight-de@331ee47160ae8d055d05738cfd885ced8be62de3 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/watchlight-ai-beacon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@331ee47160ae8d055d05738cfd885ced8be62de3 -
Trigger Event:
workflow_dispatch
-
Statement type: