Skip to main content

Govern an agent you already have

You do not have to write your agent around Watchlight. A plugin governs a framework agent: it authorizes before the call executes, and it does not change how the agent is built.

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 recorded to .watchlight/audit.jsonl

This is the same plugin you ship to production. Going there is one environment variable — set WATCHLIGHT_APDP_URL and the identical code authorizes against a running policy service.

The backend comes from the environment​

governed_plugin chooses the backend itself: the networked policy service when WATCHLIGHT_APDP_URL is set, the in-process engine when it is not. A call site cannot replace that choice. Since 0.13.0, passing governance= or apdp_url= raises TypeError naming the keyword, before anything is built:

plugin = governed_plugin("watchlight.policy.json")   # WATCHLIGHT_APDP_URL set → networked; unset → in-process

To run a plugin against a backend object of your own, construct the framework plugin directly rather than through governed_plugin. A plugin built this way uses the governance= object you pass and ignores WATCHLIGHT_APDP_URL:

ModuleConstruct instead
watchlight.langgraphwatchlight_langgraph.WatchlightLangGraphPlugin(governance=...)
watchlight.pydantic_aiwatchlight_pydantic_ai.WatchlightPydanticAIPlugin(governance=...)
watchlight.claude_agentwatchlight_claude_agent.WatchlightClaudeAgentSDKPlugin(governance=...)

If the extra is not installed, the ImportError says which one to install (langgraph, pydantic-ai or claude-agent). Any other error raised while the plugin imports surfaces as itself.

Runnable agents for all three frameworks are in examples/.

Framework-created sub-agents inherit framework tools​

Some frameworks auto-register a general-purpose sub-agent with the parent's tool list. Watchlight cannot narrow a scope the framework never asks it to authorize. In deepagents, pass an explicit same-named spec as subagents to create_deep_agent(...) so the default is not added:

subagents=[{
"name": "general-purpose",
"description": "General-purpose agent without parent custom tools.",
"system_prompt": "Complete the delegated task.",
"tools": [],
}]

An empty tools list does not remove tools added by the framework's middleware, including deepagents' filesystem tools. Configure those separately when the sub-agent needs tighter confinement.

Per-call terms go on the run handle​

governed_plugin() is constructor wiring. The terms of a single call belong to the handle:

await handle.authorize_action(
"read_ticket", "tool/read_ticket",
principal=f'User::"{user_id}"', # who the call is for
context={"caller": caller, "owner": owner}, # what a policy reads
)
permit(principal, action == Action::"read_ticket", resource)
when { context has owner && context has caller && context.caller == context.owner };

principal defaults to the agent that runs, so existing calls are unchanged. Write has before the comparison: a rule that reads a key nothing supplied falls through to Deny.

Pass principal=, context= or resource= to governed_plugin() and it refuses them by name rather than dropping them silently — a term you believe is reaching the decision, and is not, is a denial that explains nothing.

Name the entity type​

User::"u-1" is not matched by a policy naming Agent::"u-1". A bare u-1 is not inert: it matches that id under User, Agent, Group or Role, and when it matches more than one, an allow beats a forbid — the opposite of Cedar's usual rule. Always write the type. The action is always type Action; a policy that gives it another type is rejected.

A plugin run that names no principal is attributed to the agent it runs as, which is a generated Agent::"<uuid>". Gate those policies on the action, the resource, or principal is Agent — never on a principal id you cannot know in advance.

Confine a sub-agent​

When a governed agent spawns children, hand each one a strict subset of its own authority. That is scope attenuation, and the engine enforces it in-process.

Worth knowing​

  • The plugin's own tenant_id is authoritative and overrides one passed on a call.
  • Every action lands in .watchlight/audit.jsonl. Run watchlight dev beside the agent to watch the decisions and the attenuation tree.
  • All three integrations are built on one contract inside the package, so they govern identically. That contract, watchlight.integrations, is internal and may change in any release; import watchlight.<framework>.governed_plugin, which is stable. To add a framework, see Adding a framework integration in the contributing guide.
  • For a policy that must name the person a call is made for, govern that call with govern.tool(..., principal=...) or govern.authorize(principal=...) alongside the plugin. See using the governor.

Next​