feat: enhance file tools to support base64 encoding for command arguments

- Updated bash-execute-script, create-files-script, edit-files-script, and grep-search-script to handle base64 encoded JSON arguments, improving robustness against data corruption.
- Refactored corresponding tool scripts to encode command parameters as base64 before execution.
- Enhanced integration tests to validate the new base64 encoding functionality across various file tools.
This commit is contained in:
dal 2025-07-28 16:55:52 -06:00
parent 3b706a73b3
commit ed9ab33b35
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
9 changed files with 180 additions and 53 deletions

View File

@ -115,12 +115,22 @@ async function main() {
}
try {
// Parse commands from JSON argument
const firstArg = args[0];
if (!firstArg) {
// Parse commands from JSON argument (possibly base64 encoded)
let commandsJson = args[0];
if (!commandsJson) {
throw new Error('No argument provided');
}
const commands: BashCommandParams[] = JSON.parse(firstArg);
// Try to decode from base64 if it looks like base64
if (commandsJson && /^[A-Za-z0-9+/]+=*$/.test(commandsJson) && commandsJson.length % 4 === 0) {
try {
commandsJson = Buffer.from(commandsJson, 'base64').toString('utf-8');
} catch {
// If base64 decode fails, use as-is
}
}
const commands: BashCommandParams[] = JSON.parse(commandsJson);
if (!Array.isArray(commands)) {
throw new Error('Commands must be an array');

View File

@ -50,8 +50,11 @@ const executeBashCommands = wrapTraced(
const scriptPath = path.join(__dirname, 'bash-execute-script.ts');
const scriptContent = await fs.readFile(scriptPath, 'utf-8');
// Pass commands as JSON string argument
const args = [JSON.stringify(commands)];
// Build command line arguments
// Base64 encode the JSON to avoid corruption when passing through sandbox
const commandsJson = JSON.stringify(commands);
const base64Commands = Buffer.from(commandsJson).toString('base64');
const args = [base64Commands];
const result = await runTypescript(sandbox, scriptContent, { argv: args });

View File

@ -51,8 +51,11 @@ const createFilesExecution = wrapTraced(
const scriptPath = path.join(__dirname, 'create-files-script.ts');
const scriptContent = await fs.readFile(scriptPath, 'utf-8');
// Pass file parameters as JSON string argument
const args = [JSON.stringify(files)];
// Pass file parameters as base64-encoded JSON string argument
// to avoid corruption when passing through sandbox
const filesJson = JSON.stringify(files);
const base64Files = Buffer.from(filesJson).toString('base64');
const args = [base64Files];
const result = await runTypescript(sandbox, scriptContent, { argv: args });

View File

@ -77,12 +77,26 @@ async function main() {
let fileParams: FileCreateParams[];
try {
// The script expects file parameters as a JSON string in the first argument
const firstArg = args[0];
if (!firstArg) {
// The script expects file parameters as a JSON string in the first argument (possibly base64 encoded)
let fileParamsJson = args[0];
if (!fileParamsJson) {
throw new Error('No argument provided');
}
fileParams = JSON.parse(firstArg);
// Try to decode from base64 if it looks like base64
if (
fileParamsJson &&
/^[A-Za-z0-9+/]+=*$/.test(fileParamsJson) &&
fileParamsJson.length % 4 === 0
) {
try {
fileParamsJson = Buffer.from(fileParamsJson, 'base64').toString('utf-8');
} catch {
// If base64 decode fails, use as-is
}
}
fileParams = JSON.parse(fileParamsJson);
if (!Array.isArray(fileParams)) {
throw new Error('File parameters must be an array');

View File

@ -98,12 +98,22 @@ async function main() {
let edits: FileEdit[];
try {
// The first argument should be a JSON string containing the edits array
const firstArg = args[0];
if (!firstArg) {
// The first argument should be a JSON string containing the edits array (possibly base64 encoded)
let editsJson = args[0];
if (!editsJson) {
throw new Error('No argument provided');
}
edits = JSON.parse(firstArg);
// Try to decode from base64 if it looks like base64
if (editsJson && /^[A-Za-z0-9+/]+=*$/.test(editsJson) && editsJson.length % 4 === 0) {
try {
editsJson = Buffer.from(editsJson, 'base64').toString('utf-8');
} catch {
// If base64 decode fails, use as-is
}
}
edits = JSON.parse(editsJson);
if (!Array.isArray(edits)) {
throw new Error('Input must be an array of edits');

View File

@ -67,8 +67,11 @@ const editFilesExecution = wrapTraced(
const scriptPath = path.join(__dirname, 'edit-files-script.ts');
const scriptContent = await fs.readFile(scriptPath, 'utf-8');
// Pass edits as JSON string argument
const args = [JSON.stringify(edits)];
// Build command line arguments
// Base64 encode the JSON to avoid corruption when passing through sandbox
const editsJson = JSON.stringify(edits);
const base64Edits = Buffer.from(editsJson).toString('base64');
const args = [base64Edits];
const result = await runTypescript(sandbox, scriptContent, { argv: args });

View File

@ -10,6 +10,12 @@ describe('grep-search-script integration test', () => {
let sandbox: Sandbox;
let scriptContent: string;
// Helper function to base64 encode commands
const encodeCommands = (commands: Array<{ command: string }>) => {
const commandsJson = JSON.stringify(commands);
return Buffer.from(commandsJson).toString('base64');
};
beforeAll(async () => {
if (!hasApiKey) return;
@ -95,8 +101,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -114,8 +122,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -126,24 +136,31 @@ describe('grep-search-script integration test', () => {
expect(output[0].stdout).toContain('4:HELLO WORLD');
});
it.skipIf(!hasApiKey)('should handle recursive searches', async () => {
const commands = [
{
command: 'rg -n "Hello"',
},
];
it.skipIf(!hasApiKey)(
'should handle recursive searches',
async () => {
const commands = [
{
command: 'rg -n "Hello"',
},
];
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
});
const base64Commands = encodeCommands(commands);
const output = JSON.parse(result.result);
expect(output[0].success).toBe(true);
expect(output[0].stdout).toContain('file1.txt:1:Hello world');
expect(output[0].stdout).toContain('file2.txt:2:Hello again');
expect(output[0].stdout).toContain('subdir/nested1.txt:2:Hello from nested1');
expect(output[0].stdout).toContain('subdir/nested2.txt:2:Hello from nested2');
});
const result = await runTypescript(sandbox, scriptContent, {
argv: [base64Commands],
});
console.log('Recursive search result:', result);
const output = JSON.parse(result.result);
expect(output[0].success).toBe(true);
expect(output[0].stdout).toContain('file1.txt:1:Hello world');
expect(output[0].stdout).toContain('file2.txt:2:Hello again');
expect(output[0].stdout).toContain('subdir/nested1.txt:2:Hello from nested1');
expect(output[0].stdout).toContain('subdir/nested2.txt:2:Hello from nested2');
},
60000
); // Increase timeout to 60 seconds
it.skipIf(!hasApiKey)('should handle whole word matches', async () => {
const commands = [
@ -152,8 +169,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -175,8 +194,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -194,8 +215,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -214,8 +237,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -231,8 +256,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -254,8 +281,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -279,8 +308,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -300,8 +331,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -323,8 +356,12 @@ describe('grep-search-script integration test', () => {
});
it.skipIf(!hasApiKey)('should handle non-array input', async () => {
// Base64 encode non-array JSON
const notArrayJson = JSON.stringify({ not: 'array' });
const base64NotArray = Buffer.from(notArrayJson).toString('base64');
const result = await runTypescript(sandbox, scriptContent, {
argv: ['{"not": "array"}'],
argv: [base64NotArray],
});
const output = JSON.parse(result.result);
@ -339,8 +376,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -355,8 +394,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -376,8 +417,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);
@ -431,8 +474,10 @@ describe('grep-search-script integration test', () => {
},
];
const base64Commands = encodeCommands(commands);
const result = await runTypescript(sandbox, scriptContent, {
argv: [JSON.stringify(commands)],
argv: [base64Commands],
});
const output = JSON.parse(result.result);

View File

@ -1,4 +1,6 @@
import * as child_process from 'node:child_process';
import * as fs from 'node:fs';
import * as path from 'node:path';
interface RgCommand {
command: string;
@ -12,6 +14,16 @@ interface RgResult {
error?: string;
}
// Check if rg is available
function checkRgAvailable(): boolean {
try {
child_process.execSync('which rg', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
async function executeRgCommand(command: string): Promise<RgResult> {
return new Promise((resolve) => {
child_process.exec(
@ -65,14 +77,39 @@ async function main() {
const args = process.argv.slice(2);
// Extract commands from args
// Expected format: JSON array of command objects
// Expected format: JSON array of command objects (possibly base64 encoded)
if (args.length === 0) {
console.log(JSON.stringify([]));
return;
}
// Check if rg is available
if (!checkRgAvailable()) {
console.log(
JSON.stringify([
{
success: false,
command: 'unknown',
error: 'ripgrep (rg) is not installed or not available in PATH',
},
])
);
return;
}
try {
const commands = JSON.parse(args[0] || '[]');
let commandsJson = args[0] || '[]';
// Try to decode from base64 if it looks like base64
if (commandsJson && /^[A-Za-z0-9+/]+=*$/.test(commandsJson) && commandsJson.length % 4 === 0) {
try {
commandsJson = Buffer.from(commandsJson, 'base64').toString('utf-8');
} catch {
// If base64 decode fails, use as-is
}
}
const commands = JSON.parse(commandsJson);
if (!Array.isArray(commands)) {
console.log(

View File

@ -57,8 +57,10 @@ const rgSearchExecution = wrapTraced(
const scriptContent = await fs.readFile(scriptPath, 'utf-8');
// Build command line arguments
// The script expects a JSON array of commands as the first argument
const args = [JSON.stringify(commands)];
// Base64 encode the JSON to avoid corruption when passing through sandbox
const commandsJson = JSON.stringify(commands);
const base64Commands = Buffer.from(commandsJson).toString('base64');
const args = [base64Commands];
const result = await runTypescript(sandbox, scriptContent, { argv: args });