@node9/proxy 1.0.14 → 1.0.16

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/index.js CHANGED
@@ -40,6 +40,9 @@ var import_prompts = require("@inquirer/prompts");
40
40
  var import_fs2 = __toESM(require("fs"));
41
41
  var import_path4 = __toESM(require("path"));
42
42
  var import_os2 = __toESM(require("os"));
43
+ var import_net = __toESM(require("net"));
44
+ var import_crypto = require("crypto");
45
+ var import_child_process2 = require("child_process");
43
46
  var import_picomatch = __toESM(require("picomatch"));
44
47
  var import_sh_syntax = require("sh-syntax");
45
48
 
@@ -390,7 +393,13 @@ var SmartConditionSchema = import_zod.z.object({
390
393
  ),
391
394
  value: import_zod.z.string().optional(),
392
395
  flags: import_zod.z.string().optional()
393
- });
396
+ }).refine(
397
+ (c) => {
398
+ if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
399
+ return true;
400
+ },
401
+ { message: "matchesGlob and notMatchesGlob conditions require a value field" }
402
+ );
394
403
  var SmartRuleSchema = import_zod.z.object({
395
404
  name: import_zod.z.string().optional(),
396
405
  tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
@@ -409,6 +418,7 @@ var ConfigFileSchema = import_zod.z.object({
409
418
  enableUndo: import_zod.z.boolean().optional(),
410
419
  enableHookLogDebug: import_zod.z.boolean().optional(),
411
420
  approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
421
+ flightRecorder: import_zod.z.boolean().optional(),
412
422
  approvers: import_zod.z.object({
413
423
  native: import_zod.z.boolean().optional(),
414
424
  browser: import_zod.z.boolean().optional(),
@@ -883,7 +893,7 @@ function evaluateSmartConditions(args, rule) {
883
893
  case "matchesGlob":
884
894
  return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
885
895
  case "notMatchesGlob":
886
- return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
896
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
887
897
  default:
888
898
  return false;
889
899
  }
@@ -994,15 +1004,17 @@ var DANGEROUS_WORDS = [
994
1004
  // permanently overwrites file contents (unrecoverable)
995
1005
  ];
996
1006
  var DEFAULT_CONFIG = {
1007
+ version: "1.0",
997
1008
  settings: {
998
- mode: "standard",
1009
+ mode: "audit",
999
1010
  autoStartDaemon: true,
1000
1011
  enableUndo: true,
1001
1012
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
1002
- enableHookLogDebug: false,
1003
- approvalTimeoutMs: 0,
1004
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
1005
- approvers: { native: true, browser: true, cloud: true, terminal: true }
1013
+ enableHookLogDebug: true,
1014
+ approvalTimeoutMs: 3e4,
1015
+ // 30-second auto-deny timeout
1016
+ flightRecorder: true,
1017
+ approvers: { native: true, browser: true, cloud: false, terminal: true }
1006
1018
  },
1007
1019
  policy: {
1008
1020
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -1313,13 +1325,23 @@ function isIgnoredTool(toolName) {
1313
1325
  var DAEMON_PORT = 7391;
1314
1326
  var DAEMON_HOST = "127.0.0.1";
1315
1327
  function isDaemonRunning() {
1328
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1329
+ if (import_fs2.default.existsSync(pidFile)) {
1330
+ try {
1331
+ const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1332
+ if (port !== DAEMON_PORT) return false;
1333
+ process.kill(pid, 0);
1334
+ return true;
1335
+ } catch {
1336
+ return false;
1337
+ }
1338
+ }
1316
1339
  try {
1317
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1318
- if (!import_fs2.default.existsSync(pidFile)) return false;
1319
- const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1320
- if (port !== DAEMON_PORT) return false;
1321
- process.kill(pid, 0);
1322
- return true;
1340
+ const r = (0, import_child_process2.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1341
+ encoding: "utf8",
1342
+ timeout: 500
1343
+ });
1344
+ return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1323
1345
  } catch {
1324
1346
  return false;
1325
1347
  }
@@ -1335,7 +1357,7 @@ function getPersistentDecision(toolName) {
1335
1357
  }
1336
1358
  return null;
1337
1359
  }
1338
- async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1360
+ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1339
1361
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1340
1362
  const checkCtrl = new AbortController();
1341
1363
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1350,6 +1372,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1350
1372
  args,
1351
1373
  agent: meta?.agent,
1352
1374
  mcpServer: meta?.mcpServer,
1375
+ fromCLI: true,
1376
+ // Pass the flight-recorder ID so the daemon uses the same UUID for
1377
+ // activity-result as the CLI used for the pending activity event.
1378
+ // Without this, the two UUIDs never match and tail.ts never resolves
1379
+ // the pending item.
1380
+ activityId,
1353
1381
  ...riskMetadata && { riskMetadata }
1354
1382
  }),
1355
1383
  signal: checkCtrl.signal
@@ -1404,7 +1432,45 @@ async function resolveViaDaemon(id, decision, internalToken) {
1404
1432
  signal: AbortSignal.timeout(3e3)
1405
1433
  });
1406
1434
  }
1435
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : import_path4.default.join(import_os2.default.tmpdir(), "node9-activity.sock");
1436
+ function notifyActivity(data) {
1437
+ return new Promise((resolve) => {
1438
+ try {
1439
+ const payload = JSON.stringify(data);
1440
+ const sock = import_net.default.createConnection(ACTIVITY_SOCKET_PATH);
1441
+ sock.on("connect", () => {
1442
+ sock.on("close", resolve);
1443
+ sock.end(payload);
1444
+ });
1445
+ sock.on("error", resolve);
1446
+ } catch {
1447
+ resolve();
1448
+ }
1449
+ });
1450
+ }
1407
1451
  async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1452
+ if (!options?.calledFromDaemon) {
1453
+ const actId = (0, import_crypto.randomUUID)();
1454
+ const actTs = Date.now();
1455
+ await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1456
+ const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1457
+ ...options,
1458
+ activityId: actId
1459
+ });
1460
+ if (!result.noApprovalMechanism) {
1461
+ await notifyActivity({
1462
+ id: actId,
1463
+ tool: toolName,
1464
+ ts: actTs,
1465
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
1466
+ label: result.blockedByLabel
1467
+ });
1468
+ }
1469
+ return result;
1470
+ }
1471
+ return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1472
+ }
1473
+ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1408
1474
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1409
1475
  const pauseState = checkPause();
1410
1476
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1440,6 +1506,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1440
1506
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1441
1507
  };
1442
1508
  }
1509
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
1443
1510
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1444
1511
  }
1445
1512
  }
@@ -1662,7 +1729,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1662
1729
  console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1663
1730
  `));
1664
1731
  }
1665
- const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1732
+ const daemonDecision = await askDaemon(
1733
+ toolName,
1734
+ args,
1735
+ meta,
1736
+ signal,
1737
+ riskMetadata,
1738
+ options?.activityId
1739
+ );
1666
1740
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
1667
1741
  const isApproved = daemonDecision === "allow";
1668
1742
  return {
@@ -1866,7 +1940,10 @@ function getConfig() {
1866
1940
  for (const rule of shield.smartRules) {
1867
1941
  if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
1868
1942
  }
1869
- for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
1943
+ const existingWords = new Set(mergedPolicy.dangerousWords);
1944
+ for (const word of shield.dangerousWords) {
1945
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
1946
+ }
1870
1947
  }
1871
1948
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
1872
1949
  for (const rule of ADVISORY_SMART_RULES) {
package/dist/index.mjs CHANGED
@@ -4,6 +4,9 @@ import { confirm } from "@inquirer/prompts";
4
4
  import fs2 from "fs";
5
5
  import path4 from "path";
6
6
  import os2 from "os";
7
+ import net from "net";
8
+ import { randomUUID } from "crypto";
9
+ import { spawnSync } from "child_process";
7
10
  import pm from "picomatch";
8
11
  import { parse } from "sh-syntax";
9
12
 
@@ -354,7 +357,13 @@ var SmartConditionSchema = z.object({
354
357
  ),
355
358
  value: z.string().optional(),
356
359
  flags: z.string().optional()
357
- });
360
+ }).refine(
361
+ (c) => {
362
+ if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
363
+ return true;
364
+ },
365
+ { message: "matchesGlob and notMatchesGlob conditions require a value field" }
366
+ );
358
367
  var SmartRuleSchema = z.object({
359
368
  name: z.string().optional(),
360
369
  tool: z.string().min(1, "Smart rule tool must not be empty"),
@@ -373,6 +382,7 @@ var ConfigFileSchema = z.object({
373
382
  enableUndo: z.boolean().optional(),
374
383
  enableHookLogDebug: z.boolean().optional(),
375
384
  approvalTimeoutMs: z.number().nonnegative().optional(),
385
+ flightRecorder: z.boolean().optional(),
376
386
  approvers: z.object({
377
387
  native: z.boolean().optional(),
378
388
  browser: z.boolean().optional(),
@@ -847,7 +857,7 @@ function evaluateSmartConditions(args, rule) {
847
857
  case "matchesGlob":
848
858
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
849
859
  case "notMatchesGlob":
850
- return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
860
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
851
861
  default:
852
862
  return false;
853
863
  }
@@ -958,15 +968,17 @@ var DANGEROUS_WORDS = [
958
968
  // permanently overwrites file contents (unrecoverable)
959
969
  ];
960
970
  var DEFAULT_CONFIG = {
971
+ version: "1.0",
961
972
  settings: {
962
- mode: "standard",
973
+ mode: "audit",
963
974
  autoStartDaemon: true,
964
975
  enableUndo: true,
965
976
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
966
- enableHookLogDebug: false,
967
- approvalTimeoutMs: 0,
968
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
969
- approvers: { native: true, browser: true, cloud: true, terminal: true }
977
+ enableHookLogDebug: true,
978
+ approvalTimeoutMs: 3e4,
979
+ // 30-second auto-deny timeout
980
+ flightRecorder: true,
981
+ approvers: { native: true, browser: true, cloud: false, terminal: true }
970
982
  },
971
983
  policy: {
972
984
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -1277,13 +1289,23 @@ function isIgnoredTool(toolName) {
1277
1289
  var DAEMON_PORT = 7391;
1278
1290
  var DAEMON_HOST = "127.0.0.1";
1279
1291
  function isDaemonRunning() {
1292
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1293
+ if (fs2.existsSync(pidFile)) {
1294
+ try {
1295
+ const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1296
+ if (port !== DAEMON_PORT) return false;
1297
+ process.kill(pid, 0);
1298
+ return true;
1299
+ } catch {
1300
+ return false;
1301
+ }
1302
+ }
1280
1303
  try {
1281
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1282
- if (!fs2.existsSync(pidFile)) return false;
1283
- const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1284
- if (port !== DAEMON_PORT) return false;
1285
- process.kill(pid, 0);
1286
- return true;
1304
+ const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1305
+ encoding: "utf8",
1306
+ timeout: 500
1307
+ });
1308
+ return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1287
1309
  } catch {
1288
1310
  return false;
1289
1311
  }
@@ -1299,7 +1321,7 @@ function getPersistentDecision(toolName) {
1299
1321
  }
1300
1322
  return null;
1301
1323
  }
1302
- async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1324
+ async function askDaemon(toolName, args, meta, signal, riskMetadata, activityId) {
1303
1325
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
1304
1326
  const checkCtrl = new AbortController();
1305
1327
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -1314,6 +1336,12 @@ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
1314
1336
  args,
1315
1337
  agent: meta?.agent,
1316
1338
  mcpServer: meta?.mcpServer,
1339
+ fromCLI: true,
1340
+ // Pass the flight-recorder ID so the daemon uses the same UUID for
1341
+ // activity-result as the CLI used for the pending activity event.
1342
+ // Without this, the two UUIDs never match and tail.ts never resolves
1343
+ // the pending item.
1344
+ activityId,
1317
1345
  ...riskMetadata && { riskMetadata }
1318
1346
  }),
1319
1347
  signal: checkCtrl.signal
@@ -1368,7 +1396,45 @@ async function resolveViaDaemon(id, decision, internalToken) {
1368
1396
  signal: AbortSignal.timeout(3e3)
1369
1397
  });
1370
1398
  }
1399
+ var ACTIVITY_SOCKET_PATH = process.platform === "win32" ? "\\\\.\\pipe\\node9-activity" : path4.join(os2.tmpdir(), "node9-activity.sock");
1400
+ function notifyActivity(data) {
1401
+ return new Promise((resolve) => {
1402
+ try {
1403
+ const payload = JSON.stringify(data);
1404
+ const sock = net.createConnection(ACTIVITY_SOCKET_PATH);
1405
+ sock.on("connect", () => {
1406
+ sock.on("close", resolve);
1407
+ sock.end(payload);
1408
+ });
1409
+ sock.on("error", resolve);
1410
+ } catch {
1411
+ resolve();
1412
+ }
1413
+ });
1414
+ }
1371
1415
  async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
1416
+ if (!options?.calledFromDaemon) {
1417
+ const actId = randomUUID();
1418
+ const actTs = Date.now();
1419
+ await notifyActivity({ id: actId, ts: actTs, tool: toolName, args, status: "pending" });
1420
+ const result = await _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, {
1421
+ ...options,
1422
+ activityId: actId
1423
+ });
1424
+ if (!result.noApprovalMechanism) {
1425
+ await notifyActivity({
1426
+ id: actId,
1427
+ tool: toolName,
1428
+ ts: actTs,
1429
+ status: result.approved ? "allow" : result.blockedByLabel?.includes("DLP") ? "dlp" : "block",
1430
+ label: result.blockedByLabel
1431
+ });
1432
+ }
1433
+ return result;
1434
+ }
1435
+ return _authorizeHeadlessCore(toolName, args, allowTerminalFallback, meta, options);
1436
+ }
1437
+ async function _authorizeHeadlessCore(toolName, args, allowTerminalFallback = false, meta, options) {
1372
1438
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
1373
1439
  const pauseState = checkPause();
1374
1440
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -1404,6 +1470,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1404
1470
  blockedByLabel: "\u{1F6A8} Node9 DLP (Secret Detected)"
1405
1471
  };
1406
1472
  }
1473
+ if (!isManual) appendLocalAudit(toolName, args, "allow", "dlp-review-flagged", meta);
1407
1474
  explainableLabel = "\u{1F6A8} Node9 DLP (Credential Review)";
1408
1475
  }
1409
1476
  }
@@ -1626,7 +1693,14 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
1626
1693
  console.error(chalk2.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
1627
1694
  `));
1628
1695
  }
1629
- const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
1696
+ const daemonDecision = await askDaemon(
1697
+ toolName,
1698
+ args,
1699
+ meta,
1700
+ signal,
1701
+ riskMetadata,
1702
+ options?.activityId
1703
+ );
1630
1704
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
1631
1705
  const isApproved = daemonDecision === "allow";
1632
1706
  return {
@@ -1830,7 +1904,10 @@ function getConfig() {
1830
1904
  for (const rule of shield.smartRules) {
1831
1905
  if (!existingRuleNames.has(rule.name)) mergedPolicy.smartRules.push(rule);
1832
1906
  }
1833
- for (const word of shield.dangerousWords) mergedPolicy.dangerousWords.push(word);
1907
+ const existingWords = new Set(mergedPolicy.dangerousWords);
1908
+ for (const word of shield.dangerousWords) {
1909
+ if (!existingWords.has(word)) mergedPolicy.dangerousWords.push(word);
1910
+ }
1834
1911
  }
1835
1912
  const existingAdvisoryNames = new Set(mergedPolicy.smartRules.map((r) => r.name));
1836
1913
  for (const rule of ADVISORY_SMART_RULES) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",