@probelabs/probe 0.6.0-rc169 → 0.6.0-rc171

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.
@@ -13,11 +13,11 @@ import { randomUUID } from 'crypto';
13
13
  import { EventEmitter } from 'events';
14
14
  import { existsSync } from 'fs';
15
15
  import { readFile, stat } from 'fs/promises';
16
- import { resolve, isAbsolute, dirname } from 'path';
16
+ import { resolve, isAbsolute, dirname, basename, normalize, sep } from 'path';
17
17
  import { TokenCounter } from './tokenCounter.js';
18
18
  import { InMemoryStorageAdapter } from './storage/InMemoryStorageAdapter.js';
19
19
  import { HookManager, HOOK_TYPES } from './hooks/HookManager.js';
20
- import { SUPPORTED_IMAGE_EXTENSIONS, IMAGE_MIME_TYPES } from './imageConfig.js';
20
+ import { SUPPORTED_IMAGE_EXTENSIONS, IMAGE_MIME_TYPES, isFormatSupportedByProvider } from './imageConfig.js';
21
21
  import {
22
22
  createTools,
23
23
  searchToolDefinition,
@@ -475,6 +475,21 @@ export class ProbeAgent {
475
475
  throw new Error('Image path is required');
476
476
  }
477
477
 
478
+ // Validate extension before attempting to load
479
+ // Use basename to prevent path traversal attacks (e.g., 'malicious.jpg/../../../etc/passwd')
480
+ const filename = basename(imagePath);
481
+ const extension = filename.toLowerCase().split('.').pop();
482
+
483
+ // Always validate extension is in allowed list (defense-in-depth)
484
+ if (!extension || !SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
485
+ throw new Error(`Invalid or unsupported image extension: ${extension}. Supported formats: ${SUPPORTED_IMAGE_EXTENSIONS.join(', ')}`);
486
+ }
487
+
488
+ // Check provider-specific format restrictions (e.g., SVG not supported by Google Gemini)
489
+ if (this.apiType && !isFormatSupportedByProvider(extension, this.apiType)) {
490
+ throw new Error(`Image format '${extension}' is not supported by the current AI provider (${this.apiType}). Try using a different image format like PNG or JPEG.`);
491
+ }
492
+
478
493
  // Load the image using the existing loadImageIfValid method
479
494
  const loaded = await this.loadImageIfValid(imagePath);
480
495
 
@@ -1262,20 +1277,28 @@ export class ProbeAgent {
1262
1277
  }
1263
1278
 
1264
1279
  // Security validation: check if path is within any allowed directory
1280
+ // Use normalize() after resolve() to handle path traversal attempts (e.g., '/allowed/../etc/passwd')
1265
1281
  const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
1266
-
1282
+
1267
1283
  let absolutePath;
1268
1284
  let isPathAllowed = false;
1269
-
1285
+
1270
1286
  // If absolute path, check if it's within any allowed directory
1271
1287
  if (isAbsolute(imagePath)) {
1272
- absolutePath = imagePath;
1273
- isPathAllowed = allowedDirs.some(dir => absolutePath.startsWith(resolve(dir)));
1288
+ // Normalize to resolve any '..' sequences
1289
+ absolutePath = normalize(resolve(imagePath));
1290
+ isPathAllowed = allowedDirs.some(dir => {
1291
+ const normalizedDir = normalize(resolve(dir));
1292
+ // Ensure the path is within the allowed directory (add separator to prevent prefix attacks)
1293
+ return absolutePath === normalizedDir || absolutePath.startsWith(normalizedDir + sep);
1294
+ });
1274
1295
  } else {
1275
1296
  // For relative paths, try resolving against each allowed directory
1276
1297
  for (const dir of allowedDirs) {
1277
- const resolvedPath = resolve(dir, imagePath);
1278
- if (resolvedPath.startsWith(resolve(dir))) {
1298
+ const normalizedDir = normalize(resolve(dir));
1299
+ const resolvedPath = normalize(resolve(dir, imagePath));
1300
+ // Ensure the resolved path is within the allowed directory
1301
+ if (resolvedPath === normalizedDir || resolvedPath.startsWith(normalizedDir + sep)) {
1279
1302
  absolutePath = resolvedPath;
1280
1303
  isPathAllowed = true;
1281
1304
  break;
@@ -1319,6 +1342,10 @@ export class ProbeAgent {
1319
1342
  return false;
1320
1343
  }
1321
1344
 
1345
+ // Note: Provider-specific format validation (e.g., SVG not supported by Google Gemini)
1346
+ // is handled by the readImage tool which provides explicit error messages.
1347
+ // loadImageIfValid is a lower-level method that only checks general format support.
1348
+
1322
1349
  // Determine MIME type (from shared config)
1323
1350
  const mimeType = IMAGE_MIME_TYPES[extension];
1324
1351
 
@@ -2504,7 +2531,7 @@ When troubleshooting:
2504
2531
  maxIterations,
2505
2532
  parentSessionId: this.sessionId, // Pass parent session ID for tracking
2506
2533
  path: this.searchPath, // Inherit search path
2507
- provider: this.provider, // Inherit AI provider
2534
+ provider: this.apiType, // Inherit AI provider (string identifier)
2508
2535
  model: this.model, // Inherit model
2509
2536
  debug: this.debug,
2510
2537
  tracer: this.tracer
@@ -2513,7 +2540,7 @@ When troubleshooting:
2513
2540
  if (this.debug) {
2514
2541
  console.log(`[DEBUG] Executing delegate tool at iteration ${currentIteration}/${maxIterations}`);
2515
2542
  console.log(`[DEBUG] Parent session: ${this.sessionId}`);
2516
- console.log(`[DEBUG] Inherited config: path=${this.searchPath}, provider=${this.provider}, model=${this.model}`);
2543
+ console.log(`[DEBUG] Inherited config: path=${this.searchPath}, provider=${this.apiType}, model=${this.model}`);
2517
2544
  console.log(`[DEBUG] Delegate task: ${toolParams.task?.substring(0, 100)}...`);
2518
2545
  }
2519
2546
 
@@ -2597,10 +2624,10 @@ When troubleshooting:
2597
2624
  content: toolResultMessage
2598
2625
  });
2599
2626
 
2600
- // Process tool result for image references
2601
- if (toolResultContent) {
2602
- await this.processImageReferences(toolResultContent);
2603
- }
2627
+ // NOTE: Automatic image processing removed (GitHub issue #305)
2628
+ // Images are now only loaded when the AI explicitly calls the readImage tool
2629
+ // This prevents: 1) implicit behavior that users don't expect
2630
+ // 2) crashes with unsupported MIME types (e.g., SVG on Gemini)
2604
2631
 
2605
2632
  if (this.debug) {
2606
2633
  console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length}`);
@@ -21,6 +21,12 @@ export const IMAGE_MIME_TYPES = {
21
21
  'svg': 'image/svg+xml'
22
22
  };
23
23
 
24
+ // Provider-specific unsupported image formats
25
+ // These providers do not support certain MIME types and will crash if they receive them
26
+ export const PROVIDER_UNSUPPORTED_FORMATS = {
27
+ 'google': ['svg'], // Google Gemini doesn't support image/svg+xml
28
+ };
29
+
24
30
  /**
25
31
  * Generate a regex pattern string for matching image file extensions
26
32
  * @param {string[]} extensions - Array of extensions (without dots)
@@ -38,3 +44,54 @@ export function getExtensionPattern(extensions = SUPPORTED_IMAGE_EXTENSIONS) {
38
44
  export function getMimeType(extension) {
39
45
  return IMAGE_MIME_TYPES[extension.toLowerCase()];
40
46
  }
47
+
48
+ /**
49
+ * Check if an image extension is supported by a specific provider
50
+ * @param {string} extension - File extension (without dot)
51
+ * @param {string} provider - Provider name (e.g., 'google', 'anthropic', 'openai')
52
+ * @returns {boolean} True if the format is supported by the provider
53
+ */
54
+ export function isFormatSupportedByProvider(extension, provider) {
55
+ // Validate extension parameter - must be a non-empty string without path separators
56
+ if (!extension || typeof extension !== 'string') {
57
+ return false;
58
+ }
59
+ // Sanitize: reject extensions containing path traversal characters
60
+ if (extension.includes('/') || extension.includes('\\') || extension.includes('..')) {
61
+ return false;
62
+ }
63
+
64
+ const ext = extension.toLowerCase();
65
+
66
+ // First check if it's a generally supported format
67
+ if (!SUPPORTED_IMAGE_EXTENSIONS.includes(ext)) {
68
+ return false;
69
+ }
70
+
71
+ // Handle null/undefined provider gracefully (treat as no restrictions)
72
+ if (!provider || typeof provider !== 'string') {
73
+ return true;
74
+ }
75
+
76
+ // Check provider-specific restrictions
77
+ const unsupportedFormats = PROVIDER_UNSUPPORTED_FORMATS[provider];
78
+ if (unsupportedFormats && unsupportedFormats.includes(ext)) {
79
+ return false;
80
+ }
81
+
82
+ return true;
83
+ }
84
+
85
+ /**
86
+ * Get supported image extensions for a specific provider
87
+ * @param {string} provider - Provider name (e.g., 'google', 'anthropic', 'openai')
88
+ * @returns {string[]} Array of supported extensions for this provider
89
+ */
90
+ export function getSupportedExtensionsForProvider(provider) {
91
+ // Handle null/undefined/non-string provider gracefully (return all extensions)
92
+ if (!provider || typeof provider !== 'string') {
93
+ return [...SUPPORTED_IMAGE_EXTENSIONS];
94
+ }
95
+ const unsupportedFormats = PROVIDER_UNSUPPORTED_FORMATS[provider] || [];
96
+ return SUPPORTED_IMAGE_EXTENSIONS.filter(ext => !unsupportedFormats.includes(ext));
97
+ }
@@ -1998,7 +1998,27 @@ var init_HookManager = __esm({
1998
1998
  });
1999
1999
 
2000
2000
  // src/agent/imageConfig.js
2001
- var SUPPORTED_IMAGE_EXTENSIONS, IMAGE_MIME_TYPES;
2001
+ function isFormatSupportedByProvider(extension, provider) {
2002
+ if (!extension || typeof extension !== "string") {
2003
+ return false;
2004
+ }
2005
+ if (extension.includes("/") || extension.includes("\\") || extension.includes("..")) {
2006
+ return false;
2007
+ }
2008
+ const ext2 = extension.toLowerCase();
2009
+ if (!SUPPORTED_IMAGE_EXTENSIONS.includes(ext2)) {
2010
+ return false;
2011
+ }
2012
+ if (!provider || typeof provider !== "string") {
2013
+ return true;
2014
+ }
2015
+ const unsupportedFormats = PROVIDER_UNSUPPORTED_FORMATS[provider];
2016
+ if (unsupportedFormats && unsupportedFormats.includes(ext2)) {
2017
+ return false;
2018
+ }
2019
+ return true;
2020
+ }
2021
+ var SUPPORTED_IMAGE_EXTENSIONS, IMAGE_MIME_TYPES, PROVIDER_UNSUPPORTED_FORMATS;
2002
2022
  var init_imageConfig = __esm({
2003
2023
  "src/agent/imageConfig.js"() {
2004
2024
  "use strict";
@@ -2011,6 +2031,10 @@ var init_imageConfig = __esm({
2011
2031
  "bmp": "image/bmp",
2012
2032
  "svg": "image/svg+xml"
2013
2033
  };
2034
+ PROVIDER_UNSUPPORTED_FORMATS = {
2035
+ "google": ["svg"]
2036
+ // Google Gemini doesn't support image/svg+xml
2037
+ };
2014
2038
  }
2015
2039
  });
2016
2040
 
@@ -3414,7 +3438,7 @@ async function delegate({
3414
3438
  const sessionId = randomUUID();
3415
3439
  const startTime = Date.now();
3416
3440
  const remainingIterations = Math.max(1, maxIterations - currentIteration);
3417
- const delegationSpan = tracer ? tracer.createDelegationSpan(sessionId, task) : null;
3441
+ const delegationSpan = typeof tracer?.createDelegationSpan === "function" ? tracer.createDelegationSpan(sessionId, task) : null;
3418
3442
  let timeoutId = null;
3419
3443
  let acquired = false;
3420
3444
  try {
@@ -15373,7 +15397,7 @@ var init_esm4 = __esm({
15373
15397
  *
15374
15398
  * @internal
15375
15399
  */
15376
- constructor(cwd = process.cwd(), pathImpl, sep3, { nocase, childrenCacheSize = 16 * 1024, fs: fs8 = defaultFS } = {}) {
15400
+ constructor(cwd = process.cwd(), pathImpl, sep4, { nocase, childrenCacheSize = 16 * 1024, fs: fs8 = defaultFS } = {}) {
15377
15401
  this.#fs = fsFromOption(fs8);
15378
15402
  if (cwd instanceof URL || cwd.startsWith("file://")) {
15379
15403
  cwd = fileURLToPath4(cwd);
@@ -15384,7 +15408,7 @@ var init_esm4 = __esm({
15384
15408
  this.#resolveCache = new ResolveCache();
15385
15409
  this.#resolvePosixCache = new ResolveCache();
15386
15410
  this.#children = new ChildrenCache(childrenCacheSize);
15387
- const split = cwdPath.substring(this.rootPath.length).split(sep3);
15411
+ const split = cwdPath.substring(this.rootPath.length).split(sep4);
15388
15412
  if (split.length === 1 && !split[0]) {
15389
15413
  split.pop();
15390
15414
  }
@@ -36300,7 +36324,7 @@ var init_graph_builder = __esm({
36300
36324
  applyLinkStyles() {
36301
36325
  if (!this.pendingLinkStyles.length || !this.edges.length)
36302
36326
  return;
36303
- const normalize2 = (s) => {
36327
+ const normalize3 = (s) => {
36304
36328
  const out = {};
36305
36329
  for (const [kRaw, vRaw] of Object.entries(s)) {
36306
36330
  const k = kRaw.trim().toLowerCase();
@@ -36321,7 +36345,7 @@ var init_graph_builder = __esm({
36321
36345
  return out;
36322
36346
  };
36323
36347
  for (const cmd of this.pendingLinkStyles) {
36324
- const style = normalize2(cmd.props);
36348
+ const style = normalize3(cmd.props);
36325
36349
  for (const idx of cmd.indices) {
36326
36350
  if (idx >= 0 && idx < this.edges.length) {
36327
36351
  const e = this.edges[idx];
@@ -43413,7 +43437,7 @@ var require_bk = __commonJS({
43413
43437
  return xs;
43414
43438
  }
43415
43439
  function buildBlockGraph(g, layering, root2, reverseSep) {
43416
- var blockGraph = new Graph(), graphLabel = g.graph(), sepFn = sep3(graphLabel.nodesep, graphLabel.edgesep, reverseSep);
43440
+ var blockGraph = new Graph(), graphLabel = g.graph(), sepFn = sep4(graphLabel.nodesep, graphLabel.edgesep, reverseSep);
43417
43441
  _.forEach(layering, function(layer) {
43418
43442
  var u;
43419
43443
  _.forEach(layer, function(v) {
@@ -43503,7 +43527,7 @@ var require_bk = __commonJS({
43503
43527
  alignCoordinates(xss, smallestWidth);
43504
43528
  return balance(xss, g.graph().align);
43505
43529
  }
43506
- function sep3(nodeSep, edgeSep, reverseSep) {
43530
+ function sep4(nodeSep, edgeSep, reverseSep) {
43507
43531
  return function(g, v, w) {
43508
43532
  var vLabel = g.node(v);
43509
43533
  var wLabel = g.node(w);
@@ -43588,7 +43612,7 @@ var require_layout = __commonJS({
43588
43612
  "use strict";
43589
43613
  var _ = require_lodash2();
43590
43614
  var acyclic = require_acyclic();
43591
- var normalize2 = require_normalize();
43615
+ var normalize3 = require_normalize();
43592
43616
  var rank = require_rank();
43593
43617
  var normalizeRanks = require_util().normalizeRanks;
43594
43618
  var parentDummyChains = require_parent_dummy_chains();
@@ -43650,7 +43674,7 @@ var require_layout = __commonJS({
43650
43674
  removeEdgeLabelProxies(g);
43651
43675
  });
43652
43676
  time(" normalize.run", function() {
43653
- normalize2.run(g);
43677
+ normalize3.run(g);
43654
43678
  });
43655
43679
  time(" parentDummyChains", function() {
43656
43680
  parentDummyChains(g);
@@ -43677,7 +43701,7 @@ var require_layout = __commonJS({
43677
43701
  removeBorderNodes(g);
43678
43702
  });
43679
43703
  time(" normalize.undo", function() {
43680
- normalize2.undo(g);
43704
+ normalize3.undo(g);
43681
43705
  });
43682
43706
  time(" fixupEdgeLabelCoords", function() {
43683
43707
  fixupEdgeLabelCoords(g);
@@ -50048,8 +50072,8 @@ var require_resolve = __commonJS({
50048
50072
  }
50049
50073
  return count;
50050
50074
  }
50051
- function getFullPath(resolver, id = "", normalize2) {
50052
- if (normalize2 !== false)
50075
+ function getFullPath(resolver, id = "", normalize3) {
50076
+ if (normalize3 !== false)
50053
50077
  id = normalizeId(id);
50054
50078
  const p = resolver.parse(id);
50055
50079
  return _getFullPath(resolver, p);
@@ -51389,7 +51413,7 @@ var require_fast_uri = __commonJS({
51389
51413
  "use strict";
51390
51414
  var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
51391
51415
  var { SCHEMES, getSchemeHandler } = require_schemes();
51392
- function normalize2(uri, options) {
51416
+ function normalize3(uri, options) {
51393
51417
  if (typeof uri === "string") {
51394
51418
  uri = /** @type {T} */
51395
51419
  serialize(parse6(uri, options), options);
@@ -51625,7 +51649,7 @@ var require_fast_uri = __commonJS({
51625
51649
  }
51626
51650
  var fastUri = {
51627
51651
  SCHEMES,
51628
- normalize: normalize2,
51652
+ normalize: normalize3,
51629
51653
  resolve: resolve6,
51630
51654
  resolveComponent,
51631
51655
  equal,
@@ -58993,7 +59017,7 @@ import { randomUUID as randomUUID5 } from "crypto";
58993
59017
  import { EventEmitter as EventEmitter5 } from "events";
58994
59018
  import { existsSync as existsSync5 } from "fs";
58995
59019
  import { readFile, stat } from "fs/promises";
58996
- import { resolve as resolve4, isAbsolute as isAbsolute2, dirname as dirname4 } from "path";
59020
+ import { resolve as resolve4, isAbsolute as isAbsolute2, dirname as dirname4, basename, normalize as normalize2, sep as sep3 } from "path";
58997
59021
  var MAX_TOOL_ITERATIONS, MAX_HISTORY_MESSAGES, MAX_IMAGE_FILE_SIZE, ProbeAgent;
58998
59022
  var init_ProbeAgent = __esm({
58999
59023
  "src/agent/ProbeAgent.js"() {
@@ -59319,6 +59343,14 @@ var init_ProbeAgent = __esm({
59319
59343
  if (!imagePath) {
59320
59344
  throw new Error("Image path is required");
59321
59345
  }
59346
+ const filename = basename(imagePath);
59347
+ const extension = filename.toLowerCase().split(".").pop();
59348
+ if (!extension || !SUPPORTED_IMAGE_EXTENSIONS.includes(extension)) {
59349
+ throw new Error(`Invalid or unsupported image extension: ${extension}. Supported formats: ${SUPPORTED_IMAGE_EXTENSIONS.join(", ")}`);
59350
+ }
59351
+ if (this.apiType && !isFormatSupportedByProvider(extension, this.apiType)) {
59352
+ throw new Error(`Image format '${extension}' is not supported by the current AI provider (${this.apiType}). Try using a different image format like PNG or JPEG.`);
59353
+ }
59322
59354
  const loaded = await this.loadImageIfValid(imagePath);
59323
59355
  if (!loaded) {
59324
59356
  throw new Error(`Failed to load image: ${imagePath}. The file may not exist, be too large, have an unsupported format, or be outside allowed directories.`);
@@ -59937,12 +59969,16 @@ var init_ProbeAgent = __esm({
59937
59969
  let absolutePath;
59938
59970
  let isPathAllowed2 = false;
59939
59971
  if (isAbsolute2(imagePath)) {
59940
- absolutePath = imagePath;
59941
- isPathAllowed2 = allowedDirs.some((dir) => absolutePath.startsWith(resolve4(dir)));
59972
+ absolutePath = normalize2(resolve4(imagePath));
59973
+ isPathAllowed2 = allowedDirs.some((dir) => {
59974
+ const normalizedDir = normalize2(resolve4(dir));
59975
+ return absolutePath === normalizedDir || absolutePath.startsWith(normalizedDir + sep3);
59976
+ });
59942
59977
  } else {
59943
59978
  for (const dir of allowedDirs) {
59944
- const resolvedPath = resolve4(dir, imagePath);
59945
- if (resolvedPath.startsWith(resolve4(dir))) {
59979
+ const normalizedDir = normalize2(resolve4(dir));
59980
+ const resolvedPath = normalize2(resolve4(dir, imagePath));
59981
+ if (resolvedPath === normalizedDir || resolvedPath.startsWith(normalizedDir + sep3)) {
59946
59982
  absolutePath = resolvedPath;
59947
59983
  isPathAllowed2 = true;
59948
59984
  break;
@@ -60950,8 +60986,8 @@ ${toolResultContent}
60950
60986
  // Pass parent session ID for tracking
60951
60987
  path: this.searchPath,
60952
60988
  // Inherit search path
60953
- provider: this.provider,
60954
- // Inherit AI provider
60989
+ provider: this.apiType,
60990
+ // Inherit AI provider (string identifier)
60955
60991
  model: this.model,
60956
60992
  // Inherit model
60957
60993
  debug: this.debug,
@@ -60960,7 +60996,7 @@ ${toolResultContent}
60960
60996
  if (this.debug) {
60961
60997
  console.log(`[DEBUG] Executing delegate tool at iteration ${currentIteration}/${maxIterations}`);
60962
60998
  console.log(`[DEBUG] Parent session: ${this.sessionId}`);
60963
- console.log(`[DEBUG] Inherited config: path=${this.searchPath}, provider=${this.provider}, model=${this.model}`);
60999
+ console.log(`[DEBUG] Inherited config: path=${this.searchPath}, provider=${this.apiType}, model=${this.model}`);
60964
61000
  console.log(`[DEBUG] Delegate task: ${toolParams.task?.substring(0, 100)}...`);
60965
61001
  }
60966
61002
  if (this.tracer) {
@@ -61027,9 +61063,6 @@ ${toolResultContent}
61027
61063
  role: "user",
61028
61064
  content: toolResultMessage
61029
61065
  });
61030
- if (toolResultContent) {
61031
- await this.processImageReferences(toolResultContent);
61032
- }
61033
61066
  if (this.debug) {
61034
61067
  console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === "string" ? toolResult.length : JSON.stringify(toolResult).length}`);
61035
61068
  }
package/build/delegate.js CHANGED
@@ -200,8 +200,10 @@ export async function delegate({
200
200
  // Calculate remaining iterations for subagent
201
201
  const remainingIterations = Math.max(1, maxIterations - currentIteration);
202
202
 
203
- // Create delegation span for telemetry if tracer is available
204
- const delegationSpan = tracer ? tracer.createDelegationSpan(sessionId, task) : null;
203
+ // Create delegation span for telemetry if tracer is available and has the method
204
+ const delegationSpan = typeof tracer?.createDelegationSpan === 'function'
205
+ ? tracer.createDelegationSpan(sessionId, task)
206
+ : null;
205
207
 
206
208
  let timeoutId = null;
207
209
  let acquired = false;