@probelabs/probe 0.6.0-rc209 → 0.6.0-rc210

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.
@@ -272,8 +272,119 @@ export class BashPermissionChecker {
272
272
  return result;
273
273
  }
274
274
 
275
+ /**
276
+ * Split a complex command into component commands by operators
277
+ *
278
+ * ## Escape Handling (Security-Critical)
279
+ *
280
+ * This function intentionally PRESERVES escape sequences (both backslash AND
281
+ * escaped character) in the output. This is step 1 of a 2-step parsing process:
282
+ *
283
+ * 1. _splitComplexCommand: Splits by operators, PRESERVES escapes → `echo "test\" && b"`
284
+ * 2. parseCommand: Interprets escapes in each component → args: ['test" && b']
285
+ *
286
+ * This differs from stripQuotedContent() in bashCommandUtils.js which REMOVES
287
+ * escapes entirely (for operator detection only).
288
+ *
289
+ * The security rationale: if we stripped escapes here, `\"` would become `"`,
290
+ * potentially causing incorrect quote boundary detection and allowing operator
291
+ * injection. By preserving escapes, parseCommand() can correctly interpret them.
292
+ *
293
+ * See bashCommandUtils.js module header for the full escape handling architecture.
294
+ *
295
+ * @private
296
+ * @param {string} command - Complex command to split
297
+ * @returns {string[]} Array of component commands (with escapes preserved)
298
+ */
299
+ _splitComplexCommand(command) {
300
+ // Split by &&, ||, and | operators while respecting quotes and escape sequences
301
+ // IMPORTANT: Preserves backslashes so parseCommand() can interpret them correctly
302
+ const components = [];
303
+ let current = '';
304
+ let inQuotes = false;
305
+ let quoteChar = '';
306
+ let i = 0;
307
+
308
+ while (i < command.length) {
309
+ const char = command[i];
310
+ const nextChar = command[i + 1] || '';
311
+
312
+ // Handle escape sequences outside quotes
313
+ if (!inQuotes && char === '\\') {
314
+ // Keep the backslash and the next character
315
+ current += char;
316
+ if (nextChar) {
317
+ current += nextChar;
318
+ i += 2;
319
+ } else {
320
+ i++;
321
+ }
322
+ continue;
323
+ }
324
+
325
+ // Handle escape sequences inside double quotes (single quotes don't support escaping)
326
+ if (inQuotes && quoteChar === '"' && char === '\\' && nextChar) {
327
+ // Keep both the backslash and the escaped character
328
+ current += char + nextChar;
329
+ i += 2;
330
+ continue;
331
+ }
332
+
333
+ // Start of quoted section
334
+ if (!inQuotes && (char === '"' || char === "'")) {
335
+ inQuotes = true;
336
+ quoteChar = char;
337
+ current += char;
338
+ i++;
339
+ continue;
340
+ }
341
+
342
+ // End of quoted section
343
+ if (inQuotes && char === quoteChar) {
344
+ inQuotes = false;
345
+ quoteChar = '';
346
+ current += char;
347
+ i++;
348
+ continue;
349
+ }
350
+
351
+ // Check for operators only outside quotes
352
+ if (!inQuotes) {
353
+ // Check for && or ||
354
+ if ((char === '&' && nextChar === '&') || (char === '|' && nextChar === '|')) {
355
+ if (current.trim()) {
356
+ components.push(current.trim());
357
+ }
358
+ current = '';
359
+ i += 2; // Skip both characters
360
+ continue;
361
+ }
362
+ // Check for single pipe |
363
+ if (char === '|') {
364
+ if (current.trim()) {
365
+ components.push(current.trim());
366
+ }
367
+ current = '';
368
+ i++;
369
+ continue;
370
+ }
371
+ }
372
+
373
+ current += char;
374
+ i++;
375
+ }
376
+
377
+ // Add the last component
378
+ if (current.trim()) {
379
+ components.push(current.trim());
380
+ }
381
+
382
+ return components;
383
+ }
384
+
275
385
  /**
276
386
  * Check a complex command against complex patterns in allow/deny lists
387
+ * Also supports auto-allowing commands where all components are individually allowed
277
388
  * @private
278
389
  * @param {string} command - Complex command to check
279
390
  * @returns {Object} Permission result
@@ -336,7 +447,102 @@ export class BashPermissionChecker {
336
447
  }
337
448
  }
338
449
 
339
- // No matching complex pattern found - reject complex command
450
+ // No explicit complex pattern matched - try component-based evaluation
451
+ // Split the command by &&, ||, and | operators and check each component
452
+ const components = this._splitComplexCommand(command);
453
+
454
+ if (this.debug) {
455
+ console.log(`[BashPermissions] Checking ${components.length} command components: ${JSON.stringify(components)}`);
456
+ }
457
+
458
+ if (components.length > 1) {
459
+ // Check each component individually
460
+ const componentResults = [];
461
+ let allAllowed = true;
462
+ let deniedComponent = null;
463
+ let deniedReason = null;
464
+
465
+ for (const component of components) {
466
+ // Parse the component as a simple command
467
+ const parsed = parseCommand(component);
468
+
469
+ if (parsed.error || parsed.isComplex) {
470
+ // Component itself is complex or has an error - can't auto-allow
471
+ if (this.debug) {
472
+ console.log(`[BashPermissions] Component "${component}" is complex or has error: ${parsed.error}`);
473
+ }
474
+ allAllowed = false;
475
+ deniedComponent = component;
476
+ deniedReason = parsed.error || 'Component contains nested complex constructs';
477
+ break;
478
+ }
479
+
480
+ // Check against deny patterns
481
+ if (matchesAnyPattern(parsed, this.denyPatterns)) {
482
+ if (this.debug) {
483
+ console.log(`[BashPermissions] Component "${component}" matches deny pattern`);
484
+ }
485
+ allAllowed = false;
486
+ deniedComponent = component;
487
+ deniedReason = 'Component matches deny pattern';
488
+ break;
489
+ }
490
+
491
+ // Check against allow patterns
492
+ if (!matchesAnyPattern(parsed, this.allowPatterns)) {
493
+ if (this.debug) {
494
+ console.log(`[BashPermissions] Component "${component}" not in allow list`);
495
+ }
496
+ allAllowed = false;
497
+ deniedComponent = component;
498
+ deniedReason = 'Component not in allow list';
499
+ break;
500
+ }
501
+
502
+ componentResults.push({ component, parsed, allowed: true });
503
+ }
504
+
505
+ if (allAllowed) {
506
+ if (this.debug) {
507
+ console.log(`[BashPermissions] ALLOWED - all ${components.length} components passed individual checks`);
508
+ }
509
+ const result = {
510
+ allowed: true,
511
+ command: command,
512
+ isComplex: true,
513
+ allowedByComponents: true,
514
+ components: componentResults
515
+ };
516
+ this.recordBashEvent('permission.allowed', {
517
+ command,
518
+ isComplex: true,
519
+ allowedByComponents: true,
520
+ componentCount: components.length
521
+ });
522
+ return result;
523
+ } else {
524
+ if (this.debug) {
525
+ console.log(`[BashPermissions] DENIED - component "${deniedComponent}" failed: ${deniedReason}`);
526
+ }
527
+ const result = {
528
+ allowed: false,
529
+ reason: `Component "${deniedComponent}" not allowed: ${deniedReason}`,
530
+ command: command,
531
+ isComplex: true,
532
+ failedComponent: deniedComponent
533
+ };
534
+ this.recordBashEvent('permission.denied', {
535
+ command,
536
+ reason: 'component_not_allowed',
537
+ failedComponent: deniedComponent,
538
+ componentReason: deniedReason,
539
+ isComplex: true
540
+ });
541
+ return result;
542
+ }
543
+ }
544
+
545
+ // No matching complex pattern found and couldn't split into components - reject
340
546
  if (this.debug) {
341
547
  console.log(`[BashPermissions] DENIED - no matching complex pattern found`);
342
548
  }
package/src/delegate.js CHANGED
@@ -186,6 +186,9 @@ const delegationManager = new DelegationManager();
186
186
  * @param {boolean} [options.searchDelegate] - Use delegated search in the subagent
187
187
  * @param {Object|string} [options.schema] - Optional JSON schema to enforce response format
188
188
  * @param {boolean} [options.enableTasks=false] - Enable task management for the subagent (isolated instance)
189
+ * @param {boolean} [options.enableMcp=false] - Enable MCP tool integration (inherited from parent)
190
+ * @param {Object} [options.mcpConfig] - MCP configuration object (inherited from parent)
191
+ * @param {string} [options.mcpConfigPath] - Path to MCP configuration file (inherited from parent)
189
192
  * @returns {Promise<string>} The response from the delegate agent
190
193
  */
