Skip to main content

Watchlight

Watchlight — Developer Edition

Govern an AI agent in five minutes. One install, zero infrastructure, same API as production.

PyPI Python 3.9+ npm Node 18+ License: Apache-2.0 Docs

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 → your sink and/or .watchlight/audit.jsonl
                                 ▼
        watchlight dev  ·  http://localhost:7000   (reads the file)

  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 is watchlight_core (there is no watchlight-core package on PyPI).

Runnable, self-contained examples for every one of these are in examples/ — start with examples/governed_research_agent.py.


Quickstart

The govern decorator, 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.


Who is acting, and on whose behalf

A governed call answers these questions, and they are separate inputs:

Question Where it goes Example
On whose behalf does this run? principal — the subject User::"db:4412"
Which runtime is acting? the reserved actor context key, set by the SDK context.actor == "flight-booker"
Through whose delegation? the reserved actor_chain context key context.actor_chain.contains("flight-booker")
Under what narrowed authority? the attenuation scope govern.scope(tools=[...])
from watchlight import govern, principals

# the agent acting for a person
govern.authorize(action="book", principal=principals.user("db:4412"))
# the agent acting on its own behalf — an omitted principal is Agent::"<name>"
govern.authorize(action="cache")
// this runtime may book for any user — whoever it acts for
permit(principal is User, action == Action::"book", resource)
when { context.actor == "flight-booker" };

principal is always a typed entity reference; build it with principals.user / principals.agent, which escape an identifier that came from outside. The SDK sets context.actor on every call from the governor's agent name, and refuses a caller-supplied value that disagrees, so a policy can trust it.

One engine per policy set, many named agents. Construct once (with the sink and the secrets), load the policies once, then name each agent with as: it returns another Watchlight with a different name, backed by the same engine — the same compiled policies and their load memo, the same audit trail, sink and secrets, and only the stamped name differs. Construct a second governor for a genuinely different policy set — not to give an agent a name.

billing = govern.as_("billing-agent")     # no second engine, no second policy load
research = govern.as_("research-agent")

Renamed governors share the trail, so every named agent's records land in one destination, told apart by the agent field — which is what makes a single audit stream readable. Separate governors are how you get a separate trail per agent.

A sub-agent is a delegation, not a rename: delegate narrows a scope for it (engine-enforced strict subset) and extends the actor chain, so the decision and every record name both the sub-agent and whose delegation it acts under.

root = govern.scope(tools=["search", "book"])
picker = govern.delegate(root, "seat-picker", tools=["search"])
picker.authorize(action="pick_seat", principal=principals.user("db:4412"))
# records agent "seat-picker", actor_chain ["flight-booker", "seat-picker"]

The subject is a stable identifier for whoever your application already authenticated — a users-table primary key is as valid as a token's subject claim, and no identity provider is required. Derive it from something you authenticated, never from a request header or body a caller can set, and prefer an id that never moves over an email or a username.

Using the governor — where the governor lives in an application: construct it once at start-up, how many you need, what every name shares, and the request-handler / worker / test shapes.

→ Full reference: The identity model — the one-engine shape, the three cases with exact values, worked policies, where the values come from, and the 0.8.0 migration note.

