watchlight 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ Metadata-Version: 2.4
2
+ Name: watchlight
3
+ Version: 0.1.0
4
+ Summary: Watchlight Developer Edition — govern AI agents in-process, with zero infrastructure.
5
+ Author: Watchlight AI
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://watchlight.ai
8
+ Project-URL: Documentation, https://docs.watchlight.ai/de
9
+ Keywords: authorization,ai-agents,cedar,policy,governance
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: watchlight-engine<0.2,>=0.1
13
+ Provides-Extra: langgraph
14
+ Requires-Dist: watchlight-langgraph>=0.4; extra == "langgraph"
15
+ Provides-Extra: pydantic-ai
16
+ Requires-Dist: watchlight-pydantic-ai>=0.3; extra == "pydantic-ai"
17
+ Provides-Extra: claude-agent
18
+ Requires-Dist: watchlight-claude-agent>=0.2; extra == "claude-agent"
19
+ Provides-Extra: mcp
20
+ Requires-Dist: watchlight-mcp>=0.1; extra == "mcp"
21
+
22
+ # Watchlight — Developer Edition
23
+
24
+ **Govern an AI agent in five minutes. One install, zero infrastructure, same API as production.**
25
+
26
+ Watchlight puts a policy decision point in front of every action your AI agents
27
+ take — authorizing tool calls, attenuating sub-agent authority to a strict
28
+ subset, and recording a tamper-evident, value-free audit trail. The Developer
29
+ Edition runs that *entire* authorization model **in-process**, so you can see it
30
+ work in your own terminal with no server, no database, and no signup.
31
+
32
+ The code you write here is the code you ship to production. Going to production
33
+ is pointing at a running policy service — not a rewrite.
34
+
35
+ ---
36
+
37
+ ## Quickstart
38
+
39
+ > **Status: in active development.** The target experience is below; see the
40
+ > [Developer Edition docs](https://docs.watchlight.ai/de) for the current state.
41
+
42
+ ```bash
43
+ pip install watchlight
44
+ ```
45
+
46
+ ```python
47
+ from watchlight import govern
48
+
49
+ @govern.tool(intent="research")
50
+ def web_search(query: str) -> str:
51
+ ...
52
+
53
+ @govern.tool(intent="transfer") # governed, but no policy permits it
54
+ def transfer_funds(to: str, amount: int) -> str:
55
+ ...
56
+ ```
57
+
58
+ ```text
59
+ $ python agent.py
60
+ watchlight: governing 'my-agent' (dev mode, in-process engine)
61
+ watchlight: ALLOW read tool/web_search
62
+ watchlight: DENY execute tool/transfer_funds no matching policy
63
+ ```
64
+
65
+ **That `DENY` line — in your own terminal, in under five minutes, with no
66
+ account — is the product.**
67
+
68
+ ---
69
+
70
+ ## Already using a framework? Govern it in-process
71
+
72
+ Bring your existing **LangGraph**, **Pydantic AI**, or **Claude Agent SDK**
73
+ agent under governance with zero infrastructure — the *same* plugin you ship to
74
+ production, wired to the in-process engine:
75
+
76
+ ```bash
77
+ pip install 'watchlight[langgraph]' # or [pydantic-ai], [claude-agent]
78
+ ```
79
+
80
+ ```python
81
+ from watchlight.langgraph import governed_plugin # .pydantic_ai / .claude_agent
82
+
83
+ plugin = governed_plugin("watchlight.policy.json") # in-process, zero infra
84
+
85
+ async with await plugin.start_run("research-agent") as handle:
86
+ if not await handle.authorize_action("read", "tool/web_search"):
87
+ raise PermissionError("denied before it executed")
88
+ ... # your tool runs, every action governed + recorded to .watchlight/audit.jsonl
89
+ ```
90
+
91
+ Going to production is one environment variable, not a rewrite — set
92
+ `WATCHLIGHT_APDP_URL` and the identical code authorizes against a running policy
93
+ service. Runnable examples for all three frameworks are in
94
+ [`examples/`](examples/).
95
+
96
+ ---
97
+
98
+ ## Watch every decision live — `watchlight dev`
99
+
100
+ A zero-dependency local dashboard that tails your value-free audit trail and
101
+ shows every governance decision as it happens — the ALLOWs, and the DENYs that
102
+ stopped a tool **before** it ran.
103
+
104
+ ```bash
105
+ watchlight dev # → http://127.0.0.1:7000
106
+ ```
107
+
108
+ Run your governed agent in another terminal and watch the decisions stream in.
109
+ It shows only *this* process — fleet-wide lineage, signed audit, and
110
+ drift→quarantine are the governed control plane (Enterprise).
111
+
112
+ ---
113
+
114
+ ## Govern an MCP server
115
+
116
+ Put a policy decision point in front of any [MCP](https://modelcontextprotocol.io)
117
+ server (spec `2026-07-28`). Every `tools/call` is authorized in-process **before**
118
+ it reaches the server — a denied call never executes.
119
+
120
+ ```bash
121
+ pip install watchlight-mcp
122
+ ```
123
+
124
+ ```python
125
+ import watchlight_mcp
126
+
127
+ watchlight_mcp.serve(
128
+ listen_addr="127.0.0.1:9700",
129
+ upstream_url="http://localhost:3000/mcp", # the MCP server you're governing
130
+ upstream_server="github",
131
+ policy_files=["examples/mcp.policy.json"],
132
+ )
133
+ ```
134
+
135
+ Point your MCP client at `http://127.0.0.1:9700/mcp` instead of the server. A
136
+ self-contained, self-demonstrating example (it fires an allowed and a denied
137
+ call and proves the denied one never ran) is in
138
+ [`examples/governed_mcp_server.py`](examples/governed_mcp_server.py).
139
+
140
+ ---
141
+
142
+ ## What runs locally
143
+
144
+ | Capability | Developer Edition (free / open) | Enterprise |
145
+ |---|---|---|
146
+ | Policy engine | in-process Cedar, policies from a local `.cedar` file | a running, scaled policy service |
147
+ | Sub-agent scope attenuation | engine-side strict-subset validation | same, server-side |
148
+ | Content / PII screening | policy-based, in-process | a running guardrails service |
149
+ | Audit | local JSONL, greppable, value-free | a signed, tamper-evident audit service |
150
+ | Dashboard | `watchlight dev` → `localhost:7000` (policies + execution lineage) | the full operator console |
151
+
152
+ Everything the Developer Edition removes is **infrastructure**, never a
153
+ **guarantee**. Fail-closed semantics, engine-side attenuation, explicit scopes,
154
+ and value-free audit are identical in every mode.
155
+
156
+ ---
157
+
158
+ ## Progressive disclosure
159
+
160
+ Each level is one environment variable away from the next. **Nothing is
161
+ rewritten between levels.**
162
+
163
+ - **Level 0** — `pip install watchlight`. In-process engine, audit to stdout.
164
+ - **Level 1** — `watchlight dev`. Adds a local dashboard (decisions, denials, scope tree, execution lineage).
165
+ - **Level 2** — `docker compose up`. Real policy service + database; policies still from your local file.
166
+ - **Level 3** — Production. The full governed platform.
167
+
168
+ ---
169
+
170
+ ## Developer Edition vs Enterprise
171
+
172
+ The Developer Edition is the **real engine** — free, open, and running
173
+ in-process so you can evaluate the entire authorization model on your laptop
174
+ with zero infrastructure. Enterprise is the **same code** pointed at the
175
+ governed control plane; it doesn't replace anything, it adds what a fleet in
176
+ production needs:
177
+
178
+ - **Signed, tamper-evident lineage & audit** — every decision and lineage event
179
+ cryptographically signed (KMS-backed), so the trail is court-defensible.
180
+ - **Multi-tenant isolation + roll-up administration** — tenant hierarchy, scoped
181
+ admins, and a cross-tenant authorization matrix.
182
+ - **Drift & anomaly detection → automatic quarantine** — behavioural,
183
+ goal-drift, and argument-shape detectors that quarantine a misbehaving agent
184
+ *before* the next action.
185
+ - **Full enforcement-effect taxonomy** — beyond allow/deny: block, terminate,
186
+ quarantine, sever-subtree, and revoke, enforced at runtime across the plane.
187
+ - **Fleet-wide revocation & cross-environment governance** — revoke authority
188
+ across every agent at once, and govern dev, staging, and prod under one
189
+ authority model (including **sovereign / air-gapped deployment**).
190
+ - **Content / PII guardrails service** and **global execution-graph lineage**
191
+ with the full **operator console**.
192
+ - **SSO / RBAC / enterprise audit**, high availability, support, and SLAs.
193
+
194
+ ### You've outgrown the Developer Edition when…
195
+
196
+ - Compliance asks *"prove who authorized this in production"* → you need
197
+ **signed, tamper-evident lineage**.
198
+ - You're governing **more than one agent, or more than one environment** →
199
+ central policy lifecycle + the **global execution graph**.
200
+ - Security wants a misbehaving agent **stopped before its next action** →
201
+ **drift/anomaly detection → automatic quarantine**.
202
+ - You need to **revoke authority fleet-wide**, not process-by-process.
203
+ - Procurement needs **SSO, RBAC, HA, SLAs, or sovereign/air-gapped deployment**.
204
+
205
+ Each of these is a governance guarantee a single in-process engine structurally
206
+ cannot provide — it needs the control plane.
207
+
208
+ **Migrating is one environment variable — never a rewrite.** The tools you
209
+ decorate, the policies you write, and the guarantees you rely on
210
+ (fail-closed, engine-side attenuation, explicit scopes, value-free audit) are
211
+ identical in every mode. Enterprise simply points the same code at a running
212
+ plane.
213
+
214
+ > The engine ships as a compiled wheel and the Developer Edition is a deliberate
215
+ > *subset* of the platform — the governed control plane (signing, multi-tenant,
216
+ > guardrails, drift, execution-graph) is the enterprise product, never bundled
217
+ > here.
218
+
219
+ → **[Talk to us about Enterprise](mailto:enterprise@watchlight.ai)** when you're
220
+ ready for production.
221
+
222
+ ---
223
+
224
+ ## License
225
+
226
+ Apache-2.0.
@@ -0,0 +1,205 @@
1
+ # Watchlight — Developer Edition
2
+
3
+ **Govern an AI agent in five minutes. One install, zero infrastructure, same API as production.**
4
+
5
+ Watchlight puts a policy decision point in front of every action your AI agents
6
+ take — authorizing tool calls, attenuating sub-agent authority to a strict
7
+ subset, and recording a tamper-evident, value-free audit trail. The Developer
8
+ Edition runs that *entire* authorization model **in-process**, so you can see it
9
+ work in your own terminal with no server, no database, and no signup.
10
+
11
+ The code you write here is the code you ship to production. Going to production
12
+ is pointing at a running policy service — not a rewrite.
13
+
14
+ ---
15
+
16
+ ## Quickstart
17
+
18
+ > **Status: in active development.** The target experience is below; see the
19
+ > [Developer Edition docs](https://docs.watchlight.ai/de) for the current state.
20
+
21
+ ```bash
22
+ pip install watchlight
23
+ ```
24
+
25
+ ```python
26
+ from watchlight import govern
27
+
28
+ @govern.tool(intent="research")
29
+ def web_search(query: str) -> str:
30
+ ...
31
+
32
+ @govern.tool(intent="transfer") # governed, but no policy permits it
33
+ def transfer_funds(to: str, amount: int) -> str:
34
+ ...
35
+ ```
36
+
37
+ ```text
38
+ $ python agent.py
39
+ watchlight: governing 'my-agent' (dev mode, in-process engine)
40
+ watchlight: ALLOW read tool/web_search
41
+ watchlight: DENY execute tool/transfer_funds no matching policy
42
+ ```
43
+
44
+ **That `DENY` line — in your own terminal, in under five minutes, with no
45
+ account — is the product.**
46
+
47
+ ---
48
+
49
+ ## Already using a framework? Govern it in-process
50
+
51
+ Bring your existing **LangGraph**, **Pydantic AI**, or **Claude Agent SDK**
52
+ agent under governance with zero infrastructure — the *same* plugin you ship to
53
+ production, wired to the in-process engine:
54
+
55
+ ```bash
56
+ pip install 'watchlight[langgraph]' # or [pydantic-ai], [claude-agent]
57
+ ```
58
+
59
+ ```python
60
+ from watchlight.langgraph import governed_plugin # .pydantic_ai / .claude_agent
61
+
62
+ plugin = governed_plugin("watchlight.policy.json") # in-process, zero infra
63
+
64
+ async with await plugin.start_run("research-agent") as handle:
65
+ if not await handle.authorize_action("read", "tool/web_search"):
66
+ raise PermissionError("denied before it executed")
67
+ ... # your tool runs, every action governed + recorded to .watchlight/audit.jsonl
68
+ ```
69
+
70
+ Going to production is one environment variable, not a rewrite — set
71
+ `WATCHLIGHT_APDP_URL` and the identical code authorizes against a running policy
72
+ service. Runnable examples for all three frameworks are in
73
+ [`examples/`](examples/).
74
+
75
+ ---
76
+
77
+ ## Watch every decision live — `watchlight dev`
78
+
79
+ A zero-dependency local dashboard that tails your value-free audit trail and
80
+ shows every governance decision as it happens — the ALLOWs, and the DENYs that
81
+ stopped a tool **before** it ran.
82
+
83
+ ```bash
84
+ watchlight dev # → http://127.0.0.1:7000
85
+ ```
86
+
87
+ Run your governed agent in another terminal and watch the decisions stream in.
88
+ It shows only *this* process — fleet-wide lineage, signed audit, and
89
+ drift→quarantine are the governed control plane (Enterprise).
90
+
91
+ ---
92
+
93
+ ## Govern an MCP server
94
+
95
+ Put a policy decision point in front of any [MCP](https://modelcontextprotocol.io)
96
+ server (spec `2026-07-28`). Every `tools/call` is authorized in-process **before**
97
+ it reaches the server — a denied call never executes.
98
+
99
+ ```bash
100
+ pip install watchlight-mcp
101
+ ```
102
+
103
+ ```python
104
+ import watchlight_mcp
105
+
106
+ watchlight_mcp.serve(
107
+ listen_addr="127.0.0.1:9700",
108
+ upstream_url="http://localhost:3000/mcp", # the MCP server you're governing
109
+ upstream_server="github",
110
+ policy_files=["examples/mcp.policy.json"],
111
+ )
112
+ ```
113
+
114
+ Point your MCP client at `http://127.0.0.1:9700/mcp` instead of the server. A
115
+ self-contained, self-demonstrating example (it fires an allowed and a denied
116
+ call and proves the denied one never ran) is in
117
+ [`examples/governed_mcp_server.py`](examples/governed_mcp_server.py).
118
+
119
+ ---
120
+
121
+ ## What runs locally
122
+
123
+ | Capability | Developer Edition (free / open) | Enterprise |
124
+ |---|---|---|
125
+ | Policy engine | in-process Cedar, policies from a local `.cedar` file | a running, scaled policy service |
126
+ | Sub-agent scope attenuation | engine-side strict-subset validation | same, server-side |
127
+ | Content / PII screening | policy-based, in-process | a running guardrails service |
128
+ | Audit | local JSONL, greppable, value-free | a signed, tamper-evident audit service |
129
+ | Dashboard | `watchlight dev` → `localhost:7000` (policies + execution lineage) | the full operator console |
130
+
131
+ Everything the Developer Edition removes is **infrastructure**, never a
132
+ **guarantee**. Fail-closed semantics, engine-side attenuation, explicit scopes,
133
+ and value-free audit are identical in every mode.
134
+
135
+ ---
136
+
137
+ ## Progressive disclosure
138
+
139
+ Each level is one environment variable away from the next. **Nothing is
140
+ rewritten between levels.**
141
+
142
+ - **Level 0** — `pip install watchlight`. In-process engine, audit to stdout.
143
+ - **Level 1** — `watchlight dev`. Adds a local dashboard (decisions, denials, scope tree, execution lineage).
144
+ - **Level 2** — `docker compose up`. Real policy service + database; policies still from your local file.
145
+ - **Level 3** — Production. The full governed platform.
146
+
147
+ ---
148
+
149
+ ## Developer Edition vs Enterprise
150
+
151
+ The Developer Edition is the **real engine** — free, open, and running
152
+ in-process so you can evaluate the entire authorization model on your laptop
153
+ with zero infrastructure. Enterprise is the **same code** pointed at the
154
+ governed control plane; it doesn't replace anything, it adds what a fleet in
155
+ production needs:
156
+
157
+ - **Signed, tamper-evident lineage & audit** — every decision and lineage event
158
+ cryptographically signed (KMS-backed), so the trail is court-defensible.
159
+ - **Multi-tenant isolation + roll-up administration** — tenant hierarchy, scoped
160
+ admins, and a cross-tenant authorization matrix.
161
+ - **Drift & anomaly detection → automatic quarantine** — behavioural,
162
+ goal-drift, and argument-shape detectors that quarantine a misbehaving agent
163
+ *before* the next action.
164
+ - **Full enforcement-effect taxonomy** — beyond allow/deny: block, terminate,
165
+ quarantine, sever-subtree, and revoke, enforced at runtime across the plane.
166
+ - **Fleet-wide revocation & cross-environment governance** — revoke authority
167
+ across every agent at once, and govern dev, staging, and prod under one
168
+ authority model (including **sovereign / air-gapped deployment**).
169
+ - **Content / PII guardrails service** and **global execution-graph lineage**
170
+ with the full **operator console**.
171
+ - **SSO / RBAC / enterprise audit**, high availability, support, and SLAs.
172
+
173
+ ### You've outgrown the Developer Edition when…
174
+
175
+ - Compliance asks *"prove who authorized this in production"* → you need
176
+ **signed, tamper-evident lineage**.
177
+ - You're governing **more than one agent, or more than one environment** →
178
+ central policy lifecycle + the **global execution graph**.
179
+ - Security wants a misbehaving agent **stopped before its next action** →
180
+ **drift/anomaly detection → automatic quarantine**.
181
+ - You need to **revoke authority fleet-wide**, not process-by-process.
182
+ - Procurement needs **SSO, RBAC, HA, SLAs, or sovereign/air-gapped deployment**.
183
+
184
+ Each of these is a governance guarantee a single in-process engine structurally
185
+ cannot provide — it needs the control plane.
186
+
187
+ **Migrating is one environment variable — never a rewrite.** The tools you
188
+ decorate, the policies you write, and the guarantees you rely on
189
+ (fail-closed, engine-side attenuation, explicit scopes, value-free audit) are
190
+ identical in every mode. Enterprise simply points the same code at a running
191
+ plane.
192
+
193
+ > The engine ships as a compiled wheel and the Developer Edition is a deliberate
194
+ > *subset* of the platform — the governed control plane (signing, multi-tenant,
195
+ > guardrails, drift, execution-graph) is the enterprise product, never bundled
196
+ > here.
197
+
198
+ → **[Talk to us about Enterprise](mailto:enterprise@watchlight.ai)** when you're
199
+ ready for production.
200
+
201
+ ---
202
+
203
+ ## License
204
+
205
+ Apache-2.0.
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "watchlight"
7
+ version = "0.1.0"
8
+ description = "Watchlight Developer Edition — govern AI agents in-process, with zero infrastructure."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Watchlight AI" }]
13
+ keywords = ["authorization", "ai-agents", "cedar", "policy", "governance"]
14
+ dependencies = [
15
+ # The in-process authorization engine — the real Watchlight policy engine
16
+ # (Cedar evaluation + the surrounding pipeline), embedded via PyO3 and
17
+ # published to PyPI as `watchlight-engine`.
18
+ "watchlight-engine>=0.1,<0.2",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ # Govern an existing framework agent in-process. Each extra pulls the published
23
+ # framework plugin (which pulls the Watchlight SDK); the SAME plugin you ship to
24
+ # production, wired here to the in-process engine via
25
+ # `watchlight.<framework>.governed_plugin`.
26
+ langgraph = ["watchlight-langgraph>=0.4"]
27
+ pydantic-ai = ["watchlight-pydantic-ai>=0.3"]
28
+ claude-agent = ["watchlight-claude-agent>=0.2"]
29
+ # Govern an MCP server: the Watchlight MCP Runtime PEP — a policy enforcement
30
+ # point in front of any MCP (2026-07-28) server. See examples/governed_mcp_server.py.
31
+ mcp = ["watchlight-mcp>=0.1"]
32
+
33
+ [project.scripts]
34
+ # `watchlight dev` → the local decision dashboard (stdlib-only, zero extra deps).
35
+ watchlight = "watchlight.cli:main"
36
+
37
+ [project.urls]
38
+ Homepage = "https://watchlight.ai"
39
+ Documentation = "https://docs.watchlight.ai/de"
40
+
41
+ [tool.setuptools.packages.find]
42
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,171 @@
1
+ """watchlight — Developer Edition.
2
+
3
+ Govern an AI agent in-process, with zero infrastructure. Decorate a tool with
4
+ an *intent*, load a local policy, run your script, and the policy engine
5
+ authorizes every call — allowing what a policy permits and refusing everything
6
+ else **before the tool body runs**.
7
+
8
+ from watchlight import govern, Denied
9
+
10
+ govern.load("watchlight.policy.json") # or govern.allow("permit(...);")
11
+
12
+ @govern.tool(intent="research")
13
+ def web_search(query: str) -> str:
14
+ ...
15
+
16
+ The engine is the REAL Watchlight authorization engine (Cedar evaluation + the
17
+ surrounding pipeline) embedded via the ``watchlight-engine`` extension — the
18
+ same authorization model that runs in production, just in-process. Going to
19
+ production is pointing at a running policy service, not a rewrite.
20
+
21
+ Guarantees that are identical to production and MUST NOT be relaxed here:
22
+ * **Fail-closed** — no matching policy denies; an unreachable decision denies.
23
+ * **Explicit intent** — a tool is governed by the intent you declare, never
24
+ inferred from its name or body.
25
+ * **Value-free audit** — argument *values* never enter the trail; only who,
26
+ what intent, which resource, and the decision.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import datetime
32
+ import functools
33
+ import json
34
+ import os
35
+ import pathlib
36
+ from typing import Any, Callable, TypeVar
37
+
38
+ import watchlight_engine as _engine
39
+
40
+ __all__ = ["Watchlight", "Denied", "govern"]
41
+
42
+ _F = TypeVar("_F", bound=Callable[..., Any])
43
+
44
+
45
+ class Denied(PermissionError):
46
+ """Raised when the policy engine refuses a governed tool call (fail-closed).
47
+
48
+ The decorated function's body never runs — the refusal happens *before*
49
+ the side effect, which is the whole point.
50
+ """
51
+
52
+ def __init__(self, tool: str, intent: str, reason: str) -> None:
53
+ self.tool = tool
54
+ self.intent = intent
55
+ self.reason = reason
56
+ super().__init__(f"watchlight denied intent '{intent}' on tool/{tool}: {reason}")
57
+
58
+
59
+ class Watchlight:
60
+ """An in-process policy decision point for a single agent.
61
+
62
+ Wraps the ``watchlight-engine`` in-process authorization core. Policies are
63
+ loaded from a local file or added inline; each governed tool call is
64
+ authorized against them.
65
+ """
66
+
67
+ def __init__(self, agent: str | None = None, audit_dir: str | os.PathLike[str] = ".watchlight") -> None:
68
+ self._engine = _engine.PolicyEngine()
69
+ self.agent = agent or os.environ.get("WATCHLIGHT_AGENT", "my-agent")
70
+ self._audit_path = pathlib.Path(audit_dir) / "audit.jsonl"
71
+ self._announced = False
72
+ self._policy_count = 0
73
+
74
+ # ── policy loading ──────────────────────────────────────────────
75
+
76
+ def allow(self, cedar_code: str, name: str | None = None) -> "Watchlight":
77
+ """Add one Cedar policy inline. Returns self for chaining."""
78
+ self._engine.add_policy(
79
+ json.dumps({"name": name or f"policy-{self._policy_count}", "code": cedar_code})
80
+ )
81
+ self._policy_count += 1
82
+ return self
83
+
84
+ def load(self, path: str | os.PathLike[str]) -> "Watchlight":
85
+ """Load policies from a JSON file — a list of ``{"name", "code"}`` objects
86
+ (or ``{"policies": [...]}``). Fail-closed: a missing file loads nothing,
87
+ so every governed call is denied until a policy permits it."""
88
+ p = pathlib.Path(path)
89
+ if not p.exists():
90
+ return self
91
+ data = json.loads(p.read_text())
92
+ entries = data if isinstance(data, list) else data.get("policies", [])
93
+ for entry in entries:
94
+ self.allow(entry["code"], entry.get("name"))
95
+ return self
96
+
97
+ # ── governing tools ─────────────────────────────────────────────
98
+
99
+ def tool(self, intent: str) -> Callable[[_F], _F]:
100
+ """Decorate a function as a governed tool with the given *intent*.
101
+
102
+ On every call the engine authorizes ``(agent, intent, tool/<name>)``.
103
+ On ALLOW the function runs; on anything else a :class:`Denied` is raised
104
+ and the body never executes.
105
+ """
106
+
107
+ def decorator(fn: _F) -> _F:
108
+ resource = f"tool/{fn.__name__}"
109
+
110
+ @functools.wraps(fn)
111
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
112
+ decision, reason = self._authorize(intent, resource)
113
+ self._audit(intent, resource, decision, reason)
114
+ if decision != "Allow":
115
+ raise Denied(fn.__name__, intent, reason or "no matching policy")
116
+ return fn(*args, **kwargs)
117
+
118
+ return wrapper # type: ignore[return-value]
119
+
120
+ return decorator
121
+
122
+ # ── internals ───────────────────────────────────────────────────
123
+
124
+ def _authorize(self, intent: str, resource: str) -> tuple[str, str]:
125
+ response = json.loads(
126
+ self._engine.authorize(
127
+ json.dumps(
128
+ {
129
+ "principal": self.agent,
130
+ "action": intent,
131
+ "resource": resource,
132
+ "context": {},
133
+ }
134
+ )
135
+ )
136
+ )
137
+ return response.get("decision", "Deny"), response.get("reason", "")
138
+
139
+ def _announce(self) -> None:
140
+ if not self._announced:
141
+ print(f"watchlight: governing '{self.agent}' (dev mode, in-process engine)")
142
+ self._announced = True
143
+
144
+ def _audit(self, intent: str, resource: str, decision: str, reason: str) -> None:
145
+ self._announce()
146
+ allowed = decision == "Allow"
147
+ tag = "ALLOW" if allowed else "DENY"
148
+ trailer = "" if allowed else f" {reason or 'no matching policy'}"
149
+ print(f"watchlight: {tag:5} {intent:9} {resource}{trailer}")
150
+ # Value-free audit: argument VALUES never enter the trail — only the
151
+ # governance decision. This mirrors the production audit contract.
152
+ record = {
153
+ "ts": datetime.datetime.now(datetime.timezone.utc).isoformat(),
154
+ "agent": self.agent,
155
+ "intent": intent,
156
+ "resource": resource,
157
+ "decision": decision,
158
+ }
159
+ try:
160
+ self._audit_path.parent.mkdir(parents=True, exist_ok=True)
161
+ with self._audit_path.open("a", encoding="utf-8") as fh:
162
+ fh.write(json.dumps(record) + "\n")
163
+ except OSError:
164
+ # Audit is best-effort in dev mode; never let it break the app.
165
+ pass
166
+
167
+
168
+ # A ready-to-use default governor so `from watchlight import govern` just works.
169
+ # It starts with NO policies — fail-closed by default — until you `govern.load(...)`
170
+ # a policy file or `govern.allow(...)` a policy inline.
171
+ govern = Watchlight()