@juspay/neurolink 12.27.2 → 12.28.1

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/agent/directTools.d.ts +118 -94
  3. package/dist/agent/directTools.js +578 -446
  4. package/dist/agent/index.d.ts +1 -0
  5. package/dist/agent/index.js +1 -0
  6. package/dist/agent/prompts/terminalAgentPrompt.d.ts +13 -0
  7. package/dist/agent/prompts/terminalAgentPrompt.js +59 -0
  8. package/dist/browser/neurolink.min.js +384 -378
  9. package/dist/cli/factories/commandFactory.d.ts +25 -0
  10. package/dist/cli/factories/commandFactory.js +78 -8
  11. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  12. package/dist/core/baseProvider.d.ts +17 -1
  13. package/dist/core/baseProvider.js +48 -1
  14. package/dist/core/modules/ToolsManager.d.ts +7 -1
  15. package/dist/core/modules/ToolsManager.js +8 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/mcp/toolRegistry.d.ts +12 -1
  19. package/dist/mcp/toolRegistry.js +26 -2
  20. package/dist/neurolink.d.ts +13 -0
  21. package/dist/neurolink.js +97 -10
  22. package/dist/session/globalSessionState.d.ts +8 -0
  23. package/dist/session/globalSessionState.js +17 -0
  24. package/dist/types/agentMode.d.ts +9 -0
  25. package/dist/types/agentMode.js +1 -0
  26. package/dist/types/cli.d.ts +6 -0
  27. package/dist/types/config.d.ts +9 -0
  28. package/dist/types/fileToolRoots.d.ts +17 -0
  29. package/dist/types/fileToolRoots.js +7 -0
  30. package/dist/types/generate.d.ts +17 -0
  31. package/dist/types/index.d.ts +2 -0
  32. package/dist/types/index.js +2 -0
  33. package/dist/types/providers.d.ts +6 -0
  34. package/dist/types/stream.d.ts +7 -0
  35. package/dist/types/tools.d.ts +7 -0
  36. package/dist/utils/fileToolRoots.d.ts +34 -0
  37. package/dist/utils/fileToolRoots.js +111 -0
  38. package/dist/utils/messageBuilder.js +32 -1
  39. package/dist/utils/pathSandbox.d.ts +9 -6
  40. package/dist/utils/pathSandbox.js +35 -9
  41. package/docs-site/static/search-index.json +6 -5
  42. package/package.json +6 -2
@@ -7,6 +7,7 @@ import { CSVProcessor } from "../utils/csvProcessor.js";
7
7
  import { shouldEnableBashTool } from "../utils/toolUtils.js";
8
8
  import { tool } from "../utils/tool.js";
9
9
  import { withTimeout } from "../utils/async/withTimeout.js";
10
+ import { resolvePathWithinRoot, resolveWithinRoot, } from "../utils/pathSandbox.js";
10
11
  import { TIMEOUTS } from "../constants/timeouts.js";
11
12
  const MAX_OUTPUT_BYTES = 102400; // 100KB
12
13
  function truncateOutput(output) {
@@ -15,27 +16,105 @@ function truncateOutput(output) {
15
16
  }
16
17
  return output;
17
18
  }
19
+ /** The historical default: the process working directory, read at call time. */
20
+ const WORKING_DIRECTORY_POLICY = Object.freeze({
21
+ roots: null,
22
+ });
23
+ function rootsOf(policy) {
24
+ return policy.roots ?? [process.cwd()];
25
+ }
26
+ function withRoots(description, policy) {
27
+ if (policy.roots === null) {
28
+ return description;
29
+ }
30
+ const allowed = policy.roots.length
31
+ ? policy.roots.join(", ")
32
+ : "none (file access is disabled for this request)";
33
+ return `${description}. Allowed roots: ${allowed}`;
34
+ }
18
35
  /**
19
- * Contain a built-in file tool to the process working directory.
36
+ * Contain a built-in file tool to its roots.
20
37
  *
21
- * Returns the resolved absolute path when it lies inside `process.cwd()`, or an
22
- * `error` string otherwise. This is the sandbox for the default-enabled
23
- * `readFile` / `writeFile` / `listDirectory` / `analyzeCSV` tools: without it an
24
- * agent can pass an absolute path (`/etc/passwd`) or `../` traversal and reach
25
- * arbitrary files. The previous guard (`!resolvedPath.startsWith(cwd) &&
26
- * !path.isAbsolute(filePath)`) was always false for absolute paths, so it never
27
- * fired. The `cwd + path.sep` suffix prevents a sibling-prefix bypass
28
- * (`/home/app` vs `/home/app-evil`).
38
+ * Symlinks are resolved on both sides and the prefix test is separator-bounded
39
+ * (`resolvePathWithinRoot`), so neither `<root>/link → /etc` nor
40
+ * `/home/app-evil` against `/home/app` gets through. A path that does not
41
+ * exist yet is checked on its nearest existing ancestor, which is what a
42
+ * write needs. Relative paths resolve against the first root.
29
43
  */
