@probelabs/probe 0.6.0-rc178 → 0.6.0-rc187

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc178",
3
+ "version": "0.6.0-rc187",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -19,6 +19,21 @@ export interface ProbeAgentOptions {
19
19
  allowEdit?: boolean;
20
20
  /** Enable the delegate tool for task distribution to subagents */
21
21
  enableDelegate?: boolean;
22
+ /** Enable bash tool for command execution */
23
+ enableBash?: boolean;
24
+ /** Bash tool configuration (allow/deny patterns) */
25
+ bashConfig?: {
26
+ /** Additional allowed command patterns */
27
+ allow?: string[];
28
+ /** Additional denied command patterns */
29
+ deny?: string[];
30
+ /** Disable default allow list */
31
+ disableDefaultAllow?: boolean;
32
+ /** Disable default deny list */
33
+ disableDefaultDeny?: boolean;
34
+ /** Enable debug logging for permission checks */
35
+ debug?: boolean;
36
+ };
22
37
  /** Search directory path */
23
38
  path?: string;
24
39
  /** Force specific AI provider */
@@ -2372,31 +2372,41 @@ When troubleshooting:
2372
2372
 
2373
2373
  // Parse tool call from response with valid tools list
2374
2374
  // Build validTools based on allowedTools configuration (same pattern as getSystemMessage)
2375
+ // When _disableTools is set, only allow attempt_completion for JSON correction flows
2375
2376
  const validTools = [];
2376
- if (this.allowedTools.isEnabled('search')) validTools.push('search');
2377
- if (this.allowedTools.isEnabled('query')) validTools.push('query');
2378
- if (this.allowedTools.isEnabled('extract')) validTools.push('extract');
2379
- if (this.allowedTools.isEnabled('listFiles')) validTools.push('listFiles');
2380
- if (this.allowedTools.isEnabled('searchFiles')) validTools.push('searchFiles');
2381
- if (this.allowedTools.isEnabled('readImage')) validTools.push('readImage');
2382
- if (this.allowedTools.isEnabled('attempt_completion')) validTools.push('attempt_completion');
2383
-
2384
- // Edit tools (require both allowEdit flag AND allowedTools permission)
2385
- if (this.allowEdit && this.allowedTools.isEnabled('implement')) {
2386
- validTools.push('implement', 'edit', 'create');
2387
- }
2388
- // Bash tool (require both enableBash flag AND allowedTools permission)
2389
- if (this.enableBash && this.allowedTools.isEnabled('bash')) {
2390
- validTools.push('bash');
2391
- }
2392
- // Delegate tool (require both enableDelegate flag AND allowedTools permission)
2393
- if (this.enableDelegate && this.allowedTools.isEnabled('delegate')) {
2394
- validTools.push('delegate');
2377
+ if (options._disableTools) {
2378
+ // Only allow attempt_completion for JSON correction - no search/query/edit tools
2379
+ validTools.push('attempt_completion');
2380
+ if (this.debug) {
2381
+ console.log(`[DEBUG] Tools disabled for this call - only attempt_completion allowed`);
2382
+ }
2383
+ } else {
2384
+ if (this.allowedTools.isEnabled('search')) validTools.push('search');
2385
+ if (this.allowedTools.isEnabled('query')) validTools.push('query');
2386
+ if (this.allowedTools.isEnabled('extract')) validTools.push('extract');
2387
+ if (this.allowedTools.isEnabled('listFiles')) validTools.push('listFiles');
2388
+ if (this.allowedTools.isEnabled('searchFiles')) validTools.push('searchFiles');
2389
+ if (this.allowedTools.isEnabled('readImage')) validTools.push('readImage');
2390
+ if (this.allowedTools.isEnabled('attempt_completion')) validTools.push('attempt_completion');
2391
+
2392
+ // Edit tools (require both allowEdit flag AND allowedTools permission)
2393
+ if (this.allowEdit && this.allowedTools.isEnabled('implement')) {
2394
+ validTools.push('implement', 'edit', 'create');
2395
+ }
2396
+ // Bash tool (require both enableBash flag AND allowedTools permission)
2397
+ if (this.enableBash && this.allowedTools.isEnabled('bash')) {
2398
+ validTools.push('bash');
2399
+ }
2400
+ // Delegate tool (require both enableDelegate flag AND allowedTools permission)
2401
+ if (this.enableDelegate && this.allowedTools.isEnabled('delegate')) {
2402
+ validTools.push('delegate');
2403
+ }
2395
2404
  }
2396
-
2405
+
2397
2406
  // Try parsing with hybrid parser that supports both native and MCP tools
2407
+ // When _disableTools is set, skip MCP tools entirely
2398
2408
  const nativeTools = validTools;
2399
- const parsedTool = this.mcpBridge
2409
+ const parsedTool = (this.mcpBridge && !options._disableTools)
2400
2410
  ? parseHybridXmlToolCall(assistantResponseContent, nativeTools, this.mcpBridge)
2401
2411
  : parseXmlToolCallWithThinking(assistantResponseContent, validTools);
2402
2412
  if (parsedTool) {
@@ -2501,10 +2511,26 @@ When troubleshooting:
2501
2511
  // Execute native tool
2502
2512
  try {
2503
2513
  // Add sessionId and workingDirectory to params for tool execution
2514
+ // Validate and resolve workingDirectory
2515
+ let resolvedWorkingDirectory = (this.allowedFolders && this.allowedFolders[0]) || process.cwd();
2516
+ if (params.workingDirectory) {
2517
+ const requestedDir = resolve(params.workingDirectory);
2518
+ // Check if the requested directory is within allowed folders
2519
+ const isWithinAllowed = !this.allowedFolders || this.allowedFolders.length === 0 ||
2520
+ this.allowedFolders.some(folder => {
2521
+ const resolvedFolder = resolve(folder);
2522
+ return requestedDir === resolvedFolder || requestedDir.startsWith(resolvedFolder + sep);
2523
+ });
2524
+ if (isWithinAllowed) {
2525
+ resolvedWorkingDirectory = requestedDir;
2526
+ } else if (this.debug) {
2527
+ console.error(`[DEBUG] Rejected workingDirectory "${params.workingDirectory}" - not within allowed folders`);
2528
+ }
2529
+ }
2504
2530
  const toolParams = {
2505
2531
  ...params,
2506
2532
  sessionId: this.sessionId,
2507
- workingDirectory: (this.allowedFolders && this.allowedFolders[0]) || process.cwd()
2533
+ workingDirectory: resolvedWorkingDirectory
2508
2534
  };
2509
2535
 
2510
2536
  // Log tool execution in debug mode
@@ -3148,7 +3174,8 @@ Convert your previous response content into actual JSON data that follows this s
3148
3174
  finalResult = await this.answer(correctionPrompt, [], {
3149
3175
  ...options,
3150
3176
  _schemaFormatted: true,
3151
- _skipValidation: true // Skip validation in recursive correction calls to prevent loops
3177
+ _skipValidation: true, // Skip validation in recursive correction calls to prevent loops
3178
+ _disableTools: true // Only allow attempt_completion - prevent AI from using search/query tools
3152
3179
  });
3153
3180
  finalResult = cleanSchemaResponse(finalResult);
3154
3181
 
@@ -159,6 +159,69 @@ export function isComplexCommand(command) {
159
159
  return result.isComplex;
160
160
  }
161
161
 
162
+ /**
163
+ * Check if a pattern is a complex pattern (contains shell operators)
164
+ * Complex patterns are used to match full command strings including operators
165
+ * @param {string} pattern - Pattern to check
166
+ * @returns {boolean} True if pattern contains shell operators
167
+ */
168
+ export function isComplexPattern(pattern) {
169
+ if (!pattern || typeof pattern !== 'string') return false;
170
+
171
+ // Check for operators in the pattern (aligned with complexPatterns in parseSimpleCommand)
172
+ const operatorPatterns = [
173
+ /\|/, // Pipes
174
+ /&&/, // Logical AND
175
+ /\|\|/, // Logical OR
176
+ /;/, // Command separator
177
+ /&$/, // Background execution
178
+ /\$\(/, // Command substitution $()
179
+ /`/, // Command substitution ``
180
+ />/, // Redirection >
181
+ /</, // Redirection <
182
+ ];
183
+
184
+ return operatorPatterns.some(p => p.test(pattern));
185
+ }
186
+
187
+ /**
188
+ * Convert a glob-style pattern to regex for matching
189
+ * Supports * as wildcard (matches any characters except operators)
190
+ * @param {string} pattern - Glob pattern
191
+ * @returns {RegExp} Compiled regex
192
+ */
193
+ function globToRegex(pattern) {
194
+ // Escape regex special characters except *
195
+ let escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
196
+ // Convert * to .*? (non-greedy match)
197
+ escaped = escaped.replace(/\*/g, '.*?');
198
+ // Make it match the full string
199
+ return new RegExp('^' + escaped + '$', 'i');
200
+ }
201
+
202
+ /**
203
+ * Match a command string against a complex pattern
204
+ * Complex patterns use glob-style wildcards (*) for matching
205
+ * @param {string} command - Full command string
206
+ * @param {string} pattern - Complex pattern with wildcards
207
+ * @returns {boolean} True if command matches the pattern
208
+ */
209
+ export function matchesComplexPattern(command, pattern) {
210
+ if (!command || !pattern) return false;
211
+
212
+ // Normalize whitespace
213
+ const normalizedCommand = command.trim().replace(/\s+/g, ' ');
214
+ const normalizedPattern = pattern.trim().replace(/\s+/g, ' ');
215
+
216
+ try {
217
+ const regex = globToRegex(normalizedPattern);
218
+ return regex.test(normalizedCommand);
219
+ } catch (e) {
220
+ // If regex fails, fall back to exact match
221
+ return normalizedCommand === normalizedPattern;
222
+ }
223
+ }
224
+
162
225
  /**
163
226
  * Legacy compatibility function - parses command for permission checking
164
227
  * @param {string} command - Command to parse
@@ -6,7 +6,7 @@
6
6
  import { spawn } from 'child_process';
7
7
  import { resolve, join } from 'path';
8
8
  import { existsSync } from 'fs';
9
- import { parseCommandForExecution } from './bashCommandUtils.js';
9
+ import { parseCommandForExecution, isComplexCommand } from './bashCommandUtils.js';
10
10
 
11
11
  /**
12
12
  * Execute a bash command with security controls
@@ -63,31 +63,46 @@ export async function executeBashCommand(command, options = {}) {
63
63
  ...env
64
64
  };
65
65
 
66
- // Parse command for shell execution
67
- // We use shell: false for security, so we need to parse manually
68
- const args = parseCommandForExecution(command);
69
- if (!args || args.length === 0) {
70
- resolve({
71
- success: false,
72
- error: 'Failed to parse command',
73
- stdout: '',
74
- stderr: '',
75
- exitCode: 1,
76
- command,
77
- workingDirectory: cwd,
78
- duration: Date.now() - startTime
79
- });
80
- return;
81
- }
66
+ // Check if this is a complex command (contains pipes, operators, etc.)
67
+ const isComplex = isComplexCommand(command);
82
68
 
83
- const [cmd, ...cmdArgs] = args;
69
+ let cmd, cmdArgs, useShell;
70
+
71
+ if (isComplex) {
72
+ // For complex commands, use sh -c to execute through shell
73
+ // This is only reached if the permission checker allowed the complex command
74
+ cmd = 'sh';
75
+ cmdArgs = ['-c', command];
76
+ useShell = false; // We explicitly use sh -c, not spawn's shell option
77
+ if (debug) {
78
+ console.log(`[BashExecutor] Complex command - using sh -c`);
79
+ }
80
+ } else {
81
+ // Parse simple command for direct execution
82
+ const args = parseCommandForExecution(command);
83
+ if (!args || args.length === 0) {
84
+ resolve({
85
+ success: false,
86
+ error: 'Failed to parse command',
87
+ stdout: '',
88
+ stderr: '',
89
+ exitCode: 1,
90
+ command,
91
+ workingDirectory: cwd,
92
+ duration: Date.now() - startTime
93
+ });
94
+ return;
95
+ }
96
+ [cmd, ...cmdArgs] = args;
97
+ useShell = false;
98
+ }
84
99
 
85
100
  // Spawn the process
86
101
  const child = spawn(cmd, cmdArgs, {
87
102
  cwd,
88
103
  env: processEnv,
89
104
  stdio: ['ignore', 'pipe', 'pipe'], // stdin ignored, capture stdout/stderr
90
- shell: false, // For security
105
+ shell: useShell, // false for security
91
106
  windowsHide: true
92
107
  });
93
108
 
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { DEFAULT_ALLOW_PATTERNS, DEFAULT_DENY_PATTERNS } from './bashDefaults.js';
7
- import { parseCommand, isComplexCommand } from './bashCommandUtils.js';
7
+ import { parseCommand, isComplexCommand, isComplexPattern, matchesComplexPattern } from './bashCommandUtils.js';
8
8
 
9
9
  /**
10
10
  * Check if a pattern matches a parsed command
@@ -125,7 +125,7 @@ export class BashPermissionChecker {
125
125
  }
126
126
 
127
127
  /**
128
- * Check if a simple command is allowed (rejects complex commands for security)
128
+ * Check if a simple command is allowed (complex commands allowed if they match patterns)
129
129
  * @param {string} command - Command to check
130
130
  * @returns {Object} Permission result
131
131
  */
@@ -138,19 +138,17 @@ export class BashPermissionChecker {
138
138
  };
139
139
  }
140
140
 
141
- // First check if this is a complex command - reject immediately for security
142
- if (isComplexCommand(command)) {
143
- return {
144
- allowed: false,
145
- reason: 'Complex shell commands with pipes, operators, or redirections are not supported for security reasons',
146
- command: command,
147
- isComplex: true
148
- };
141
+ // Check if this is a complex command
142
+ const commandIsComplex = isComplexCommand(command);
143
+
144
+ if (commandIsComplex) {
145
+ // For complex commands, check against complex patterns in allow/deny lists
146
+ return this._checkComplexCommand(command);
149
147
  }
150
148
 
151
149
  // Parse the simple command
152
150
  const parsed = parseCommand(command);
153
-
151
+
154
152
  if (parsed.error) {
155
153
  return {
156
154
  allowed: false,
@@ -203,14 +201,77 @@ export class BashPermissionChecker {
203
201
  parsed: parsed,
204
202
  isComplex: false
205
203
  };
206
-
204
+
207
205
  if (this.debug) {
208
206
  console.log(`[BashPermissions] ALLOWED - command passed all checks`);
209
207
  }
210
-
208
+
211
209
  return result;
212
210
  }
213
211
 
212
+ /**
213
+ * Check a complex command against complex patterns in allow/deny lists
214
+ * @private
215
+ * @param {string} command - Complex command to check
216
+ * @returns {Object} Permission result
217
+ */
218
+ _checkComplexCommand(command) {
219
+ if (this.debug) {
220
+ console.log(`[BashPermissions] Checking complex command: "${command}"`);
221
+ }
222
+
223
+ // Get complex patterns from allow and deny lists
224
+ const complexAllowPatterns = this.allowPatterns.filter(p => isComplexPattern(p));
225
+ const complexDenyPatterns = this.denyPatterns.filter(p => isComplexPattern(p));
226
+
227
+ if (this.debug) {
228
+ console.log(`[BashPermissions] Complex allow patterns: ${complexAllowPatterns.length}`);
229
+ console.log(`[BashPermissions] Complex deny patterns: ${complexDenyPatterns.length}`);
230
+ }
231
+
232
+ // Check deny patterns first (deny takes precedence)
233
+ for (const pattern of complexDenyPatterns) {
234
+ if (matchesComplexPattern(command, pattern)) {
235
+ if (this.debug) {
236
+ console.log(`[BashPermissions] DENIED - matches complex deny pattern: ${pattern}`);
237
+ }
238
+ return {
239
+ allowed: false,
240
+ reason: `Command matches deny pattern: ${pattern}`,
241
+ command: command,
242
+ isComplex: true,
243
+ matchedPatterns: [pattern]
244
+ };
245
+ }
246
+ }
247
+
248
+ // Check allow patterns
249
+ for (const pattern of complexAllowPatterns) {
250
+ if (matchesComplexPattern(command, pattern)) {
251
+ if (this.debug) {
252
+ console.log(`[BashPermissions] ALLOWED - matches complex allow pattern: ${pattern}`);
253
+ }
254
+ return {
255
+ allowed: true,
256
+ command: command,
257
+ isComplex: true,
258
+ matchedPattern: pattern
259
+ };
260
+ }
261
+ }
262
+
263
+ // No matching complex pattern found - reject complex command
264
+ if (this.debug) {
265
+ console.log(`[BashPermissions] DENIED - no matching complex pattern found`);
266
+ }
267
+ return {
268
+ allowed: false,
269
+ reason: 'Complex shell commands require explicit allow patterns (e.g., "cd * && git *")',
270
+ command: command,
271
+ isComplex: true
272
+ };
273
+ }
274
+
214
275
  /**
215
276
  * Get configuration summary
216
277
  * @returns {Object} Configuration info
@@ -520,7 +520,7 @@ class ProbeAgentMcpServer {
520
520
  // Retry once with correction prompt
521
521
  const correctionPrompt = createJsonCorrectionPrompt(result, schema, validation.error);
522
522
  try {
523
- result = await agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true });
523
+ result = await agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true });
524
524
  result = cleanSchemaResponse(result);
525
525
 
526
526
  // Validate again after correction
@@ -863,11 +863,11 @@ async function main() {
863
863
  try {
864
864
  if (appTracer) {
865
865
  result = await appTracer.withSpan('agent.json_correction',
866
- () => agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true }),
866
+ () => agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true }),
867
867
  { 'original_error': validation.error }
868
868
  );
869
869
  } else {
870
- result = await agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true });
870
+ result = await agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true });
871
871
  }
872
872
  result = cleanSchemaResponse(result);
873
873
 
@@ -787,21 +787,22 @@ export function createJsonCorrectionPrompt(invalidResponse, schema, errorOrValid
787
787
  }
788
788
 
789
789
  // Create increasingly stronger prompts based on retry attempt
790
+ // These prompts explicitly instruct the AI to use attempt_completion with the JSON result
790
791
  const strengthLevels = [
791
792
  {
792
793
  prefix: "CRITICAL JSON ERROR:",
793
- instruction: "You MUST fix this and return ONLY valid JSON.",
794
- emphasis: "Return ONLY the corrected JSON, with no additional text or markdown formatting."
794
+ instruction: "You MUST fix this and respond using attempt_completion with ONLY valid JSON as the result.",
795
+ emphasis: "Use attempt_completion with ONLY the corrected JSON in the result field. No explanatory text, no markdown, no code blocks."
795
796
  },
796
797
  {
797
798
  prefix: "URGENT - JSON PARSING FAILED:",
798
- instruction: "This is your second chance. Return ONLY valid JSON that can be parsed by JSON.parse().",
799
- emphasis: "ABSOLUTELY NO explanatory text, greetings, or formatting. ONLY JSON."
799
+ instruction: "This is your second chance. Use attempt_completion with valid JSON that can be parsed by JSON.parse().",
800
+ emphasis: "ABSOLUTELY NO explanatory text or formatting. Use attempt_completion with ONLY raw JSON in the result."
800
801
  },
801
802
  {
802
803
  prefix: "FINAL ATTEMPT - CRITICAL JSON ERROR:",
803
- instruction: "This is the final retry. You MUST return ONLY raw JSON without any other content.",
804
- emphasis: "EXAMPLE: {\"key\": \"value\"} NOT: ```json{\"key\": \"value\"}``` NOT: Here is the JSON: {\"key\": \"value\"}"
804
+ instruction: "This is the final retry. You MUST use attempt_completion with ONLY raw JSON in the result field.",
805
+ emphasis: "CORRECT: <attempt_completion><result>{\"key\": \"value\"}</result></attempt_completion>\nWRONG: Here is the JSON: {\"key\": \"value\"}\nWRONG: ```json{\"key\": \"value\"}```"
805
806
  }
806
807
  ];
807
808
 
package/src/delegate.js CHANGED
@@ -176,6 +176,8 @@ const delegationManager = new DelegationManager();
176
176
  * @param {string} [options.provider] - AI provider (inherited from parent)
177
177
  * @param {string} [options.model] - AI model (inherited from parent)
178
178
  * @param {Object} [options.tracer=null] - Telemetry tracer instance
179
+ * @param {boolean} [options.enableBash=false] - Enable bash tool (inherited from parent)
180
+ * @param {Object} [options.bashConfig] - Bash configuration (inherited from parent)
179
181
  * @returns {Promise<string>} The response from the delegate agent
180
182
  */
181
183
  export async function delegate({
@@ -188,7 +190,9 @@ export async function delegate({
188
190
  parentSessionId = null,
189
191
  path = null,
190
192
  provider = null,
191
- model = null
193
+ model = null,
194
+ enableBash = false,
195
+ bashConfig = null
192
196
  }) {
193
197
  if (!task || typeof task !== 'string') {
194
198
  throw new Error('Task parameter is required and must be a string');
@@ -234,9 +238,11 @@ export async function delegate({
234
238
  maxIterations: remainingIterations,
235
239
  debug,
236
240
  tracer,
237
- path, // Inherit from parent
238
- provider, // Inherit from parent
239
- model // Inherit from parent
241
+ path, // Inherit from parent
242
+ provider, // Inherit from parent
243
+ model, // Inherit from parent
244
+ enableBash, // Inherit from parent
245
+ bashConfig // Inherit from parent
240
246
  });
241
247
 
242
248
  if (debug) {
@@ -239,10 +239,12 @@ export const extractTool = (options = {}) => {
239
239
  * @param {number} [options.timeout=300] - Default timeout in seconds
240
240
  * @param {string} [options.cwd] - Working directory to use if not specified in call
241
241
  * @param {string[]} [options.allowedFolders] - Allowed folders for workspace isolation
242
+ * @param {boolean} [options.enableBash=false] - Enable bash tool for sub-agents
243
+ * @param {Object} [options.bashConfig] - Bash configuration (allow/deny patterns)
242
244
  * @returns {Object} Configured delegate tool
243
245
  */
244
246
  export const delegateTool = (options = {}) => {
245
- const { debug = false, timeout = 300, cwd, allowedFolders } = options;
247
+ const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig } = options;
246
248
 
247
249
  return tool({
248
250
  name: 'delegate',
@@ -309,7 +311,9 @@ export const delegateTool = (options = {}) => {
309
311
  path: effectivePath,
310
312
  provider,
311
313
  model,
312
- tracer
314
+ tracer,
315
+ enableBash,
316
+ bashConfig
313
317
  });
314
318
 
315
319
  return result;