@probelabs/probe 0.6.0-rc209 → 0.6.0-rc211
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-rc211-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc211-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc211-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc211-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc211-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/ProbeAgent.js +100 -7
- package/build/agent/bashCommandUtils.js +98 -12
- package/build/agent/bashPermissions.js +207 -1
- package/build/agent/index.js +911 -90
- package/build/agent/probeTool.js +11 -2
- package/build/agent/tools.js +8 -0
- package/build/delegate.js +11 -2
- package/build/index.js +6 -1
- package/build/search.js +2 -2
- package/build/tools/analyzeAll.js +624 -0
- package/build/tools/common.js +149 -85
- package/build/tools/langchain.js +1 -1
- package/build/tools/vercel.js +66 -4
- package/cjs/agent/ProbeAgent.cjs +9841 -6642
- package/cjs/index.cjs +9955 -6750
- package/package.json +1 -1
- package/src/agent/ProbeAgent.js +100 -7
- package/src/agent/bashCommandUtils.js +98 -12
- package/src/agent/bashPermissions.js +207 -1
- package/src/agent/probeTool.js +11 -2
- package/src/agent/tools.js +8 -0
- package/src/delegate.js +11 -2
- package/src/index.js +6 -1
- package/src/search.js +2 -2
- package/src/tools/analyzeAll.js +624 -0
- package/src/tools/common.js +149 -85
- package/src/tools/langchain.js +1 -1
- package/src/tools/vercel.js +66 -4
- package/bin/binaries/probe-v0.6.0-rc209-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc209-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc209-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc209-x86_64-pc-windows-msvc.zip +0 -0
- package/bin/binaries/probe-v0.6.0-rc209-x86_64-unknown-linux-musl.tar.gz +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
queryToolDefinition,
|
|
25
25
|
extractToolDefinition,
|
|
26
26
|
delegateToolDefinition,
|
|
27
|
+
analyzeAllToolDefinition,
|
|
27
28
|
bashToolDefinition,
|
|
28
29
|
listFilesToolDefinition,
|
|
29
30
|
searchFilesToolDefinition,
|
|
@@ -561,6 +562,9 @@ export class ProbeAgent {
|
|
|
561
562
|
if (this.enableDelegate && wrappedTools.delegateToolInstance && isToolAllowed('delegate')) {
|
|
562
563
|
this.toolImplementations.delegate = wrappedTools.delegateToolInstance;
|
|
563
564
|
}
|
|
565
|
+
if (wrappedTools.analyzeAllToolInstance && isToolAllowed('analyze_all')) {
|
|
566
|
+
this.toolImplementations.analyze_all = wrappedTools.analyzeAllToolInstance;
|
|
567
|
+
}
|
|
564
568
|
|
|
565
569
|
// File browsing tools
|
|
566
570
|
if (isToolAllowed('listFiles')) {
|
|
@@ -2065,6 +2069,11 @@ ${extractGuidance}
|
|
|
2065
2069
|
toolDefinitions += `${delegateToolDefinition}\n`;
|
|
2066
2070
|
}
|
|
2067
2071
|
|
|
2072
|
+
// Analyze All tool for bulk data processing
|
|
2073
|
+
if (isToolAllowed('analyze_all')) {
|
|
2074
|
+
toolDefinitions += `${analyzeAllToolDefinition}\n`;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2068
2077
|
// Build XML tool guidelines with dynamic examples based on allowed tools
|
|
2069
2078
|
// Build examples only for allowed tools
|
|
2070
2079
|
let toolExamples = '';
|
|
@@ -2129,6 +2138,9 @@ The configuration is loaded from src/config.js lines 15-25 which contains the da
|
|
|
2129
2138
|
if (this.enableDelegate && isToolAllowed('delegate')) {
|
|
2130
2139
|
availableToolsList += '- delegate: Delegate big distinct tasks to specialized probe subagents.\n';
|
|
2131
2140
|
}
|
|
2141
|
+
if (isToolAllowed('analyze_all')) {
|
|
2142
|
+
availableToolsList += '- analyze_all: Process ALL data matching a query using map-reduce (for aggregate questions needing 100% coverage).\n';
|
|
2143
|
+
}
|
|
2132
2144
|
if (this.enableBash && isToolAllowed('bash')) {
|
|
2133
2145
|
availableToolsList += '- bash: Execute bash commands for system operations.\n';
|
|
2134
2146
|
}
|
|
@@ -2587,6 +2599,11 @@ Follow these instructions carefully:
|
|
|
2587
2599
|
let sameFormatErrorCount = 0;
|
|
2588
2600
|
const MAX_REPEATED_FORMAT_ERRORS = 3;
|
|
2589
2601
|
|
|
2602
|
+
// Circuit breaker for repeated identical responses without tool calls
|
|
2603
|
+
let lastNoToolResponse = null;
|
|
2604
|
+
let sameResponseCount = 0;
|
|
2605
|
+
const MAX_REPEATED_IDENTICAL_RESPONSES = 3;
|
|
2606
|
+
|
|
2590
2607
|
// Tool iteration loop (only for non-CLI engines like Vercel/Anthropic/OpenAI)
|
|
2591
2608
|
while (currentIteration < maxIterations && !completionAttempted) {
|
|
2592
2609
|
currentIteration++;
|
|
@@ -2637,11 +2654,11 @@ Follow these instructions carefully:
|
|
|
2637
2654
|
if (!maxResponseTokens) {
|
|
2638
2655
|
// Use model-based defaults if not explicitly configured
|
|
2639
2656
|
maxResponseTokens = 4000;
|
|
2640
|
-
if (this.model.includes('opus') || this.model.includes('sonnet') || this.model.startsWith('gpt-4-')) {
|
|
2657
|
+
if (this.model && this.model.includes('opus') || this.model && this.model.includes('sonnet') || this.model && this.model.startsWith('gpt-4-')) {
|
|
2641
2658
|
maxResponseTokens = 8192;
|
|
2642
|
-
} else if (this.model.startsWith('gpt-4o')) {
|
|
2659
|
+
} else if (this.model && this.model.startsWith('gpt-4o')) {
|
|
2643
2660
|
maxResponseTokens = 8192;
|
|
2644
|
-
} else if (this.model.startsWith('gemini')) {
|
|
2661
|
+
} else if (this.model && this.model.startsWith('gemini')) {
|
|
2645
2662
|
maxResponseTokens = 32000;
|
|
2646
2663
|
}
|
|
2647
2664
|
}
|
|
@@ -3224,6 +3241,36 @@ Follow these instructions carefully:
|
|
|
3224
3241
|
break;
|
|
3225
3242
|
}
|
|
3226
3243
|
|
|
3244
|
+
// Check for repeated identical responses - if AI gives same response 3 times,
|
|
3245
|
+
// accept it as the final answer instead of continuing the loop
|
|
3246
|
+
if (lastNoToolResponse !== null && assistantResponseContent === lastNoToolResponse) {
|
|
3247
|
+
sameResponseCount++;
|
|
3248
|
+
if (sameResponseCount >= MAX_REPEATED_IDENTICAL_RESPONSES) {
|
|
3249
|
+
// Clean up the response - remove thinking tags
|
|
3250
|
+
let cleanedResponse = assistantResponseContent;
|
|
3251
|
+
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*?<\/thinking>/gi, '').trim();
|
|
3252
|
+
cleanedResponse = cleanedResponse.replace(/<thinking>[\s\S]*$/gi, '').trim();
|
|
3253
|
+
|
|
3254
|
+
const hasSubstantialContent = cleanedResponse.length > 50 &&
|
|
3255
|
+
!cleanedResponse.includes('<api_call>') &&
|
|
3256
|
+
!cleanedResponse.includes('<tool_name>') &&
|
|
3257
|
+
!cleanedResponse.includes('<function>');
|
|
3258
|
+
|
|
3259
|
+
if (hasSubstantialContent) {
|
|
3260
|
+
if (this.debug) {
|
|
3261
|
+
console.log(`[DEBUG] Same response repeated ${sameResponseCount} times - accepting as final answer (${cleanedResponse.length} chars)`);
|
|
3262
|
+
}
|
|
3263
|
+
finalResult = cleanedResponse;
|
|
3264
|
+
completionAttempted = true;
|
|
3265
|
+
break;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
} else {
|
|
3269
|
+
// Different response, reset counter
|
|
3270
|
+
lastNoToolResponse = assistantResponseContent;
|
|
3271
|
+
sameResponseCount = 1;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3227
3274
|
// Add assistant response and ask for tool usage
|
|
3228
3275
|
currentMessages.push({ role: 'assistant', content: assistantResponseContent });
|
|
3229
3276
|
|
|
@@ -3313,10 +3360,56 @@ Or if your previous response already contains a complete, direct answer (not a t
|
|
|
3313
3360
|
Note: <attempt_complete></attempt_complete> reuses your PREVIOUS assistant message as the final answer. Only use this if that message was already a valid, complete response to the user's question.`;
|
|
3314
3361
|
}
|
|
3315
3362
|
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3363
|
+
// Check if we should replace the previous reminder instead of appending
|
|
3364
|
+
// After pushing assistant message, the previous user message (if a reminder) is at length - 2
|
|
3365
|
+
// Message pattern: [..., prev_assistant, prev_user_reminder, current_assistant]
|
|
3366
|
+
const prevUserMsgIndex = currentMessages.length - 2;
|
|
3367
|
+
const prevUserMsg = currentMessages[prevUserMsgIndex];
|
|
3368
|
+
const isExistingReminder = prevUserMsg && prevUserMsg.role === 'user' &&
|
|
3369
|
+
(prevUserMsg.content.includes('Please use one of the available tools') ||
|
|
3370
|
+
prevUserMsg.content.includes('<tool_result>'));
|
|
3371
|
+
|
|
3372
|
+
if (isExistingReminder && sameResponseCount > 1) {
|
|
3373
|
+
// Replace the previous reminder with updated content and remove duplicated assistant message
|
|
3374
|
+
// This prevents context bloat from repeated identical exchanges
|
|
3375
|
+
// Pattern: [..., prev_assistant, prev_user_reminder, current_assistant] -> [..., current_assistant, new_reminder]
|
|
3376
|
+
const prevAssistantIndex = prevUserMsgIndex - 1;
|
|
3377
|
+
|
|
3378
|
+
// Validate the expected pattern before splicing:
|
|
3379
|
+
// 1. prevAssistantIndex must be valid (>= 0)
|
|
3380
|
+
// 2. If there's a system message at index 0, don't remove it (prevAssistantIndex > 0)
|
|
3381
|
+
// 3. Must be an assistant message at prevAssistantIndex
|
|
3382
|
+
// 4. After removal, array should have at least 2 messages (current assistant + new reminder)
|
|
3383
|
+
const hasSystemMessage = currentMessages.length > 0 && currentMessages[0].role === 'system';
|
|
3384
|
+
const minValidIndex = hasSystemMessage ? 1 : 0;
|
|
3385
|
+
const canSafelyRemove = prevAssistantIndex >= minValidIndex &&
|
|
3386
|
+
currentMessages[prevAssistantIndex] &&
|
|
3387
|
+
currentMessages[prevAssistantIndex].role === 'assistant' &&
|
|
3388
|
+
(currentMessages.length - 2) >= (hasSystemMessage ? 2 : 1); // After removal: at least system+assistant or just assistant
|
|
3389
|
+
|
|
3390
|
+
if (canSafelyRemove) {
|
|
3391
|
+
// Remove the duplicate assistant and old reminder (2 messages starting at prevAssistantIndex)
|
|
3392
|
+
currentMessages.splice(prevAssistantIndex, 2);
|
|
3393
|
+
if (this.debug) {
|
|
3394
|
+
console.log(`[DEBUG] Removed duplicate assistant+reminder pair (iteration ${currentIteration}, same response #${sameResponseCount})`);
|
|
3395
|
+
}
|
|
3396
|
+
} else if (this.debug) {
|
|
3397
|
+
console.log(`[DEBUG] Skipped deduplication: pattern validation failed (prevAssistantIndex=${prevAssistantIndex}, arrayLength=${currentMessages.length})`);
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3400
|
+
// Add iteration context to help the AI understand this is a repeated attempt
|
|
3401
|
+
const iterationHint = `\n\n(Attempt #${sameResponseCount}: Your previous ${sameResponseCount} responses were identical. If you have a complete answer, use <attempt_complete></attempt_complete> to finalize it.)`;
|
|
3402
|
+
currentMessages.push({
|
|
3403
|
+
role: 'user',
|
|
3404
|
+
content: reminderContent + iterationHint
|
|
3405
|
+
});
|
|
3406
|
+
} else {
|
|
3407
|
+
currentMessages.push({
|
|
3408
|
+
role: 'user',
|
|
3409
|
+
content: reminderContent
|
|
3410
|
+
});
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3320
3413
|
if (this.debug) {
|
|
3321
3414
|
if (unrecognizedTool) {
|
|
3322
3415
|
console.log(`[DEBUG] Unrecognized tool '${unrecognizedTool}' used. Providing error feedback.`);
|
|
@@ -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(
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
}
|