@probelabs/probe 0.6.0-rc225 → 0.6.0-rc227
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/bin/binaries/probe-v0.6.0-rc227-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc227-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc227-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc227-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc227-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.d.ts +24 -0
- package/build/agent/ProbeAgent.js +310 -141
- package/build/agent/engines/enhanced-claude-code.js +72 -3
- package/build/agent/index.js +386 -129
- package/build/tools/analyzeAll.js +6 -1
- package/build/tools/bash.js +18 -3
- package/build/tools/edit.js +19 -10
- package/build/tools/vercel.js +17 -7
- package/build/utils/path-validation.js +148 -1
- package/cjs/agent/ProbeAgent.cjs +683 -389
- package/cjs/index.cjs +680 -389
- package/package.json +1 -1
- package/src/agent/ProbeAgent.d.ts +24 -0
- package/src/agent/ProbeAgent.js +310 -141
- package/src/agent/engines/enhanced-claude-code.js +72 -3
- package/src/tools/analyzeAll.js +6 -1
- package/src/tools/bash.js +18 -3
- package/src/tools/edit.js +19 -10
- package/src/tools/vercel.js +17 -7
- package/src/utils/path-validation.js +148 -1
- package/bin/binaries/probe-v0.6.0-rc225-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc225-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc225-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc225-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc225-x86_64-unknown-linux-musl.tar.gz +0 -0
package/src/agent/ProbeAgent.js
CHANGED
|
@@ -4,6 +4,29 @@
|
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
5
|
dotenv.config();
|
|
6
6
|
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Timeout Configuration Constants
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default activity timeout for engine streams (3 minutes).
|
|
13
|
+
* This is the time allowed between stream chunks before considering the stream stalled.
|
|
14
|
+
* Conservative default to handle extended thinking models that may not stream during thinking.
|
|
15
|
+
*/
|
|
16
|
+
export const ENGINE_ACTIVITY_TIMEOUT_DEFAULT = 180000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimum allowed activity timeout (5 seconds).
|
|
20
|
+
* Prevents unreasonably short timeouts that could cause premature failures.
|
|
21
|
+
*/
|
|
22
|
+
export const ENGINE_ACTIVITY_TIMEOUT_MIN = 5000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maximum allowed activity timeout (10 minutes).
|
|
26
|
+
* Prevents excessively long waits for stalled streams.
|
|
27
|
+
*/
|
|
28
|
+
export const ENGINE_ACTIVITY_TIMEOUT_MAX = 600000;
|
|
29
|
+
|
|
7
30
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
8
31
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
9
32
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
@@ -71,6 +94,7 @@ import { RetryManager, createRetryManagerFromEnv } from './RetryManager.js';
|
|
|
71
94
|
import { FallbackManager, createFallbackManagerFromEnv, buildFallbackProvidersFromEnv } from './FallbackManager.js';
|
|
72
95
|
import { handleContextLimitError } from './contextCompactor.js';
|
|
73
96
|
import { formatErrorForAI, ParameterError } from '../utils/error-types.js';
|
|
97
|
+
import { getCommonPrefix, toRelativePath, safeRealpath } from '../utils/path-validation.js';
|
|
74
98
|
import { truncateIfNeeded, getMaxOutputTokens } from './outputTruncator.js';
|
|
75
99
|
import { DelegationManager } from '../delegate.js';
|
|
76
100
|
import {
|
|
@@ -188,6 +212,8 @@ export class ProbeAgent {
|
|
|
188
212
|
* @param {number} [options.fallback.maxTotalAttempts=10] - Maximum total attempts across all providers
|
|
189
213
|
* @param {string} [options.completionPrompt] - Custom prompt to run after attempt_completion for validation/review (runs before mermaid/JSON validation)
|
|
190
214
|
* @param {number} [options.maxOutputTokens] - Maximum tokens for tool output before truncation (default: 20000, can also be set via PROBE_MAX_OUTPUT_TOKENS env var)
|
|
215
|
+
* @param {number} [options.requestTimeout] - Timeout in ms for AI requests (default: 120000 or REQUEST_TIMEOUT env var). Used to abort hung requests.
|
|
216
|
+
* @param {number} [options.maxOperationTimeout] - Maximum timeout in ms for the entire operation including all retries and fallbacks (default: 300000 or MAX_OPERATION_TIMEOUT env var). This is the absolute maximum time for streamTextWithRetryAndFallback.
|
|
191
217
|
*/
|
|
192
218
|
constructor(options = {}) {
|
|
193
219
|
// Basic configuration
|
|
@@ -269,8 +295,15 @@ export class ProbeAgent {
|
|
|
269
295
|
this.allowedFolders = [process.cwd()];
|
|
270
296
|
}
|
|
271
297
|
|
|
272
|
-
//
|
|
273
|
-
|
|
298
|
+
// Compute workspace root as common prefix of all allowed folders
|
|
299
|
+
// This provides a single "root" for relative path resolution and default cwd
|
|
300
|
+
// IMPORTANT: workspaceRoot is NOT a security boundary - all security checks
|
|
301
|
+
// must be performed against this.allowedFolders, not workspaceRoot
|
|
302
|
+
this.workspaceRoot = getCommonPrefix(this.allowedFolders);
|
|
303
|
+
|
|
304
|
+
// Working directory for resolving relative paths
|
|
305
|
+
// If not explicitly provided, use workspace root for consistency
|
|
306
|
+
this.cwd = options.cwd || this.workspaceRoot;
|
|
274
307
|
|
|
275
308
|
// API configuration
|
|
276
309
|
this.clientApiProvider = options.provider || null;
|
|
@@ -289,6 +322,8 @@ export class ProbeAgent {
|
|
|
289
322
|
console.log(`[DEBUG] Maximum tool iterations configured: ${MAX_TOOL_ITERATIONS}`);
|
|
290
323
|
console.log(`[DEBUG] Allow Edit (implement tool): ${this.allowEdit}`);
|
|
291
324
|
console.log(`[DEBUG] Search delegation enabled: ${this.searchDelegate}`);
|
|
325
|
+
console.log(`[DEBUG] Workspace root: ${this.workspaceRoot}`);
|
|
326
|
+
console.log(`[DEBUG] Working directory (cwd): ${this.cwd}`);
|
|
292
327
|
}
|
|
293
328
|
|
|
294
329
|
// Initialize tools
|
|
@@ -320,6 +355,41 @@ export class ProbeAgent {
|
|
|
320
355
|
// Each ProbeAgent instance has its own limits, not shared globally
|
|
321
356
|
this.delegationManager = new DelegationManager();
|
|
322
357
|
|
|
358
|
+
// Request timeout configuration (default 2 minutes)
|
|
359
|
+
// Validates env var to prevent NaN or unreasonable values
|
|
360
|
+
this.requestTimeout = options.requestTimeout ?? (() => {
|
|
361
|
+
if (process.env.REQUEST_TIMEOUT) {
|
|
362
|
+
const parsed = parseInt(process.env.REQUEST_TIMEOUT, 10);
|
|
363
|
+
// Validate: must be positive number between 1s and 1 hour
|
|
364
|
+
if (isNaN(parsed) || parsed < 1000 || parsed > 3600000) {
|
|
365
|
+
return 120000; // Default 2 minutes
|
|
366
|
+
}
|
|
367
|
+
return parsed;
|
|
368
|
+
}
|
|
369
|
+
return 120000;
|
|
370
|
+
})();
|
|
371
|
+
if (this.debug) {
|
|
372
|
+
console.log(`[DEBUG] Request timeout: ${this.requestTimeout}ms`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Maximum operation timeout for entire streamTextWithRetryAndFallback operation (default 5 minutes)
|
|
376
|
+
// This is the absolute maximum time including all retries and fallbacks
|
|
377
|
+
// Validates env var to prevent NaN or unreasonable values
|
|
378
|
+
this.maxOperationTimeout = options.maxOperationTimeout ?? (() => {
|
|
379
|
+
if (process.env.MAX_OPERATION_TIMEOUT) {
|
|
380
|
+
const parsed = parseInt(process.env.MAX_OPERATION_TIMEOUT, 10);
|
|
381
|
+
// Validate: must be positive number between 1s and 2 hours
|
|
382
|
+
if (isNaN(parsed) || parsed < 1000 || parsed > 7200000) {
|
|
383
|
+
return 300000; // Default 5 minutes
|
|
384
|
+
}
|
|
385
|
+
return parsed;
|
|
386
|
+
}
|
|
387
|
+
return 300000;
|
|
388
|
+
})();
|
|
389
|
+
if (this.debug) {
|
|
390
|
+
console.log(`[DEBUG] Max operation timeout: ${this.maxOperationTimeout}ms`);
|
|
391
|
+
}
|
|
392
|
+
|
|
323
393
|
// Retry configuration
|
|
324
394
|
this.retryConfig = options.retry || {};
|
|
325
395
|
this.retryManager = null; // Will be initialized lazily when needed
|
|
@@ -732,8 +802,9 @@ export class ProbeAgent {
|
|
|
732
802
|
const configOptions = {
|
|
733
803
|
sessionId: this.sessionId,
|
|
734
804
|
debug: this.debug,
|
|
735
|
-
// Use
|
|
736
|
-
cwd: this.cwd
|
|
805
|
+
// Use cwd (which defaults to workspaceRoot in constructor)
|
|
806
|
+
cwd: this.cwd,
|
|
807
|
+
workspaceRoot: this.workspaceRoot,
|
|
737
808
|
allowedFolders: this.allowedFolders,
|
|
738
809
|
outline: this.outline,
|
|
739
810
|
searchDelegate: this.searchDelegate,
|
|
@@ -1094,120 +1165,128 @@ export class ProbeAgent {
|
|
|
1094
1165
|
}
|
|
1095
1166
|
|
|
1096
1167
|
/**
|
|
1097
|
-
*
|
|
1098
|
-
* @param {
|
|
1099
|
-
* @
|
|
1168
|
+
* Create a streamText-compatible result from an engine stream with timeout handling
|
|
1169
|
+
* @param {AsyncGenerator} engineStream - The engine's query result
|
|
1170
|
+
* @param {AbortSignal} abortSignal - Signal for aborting the operation
|
|
1171
|
+
* @param {number} requestTimeout - Per-request timeout in ms
|
|
1172
|
+
* @param {Object} timeoutState - Object with timeoutId property (mutable for cleanup)
|
|
1173
|
+
* @returns {Object} - streamText-compatible result with textStream
|
|
1100
1174
|
* @private
|
|
1101
1175
|
*/
|
|
1102
|
-
|
|
1103
|
-
//
|
|
1104
|
-
|
|
1176
|
+
_createEngineTextStreamResult(engineStream, abortSignal, requestTimeout, timeoutState) {
|
|
1177
|
+
// Activity timeout for engine stream - validates env var against defined bounds
|
|
1178
|
+
const activityTimeout = (() => {
|
|
1179
|
+
const parsed = parseInt(process.env.ENGINE_ACTIVITY_TIMEOUT, 10);
|
|
1180
|
+
return isNaN(parsed) || parsed < ENGINE_ACTIVITY_TIMEOUT_MIN || parsed > ENGINE_ACTIVITY_TIMEOUT_MAX
|
|
1181
|
+
? ENGINE_ACTIVITY_TIMEOUT_DEFAULT
|
|
1182
|
+
: parsed;
|
|
1183
|
+
})();
|
|
1184
|
+
const startTime = Date.now();
|
|
1185
|
+
|
|
1186
|
+
// Create a text stream that extracts text from engine messages with timeout
|
|
1187
|
+
// The generator clears the operation timeout when done to handle the case
|
|
1188
|
+
// where the stream is returned immediately but consumed later
|
|
1189
|
+
async function* createTextStream() {
|
|
1190
|
+
let lastActivity = Date.now();
|
|
1191
|
+
|
|
1105
1192
|
try {
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
m.role === 'user' &&
|
|
1113
|
-
!m.content.includes('WARNING: You have reached the maximum tool iterations limit')
|
|
1114
|
-
);
|
|
1115
|
-
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
1116
|
-
const prompt = lastUserMessage ? lastUserMessage.content : '';
|
|
1117
|
-
|
|
1118
|
-
// Pass system message and other options
|
|
1119
|
-
const engineOptions = {
|
|
1120
|
-
maxTokens: options.maxTokens,
|
|
1121
|
-
temperature: options.temperature,
|
|
1122
|
-
messages: options.messages,
|
|
1123
|
-
systemPrompt: options.messages.find(m => m.role === 'system')?.content
|
|
1124
|
-
};
|
|
1125
|
-
|
|
1126
|
-
// Get the engine's query result (async generator)
|
|
1127
|
-
const engineStream = engine.query(prompt, engineOptions);
|
|
1128
|
-
|
|
1129
|
-
// Create a text stream that extracts text from engine messages
|
|
1130
|
-
async function* createTextStream() {
|
|
1131
|
-
for await (const message of engineStream) {
|
|
1132
|
-
if (message.type === 'text' && message.content) {
|
|
1133
|
-
yield message.content;
|
|
1134
|
-
} else if (typeof message === 'string') {
|
|
1135
|
-
// If engine returns plain strings, pass them through
|
|
1136
|
-
yield message;
|
|
1137
|
-
}
|
|
1138
|
-
// Ignore other message types for the text stream
|
|
1139
|
-
}
|
|
1193
|
+
for await (const message of engineStream) {
|
|
1194
|
+
// Check for abort signal
|
|
1195
|
+
if (abortSignal.aborted) {
|
|
1196
|
+
const abortError = new Error('Operation aborted');
|
|
1197
|
+
abortError.name = 'AbortError';
|
|
1198
|
+
throw abortError;
|
|
1140
1199
|
}
|
|
1141
1200
|
|
|
1142
|
-
|
|
1143
|
-
return {
|
|
1144
|
-
textStream: createTextStream(),
|
|
1145
|
-
usage: Promise.resolve({}), // Engine should handle its own usage tracking
|
|
1146
|
-
// Add other streamText-compatible properties as needed
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
} catch (error) {
|
|
1150
|
-
if (this.debug) {
|
|
1151
|
-
console.log(`[DEBUG] Failed to use Claude Code engine, falling back to Vercel:`, error.message);
|
|
1152
|
-
}
|
|
1153
|
-
// Fall through to use Vercel engine as fallback
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1201
|
+
const now = Date.now();
|
|
1156
1202
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
//
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
const userMessages = options.messages.filter(m =>
|
|
1166
|
-
m.role === 'user' &&
|
|
1167
|
-
!m.content.includes('WARNING: You have reached the maximum tool iterations limit')
|
|
1168
|
-
);
|
|
1169
|
-
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
1170
|
-
const prompt = lastUserMessage ? lastUserMessage.content : '';
|
|
1171
|
-
|
|
1172
|
-
// Pass system message and other options
|
|
1173
|
-
const engineOptions = {
|
|
1174
|
-
maxTokens: options.maxTokens,
|
|
1175
|
-
temperature: options.temperature,
|
|
1176
|
-
messages: options.messages,
|
|
1177
|
-
systemPrompt: options.messages.find(m => m.role === 'system')?.content
|
|
1178
|
-
};
|
|
1179
|
-
|
|
1180
|
-
// Get the engine's query result (async generator)
|
|
1181
|
-
const engineStream = engine.query(prompt, engineOptions);
|
|
1182
|
-
|
|
1183
|
-
// Create a text stream that extracts text from engine messages
|
|
1184
|
-
async function* createTextStream() {
|
|
1185
|
-
for await (const message of engineStream) {
|
|
1186
|
-
if (message.type === 'text' && message.content) {
|
|
1187
|
-
yield message.content;
|
|
1188
|
-
} else if (typeof message === 'string') {
|
|
1189
|
-
// If engine returns plain strings, pass them through
|
|
1190
|
-
yield message;
|
|
1191
|
-
}
|
|
1192
|
-
// Ignore other message types for the text stream
|
|
1193
|
-
}
|
|
1203
|
+
// Check for activity timeout (no data received for too long)
|
|
1204
|
+
if (now - lastActivity > activityTimeout) {
|
|
1205
|
+
throw new Error(`Engine stream timeout - no activity for ${activityTimeout}ms`);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Check for overall request timeout
|
|
1209
|
+
if (requestTimeout > 0 && now - startTime > requestTimeout) {
|
|
1210
|
+
throw new Error(`Engine stream timeout - request exceeded ${requestTimeout}ms`);
|
|
1194
1211
|
}
|
|
1195
1212
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1213
|
+
lastActivity = now;
|
|
1214
|
+
|
|
1215
|
+
if (message.type === 'text' && message.content) {
|
|
1216
|
+
yield message.content;
|
|
1217
|
+
} else if (typeof message === 'string') {
|
|
1218
|
+
// If engine returns plain strings, pass them through
|
|
1219
|
+
yield message;
|
|
1220
|
+
}
|
|
1221
|
+
// Ignore other message types for the text stream
|
|
1202
1222
|
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1223
|
+
} finally {
|
|
1224
|
+
// Clear operation timeout when stream completes (success or error)
|
|
1225
|
+
// This is done here because for engine paths, the stream is returned
|
|
1226
|
+
// immediately but consumed later by the caller
|
|
1227
|
+
if (timeoutState.timeoutId) {
|
|
1228
|
+
clearTimeout(timeoutState.timeoutId);
|
|
1229
|
+
timeoutState.timeoutId = null;
|
|
1206
1230
|
}
|
|
1207
|
-
// Fall through to use Vercel engine as fallback
|
|
1208
1231
|
}
|
|
1209
1232
|
}
|
|
1210
1233
|
|
|
1234
|
+
// Wrap the engine result to match streamText interface
|
|
1235
|
+
// Note: maxOperationTimeout cleanup is handled by the generator's finally block
|
|
1236
|
+
// since the stream is consumed after this function returns.
|
|
1237
|
+
return {
|
|
1238
|
+
textStream: createTextStream(),
|
|
1239
|
+
usage: Promise.resolve({}), // Engine should handle its own usage tracking
|
|
1240
|
+
// Add other streamText-compatible properties as needed
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* Try to use an engine (claude-code or codex) for streaming
|
|
1246
|
+
* @param {Object} options - streamText options
|
|
1247
|
+
* @param {AbortController} controller - Abort controller for the operation
|
|
1248
|
+
* @param {Object} timeoutState - Mutable timeout state for cleanup
|
|
1249
|
+
* @returns {Promise<Object|null>} - Stream result or null if engine unavailable
|
|
1250
|
+
* @private
|
|
1251
|
+
*/
|
|
1252
|
+
async _tryEngineStreamPath(options, controller, timeoutState) {
|
|
1253
|
+
const engine = await this.getEngine();
|
|
1254
|
+
if (!engine || !engine.query) {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// Extract the ORIGINAL user message as the main prompt (skip any warning messages)
|
|
1259
|
+
const userMessages = options.messages.filter(m =>
|
|
1260
|
+
m.role === 'user' &&
|
|
1261
|
+
!m.content.includes('WARNING: You have reached the maximum tool iterations limit')
|
|
1262
|
+
);
|
|
1263
|
+
const lastUserMessage = userMessages[userMessages.length - 1];
|
|
1264
|
+
const prompt = lastUserMessage ? lastUserMessage.content : '';
|
|
1265
|
+
|
|
1266
|
+
// Pass system message and other options including abort signal
|
|
1267
|
+
const engineOptions = {
|
|
1268
|
+
maxTokens: options.maxTokens,
|
|
1269
|
+
temperature: options.temperature,
|
|
1270
|
+
messages: options.messages,
|
|
1271
|
+
systemPrompt: options.messages.find(m => m.role === 'system')?.content,
|
|
1272
|
+
abortSignal: controller.signal
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
// Get the engine's query result and wrap with timeout handling
|
|
1276
|
+
const engineStream = engine.query(prompt, engineOptions);
|
|
1277
|
+
return this._createEngineTextStreamResult(
|
|
1278
|
+
engineStream, controller.signal, this.requestTimeout, timeoutState
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* Execute streamText with Vercel AI SDK using retry/fallback logic
|
|
1284
|
+
* @param {Object} options - streamText options
|
|
1285
|
+
* @param {AbortController} controller - Abort controller for the operation
|
|
1286
|
+
* @returns {Promise<Object>} - Stream result
|
|
1287
|
+
* @private
|
|
1288
|
+
*/
|
|
1289
|
+
async _executeWithVercelProvider(options, controller) {
|
|
1211
1290
|
// Initialize retry manager if not already created
|
|
1212
1291
|
if (!this.retryManager) {
|
|
1213
1292
|
this.retryManager = new RetryManager({
|
|
@@ -1223,10 +1302,11 @@ export class ProbeAgent {
|
|
|
1223
1302
|
// If no fallback manager, just use retry with current provider
|
|
1224
1303
|
if (!this.fallbackManager) {
|
|
1225
1304
|
return await this.retryManager.executeWithRetry(
|
|
1226
|
-
() => streamText(options),
|
|
1305
|
+
() => streamText({ ...options, abortSignal: controller.signal }),
|
|
1227
1306
|
{
|
|
1228
1307
|
provider: this.apiType,
|
|
1229
|
-
model: this.model
|
|
1308
|
+
model: this.model,
|
|
1309
|
+
signal: controller.signal
|
|
1230
1310
|
}
|
|
1231
1311
|
);
|
|
1232
1312
|
}
|
|
@@ -1234,13 +1314,12 @@ export class ProbeAgent {
|
|
|
1234
1314
|
// Use fallback manager with retry for each provider
|
|
1235
1315
|
return await this.fallbackManager.executeWithFallback(
|
|
1236
1316
|
async (provider, model, config) => {
|
|
1237
|
-
// Create options with the fallback provider
|
|
1238
1317
|
const fallbackOptions = {
|
|
1239
1318
|
...options,
|
|
1240
|
-
model: provider(model)
|
|
1319
|
+
model: provider(model),
|
|
1320
|
+
abortSignal: controller.signal
|
|
1241
1321
|
};
|
|
1242
1322
|
|
|
1243
|
-
// Create a retry manager for this specific provider
|
|
1244
1323
|
const providerRetryManager = new RetryManager({
|
|
1245
1324
|
maxRetries: config.maxRetries ?? this.retryConfig.maxRetries ?? 3,
|
|
1246
1325
|
initialDelay: this.retryConfig.initialDelay ?? 1000,
|
|
@@ -1250,18 +1329,70 @@ export class ProbeAgent {
|
|
|
1250
1329
|
debug: this.debug
|
|
1251
1330
|
});
|
|
1252
1331
|
|
|
1253
|
-
// Execute with retry for this provider
|
|
1254
1332
|
return await providerRetryManager.executeWithRetry(
|
|
1255
1333
|
() => streamText(fallbackOptions),
|
|
1256
1334
|
{
|
|
1257
1335
|
provider: config.provider,
|
|
1258
|
-
model: model
|
|
1336
|
+
model: model,
|
|
1337
|
+
signal: controller.signal
|
|
1259
1338
|
}
|
|
1260
1339
|
);
|
|
1261
1340
|
}
|
|
1262
1341
|
);
|
|
1263
1342
|
}
|
|
1264
1343
|
|
|
1344
|
+
/**
|
|
1345
|
+
* Execute streamText with retry and fallback support
|
|
1346
|
+
* @param {Object} options - streamText options
|
|
1347
|
+
* @returns {Promise<Object>} - streamText result
|
|
1348
|
+
* @private
|
|
1349
|
+
*/
|
|
1350
|
+
async streamTextWithRetryAndFallback(options) {
|
|
1351
|
+
// Create AbortController for overall operation timeout
|
|
1352
|
+
const controller = new AbortController();
|
|
1353
|
+
const timeoutState = { timeoutId: null };
|
|
1354
|
+
|
|
1355
|
+
// Set up overall operation timeout (default 5 minutes)
|
|
1356
|
+
if (this.maxOperationTimeout && this.maxOperationTimeout > 0) {
|
|
1357
|
+
timeoutState.timeoutId = setTimeout(() => {
|
|
1358
|
+
controller.abort();
|
|
1359
|
+
if (this.debug) {
|
|
1360
|
+
console.log(`[DEBUG] Operation timed out after ${this.maxOperationTimeout}ms (max operation timeout)`);
|
|
1361
|
+
}
|
|
1362
|
+
}, this.maxOperationTimeout);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
try {
|
|
1366
|
+
// Try engine paths (claude-code or codex)
|
|
1367
|
+
const useClaudeCode = this.clientApiProvider === 'claude-code' || process.env.USE_CLAUDE_CODE === 'true';
|
|
1368
|
+
const useCodex = this.clientApiProvider === 'codex' || process.env.USE_CODEX === 'true';
|
|
1369
|
+
|
|
1370
|
+
if (useClaudeCode || useCodex) {
|
|
1371
|
+
try {
|
|
1372
|
+
const result = await this._tryEngineStreamPath(options, controller, timeoutState);
|
|
1373
|
+
if (result) {
|
|
1374
|
+
return result;
|
|
1375
|
+
}
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
if (this.debug) {
|
|
1378
|
+
const engineType = useClaudeCode ? 'Claude Code' : 'Codex';
|
|
1379
|
+
console.log(`[DEBUG] Failed to use ${engineType} engine, falling back to Vercel:`, error.message);
|
|
1380
|
+
}
|
|
1381
|
+
// Fall through to Vercel provider
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Use Vercel AI SDK with retry/fallback
|
|
1386
|
+
return await this._executeWithVercelProvider(options, controller);
|
|
1387
|
+
} finally {
|
|
1388
|
+
// Clean up timeout (for non-engine paths; engine paths clean up in the generator)
|
|
1389
|
+
if (timeoutState.timeoutId) {
|
|
1390
|
+
clearTimeout(timeoutState.timeoutId);
|
|
1391
|
+
timeoutState.timeoutId = null;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1265
1396
|
/**
|
|
1266
1397
|
* Initialize Anthropic model
|
|
1267
1398
|
*/
|
|
@@ -1612,7 +1743,8 @@ export class ProbeAgent {
|
|
|
1612
1743
|
}
|
|
1613
1744
|
|
|
1614
1745
|
// Security validation: check if path is within any allowed directory
|
|
1615
|
-
// Use
|
|
1746
|
+
// Use safeRealpath() to resolve symlinks and handle path traversal attempts (e.g., '/allowed/../etc/passwd')
|
|
1747
|
+
// This prevents symlink bypass attacks (e.g., /tmp -> /private/tmp on macOS)
|
|
1616
1748
|
const allowedDirs = this.allowedFolders && this.allowedFolders.length > 0 ? this.allowedFolders : [process.cwd()];
|
|
1617
1749
|
|
|
1618
1750
|
let absolutePath;
|
|
@@ -1620,20 +1752,20 @@ export class ProbeAgent {
|
|
|
1620
1752
|
|
|
1621
1753
|
// If absolute path, check if it's within any allowed directory
|
|
1622
1754
|
if (isAbsolute(imagePath)) {
|
|
1623
|
-
//
|
|
1624
|
-
absolutePath =
|
|
1755
|
+
// Use safeRealpath to resolve symlinks for security
|
|
1756
|
+
absolutePath = safeRealpath(resolve(imagePath));
|
|
1625
1757
|
isPathAllowed = allowedDirs.some(dir => {
|
|
1626
|
-
const
|
|
1758
|
+
const resolvedDir = safeRealpath(dir);
|
|
1627
1759
|
// Ensure the path is within the allowed directory (add separator to prevent prefix attacks)
|
|
1628
|
-
return absolutePath ===
|
|
1760
|
+
return absolutePath === resolvedDir || absolutePath.startsWith(resolvedDir + sep);
|
|
1629
1761
|
});
|
|
1630
1762
|
} else {
|
|
1631
1763
|
// For relative paths, try resolving against each allowed directory
|
|
1632
1764
|
for (const dir of allowedDirs) {
|
|
1633
|
-
const
|
|
1634
|
-
const resolvedPath =
|
|
1765
|
+
const resolvedDir = safeRealpath(dir);
|
|
1766
|
+
const resolvedPath = safeRealpath(resolve(dir, imagePath));
|
|
1635
1767
|
// Ensure the resolved path is within the allowed directory
|
|
1636
|
-
if (resolvedPath ===
|
|
1768
|
+
if (resolvedPath === resolvedDir || resolvedPath.startsWith(resolvedDir + sep)) {
|
|
1637
1769
|
absolutePath = resolvedPath;
|
|
1638
1770
|
isPathAllowed = true;
|
|
1639
1771
|
break;
|
|
@@ -1870,7 +2002,8 @@ export class ProbeAgent {
|
|
|
1870
2002
|
return this.architectureContext;
|
|
1871
2003
|
}
|
|
1872
2004
|
|
|
1873
|
-
|
|
2005
|
+
// Use workspaceRoot for consistent path handling
|
|
2006
|
+
const rootDirectory = this.workspaceRoot || (this.allowedFolders.length > 0 ? this.allowedFolders[0] : process.cwd());
|
|
1874
2007
|
const configuredName =
|
|
1875
2008
|
typeof this.architectureFileName === 'string' ? this.architectureFileName.trim() : '';
|
|
1876
2009
|
const hasConfiguredName = !!configuredName;
|
|
@@ -2028,6 +2161,10 @@ export class ProbeAgent {
|
|
|
2028
2161
|
}
|
|
2029
2162
|
|
|
2030
2163
|
_getSkillsRepoRoot() {
|
|
2164
|
+
// Use workspaceRoot for consistent path handling
|
|
2165
|
+
if (this.workspaceRoot) {
|
|
2166
|
+
return resolve(this.workspaceRoot);
|
|
2167
|
+
}
|
|
2031
2168
|
if (this.allowedFolders && this.allowedFolders.length > 0) {
|
|
2032
2169
|
return resolve(this.allowedFolders[0]);
|
|
2033
2170
|
}
|
|
@@ -2108,7 +2245,7 @@ ${extractGuidance}
|
|
|
2108
2245
|
// Add repository structure if available
|
|
2109
2246
|
if (this.fileList) {
|
|
2110
2247
|
systemPrompt += `\n\n# Repository Structure\n`;
|
|
2111
|
-
systemPrompt += `You are working with a repository located at: ${this.
|
|
2248
|
+
systemPrompt += `You are working with a repository located at: ${this.workspaceRoot}\n\n`;
|
|
2112
2249
|
systemPrompt += `Here's an overview of the repository structure (showing up to 100 most relevant files):\n\n`;
|
|
2113
2250
|
systemPrompt += '```\n' + this.fileList + '\n```\n';
|
|
2114
2251
|
}
|
|
@@ -2170,7 +2307,7 @@ ${extractGuidance}
|
|
|
2170
2307
|
// Add repository structure if available
|
|
2171
2308
|
if (this.fileList) {
|
|
2172
2309
|
systemPrompt += `\n\n# Repository Structure\n`;
|
|
2173
|
-
systemPrompt += `You are working with a repository located at: ${this.
|
|
2310
|
+
systemPrompt += `You are working with a repository located at: ${this.workspaceRoot}\n\n`;
|
|
2174
2311
|
systemPrompt += `Here's an overview of the repository structure (showing up to 100 most relevant files):\n\n`;
|
|
2175
2312
|
systemPrompt += '```\n' + this.fileList + '\n```\n';
|
|
2176
2313
|
}
|
|
@@ -2484,10 +2621,29 @@ Follow these instructions carefully:
|
|
|
2484
2621
|
}
|
|
2485
2622
|
}
|
|
2486
2623
|
|
|
2487
|
-
// Add folder information
|
|
2488
|
-
const searchDirectory = this.
|
|
2624
|
+
// Add folder information using workspace root and relative paths
|
|
2625
|
+
const searchDirectory = this.workspaceRoot;
|
|
2489
2626
|
if (this.debug) {
|
|
2490
|
-
console.log(`[DEBUG] Generating file list for
|
|
2627
|
+
console.log(`[DEBUG] Generating file list for workspace root: ${searchDirectory}...`);
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// Convert allowed folders to relative paths for cleaner AI context
|
|
2631
|
+
// Add ./ prefix to make it clear these are relative paths
|
|
2632
|
+
const relativeWorkspaces = this.allowedFolders.map(f => {
|
|
2633
|
+
const rel = toRelativePath(f, this.workspaceRoot);
|
|
2634
|
+
// Add ./ prefix if not already starting with . and not an absolute path
|
|
2635
|
+
if (rel && rel !== '.' && !rel.startsWith('.') && !rel.startsWith('/')) {
|
|
2636
|
+
return './' + rel;
|
|
2637
|
+
}
|
|
2638
|
+
return rel;
|
|
2639
|
+
}).filter(f => f && f !== '.');
|
|
2640
|
+
|
|
2641
|
+
// Describe available paths in a user-friendly way
|
|
2642
|
+
let workspaceDesc;
|
|
2643
|
+
if (relativeWorkspaces.length === 0) {
|
|
2644
|
+
workspaceDesc = '. (current directory)';
|
|
2645
|
+
} else {
|
|
2646
|
+
workspaceDesc = relativeWorkspaces.join(', ');
|
|
2491
2647
|
}
|
|
2492
2648
|
|
|
2493
2649
|
try {
|
|
@@ -2495,15 +2651,15 @@ Follow these instructions carefully:
|
|
|
2495
2651
|
directory: searchDirectory,
|
|
2496
2652
|
maxFiles: 100,
|
|
2497
2653
|
respectGitignore: !process.env.PROBE_NO_GITIGNORE || process.env.PROBE_NO_GITIGNORE === '',
|
|
2498
|
-
cwd:
|
|
2654
|
+
cwd: this.workspaceRoot
|
|
2499
2655
|
});
|
|
2500
2656
|
|
|
2501
|
-
systemMessage += `\n# Repository Structure\n\nYou are working with a
|
|
2657
|
+
systemMessage += `\n# Repository Structure\n\nYou are working with a workspace. Available paths: ${workspaceDesc}\n\nHere's an overview of the repository structure (showing up to 100 most relevant files):\n\n\`\`\`\n${files}\n\`\`\`\n\n`;
|
|
2502
2658
|
} catch (error) {
|
|
2503
2659
|
if (this.debug) {
|
|
2504
2660
|
console.log(`[DEBUG] Could not generate file list: ${error.message}`);
|
|
2505
2661
|
}
|
|
2506
|
-
systemMessage += `\n# Repository Structure\n\nYou are working with a
|
|
2662
|
+
systemMessage += `\n# Repository Structure\n\nYou are working with a workspace. Available paths: ${workspaceDesc}\n\n`;
|
|
2507
2663
|
}
|
|
2508
2664
|
|
|
2509
2665
|
// Add architecture context if available
|
|
@@ -2511,7 +2667,15 @@ Follow these instructions carefully:
|
|
|
2511
2667
|
systemMessage += this.getArchitectureSection();
|
|
2512
2668
|
|
|
2513
2669
|
if (this.allowedFolders.length > 0) {
|
|
2514
|
-
|
|
2670
|
+
const relativeAllowed = this.allowedFolders.map(f => {
|
|
2671
|
+
const rel = toRelativePath(f, this.workspaceRoot);
|
|
2672
|
+
// Add ./ prefix if not already starting with . and not an absolute path
|
|
2673
|
+
if (rel && rel !== '.' && !rel.startsWith('.') && !rel.startsWith('/')) {
|
|
2674
|
+
return './' + rel;
|
|
2675
|
+
}
|
|
2676
|
+
return rel;
|
|
2677
|
+
});
|
|
2678
|
+
systemMessage += `\n**Important**: For security reasons, you can only access these paths: ${relativeAllowed.join(', ')}\n\n`;
|
|
2515
2679
|
}
|
|
2516
2680
|
|
|
2517
2681
|
return systemMessage;
|
|
@@ -3234,6 +3398,8 @@ Follow these instructions carefully:
|
|
|
3234
3398
|
console.error(`[DEBUG] ========================================\n`);
|
|
3235
3399
|
}
|
|
3236
3400
|
|
|
3401
|
+
// Add assistant message with tool call (matching native tool pattern)
|
|
3402
|
+
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
3237
3403
|
currentMessages.push({ role: 'user', content: `<tool_result>\n${toolResultContent}\n</tool_result>` });
|
|
3238
3404
|
} catch (error) {
|
|
3239
3405
|
// Record MCP tool end event (failure)
|
|
@@ -3257,24 +3423,27 @@ Follow these instructions carefully:
|
|
|
3257
3423
|
|
|
3258
3424
|
// Format error with structured information for AI
|
|
3259
3425
|
const errorXml = formatErrorForAI(error);
|
|
3426
|
+
// Add assistant message with tool call (matching native tool pattern)
|
|
3427
|
+
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
3260
3428
|
currentMessages.push({ role: 'user', content: `<tool_result>\n${errorXml}\n</tool_result>` });
|
|
3261
3429
|
}
|
|
3262
3430
|
} else if (this.toolImplementations[toolName]) {
|
|
3263
3431
|
// Execute native tool
|
|
3264
3432
|
try {
|
|
3265
3433
|
// Add sessionId and workingDirectory to params for tool execution
|
|
3266
|
-
// Validate and resolve workingDirectory
|
|
3267
|
-
//
|
|
3268
|
-
let resolvedWorkingDirectory = this.cwd || (this.allowedFolders && this.allowedFolders[0]) || process.cwd();
|
|
3434
|
+
// Validate and resolve workingDirectory using safeRealpath for symlink security
|
|
3435
|
+
// Consistent fallback chain: workspaceRoot > cwd > allowedFolders[0] > process.cwd()
|
|
3436
|
+
let resolvedWorkingDirectory = this.workspaceRoot || this.cwd || (this.allowedFolders && this.allowedFolders[0]) || process.cwd();
|
|
3269
3437
|
if (params.workingDirectory) {
|
|
3270
3438
|
// Resolve relative paths against the current working directory context, not process.cwd()
|
|
3271
|
-
|
|
3439
|
+
// Use safeRealpath to resolve symlinks and prevent bypass attacks
|
|
3440
|
+
const requestedDir = safeRealpath(isAbsolute(params.workingDirectory)
|
|
3272
3441
|
? resolve(params.workingDirectory)
|
|
3273
|
-
: resolve(resolvedWorkingDirectory, params.workingDirectory);
|
|
3442
|
+
: resolve(resolvedWorkingDirectory, params.workingDirectory));
|
|
3274
3443
|
// Check if the requested directory is within allowed folders
|
|
3275
3444
|
const isWithinAllowed = !this.allowedFolders || this.allowedFolders.length === 0 ||
|
|
3276
3445
|
this.allowedFolders.some(folder => {
|
|
3277
|
-
const resolvedFolder =
|
|
3446
|
+
const resolvedFolder = safeRealpath(folder);
|
|
3278
3447
|
return requestedDir === resolvedFolder || requestedDir.startsWith(resolvedFolder + sep);
|
|
3279
3448
|
});
|
|
3280
3449
|
if (isWithinAllowed) {
|
|
@@ -3887,7 +4056,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
3887
4056
|
|
|
3888
4057
|
const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
|
|
3889
4058
|
debug: this.debug,
|
|
3890
|
-
path: this.allowedFolders[0],
|
|
4059
|
+
path: this.workspaceRoot || this.allowedFolders[0],
|
|
3891
4060
|
provider: this.clientApiProvider,
|
|
3892
4061
|
model: this.model,
|
|
3893
4062
|
tracer: this.tracer
|
|
@@ -3977,7 +4146,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
3977
4146
|
|
|
3978
4147
|
const { JsonFixingAgent } = await import('./schemaUtils.js');
|
|
3979
4148
|
const jsonFixer = new JsonFixingAgent({
|
|
3980
|
-
path: this.allowedFolders[0],
|
|
4149
|
+
path: this.workspaceRoot || this.allowedFolders[0],
|
|
3981
4150
|
provider: this.clientApiProvider,
|
|
3982
4151
|
model: this.model,
|
|
3983
4152
|
debug: this.debug,
|
|
@@ -4065,7 +4234,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
4065
4234
|
|
|
4066
4235
|
const mermaidValidation = await validateAndFixMermaidResponse(finalResult, {
|
|
4067
4236
|
debug: this.debug,
|
|
4068
|
-
path: this.allowedFolders[0],
|
|
4237
|
+
path: this.workspaceRoot || this.allowedFolders[0],
|
|
4069
4238
|
provider: this.clientApiProvider,
|
|
4070
4239
|
model: this.model,
|
|
4071
4240
|
tracer: this.tracer
|
|
@@ -4221,7 +4390,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
4221
4390
|
|
|
4222
4391
|
const finalMermaidValidation = await validateAndFixMermaidResponse(finalResult, {
|
|
4223
4392
|
debug: this.debug,
|
|
4224
|
-
path: this.allowedFolders[0],
|
|
4393
|
+
path: this.workspaceRoot || this.allowedFolders[0],
|
|
4225
4394
|
provider: this.clientApiProvider,
|
|
4226
4395
|
model: this.model,
|
|
4227
4396
|
tracer: this.tracer
|
|
@@ -4419,7 +4588,7 @@ Convert your previous response content into actual JSON data that follows this s
|
|
|
4419
4588
|
allowEdit: this.allowEdit,
|
|
4420
4589
|
enableDelegate: this.enableDelegate,
|
|
4421
4590
|
architectureFileName: this.architectureFileName,
|
|
4422
|
-
|
|
4591
|
+
// Pass allowedFolders which will recompute workspaceRoot correctly
|
|
4423
4592
|
allowedFolders: [...this.allowedFolders],
|
|
4424
4593
|
cwd: this.cwd, // Preserve explicit working directory
|
|
4425
4594
|
provider: this.clientApiProvider,
|