@probelabs/probe 0.6.0-rc208 → 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.
Files changed (30) hide show
  1. package/bin/binaries/probe-v0.6.0-rc210-aarch64-apple-darwin.tar.gz +0 -0
  2. package/bin/binaries/probe-v0.6.0-rc210-aarch64-unknown-linux-musl.tar.gz +0 -0
  3. package/bin/binaries/probe-v0.6.0-rc210-x86_64-apple-darwin.tar.gz +0 -0
  4. package/bin/binaries/probe-v0.6.0-rc210-x86_64-pc-windows-msvc.zip +0 -0
  5. package/bin/binaries/probe-v0.6.0-rc210-x86_64-unknown-linux-musl.tar.gz +0 -0
  6. package/build/agent/ProbeAgent.d.ts +4 -2
  7. package/build/agent/ProbeAgent.js +87 -5
  8. package/build/agent/bashCommandUtils.js +98 -12
  9. package/build/agent/bashPermissions.js +207 -1
  10. package/build/agent/index.js +283 -23
  11. package/build/agent/mcp/client.js +2 -1
  12. package/build/delegate.js +11 -2
  13. package/build/tools/vercel.js +5 -2
  14. package/cjs/agent/ProbeAgent.cjs +277 -17
  15. package/cjs/index.cjs +277 -17
  16. package/index.d.ts +4 -2
  17. package/package.json +1 -1
  18. package/src/agent/ProbeAgent.d.ts +4 -2
  19. package/src/agent/ProbeAgent.js +87 -5
  20. package/src/agent/bashCommandUtils.js +98 -12
  21. package/src/agent/bashPermissions.js +207 -1
  22. package/src/agent/index.js +5 -5
  23. package/src/agent/mcp/client.js +2 -1
  24. package/src/delegate.js +11 -2
  25. package/src/tools/vercel.js +5 -2
  26. package/bin/binaries/probe-v0.6.0-rc208-aarch64-apple-darwin.tar.gz +0 -0
  27. package/bin/binaries/probe-v0.6.0-rc208-aarch64-unknown-linux-musl.tar.gz +0 -0
  28. package/bin/binaries/probe-v0.6.0-rc208-x86_64-apple-darwin.tar.gz +0 -0
  29. package/bin/binaries/probe-v0.6.0-rc208-x86_64-pc-windows-msvc.zip +0 -0
  30. package/bin/binaries/probe-v0.6.0-rc208-x86_64-unknown-linux-musl.tar.gz +0 -0
@@ -1,10 +1,36 @@
1
1
  /**
2
2
  * Unified command parsing utilities for bash tool
3
- *
3
+ *
4
4
  * This module provides a single source of truth for parsing shell commands.
5
5
  * It supports only simple commands (no pipes, operators, or substitutions)
6
6
  * to align with the executor's capabilities.
7
- *
7
+ *
8
+ * ## Escape Handling Architecture
9
+ *
10
+ * There are THREE different escape handling behaviors in the bash permission system,
11
+ * each serving a distinct purpose:
12
+ *
13
+ * 1. **stripQuotedContent()** (in parseSimpleCommand): SKIPS both backslash AND next char
14
+ * - Purpose: Detect operators (|, &&, ||) that exist OUTSIDE quoted strings
15
+ * - Output is never used for execution, only for operator detection
16
+ * - Example: `echo "a && b"` → strips quoted content → no `&&` detected outside quotes
17
+ *
18
+ * 2. **parseSimpleCommand()** main loop: STRIPS backslash, KEEPS escaped char
19
+ * - Purpose: Extract actual argument values that would be passed to the command
20
+ * - Matches bash behavior where `\"` inside double quotes becomes `"`
21
+ * - Example: `echo "he said \"hi\""` → args: ['he said "hi"']
22
+ *
23
+ * 3. **_splitComplexCommand()** (in bashPermissions.js): PRESERVES both backslash AND next char
24
+ * - Purpose: Split complex commands by operators while preserving escape sequences
25
+ * - Output is passed to parseCommand() which will then interpret the escapes
26
+ * - Example: `echo "test\" && b" && cmd` → components passed to parseCommand for final parsing
27
+ *
28
+ * This design ensures:
29
+ * - Commands with operators inside quotes (e.g., `echo "a && b"`) are NOT incorrectly
30
+ * flagged as complex commands
31
+ * - Escaped quotes (e.g., `\"`) don't prematurely end quoted sections
32
+ * - Each component gets proper escape interpretation in the final parsing step
33
+ *
8
34
  * @module bashCommandUtils
9
35
  */
