netzilo 4.3.14

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/dist/trust.js ADDED
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.installTrust = installTrust;
37
+ exports.installRouting = installRouting;
38
+ exports.uninstallRouting = uninstallRouting;
39
+ /**
40
+ * In-process CA trust + traffic routing for the embedded MITM proxy.
41
+ *
42
+ * The MITM proxy signs intercepted TLS with a per-install Netzilo CA (written
43
+ * by the native client to <configDir>/netzilo-ca.pem when advanced governance
44
+ * is on). For the host process to accept those connections, the process's TLS
45
+ * stack must trust that CA, and its outbound traffic must actually reach the
46
+ * proxy:
47
+ *
48
+ * CA trust:
49
+ * - There is no reliable, Node-version-independent way to change the
50
+ * DEFAULT trust store for `fetch()`/undici at runtime. The mechanism
51
+ * that actually works, confirmed against a real intercepted LLM call:
52
+ * pass the CA directly into undici's EnvHttpProxyAgent via its
53
+ * `connect`/`requestTls` options (both accept the same shape as
54
+ * `tls.ConnectionOptions`, including `ca`) — see installRouting()
55
+ * below, which is where the CA is actually consumed. NODE_EXTRA_CA_CERTS
56
+ * is still set as a best-effort extra (helps subprocesses that inherit
57
+ * the env; Node only guarantees it's read at process bootstrap, so it
58
+ * is NOT relied on for this process's own fetch() calls).
59
+ *
60
+ * Traffic routing:
61
+ * - Most Node HTTP clients do NOT read HTTP_PROXY/HTTPS_PROXY
62
+ * automatically. We (a) set the env vars anyway, which covers
63
+ * subprocesses (curl, git) and any library that opts into
64
+ * proxy-from-env, and (b) — when the optional `undici` peer dependency
65
+ * is present — install `EnvHttpProxyAgent` as undici's global
66
+ * dispatcher, which IS the mechanism Node's global `fetch` honors. Note
67
+ * this does not cover libraries that build their own http.Agent without
68
+ * consulting undici or the proxy env vars.
69
+ *
70
+ * IMPORTANT for consumers: `undici` must be resolvable from THIS
71
+ * package's own require() — i.e. present in a node_modules directory
72
+ * that is an ancestor of where netzilo itself is installed. A normal
73
+ * `npm install undici` in a real consuming project satisfies this via
74
+ * npm's normal hoisting; it does NOT work if netzilo is linked in via a
75
+ * nested/isolated layout (e.g. a local `file:` dependency, or pnpm's
76
+ * strict non-hoisted node_modules) unless undici is installed at the
77
+ * location netzilo's own require() resolves from.
78
+ */
79
+ const fs = __importStar(require("fs"));
80
+ const path = __importStar(require("path"));
81
+ const tls = __importStar(require("tls"));
82
+ let trustApplied = false;
83
+ // The combined (public roots + Netzilo CA) PEM content, read by
84
+ // installRouting() to configure undici's TLS trust for the MITM'd traffic.
85
+ // This — not any global Node API — is the actual trust mechanism that works.
86
+ let combinedCaPem = null;
87
+ const CA_FILENAME = "netzilo-ca.pem";
88
+ function readFile(p) {
89
+ try {
90
+ return fs.readFileSync(p);
91
+ }
92
+ catch {
93
+ return Buffer.alloc(0);
94
+ }
95
+ }
96
+ function normalize(pem) {
97
+ // Whitespace-insensitive key for per-cert dedupe (base64 body + markers).
98
+ return pem.toString("utf8").replace(/\s+/g, "");
99
+ }
100
+ function contains(haystack, needle) {
101
+ if (!needle.toString("utf8").trim()) {
102
+ return false;
103
+ }
104
+ return normalize(haystack).includes(normalize(needle));
105
+ }
106
+ function sleep(ms) {
107
+ return new Promise((resolve) => setTimeout(resolve, ms));
108
+ }
109
+ /**
110
+ * Make the current process trust the MITM CA. Idempotent; no-op if the CA
111
+ * isn't present (MITM disabled). Never throws — trust setup must not break
112
+ * the agent. Returns true if trust was applied.
113
+ */
114
+ async function installTrust(configDir, waitSecs = 10.0) {
115
+ if (trustApplied) {
116
+ return true;
117
+ }
118
+ try {
119
+ const caPath = path.join(configDir, CA_FILENAME);
120
+ // The native client writes the CA asynchronously during startup, so wait
121
+ // briefly for it when MITM is enabled.
122
+ const deadline = Date.now() + Math.max(0, waitSecs) * 1000;
123
+ while (!fs.existsSync(caPath) && Date.now() < deadline) {
124
+ await sleep(250);
125
+ }
126
+ const caPem = readFile(caPath);
127
+ if (!caPem.toString("utf8").trim()) {
128
+ // eslint-disable-next-line no-console
129
+ console.log(`netzilo.trust: no MITM CA at ${caPath} — skipping (MITM off?)`);
130
+ return false;
131
+ }
132
+ // Build a combined bundle = Node's default roots + the Netzilo CA, so we
133
+ // never narrow trust to the CA alone.
134
+ const defaultRoots = tls.rootCertificates ? tls.rootCertificates.join("\n") : "";
135
+ const combined = contains(Buffer.from(defaultRoots), caPem)
136
+ ? defaultRoots
137
+ : `${defaultRoots}\n${caPem.toString("utf8")}`;
138
+ const combinedPath = path.join(configDir, "ca-bundle.pem");
139
+ try {
140
+ const tmp = `${combinedPath}.tmp`;
141
+ fs.writeFileSync(tmp, combined);
142
+ fs.renameSync(tmp, combinedPath);
143
+ }
144
+ catch (exc) {
145
+ // eslint-disable-next-line no-console
146
+ console.warn("netzilo.trust: cannot write combined bundle:", exc);
147
+ }
148
+ // The actual trust mechanism for this process's own fetch()/undici calls:
149
+ // stash the combined PEM for installRouting() to hand to undici's
150
+ // EnvHttpProxyAgent via its connect/requestTls options. See the module
151
+ // comment for why this — not a global Node TLS API — is what works.
152
+ combinedCaPem = combined;
153
+ // Best-effort extras: cover subprocesses (curl, git, a spawned scripting
154
+ // tool) that read these at their own startup, and this process's own
155
+ // fetch() IF this module happens to load before any TLS context is
156
+ // created (Node's docs say NODE_EXTRA_CA_CERTS is read at bootstrap, so
157
+ // this is not relied upon — installRouting()'s explicit `ca` option is).
158
+ process.env.NODE_EXTRA_CA_CERTS = caPath;
159
+ process.env.SSL_CERT_FILE = combinedPath;
160
+ process.env.REQUESTS_CA_BUNDLE = combinedPath; // covers spawned Python-based tools
161
+ process.env.CURL_CA_BUNDLE = combinedPath; // covers spawned curl
162
+ trustApplied = true;
163
+ return true;
164
+ }
165
+ catch (exc) {
166
+ // eslint-disable-next-line no-console
167
+ console.warn("netzilo.trust: install failed (MITM HTTPS may not verify):", exc);
168
+ return false;
169
+ }
170
+ }
171
+ let routed = false;
172
+ // Original values of the env vars we override, captured at install time so
173
+ // uninstallRouting() can restore them exactly (undefined = was unset).
174
+ const priorProxyEnv = {};
175
+ const PROXY_VARS = [
176
+ "HTTP_PROXY",
177
+ "http_proxy",
178
+ "HTTPS_PROXY",
179
+ "https_proxy",
180
+ "ALL_PROXY",
181
+ "all_proxy",
182
+ "NO_PROXY",
183
+ "no_proxy",
184
+ ];
185
+ // Hosts that must NEVER be proxied: localhost (the proxy + gateway live here)
186
+ // and the Netzilo CONTROL PLANE (.netzilo.com — management/admin/signal).
187
+ // Routing those through our own unified proxy would loop.
188
+ //
189
+ // NOTE: .netzilo.network (ZTNA peer/resource FQDNs) is deliberately NOT
190
+ // excluded — those MUST go through the proxy so the native client resolves
191
+ // the magic-DNS name and tunnels the connection to the peer.
192
+ const NO_PROXY_BASE = "127.0.0.1,localhost,::1,.netzilo.com";
193
+ /**
194
+ * Route this process's (and subprocesses') traffic through the unified proxy
195
+ * so it can be governed. Sets standard proxy env vars (covers subprocesses
196
+ * and any proxy-env-aware library) and, when the optional `undici` peer
197
+ * dependency is installed, wires undici's EnvHttpProxyAgent as the global
198
+ * dispatcher — the mechanism Node's global fetch() actually honors.
199
+ *
200
+ * Idempotent; never throws. Returns true if applied.
201
+ */
202
+ function installRouting(unifiedPort, extraNoProxy) {
203
+ if (routed) {
204
+ return true;
205
+ }
206
+ try {
207
+ if (!unifiedPort) {
208
+ return false;
209
+ }
210
+ const proxy = `http://127.0.0.1:${unifiedPort}`;
211
+ const socks = `socks5h://127.0.0.1:${unifiedPort}`;
212
+ let noProxy = NO_PROXY_BASE;
213
+ if (extraNoProxy) {
214
+ noProxy = `${noProxy},${extraNoProxy}`;
215
+ }
216
+ for (const existing of [process.env.NO_PROXY, process.env.no_proxy]) {
217
+ if (existing) {
218
+ noProxy = `${noProxy},${existing}`;
219
+ }
220
+ }
221
+ for (const v of PROXY_VARS) {
222
+ priorProxyEnv[v] = process.env[v];
223
+ }
224
+ process.env.HTTPS_PROXY = proxy;
225
+ process.env.https_proxy = proxy;
226
+ process.env.ALL_PROXY = socks;
227
+ process.env.all_proxy = socks;
228
+ // Drop any inherited HTTP_PROXY so plaintext HTTP routes via SOCKS5
229
+ // instead (a plaintext GET through the HTTP-CONNECT proxy produces no
230
+ // graph edge; SOCKS5 yields a CONNECTS edge instead).
231
+ delete process.env.HTTP_PROXY;
232
+ delete process.env.http_proxy;
233
+ process.env.NO_PROXY = noProxy;
234
+ process.env.no_proxy = noProxy;
235
+ // Wire Node's global fetch() (undici under the hood) to honor the env
236
+ // vars just set, AND to trust the MITM CA for the tunneled TLS connection
237
+ // — undici does its own certificate verification independent of Node's
238
+ // global tls settings, so the CA has to be handed to it directly via
239
+ // connect/requestTls (both accept the same shape as tls.ConnectionOptions,
240
+ // confirmed against undici's own type definitions). Optional: only
241
+ // attempted if `undici` is resolvable.
242
+ try {
243
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
244
+ const undici = require("undici");
245
+ if (undici.EnvHttpProxyAgent && undici.setGlobalDispatcher) {
246
+ const tlsOpts = combinedCaPem ? { ca: combinedCaPem } : undefined;
247
+ undici.setGlobalDispatcher(new undici.EnvHttpProxyAgent(tlsOpts ? { connect: tlsOpts, requestTls: tlsOpts } : undefined));
248
+ // eslint-disable-next-line no-console
249
+ console.log(`netzilo.trust: wired undici EnvHttpProxyAgent as global fetch dispatcher${tlsOpts ? " (with MITM CA trust)" : " (no MITM CA available — TLS verification may fail)"}`);
250
+ }
251
+ }
252
+ catch {
253
+ // eslint-disable-next-line no-console
254
+ console.log("netzilo.trust: `undici` not resolvable — proxy env vars are set (covers " +
255
+ "subprocesses and proxy-aware libraries), but Node's global fetch() may " +
256
+ "bypass the proxy. `npm install undici` (resolvable from netzilo's own " +
257
+ "require() — see module comment) to also cover fetch().");
258
+ }
259
+ routed = true;
260
+ // eslint-disable-next-line no-console
261
+ console.log(`netzilo.trust: routing HTTPS via ${proxy}, all else via ${socks} (NO_PROXY=${noProxy})`);
262
+ return true;
263
+ }
264
+ catch (exc) {
265
+ // eslint-disable-next-line no-console
266
+ console.warn("netzilo.trust: routing setup failed:", exc);
267
+ return false;
268
+ }
269
+ }
270
+ /**
271
+ * Restore the proxy env vars to their pre-install values. Called on stop() so
272
+ * a long-lived host process that keeps running after stop() does not keep
273
+ * routing traffic to the (now closed) egress port. Idempotent; never throws.
274
+ */
275
+ function uninstallRouting() {
276
+ if (!routed) {
277
+ return;
278
+ }
279
+ try {
280
+ for (const [v, orig] of Object.entries(priorProxyEnv)) {
281
+ if (orig === undefined) {
282
+ delete process.env[v];
283
+ }
284
+ else {
285
+ process.env[v] = orig;
286
+ }
287
+ }
288
+ }
289
+ catch (exc) {
290
+ // eslint-disable-next-line no-console
291
+ console.warn("netzilo.trust: routing teardown failed:", exc);
292
+ }
293
+ finally {
294
+ for (const k of Object.keys(priorProxyEnv)) {
295
+ delete priorProxyEnv[k];
296
+ }
297
+ routed = false;
298
+ }
299
+ }
@@ -0,0 +1,22 @@
1
+ export declare class NetziloBlockedError extends Error {
2
+ toolName: string;
3
+ reason: string;
4
+ constructor(toolName: string, reason: string);
5
+ }
6
+ export interface WrapToolOptions {
7
+ /** Attribution tag for events (default: "node-agent"). */
8
+ source?: string;
9
+ /** If false, a scanner error re-throws instead of failing open. Default true. */
10
+ failOpen?: boolean;
11
+ }
12
+ /**
13
+ * Wrap an async tool function with pre-call gating and post-call reporting.
14
+ *
15
+ * - Before the call: runs isAllowed(name, args) — throws NetziloBlockedError
16
+ * if the policy blocks it.
17
+ * - After the call: string results are passed through checkResponse() so
18
+ * redaction rules (e.g. AWS-key / PII scrubbing) apply to tool OUTPUT before
19
+ * the agent's LLM ever sees it; non-string results are reported as-is via
20
+ * reportResult() for observability without being altered.
21
+ */
22
+ export declare function wrapTool<Args extends Record<string, unknown>, Result>(name: string, fn: (args: Args) => Promise<Result>, opts?: WrapToolOptions): (args: Args) => Promise<Result>;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NetziloBlockedError = void 0;
4
+ exports.wrapTool = wrapTool;
5
+ /**
6
+ * netzilo/wrapTool — framework-agnostic tool gating.
7
+ *
8
+ * Every Node agent framework's "tool" boils down to an async function the
9
+ * model calls with some arguments. Rather than bind to one framework's
10
+ * specific hook API (which drifts release to release — LangChain.js,
11
+ * LlamaIndex.TS, the Vercel AI SDK, and raw OpenAI/Anthropic tool-use loops
12
+ * all shape this differently), wrapTool() gates at that common boundary: wrap
13
+ * your tool function once, and every call is pre-checked (block/redact) and
14
+ * post-reported through the same in-process scanner + AIDR pipeline used
15
+ * throughout this SDK.
16
+ *
17
+ * import { wrapTool } from "netzilo/wrapTool";
18
+ *
19
+ * const getOrderStatus = wrapTool(
20
+ * "get_order_status",
21
+ * async (args: { orderId: string }) => fetchOrder(args.orderId)
22
+ * );
23
+ *
24
+ * // register `getOrderStatus` with LangChain's tool(), the Vercel AI
25
+ * // SDK's tool(), or any framework's tool registry exactly as you would
26
+ * // the original function — the governance is transparent to the caller.
27
+ *
28
+ * A blocked call throws NetziloBlockedError instead of invoking the wrapped
29
+ * function; a redacted call proceeds with the wrapped function's OWN return
30
+ * value passed back through checkResponse() so text output can be sanitized
31
+ * too.
32
+ */
33
+ const index_1 = require("./index");
34
+ class NetziloBlockedError extends Error {
35
+ constructor(toolName, reason) {
36
+ super(`netzilo blocked tool call "${toolName}": ${reason || "policy violation"}`);
37
+ this.name = "NetziloBlockedError";
38
+ this.toolName = toolName;
39
+ this.reason = reason;
40
+ }
41
+ }
42
+ exports.NetziloBlockedError = NetziloBlockedError;
43
+ /**
44
+ * Wrap an async tool function with pre-call gating and post-call reporting.
45
+ *
46
+ * - Before the call: runs isAllowed(name, args) — throws NetziloBlockedError
47
+ * if the policy blocks it.
48
+ * - After the call: string results are passed through checkResponse() so
49
+ * redaction rules (e.g. AWS-key / PII scrubbing) apply to tool OUTPUT before
50
+ * the agent's LLM ever sees it; non-string results are reported as-is via
51
+ * reportResult() for observability without being altered.
52
+ */
53
+ function wrapTool(name, fn, opts = {}) {
54
+ const source = opts.source ?? "node-agent";
55
+ const failOpen = opts.failOpen ?? true;
56
+ return async (args) => {
57
+ try {
58
+ const { allowed, reason } = (0, index_1.isAllowed)(name, args, { source });
59
+ if (!allowed) {
60
+ throw new NetziloBlockedError(name, reason);
61
+ }
62
+ }
63
+ catch (err) {
64
+ if (err instanceof NetziloBlockedError || !failOpen) {
65
+ throw err;
66
+ }
67
+ // Scanner not running / transient error: fail open rather than break
68
+ // the agent, matching evaluate()'s own fail-open contract.
69
+ }
70
+ const result = await fn(args);
71
+ try {
72
+ if (typeof result === "string") {
73
+ const { redactedResponse } = (0, index_1.checkResponse)(result, { source });
74
+ if (redactedResponse !== null) {
75
+ return redactedResponse;
76
+ }
77
+ (0, index_1.reportResult)(name, result, { source });
78
+ }
79
+ else {
80
+ (0, index_1.reportResult)(name, JSON.stringify(result, (_k, v) => (v === undefined ? null : v)), {
81
+ source,
82
+ });
83
+ }
84
+ }
85
+ catch {
86
+ // Observability/redaction must never break the agent's happy path.
87
+ }
88
+ return result;
89
+ };
90
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "netzilo",
3
+ "version": "4.3.14",
4
+ "description": "Netzilo AI Detection & Response (AIDR) — governance for Node.js/TypeScript AI agents.",
5
+ "license": "Commercial",
6
+ "author": "Netzilo",
7
+ "keywords": [
8
+ "ai",
9
+ "agent",
10
+ "governance",
11
+ "security",
12
+ "aidr",
13
+ "netzilo"
14
+ ],
15
+ "homepage": "https://www.netzilo.com",
16
+ "type": "commonjs",
17
+ "main": "dist/index.js",
18
+ "types": "dist/index.d.ts",
19
+ "exports": {
20
+ ".": "./dist/index.js",
21
+ "./wrapTool": "./dist/wrapTool.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "koffi": "^2.9.0"
36
+ },
37
+ "optionalDependencies": {
38
+ "netzilo-darwin-arm64": "4.3.14",
39
+ "netzilo-darwin-x64": "4.3.14",
40
+ "netzilo-linux-arm64": "4.3.14",
41
+ "netzilo-linux-x64": "4.3.14",
42
+ "netzilo-windows-x64": "4.3.14"
43
+ },
44
+ "peerDependencies": {
45
+ "undici": "^8.5.0"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "undici": {
49
+ "optional": true
50
+ }
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^20.14.0",
54
+ "typescript": "^5.5.0"
55
+ }
56
+ }