@probelabs/probe 0.6.0-rc255 → 0.6.0-rc257
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/README.md +5 -5
- package/bin/binaries/probe-v0.6.0-rc257-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc257-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc257-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/{probe-v0.6.0-rc255-x86_64-pc-windows-msvc.zip → probe-v0.6.0-rc257-x86_64-pc-windows-msvc.zip} +0 -0
- package/bin/binaries/probe-v0.6.0-rc257-x86_64-unknown-linux-musl.tar.gz +0 -0
- package/build/agent/FallbackManager.js +4 -4
- package/build/agent/ProbeAgent.js +23 -17
- package/build/agent/bashDefaults.js +175 -97
- package/build/agent/bashPermissions.js +98 -45
- package/build/agent/index.js +335 -205
- package/build/agent/mcp/xmlBridge.js +3 -2
- package/build/agent/schemaUtils.js +127 -0
- package/build/tools/bash.js +2 -2
- package/build/tools/common.js +20 -3
- package/cjs/agent/ProbeAgent.cjs +343 -203
- package/cjs/index.cjs +343 -203
- package/package.json +1 -1
- package/src/agent/FallbackManager.js +4 -4
- package/src/agent/ProbeAgent.js +23 -17
- package/src/agent/bashDefaults.js +175 -97
- package/src/agent/bashPermissions.js +98 -45
- package/src/agent/index.js +4 -4
- package/src/agent/mcp/xmlBridge.js +3 -2
- package/src/agent/schemaUtils.js +127 -0
- package/src/tools/bash.js +2 -2
- package/src/tools/common.js +20 -3
- package/bin/binaries/probe-v0.6.0-rc255-aarch64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-aarch64-unknown-linux-musl.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-apple-darwin.tar.gz +0 -0
- package/bin/binaries/probe-v0.6.0-rc255-x86_64-unknown-linux-musl.tar.gz +0 -0
|
@@ -79,9 +79,19 @@ function matchesAnyPattern(parsedCommand, patterns) {
|
|
|
79
79
|
export class BashPermissionChecker {
|
|
80
80
|
/**
|
|
81
81
|
* Create a permission checker
|
|
82
|
+
*
|
|
83
|
+
* Priority order (highest to lowest):
|
|
84
|
+
* 1. Custom deny — always blocks (user explicitly blocked it)
|
|
85
|
+
* 2. Custom allow — overrides default deny (user explicitly allowed it)
|
|
86
|
+
* 3. Default deny — blocks by default
|
|
87
|
+
* 4. Allow list — allows recognized safe commands
|
|
88
|
+
*
|
|
89
|
+
* This means `--bash-allow "git:push"` overrides the default deny for git:push
|
|
90
|
+
* without requiring `--no-default-bash-deny`.
|
|
91
|
+
*
|
|
82
92
|
* @param {Object} config - Configuration options
|
|
83
|
-
* @param {string[]} [config.allow] - Additional allow patterns
|
|
84
|
-
* @param {string[]} [config.deny] - Additional deny patterns
|
|
93
|
+
* @param {string[]} [config.allow] - Additional allow patterns (override default deny)
|
|
94
|
+
* @param {string[]} [config.deny] - Additional deny patterns (always win)
|
|
85
95
|
* @param {boolean} [config.disableDefaultAllow] - Disable default allow list
|
|
86
96
|
* @param {boolean} [config.disableDefaultDeny] - Disable default deny list
|
|
87
97
|
* @param {boolean} [config.debug] - Enable debug logging
|
|
@@ -90,38 +100,19 @@ export class BashPermissionChecker {
|
|
|
90
100
|
constructor(config = {}) {
|
|
91
101
|
this.debug = config.debug || false;
|
|
92
102
|
this.tracer = config.tracer || null;
|
|
93
|
-
|
|
94
|
-
// Build allow patterns
|
|
95
|
-
this.allowPatterns = [];
|
|
96
|
-
if (!config.disableDefaultAllow) {
|
|
97
|
-
this.allowPatterns.push(...DEFAULT_ALLOW_PATTERNS);
|
|
98
|
-
if (this.debug) {
|
|
99
|
-
console.log(`[BashPermissions] Added ${DEFAULT_ALLOW_PATTERNS.length} default allow patterns`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (config.allow && Array.isArray(config.allow)) {
|
|
103
|
-
this.allowPatterns.push(...config.allow);
|
|
104
|
-
if (this.debug) {
|
|
105
|
-
console.log(`[BashPermissions] Added ${config.allow.length} custom allow patterns:`, config.allow);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
103
|
|
|
109
|
-
//
|
|
110
|
-
this.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (config.deny && Array.isArray(config.deny)) {
|
|
118
|
-
this.denyPatterns.push(...config.deny);
|
|
119
|
-
if (this.debug) {
|
|
120
|
-
console.log(`[BashPermissions] Added ${config.deny.length} custom deny patterns:`, config.deny);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
104
|
+
// Separate default and custom patterns for priority-based resolution
|
|
105
|
+
this.defaultAllowPatterns = config.disableDefaultAllow ? [] : [...DEFAULT_ALLOW_PATTERNS];
|
|
106
|
+
this.customAllowPatterns = (config.allow && Array.isArray(config.allow)) ? [...config.allow] : [];
|
|
107
|
+
this.allowPatterns = [...this.defaultAllowPatterns, ...this.customAllowPatterns];
|
|
108
|
+
|
|
109
|
+
this.defaultDenyPatterns = config.disableDefaultDeny ? [] : [...DEFAULT_DENY_PATTERNS];
|
|
110
|
+
this.customDenyPatterns = (config.deny && Array.isArray(config.deny)) ? [...config.deny] : [];
|
|
111
|
+
this.denyPatterns = [...this.defaultDenyPatterns, ...this.customDenyPatterns];
|
|
123
112
|
|
|
124
113
|
if (this.debug) {
|
|
114
|
+
console.log(`[BashPermissions] Default allow: ${this.defaultAllowPatterns.length}, Custom allow: ${this.customAllowPatterns.length}`);
|
|
115
|
+
console.log(`[BashPermissions] Default deny: ${this.defaultDenyPatterns.length}, Custom deny: ${this.customDenyPatterns.length}`);
|
|
125
116
|
console.log(`[BashPermissions] Total patterns - Allow: ${this.allowPatterns.length}, Deny: ${this.denyPatterns.length}`);
|
|
126
117
|
}
|
|
127
118
|
|
|
@@ -129,8 +120,8 @@ export class BashPermissionChecker {
|
|
|
129
120
|
this.recordBashEvent('permissions.initialized', {
|
|
130
121
|
allowPatternCount: this.allowPatterns.length,
|
|
131
122
|
denyPatternCount: this.denyPatterns.length,
|
|
132
|
-
hasCustomAllowPatterns:
|
|
133
|
-
hasCustomDenyPatterns:
|
|
123
|
+
hasCustomAllowPatterns: this.customAllowPatterns.length > 0,
|
|
124
|
+
hasCustomDenyPatterns: this.customDenyPatterns.length > 0,
|
|
134
125
|
disableDefaultAllow: !!config.disableDefaultAllow,
|
|
135
126
|
disableDefaultDeny: !!config.disableDefaultDeny
|
|
136
127
|
});
|
|
@@ -212,9 +203,18 @@ export class BashPermissionChecker {
|
|
|
212
203
|
console.log(`[BashPermissions] Parsed: ${parsed.command} with args: [${parsed.args.join(', ')}]`);
|
|
213
204
|
}
|
|
214
205
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
206
|
+
// Priority-based permission check:
|
|
207
|
+
// 1. Custom deny always wins
|
|
208
|
+
// 2. Custom allow overrides default deny
|
|
209
|
+
// 3. Default deny blocks
|
|
210
|
+
// 4. Allow list permits
|
|
211
|
+
|
|
212
|
+
// Step 1: Custom deny always wins
|
|
213
|
+
if (matchesAnyPattern(parsed, this.customDenyPatterns)) {
|
|
214
|
+
const matchedPatterns = this.customDenyPatterns.filter(pattern => matchesPattern(parsed, pattern));
|
|
215
|
+
if (this.debug) {
|
|
216
|
+
console.log(`[BashPermissions] DENIED - matches custom deny pattern: ${matchedPatterns[0]}`);
|
|
217
|
+
}
|
|
218
218
|
const result = {
|
|
219
219
|
allowed: false,
|
|
220
220
|
reason: `Command matches deny pattern: ${matchedPatterns[0]}`,
|
|
@@ -227,12 +227,40 @@ export class BashPermissionChecker {
|
|
|
227
227
|
parsedCommand: parsed.command,
|
|
228
228
|
reason: 'matches_deny_pattern',
|
|
229
229
|
matchedPattern: matchedPatterns[0],
|
|
230
|
-
isComplex: false
|
|
230
|
+
isComplex: false,
|
|
231
|
+
isCustomDeny: true
|
|
231
232
|
});
|
|
232
233
|
return result;
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
//
|
|
236
|
+
// Step 2: Custom allow overrides default deny
|
|
237
|
+
const matchesCustomAllow = matchesAnyPattern(parsed, this.customAllowPatterns);
|
|
238
|
+
|
|
239
|
+
// Step 3: Default deny (skipped if custom allow matches)
|
|
240
|
+
if (!matchesCustomAllow && matchesAnyPattern(parsed, this.defaultDenyPatterns)) {
|
|
241
|
+
const matchedPatterns = this.defaultDenyPatterns.filter(pattern => matchesPattern(parsed, pattern));
|
|
242
|
+
if (this.debug) {
|
|
243
|
+
console.log(`[BashPermissions] DENIED - matches default deny pattern: ${matchedPatterns[0]}`);
|
|
244
|
+
}
|
|
245
|
+
const result = {
|
|
246
|
+
allowed: false,
|
|
247
|
+
reason: `Command matches deny pattern: ${matchedPatterns[0]}`,
|
|
248
|
+
command: command,
|
|
249
|
+
parsed: parsed,
|
|
250
|
+
matchedPatterns: matchedPatterns
|
|
251
|
+
};
|
|
252
|
+
this.recordBashEvent('permission.denied', {
|
|
253
|
+
command,
|
|
254
|
+
parsedCommand: parsed.command,
|
|
255
|
+
reason: 'matches_deny_pattern',
|
|
256
|
+
matchedPattern: matchedPatterns[0],
|
|
257
|
+
isComplex: false,
|
|
258
|
+
isCustomDeny: false
|
|
259
|
+
});
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Step 4: Check allow patterns
|
|
236
264
|
if (this.allowPatterns.length > 0) {
|
|
237
265
|
if (!matchesAnyPattern(parsed, this.allowPatterns)) {
|
|
238
266
|
const result = {
|
|
@@ -256,17 +284,23 @@ export class BashPermissionChecker {
|
|
|
256
284
|
allowed: true,
|
|
257
285
|
command: command,
|
|
258
286
|
parsed: parsed,
|
|
259
|
-
isComplex: false
|
|
287
|
+
isComplex: false,
|
|
288
|
+
overriddenDeny: matchesCustomAllow && matchesAnyPattern(parsed, this.defaultDenyPatterns)
|
|
260
289
|
};
|
|
261
290
|
|
|
262
291
|
if (this.debug) {
|
|
263
|
-
|
|
292
|
+
if (result.overriddenDeny) {
|
|
293
|
+
console.log(`[BashPermissions] ALLOWED - custom allow overrides default deny`);
|
|
294
|
+
} else {
|
|
295
|
+
console.log(`[BashPermissions] ALLOWED - command passed all checks`);
|
|
296
|
+
}
|
|
264
297
|
}
|
|
265
298
|
|
|
266
299
|
this.recordBashEvent('permission.allowed', {
|
|
267
300
|
command,
|
|
268
301
|
parsedCommand: parsed.command,
|
|
269
|
-
isComplex: false
|
|
302
|
+
isComplex: false,
|
|
303
|
+
overriddenDeny: result.overriddenDeny || false
|
|
270
304
|
});
|
|
271
305
|
|
|
272
306
|
return result;
|
|
@@ -477,10 +511,25 @@ export class BashPermissionChecker {
|
|
|
477
511
|
break;
|
|
478
512
|
}
|
|
479
513
|
|
|
480
|
-
// Check
|
|
481
|
-
|
|
514
|
+
// Check using same priority logic as simple commands:
|
|
515
|
+
// 1. Custom deny always wins
|
|
516
|
+
if (matchesAnyPattern(parsed, this.customDenyPatterns)) {
|
|
517
|
+
if (this.debug) {
|
|
518
|
+
console.log(`[BashPermissions] Component "${component}" matches custom deny pattern`);
|
|
519
|
+
}
|
|
520
|
+
allAllowed = false;
|
|
521
|
+
deniedComponent = component;
|
|
522
|
+
deniedReason = 'Component matches deny pattern';
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 2. Custom allow overrides default deny
|
|
527
|
+
const componentMatchesCustomAllow = matchesAnyPattern(parsed, this.customAllowPatterns);
|
|
528
|
+
|
|
529
|
+
// 3. Default deny (skipped if custom allow matches)
|
|
530
|
+
if (!componentMatchesCustomAllow && matchesAnyPattern(parsed, this.defaultDenyPatterns)) {
|
|
482
531
|
if (this.debug) {
|
|
483
|
-
console.log(`[BashPermissions] Component "${component}" matches deny pattern`);
|
|
532
|
+
console.log(`[BashPermissions] Component "${component}" matches default deny pattern`);
|
|
484
533
|
}
|
|
485
534
|
allAllowed = false;
|
|
486
535
|
deniedComponent = component;
|
|
@@ -488,7 +537,7 @@ export class BashPermissionChecker {
|
|
|
488
537
|
break;
|
|
489
538
|
}
|
|
490
539
|
|
|
491
|
-
// Check
|
|
540
|
+
// 4. Check allow patterns
|
|
492
541
|
if (!matchesAnyPattern(parsed, this.allowPatterns)) {
|
|
493
542
|
if (this.debug) {
|
|
494
543
|
console.log(`[BashPermissions] Component "${component}" not in allow list`);
|
|
@@ -567,6 +616,10 @@ export class BashPermissionChecker {
|
|
|
567
616
|
return {
|
|
568
617
|
allowPatterns: this.allowPatterns.length,
|
|
569
618
|
denyPatterns: this.denyPatterns.length,
|
|
619
|
+
customAllowPatterns: this.customAllowPatterns.length,
|
|
620
|
+
customDenyPatterns: this.customDenyPatterns.length,
|
|
621
|
+
defaultAllowPatterns: this.defaultAllowPatterns.length,
|
|
622
|
+
defaultDenyPatterns: this.defaultDenyPatterns.length,
|
|
570
623
|
totalPatterns: this.allowPatterns.length + this.denyPatterns.length
|
|
571
624
|
};
|
|
572
625
|
}
|
package/src/agent/index.js
CHANGED
|
@@ -623,9 +623,9 @@ class ProbeAgentMcpServer {
|
|
|
623
623
|
// Retry once with correction prompt
|
|
624
624
|
const correctionPrompt = createJsonCorrectionPrompt(result, schema, validation.error);
|
|
625
625
|
try {
|
|
626
|
-
result = await agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true });
|
|
626
|
+
result = await agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true, _maxIterationsOverride: 3 });
|
|
627
627
|
result = cleanSchemaResponse(result);
|
|
628
|
-
|
|
628
|
+
|
|
629
629
|
// Validate again after correction
|
|
630
630
|
const finalValidation = validateJsonResponse(result);
|
|
631
631
|
if (!finalValidation.isValid && args.debug) {
|
|
@@ -971,11 +971,11 @@ async function main() {
|
|
|
971
971
|
try {
|
|
972
972
|
if (appTracer) {
|
|
973
973
|
result = await appTracer.withSpan('agent.json_correction',
|
|
974
|
-
() => agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true }),
|
|
974
|
+
() => agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true, _maxIterationsOverride: 3 }),
|
|
975
975
|
{ 'original_error': validation.error }
|
|
976
976
|
);
|
|
977
977
|
} else {
|
|
978
|
-
result = await agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true });
|
|
978
|
+
result = await agent.answer(correctionPrompt, [], { schema, _schemaFormatted: true, _disableTools: true, _maxIterationsOverride: 3 });
|
|
979
979
|
}
|
|
980
980
|
result = cleanSchemaResponse(result);
|
|
981
981
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { MCPClientManager } from './client.js';
|
|
7
7
|
import { loadMCPConfiguration } from './config.js';
|
|
8
8
|
import { processXmlWithThinkingAndRecovery } from '../xmlParsingUtils.js';
|
|
9
|
+
import { unescapeXmlEntities } from '../../tools/common.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Convert MCP tool to XML definition format
|
|
@@ -111,7 +112,7 @@ export function parseXmlMcpToolCall(xmlString, mcpToolNames = []) {
|
|
|
111
112
|
let match;
|
|
112
113
|
while ((match = paramPattern.exec(content)) !== null) {
|
|
113
114
|
const [, paramName, paramValue] = match;
|
|
114
|
-
params[paramName] = paramValue.trim();
|
|
115
|
+
params[paramName] = unescapeXmlEntities(paramValue.trim());
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -393,7 +394,7 @@ function parseNativeXmlTool(xmlString, toolName) {
|
|
|
393
394
|
const [, paramName, paramValue] = match;
|
|
394
395
|
// Skip if this is the params tag itself (MCP format)
|
|
395
396
|
if (paramName !== 'params') {
|
|
396
|
-
params[paramName] = paramValue.trim();
|
|
397
|
+
params[paramName] = unescapeXmlEntities(paramValue.trim());
|
|
397
398
|
}
|
|
398
399
|
}
|
|
399
400
|
|
package/src/agent/schemaUtils.js
CHANGED
|
@@ -603,6 +603,17 @@ export function validateJsonResponse(response, options = {}) {
|
|
|
603
603
|
}
|
|
604
604
|
}
|
|
605
605
|
|
|
606
|
+
// Quick recovery: try to extract a valid JSON prefix before returning error.
|
|
607
|
+
// This handles the common case where AI returns valid JSON followed by markdown content,
|
|
608
|
+
// e.g., "Unexpected non-whitespace character after JSON at position 477" (issue #447).
|
|
609
|
+
const prefixResult = tryExtractValidJsonPrefix(responseToValidate, { schema, debug });
|
|
610
|
+
if (prefixResult && prefixResult.isValid) {
|
|
611
|
+
if (debug) {
|
|
612
|
+
console.log(`[DEBUG] JSON validation: Recovered valid JSON prefix (${prefixResult.extracted.length} chars) from response with trailing content`);
|
|
613
|
+
}
|
|
614
|
+
return { isValid: true, parsed: prefixResult.parsed };
|
|
615
|
+
}
|
|
616
|
+
|
|
606
617
|
// Create enhanced error message with context snippet
|
|
607
618
|
let enhancedError = error.message;
|
|
608
619
|
let errorContext = null;
|
|
@@ -667,6 +678,122 @@ ${pointer} here`;
|
|
|
667
678
|
}
|
|
668
679
|
}
|
|
669
680
|
|
|
681
|
+
/**
|
|
682
|
+
* Try to extract a valid JSON prefix from a string that has trailing non-JSON content.
|
|
683
|
+
* This handles the common case where an AI returns valid JSON followed by markdown text.
|
|
684
|
+
*
|
|
685
|
+
* Uses bracket-matching to find the end of the first complete JSON object/array,
|
|
686
|
+
* then validates the prefix with JSON.parse(). Optionally validates against a schema.
|
|
687
|
+
*
|
|
688
|
+
* @param {string} response - The full response string
|
|
689
|
+
* @param {Object} [options] - Options
|
|
690
|
+
* @param {Object|string} [options.schema] - JSON schema to validate against
|
|
691
|
+
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
692
|
+
* @returns {Object|null} - {isValid: true, parsed: Object, extracted: string} or null if no valid prefix found
|
|
693
|
+
*/
|
|
694
|
+
export function tryExtractValidJsonPrefix(response, options = {}) {
|
|
695
|
+
const { schema = null, debug = false } = options;
|
|
696
|
+
|
|
697
|
+
if (!response || typeof response !== 'string') {
|
|
698
|
+
return null;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const trimmed = response.trim();
|
|
702
|
+
if (trimmed.length === 0) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Must start with { or [
|
|
707
|
+
const firstChar = trimmed[0];
|
|
708
|
+
if (firstChar !== '{' && firstChar !== '[') {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// First, check if the full string already parses - no need to extract prefix
|
|
713
|
+
try {
|
|
714
|
+
JSON.parse(trimmed);
|
|
715
|
+
return null; // Full string is valid JSON, no extraction needed
|
|
716
|
+
} catch {
|
|
717
|
+
// Expected - continue with prefix extraction
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Find the end of the first complete JSON object/array using bracket matching
|
|
721
|
+
const openChar = firstChar;
|
|
722
|
+
const closeChar = openChar === '{' ? '}' : ']';
|
|
723
|
+
let depth = 0;
|
|
724
|
+
let inString = false;
|
|
725
|
+
let escapeNext = false;
|
|
726
|
+
let endPos = -1;
|
|
727
|
+
|
|
728
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
729
|
+
const char = trimmed[i];
|
|
730
|
+
|
|
731
|
+
if (escapeNext) {
|
|
732
|
+
escapeNext = false;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (char === '\\' && inString) {
|
|
737
|
+
escapeNext = true;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (char === '"') {
|
|
742
|
+
inString = !inString;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (inString) {
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (char === openChar) {
|
|
751
|
+
depth++;
|
|
752
|
+
} else if (char === closeChar) {
|
|
753
|
+
depth--;
|
|
754
|
+
if (depth === 0) {
|
|
755
|
+
endPos = i + 1;
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (endPos <= 0 || endPos >= trimmed.length) {
|
|
762
|
+
return null; // No complete JSON found, or JSON spans the entire string
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Check that there's actual non-whitespace trailing content
|
|
766
|
+
const remainder = trimmed.substring(endPos).trim();
|
|
767
|
+
if (remainder.length === 0) {
|
|
768
|
+
return null; // Only whitespace after JSON, no trailing content issue
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Try to parse the prefix
|
|
772
|
+
const prefix = trimmed.substring(0, endPos);
|
|
773
|
+
try {
|
|
774
|
+
const parsed = JSON.parse(prefix);
|
|
775
|
+
|
|
776
|
+
if (debug) {
|
|
777
|
+
console.log(`[DEBUG] tryExtractValidJsonPrefix: Extracted valid JSON prefix (${prefix.length} chars), stripped trailing content (${remainder.length} chars)`);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// If schema provided, validate against it
|
|
781
|
+
if (schema) {
|
|
782
|
+
const schemaValidation = validateJsonResponse(prefix, { debug, schema });
|
|
783
|
+
if (!schemaValidation.isValid) {
|
|
784
|
+
if (debug) {
|
|
785
|
+
console.log(`[DEBUG] tryExtractValidJsonPrefix: Prefix is valid JSON but fails schema validation: ${schemaValidation.error}`);
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return { isValid: true, parsed, extracted: prefix };
|
|
792
|
+
} catch {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
670
797
|
/**
|
|
671
798
|
* Validate that the cleaned response is valid XML if expected
|
|
672
799
|
* @param {string} response - Cleaned response
|
package/src/tools/bash.js
CHANGED
|
@@ -146,8 +146,8 @@ Common reasons:
|
|
|
146
146
|
2. The command is not in the allow list (not a recognized safe command)
|
|
147
147
|
|
|
148
148
|
If you believe this command should be allowed, you can:
|
|
149
|
-
- Use the --bash-allow option to add specific patterns
|
|
150
|
-
|
|
149
|
+
- Use the --bash-allow option to add specific patterns (overrides default deny list)
|
|
150
|
+
Example: --bash-allow "git:push" allows git push while keeping all other deny rules
|
|
151
151
|
|
|
152
152
|
For code exploration, try these safe alternatives:
|
|
153
153
|
- ls, cat, head, tail for file operations
|
package/src/tools/common.js
CHANGED
|
@@ -521,6 +521,23 @@ function getValidParamsForTool(toolName) {
|
|
|
521
521
|
return [];
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Unescape standard XML entities in a string value.
|
|
526
|
+
* Order matters: & must be decoded LAST to avoid double-decoding
|
|
527
|
+
* (e.g., &lt; should become <, not <).
|
|
528
|
+
* @param {string} str - The string to unescape
|
|
529
|
+
* @returns {string} The unescaped string
|
|
530
|
+
*/
|
|
531
|
+
export function unescapeXmlEntities(str) {
|
|
532
|
+
if (typeof str !== 'string') return str;
|
|
533
|
+
return str
|
|
534
|
+
.replace(/</g, '<')
|
|
535
|
+
.replace(/>/g, '>')
|
|
536
|
+
.replace(/"/g, '"')
|
|
537
|
+
.replace(/'/g, "'")
|
|
538
|
+
.replace(/&/g, '&');
|
|
539
|
+
}
|
|
540
|
+
|
|
524
541
|
// Simple XML parser helper - safer string-based approach
|
|
525
542
|
export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
|
|
526
543
|
// Find the tool that appears EARLIEST in the string
|
|
@@ -609,10 +626,10 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
|
|
|
609
626
|
paramCloseIndex = nextTagIndex;
|
|
610
627
|
}
|
|
611
628
|
|
|
612
|
-
let paramValue = innerContent.substring(
|
|
629
|
+
let paramValue = unescapeXmlEntities(innerContent.substring(
|
|
613
630
|
paramOpenIndex + paramOpenTag.length,
|
|
614
631
|
paramCloseIndex
|
|
615
|
-
).trim();
|
|
632
|
+
).trim());
|
|
616
633
|
|
|
617
634
|
// Basic type inference (can be improved)
|
|
618
635
|
if (paramValue.toLowerCase() === 'true') {
|
|
@@ -633,7 +650,7 @@ export function parseXmlToolCall(xmlString, validTools = DEFAULT_VALID_TOOLS) {
|
|
|
633
650
|
|
|
634
651
|
// Special handling for attempt_completion - use entire inner content as result
|
|
635
652
|
if (toolName === 'attempt_completion') {
|
|
636
|
-
params['result'] = innerContent.trim();
|
|
653
|
+
params['result'] = unescapeXmlEntities(innerContent.trim());
|
|
637
654
|
// Remove command parameter if it was parsed by generic logic above (legacy compatibility)
|
|
638
655
|
if (params.command) {
|
|
639
656
|
delete params.command;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|