@watchlight/sdk 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or Derivative
95
+ Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Watchlight AI
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @watchlight/sdk
2
+
3
+ Govern your Node/TypeScript agent's tools with a **fail-closed, in-process
4
+ policy decision** — zero infrastructure. Declare an intent, wrap a tool, and the
5
+ call is authorized against your Cedar policies before it runs; denied calls throw
6
+ and their body never executes. Every decision lands in a **value-free**
7
+ `.watchlight/audit.jsonl`.
8
+
9
+ This is the TypeScript counterpart of the Python `watchlight` package. It is thin
10
+ glue over [`@watchlight/engine`](https://www.npmjs.com/package/@watchlight/engine)
11
+ (the real `wl-apdp` Cedar core compiled to WebAssembly) and contains **no
12
+ decision logic** — every ALLOW/DENY comes from the engine.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @watchlight/sdk
18
+ ```
19
+
20
+ ## Govern a tool
21
+
22
+ ```ts
23
+ import { govern, Denied } from "@watchlight/sdk";
24
+
25
+ govern.load("watchlight.policy.json"); // or govern.allow('permit(principal, action == Action::"research", resource);')
26
+
27
+ const webSearch = govern.tool(async (q: string) => search(q), { intent: "research" });
28
+ const transferFunds = govern.tool(async (amt: number) => bank.send(amt), { intent: "transfer" });
29
+
30
+ await webSearch("cedar policy"); // ALLOW → runs
31
+ try {
32
+ await transferFunds(1000); // no policy permits "transfer" → DENY
33
+ } catch (e) {
34
+ if (e instanceof Denied) console.error(e.message); // never executed
35
+ }
36
+ ```
37
+
38
+ TypeScript uses a higher-order function (`govern.tool(fn, { intent })`) rather
39
+ than a decorator — it works across every TS build setup with full type
40
+ inference. Governed functions are always async (the engine's authorize path is
41
+ async in WebAssembly).
42
+
43
+ Fail-closed by default: with no matching policy, every governed call is denied.
44
+
45
+ ## Sub-agent scope attenuation
46
+
47
+ Derive strictly-narrower child scopes for sub-agents; the real engine enforces
48
+ strict-subset, so a child can never hold a capability its parent lacks.
49
+
50
+ ```ts
51
+ const root = await govern.scope({ tools: ["read", "search"], timeBudgetSeconds: 600 });
52
+ const child = root.attenuate({ tools: ["read"] }); // ⊆ parent → OK
53
+ root.attenuate({ tools: ["read", "write"] }); // escalation → throws AttenuationDenied
54
+ ```
55
+
56
+ The Developer Edition governs the tree up to depth `DE_MAX_DEPTH` (5); beyond it,
57
+ `attenuate` throws `DevEditionCeiling`. Enterprise removes the cap and enforces
58
+ it server-side.
59
+
60
+ ## Claude Agent SDK
61
+
62
+ Govern an SDK-managed agent's tool calls with a `PreToolUse` gate — no glue in
63
+ your tool bodies. Denied tools are blocked by the SDK before they run.
64
+
65
+ ```ts
66
+ import { query } from "@anthropic-ai/claude-agent-sdk";
67
+ import { govern, governedHooks } from "@watchlight/sdk";
68
+
69
+ govern.load("watchlight.policy.json");
70
+
71
+ // Map Claude tool names → governance intents.
72
+ const TOOL_INTENTS: Record<string, string> = { WebSearch: "research", Bash: "execute" };
73
+ const { hooks } = governedHooks({ intentFor: (t) => TOOL_INTENTS[t] ?? t });
74
+
75
+ for await (const msg of query({ prompt, options: { hooks } })) {
76
+ // WebSearch runs if a policy permits "research"; anything unpermitted is
77
+ // denied before execution.
78
+ }
79
+ ```
80
+
81
+ The hook is fail-closed and never throws back to the SDK — a governance error
82
+ denies the call. Every decision is audited.
83
+
84
+ ## Value-free audit
85
+
86
+ `.watchlight/audit.jsonl` records **who / what intent / which tool / the
87
+ decision** — never argument values. Same contract as the production audit trail.
88
+
89
+ ```json
90
+ {"ts":"2026-08-29T…Z","agent":"my-agent","intent":"research","resource":"tool/webSearch","decision":"Allow"}
91
+ ```
92
+
93
+ ## Graduation to Enterprise
94
+
95
+ Set `WATCHLIGHT_APDP_URL` and the **same code** authorizes against the networked
96
+ Watchlight control plane (signed lineage, cross-tenant isolation, IdP/mTLS
97
+ attestation) instead of the in-process engine — no policy or code change. The
98
+ authorize request/response shape is identical; only the transport swaps.
99
+
100
+ ```bash
101
+ export WATCHLIGHT_APDP_URL=https://apdp.example.com # → networked (Enterprise)
102
+ export WATCHLIGHT_PLUGIN_TOKEN=... # bearer for the control plane
103
+ export WATCHLIGHT_TENANT_ID=... # X-Wl-Tenant-Id
104
+ # unset WATCHLIGHT_APDP_URL → in-process (Developer Edition)
105
+ ```
106
+
107
+ Or per-instance: `new Watchlight({ apdpUrl, token, tenantId })`. Check which is
108
+ live with `governor.mode` (`"in-process"` | `"networked"`). Networked mode is
109
+ fail-closed — an unreachable control plane denies. Sub-agent `attenuate()` runs
110
+ in-process in the DE; under `WATCHLIGHT_APDP_URL` it is enforced server-side, so
111
+ `scope()` defers to the control plane.
112
+
113
+ ## License
114
+
115
+ Apache-2.0. The compiled engine it depends on (`@watchlight/engine`) is under the
116
+ Watchlight Developer Edition License (free for development, testing, and
117
+ production — including commercially — up to 25 governed agents per organization).
@@ -0,0 +1,63 @@
1
+ import type { Engine } from "@watchlight/engine";
2
+ /** Developer-Edition sub-agent tree depth ceiling. */
3
+ export declare const DE_MAX_DEPTH = 5;
4
+ /** Raised at the Developer-Edition depth ceiling. NOT a policy denial — a
5
+ * product boundary. Every attenuation up to the cap was a real, engine-validated
6
+ * strict subset. */
7
+ export declare class DevEditionCeiling extends Error {
8
+ readonly depth: number;
9
+ readonly cap: number;
10
+ constructor(depth: number);
11
+ }
12
+ /** Raised when a requested child scope is not a strict subset of its parent. */
13
+ export declare class AttenuationDenied extends Error {
14
+ readonly violations: string[];
15
+ readonly reason: string;
16
+ constructor(violations: string[], reason: string);
17
+ }
18
+ export interface AttenuateOptions {
19
+ tools?: readonly string[];
20
+ resources?: readonly string[];
21
+ intents?: readonly string[];
22
+ timeBudgetSeconds?: number;
23
+ }
24
+ interface ScopeInit {
25
+ engine: Engine;
26
+ auditPath: string;
27
+ agent: string;
28
+ allowedTools: string[];
29
+ allowedResources: string[];
30
+ allowedIntents: string[];
31
+ maxDepth: number;
32
+ timeBudgetSeconds: number;
33
+ depth: number;
34
+ parentId?: string;
35
+ }
36
+ /** A capability scope that can spawn strictly-narrower child scopes. Create the
37
+ * root with {@link Watchlight.scope}; call {@link attenuate} to derive a
38
+ * sub-agent scope. `attenuate` is synchronous (the engine validator is sync). */
39
+ export declare class Scope {
40
+ readonly agent: string;
41
+ readonly allowedTools: string[];
42
+ readonly allowedResources: string[];
43
+ readonly allowedIntents: string[];
44
+ readonly maxDepth: number;
45
+ readonly timeBudgetSeconds: number;
46
+ readonly depth: number;
47
+ readonly nodeId: string;
48
+ readonly parentId?: string;
49
+ private readonly _engine;
50
+ private readonly _auditPath;
51
+ constructor(init: ScopeInit);
52
+ /**
53
+ * Derive a sub-agent scope — a strict subset of this one. Any dimension you
54
+ * omit inherits the parent's (and the engine clamps it regardless). Throws
55
+ * {@link AttenuationDenied} if the request exceeds the parent, and
56
+ * {@link DevEditionCeiling} at the Developer-Edition depth ceiling.
57
+ */
58
+ attenuate(opts?: AttenuateOptions): Scope;
59
+ /** Record this scope as the root of an attenuation tree (parent-less). */
60
+ emitRoot(): void;
61
+ private _record;
62
+ }
63
+ export {};
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ // Sub-agent scope attenuation — the TS mirror of Python `watchlight.attenuation`.
3
+ //
4
+ // A Scope is a capability set that can spawn strictly-narrower child scopes. Any
5
+ // dimension a child requests that the parent does not hold is denied by the real
6
+ // engine strict-subset validator (@watchlight/engine), and every attenuation is
7
+ // written to the value-free audit trail. The Developer Edition governs the tree
8
+ // up to DE_MAX_DEPTH; Enterprise removes the cap and enforces it server-side.
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.Scope = exports.AttenuationDenied = exports.DevEditionCeiling = exports.DE_MAX_DEPTH = void 0;
44
+ const fs = __importStar(require("node:fs"));
45
+ const path = __importStar(require("node:path"));
46
+ const crypto = __importStar(require("node:crypto"));
47
+ /** Developer-Edition sub-agent tree depth ceiling. */
48
+ exports.DE_MAX_DEPTH = 5;
49
+ const CEILING_NOTICE = (cap, depth) => `Developer Edition governs sub-agent trees up to depth ${cap}; ` +
50
+ `requested depth ${depth}. Enterprise removes this cap and enforces it server-side.`;
51
+ /** Raised at the Developer-Edition depth ceiling. NOT a policy denial — a
52
+ * product boundary. Every attenuation up to the cap was a real, engine-validated
53
+ * strict subset. */
54
+ class DevEditionCeiling extends Error {
55
+ constructor(depth) {
56
+ super(CEILING_NOTICE(exports.DE_MAX_DEPTH, depth));
57
+ this.name = "DevEditionCeiling";
58
+ this.depth = depth;
59
+ this.cap = exports.DE_MAX_DEPTH;
60
+ }
61
+ }
62
+ exports.DevEditionCeiling = DevEditionCeiling;
63
+ /** Raised when a requested child scope is not a strict subset of its parent. */
64
+ class AttenuationDenied extends Error {
65
+ constructor(violations, reason) {
66
+ super(`sub-agent scope denied: ${reason}`);
67
+ this.name = "AttenuationDenied";
68
+ this.violations = violations;
69
+ this.reason = reason;
70
+ }
71
+ }
72
+ exports.AttenuationDenied = AttenuationDenied;
73
+ const norm = (x) => (x ? [...x] : []);
74
+ const nodeId = () => crypto.randomBytes(4).toString("hex");
75
+ /** A capability scope that can spawn strictly-narrower child scopes. Create the
76
+ * root with {@link Watchlight.scope}; call {@link attenuate} to derive a
77
+ * sub-agent scope. `attenuate` is synchronous (the engine validator is sync). */
78
+ class Scope {
79
+ constructor(init) {
80
+ this._engine = init.engine;
81
+ this._auditPath = init.auditPath;
82
+ this.agent = init.agent;
83
+ this.allowedTools = norm(init.allowedTools);
84
+ this.allowedResources = norm(init.allowedResources);
85
+ this.allowedIntents = norm(init.allowedIntents);
86
+ this.maxDepth = init.maxDepth;
87
+ this.timeBudgetSeconds = init.timeBudgetSeconds;
88
+ this.depth = init.depth;
89
+ this.nodeId = nodeId();
90
+ this.parentId = init.parentId;
91
+ }
92
+ /**
93
+ * Derive a sub-agent scope — a strict subset of this one. Any dimension you
94
+ * omit inherits the parent's (and the engine clamps it regardless). Throws
95
+ * {@link AttenuationDenied} if the request exceeds the parent, and
96
+ * {@link DevEditionCeiling} at the Developer-Edition depth ceiling.
97
+ */
98
+ attenuate(opts = {}) {
99
+ const childDepth = this.depth + 1;
100
+ const requestedTools = opts.tools !== undefined ? norm(opts.tools) : this.allowedTools;
101
+ // Developer-Edition ceiling — a product boundary, checked before the engine.
102
+ if (childDepth > exports.DE_MAX_DEPTH) {
103
+ this._record({
104
+ nodeId: nodeId(),
105
+ parentId: this.nodeId,
106
+ tools: requestedTools,
107
+ resource: `sub-agent depth ${childDepth}`,
108
+ decision: "Deny",
109
+ depth: childDepth,
110
+ reason: CEILING_NOTICE(exports.DE_MAX_DEPTH, childDepth),
111
+ });
112
+ throw new DevEditionCeiling(childDepth);
113
+ }
114
+ const parent = {
115
+ allowed_tools: this.allowedTools,
116
+ allowed_resources: this.allowedResources.map((matcher) => ({ matcher })),
117
+ allowed_intents: this.allowedIntents,
118
+ max_depth: this.maxDepth,
119
+ time_budget_seconds: this.timeBudgetSeconds,
120
+ depth: this.depth,
121
+ };
122
+ const request = {
123
+ allowed_tools: requestedTools,
124
+ allowed_resources: (opts.resources !== undefined ? norm(opts.resources) : this.allowedResources).map((matcher) => ({ matcher })),
125
+ allowed_intents: opts.intents !== undefined ? norm(opts.intents) : this.allowedIntents,
126
+ max_depth: Math.max(0, this.maxDepth - 1),
127
+ time_budget_seconds: opts.timeBudgetSeconds !== undefined ? opts.timeBudgetSeconds : this.timeBudgetSeconds,
128
+ };
129
+ const resp = this._engine.attenuateScope(parent, request);
130
+ if (resp.decision !== "Allow") {
131
+ const violations = "violations" in resp ? resp.violations : [];
132
+ const reason = ("reason" in resp && resp.reason) || "requested scope is not a strict subset of the parent";
133
+ this._record({
134
+ nodeId: nodeId(),
135
+ parentId: this.nodeId,
136
+ tools: requestedTools,
137
+ resource: `sub-agent depth ${childDepth}`,
138
+ decision: "Deny",
139
+ depth: childDepth,
140
+ reason,
141
+ });
142
+ throw new AttenuationDenied(violations, reason);
143
+ }
144
+ // The engine returns the CLAMPED grant — never the child's raw request.
145
+ const granted = resp.granted_scope;
146
+ const grantedResources = (granted.allowed_resources ?? request.allowed_resources).map((r) => typeof r === "string" ? r : r.matcher);
147
+ const child = new Scope({
148
+ engine: this._engine,
149
+ auditPath: this._auditPath,
150
+ agent: this.agent,
151
+ allowedTools: granted.allowed_tools ?? request.allowed_tools,
152
+ allowedResources: grantedResources,
153
+ allowedIntents: granted.allowed_intents ?? request.allowed_intents,
154
+ maxDepth: granted.max_depth ?? request.max_depth,
155
+ timeBudgetSeconds: granted.time_budget_seconds ?? request.time_budget_seconds,
156
+ depth: granted.depth ?? childDepth,
157
+ parentId: this.nodeId,
158
+ });
159
+ this._record({
160
+ nodeId: child.nodeId,
161
+ parentId: this.nodeId,
162
+ tools: child.allowedTools,
163
+ resource: `sub-agent depth ${child.depth}`,
164
+ decision: "Allow",
165
+ depth: child.depth,
166
+ });
167
+ return child;
168
+ }
169
+ /** Record this scope as the root of an attenuation tree (parent-less). */
170
+ emitRoot() {
171
+ this._record({
172
+ nodeId: this.nodeId,
173
+ parentId: undefined,
174
+ tools: this.allowedTools,
175
+ resource: "root scope",
176
+ decision: "Allow",
177
+ depth: this.depth,
178
+ });
179
+ }
180
+ _record(r) {
181
+ // Value-free by construction — a scope's dimensions are capability NAMES,
182
+ // never argument values. Shape matches Python's audit tree records.
183
+ const record = {
184
+ ts: new Date().toISOString(),
185
+ agent: this.agent,
186
+ intent: "attenuate",
187
+ event: "attenuation",
188
+ node_id: r.nodeId,
189
+ resource: r.resource,
190
+ decision: r.decision,
191
+ depth: r.depth,
192
+ tools: [...r.tools],
193
+ };
194
+ if (r.parentId)
195
+ record.parent_id = r.parentId;
196
+ if (r.reason)
197
+ record.reason = r.reason;
198
+ try {
199
+ fs.mkdirSync(path.dirname(this._auditPath), { recursive: true });
200
+ fs.appendFileSync(this._auditPath, JSON.stringify(record) + "\n", "utf8");
201
+ }
202
+ catch {
203
+ // Best-effort in dev mode.
204
+ }
205
+ }
206
+ }
207
+ exports.Scope = Scope;
@@ -0,0 +1,61 @@
1
+ import { Engine } from "@watchlight/engine";
2
+ export interface AuthorizeRequest {
3
+ principal: string;
4
+ action: string;
5
+ resource: string;
6
+ context?: Record<string, unknown>;
7
+ }
8
+ export interface Decision {
9
+ decision: string;
10
+ reason: string;
11
+ }
12
+ export interface GovernanceBackend {
13
+ readonly kind: "in-process" | "networked";
14
+ /** A short human label for the dev announce line. */
15
+ readonly label: string;
16
+ /** Register a policy. In-process loads it; networked ignores it (policies are
17
+ * managed by the control plane) after warning once. */
18
+ addPolicy(policy: {
19
+ name: string;
20
+ code: string;
21
+ }): void;
22
+ /** Authorize a request. Fail-closed. */
23
+ authorize(req: AuthorizeRequest): Promise<Decision>;
24
+ /** The in-process engine (for local sub-agent attenuation), or null when
25
+ * networked — attenuation is enforced server-side in Enterprise. */
26
+ engine(): Promise<Engine> | null;
27
+ }
28
+ /** DE default — the compiled engine in-process. */
29
+ export declare class InProcessBackend implements GovernanceBackend {
30
+ readonly kind: "in-process";
31
+ readonly label = "dev mode, in-process engine";
32
+ private _enginePromise?;
33
+ private _pending;
34
+ addPolicy(policy: {
35
+ name: string;
36
+ code: string;
37
+ }): void;
38
+ private _ready;
39
+ authorize(req: AuthorizeRequest): Promise<Decision>;
40
+ engine(): Promise<Engine>;
41
+ }
42
+ /** Enterprise — POST /authorize to the networked control plane. */
43
+ export declare class NetworkedBackend implements GovernanceBackend {
44
+ readonly kind: "networked";
45
+ readonly label: string;
46
+ private readonly _base;
47
+ private readonly _token?;
48
+ private readonly _tenantId?;
49
+ private _warnedPolicy;
50
+ constructor(url: string, token?: string, tenantId?: string);
51
+ addPolicy(): void;
52
+ authorize(req: AuthorizeRequest): Promise<Decision>;
53
+ engine(): null;
54
+ }
55
+ /** Select the backend: networked when a URL is given (option or
56
+ * WATCHLIGHT_APDP_URL), in-process otherwise. */
57
+ export declare function selectBackend(opts: {
58
+ apdpUrl?: string;
59
+ token?: string;
60
+ tenantId?: string;
61
+ }): GovernanceBackend;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ // Governance backend seam — the one object that graduation swaps.
3
+ //
4
+ // The authorize request/response shape is IDENTICAL in both editions
5
+ // (`{principal, action, resource, context}` → `{decision, reason}`), so the same
6
+ // govern/tool/hook code works either way:
7
+ //
8
+ // * Developer Edition (default) — InProcessBackend runs the compiled
9
+ // @watchlight/engine wasm core in-process. Zero infrastructure.
10
+ // * Enterprise — set WATCHLIGHT_APDP_URL and NetworkedBackend POSTs the same
11
+ // request to the control plane's /authorize (signed lineage, cross-tenant
12
+ // isolation, IdP/mTLS attestation live there). No policy or code change.
13
+ //
14
+ // Fail-closed everywhere: any transport/engine error resolves to Deny.
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.NetworkedBackend = exports.InProcessBackend = void 0;
17
+ exports.selectBackend = selectBackend;
18
+ const engine_1 = require("@watchlight/engine");
19
+ /** DE default — the compiled engine in-process. */
20
+ class InProcessBackend {
21
+ constructor() {
22
+ this.kind = "in-process";
23
+ this.label = "dev mode, in-process engine";
24
+ this._pending = [];
25
+ }
26
+ addPolicy(policy) {
27
+ this._pending.push(policy);
28
+ }
29
+ async _ready() {
30
+ if (!this._enginePromise)
31
+ this._enginePromise = engine_1.Engine.create();
32
+ const engine = await this._enginePromise;
33
+ if (this._pending.length) {
34
+ const batch = this._pending;
35
+ this._pending = [];
36
+ for (const p of batch)
37
+ await engine.addPolicy(p);
38
+ }
39
+ return engine;
40
+ }
41
+ async authorize(req) {
42
+ const engine = await this._ready();
43
+ const resp = await engine.authorize({
44
+ principal: req.principal,
45
+ action: req.action,
46
+ resource: req.resource,
47
+ context: req.context ?? {},
48
+ });
49
+ return { decision: resp.decision ?? "Deny", reason: resp.reason ?? "" };
50
+ }
51
+ engine() {
52
+ return this._ready();
53
+ }
54
+ }
55
+ exports.InProcessBackend = InProcessBackend;
56
+ /** Enterprise — POST /authorize to the networked control plane. */
57
+ class NetworkedBackend {
58
+ constructor(url, token, tenantId) {
59
+ this.kind = "networked";
60
+ this._warnedPolicy = false;
61
+ this._base = url.replace(/\/+$/, "");
62
+ this._token = token;
63
+ this._tenantId = tenantId;
64
+ this.label = `control plane: ${this._base}`;
65
+ }
66
+ addPolicy() {
67
+ if (!this._warnedPolicy) {
68
+ // eslint-disable-next-line no-console
69
+ console.warn("watchlight: WATCHLIGHT_APDP_URL is set — policies are managed by the " +
70
+ "control plane; local allow()/load() is ignored.");
71
+ this._warnedPolicy = true;
72
+ }
73
+ }
74
+ async authorize(req) {
75
+ const headers = { "content-type": "application/json" };
76
+ if (this._token)
77
+ headers["authorization"] = `Bearer ${this._token}`;
78
+ if (this._tenantId)
79
+ headers["x-wl-tenant-id"] = this._tenantId;
80
+ try {
81
+ const resp = await fetch(`${this._base}/authorize`, {
82
+ method: "POST",
83
+ headers,
84
+ body: JSON.stringify({
85
+ principal: req.principal,
86
+ action: req.action,
87
+ resource: req.resource,
88
+ context: req.context ?? {},
89
+ }),
90
+ });
91
+ if (!resp.ok)
92
+ return { decision: "Deny", reason: `APDP error: ${resp.status}` };
93
+ const data = (await resp.json());
94
+ return { decision: data.decision ?? "Deny", reason: data.reason ?? "" };
95
+ }
96
+ catch (e) {
97
+ // Fail-closed: an unreachable control plane denies.
98
+ return { decision: "Deny", reason: `APDP unreachable: ${String(e)}` };
99
+ }
100
+ }
101
+ engine() {
102
+ return null;
103
+ }
104
+ }
105
+ exports.NetworkedBackend = NetworkedBackend;
106
+ /** Select the backend: networked when a URL is given (option or
107
+ * WATCHLIGHT_APDP_URL), in-process otherwise. */
108
+ function selectBackend(opts) {
109
+ const url = opts.apdpUrl ?? process.env.WATCHLIGHT_APDP_URL;
110
+ if (url && url.trim()) {
111
+ return new NetworkedBackend(url.trim(), opts.token ?? process.env.WATCHLIGHT_PLUGIN_TOKEN, opts.tenantId ?? process.env.WATCHLIGHT_TENANT_ID);
112
+ }
113
+ return new InProcessBackend();
114
+ }
@@ -0,0 +1,29 @@
1
+ export type HookEventName = "PreToolUse" | "PostToolUse" | "UserPromptSubmit" | "Stop" | "SessionStart" | "SessionEnd" | "SubagentStart" | "SubagentStop";
2
+ /** Permission verdict the SDK accepts on a PreToolUse hook. */
3
+ export type PermissionDecision = "allow" | "deny" | "ask" | "defer";
4
+ export interface BaseHookInput {
5
+ hook_event_name: HookEventName;
6
+ session_id?: string;
7
+ cwd?: string;
8
+ agent_id?: string;
9
+ agent_type?: string;
10
+ }
11
+ export interface PreToolUseHookInput extends BaseHookInput {
12
+ hook_event_name: "PreToolUse";
13
+ tool_name: string;
14
+ tool_input: Record<string, unknown>;
15
+ }
16
+ export interface HookOutput {
17
+ continue?: boolean;
18
+ hookSpecificOutput?: {
19
+ hookEventName: HookEventName;
20
+ permissionDecision?: PermissionDecision;
21
+ permissionDecisionReason?: string;
22
+ };
23
+ }
24
+ export type HookCallback = (input: BaseHookInput) => Promise<HookOutput> | HookOutput;
25
+ export interface HookMatcherEntry {
26
+ matcher?: string;
27
+ hooks: HookCallback[];
28
+ }
29
+ export type HooksOption = Partial<Record<HookEventName, HookMatcherEntry[]>>;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ // Minimal local type definitions mirroring the Claude Agent SDK's hook contract
3
+ // — just the subset the PreToolUse governance gate needs. Kept local so
4
+ // @watchlight/sdk does not take a hard dependency on the Claude Agent SDK; it is
5
+ // a peer you install alongside. Shapes match the SDK's documented hook I/O.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,21 @@
1
+ import { Watchlight } from "./index";
2
+ import type { HooksOption } from "./claude-agent-types";
3
+ export type { HookEventName, PermissionDecision, BaseHookInput, PreToolUseHookInput, HookOutput, HookCallback, HookMatcherEntry, HooksOption, } from "./claude-agent-types";
4
+ export interface GovernedHooksOptions {
5
+ /** The governor to authorize against. Defaults to the shared `govern`. */
6
+ governor?: Watchlight;
7
+ /** Map a Claude tool name to a governance intent. Defaults to identity
8
+ * (the intent is the tool name). Provide this to bind semantic intents,
9
+ * e.g. `(t) => ({ WebSearch: "research", Bash: "execute" }[t] ?? t)`. */
10
+ intentFor?: (toolName: string) => string;
11
+ }
12
+ export interface GovernedHooksResult {
13
+ /** Pass directly to the Claude Agent SDK: `query({ options: { hooks } })`. */
14
+ hooks: HooksOption;
15
+ }
16
+ /**
17
+ * Build Claude Agent SDK hooks that gate every tool call through the in-process
18
+ * Watchlight engine. Fail-closed. The hook never throws back to the SDK — a
19
+ * governance error becomes a `deny`.
20
+ */
21
+ export declare function governedHooks(options?: GovernedHooksOptions): GovernedHooksResult;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ // Claude Agent SDK integration — govern an SDK-managed agent's tool calls with
3
+ // the in-process engine. The TS counterpart of Python `watchlight.claude_agent`.
4
+ //
5
+ // import { query } from "@anthropic-ai/claude-agent-sdk";
6
+ // import { govern, governedHooks } from "@watchlight/sdk";
7
+ //
8
+ // govern.load("watchlight.policy.json");
9
+ // const { hooks } = governedHooks({ intentFor: (t) => TOOL_INTENTS[t] ?? t });
10
+ //
11
+ // for await (const msg of query({ prompt, options: { hooks } })) { … }
12
+ //
13
+ // The returned `hooks` install a PreToolUse gate: before the SDK runs any tool,
14
+ // the in-process engine authorizes (agent, intent, tool/<name>). ALLOW lets the
15
+ // call proceed; anything else returns a `deny` permission decision and the tool
16
+ // never executes — denied before it runs. Fail-closed: a governance error also
17
+ // denies. Every decision lands in the value-free `.watchlight/audit.jsonl`.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.governedHooks = governedHooks;
20
+ const index_1 = require("./index");
21
+ /**
22
+ * Build Claude Agent SDK hooks that gate every tool call through the in-process
23
+ * Watchlight engine. Fail-closed. The hook never throws back to the SDK — a
24
+ * governance error becomes a `deny`.
25
+ */
26
+ function governedHooks(options = {}) {
27
+ const governor = options.governor ?? index_1.govern;
28
+ const intentFor = options.intentFor ?? ((t) => t);
29
+ const preToolUse = async (input) => {
30
+ const ev = input;
31
+ const toolName = ev.tool_name ?? "unknown";
32
+ try {
33
+ const intent = intentFor(toolName);
34
+ const { allowed, reason } = await governor.check(intent, toolName);
35
+ return {
36
+ hookSpecificOutput: {
37
+ hookEventName: "PreToolUse",
38
+ permissionDecision: allowed ? "allow" : "deny",
39
+ ...(allowed ? {} : { permissionDecisionReason: reason || "no matching policy" }),
40
+ },
41
+ };
42
+ }
43
+ catch (e) {
44
+ // Hooks must never throw back to the SDK; a governance error is
45
+ // fail-closed — deny the tool call rather than let it through.
46
+ return {
47
+ hookSpecificOutput: {
48
+ hookEventName: "PreToolUse",
49
+ permissionDecision: "deny",
50
+ permissionDecisionReason: `watchlight governance error (fail-closed): ${String(e)}`,
51
+ },
52
+ };
53
+ }
54
+ };
55
+ return { hooks: { PreToolUse: [{ hooks: [preToolUse] }] } };
56
+ }
@@ -0,0 +1,97 @@
1
+ import { Scope } from "./attenuation";
2
+ export { Scope, DE_MAX_DEPTH, AttenuationDenied, DevEditionCeiling } from "./attenuation";
3
+ export { governedHooks } from "./claude-agent";
4
+ export type { GovernedHooksOptions, GovernedHooksResult } from "./claude-agent";
5
+ export type { GovernanceBackend, Decision, AuthorizeRequest } from "./backend";
6
+ export { InProcessBackend, NetworkedBackend } from "./backend";
7
+ /** Raised when the policy engine refuses a governed tool call (fail-closed). */
8
+ export declare class Denied extends Error {
9
+ readonly tool: string;
10
+ readonly intent: string;
11
+ readonly reason: string;
12
+ constructor(tool: string, intent: string, reason: string);
13
+ }
14
+ /** A function governed by {@link Watchlight.tool} — always async (the engine's
15
+ * authorize path is async in WebAssembly). */
16
+ export type Governed<A extends unknown[], R> = (...args: A) => Promise<Awaited<R>>;
17
+ export interface WatchlightOptions {
18
+ /** Stable agent identity for the audit trail. Defaults to
19
+ * `WATCHLIGHT_AGENT` env or `"my-agent"`. */
20
+ agent?: string;
21
+ /** Directory for the audit trail. `audit.jsonl` is written inside it.
22
+ * Defaults to `.watchlight`. */
23
+ auditDir?: string;
24
+ /** Graduate to the networked control plane: authorize against this APDP URL
25
+ * instead of the in-process engine. Defaults to `WATCHLIGHT_APDP_URL`. When
26
+ * unset, governance runs fully in-process (Developer Edition). */
27
+ apdpUrl?: string;
28
+ /** Bearer token for the networked control plane. Defaults to
29
+ * `WATCHLIGHT_PLUGIN_TOKEN`. Ignored in-process. */
30
+ token?: string;
31
+ /** Tenant id (`X-Wl-Tenant-Id`) for the networked control plane. Defaults to
32
+ * `WATCHLIGHT_TENANT_ID`. Ignored in-process. */
33
+ tenantId?: string;
34
+ }
35
+ export interface ScopeOptions {
36
+ tools?: readonly string[];
37
+ resources?: readonly string[];
38
+ intents?: readonly string[];
39
+ maxDepth?: number;
40
+ timeBudgetSeconds?: number;
41
+ }
42
+ /**
43
+ * An in-process policy decision point for a single agent. Wraps the
44
+ * `@watchlight/engine` core; policies are loaded from a file or added inline and
45
+ * each governed call is authorized against them. Fail-closed: with no matching
46
+ * policy, every call is denied.
47
+ */
48
+ export declare class Watchlight {
49
+ readonly agent: string;
50
+ private readonly _auditPath;
51
+ private readonly _backend;
52
+ private _policyCount;
53
+ private _announced;
54
+ constructor(opts?: WatchlightOptions);
55
+ /** `"in-process"` (Developer Edition) or `"networked"` (graduated to the
56
+ * control plane via WATCHLIGHT_APDP_URL). */
57
+ get mode(): "in-process" | "networked";
58
+ /** Add one Cedar policy inline. Chainable. (In networked mode policies are
59
+ * managed by the control plane and this is ignored, with a one-time warning.) */
60
+ allow(cedarCode: string, name?: string): this;
61
+ /** Load policies from a JSON file — a list of `{name, code}` (or
62
+ * `{policies:[...]}`). Fail-closed: a missing file loads nothing, so every
63
+ * governed call is denied until a policy permits it. Chainable. */
64
+ load(file: string): this;
65
+ /** Create a root capability scope for this agent, from which sub-agent scopes
66
+ * are attenuated (strict-subset). Async because the engine initializes
67
+ * lazily; `attenuate()` on the returned scope is synchronous. The Developer
68
+ * Edition governs the tree up to depth {@link DE_MAX_DEPTH}. */
69
+ scope(opts?: ScopeOptions): Promise<Scope>;
70
+ /**
71
+ * Wrap a function as a governed tool with the given `intent`. On every call
72
+ * the engine authorizes `(agent, intent, tool/<name>)`. On ALLOW the function
73
+ * runs; on anything else a {@link Denied} is thrown and the body never
74
+ * executes. The returned function is async.
75
+ */
76
+ tool<A extends unknown[], R>(fn: (...args: A) => R, opts: {
77
+ intent: string;
78
+ }): Governed<A, R>;
79
+ /**
80
+ * Authorize a raw `(intent, tool)` pair, audit the decision, and return it.
81
+ * Fail-closed. Used by framework adapters (e.g. the Claude Agent SDK hooks)
82
+ * that gate tool calls themselves rather than wrapping the function — the
83
+ * decision is identical to {@link tool}, just without running a body.
84
+ */
85
+ check(intent: string, toolName: string): Promise<{
86
+ allowed: boolean;
87
+ decision: string;
88
+ reason: string;
89
+ }>;
90
+ private _authorize;
91
+ private _announce;
92
+ private _audit;
93
+ }
94
+ /** A ready-to-use default governor so `import { govern } from "@watchlight/sdk"`
95
+ * just works. Starts with NO policies — fail-closed — until you `govern.load()`
96
+ * a file or `govern.allow()` a policy inline. */
97
+ export declare const govern: Watchlight;
package/dist/index.js ADDED
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ // @watchlight/sdk — the Watchlight Developer-Edition govern glue for Node/TS.
3
+ //
4
+ // Thin, dependency-light glue over @watchlight/engine (the compiled wl-apdp
5
+ // core). It contains ZERO decision logic — every ALLOW/DENY comes from the
6
+ // engine. It mirrors the Python `watchlight` package: declare intent, govern a
7
+ // tool, get a fail-closed decision, and a value-free `.watchlight/audit.jsonl`
8
+ // trail.
9
+ //
10
+ // import { govern, Denied } from "@watchlight/sdk";
11
+ // govern.load("watchlight.policy.json"); // or govern.allow("permit(...);")
12
+ // const search = govern.tool(webSearch, { intent: "research" });
13
+ // await search(query); // ALLOW → runs; else throws Denied
14
+ //
15
+ // TypeScript uses a higher-order function (`govern.tool(fn, {intent})`) rather
16
+ // than a decorator — decorators are still awkward across TS build setups, and a
17
+ // HOF works everywhere with full type inference.
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.govern = exports.Watchlight = exports.Denied = exports.NetworkedBackend = exports.InProcessBackend = exports.governedHooks = exports.DevEditionCeiling = exports.AttenuationDenied = exports.DE_MAX_DEPTH = exports.Scope = void 0;
53
+ const fs = __importStar(require("node:fs"));
54
+ const path = __importStar(require("node:path"));
55
+ const attenuation_1 = require("./attenuation");
56
+ const backend_1 = require("./backend");
57
+ var attenuation_2 = require("./attenuation");
58
+ Object.defineProperty(exports, "Scope", { enumerable: true, get: function () { return attenuation_2.Scope; } });
59
+ Object.defineProperty(exports, "DE_MAX_DEPTH", { enumerable: true, get: function () { return attenuation_2.DE_MAX_DEPTH; } });
60
+ Object.defineProperty(exports, "AttenuationDenied", { enumerable: true, get: function () { return attenuation_2.AttenuationDenied; } });
61
+ Object.defineProperty(exports, "DevEditionCeiling", { enumerable: true, get: function () { return attenuation_2.DevEditionCeiling; } });
62
+ var claude_agent_1 = require("./claude-agent");
63
+ Object.defineProperty(exports, "governedHooks", { enumerable: true, get: function () { return claude_agent_1.governedHooks; } });
64
+ var backend_2 = require("./backend");
65
+ Object.defineProperty(exports, "InProcessBackend", { enumerable: true, get: function () { return backend_2.InProcessBackend; } });
66
+ Object.defineProperty(exports, "NetworkedBackend", { enumerable: true, get: function () { return backend_2.NetworkedBackend; } });
67
+ /** Raised when the policy engine refuses a governed tool call (fail-closed). */
68
+ class Denied extends Error {
69
+ constructor(tool, intent, reason) {
70
+ super(`watchlight denied intent '${intent}' on tool/${tool}: ${reason}`);
71
+ this.name = "Denied";
72
+ this.tool = tool;
73
+ this.intent = intent;
74
+ this.reason = reason;
75
+ }
76
+ }
77
+ exports.Denied = Denied;
78
+ const norm = (x) => (x ? [...x] : []);
79
+ /**
80
+ * An in-process policy decision point for a single agent. Wraps the
81
+ * `@watchlight/engine` core; policies are loaded from a file or added inline and
82
+ * each governed call is authorized against them. Fail-closed: with no matching
83
+ * policy, every call is denied.
84
+ */
85
+ class Watchlight {
86
+ constructor(opts = {}) {
87
+ this._policyCount = 0;
88
+ this._announced = false;
89
+ this.agent = opts.agent ?? process.env.WATCHLIGHT_AGENT ?? "my-agent";
90
+ this._auditPath = path.join(opts.auditDir ?? ".watchlight", "audit.jsonl");
91
+ this._backend = (0, backend_1.selectBackend)({
92
+ apdpUrl: opts.apdpUrl,
93
+ token: opts.token,
94
+ tenantId: opts.tenantId,
95
+ });
96
+ }
97
+ /** `"in-process"` (Developer Edition) or `"networked"` (graduated to the
98
+ * control plane via WATCHLIGHT_APDP_URL). */
99
+ get mode() {
100
+ return this._backend.kind;
101
+ }
102
+ // ── policy loading ────────────────────────────────────────────────
103
+ /** Add one Cedar policy inline. Chainable. (In networked mode policies are
104
+ * managed by the control plane and this is ignored, with a one-time warning.) */
105
+ allow(cedarCode, name) {
106
+ this._backend.addPolicy({ name: name ?? `policy-${this._policyCount}`, code: cedarCode });
107
+ this._policyCount += 1;
108
+ return this;
109
+ }
110
+ /** Load policies from a JSON file — a list of `{name, code}` (or
111
+ * `{policies:[...]}`). Fail-closed: a missing file loads nothing, so every
112
+ * governed call is denied until a policy permits it. Chainable. */
113
+ load(file) {
114
+ if (!fs.existsSync(file))
115
+ return this;
116
+ const data = JSON.parse(fs.readFileSync(file, "utf8"));
117
+ const entries = Array.isArray(data)
118
+ ? data
119
+ : (data.policies ?? []);
120
+ for (const e of entries)
121
+ this.allow(e.code, e.name);
122
+ return this;
123
+ }
124
+ // ── sub-agent scope attenuation ───────────────────────────────────
125
+ /** Create a root capability scope for this agent, from which sub-agent scopes
126
+ * are attenuated (strict-subset). Async because the engine initializes
127
+ * lazily; `attenuate()` on the returned scope is synchronous. The Developer
128
+ * Edition governs the tree up to depth {@link DE_MAX_DEPTH}. */
129
+ async scope(opts = {}) {
130
+ const eng = this._backend.engine();
131
+ if (!eng) {
132
+ throw new Error("sub-agent attenuation runs in-process; with WATCHLIGHT_APDP_URL set it is " +
133
+ "enforced by the control plane server-side. Use the Enterprise API for networked attenuation.");
134
+ }
135
+ const engine = await eng;
136
+ const root = new attenuation_1.Scope({
137
+ engine,
138
+ auditPath: this._auditPath,
139
+ agent: this.agent,
140
+ allowedTools: norm(opts.tools),
141
+ allowedResources: norm(opts.resources),
142
+ allowedIntents: norm(opts.intents),
143
+ maxDepth: Math.min(opts.maxDepth ?? attenuation_1.DE_MAX_DEPTH, attenuation_1.DE_MAX_DEPTH),
144
+ timeBudgetSeconds: opts.timeBudgetSeconds ?? 3600,
145
+ depth: 0,
146
+ });
147
+ root.emitRoot();
148
+ return root;
149
+ }
150
+ // ── governing tools ───────────────────────────────────────────────
151
+ /**
152
+ * Wrap a function as a governed tool with the given `intent`. On every call
153
+ * the engine authorizes `(agent, intent, tool/<name>)`. On ALLOW the function
154
+ * runs; on anything else a {@link Denied} is thrown and the body never
155
+ * executes. The returned function is async.
156
+ */
157
+ tool(fn, opts) {
158
+ const intent = opts.intent;
159
+ const name = fn.name || "anonymous";
160
+ const resource = `tool/${name}`;
161
+ return async (...args) => {
162
+ const [decision, reason] = await this._authorize(intent, resource);
163
+ this._audit(intent, resource, decision, reason);
164
+ if (decision !== "Allow") {
165
+ throw new Denied(name, intent, reason || "no matching policy");
166
+ }
167
+ return (await fn(...args));
168
+ };
169
+ }
170
+ /**
171
+ * Authorize a raw `(intent, tool)` pair, audit the decision, and return it.
172
+ * Fail-closed. Used by framework adapters (e.g. the Claude Agent SDK hooks)
173
+ * that gate tool calls themselves rather than wrapping the function — the
174
+ * decision is identical to {@link tool}, just without running a body.
175
+ */
176
+ async check(intent, toolName) {
177
+ const resource = `tool/${toolName}`;
178
+ const [decision, reason] = await this._authorize(intent, resource);
179
+ this._audit(intent, resource, decision, reason);
180
+ return { allowed: decision === "Allow", decision, reason };
181
+ }
182
+ // ── internals ─────────────────────────────────────────────────────
183
+ async _authorize(intent, resource) {
184
+ const { decision, reason } = await this._backend.authorize({
185
+ principal: this.agent,
186
+ action: intent,
187
+ resource,
188
+ context: {},
189
+ });
190
+ return [decision, reason];
191
+ }
192
+ _announce() {
193
+ if (!this._announced) {
194
+ // eslint-disable-next-line no-console
195
+ console.log(`watchlight: governing '${this.agent}' (${this._backend.label})`);
196
+ this._announced = true;
197
+ }
198
+ }
199
+ _audit(intent, resource, decision, reason) {
200
+ this._announce();
201
+ const allowed = decision === "Allow";
202
+ const tag = allowed ? "ALLOW" : "DENY";
203
+ const trailer = allowed ? "" : ` ${reason || "no matching policy"}`;
204
+ // eslint-disable-next-line no-console
205
+ console.log(`watchlight: ${tag.padEnd(5)} ${intent.padEnd(9)} ${resource}${trailer}`);
206
+ // Value-free audit: argument VALUES never enter the trail — only the
207
+ // governance decision. Mirrors the production audit contract.
208
+ const record = {
209
+ ts: new Date().toISOString(),
210
+ agent: this.agent,
211
+ intent,
212
+ resource,
213
+ decision,
214
+ };
215
+ try {
216
+ fs.mkdirSync(path.dirname(this._auditPath), { recursive: true });
217
+ fs.appendFileSync(this._auditPath, JSON.stringify(record) + "\n", "utf8");
218
+ }
219
+ catch {
220
+ // Audit is best-effort in dev mode; never let it break the app.
221
+ }
222
+ }
223
+ }
224
+ exports.Watchlight = Watchlight;
225
+ /** A ready-to-use default governor so `import { govern } from "@watchlight/sdk"`
226
+ * just works. Starts with NO policies — fail-closed — until you `govern.load()`
227
+ * a file or `govern.allow()` a policy inline. */
228
+ exports.govern = new Watchlight();
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@watchlight/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Watchlight Developer Edition govern glue for Node/TypeScript — declare intent, govern a tool with a fail-closed in-process policy decision, and get a value-free audit trail. Glue over @watchlight/engine; zero decision logic in JS.",
5
+ "type": "commonjs",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "LICENSE",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "test": "npm run build && node test/govern.test.mjs && node test/claude-agent.test.mjs && node test/graduation.test.mjs",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "keywords": [
22
+ "authorization",
23
+ "cedar",
24
+ "policy",
25
+ "agent",
26
+ "governance",
27
+ "watchlight",
28
+ "ai-agents"
29
+ ],
30
+ "dependencies": {
31
+ "@watchlight/engine": "^0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.5.0",
35
+ "@types/node": "^20.0.0"
36
+ },
37
+ "license": "Apache-2.0",
38
+ "homepage": "https://www.watchlight.ai",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/watchlight-ai-beacon/watchlight-de.git",
42
+ "directory": "ts"
43
+ },
44
+ "bugs": {
45
+ "url": "https://github.com/watchlight-ai-beacon/watchlight-de/issues"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }