@node9/proxy 1.0.7 → 1.0.9

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
@@ -38,24 +38,124 @@ module.exports = __toCommonJS(src_exports);
38
38
  var import_chalk2 = __toESM(require("chalk"));
39
39
  var import_prompts = require("@inquirer/prompts");
40
40
  var import_fs = __toESM(require("fs"));
41
- var import_path = __toESM(require("path"));
41
+ var import_path3 = __toESM(require("path"));
42
42
  var import_os = __toESM(require("os"));
43
43
  var import_picomatch = __toESM(require("picomatch"));
44
44
  var import_sh_syntax = require("sh-syntax");
45
45
 
46
46
  // src/ui/native.ts
47
47
  var import_child_process = require("child_process");
48
+ var import_path2 = __toESM(require("path"));
48
49
  var import_chalk = __toESM(require("chalk"));
49
- var isTestEnv = () => {
50
- return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
51
- };
50
+
51
+ // src/context-sniper.ts
52
+ var import_path = __toESM(require("path"));
52
53
  function smartTruncate(str, maxLen = 500) {
53
54
  if (str.length <= maxLen) return str;
54
55
  const edge = Math.floor(maxLen / 2) - 3;
55
56
  return `${str.slice(0, edge)} ... ${str.slice(-edge)}`;
56
57
  }
57
- function formatArgs(args) {
58
- if (args === null || args === void 0) return "(none)";
58
+ function extractContext(text, matchedWord) {
59
+ const lines = text.split("\n");
60
+ if (lines.length <= 7 || !matchedWord) {
61
+ return { snippet: smartTruncate(text, 500), lineIndex: -1 };
62
+ }
63
+ const escaped = matchedWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
64
+ const pattern = new RegExp(`\\b${escaped}\\b`, "i");
65
+ const allHits = lines.map((line, i) => ({ i, line })).filter(({ line }) => pattern.test(line));
66
+ if (allHits.length === 0) return { snippet: smartTruncate(text, 500), lineIndex: -1 };
67
+ const nonComment = allHits.find(({ line }) => {
68
+ const trimmed = line.trim();
69
+ return !trimmed.startsWith("//") && !trimmed.startsWith("#");
70
+ });
71
+ const hitIndex = (nonComment ?? allHits[0]).i;
72
+ const start = Math.max(0, hitIndex - 3);
73
+ const end = Math.min(lines.length, hitIndex + 4);
74
+ const lineIndex = hitIndex - start;
75
+ const snippet = lines.slice(start, end).map((line, i) => `${start + i === hitIndex ? "\u{1F6D1} " : " "}${line}`).join("\n");
76
+ const head = start > 0 ? `... [${start} lines hidden] ...
77
+ ` : "";
78
+ const tail = end < lines.length ? `
79
+ ... [${lines.length - end} lines hidden] ...` : "";
80
+ return { snippet: `${head}${snippet}${tail}`, lineIndex };
81
+ }
82
+ var CODE_KEYS = [
83
+ "command",
84
+ "cmd",
85
+ "shell_command",
86
+ "bash_command",
87
+ "script",
88
+ "code",
89
+ "input",
90
+ "sql",
91
+ "query",
92
+ "arguments",
93
+ "args",
94
+ "param",
95
+ "params",
96
+ "text"
97
+ ];
98
+ function computeRiskMetadata(args, tier, blockedByLabel, matchedField, matchedWord, ruleName) {
99
+ let intent = "EXEC";
100
+ let contextSnippet;
101
+ let contextLineIndex;
102
+ let editFileName;
103
+ let editFilePath;
104
+ let parsed = args;
105
+ if (typeof args === "string") {
106
+ const trimmed = args.trim();
107
+ if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
108
+ try {
109
+ parsed = JSON.parse(trimmed);
110
+ } catch {
111
+ }
112
+ }
113
+ }
114
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
115
+ const obj = parsed;
116
+ if (obj.old_string !== void 0 && obj.new_string !== void 0) {
117
+ intent = "EDIT";
118
+ if (obj.file_path) {
119
+ editFilePath = String(obj.file_path);
120
+ editFileName = import_path.default.basename(editFilePath);
121
+ }
122
+ const result = extractContext(String(obj.new_string), matchedWord);
123
+ contextSnippet = result.snippet;
124
+ if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
125
+ } else if (matchedField && obj[matchedField] !== void 0) {
126
+ const result = extractContext(String(obj[matchedField]), matchedWord);
127
+ contextSnippet = result.snippet;
128
+ if (result.lineIndex >= 0) contextLineIndex = result.lineIndex;
129
+ } else {
130
+ const foundKey = Object.keys(obj).find((k) => CODE_KEYS.includes(k.toLowerCase()));
131
+ if (foundKey) {
132
+ const val = obj[foundKey];
133
+ contextSnippet = smartTruncate(typeof val === "string" ? val : JSON.stringify(val), 500);
134
+ }
135
+ }
136
+ } else if (typeof parsed === "string") {
137
+ contextSnippet = smartTruncate(parsed, 500);
138
+ }
139
+ return {
140
+ intent,
141
+ tier,
142
+ blockedByLabel,
143
+ ...matchedWord && { matchedWord },
144
+ ...matchedField && { matchedField },
145
+ ...contextSnippet !== void 0 && { contextSnippet },
146
+ ...contextLineIndex !== void 0 && { contextLineIndex },
147
+ ...editFileName && { editFileName },
148
+ ...editFilePath && { editFilePath },
149
+ ...ruleName && { ruleName }
150
+ };
151
+ }
152
+
153
+ // src/ui/native.ts
154
+ var isTestEnv = () => {
155
+ return process.env.NODE_ENV === "test" || process.env.VITEST === "true" || !!process.env.VITEST || process.env.CI === "true" || !!process.env.CI || process.env.NODE9_TESTING === "1";
156
+ };
157
+ function formatArgs(args, matchedField, matchedWord) {
158
+ if (args === null || args === void 0) return { message: "(none)", intent: "EXEC" };
59
159
  let parsed = args;
60
160
  if (typeof args === "string") {
61
161
  const trimmed = args.trim();
@@ -66,11 +166,39 @@ function formatArgs(args) {
66
166
  parsed = args;
67
167
  }
68
168
  } else {
69
- return smartTruncate(args, 600);
169
+ return { message: smartTruncate(args, 600), intent: "EXEC" };
70
170
  }
71
171
  }
