@juspay/neurolink 12.36.0 → 12.37.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/CHANGELOG.md CHANGED
@@ -1,8 +1,8 @@
1
- ## [12.36.0](https://github.com/juspay/neurolink/compare/v12.35.5...v12.36.0) (2026-09-28)
1
+ ## [12.37.0](https://github.com/juspay/neurolink/compare/v12.36.1...v12.37.0) (2026-09-28)
2
2
 
3
3
  ### Features
4
4
 
5
- - **(proxy):** translate native Codex requests to Anthropic Messages shape ([70c11ea](https://github.com/juspay/neurolink/commit/70c11eaec4f2c792b401d1f1d70d6b24cfe57610))
5
+ - **(proxy):** translate Anthropic responses and tool calls to the Codex wire format ([bef0c19](https://github.com/juspay/neurolink/commit/bef0c19db259cd2d5f6ee4816ef6d9bbf72c8271)), closes [#1842](https://github.com/juspay/neurolink/issues/1842)
6
6
 
7
7
  ## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
8
8
 
@@ -40,6 +40,14 @@ function withRoots(description, policy) {
40
40
  * `/home/app-evil` against `/home/app` gets through. A path that does not
41
41
  * exist yet is checked on its nearest existing ancestor, which is what a
42
42
  * write needs. Relative paths resolve against the first root.
43
+ *
44
+ * The result is a REAL path, and every consumer opens it with O_NOFOLLOW (or
45
+ * O_EXCL), so a link swapped in at the final component after this check is
46
+ * refused by the kernel — on Windows, which has no O_NOFOLLOW, only the
47
+ * O_EXCL create path keeps that guarantee. A directory component swapped for
48
+ * a link in that same window is not caught anywhere: Node exposes no
49
+ * openat/O_PATH walk, so that race is accepted here exactly as
50
+ * `FileDetector.loadFromPath` accepts it.
43
51
  */
44
52
  function resolveWithinFileToolRoots(target, policy) {
45
53
  const roots = rootsOf(policy);
@@ -65,6 +73,28 @@ function resolveWithinFileToolRoots(target, policy) {
65
73
  }
66
74
  /** Unavailable on Windows, where it is 0 and has no effect. */
67
75
  const O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;
76
+ const SENSITIVE_SYSTEM_PATH_PREFIXES = [
77
+ "/etc/",
78
+ "/sys/",
79
+ "/proc/",
80
+ "/dev/",
81
+ "/root/",
82
+ "/.ssh/",
83
+ "/private/etc/",
84
+ "/private/var/",
85
+ "c:/windows/",
86
+ "c:/program files/",
87
+ "c:/programdata/",
88
+ ];
89
+ /**
90
+ * Denylist of system directories analyzeCSV refuses even inside a permitted
91
+ * root. Expects an already-resolved real path, so what is judged is where the
92
+ * file actually lives rather than the name it was asked for by.
93
+ */
94
+ function isSensitiveSystemPath(realPath) {
95
+ const normalized = realPath.toLowerCase().replace(/\\/g, "/");
96
+ return SENSITIVE_SYSTEM_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix));
97
+ }
68
98
  /**
69
99
  * Write without following a symlink at the final path component. The root
70
100
  * check ran on the resolved path, so a link that appears there afterwards, or
@@ -238,6 +268,7 @@ function createReadFileTool(policy) {
238
268
  },
239
269
  });
240
270
  }
271
+ /** List one directory inside the roots; entries are lstat'd, never followed. */
241
272
  function createListDirectoryTool(policy) {
242
273
  return tool({
243
274
  description: withRoots("List files and directories in a specified directory", policy),
@@ -263,12 +294,20 @@ function createListDirectoryTool(policy) {
263
294
  const filteredItems = includeHidden
264
295
  ? items
265
296
  : items.filter((item) => !item.startsWith("."));
297
+ // lstat, not stat: an entry that is a symlink is reported as one, with
298
+ // its own metadata. Following it would leak the size and mtime of a
299
+ // target outside the roots, and a dangling link would fail the whole
300
+ // listing with ENOENT.
266
301
  const itemDetails = filteredItems.map((item) => {
267
302
  const itemPath = path.join(resolvedPath, item);
268
- const stats = fs.statSync(itemPath);
303
+ const stats = fs.lstatSync(itemPath);
269
304
  return {
270
305
  name: item,
271
- type: stats.isDirectory() ? "directory" : "file",
306
+ type: stats.isSymbolicLink()
307
+ ? "symlink"
308
+ : stats.isDirectory()
309
+ ? "directory"
310
+ : "file",
272
311
  size: stats.isFile() ? stats.size : undefined,
273
312
  lastModified: stats.mtime.toISOString(),
274
313
  };
@@ -336,6 +375,10 @@ function createWriteFileTool(policy) {
336
375
  },
337
376
  });
338
377
  }
378
+ /**
379
+ * Execute one analyzeCSV call: contain the path, parse the file without
380
+ * following a final-component link, then run the requested aggregation.
381
+ */
339
382
  async function runAnalyzeCSV({ filePath, operation, column, maxRows = 1000 }, policy) {
340
383
  const startTime = Date.now();
341
384
  logger.info(`[analyzeCSV] 🚀 START: file=${filePath}, operation=${operation}, column=${column}, maxRows=${maxRows}`);
@@ -353,7 +396,9 @@ async function runAnalyzeCSV({ filePath, operation, column, maxRows = 1000 }, po
353
396
  logger.info(`[analyzeCSV] Starting CSV parsing (max ${maxRows} rows)...`);
354
397
  // #384: parseCSVFile now returns validated Record<string, string |
355
398
  // undefined>[] rows, so the previous unchecked cast is unnecessary.
356
- const rows = await CSVProcessor.parseCSVFile(resolvedPath, maxRows);
399
+ // The root check ran on the real path; O_NOFOLLOW refuses a link swapped
400
+ // in at the final component afterwards, as readFile does.
401
+ const rows = await CSVProcessor.parseCSVFile(resolvedPath, maxRows, undefined, { followSymlinks: false });
357
402
  logger.info(`[analyzeCSV] ✅ CSV parsing complete: ${rows.length} rows`);
358
403
  if (rows.length === 0) {
359
404
  logger.warn(`[analyzeCSV] No data rows found`);
@@ -557,6 +602,7 @@ async function runAnalyzeCSV({ filePath, operation, column, maxRows = 1000 }, po
557
602
  };
558
603
  }
559
604
  }
605
+ /** The analyzeCSV tool bound to `policy`; its schema refuses paths the roots would. */
560
606
  function createAnalyzeCsvTool(policy) {
561
607
  return tool({
562
608
  description: withRoots("Analyze CSV file for accurate counting, aggregation, and statistical analysis. Use this for precise data operations like counting rows by column, calculating sums/averages, finding min/max values, etc. The tool reads the file directly - do NOT pass CSV content.", policy),
@@ -564,25 +610,13 @@ function createAnalyzeCsvTool(policy) {
564
610
  filePath: z
565
611
  .string()
566
612
  .refine((inputPath) => {
567
- const resolvedPath = path.resolve(inputPath);
568
- const normalizedPath = resolvedPath
569
- .toLowerCase()
570
- .replace(/\\/g, "/");
571
- const sensitivePatterns = [
572
- "/etc/",
573
- "/sys/",
574
- "/proc/",
575
- "/dev/",
576
- "/root/",
577
- "/.ssh/",
578
- "/private/etc/",
579
- "/private/var/",
580
- "c:/windows/",
581
- "c:/program files/",
582
- "c:/programdata/",
583
- ];
584
- return (!sensitivePatterns.some((pattern) => normalizedPath.startsWith(pattern)) &&
585
- resolveWithinFileToolRoots(inputPath, policy).path !== undefined);
613
+ // Contain first, then apply the system-directory denylist to the
614
+ // REAL path the guard resolved: a relative path is thereby judged
615
+ // against the tool root rather than process.cwd(), and a symlink
616
+ // into /etc under a wide root is seen for where it points.
617
+ const contained = resolveWithinFileToolRoots(inputPath, policy);
618
+ return (contained.path !== undefined &&
619
+ !isSensitiveSystemPath(contained.path));
586
620
  }, {
587
621
  message: "Invalid file path: access to system directories or paths outside the tool roots is not allowed",
588
622
  })