10
36
 
@@ -38,7 +64,64 @@ export function parseSimpleCommand(command) {
38
64
  };
39
65
  }
40
66
 
67
+ // Strip quoted content before checking for complex operators
68
+ // This prevents detecting operators inside quotes (e.g., echo "a && b")
69
+ //
70
+ // IMPORTANT: This function is ONLY used to detect operators, NOT for argument parsing.
71
+ // It REMOVES both backslash AND escaped character from the output, unlike parseSimpleCommand
72
+ // which interprets escapes. This is intentional - we only care about finding operators
73
+ // that exist outside of quoted strings. See module header for architecture details.
74
+ const stripQuotedContent = (str) => {
75
+ let result = '';
76
+ let inQuotes = false;
77
+ let quoteChar = '';
78
+
79
+ for (let i = 0; i < str.length; i++) {
80
+ const char = str[i];
81
+ const nextChar = str[i + 1];
82
+
83
+ // Handle escape sequences outside quotes - skip both chars (not operators)
84
+ if (!inQuotes && char === '\\' && nextChar !== undefined) {
85
+ // Skip the backslash and next char - they can't be operators
86
+ i++;
87
+ continue;
88
+ }
89
+
90
+ // Handle escape sequences inside double quotes
91
+ // In bash, only ", $, `, \, and newline are escapable in double quotes
92
+ if (inQuotes && quoteChar === '"' && char === '\\' && nextChar !== undefined) {
93
+ // Skip both the backslash and the escaped character (stays inside quotes)
94
+ i++;
95
+ continue;
96
+ }
97
+
98
+ // Start of quoted section
99
+ if (!inQuotes && (char === '"' || char === "'")) {
100
+ inQuotes = true;
101
+ quoteChar = char;
102
+ continue;
103
+ }
104
+
105
+ // End of quoted section
106
+ if (inQuotes && char === quoteChar) {
107
+ inQuotes = false;
108
+ quoteChar = '';
109
+ continue;
110
+ }
111
+
112
+ // Only add characters that are outside quotes
113
+ if (!inQuotes) {
114
+ result += char;
115
+ }
116
+ }
117
+
118
+ return result;
119
+ };
120
+
41
121
  // Check for complex shell constructs that we don't support