Glossary — governor, subject, actor and chain, then every other term this documentation uses, with the easy-to-confuse pairs contrasted side by side.


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 Cedar context.*, per-call principal, and a three-state Allow / Deny / NeedsApproval verdict with a single-use approval token. Approval tokens are signed with a random per-process key and recorded as used in an in-process map unless you configure otherwise — so by default a token cannot cross a process boundary, a restart invalidates outstanding approvals, and behind two replicas the same token can be consumed once on each. approvalSecret / approval_secret (or WATCHLIGHT_APPROVAL_SECRET, or the existing signingSecret, which covers both kinds of token — see the signing secret) makes a token portable; an approvalStore / approval_store (one method: add(id, expiresAt)) backed by a shared store makes single-use hold across replicas. add must be an atomic check-and-set — reserve the id only if absent, and say whether the reservation was new; a read followed by an unconditional write cannot enforce single use, because concurrent consumes of one token would all pass through the gap. The built-in default is atomic, so within one process N parallel consumes of one token yield exactly one Allow. A store that fails, times out, or will not report refuses the approval — it never admits one. Breaking in 0.8.0: the signed payload is now length-prefixed and versioned, so no two different (principal, action, resource) triples can sign the same bytes — approval tokens minted by an earlier version do not verify against 0.8.0. The tokens are short-lived, so drain in-flight approvals across the upgrade.
  • Govern what a tool returns: onResult(result, { intent, resource, principal, decisionId, obligations? }) (Python on_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-free egress audit record joined to the decision by decision_id.
  • Obligations on an Allow: a permit annotated @obligate_redact("ssn"), @obligate_max_items("25"), @obligate_log_values("false") (or any @obligate_<name>("raw")) yields d.obligations{ redact, maxItems, logValues, extra } (Python result["obligations"]: redact / max_items / log_values / extra, the last as {name: [values]}) — constraints your code or onResult must honour. Several carriers merge to the strictest reading; only an Allow carries them; Deny and NeedsApproval never 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() / governTools() for LangChain / LangGraph.js. Each takes the same governance terms as govern.tool()principal, agent, resource (resourceFor on the mapping forms), context, onNeedsApproval, onResult — so a policy that reads Cedar context.* reaches the same verdict through an adapter as it does through a hand-written governed tool, and the record names the person the call was made for. Each is a fixed value or a function of the call. Pass none and the defaults are unchanged: the agent is the subject, the resource is tool/<name>, the context is empty. See the context-through-an-adapter pattern.
  • Data minimization: govern.sanitize(text, { resource, decisionId, principal?, 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-supplied known dictionary (KNOWN; simple case-insensitive match — Unicode case folding differs between lanes), and opt-in PERSON / ADDRESS heuristics. Pass the decisionId from authorize and the sanitization audit line joins the decision on decision_id; pass principal (Python principal=) and the line names whose data was redacted, under the same key the decision line uses. Omit it and the line still names a subject — this agent, typed as Agent::"<name>", exactly as a decision that names no principal is recorded — so naming the person is what turns "redacted for the agent" into an answer a data-minimisation audit can use, and it is the only way to get one when the sanitization runs before any decision exists to join to.
  • Content screening: govern.screen(text, { resource, decisionId?, principal? }) — flag or redact prompt-injection shapes in what a read returns, before it reaches the model; with the decisionId the screening audit line joins the decision, and principal names whom it was screened for. Both fields are identifiers you supply — never anything derived from the content — and carry the same validation (1–128 characters, no control or line-separator characters).
  • 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 a decisionId to join to your records; WATCHLIGHT_APDP_URL graduates 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/.

What a plugin can and cannot express

governed_plugin() is constructor wiring: it builds the published plugin and hands it the in-process engine. The governance terms of a single call belong to the plugin's own run handle, not to the factory.

  • Cedar context and the resource: yes, per call. await handle.authorize_action("read_ticket", "tool/read_ticket", context={"caller": caller, "owner": owner}) — a policy that reads context.* is satisfiable this way. (tenant_id is the plugin's own and wins over a value passed here.)
  • An acting subject: no. A plugin attributes every decision to the agent it runs — Agent::"<agent>" — and there is no per-call principal. For a policy that must name the person or tenant a call is made for, govern that call with Watchlight.tool(..., principal=...) (or authorize(principal=...)), alongside the plugin or instead of it.

governed_plugin() refuses principal=, context= and resource= by name rather than forwarding them into a constructor that would drop them — a term you believe is reaching the decision, and is not, is a policy that never matches.


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.

audit_file=False makes the sink the sole destination: no .watchlight directory, no file, and govern.counters(...) — which reads the local file — raises rather than counting zero. watchlight dev tails that same file, so it has nothing to show once the file is off; read your sink's store instead. With neither a file nor a sink the SDK says so once instead of discarding records silently. Note that the file is shared: every governor pointed at the same directory, including concurrent instances in one process and a test run in the same working directory, appends to the same audit.jsonl, so those records interleave and are told apart only by their fields.

The module-level govern is pre-constructed, so configure it before its first governed call — otherwise it has no sink, and it says so the first time it writes:

from watchlight import govern, configure_default

configure_default(agent="billing-agent", audit_sink=my_store.insert)

configure_default(...) raises once the default governor has written a record: records already written cannot reach a sink added later, and a trail split across two destinations reads like a data bug.

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"]})

By default that count comes from the local file, which is per-container and does not survive a deploy. counter_source / counterSource is the read side of the sink: the same query, answered by the durable store the sink writes to, so the quota spans every replica.

govern = Watchlight(
    agent="my-agent",
    audit_sink=lambda record: my_store.insert(record),
    counter_source=lambda query: my_store.count_decisions(query),
)
c = govern.counters(principal='User::"u1"', intent="read", window="1h")   # c["source"] == "external"

The source is handed the validated, resolved query — principal, outcome, a window whose start is exclusive and end inclusive, plus intent / resource when the caller filtered on them — and must return a non-negative integer. It has to count decision rows only, exactly as the local scan does: the trail also carries sanitization, screening, egress and attenuation records, so a query filtered on principal and window alone over-counts and the quota denies early. Fail-closed: it never falls back to the local file, so a quota can never quietly under-count.

An async source — a durable store is a network call — is read with counters_async(...) / countersAsync(...), and a context binding may itself be async: it is awaited before the decision, so the durable count is what the policy evaluates and the quota works through a governed tool.

async def quota(o):
    c = await govern.counters_async(principal=user(o), intent="read", window="1h")
    return {} if c["truncated"] else {"reads_this_hour": c["count"]}

# An async binding needs an `async def` body: the decision is made before the
# body runs, so a synchronous tool cannot await one and raises `TypeError`.
@govern.tool("read", principal=user, context=quota)
async def fetch_document(o): ...

A synchronous binding reads the local file or a synchronous source; an async source needs the async binding — calling the synchronous counters() on one raises, naming counters_async, rather than answering from the file.

The async form is a property of the tool binding, not of authorize, which takes a context you have already resolved. Handing authorize an unresolved awaitable raises UnresolvedContextError before anything reaches the engine, and records no decision — the attributes it was going to carry are not there yet, and a policy evaluated without them denies for a reason that has nothing to do with the caller.

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.

govern.load(path) is idempotent per source: the real path (symlinks resolved) or an explicit source_id= is remembered, so priming an engine in a factory and loading the same file again from an initialiser cannot double the set. A missing file is not remembered, so it loads once it appears. The memo is keyed on identity, not content — editing a loaded file and calling load again is a no-op; pass force=True to load it again (additively). govern.allow(code) is always additive — the same code twice is two policies. govern.policy_count and govern.has_policies report what an engine holds, which is worth asserting at start-up: no policies means every call is denied.

from watchlight import govern

govern.load("watchlight.policy.json")
assert govern.has_policies, "no policies loaded — every call would be denied"
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
Allow, but redact Say "yes, but…" in the policy@obligate_redact / @obligate_max_items / @obligate_log_values ride on the Allow, and the suite asserts them. allow-but-redact.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 devlocalhost: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 — the govern decorator, govern.scope attenuation, the CLI, and the watchlight dev dashboard
  • 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 wheelwatchlight-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 yours to read. Each ALLOW/DENY is emitted value-free to whatever destination you configure — by default .watchlight/audit.jsonl, so you can inspect the engine's behaviour on your own machine, tool by tool, with nothing set up.

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.

What the principal contains — the subject, the acting runtime, and how a policy names each — is The identity model.


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.

watchlight.ai


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

watchlight-0.8.1.tar.gz (171.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

watchlight-0.8.1-py3-none-any.whl (106.3 kB view details)

Uploaded Python 3

File details

Details for the file watchlight-0.8.1.tar.gz.

File metadata

  • Download URL: watchlight-0.8.1.tar.gz
  • Upload date:
  • Size: 171.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for watchlight-0.8.1.tar.gz
Algorithm Hash digest
SHA256 61346bd45b80fabd69d5ab7556b5eaddc4088190933cb0ec3cc1cc65d520c402
MD5 937de119818f4e04902acbded63ab978
BLAKE2b-256 25b2730812ee5b50f122b7bd0bf02b7d4062cec2f3c443bb4a9aee6c77638181

See more details on using hashes here.

Provenance

The following attestation bundles were made for watchlight-0.8.1.tar.gz:

Publisher: publish.yml on watchlight-ai-beacon/watchlight-de

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file watchlight-0.8.1-py3-none-any.whl.

File metadata

  • Download URL: watchlight-0.8.1-py3-none-any.whl
  • Upload date:
  • Size: 106.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for watchlight-0.8.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3c3b85916ae52d472be7777502f77bfae3bb8f046ca33377df636898e11532e6
MD5 d42d74ea8b31d7a84c887106b1d40c2f
BLAKE2b-256 972388e0bf9f36264aab350914971ac234bd2f8361e5f06e786170113bd41545

See more details on using hashes here.

Provenance

The following attestation bundles were made for watchlight-0.8.1-py3-none-any.whl:

Publisher: publish.yml on watchlight-ai-beacon/watchlight-de

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.11.0

2 files

0.10.0

2 files

0.9.1

2 files

0.9.0

2 files

0.8.2

2 files

This release

0.8.1 This release

2 files

0.8.0

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page