72
172
  if (typeof parsed === "object" && !Array.isArray(parsed)) {
73
173
  const obj = parsed;
174
+ if (obj.old_string !== void 0 && obj.new_string !== void 0) {
175
+ const file = obj.file_path ? import_path2.default.basename(String(obj.file_path)) : "file";
176
+ const oldPreview = smartTruncate(String(obj.old_string), 120);
177
+ const newPreview = extractContext(String(obj.new_string), matchedWord).snippet;
178
+ return {
179
+ intent: "EDIT",
180
+ message: `\u{1F4DD} EDITING: ${file}
181
+ \u{1F4C2} PATH: ${obj.file_path}
182
+
183
+ --- REPLACING ---
184
+ ${oldPreview}
185
+
186
+ +++ NEW CODE +++
187
+ ${newPreview}`
188
+ };
189
+ }
190
+ if (matchedField && obj[matchedField] !== void 0) {
191
+ const otherKeys = Object.keys(obj).filter((k) => k !== matchedField);
192
+ const context = otherKeys.length > 0 ? `\u2699\uFE0F Context: ${otherKeys.map((k) => `${k}=${smartTruncate(typeof obj[k] === "object" ? JSON.stringify(obj[k]) : String(obj[k]), 30)}`).join(", ")}
193
+
194
+ ` : "";
195
+ const content = extractContext(String(obj[matchedField]), matchedWord).snippet;
196
+ return {
197
+ intent: "EXEC",
198
+ message: `${context}\u{1F6D1} [${matchedField.toUpperCase()}]:
199
+ ${content}`
200
+ };
201
+ }
74
202
  const codeKeys = [
75
203
  "command",
76
204
  "cmd",
@@ -91,14 +219,18 @@ function formatArgs(args) {
91
219
  if (foundKey) {
92
220
  const val = obj[foundKey];
93
221
  const str = typeof val === "string" ? val : JSON.stringify(val);
94
- return `[${foundKey.toUpperCase()}]:
95
- ${smartTruncate(str, 500)}`;
222
+ return {
223
+ intent: "EXEC",
224
+ message: `[${foundKey.toUpperCase()}]:
225
+ ${smartTruncate(str, 500)}`
226
+ };
96
227
  }
97
- return Object.entries(obj).slice(0, 5).map(
228
+ const msg = Object.entries(obj).slice(0, 5).map(
98
229
  ([k, v]) => ` ${k}: ${smartTruncate(typeof v === "string" ? v : JSON.stringify(v), 300)}`
99
230
  ).join("\n");
231
+ return { intent: "EXEC", message: msg };
100
232
  }
101
- return smartTruncate(JSON.stringify(parsed), 200);
233
+ return { intent: "EXEC", message: smartTruncate(JSON.stringify(parsed), 200) };
102
234
  }
103
235
  function sendDesktopNotification(title, body) {
104
236
  if (isTestEnv()) return;
@@ -151,10 +283,11 @@ function buildPangoMessage(toolName, formattedArgs, agent, explainableLabel, loc
151
283
  }
152
284
  return lines.join("\n");
153
285
  }
154
- async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal) {
286
+ async function askNativePopup(toolName, args, agent, explainableLabel, locked = false, signal, matchedField, matchedWord) {
155
287
  if (isTestEnv()) return "deny";
156
- const formattedArgs = formatArgs(args);
157
- const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 Action Approval`;
288
+ const { message: formattedArgs, intent } = formatArgs(args, matchedField, matchedWord);
289
+ const intentLabel = intent === "EDIT" ? "Code Edit" : "Action Approval";
290
+ const title = locked ? `\u26A1 Node9 \u2014 Locked` : `\u{1F6E1}\uFE0F Node9 \u2014 ${intentLabel}`;
158
291
  const message = buildPlainMessage(toolName, formattedArgs, agent, explainableLabel, locked);
159
292
  process.stderr.write(import_chalk.default.yellow(`
160
293
  \u{1F6E1}\uFE0F Node9: Intercepted "${toolName}" \u2014 awaiting user...
@@ -231,11 +364,113 @@ end run`;
231
364
  });
232
365
  }
233
366
 
367
+ // src/config-schema.ts
368
+ var import_zod = require("zod");
369
+ var noNewlines = import_zod.z.string().refine((s) => !s.includes("\n") && !s.includes("\r"), {
370
+ message: "Value must not contain literal newline characters (use \\n instead)"
371
+ });
372
+ var validRegex = noNewlines.refine(
373
+ (s) => {
374
+ try {
375
+ new RegExp(s);
376
+ return true;
377
+ } catch {
378
+ return false;
379
+ }
380
+ },
381
+ { message: "Value must be a valid regular expression" }
382
+ );
383
+ var SmartConditionSchema = import_zod.z.object({
384
+ field: import_zod.z.string().min(1, "Condition field must not be empty"),
385
+ op: import_zod.z.enum(["matches", "notMatches", "contains", "notContains", "exists", "notExists"], {
386
+ errorMap: () => ({
387
+ message: "op must be one of: matches, notMatches, contains, notContains, exists, notExists"
388
+ })
389
+ }),
390
+ value: validRegex.optional(),
391
+ flags: import_zod.z.string().optional()
392
+ });
393
+ var SmartRuleSchema = import_zod.z.object({
394
+ name: import_zod.z.string().optional(),
395
+ tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
396
+ conditions: import_zod.z.array(SmartConditionSchema).min(1, "Smart rule must have at least one condition"),
397
+ conditionMode: import_zod.z.enum(["all", "any"]).optional(),
398
+ verdict: import_zod.z.enum(["allow", "review", "block"], {
399
+ errorMap: () => ({ message: "verdict must be one of: allow, review, block" })
400
+ }),
401
+ reason: import_zod.z.string().optional()
402
+ });
403
+ var PolicyRuleSchema = import_zod.z.object({
404
+ action: import_zod.z.string().min(1),
405
+ allowPaths: import_zod.z.array(import_zod.z.string()).optional(),
406
+ blockPaths: import_zod.z.array(import_zod.z.string()).optional()
407
+ });
408
+ var ConfigFileSchema = import_zod.z.object({
409
+ version: import_zod.z.string().optional(),
410
+ settings: import_zod.z.object({
411
+ mode: import_zod.z.enum(["standard", "strict", "audit"]).optional(),
412
+ autoStartDaemon: import_zod.z.boolean().optional(),
413
+ enableUndo: import_zod.z.boolean().optional(),
414
+ enableHookLogDebug: import_zod.z.boolean().optional(),
415
+ approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
416
+ approvers: import_zod.z.object({
417
+ native: import_zod.z.boolean().optional(),
418
+ browser: import_zod.z.boolean().optional(),
419
+ cloud: import_zod.z.boolean().optional(),
420
+ terminal: import_zod.z.boolean().optional()
421
+ }).optional(),
422
+ environment: import_zod.z.string().optional(),
423
+ slackEnabled: import_zod.z.boolean().optional(),
424
+ enableTrustSessions: import_zod.z.boolean().optional(),
425
+ allowGlobalPause: import_zod.z.boolean().optional()
426
+ }).optional(),
427
+ policy: import_zod.z.object({
428
+ sandboxPaths: import_zod.z.array(import_zod.z.string()).optional(),
429
+ dangerousWords: import_zod.z.array(noNewlines).optional(),
430
+ ignoredTools: import_zod.z.array(import_zod.z.string()).optional(),
431
+ toolInspection: import_zod.z.record(import_zod.z.string()).optional(),
432
+ rules: import_zod.z.array(PolicyRuleSchema).optional(),
433
+ smartRules: import_zod.z.array(SmartRuleSchema).optional(),
434
+ snapshot: import_zod.z.object({
435
+ tools: import_zod.z.array(import_zod.z.string()).optional(),
436
+ onlyPaths: import_zod.z.array(import_zod.z.string()).optional(),
437
+ ignorePaths: import_zod.z.array(import_zod.z.string()).optional()
438
+ }).optional()
439
+ }).optional(),
440
+ environments: import_zod.z.record(import_zod.z.object({ requireApproval: import_zod.z.boolean().optional() })).optional()
441
+ }).strict({ message: "Config contains unknown top-level keys" });
442
+ function sanitizeConfig(raw) {
443
+ const result = ConfigFileSchema.safeParse(raw);
444
+ if (result.success) {
445
+ return { sanitized: result.data, error: null };
446
+ }
447
+ const invalidTopLevelKeys = new Set(
448
+ result.error.issues.filter((issue) => issue.path.length > 0).map((issue) => String(issue.path[0]))
449
+ );
450
+ const sanitized = {};
451
+ if (typeof raw === "object" && raw !== null) {
452
+ for (const [key, value] of Object.entries(raw)) {
453
+ if (!invalidTopLevelKeys.has(key)) {
454
+ sanitized[key] = value;
455
+ }
456
+ }
457
+ }
458
+ const lines = result.error.issues.map((issue) => {
459
+ const path4 = issue.path.length > 0 ? issue.path.join(".") : "root";
460
+ return ` \u2022 ${path4}: ${issue.message}`;
461
+ });
462
+ return {
463
+ sanitized,
464
+ error: `Invalid config:
465
+ ${lines.join("\n")}`
466
+ };
467
+ }
468
+
234
469
  // src/core.ts
235
- var PAUSED_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "PAUSED");
236
- var TRUST_FILE = import_path.default.join(import_os.default.homedir(), ".node9", "trust.json");
237
- var LOCAL_AUDIT_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "audit.log");
238
- var HOOK_DEBUG_LOG = import_path.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
470
+ var PAUSED_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "PAUSED");
471
+ var TRUST_FILE = import_path3.default.join(import_os.default.homedir(), ".node9", "trust.json");
472
+ var LOCAL_AUDIT_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "audit.log");
473
+ var HOOK_DEBUG_LOG = import_path3.default.join(import_os.default.homedir(), ".node9", "hook-debug.log");
239
474
  function checkPause() {
240
475
  try {
241
476
  if (!import_fs.default.existsSync(PAUSED_FILE)) return { paused: false };
@@ -253,7 +488,7 @@ function checkPause() {
253
488
  }
254
489
  }
255
490
  function atomicWriteSync(filePath, data, options) {
256
- const dir = import_path.default.dirname(filePath);
491
+ const dir = import_path3.default.dirname(filePath);
257
492
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
258
493
  const tmpPath = `${filePath}.${import_os.default.hostname()}.${process.pid}.tmp`;
259
494
  import_fs.default.writeFileSync(tmpPath, data, options);
@@ -294,7 +529,7 @@ function writeTrustSession(toolName, durationMs) {
294
529
  }
295
530
  function appendToLog(logPath, entry) {
296
531
  try {
297
- const dir = import_path.default.dirname(logPath);
532
+ const dir = import_path3.default.dirname(logPath);
298
533
  if (!import_fs.default.existsSync(dir)) import_fs.default.mkdirSync(dir, { recursive: true });
299
534
  import_fs.default.appendFileSync(logPath, JSON.stringify(entry) + "\n");
300
535
  } catch {
@@ -338,9 +573,9 @@ function matchesPattern(text, patterns) {
338
573
  const withoutDotSlash = text.replace(/^\.\//, "");
339
574
  return isMatch(withoutDotSlash) || isMatch(`./${withoutDotSlash}`);
340
575
  }
341
- function getNestedValue(obj, path2) {
576
+ function getNestedValue(obj, path4) {
342
577
  if (!obj || typeof obj !== "object") return null;
343
- return path2.split(".").reduce((prev, curr) => prev?.[curr], obj);
578
+ return path4.split(".").reduce((prev, curr) => prev?.[curr], obj);
344
579
  }
345
580
  function evaluateSmartConditions(args, rule) {
346
581
  if (!rule.conditions || rule.conditions.length === 0) return true;
@@ -477,15 +712,10 @@ function redactSecrets(text) {
477
712
  return redacted;
478
713
  }
479
714
  var DANGEROUS_WORDS = [
480
- "drop",
481
- "truncate",
482
- "purge",
483
- "format",
484
- "destroy",
485
- "terminate",
486
- "revoke",
487
- "docker",
488
- "psql"
715
+ "mkfs",
716
+ // formats/wipes a filesystem partition
717
+ "shred"
718
+ // permanently overwrites file contents (unrecoverable)
489
719
  ];
490
720
  var DEFAULT_CONFIG = {
491
721
  settings: {
@@ -529,7 +759,21 @@ var DEFAULT_CONFIG = {
529
759
  "terminal.execute": "command",
530
760
  "postgres:query": "sql"
531
761
  },
762
+ snapshot: {
763
+ tools: [
764
+ "str_replace_based_edit_tool",
765
+ "write_file",
766
+ "edit_file",
767
+ "create_file",
768
+ "edit",
769
+ "replace"
770
+ ],
771
+ onlyPaths: [],
772
+ ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
773
+ },
532
774
  rules: [
775
+ // Only use the legacy rules format for simple path-based rm control.
776
+ // All other command-level enforcement lives in smartRules below.
533
777
  {
534
778
  action: "rm",
535
779
  allowPaths: [
@@ -546,6 +790,7 @@ var DEFAULT_CONFIG = {
546
790
  }
547
791
  ],
548
792
  smartRules: [
793
+ // ── SQL safety ────────────────────────────────────────────────────────
549
794
  {
550
795
  name: "no-delete-without-where",
551
796
  tool: "*",
@@ -556,6 +801,84 @@ var DEFAULT_CONFIG = {
556
801
  conditionMode: "all",
557
802
  verdict: "review",
558
803
  reason: "DELETE/UPDATE without WHERE clause \u2014 would affect every row in the table"
804
+ },
805
+ {
806
+ name: "review-drop-truncate-shell",
807
+ tool: "bash",
808
+ conditions: [
809
+ {
810
+ field: "command",
811
+ op: "matches",
812
+ value: "\\b(DROP|TRUNCATE)\\s+(TABLE|DATABASE|SCHEMA|INDEX)",
813
+ flags: "i"
814
+ }
815
+ ],
816
+ conditionMode: "all",
817
+ verdict: "review",
818
+ reason: "SQL DDL destructive statement inside a shell command"
819
+ },
820
+ // ── Git safety ────────────────────────────────────────────────────────
821
+ {
822
+ name: "block-force-push",
823
+ tool: "bash",
824
+ conditions: [
825
+ {
826
+ field: "command",
827
+ op: "matches",
828
+ value: "git push.*(--force|--force-with-lease|-f\\b)",
829
+ flags: "i"
830
+ }
831
+ ],
832
+ conditionMode: "all",
833
+ verdict: "block",
834
+ reason: "Force push overwrites remote history and cannot be undone"
835
+ },
836
+ {
837
+ name: "review-git-push",
838
+ tool: "bash",
839
+ conditions: [{ field: "command", op: "matches", value: "^\\s*git push\\b", flags: "i" }],
840
+ conditionMode: "all",
841
+ verdict: "review",
842
+ reason: "git push sends changes to a shared remote"
843
+ },
844
+ {
845
+ name: "review-git-destructive",
846
+ tool: "bash",
847
+ conditions: [
848
+ {
849
+ field: "command",
850
+ op: "matches",
851
+ value: "git\\s+(reset\\s+--hard|clean\\s+-[fdxX]|rebase|tag\\s+-d|branch\\s+-[dD])",
852
+ flags: "i"
853
+ }
854
+ ],
855
+ conditionMode: "all",
856
+ verdict: "review",
857
+ reason: "Destructive git operation \u2014 discards history or working-tree changes"
858
+ },
859
+ // ── Shell safety ──────────────────────────────────────────────────────
860
+ {
861
+ name: "review-sudo",
862
+ tool: "bash",
863
+ conditions: [{ field: "command", op: "matches", value: "^\\s*sudo\\s", flags: "i" }],
864
+ conditionMode: "all",
865
+ verdict: "review",
866
+ reason: "Command requires elevated privileges"
867
+ },
868
+ {
869
+ name: "review-curl-pipe-shell",
870
+ tool: "bash",
871
+ conditions: [
872
+ {
873
+ field: "command",
874
+ op: "matches",
875
+ value: "(curl|wget)[^|]*\\|\\s*(ba|z|da|fi|c|k)?sh",
876
+ flags: "i"
877
+ }
878
+ ],
879
+ conditionMode: "all",
880
+ verdict: "block",
881
+ reason: "Piping remote script into a shell is a supply-chain attack vector"
559
882
  }
560
883
  ]
561
884
  },
@@ -564,7 +887,7 @@ var DEFAULT_CONFIG = {
564
887
  var cachedConfig = null;
565
888
  function getInternalToken() {
566
889
  try {
567
- const pidFile = import_path.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
890
+ const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
568
891
  if (!import_fs.default.existsSync(pidFile)) return null;
569
892
  const data = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
570
893
  process.kill(data.pid, 0);
@@ -585,7 +908,9 @@ async function evaluatePolicy(toolName, args, agent) {
585
908
  return {
586
909
  decision: matchedRule.verdict,
587
910
  blockedByLabel: `Smart Rule: ${matchedRule.name ?? matchedRule.tool}`,
588
- reason: matchedRule.reason
911
+ reason: matchedRule.reason,
912
+ tier: 2,
913
+ ruleName: matchedRule.name ?? matchedRule.tool
589
914
  };
590
915
  }
591
916
  }
@@ -600,7 +925,7 @@ async function evaluatePolicy(toolName, args, agent) {
600
925
  pathTokens = analyzed.paths;
601
926
  const INLINE_EXEC_PATTERN = /^(python3?|bash|sh|zsh|perl|ruby|node|php|lua)\s+(-c|-e|-eval)\s/i;
602
927
  if (INLINE_EXEC_PATTERN.test(shellCommand.trim())) {
603
- return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)" };
928
+ return { decision: "review", blockedByLabel: "Node9 Standard (Inline Execution)", tier: 3 };
604
929
  }
605
930
  if (isSqlTool(toolName, config.policy.toolInspection)) {
606
931
  allTokens = allTokens.filter((t) => !SQL_DML_KEYWORDS.has(t.toLowerCase()));
@@ -623,7 +948,7 @@ async function evaluatePolicy(toolName, args, agent) {
623
948
  );
624
949
  const isRootWipe = allTokens.includes("rm") && (allTokens.includes("/") || allTokens.includes("/*"));
625
950
  if (hasSystemDisaster || isRootWipe) {
626
- return { decision: "review", blockedByLabel: "Manual Nuclear Protection" };
951
+ return { decision: "review", blockedByLabel: "Manual Nuclear Protection", tier: 3 };
627
952
  }
628
953
  return { decision: "allow" };
629
954
  }
@@ -641,14 +966,16 @@ async function evaluatePolicy(toolName, args, agent) {
641
966
  if (anyBlocked)
642
967
  return {
643
968
  decision: "review",
644
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`
969
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (path blocked)`,
970
+ tier: 5
645
971
  };
646
972
  const allAllowed = pathTokens.every((p) => matchesPattern(p, rule.allowPaths || []));
647
973
  if (allAllowed) return { decision: "allow" };
648
974
  }
649
975
  return {
650
976
  decision: "review",
651
- blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`
977
+ blockedByLabel: `Project/Global Config \u2014 rule "${rule.action}" (default block)`,
978
+ tier: 5
652
979
  };
653
980
  }
654
981
  }