122
+ // Use stripped version (without quoted content) for operator detection
123
+ const strippedForOperators = stripQuotedContent(trimmed);
124
+
42
125
  const complexPatterns = [
43
126
  /\|/, // Pipes
44
127
  /&&/, // Logical AND
@@ -54,7 +137,7 @@ export function parseSimpleCommand(command) {
54
137
  ];
55
138
 
56
139
  for (const pattern of complexPatterns) {
57
- if (pattern.test(trimmed)) {
140
+ if (pattern.test(strippedForOperators)) {
58
141
  return {
59
142
  success: false,
60
143
  error: 'Complex shell commands with pipes, operators, or redirections are not supported for security reasons',
@@ -71,22 +154,25 @@ export function parseSimpleCommand(command) {
71
154
  let current = '';
72
155
  let inQuotes = false;
73
156
  let quoteChar = '';
74
- let escaped = false;
75
157
 
76
158
  for (let i = 0; i < trimmed.length; i++) {
77
159
  const char = trimmed[i];
78
160
  const nextChar = i + 1 < trimmed.length ? trimmed[i + 1] : '';
79
-
80
- if (escaped) {
81
- // Handle escaped characters
82
- current += char;
83
- escaped = false;
161
+
162
+ // Handle escapes outside quotes
163
+ if (!inQuotes && char === '\\' && nextChar) {
164
+ // Add the escaped character (skip the backslash)
165
+ current += nextChar;
166
+ i++; // Skip next character
84
167
  continue;
85
168
  }
86
169
 
87
- if (char === '\\' && !inQuotes) {
88
- // Escape next character
89
- escaped = true;
170
+ // Handle escapes inside double quotes (single quotes don't process escapes)
171
+ if (inQuotes && quoteChar === '"' && char === '\\' && nextChar) {
172
+ // In double quotes, backslash escapes certain characters
173
+ // Add the escaped character to current
174
+ current += nextChar;
175
+ i++; // Skip next character
90
176
  continue;
91
177
  }
92
178
 
@@ -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
  }
@@ -138,7 +138,7 @@ function parseArgs() {
138
138
  noMermaidValidation: false, // New flag to disable mermaid validation
139
139
  allowedTools: null, // Tool filtering: ['*'] = all, [] = none, ['tool1', 'tool2'] = specific
140
140
  disableTools: false, // Convenience flag to disable all tools
141
- disableSkills: false, // Disable skill discovery and activation
141
+ allowSkills: false, // Enable skill discovery and activation (disabled by default)
142
142
  skillDirs: null, // Comma-separated list of repo-relative skill directories
143
143
  // Task management
144
144
  enableTasks: false, // Enable task tracking for progress management
@@ -212,8 +212,8 @@ function parseArgs() {
212
212
  } else if (arg === '--disable-tools') {
213
213
  // Convenience flag to disable all tools (raw AI mode)
214
214
  config.disableTools = true;
215
- } else if (arg === '--no-skills') {
216
- config.disableSkills = true;
215
+ } else if (arg === '--allow-skills') {
216
+ config.allowSkills = true;
217
217
  } else if (arg === '--skills-dir' && i + 1 < args.length) {
218
218
  config.skillDirs = args[++i].split(',').map(dir => dir.trim()).filter(Boolean);
219
219
  } else if (arg === '--allow-tasks') {
@@ -280,8 +280,8 @@ Options:
280
280
  Supports exclusion: '*,!bash' (all except bash)
281
281
  --disable-tools Disable all tools (raw AI mode, no code analysis)
282
282
  Convenience flag equivalent to --allowed-tools none
283
+ --allow-skills Enable skill discovery and activation (disabled by default)
283
284
  --skills-dir <dirs> Comma-separated list of repo-relative skill directories to scan
284
- --no-skills Disable skill discovery and activation
285
285
  --allow-tasks Enable task management for tracking multi-step progress
286
286
  --verbose Enable verbose output
287
287
  --outline Use outline-xml format for code search results
@@ -841,7 +841,7 @@ async function main() {
841
841
  disableMermaidValidation: config.noMermaidValidation,
842
842
  allowedTools: config.allowedTools,
843
843
  disableTools: config.disableTools,
844
- enableSkills: !config.disableSkills,
844
+ allowSkills: config.allowSkills,
845
845
  skillDirs: config.skillDirs,
846
846
  enableBash: config.enableBash,
847
847
  bashConfig: bashConfig,
@@ -474,11 +474,12 @@ export class MCPClientManager {
474
474
  });
475
475
 
476
476
  // Race between the actual call and timeout
477
+ // Pass timeout to SDK's callTool to override its default 60s timeout
477
478
  const result = await Promise.race([
478
479
  clientInfo.client.callTool({
479
480
  name: tool.originalName,
480
481
  arguments: args
481
- }),
482
+ }, undefined, { timeout }),
482
483
  timeoutPromise
483
484
  ]);
484
485
 
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;