@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 +201 -0
- package/README.md +117 -0
- package/dist/attenuation.d.ts +63 -0
- package/dist/attenuation.js +207 -0
- package/dist/backend.d.ts +61 -0
- package/dist/backend.js +114 -0
- package/dist/claude-agent-types.d.ts +29 -0
- package/dist/claude-agent-types.js +6 -0
- package/dist/claude-agent.d.ts +21 -0
- package/dist/claude-agent.js +56 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.js +228 -0
- package/package.json +50 -0
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;
|
package/dist/backend.js
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|