@probelabs/probe 0.6.0-rc168 → 0.6.0-rc170
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/build/agent/ProbeAgent.js +82 -31
- package/build/agent/imageConfig.js +57 -0
- package/build/agent/index.js +111 -47
- package/build/agent/tools.js +26 -9
- package/cjs/agent/ProbeAgent.cjs +211 -73
- package/cjs/index.cjs +211 -73
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +82 -31
- package/src/agent/imageConfig.js +57 -0
- package/src/agent/tools.js +26 -9
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -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,
|
|
@@ -420,14 +420,20 @@ export class ProbeAgent {
|
|
|
420
420
|
* Initialize tools with configuration
|
|
421
421
|
*/
|
|
422
422
|
initializeTools() {
|
|
423
|
+
const isToolAllowed = (toolName) => this.allowedTools.isEnabled(toolName);
|
|
424
|
+
|
|
423
425
|
const configOptions = {
|
|
424
426
|
sessionId: this.sessionId,
|
|
425
427
|
debug: this.debug,
|
|
426
428
|
defaultPath: this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd(),
|
|
427
429
|
allowedFolders: this.allowedFolders,
|
|
428
430
|
outline: this.outline,
|
|
431
|
+
allowEdit: this.allowEdit,
|
|
432
|
+
enableDelegate: this.enableDelegate,
|
|
429
433
|
enableBash: this.enableBash,
|
|
430
|
-
bashConfig: this.bashConfig
|
|
434
|
+
bashConfig: this.bashConfig,
|
|
435
|
+
allowedTools: this.allowedTools,
|
|
436
|
+
isToolAllowed
|
|
431
437
|
};
|
|
432
438
|
|
|
433
439
|
// Create base tools
|
|
@@ -436,21 +442,54 @@ export class ProbeAgent {
|
|
|
436
442
|
// Create wrapped tools with event emission
|
|
437
443
|
const wrappedTools = createWrappedTools(baseTools);
|
|
438
444
|
|
|
439
|
-
// Store tool instances for execution
|
|
440
|
-
this.toolImplementations = {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
445
|
+
// Store tool instances for execution (respect allowedTools + feature flags)
|
|
446
|
+
this.toolImplementations = {};
|
|
447
|
+
|
|
448
|
+
if (wrappedTools.searchToolInstance && isToolAllowed('search')) {
|
|
449
|
+
this.toolImplementations.search = wrappedTools.searchToolInstance;
|
|
450
|
+
}
|
|
451
|
+
if (wrappedTools.queryToolInstance && isToolAllowed('query')) {
|
|
452
|
+
this.toolImplementations.query = wrappedTools.queryToolInstance;
|
|
453
|
+
}
|
|
454
|
+
if (wrappedTools.extractToolInstance && isToolAllowed('extract')) {
|
|
455
|
+
this.toolImplementations.extract = wrappedTools.extractToolInstance;
|
|
456
|
+
}
|
|
457
|
+
if (this.enableDelegate && wrappedTools.delegateToolInstance && isToolAllowed('delegate')) {
|
|
458
|
+
this.toolImplementations.delegate = wrappedTools.delegateToolInstance;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// File browsing tools
|
|
462
|
+
if (isToolAllowed('listFiles')) {
|
|
463
|
+
this.toolImplementations.listFiles = listFilesToolInstance;
|
|
464
|
+
}
|
|
465
|
+
if (isToolAllowed('searchFiles')) {
|
|
466
|
+
this.toolImplementations.searchFiles = searchFilesToolInstance;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Image loading tool
|
|
470
|
+
if (isToolAllowed('readImage')) {
|
|
471
|
+
this.toolImplementations.readImage = {
|
|
448
472
|
execute: async (params) => {
|
|
449
473
|
const imagePath = params.path;
|
|
450
474
|
if (!imagePath) {
|
|
451
475
|
throw new Error('Image path is required');
|
|
452
476
|
}
|
|
453
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
|
+
|
|
454
493
|
// Load the image using the existing loadImageIfValid method
|
|
455
494
|
const loaded = await this.loadImageIfValid(imagePath);
|
|
456
495
|
|
|
@@ -460,20 +499,20 @@ export class ProbeAgent {
|
|
|
460
499
|
|
|
461
500
|
return `Image loaded successfully: ${imagePath}. The image is now available for analysis in the conversation.`;
|
|
462
501
|
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
465
504
|
|
|
466
|
-
// Add bash tool if enabled
|
|
467
|
-
if (this.enableBash && wrappedTools.bashToolInstance) {
|
|
505
|
+
// Add bash tool if enabled and allowed
|
|
506
|
+
if (this.enableBash && wrappedTools.bashToolInstance && isToolAllowed('bash')) {
|
|
468
507
|
this.toolImplementations.bash = wrappedTools.bashToolInstance;
|
|
469
508
|
}
|
|
470
509
|
|
|
471
|
-
// Add edit and create tools if enabled
|
|
510
|
+
// Add edit and create tools if enabled and allowed
|
|
472
511
|
if (this.allowEdit) {
|
|
473
|
-
if (wrappedTools.editToolInstance) {
|
|
512
|
+
if (wrappedTools.editToolInstance && isToolAllowed('edit')) {
|
|
474
513
|
this.toolImplementations.edit = wrappedTools.editToolInstance;
|
|
475
514
|
}
|
|
476
|
-
if (wrappedTools.createToolInstance) {
|
|
515
|
+
if (wrappedTools.createToolInstance && isToolAllowed('create')) {
|
|
477
516
|
this.toolImplementations.create = wrappedTools.createToolInstance;
|
|
478
517
|
}
|
|
479
518
|
}
|
|
@@ -1238,20 +1277,28 @@ export class ProbeAgent {
|
|
|
1238
1277
|
}
|
|
1239
1278
|
|
|
1240
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')
|
|
1241
1281
|
const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
|
|
1242
|
-
|
|
1282
|
+
|
|
1243
1283
|
let absolutePath;
|
|
1244
1284
|
let isPathAllowed = false;
|
|
1245
|
-
|
|
1285
|
+
|
|
1246
1286
|
// If absolute path, check if it's within any allowed directory
|
|
1247
1287
|
if (isAbsolute(imagePath)) {
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
+
});
|
|
1250
1295
|
} else {
|
|
1251
1296
|
// For relative paths, try resolving against each allowed directory
|
|
1252
1297
|
for (const dir of allowedDirs) {
|
|
1253
|
-
const
|
|
1254
|
-
|
|
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)) {
|
|
1255
1302
|
absolutePath = resolvedPath;
|
|
1256
1303
|
isPathAllowed = true;
|
|
1257
1304
|
break;
|
|
@@ -1295,6 +1342,10 @@ export class ProbeAgent {
|
|
|
1295
1342
|
return false;
|
|
1296
1343
|
}
|
|
1297
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
|
+
|
|
1298
1349
|
// Determine MIME type (from shared config)
|
|
1299
1350
|
const mimeType = IMAGE_MIME_TYPES[extension];
|
|
1300
1351
|
|
|
@@ -2480,7 +2531,7 @@ When troubleshooting:
|
|
|
2480
2531
|
maxIterations,
|
|
2481
2532
|
parentSessionId: this.sessionId, // Pass parent session ID for tracking
|
|
2482
2533
|
path: this.searchPath, // Inherit search path
|
|
2483
|
-
provider: this.
|
|
2534
|
+
provider: this.apiType, // Inherit AI provider (string identifier)
|
|
2484
2535
|
model: this.model, // Inherit model
|
|
2485
2536
|
debug: this.debug,
|
|
2486
2537
|
tracer: this.tracer
|
|
@@ -2489,7 +2540,7 @@ When troubleshooting:
|
|
|
2489
2540
|
if (this.debug) {
|
|
2490
2541
|
console.log(`[DEBUG] Executing delegate tool at iteration ${currentIteration}/${maxIterations}`);
|
|
2491
2542
|
console.log(`[DEBUG] Parent session: ${this.sessionId}`);
|
|
2492
|
-
console.log(`[DEBUG] Inherited config: path=${this.searchPath}, provider=${this.
|
|
2543
|
+
console.log(`[DEBUG] Inherited config: path=${this.searchPath}, provider=${this.apiType}, model=${this.model}`);
|
|
2493
2544
|
console.log(`[DEBUG] Delegate task: ${toolParams.task?.substring(0, 100)}...`);
|
|
2494
2545
|
}
|
|
2495
2546
|
|
|
@@ -2573,10 +2624,10 @@ When troubleshooting:
|
|
|
2573
2624
|
content: toolResultMessage
|
|
2574
2625
|
});
|
|
2575
2626
|
|
|
2576
|
-
//
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
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)
|
|
2580
2631
|
|
|
2581
2632
|
if (this.debug) {
|
|
2582
2633
|
console.log(`[DEBUG] Tool ${toolName} executed successfully. Result length: ${typeof toolResult === 'string' ? toolResult.length : JSON.stringify(toolResult).length}`);
|
package/src/agent/imageConfig.js
CHANGED
|
@@ -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
|
+
}
|
package/src/agent/tools.js
CHANGED
|
@@ -31,21 +31,39 @@ import { processXmlWithThinkingAndRecovery } from './xmlParsingUtils.js';
|
|
|
31
31
|
|
|
32
32
|
// Create configured tool instances
|
|
33
33
|
export function createTools(configOptions) {
|
|
34
|
-
const tools = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
const tools = {};
|
|
35
|
+
|
|
36
|
+
const isToolAllowed =
|
|
37
|
+
configOptions.isToolAllowed ||
|
|
38
|
+
((toolName) => {
|
|
39
|
+
if (!configOptions.allowedTools) return true;
|
|
40
|
+
return configOptions.allowedTools.isEnabled(toolName);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Core tools
|
|
44
|
+
if (isToolAllowed('search')) {
|
|
45
|
+
tools.searchTool = searchTool(configOptions);
|
|
46
|
+
}
|
|
47
|
+
if (isToolAllowed('query')) {
|
|
48
|
+
tools.queryTool = queryTool(configOptions);
|
|
49
|
+
}
|
|
50
|
+
if (isToolAllowed('extract')) {
|
|
51
|
+
tools.extractTool = extractTool(configOptions);
|
|
52
|
+
}
|
|
53
|
+
if (configOptions.enableDelegate && isToolAllowed('delegate')) {
|
|
54
|
+
tools.delegateTool = delegateTool(configOptions);
|
|
55
|
+
}
|
|
40
56
|
|
|
41
57
|
// Add bash tool if enabled
|
|
42
|
-
if (configOptions.enableBash) {
|
|
58
|
+
if (configOptions.enableBash && isToolAllowed('bash')) {
|
|
43
59
|
tools.bashTool = bashTool(configOptions);
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
// Add edit and create tools if enabled
|
|
47
|
-
if (configOptions.allowEdit) {
|
|
63
|
+
if (configOptions.allowEdit && isToolAllowed('edit')) {
|
|
48
64
|
tools.editTool = editTool(configOptions);
|
|
65
|
+
}
|
|
66
|
+
if (configOptions.allowEdit && isToolAllowed('create')) {
|
|
49
67
|
tools.createTool = createTool(configOptions);
|
|
50
68
|
}
|
|
51
69
|
|
|
@@ -199,4 +217,3 @@ export function parseXmlToolCallWithThinking(xmlString, validTools) {
|
|
|
199
217
|
// Otherwise, use the original parseXmlToolCall function to parse the cleaned XML string
|
|
200
218
|
return parseXmlToolCall(cleanedXmlString, validTools);
|
|
201
219
|
}
|
|
202
|
-
|