30
- function resolveWithinCwd(filePath) {
31
- const resolvedPath = path.resolve(filePath);
32
- const cwd = path.resolve(process.cwd());
33
- if (resolvedPath !== cwd && !resolvedPath.startsWith(cwd + path.sep)) {
44
+ function resolveWithinFileToolRoots(target, policy) {
45
+ const roots = rootsOf(policy);
46
+ if (roots.length === 0) {
34
47
  return {
35
- error: `Access denied: "${filePath}" resolves outside the working directory`,
48
+ error: "Access denied: file tools are disabled for this request (no tool roots are configured)",
36
49
  };
37
50
  }
38
- return { path: resolvedPath };
51
+ const requested = path.isAbsolute(target)
52
+ ? target
53
+ : path.resolve(roots[0], target);
54
+ for (const root of roots) {
55
+ const contained = resolvePathWithinRoot(requested, root);
56
+ if (contained.path !== undefined) {
57
+ return contained;
58
+ }
59
+ }
60
+ return {
61
+ error: policy.roots === null
62
+ ? `Access denied: "${target}" resolves outside the working directory`
63
+ : `Access denied: "${target}" resolves outside the permitted roots (${roots.join(", ")})`,
64
+ };
65
+ }
66
+ /** Unavailable on Windows, where it is 0 and has no effect. */
67
+ const O_NOFOLLOW = fs.constants.O_NOFOLLOW ?? 0;
68
+ /**
69
+ * Write without following a symlink at the final path component. The root
70
+ * check ran on the resolved path, so a link that appears there afterwards, or
71
+ * a dangling one, is refused by the kernel instead of redirecting the write:
72
+ * `create` uses O_EXCL (which also fails on an existing link), the other
73
+ * modes O_NOFOLLOW.
74
+ */
75
+ function writeWithoutFollowingLinks(resolvedPath, content, mode) {
76
+ const { O_WRONLY, O_CREAT, O_EXCL, O_TRUNC, O_APPEND } = fs.constants;
77
+ const flags = mode === "create"
78
+ ? O_WRONLY | O_CREAT | O_EXCL
79
+ : O_WRONLY |
80
+ O_CREAT |
81
+ O_NOFOLLOW |
82
+ (mode === "append" ? O_APPEND : O_TRUNC);
83
+ const fd = fs.openSync(resolvedPath, flags, 0o666);
84
+ try {
85
+ fs.writeFileSync(fd, content, "utf-8");
86
+ }
87
+ finally {
88
+ fs.closeSync(fd);
89
+ }
90
+ }
91
+ /** Read without following a symlink at the final path component. */
92
+ function readWithoutFollowingLinks(resolvedPath) {
93
+ const fd = fs.openSync(resolvedPath, fs.constants.O_RDONLY | O_NOFOLLOW);
94
+ try {
95
+ return fs.readFileSync(fd);
96
+ }
97
+ finally {
98
+ fs.closeSync(fd);
99
+ }
100
+ }
101
+ const ZIP_SIGNATURE = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
102
+ const OLE2_SIGNATURE = Buffer.from([
103
+ 0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1,
104
+ ]);
105
+ /**
106
+ * Office documents are archives or compound files, so decoding them as UTF-8
107
+ * hands the model mojibake it then tries to reason about. Detected by content
108
+ * because extensions are often missing or wrong.
109
+ */
110
+ function detectBinaryDocumentFormat(bytes) {
111
+ if (bytes.subarray(0, ZIP_SIGNATURE.length).equals(ZIP_SIGNATURE)) {
112
+ return "ZIP-packaged (such as .docx, .xlsx, .pptx, .odt)";
113
+ }
114
+ if (bytes.subarray(0, OLE2_SIGNATURE.length).equals(OLE2_SIGNATURE)) {
115
+ return "OLE2 compound (such as legacy .doc, .xls, .ppt)";
116
+ }
117
+ return undefined;
39
118
  }
40
119
  /**
41
120
  * Vertex location for the websearchGrounding tool:
@@ -110,59 +189,36 @@ export function buildWebsearchResults(groundingMetadata, searchContent, limitedR
110
189
  return searchResults;
111
190
  }
112
191
  /**
113
- * Direct tool definitions that work immediately with Gemini/AI SDK
114
- * These bypass MCP complexity and provide reliable agent functionality
192
+ * The file tools, bound to one root policy. Built per request when roots are
193
+ * configured, so concurrent requests never share a boundary.
115
194
  */
116
- export const directAgentTools = {
117
- getCurrentTime: tool({
118
- description: "Get the current date and time",
119
- inputSchema: z.object({
120
- timezone: z
121
- .string()
122
- .optional()
123
- .describe('Timezone (e.g., "America/New_York", "Asia/Kolkata"). Defaults to system local time.'),
124
- }),
125
- execute: async ({ timezone }) => {
126
- try {
127
- const now = new Date();
128
- if (timezone) {
129
- return {
130
- success: true,
131
- time: now.toLocaleString("en-US", { timeZone: timezone }),
132
- timezone: timezone,
133
- iso: now.toISOString(),
134
- };
135
- }
136
- return {
137
- success: true,
138
- time: now.toLocaleString(),
139
- iso: now.toISOString(),
140
- timestamp: now.getTime(),
141
- };
142
- }
143
- catch (error) {
144
- return {
145
- success: false,
146
- error: error instanceof Error ? error.message : String(error),
147
- };
148
- }
149
- },
150
- }),
151
- readFile: tool({
152
- description: "Read the contents of a file from the filesystem",
195
+ function createReadFileTool(policy) {
196
+ return tool({
197
+ description: withRoots("Read the contents of a file from the filesystem", policy),
153
198
  inputSchema: z.object({
154
199
  path: z.string().describe("File path to read (relative or absolute)"),
155
200
  }),
156
201
  execute: async ({ path: filePath }) => {
157
202
  try {
158
- // Sandbox: contain reads to the working directory (blocks absolute-path
159
- // escape and ../ traversal).
160
- const guard = resolveWithinCwd(filePath);
161
- if ("error" in guard) {
203
+ // Sandbox: contain reads to the tool roots (symlink-aware).
204
+ const guard = resolveWithinFileToolRoots(filePath, policy);
205
+ if (guard.error !== undefined) {
162
206
  return { success: false, error: guard.error };
163
207
  }
164
208
  const resolvedPath = guard.path;
165
- const content = fs.readFileSync(resolvedPath, "utf-8");
209
+ const bytes = readWithoutFollowingLinks(resolvedPath);
210
+ const binaryFormat = detectBinaryDocumentFormat(bytes);
211
+ if (binaryFormat) {
212
+ return {
213
+ success: false,
214
+ error: `${filePath} is a binary ${binaryFormat} document, so its bytes are not readable as text. Read it with a parser or converter for that format (for example from a shell command) instead of readFile.`,
215
+ binaryDocument: true,
216
+ format: binaryFormat,
217
+ size: bytes.length,
218
+ path: resolvedPath,
219
+ };
220
+ }
221
+ const content = bytes.toString("utf-8");
166
222
  const stats = fs.statSync(resolvedPath);
167
223
  return {
168
224
  success: true,
@@ -180,9 +236,11 @@ export const directAgentTools = {
180
236
  };
181
237
  }
182
238
  },
183
- }),
184
- listDirectory: tool({
185
- description: "List files and directories in a specified directory",
239
+ });
240
+ }
241
+ function createListDirectoryTool(policy) {
242
+ return tool({
243
+ description: withRoots("List files and directories in a specified directory", policy),
186
244
  inputSchema: z.object({
187
245
  path: z
188
246
  .string()
@@ -195,9 +253,9 @@ export const directAgentTools = {
195
253
  }),
196
254
  execute: async ({ path: dirPath, includeHidden }) => {
197
255
  try {
198
- // Sandbox: contain directory listing to the working directory.
199
- const guard = resolveWithinCwd(dirPath);
200
- if ("error" in guard) {
256
+ // Sandbox: contain directory listing to the tool roots.
257
+ const guard = resolveWithinFileToolRoots(dirPath, policy);
258
+ if (guard.error !== undefined) {
201
259
  return { success: false, error: guard.error };
202
260
  }
203
261
  const resolvedPath = guard.path;
@@ -230,82 +288,11 @@ export const directAgentTools = {
230
288
  };
231
289
  }
232
290
  },
233
- }),
234
- calculateMath: tool({
235
- description: "Perform mathematical calculations safely",
236
- inputSchema: z.object({
237
- expression: z
238
- .string()
239
- .describe('Mathematical expression to evaluate (e.g., "2 + 2", "Math.sqrt(16)")'),
240
- precision: z
241
- .number()
242
- .optional()
243
- .describe("Number of decimal places for result")
244
- .default(2),
245
- }),
246
- execute: async ({ expression, precision }) => {
247
- try {
248
- // Simple safe evaluation - only allow basic math operations
249
- const sanitizedExpression = expression.replace(/[^0-9+\-*/().\s]/g, "");
250
- if (sanitizedExpression !== expression) {
251
- // Try Math functions for more complex operations
252
- const allowedMathFunctions = [
253
- "Math.abs",
254
- "Math.ceil",
255
- "Math.floor",
256
- "Math.round",
257
- "Math.sqrt",
258
- "Math.pow",
259
- "Math.sin",
260
- "Math.cos",
261
- "Math.tan",
262
- "Math.log",
263
- "Math.exp",
264
- "Math.PI",
265
- "Math.E",
266
- ];
267
- let safeExpression = expression;
268
- for (const func of allowedMathFunctions) {
269
- safeExpression = safeExpression.replace(new RegExp(func, "g"), func);
270
- }
271
- // Remove remaining non-safe characters except Math functions
272
- const mathSafe = /^[0-9+\-*/().\s]|Math\.(abs|ceil|floor|round|sqrt|pow|sin|cos|tan|log|exp|PI|E)/g;
273
- if (!safeExpression
274
- .split("")
275
- .every((char) => mathSafe.test(char) ||
276
- char === "(" ||
277
- char === ")" ||
278
- char === "," ||
279
- char === " ")) {
280
- return {
281
- success: false,
282
- error: `Unsafe expression: Only basic math operations and Math functions are allowed`,
283
- };
284
- }
285
- }
286
- // Use Function constructor for safe evaluation
287
- const result = new Function(`'use strict'; return (${expression})`)();
288
- const roundedResult = typeof result === "number"
289
- ? Number(result.toFixed(precision))
290
- : result;
291
- return {
292
- success: true,
293
- expression,
294
- result: roundedResult,
295
- type: typeof result,
296
- };
297
- }
298
- catch (error) {
299
- return {
300
- success: false,
301
- error: error instanceof Error ? error.message : String(error),
302
- expression,
303
- };
304
- }
305
- },
306
- }),
307
- writeFile: tool({
308
- description: "Write content to a file (use with caution)",
291
+ });
292
+ }
293
+ function createWriteFileTool(policy) {
294
+ return tool({
295
+ description: withRoots("Write content to a file (use with caution)", policy),
309
296
  inputSchema: z.object({
310
297
  path: z.string().describe("File path to write to"),
311
298
  content: z.string().describe("Content to write to the file"),
@@ -316,10 +303,9 @@ export const directAgentTools = {
316
303
  }),
317
304
  execute: async ({ path: filePath, content, mode }) => {
318
305
  try {
319
- // Sandbox: contain writes to the working directory (blocks absolute-path
320
- // escape and ../ traversal).
321
- const guard = resolveWithinCwd(filePath);
322
- if ("error" in guard) {
306
+ // Sandbox: contain writes to the tool roots (symlink-aware).
307
+ const guard = resolveWithinFileToolRoots(filePath, policy);
308
+ if (guard.error !== undefined) {
323
309
  return { success: false, error: guard.error };
324
310
  }
325
311
  const resolvedPath = guard.path;
@@ -330,12 +316,7 @@ export const directAgentTools = {
330
316
  error: `File already exists. Use 'overwrite' or 'append' mode to modify existing files.`,
331
317
  };
332
318
  }
333
- let finalContent = content;
334
- if (mode === "append" && fs.existsSync(resolvedPath)) {
335
- const existingContent = fs.readFileSync(resolvedPath, "utf-8");
336
- finalContent = existingContent + content;
337
- }
338
- fs.writeFileSync(resolvedPath, finalContent, "utf-8");
319
+ writeWithoutFollowingLinks(resolvedPath, content, mode);
339
320
  const stats = fs.statSync(resolvedPath);
340
321
  return {
341
322
  success: true,
@@ -353,12 +334,232 @@ export const directAgentTools = {
353
334
  };
354
335
  }
355
336
  },
356
- }),
357
- // NOTE: searchFiles was removed to avoid naming conflict with external MCP 'search_files' tool
358
- // from @modelcontextprotocol/server-filesystem which provides the same functionality
359
- // with parameters {path, pattern, excludePatterns}
360
- analyzeCSV: tool({
361
- description: "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.",
337
+ });
338
+ }
339
+ async function runAnalyzeCSV({ filePath, operation, column, maxRows = 1000 }, policy) {
340
+ const startTime = Date.now();
341
+ logger.info(`[analyzeCSV] 🚀 START: file=${filePath}, operation=${operation}, column=${column}, maxRows=${maxRows}`);
342
+ try {
343
+ // Resolve file path
344
+ logger.debug(`[analyzeCSV] Resolving file: ${filePath}`);
345
+ // Sandbox: contain CSV reads to the tool roots.
346
+ const guard = resolveWithinFileToolRoots(filePath, policy);
347
+ if (guard.error !== undefined) {
348
+ return { success: false, error: guard.error };
349
+ }
350
+ const resolvedPath = guard.path;
351
+ logger.debug(`[analyzeCSV] Resolved path: ${resolvedPath}`);
352
+ // Parse CSV using streaming from disk (memory efficient)
353
+ logger.info(`[analyzeCSV] Starting CSV parsing (max ${maxRows} rows)...`);
354
+ // #384: parseCSVFile now returns validated Record<string, string |
355
+ // undefined>[] rows, so the previous unchecked cast is unnecessary.
356
+ const rows = await CSVProcessor.parseCSVFile(resolvedPath, maxRows);
357
+ logger.info(`[analyzeCSV] ✅ CSV parsing complete: ${rows.length} rows`);
358
+ if (rows.length === 0) {
359
+ logger.warn(`[analyzeCSV] No data rows found`);
360
+ return {
361
+ success: false,
362
+ error: "No data rows found in CSV",
363
+ };
364
+ }
365
+ // Log column names
366
+ const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
367
+ logger.info(`[analyzeCSV] Found ${rows.length} rows with columns:`, columnNames);
368
+ logger.info(`[analyzeCSV] Executing operation: ${operation}`);
369
+ let result;
370
+ switch (operation) {
371
+ case "count_by_column": {
372
+ logger.info(`[analyzeCSV] count_by_column: column=${column}`);
373
+ if (!column) {
374
+ return {
375
+ success: false,
376
+ error: "Column name required for count_by_column operation",
377
+ };
378
+ }
379
+ // Count occurrences of each value in the column
380
+ const counts = {};
381
+ logger.debug(`[analyzeCSV] Counting rows...`);
382
+ for (const row of rows) {
383
+ const value = row[column];
384
+ if (value !== undefined) {
385
+ counts[value] = (counts[value] || 0) + 1;
386
+ }
387
+ }
388
+ logger.debug(`[analyzeCSV] Found ${Object.keys(counts).length} unique values`);
389
+ // Sort by count descending
390
+ logger.debug(`[analyzeCSV] Sorting results...`);
391
+ result = Object.fromEntries(Object.entries(counts).sort(([, a], [, b]) => b - a));
392
+ logger.info(`[analyzeCSV] ✅ count_by_column complete. Result:`, result);
393
+ break;
394
+ }
395
+ case "sum_by_column": {
396
+ logger.info(`[analyzeCSV] sum_by_column: column=${column}`);
397
+ if (!column) {
398
+ return {
399
+ success: false,
400
+ error: "Column name required for sum_by_column operation",
401
+ };
402
+ }
403
+ // Sum numeric values from the target column itself for each group
404
+ const groups = {};
405
+ logger.debug(`[analyzeCSV] Grouping and summing ${rows.length} rows...`);
406
+ let processedRows = 0;
407
+ let totalNumericValuesFound = 0;
408
+ for (const row of rows) {
409
+ const key = row[column];
410
+ if (!key) {
411
+ continue;
412
+ }
413
+ // Parse numeric value from the target column
414
+ const value = row[column];
415
+ if (value === undefined || value === null || value === "") {
416
+ continue;
417
+ }
418
+ const num = parseFloat(value);
419
+ if (isNaN(num)) {
420
+ continue;
421
+ }
422
+ if (!groups[key]) {
423
+ groups[key] = 0;
424
+ }
425
+ groups[key] += num;
426
+ totalNumericValuesFound++;
427
+ processedRows++;
428
+ if (processedRows % 10 === 0) {
429
+ logger.debug(`[analyzeCSV] Processed ${processedRows}/${rows.length} rows`);
430
+ }
431
+ }
432
+ // Fail fast if no numeric data found in the requested column
433
+ if (totalNumericValuesFound === 0) {
434
+ return {
435
+ success: false,
436
+ error: `No numeric data found in column "${column}" for sum_by_column operation`,
437
+ };
438
+ }
439
+ logger.debug(`[analyzeCSV] Calculated sums for ${Object.keys(groups).length} groups (${totalNumericValuesFound} numeric values)`);
440
+ result = groups;
441
+ logger.info(`[analyzeCSV] ✅ sum_by_column complete`);
442
+ break;
443
+ }
444
+ case "average_by_column": {
445
+ logger.info(`[analyzeCSV] average_by_column: column=${column}`);
446
+ if (!column) {
447
+ return {
448
+ success: false,
449
+ error: "Column name required for average_by_column operation",
450
+ };
451
+ }
452
+ // Average numeric values from the target column itself for each group
453
+ const groups = {};
454
+ logger.debug(`[analyzeCSV] Grouping and averaging ${rows.length} rows...`);
455
+ let processedRows = 0;
456
+ let totalNumericValuesFound = 0;
457
+ for (const row of rows) {
458
+ const key = row[column];
459
+ if (!key) {
460
+ continue;
461
+ }
462
+ // Parse numeric value from the target column
463
+ const value = row[column];
464
+ if (value === undefined || value === null || value === "") {
465
+ continue;
466
+ }
467
+ const num = parseFloat(value);
468
+ if (isNaN(num)) {
469
+ continue;
470
+ }
471
+ if (!groups[key]) {
472
+ groups[key] = { sum: 0, count: 0 };
473
+ }
474
+ groups[key].sum += num;
475
+ groups[key].count++;
476
+ totalNumericValuesFound++;
477
+ processedRows++;
478
+ if (processedRows % 10 === 0) {
479
+ logger.debug(`[analyzeCSV] Processed ${processedRows}/${rows.length} rows`);
480
+ }
481
+ }
482
+ // Fail fast if no numeric data found in the requested column
483
+ if (totalNumericValuesFound === 0) {
484
+ return {
485
+ success: false,
486
+ error: `No numeric data found in column "${column}" for average_by_column operation`,
487
+ };
488
+ }
489
+ logger.debug(`[analyzeCSV] Calculated averages for ${Object.keys(groups).length} groups (${totalNumericValuesFound} numeric values)`);
490
+ result = Object.fromEntries(Object.entries(groups).map(([k, v]) => [
491
+ k,
492
+ v.count > 0 ? v.sum / v.count : 0,
493
+ ]));
494
+ logger.info(`[analyzeCSV] ✅ average_by_column complete`);
495
+ break;
496
+ }
497
+ case "min_max_by_column": {
498
+ if (!column) {
499
+ return {
500
+ success: false,
501
+ error: "Column name required for min_max_by_column operation",
502
+ };
503
+ }
504
+ const values = rows
505
+ .map((row) => row[column])
506
+ .filter((v) => v !== undefined && v !== "");
507
+ const numericValues = values
508
+ .map((v) => parseFloat(v))
509
+ .filter((n) => !isNaN(n));
510
+ if (numericValues.length === 0) {
511
+ return {
512
+ success: false,
513
+ error: `No numeric data found in column "${column}" for min_max_by_column operation`,
514
+ };
515
+ }
516
+ result = {
517
+ min: Math.min(...numericValues),
518
+ max: Math.max(...numericValues),
519
+ numericCount: numericValues.length,
520
+ totalCount: values.length,
521
+ };
522
+ break;
523
+ }
524
+ case "describe": {
525
+ const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
526
+ result = {
527
+ total_rows: rows.length,
528
+ columns: columnNames,
529
+ column_count: columnNames.length,
530
+ };
531
+ break;
532
+ }
533
+ default:
534
+ return {
535
+ success: false,
536
+ error: `Unknown operation: ${operation}`,
537
+ };
538
+ }
539
+ const duration = Date.now() - startTime;
540
+ logger.info(`[analyzeCSV] 🏁 COMPLETE: ${operation} took ${duration}ms`);
541
+ const response = {
542
+ success: true,
543
+ operation,
544
+ column,
545
+ result: JSON.stringify(result, null, 2),
546
+ rowCount: rows.length,
547
+ };
548
+ logger.debug(`[analyzeCSV] 📤 RETURNING TO LLM:`, JSON.stringify(response, null, 2));
549
+ return response;
550
+ }
551
+ catch (error) {
552
+ return {
553
+ success: false,
554
+ error: error instanceof Error ? error.message : String(error),
555
+ operation,
556
+ column,
557
+ };
558
+ }
559
+ }
560
+ function createAnalyzeCsvTool(policy) {
561
+ return tool({
562
+ 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),
362
563
  inputSchema: z.object({
363
564
  filePath: z
364
565
  .string()
@@ -380,9 +581,10 @@ export const directAgentTools = {
380
581
  "c:/program files/",
381
582
  "c:/programdata/",
382
583
  ];
383
- return !sensitivePatterns.some((pattern) => normalizedPath.startsWith(pattern));
584
+ return (!sensitivePatterns.some((pattern) => normalizedPath.startsWith(pattern)) &&
585
+ resolveWithinFileToolRoots(inputPath, policy).path !== undefined);
384
586
  }, {
385
- message: "Invalid file path: access to system directories is not allowed",
587
+ message: "Invalid file path: access to system directories or paths outside the tool roots is not allowed",
386
588
  })
387
589
  .describe("Path to the CSV file to analyze (e.g., 'test/data.csv' or '/absolute/path/file.csv')"),
388
590
  operation: z
@@ -405,231 +607,126 @@ export const directAgentTools = {
405
607
  .default(1000)
406
608
  .describe("Maximum rows to process (default: 1000)"),
407
609
  }),
408
- execute: async ({ filePath, operation, column, maxRows = 1000 }) => {
409
- const startTime = Date.now();
410
- logger.info(`[analyzeCSV] 🚀 START: file=${filePath}, operation=${operation}, column=${column}, maxRows=${maxRows}`);
610
+ execute: async (args) => runAnalyzeCSV(args, policy),
611
+ });
612
+ }
613
+ function createFileTools(policy) {
614
+ return {
615
+ readFile: createReadFileTool(policy),
616
+ listDirectory: createListDirectoryTool(policy),
617
+ writeFile: createWriteFileTool(policy),
618
+ analyzeCSV: createAnalyzeCsvTool(policy),
619
+ };
620
+ }
621
+ /** Tools that never touch the filesystem, shared by every root policy. */
622
+ const policyFreeTools = {
623
+ getCurrentTime: tool({
624
+ description: "Get the current date and time",
625
+ inputSchema: z.object({
626
+ timezone: z
627
+ .string()
628
+ .optional()
629
+ .describe('Timezone (e.g., "America/New_York", "Asia/Kolkata"). Defaults to system local time.'),
630
+ }),
631
+ execute: async ({ timezone }) => {
411
632
  try {
412
- // Resolve file path
413
- logger.debug(`[analyzeCSV] Resolving file: ${filePath}`);
414
- // Sandbox: contain CSV reads to the working directory.
415
- const guard = resolveWithinCwd(filePath);
416
- if ("error" in guard) {
417
- return { success: false, error: guard.error };
418
- }
419
- const resolvedPath = guard.path;
420
- logger.debug(`[analyzeCSV] Resolved path: ${resolvedPath}`);
421
- // Parse CSV using streaming from disk (memory efficient)
422
- logger.info(`[analyzeCSV] Starting CSV parsing (max ${maxRows} rows)...`);
423
- // #384: parseCSVFile now returns validated Record<string, string |
424
- // undefined>[] rows, so the previous unchecked cast is unnecessary.
425
- const rows = await CSVProcessor.parseCSVFile(resolvedPath, maxRows);
426
- logger.info(`[analyzeCSV] ✅ CSV parsing complete: ${rows.length} rows`);
427
- if (rows.length === 0) {
428
- logger.warn(`[analyzeCSV] No data rows found`);
633
+ const now = new Date();
634
+ if (timezone) {
429
635
  return {
430
- success: false,
431
- error: "No data rows found in CSV",
636
+ success: true,
637
+ time: now.toLocaleString("en-US", { timeZone: timezone }),
638
+ timezone: timezone,
639
+ iso: now.toISOString(),
432
640
  };
433
641
  }
434
- // Log column names
435
- const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
436
- logger.info(`[analyzeCSV] Found ${rows.length} rows with columns:`, columnNames);
437
- logger.info(`[analyzeCSV] Executing operation: ${operation}`);
438
- let result;
439
- switch (operation) {
440
- case "count_by_column": {
441
- logger.info(`[analyzeCSV] count_by_column: column=${column}`);
442
- if (!column) {
443
- return {
444
- success: false,
445
- error: "Column name required for count_by_column operation",
446
- };
447
- }
448
- // Count occurrences of each value in the column
449
- const counts = {};
450
- logger.debug(`[analyzeCSV] Counting rows...`);
451
- for (const row of rows) {
452
- const value = row[column];
453
- if (value !== undefined) {
454
- counts[value] = (counts[value] || 0) + 1;
455
- }
456
- }
457
- logger.debug(`[analyzeCSV] Found ${Object.keys(counts).length} unique values`);
458
- // Sort by count descending
459
- logger.debug(`[analyzeCSV] Sorting results...`);
460
- result = Object.fromEntries(Object.entries(counts).sort(([, a], [, b]) => b - a));
461
- logger.info(`[analyzeCSV] ✅ count_by_column complete. Result:`, result);
462
- break;
463
- }
464
- case "sum_by_column": {
465
- logger.info(`[analyzeCSV] sum_by_column: column=${column}`);
466
- if (!column) {
467
- return {
468
- success: false,
469
- error: "Column name required for sum_by_column operation",
470
- };
471
- }
472
- // Sum numeric values from the target column itself for each group
473
- const groups = {};
474
- logger.debug(`[analyzeCSV] Grouping and summing ${rows.length} rows...`);
475
- let processedRows = 0;
476
- let totalNumericValuesFound = 0;
477
- for (const row of rows) {
478
- const key = row[column];
479
- if (!key) {
480
- continue;
481
- }
482
- // Parse numeric value from the target column
483
- const value = row[column];
484
- if (value === undefined || value === null || value === "") {
485
- continue;
486
- }
487
- const num = parseFloat(value);
488
- if (isNaN(num)) {
489
- continue;
490
- }
491
- if (!groups[key]) {
492
- groups[key] = 0;
493
- }
494
- groups[key] += num;
495
- totalNumericValuesFound++;
496
- processedRows++;
497
- if (processedRows % 10 === 0) {
498
- logger.debug(`[analyzeCSV] Processed ${processedRows}/${rows.length} rows`);
499
- }
500
- }
501
- // Fail fast if no numeric data found in the requested column
502
- if (totalNumericValuesFound === 0) {
503
- return {
504
- success: false,
505
- error: `No numeric data found in column "${column}" for sum_by_column operation`,
506
- };
507
- }
508
- logger.debug(`[analyzeCSV] Calculated sums for ${Object.keys(groups).length} groups (${totalNumericValuesFound} numeric values)`);
509
- result = groups;
510
- logger.info(`[analyzeCSV] ✅ sum_by_column complete`);
511
- break;
512
- }
513
- case "average_by_column": {
514
- logger.info(`[analyzeCSV] average_by_column: column=${column}`);
515
- if (!column) {
516
- return {
517
- success: false,
518
- error: "Column name required for average_by_column operation",
519
- };
520
- }
521
- // Average numeric values from the target column itself for each group
522
- const groups = {};
523
- logger.debug(`[analyzeCSV] Grouping and averaging ${rows.length} rows...`);
524
- let processedRows = 0;
525
- let totalNumericValuesFound = 0;
526
- for (const row of rows) {
527
- const key = row[column];
528
- if (!key) {
529
- continue;
530
- }
531
- // Parse numeric value from the target column
532
- const value = row[column];
533
- if (value === undefined || value === null || value === "") {
534
- continue;
535
- }
536
- const num = parseFloat(value);
537
- if (isNaN(num)) {
538
- continue;
539
- }
540
- if (!groups[key]) {
541
- groups[key] = { sum: 0, count: 0 };
542
- }
543
- groups[key].sum += num;
544
- groups[key].count++;
545
- totalNumericValuesFound++;
546
- processedRows++;
547
- if (processedRows % 10 === 0) {
548
- logger.debug(`[analyzeCSV] Processed ${processedRows}/${rows.length} rows`);
549
- }
550
- }
551
- // Fail fast if no numeric data found in the requested column
552
- if (totalNumericValuesFound === 0) {
553
- return {
554
- success: false,
555
- error: `No numeric data found in column "${column}" for average_by_column operation`,
556
- };
557
- }
558
- logger.debug(`[analyzeCSV] Calculated averages for ${Object.keys(groups).length} groups (${totalNumericValuesFound} numeric values)`);
559
- result = Object.fromEntries(Object.entries(groups).map(([k, v]) => [
560
- k,
561
- v.count > 0 ? v.sum / v.count : 0,
562
- ]));
563
- logger.info(`[analyzeCSV] ✅ average_by_column complete`);
564
- break;
565
- }
566
- case "min_max_by_column": {
567
- if (!column) {
568
- return {
569
- success: false,
570
- error: "Column name required for min_max_by_column operation",
571
- };
572
- }
573
- const values = rows
574
- .map((row) => row[column])
575
- .filter((v) => v !== undefined && v !== "");
576
- const numericValues = values
577
- .map((v) => parseFloat(v))
578
- .filter((n) => !isNaN(n));
579
- if (numericValues.length === 0) {
580
- return {
581
- success: false,
582
- error: `No numeric data found in column "${column}" for min_max_by_column operation`,
583
- };
584
- }
585
- result = {
586
- min: Math.min(...numericValues),
587
- max: Math.max(...numericValues),
588
- numericCount: numericValues.length,
589
- totalCount: values.length,
590
- };
591
- break;
592
- }
593
- case "describe": {
594
- const columnNames = rows.length > 0 ? Object.keys(rows[0]) : [];
595
- result = {
596
- total_rows: rows.length,
597
- columns: columnNames,
598
- column_count: columnNames.length,
599
- };
600
- break;
642
+ return {
643
+ success: true,
644
+ time: now.toLocaleString(),
645
+ iso: now.toISOString(),
646
+ timestamp: now.getTime(),
647
+ };
648
+ }
649
+ catch (error) {
650
+ return {
651
+ success: false,
652
+ error: error instanceof Error ? error.message : String(error),
653
+ };
654
+ }
655
+ },
656
+ }),
657
+ calculateMath: tool({
658
+ description: "Perform mathematical calculations safely",
659
+ inputSchema: z.object({
660
+ expression: z
661
+ .string()
662
+ .describe('Mathematical expression to evaluate (e.g., "2 + 2", "Math.sqrt(16)")'),
663
+ precision: z
664
+ .number()
665
+ .optional()
666
+ .describe("Number of decimal places for result")
667
+ .default(2),
668
+ }),
669
+ execute: async ({ expression, precision }) => {
670
+ try {
671
+ // Simple safe evaluation - only allow basic math operations
672
+ const sanitizedExpression = expression.replace(/[^0-9+\-*/().\s]/g, "");
673
+ if (sanitizedExpression !== expression) {
674
+ // Try Math functions for more complex operations
675
+ const allowedMathFunctions = [
676
+ "Math.abs",
677
+ "Math.ceil",
678
+ "Math.floor",
679
+ "Math.round",
680
+ "Math.sqrt",
681
+ "Math.pow",
682
+ "Math.sin",
683
+ "Math.cos",
684
+ "Math.tan",
685
+ "Math.log",
686
+ "Math.exp",
687
+ "Math.PI",
688
+ "Math.E",
689
+ ];
690
+ let safeExpression = expression;
691
+ for (const func of allowedMathFunctions) {
692
+ safeExpression = safeExpression.replace(new RegExp(func, "g"), func);
601
693
  }
602
- default:
694
+ // Remove remaining non-safe characters except Math functions
695
+ const mathSafe = /^[0-9+\-*/().\s]|Math\.(abs|ceil|floor|round|sqrt|pow|sin|cos|tan|log|exp|PI|E)/g;
696
+ if (!safeExpression
697
+ .split("")
698
+ .every((char) => mathSafe.test(char) ||
699
+ char === "(" ||
700
+ char === ")" ||
701
+ char === "," ||
702
+ char === " ")) {
603
703
  return {
604
704
  success: false,
605
- error: `Unknown operation: ${operation}`,
705
+ error: `Unsafe expression: Only basic math operations and Math functions are allowed`,
606
706
  };
707
+ }
607
708
  }
608
- const duration = Date.now() - startTime;
609
- logger.info(`[analyzeCSV] 🏁 COMPLETE: ${operation} took ${duration}ms`);
610
- const response = {
709
+ // Use Function constructor for safe evaluation
710
+ const result = new Function(`'use strict'; return (${expression})`)();
711
+ const roundedResult = typeof result === "number"
712
+ ? Number(result.toFixed(precision))
713
+ : result;
714
+ return {
611
715
  success: true,
612
- operation,
613
- column,
614
- result: JSON.stringify(result, null, 2),
615
- rowCount: rows.length,
716
+ expression,
717
+ result: roundedResult,
718
+ type: typeof result,
616
719
  };
617
- logger.debug(`[analyzeCSV] 📤 RETURNING TO LLM:`, JSON.stringify(response, null, 2));
618
- return response;
619
720
  }
620
721
  catch (error) {
621
722
  return {
622
723
  success: false,
623
724
  error: error instanceof Error ? error.message : String(error),
624
- operation,
625
- column,
725
+ expression,
626
726
  };
627
727
  }
628
728
  },
629
729
  }),
630
- // NOTE: executeBashCommand was moved to a separate opt-in export (bashTool) for security.
631
- // It is only included in directAgentTools when NEUROLINK_ENABLE_BASH_TOOL=true or
632
- // toolConfig.enableBashTool is explicitly set to true. See shouldEnableBashTool() in toolUtils.ts.
633
730
  websearchGrounding: tool({
634
731
  description: "Performs a Google Search and returns a summarized answer with source citations. Always check the current date before constructing the query. Use whenever the answer depends on time-sensitive facts or requires verification against real-world sources.",
635
732
  inputSchema: z.object({
@@ -735,6 +832,32 @@ export const directAgentTools = {
735
832
  },
736
833
  }),
737
834
  };
835
+ /**
836
+ * Direct tool definitions that work immediately with Gemini/AI SDK.
837
+ * These bypass MCP complexity and provide reliable agent functionality.
838
+ *
839
+ * `createDirectAgentTools` builds a fresh set bound to `policy`; the shared
840
+ * `directAgentTools` below keeps the working-directory default.
841
+ */
842
+ export function createDirectAgentTools(policy) {
843
+ const fileTools = createFileTools(policy);
844
+ const tools = {
845
+ getCurrentTime: policyFreeTools.getCurrentTime,
846
+ readFile: fileTools.readFile,
847
+ listDirectory: fileTools.listDirectory,
848
+ calculateMath: policyFreeTools.calculateMath,
849
+ writeFile: fileTools.writeFile,
850
+ analyzeCSV: fileTools.analyzeCSV,
851
+ websearchGrounding: policyFreeTools.websearchGrounding,
852
+ };
853
+ // executeBashCommand is opt-in: only included when NEUROLINK_ENABLE_BASH_TOOL=true
854
+ // or toolConfig.enableBashTool is set. See shouldEnableBashTool() in toolUtils.ts.
855
+ if (shouldEnableBashTool()) {
856
+ tools.executeBashCommand =
857
+ createBashTool(policy);
858
+ }
859
+ return tools;
860
+ }
738
861
  /**
739
862
  * Bash command execution tool - exported separately for opt-in use.
740
863
  *
@@ -746,105 +869,114 @@ export const directAgentTools = {
746
869
  * Import this directly when you need bash execution capabilities:
747
870
  * import { bashTool } from '../agent/directTools.js';
748
871
  */
749
- export const bashTool = tool({
750
- description: "Execute a bash/shell command and return stdout, stderr, and exit code. Supports full shell syntax including pipes, redirects, and variable expansion. Requires HITL confirmation when enabled.",
751
- inputSchema: z.object({
752
- command: z
753
- .string()
754
- .describe("The shell command to execute (supports pipes, redirects, etc.)"),
755
- timeout: z
756
- .number()
757
- .optional()
758
- .default(30000)
759
- .describe("Timeout in milliseconds (default: 30000, max: 120000)"),
760
- cwd: z
761
- .string()
762
- .optional()
763
- .describe("Working directory (defaults to process.cwd())"),
764
- }),
765
- execute: async ({ command, timeout = 30000, cwd }) => {
766
- try {
767
- const effectiveTimeout = Math.min(Math.max(timeout, 100), 120000);
768
- const resolvedCwd = cwd ? path.resolve(cwd) : process.cwd();
769
- const currentCwd = process.cwd();
770
- // Verify cwd exists before resolving symlinks
771
- if (!fs.existsSync(resolvedCwd) ||
772
- !fs.statSync(resolvedCwd).isDirectory()) {
773
- return {
774
- success: false,
775
- code: -1,
776
- stdout: "",
777
- stderr: "",
778
- error: `Directory does not exist: ${resolvedCwd}`,
779
- };
780
- }
781
- // Security: resolve symlinks and prevent execution outside current directory
872
+ export function createBashTool(policy) {
873
+ return tool({
874
+ description: "Execute a bash/shell command and return stdout, stderr, and exit code. Supports full shell syntax including pipes, redirects, and variable expansion. Requires HITL confirmation when enabled. The command runs with the full privileges of the process: tool roots constrain only its starting directory (cwd), not where the command itself can reach.",
875
+ inputSchema: z.object({
876
+ command: z
877
+ .string()
878
+ .describe("The shell command to execute (supports pipes, redirects, etc.)"),
879
+ timeout: z
880
+ .number()
881
+ .optional()
882
+ .default(30000)
883
+ .describe("Timeout in milliseconds (default: 30000, max: 120000)"),
884
+ cwd: z
885
+ .string()
886
+ .optional()
887
+ .describe(policy.roots === null
888
+ ? "Working directory (defaults to process.cwd())"
889
+ : `Working directory; must be inside one of: ${policy.roots.join(", ")}`),
890
+ }),
891
+ execute: async ({ command, timeout = 30000, cwd }) => {
782
892
  try {
783
- const realCwd = fs.realpathSync(currentCwd);
784
- const realResolvedCwd = fs.realpathSync(resolvedCwd);
785
- if (!realResolvedCwd.startsWith(realCwd)) {
893
+ const effectiveTimeout = Math.min(Math.max(timeout, 100), 120000);
894
+ const roots = rootsOf(policy);
895
+ if (roots.length === 0) {
896
+ return {
897
+ success: false,
898
+ code: -1,
899
+ stdout: "",
900
+ stderr: "",
901
+ error: "Access denied: no tool roots are configured, so there is no permitted working directory",
902
+ };
903
+ }
904
+ const requestedCwd = cwd ? path.resolve(roots[0], cwd) : roots[0];
905
+ // Verify cwd exists before resolving symlinks
906
+ if (!fs.existsSync(requestedCwd) ||
907
+ !fs.statSync(requestedCwd).isDirectory()) {
908
+ return {
909
+ success: false,
910
+ code: -1,
911
+ stdout: "",
912
+ stderr: "",
913
+ error: `Directory does not exist: ${requestedCwd}`,
914
+ };
915
+ }
916
+ // Security: resolve symlinks on both sides and require a separator-bounded
917
+ // prefix, so neither a symlink nor a sibling like <root>-evil escapes.
918
+ let resolvedCwd;
919
+ for (const root of roots) {
920
+ const contained = resolveWithinRoot(requestedCwd, root);
921
+ if (contained.path !== undefined) {
922
+ resolvedCwd = contained.path;
923
+ break;
924
+ }
925
+ }
926
+ if (resolvedCwd === undefined) {
786
927
  return {
787
928
  success: false,
788
929
  code: -1,
789
930
  stdout: "",
790
931
  stderr: "",
791
- error: "Access denied: Cannot execute commands outside current directory",
932
+ error: policy.roots === null
933
+ ? "Access denied: Cannot execute commands outside current directory"
934
+ : `Access denied: Cannot execute commands outside the permitted roots (${roots.join(", ")})`,
792
935
  };
793
936
  }
937
+ // Use /bin/bash -c to support full shell syntax (pipes, redirects, etc.)
938
+ return await new Promise((resolve) => {
939
+ execFile("/bin/bash", ["-c", command], {
940
+ timeout: effectiveTimeout,
941
+ cwd: resolvedCwd,
942
+ maxBuffer: MAX_OUTPUT_BYTES,
943
+ }, (error, stdout, stderr) => {
944
+ if (error) {
945
+ const exitCode = typeof error.code === "number" ? error.code : 1;
946
+ resolve({
947
+ success: false,
948
+ code: exitCode,
949
+ stdout: truncateOutput(stdout || ""),
950
+ stderr: truncateOutput(stderr || error.message),
951
+ error: error.killed ? "Command timed out" : error.message,
952
+ });
953
+ }
954
+ else {
955
+ resolve({
956
+ success: true,
957
+ code: 0,
958
+ stdout: truncateOutput(stdout),
959
+ stderr: truncateOutput(stderr),
960
+ });
961
+ }
962
+ });
963
+ });
794
964
  }
795
- catch {
965
+ catch (error) {
796
966
  return {
797
967
  success: false,
798
968
  code: -1,
799
969
  stdout: "",
800
970
  stderr: "",
801
- error: "Access denied: Cannot resolve directory path",
971
+ error: error instanceof Error ? error.message : String(error),
802
972
  };
803
973
  }
804
- // Use /bin/bash -c to support full shell syntax (pipes, redirects, etc.)
805
- return await new Promise((resolve) => {
806
- execFile("/bin/bash", ["-c", command], {
807
- timeout: effectiveTimeout,
808
- cwd: resolvedCwd,
809
- maxBuffer: MAX_OUTPUT_BYTES,
810
- }, (error, stdout, stderr) => {
811
- if (error) {
812
- const exitCode = typeof error.code === "number" ? error.code : 1;
813
- resolve({
814
- success: false,
815
- code: exitCode,
816
- stdout: truncateOutput(stdout || ""),
817
- stderr: truncateOutput(stderr || error.message),
818
- error: error.killed ? "Command timed out" : error.message,
819
- });
820
- }
821
- else {
822
- resolve({
823
- success: true,
824
- code: 0,
825
- stdout: truncateOutput(stdout),
826
- stderr: truncateOutput(stderr),
827
- });
828
- }
829
- });
830
- });
831
- }
832
- catch (error) {
833
- return {
834
- success: false,
835
- code: -1,
836
- stdout: "",
837
- stderr: "",
838
- error: error instanceof Error ? error.message : String(error),
839
- };
840
- }
841
- },
842
- });
843
- // Conditionally inject executeBashCommand into directAgentTools when opted in.
844
- // This ensures the tool is only available to SDK consumers who explicitly enable it.
845
- if (shouldEnableBashTool()) {
846
- directAgentTools.executeBashCommand = bashTool;
974
+ },
975
+ });
847
976
  }
977
+ /** The opt-in bash tool with the working-directory default. */
978
+ export const bashTool = createBashTool(WORKING_DIRECTORY_POLICY);
979
+ export const directAgentTools = createDirectAgentTools(WORKING_DIRECTORY_POLICY);
848
980
  // eslint-disable-next-line no-redeclare
849
981
  export function getToolsForCategory(category = "all") {
850
982
  switch (category) {