@@ -668,15 +995,36 @@ async function evaluatePolicy(toolName, args, agent) {
668
995
  })
669
996
  );
670
997
  if (isDangerous) {
998
+ let matchedField;
999
+ if (matchedDangerousWord && args && typeof args === "object" && !Array.isArray(args)) {
1000
+ const obj = args;
1001
+ for (const [key, value] of Object.entries(obj)) {
1002
+ if (typeof value === "string") {
1003
+ try {
1004
+ if (new RegExp(
1005
+ `\\b${matchedDangerousWord.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
1006
+ "i"
1007
+ ).test(value)) {
1008
+ matchedField = key;
1009
+ break;
1010
+ }
1011
+ } catch {
1012
+ }
1013
+ }
1014
+ }
1015
+ }
671
1016
  return {
672
1017
  decision: "review",
673
- blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`
1018
+ blockedByLabel: `Project/Global Config \u2014 dangerous word: "${matchedDangerousWord}"`,
1019
+ matchedWord: matchedDangerousWord,
1020
+ matchedField,
1021
+ tier: 6
674
1022
  };
675
1023
  }
676
1024
  if (config.settings.mode === "strict") {
677
1025
  const envConfig = getActiveEnvironment(config);
678
1026
  if (envConfig?.requireApproval === false) return { decision: "allow" };
679
- return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)" };
1027
+ return { decision: "review", blockedByLabel: "Global Config (Strict Mode Active)", tier: 7 };
680
1028
  }
681
1029
  return { decision: "allow" };
682
1030
  }
@@ -688,7 +1036,7 @@ var DAEMON_PORT = 7391;
688
1036
  var DAEMON_HOST = "127.0.0.1";
689
1037
  function isDaemonRunning() {
690
1038
  try {
691
- const pidFile = import_path.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
1039
+ const pidFile = import_path3.default.join(import_os.default.homedir(), ".node9", "daemon.pid");
692
1040
  if (!import_fs.default.existsSync(pidFile)) return false;
693
1041
  const { pid, port } = JSON.parse(import_fs.default.readFileSync(pidFile, "utf-8"));
694
1042
  if (port !== DAEMON_PORT) return false;
@@ -700,7 +1048,7 @@ function isDaemonRunning() {
700
1048
  }
701
1049
  function getPersistentDecision(toolName) {
702
1050
  try {
703
- const file = import_path.default.join(import_os.default.homedir(), ".node9", "decisions.json");
1051
+ const file = import_path3.default.join(import_os.default.homedir(), ".node9", "decisions.json");
704
1052
  if (!import_fs.default.existsSync(file)) return null;
705
1053
  const decisions = JSON.parse(import_fs.default.readFileSync(file, "utf-8"));
706
1054
  const d = decisions[toolName];
@@ -709,7 +1057,7 @@ function getPersistentDecision(toolName) {
709
1057
  }
710
1058
  return null;
711
1059
  }
712
- async function askDaemon(toolName, args, meta, signal) {
1060
+ async function askDaemon(toolName, args, meta, signal, riskMetadata) {
713
1061
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
714
1062
  const checkCtrl = new AbortController();
715
1063
  const checkTimer = setTimeout(() => checkCtrl.abort(), 5e3);
@@ -719,7 +1067,13 @@ async function askDaemon(toolName, args, meta, signal) {
719
1067
  const checkRes = await fetch(`${base}/check`, {
720
1068
  method: "POST",
721
1069
  headers: { "Content-Type": "application/json" },
722
- body: JSON.stringify({ toolName, args, agent: meta?.agent, mcpServer: meta?.mcpServer }),
1070
+ body: JSON.stringify({
1071
+ toolName,
1072
+ args,
1073
+ agent: meta?.agent,
1074
+ mcpServer: meta?.mcpServer,
1075
+ ...riskMetadata && { riskMetadata }
1076
+ }),
723
1077
  signal: checkCtrl.signal
724
1078
  });
725
1079
  if (!checkRes.ok) throw new Error("Daemon fail");
@@ -744,7 +1098,7 @@ async function askDaemon(toolName, args, meta, signal) {
744
1098
  if (signal) signal.removeEventListener("abort", onAbort);
745
1099
  }
746
1100
  }
747
- async function notifyDaemonViewer(toolName, args, meta) {
1101
+ async function notifyDaemonViewer(toolName, args, meta, riskMetadata) {
748
1102
  const base = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
749
1103
  const res = await fetch(`${base}/check`, {
750
1104
  method: "POST",
@@ -754,7 +1108,8 @@ async function notifyDaemonViewer(toolName, args, meta) {
754
1108
  args,
755
1109
  slackDelegated: true,
756
1110
  agent: meta?.agent,
757
- mcpServer: meta?.mcpServer
1111
+ mcpServer: meta?.mcpServer,
1112
+ ...riskMetadata && { riskMetadata }
758
1113
  }),
759
1114
  signal: AbortSignal.timeout(3e3)
760
1115
  });
@@ -771,7 +1126,7 @@ async function resolveViaDaemon(id, decision, internalToken) {
771
1126
  signal: AbortSignal.timeout(3e3)
772
1127
  });
773
1128
  }
774
- async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta) {
1129
+ async function authorizeHeadless(toolName, args, allowTerminalFallback = false, meta, options) {
775
1130
  if (process.env.NODE9_PAUSED === "1") return { approved: true, checkedBy: "paused" };
776
1131
  const pauseState = checkPause();
777
1132
  if (pauseState.paused) return { approved: true, checkedBy: "paused" };
@@ -791,11 +1146,17 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
791
1146
  }
792
1147
  const isManual = meta?.agent === "Terminal";
793
1148
  let explainableLabel = "Local Config";
1149
+ let policyMatchedField;
1150
+ let policyMatchedWord;
1151
+ let riskMetadata;
794
1152
  if (config.settings.mode === "audit") {
795
1153
  if (!isIgnoredTool(toolName)) {
796
1154
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
797
1155
  if (policyResult.decision === "review") {
798
1156
  appendLocalAudit(toolName, args, "allow", "audit-mode", meta);
1157
+ if (approvers.cloud && creds?.apiKey) {
1158
+ await auditLocalAllow(toolName, args, "audit-mode", creds, meta);
1159
+ }
799
1160
  sendDesktopNotification(
800
1161
  "Node9 Audit Mode",
801
1162
  `Would have blocked "${toolName}" (${policyResult.blockedByLabel || "Local Config"}) \u2014 running in audit mode`
@@ -806,13 +1167,15 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
806
1167
  }
807
1168
  if (!isIgnoredTool(toolName)) {
808
1169
  if (getActiveTrustSession(toolName)) {
809
- if (creds?.apiKey) auditLocalAllow(toolName, args, "trust", creds, meta);
1170
+ if (approvers.cloud && creds?.apiKey)
1171
+ await auditLocalAllow(toolName, args, "trust", creds, meta);
810
1172
  if (!isManual) appendLocalAudit(toolName, args, "allow", "trust", meta);
811
1173
  return { approved: true, checkedBy: "trust" };
812
1174
  }
813
1175
  const policyResult = await evaluatePolicy(toolName, args, meta?.agent);
814
1176
  if (policyResult.decision === "allow") {
815
- if (creds?.apiKey) auditLocalAllow(toolName, args, "local-policy", creds, meta);
1177
+ if (approvers.cloud && creds?.apiKey)
1178
+ auditLocalAllow(toolName, args, "local-policy", creds, meta);
816
1179
  if (!isManual) appendLocalAudit(toolName, args, "allow", "local-policy", meta);
817
1180
  return { approved: true, checkedBy: "local-policy" };
818
1181
  }
@@ -826,9 +1189,20 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
826
1189
  };
827
1190
  }
828
1191
  explainableLabel = policyResult.blockedByLabel || "Local Config";
1192
+ policyMatchedField = policyResult.matchedField;
1193
+ policyMatchedWord = policyResult.matchedWord;
1194
+ riskMetadata = computeRiskMetadata(
1195
+ args,
1196
+ policyResult.tier ?? 6,
1197
+ explainableLabel,
1198
+ policyMatchedField,
1199
+ policyMatchedWord,
1200
+ policyResult.ruleName
1201
+ );
829
1202
  const persistent = getPersistentDecision(toolName);
830
1203
  if (persistent === "allow") {
831
- if (creds?.apiKey) auditLocalAllow(toolName, args, "persistent", creds, meta);
1204
+ if (approvers.cloud && creds?.apiKey)
1205
+ await auditLocalAllow(toolName, args, "persistent", creds, meta);
832
1206
  if (!isManual) appendLocalAudit(toolName, args, "allow", "persistent", meta);
833
1207
  return { approved: true, checkedBy: "persistent" };
834
1208
  }
@@ -842,7 +1216,6 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
842
1216
  };
843
1217
  }
844
1218
  } else {
845
- if (creds?.apiKey) auditLocalAllow(toolName, args, "ignoredTools", creds, meta);
846
1219
  if (!isManual) appendLocalAudit(toolName, args, "allow", "ignored", meta);
847
1220
  return { approved: true };
848
1221
  }
@@ -851,8 +1224,21 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
851
1224
  const cloudEnforced = approvers.cloud && !!creds?.apiKey;
852
1225
  if (cloudEnforced) {
853
1226
  try {
854
- const initResult = await initNode9SaaS(toolName, args, creds, meta);
1227
+ const initResult = await initNode9SaaS(toolName, args, creds, meta, riskMetadata);
855
1228
  if (!initResult.pending) {
1229
+ if (initResult.shadowMode) {
1230
+ console.error(
1231
+ import_chalk2.default.yellow(
1232
+ `
1233
+ \u26A0\uFE0F Node9 Shadow Mode: Action allowed, but would have been blocked by company policy.`
1234
+ )
1235
+ );
1236
+ if (initResult.shadowReason) {
1237
+ console.error(import_chalk2.default.dim(` Reason: ${initResult.shadowReason}
1238
+ `));
1239
+ }
1240
+ return { approved: true, checkedBy: "cloud" };
1241
+ }
856
1242
  return {
857
1243
  approved: !!initResult.approved,
858
1244
  reason: initResult.reason || (initResult.approved ? void 0 : "Action rejected by organization policy."),
@@ -877,18 +1263,22 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
877
1263
  );
878
1264
  }
879
1265
  }
880
- if (cloudEnforced && cloudRequestId) {
881
- console.error(
882
- import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
883
- );
884
- console.error(import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n"));
885
- } else if (!cloudEnforced) {
886
- const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
887
- console.error(
888
- import_chalk2.default.dim(`
1266
+ if (!options?.calledFromDaemon) {
1267
+ if (cloudEnforced && cloudRequestId) {
1268
+ console.error(
1269
+ import_chalk2.default.yellow("\n\u{1F6E1}\uFE0F Node9: Action suspended \u2014 waiting for Organization approval.")
1270
+ );
1271
+ console.error(
1272
+ import_chalk2.default.cyan(" Dashboard \u2192 ") + import_chalk2.default.bold("Mission Control > Activity Feed\n")
1273
+ );
1274
+ } else if (!cloudEnforced) {
1275
+ const cloudOffReason = !creds?.apiKey ? "no API key \u2014 run `node9 login` to connect" : "privacy mode (cloud disabled)";
1276
+ console.error(
1277
+ import_chalk2.default.dim(`
889
1278
  \u{1F6E1}\uFE0F Node9: intercepted "${toolName}" \u2014 cloud off (${cloudOffReason})
890
1279
  `)
891
- );
1280
+ );
1281
+ }
892
1282
  }
893
1283
  const abortController = new AbortController();
894
1284
  const { signal } = abortController;
@@ -918,8 +1308,10 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
918
1308
  racePromises.push(
919
1309
  (async () => {
920
1310
  try {
921
- if (isDaemonRunning() && internalToken) {
922
- viewerId = await notifyDaemonViewer(toolName, args, meta).catch(() => null);
1311
+ if (isDaemonRunning() && internalToken && !options?.calledFromDaemon) {
1312
+ viewerId = await notifyDaemonViewer(toolName, args, meta, riskMetadata).catch(
1313
+ () => null
1314
+ );
923
1315
  }
924
1316
  const cloudResult = await pollNode9SaaS(cloudRequestId, creds, signal);
925
1317
  return {
@@ -937,7 +1329,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
937
1329
  })()
938
1330
  );
939
1331
  }
940
- if (approvers.native && !isManual) {
1332
+ if (approvers.native && !isManual && !options?.calledFromDaemon) {
941
1333
  racePromises.push(
942
1334
  (async () => {
943
1335
  const decision = await askNativePopup(
@@ -946,7 +1338,9 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
946
1338
  meta?.agent,
947
1339
  explainableLabel,
948
1340
  isRemoteLocked,
949
- signal
1341
+ signal,
1342
+ policyMatchedField,
1343
+ policyMatchedWord
950
1344
  );
951
1345
  if (decision === "always_allow") {
952
1346
  writeTrustSession(toolName, 36e5);
@@ -963,7 +1357,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
963
1357
  })()
964
1358
  );
965
1359
  }
966
- if (approvers.browser && isDaemonRunning()) {
1360
+ if (approvers.browser && isDaemonRunning() && !options?.calledFromDaemon && !cloudEnforced) {
967
1361
  racePromises.push(
968
1362
  (async () => {
969
1363
  try {
@@ -974,7 +1368,7 @@ async function authorizeHeadless(toolName, args, allowTerminalFallback = false,
974
1368
  console.error(import_chalk2.default.cyan(` URL \u2192 http://${DAEMON_HOST}:${DAEMON_PORT}/
975
1369
  `));
976
1370
  }
977
- const daemonDecision = await askDaemon(toolName, args, meta, signal);
1371
+ const daemonDecision = await askDaemon(toolName, args, meta, signal, riskMetadata);
978
1372
  if (daemonDecision === "abandoned") throw new Error("Abandoned");
979
1373
  const isApproved = daemonDecision === "allow";
980
1374
  return {
@@ -1099,8 +1493,8 @@ REASON: Action blocked because no approval channels are available. (Native/Brows
1099
1493
  }
1100
1494
  function getConfig() {
1101
1495
  if (cachedConfig) return cachedConfig;
1102
- const globalPath = import_path.default.join(import_os.default.homedir(), ".node9", "config.json");
1103
- const projectPath = import_path.default.join(process.cwd(), "node9.config.json");
1496
+ const globalPath = import_path3.default.join(import_os.default.homedir(), ".node9", "config.json");
1497
+ const projectPath = import_path3.default.join(process.cwd(), "node9.config.json");
1104
1498
  const globalConfig = tryLoadConfig(globalPath);
1105
1499
  const projectConfig = tryLoadConfig(projectPath);
1106
1500
  const mergedSettings = {
@@ -1113,7 +1507,12 @@ function getConfig() {
1113
1507
  ignoredTools: [...DEFAULT_CONFIG.policy.ignoredTools],
1114
1508
  toolInspection: { ...DEFAULT_CONFIG.policy.toolInspection },
1115
1509
  rules: [...DEFAULT_CONFIG.policy.rules],
1116
- smartRules: [...DEFAULT_CONFIG.policy.smartRules]
1510
+ smartRules: [...DEFAULT_CONFIG.policy.smartRules],
1511
+ snapshot: {
1512
+ tools: [...DEFAULT_CONFIG.policy.snapshot.tools],
1513
+ onlyPaths: [...DEFAULT_CONFIG.policy.snapshot.onlyPaths],
1514
+ ignorePaths: [...DEFAULT_CONFIG.policy.snapshot.ignorePaths]
1515
+ }
1117
1516
  };
1118
1517
  const applyLayer = (source) => {
1119
1518
  if (!source) return;
@@ -1125,6 +1524,7 @@ function getConfig() {
1125
1524
  if (s.enableHookLogDebug !== void 0)
1126
1525
  mergedSettings.enableHookLogDebug = s.enableHookLogDebug;
1127
1526
  if (s.approvers) mergedSettings.approvers = { ...mergedSettings.approvers, ...s.approvers };
1527
+ if (s.approvalTimeoutMs !== void 0) mergedSettings.approvalTimeoutMs = s.approvalTimeoutMs;
1128
1528
  if (s.environment !== void 0) mergedSettings.environment = s.environment;
1129
1529
  if (p.sandboxPaths) mergedPolicy.sandboxPaths.push(...p.sandboxPaths);
1130
1530
  if (p.ignoredTools) mergedPolicy.ignoredTools.push(...p.ignoredTools);
@@ -1133,6 +1533,12 @@ function getConfig() {
1133
1533
  mergedPolicy.toolInspection = { ...mergedPolicy.toolInspection, ...p.toolInspection };
1134
1534
  if (p.rules) mergedPolicy.rules.push(...p.rules);
1135
1535
  if (p.smartRules) mergedPolicy.smartRules.push(...p.smartRules);
1536
+ if (p.snapshot) {
1537
+ const s2 = p.snapshot;
1538
+ if (s2.tools) mergedPolicy.snapshot.tools.push(...s2.tools);
1539
+ if (s2.onlyPaths) mergedPolicy.snapshot.onlyPaths.push(...s2.onlyPaths);
1540
+ if (s2.ignorePaths) mergedPolicy.snapshot.ignorePaths.push(...s2.ignorePaths);
1541
+ }
1136
1542
  };
1137
1543
  applyLayer(globalConfig);
1138
1544
  applyLayer(projectConfig);
@@ -1140,6 +1546,9 @@ function getConfig() {
1140
1546
  mergedPolicy.sandboxPaths = [...new Set(mergedPolicy.sandboxPaths)];
1141
1547
  mergedPolicy.dangerousWords = [...new Set(mergedPolicy.dangerousWords)];
1142
1548
  mergedPolicy.ignoredTools = [...new Set(mergedPolicy.ignoredTools)];
1549
+ mergedPolicy.snapshot.tools = [...new Set(mergedPolicy.snapshot.tools)];
1550
+ mergedPolicy.snapshot.onlyPaths = [...new Set(mergedPolicy.snapshot.onlyPaths)];
1551
+ mergedPolicy.snapshot.ignorePaths = [...new Set(mergedPolicy.snapshot.ignorePaths)];
1143
1552
  cachedConfig = {
1144
1553
  settings: mergedSettings,
1145
1554
  policy: mergedPolicy,
@@ -1149,11 +1558,33 @@ function getConfig() {
1149
1558
  }
1150
1559
  function tryLoadConfig(filePath) {
1151
1560
  if (!import_fs.default.existsSync(filePath)) return null;
1561
+ let raw;
1152
1562
  try {
1153
- return JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
1154
- } catch {
1563
+ raw = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
1564
+ } catch (err) {
1565
+ const msg = err instanceof Error ? err.message : String(err);
1566
+ process.stderr.write(
1567
+ `
1568
+ \u26A0\uFE0F Node9: Failed to parse ${filePath}
1569
+ ${msg}
1570
+ \u2192 Using default config
1571
+
1572
+ `
1573
+ );
1155
1574
  return null;
1156
1575
  }
1576
+ const { sanitized, error } = sanitizeConfig(raw);
1577
+ if (error) {
1578
+ process.stderr.write(
1579
+ `
1580
+ \u26A0\uFE0F Node9: Invalid config at ${filePath}:
1581
+ ${error.replace("Invalid config:\n", "")}
1582
+ \u2192 Invalid fields ignored, using defaults for those keys
1583
+
1584
+ `
1585
+ );
1586
+ }
1587
+ return sanitized;
1157
1588
  }
1158
1589
  function getActiveEnvironment(config) {
1159
1590
  const env = config.settings.environment || process.env.NODE_ENV || "development";
@@ -1168,7 +1599,7 @@ function getCredentials() {
1168
1599
  };
1169
1600
  }
1170
1601
  try {
1171
- const credPath = import_path.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1602
+ const credPath = import_path3.default.join(import_os.default.homedir(), ".node9", "credentials.json");
1172
1603
  if (import_fs.default.existsSync(credPath)) {
1173
1604
  const creds = JSON.parse(import_fs.default.readFileSync(credPath, "utf-8"));
1174
1605
  const profileName = process.env.NODE9_PROFILE || "default";
@@ -1195,9 +1626,7 @@ async function authorizeAction(toolName, args) {
1195
1626
  return result.approved;
1196
1627
  }
1197
1628
  function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1198
- const controller = new AbortController();
1199
- setTimeout(() => controller.abort(), 5e3);
1200
- fetch(`${creds.apiUrl}/audit`, {
1629
+ return fetch(`${creds.apiUrl}/audit`, {
1201
1630
  method: "POST",
1202
1631
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${creds.apiKey}` },
1203
1632
  body: JSON.stringify({
@@ -1212,11 +1641,12 @@ function auditLocalAllow(toolName, args, checkedBy, creds, meta) {
1212
1641
  platform: import_os.default.platform()
1213
1642
  }
1214
1643
  }),
1215
- signal: controller.signal
1644
+ signal: AbortSignal.timeout(5e3)
1645
+ }).then(() => {
1216
1646
  }).catch(() => {
1217
1647
  });
1218
1648
  }
1219
- async function initNode9SaaS(toolName, args, creds, meta) {
1649
+ async function initNode9SaaS(toolName, args, creds, meta, riskMetadata) {
1220
1650
  const controller = new AbortController();
1221
1651
  const timeout = setTimeout(() => controller.abort(), 1e4);
1222
1652
  try {
@@ -1232,7 +1662,8 @@ async function initNode9SaaS(toolName, args, creds, meta) {
1232
1662
  hostname: import_os.default.hostname(),
1233
1663
  cwd: process.cwd(),
1234
1664
  platform: import_os.default.platform()
1235
- }
1665
+ },
1666
+ ...riskMetadata && { riskMetadata }
1236
1667
  }),
1237
1668
  signal: controller.signal
1238
1669
  });