forked from claude-did-this/claude-hub
Fix Docker environment variable passing for long commands
- Remove temp file approach that used invalid @file syntax with Docker - Pass long commands directly as environment variables - Update test to verify long command handling without temp files - Remove unused fsSync import The previous implementation attempted to use Docker's non-existent @file syntax for reading environment variables from files, which caused the COMMAND variable to be empty in the container. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
const { execFileSync } = require('child_process');
|
||||
// Use sync methods for file operations that need to be synchronous
|
||||
const fsSync = require('fs');
|
||||
const path = require('path');
|
||||
// const os = require('os');
|
||||
const { createLogger } = require('../utils/logger');
|
||||
@@ -146,25 +144,8 @@ Please complete this task fully and autonomously.`;
|
||||
ANTHROPIC_API_KEY: secureCredentials.get('ANTHROPIC_API_KEY')
|
||||
};
|
||||
|
||||
// Build docker run command - properly escape values for shell
|
||||
Object.entries(envVars)
|
||||
.filter(([_, value]) => value !== undefined && value !== '')
|
||||
.map(([key, value]) => {
|
||||
// Convert to string and escape shell special characters in the value
|
||||
const stringValue = String(value);
|
||||
// Write complex values to files for safer handling
|
||||
if (key === 'COMMAND' && stringValue.length > 500) {
|
||||
const crypto = require('crypto');
|
||||
const randomSuffix = crypto.randomBytes(16).toString('hex');
|
||||
const tmpFile = `/tmp/claude-command-${Date.now()}-${randomSuffix}.txt`;
|
||||
fsSync.writeFileSync(tmpFile, stringValue, { mode: 0o600 }); // Secure file permissions
|
||||
return `-e ${key}="$(cat ${tmpFile})"`;
|
||||
}
|
||||
// Escape for shell with double quotes (more reliable than single quotes)
|
||||
const escapedValue = stringValue.replace(/["\\$`!]/g, '\\$&');
|
||||
return `-e ${key}="${escapedValue}"`;
|
||||
})
|
||||
.join(' ');
|
||||
// Note: Environment variables will be added as separate arguments to docker command
|
||||
// This is safer than building a shell command string
|
||||
|
||||
// Run the container
|
||||
logger.info(
|
||||
@@ -228,13 +209,12 @@ Please complete this task fully and autonomously.`;
|
||||
Object.entries(envVars)
|
||||
.filter(([_, value]) => value !== undefined && value !== '')
|
||||
.forEach(([key, value]) => {
|
||||
// Write complex values to files for safer handling
|
||||
// For long commands, we need to pass them differently
|
||||
// Docker doesn't support reading env values from files with @ syntax
|
||||
if (key === 'COMMAND' && String(value).length > 500) {
|
||||
const crypto = require('crypto');
|
||||
const randomSuffix = crypto.randomBytes(16).toString('hex');
|
||||
const tmpFile = `/tmp/claude-command-${Date.now()}-${randomSuffix}.txt`;
|
||||
fsSync.writeFileSync(tmpFile, String(value), { mode: 0o600 }); // Secure file permissions
|
||||
dockerArgs.push('-e', `${key}=@${tmpFile}`);
|
||||
// We'll pass the command via stdin or mount it as a volume
|
||||
// For now, let's just pass it directly but properly escaped
|
||||
dockerArgs.push('-e', `${key}=${String(value)}`);
|
||||
} else {
|
||||
dockerArgs.push('-e', `${key}=${String(value)}`);
|
||||
}
|
||||
@@ -272,26 +252,7 @@ Please complete this task fully and autonomously.`;
|
||||
try {
|
||||
logger.info({ dockerArgs: sanitizedArgs }, 'Executing Docker command');
|
||||
|
||||
// Clear any temporary command files after execution
|
||||
const cleanupTempFiles = () => {
|
||||
try {
|
||||
const tempFiles = execFileSync('find', ['/tmp', '-name', 'claude-command-*.txt', '-type', 'f'])
|
||||
.toString()
|
||||
.split('\n');
|
||||
tempFiles
|
||||
.filter(f => f)
|
||||
.forEach(file => {
|
||||
try {
|
||||
fsSync.unlinkSync(file);
|
||||
logger.info(`Removed temp file: ${file}`);
|
||||
} catch {
|
||||
logger.warn(`Failed to remove temp file: ${file}`);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
logger.warn('Failed to clean up temp files');
|
||||
}
|
||||
};
|
||||
// No longer using temp files for commands
|
||||
|
||||
// Get container lifetime from environment variable or use default (2 hours)
|
||||
const containerLifetimeMs = parseInt(process.env.CONTAINER_LIFETIME_MS, 10) || 7200000; // 2 hours in milliseconds
|
||||
@@ -306,8 +267,7 @@ Please complete this task fully and autonomously.`;
|
||||
timeout: containerLifetimeMs // Container lifetime in milliseconds
|
||||
});
|
||||
|
||||
// Clean up temporary files used for command passing
|
||||
cleanupTempFiles();
|
||||
// No cleanup needed anymore
|
||||
|
||||
let responseText = result.stdout.trim();
|
||||
|
||||
@@ -357,23 +317,7 @@ Please complete this task fully and autonomously.`;
|
||||
|
||||
return responseText;
|
||||
} catch (error) {
|
||||
// Clean up temporary files even when there's an error
|
||||
try {
|
||||
const tempFiles = execFileSync('find', ['/tmp', '-name', 'claude-command-*.txt', '-type', 'f'])
|
||||
.toString()
|
||||
.split('\n');
|
||||
tempFiles
|
||||
.filter(f => f)
|
||||
.forEach(file => {
|
||||
try {
|
||||
fsSync.unlinkSync(file);
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
// No cleanup needed - we're not using temp files anymore
|
||||
|
||||
// Sanitize stderr and stdout to remove any potential credentials
|
||||
const sanitizeOutput = output => {
|
||||
|
||||
@@ -181,11 +181,11 @@ describe('Claude Service', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('processCommand should write long commands to temp files', async () => {
|
||||
test('processCommand should handle long commands properly', async () => {
|
||||
// Save original function for restoration
|
||||
const originalProcessCommand = claudeService.processCommand;
|
||||
|
||||
// Create a testing implementation that checks for file writing
|
||||
// Create a testing implementation that checks for long command handling
|
||||
claudeService.processCommand = async (options) => {
|
||||
// Set up test environment
|
||||
const originalNodeEnv = process.env.NODE_ENV;
|
||||
@@ -197,40 +197,46 @@ describe('Claude Service', () => {
|
||||
return 'mocked output';
|
||||
});
|
||||
|
||||
// Capture file write calls
|
||||
writeFileSync.mockImplementation((_path, _content, _options) => {
|
||||
// File write is mocked
|
||||
// Configure execFileAsync mock
|
||||
const execFileAsync = promisify(require('child_process').execFile);
|
||||
execFileAsync.mockResolvedValue({
|
||||
stdout: 'Claude response for long command',
|
||||
stderr: ''
|
||||
});
|
||||
|
||||
// Call the original implementation
|
||||
try {
|
||||
await originalProcessCommand(options);
|
||||
} catch (_e) {
|
||||
// Ignore errors, we just want to check if writeFileSync was called
|
||||
}
|
||||
const result = await originalProcessCommand(options);
|
||||
|
||||
// Verify temp file creation occurred
|
||||
expect(writeFileSync).toHaveBeenCalled();
|
||||
// Verify Docker was called with the command as an environment variable
|
||||
expect(execFileAsync).toHaveBeenCalled();
|
||||
const dockerArgs = execFileAsync.mock.calls[0][1];
|
||||
|
||||
// Check that COMMAND env var is present in the docker args
|
||||
// The format is ['-e', 'COMMAND=value']
|
||||
const commandEnvIndex = dockerArgs.findIndex((arg) =>
|
||||
typeof arg === 'string' && arg.startsWith('COMMAND=')
|
||||
);
|
||||
expect(commandEnvIndex).toBeGreaterThan(-1);
|
||||
|
||||
// Restore environment
|
||||
process.env.NODE_ENV = originalNodeEnv;
|
||||
|
||||
return 'test response';
|
||||
return result;
|
||||
};
|
||||
|
||||
try {
|
||||
// Call the function with a long command
|
||||
const longCommand = 'A'.repeat(1000);
|
||||
|
||||
await claudeService.processCommand({
|
||||
const result = await claudeService.processCommand({
|
||||
repoFullName: 'test/repo',
|
||||
issueNumber: 123,
|
||||
command: longCommand,
|
||||
isPullRequest: false
|
||||
});
|
||||
|
||||
// Verification happens in the override function
|
||||
expect(writeFileSync).toHaveBeenCalled();
|
||||
// Verify we got a response
|
||||
expect(result).toBe('Claude response for long command');
|
||||
} finally {
|
||||
// Restore original function
|
||||
claudeService.processCommand = originalProcessCommand;
|
||||
|
||||
Reference in New Issue
Block a user