191
194
  export async function delegate({
@@ -208,7 +211,10 @@ export async function delegate({
208
211
  disableTools = false,
209
212
  searchDelegate = undefined,
210
213
  schema = null,
211
- enableTasks = false
214
+ enableTasks = false,
215
+ enableMcp = false,
216
+ mcpConfig = null,
217
+ mcpConfigPath = null
212
218
  }) {
213
219
  if (!task || typeof task !== 'string') {
214
220
  throw new Error('Task parameter is required and must be a string');
@@ -270,7 +276,10 @@ export async function delegate({
270
276
  allowedTools,
271
277
  disableTools,
272
278
  searchDelegate,
273
- enableTasks // Inherit from parent (subagent gets isolated TaskManager)
279
+ enableTasks, // Inherit from parent (subagent gets isolated TaskManager)
280
+ enableMcp, // Inherit from parent (subagent creates own MCPXmlBridge)
281
+ mcpConfig, // Inherit from parent
282
+ mcpConfigPath // Inherit from parent
274
283
  });
275
284
 
276
285
  if (debug) {
@@ -469,7 +469,7 @@ export const extractTool = (options = {}) => {
469
469
  * @returns {Object} Configured delegate tool
470
470
  */
471
471
  export const delegateTool = (options = {}) => {
472
- const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName } = options;
472
+ const { debug = false, timeout = 300, cwd, allowedFolders, enableBash = false, bashConfig, architectureFileName, enableMcp = false, mcpConfig = null, mcpConfigPath = null } = options;
473
473
 
474
474
  return tool({
475
475
  name: 'delegate',
@@ -558,7 +558,10 @@ export const delegateTool = (options = {}) => {
558
558
  enableBash,
559
559
  bashConfig,
560
560
  architectureFileName,
561
- searchDelegate
561
+ searchDelegate,
562
+ enableMcp,
563
+ mcpConfig,
564
+ mcpConfigPath
562
565
  });
563
566
 
564
567
  return result;