diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index e3d74ec7..257b3706 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1645,6 +1645,42 @@ describe('docker-manager', () => { } }); + it('should skip env vars exceeding MAX_ENV_VALUE_SIZE from env-all passthrough', () => { + const largeVarName = 'AWF_TEST_OVERSIZED_VAR'; + const saved = process.env[largeVarName]; + // Create a value larger than 64KB + process.env[largeVarName] = 'x'.repeat(65 * 1024); + + try { + const configWithEnvAll = { ...mockConfig, envAll: true }; + const result = generateDockerCompose(configWithEnvAll, mockNetworkConfig); + const env = result.services.agent.environment as Record; + + // Oversized var should be skipped + expect(env[largeVarName]).toBeUndefined(); + } finally { + if (saved !== undefined) process.env[largeVarName] = saved; + else delete process.env[largeVarName]; + } + }); + + it('should pass env vars under MAX_ENV_VALUE_SIZE from env-all passthrough', () => { + const normalVarName = 'AWF_TEST_NORMAL_VAR'; + const saved = process.env[normalVarName]; + process.env[normalVarName] = 'normal_value'; + + try { + const configWithEnvAll = { ...mockConfig, envAll: true }; + const result = generateDockerCompose(configWithEnvAll, mockNetworkConfig); + const env = result.services.agent.environment as Record; + + expect(env[normalVarName]).toBe('normal_value'); + } finally { + if (saved !== undefined) process.env[normalVarName] = saved; + else delete process.env[normalVarName]; + } + }); + it('should auto-inject GH_HOST from GITHUB_SERVER_URL when envAll is true', () => { const prevServerUrl = process.env.GITHUB_SERVER_URL; const prevGhHost = process.env.GH_HOST; diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 802abd61..b6582e71 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -24,6 +24,19 @@ const API_PROXY_CONTAINER_NAME = 'awf-api-proxy'; const DOH_PROXY_CONTAINER_NAME = 'awf-doh-proxy'; const CLI_PROXY_CONTAINER_NAME = 'awf-cli-proxy'; +/** + * Maximum size (bytes) of a single environment variable value allowed through + * --env-all passthrough. Variables exceeding this are skipped with a warning + * to prevent E2BIG errors from ARG_MAX exhaustion. + */ +const MAX_ENV_VALUE_SIZE = 64 * 1024; // 64 KB + +/** + * Total environment size (bytes) threshold for issuing an ARG_MAX warning. + * Linux ARG_MAX is ~2 MB for argv + envp combined; warn well before that. + */ +const ENV_SIZE_WARNING_THRESHOLD = 1_500_000; // ~1.5 MB + /** * Flag set by fastKillAgentContainer() to signal runAgentCommand() that * the container was externally stopped. When true, runAgentCommand() skips @@ -794,11 +807,27 @@ export function generateDockerCompose( // If --env-all is specified, pass through all host environment variables (except excluded ones) if (config.envAll) { + const skippedLargeVars: string[] = []; for (const [key, value] of Object.entries(process.env)) { if (value !== undefined && !EXCLUDED_ENV_VARS.has(key) && !Object.prototype.hasOwnProperty.call(environment, key)) { + // Skip oversized values to prevent E2BIG (Argument list too long) errors. + // The Linux kernel enforces ARG_MAX (~2MB) on argv+envp combined; large env + // vars can exhaust this budget, especially when combined with large prompts. + const valueSizeBytes = Buffer.byteLength(value, 'utf8'); + if (valueSizeBytes > MAX_ENV_VALUE_SIZE) { + skippedLargeVars.push(`${key} (${(valueSizeBytes / 1024).toFixed(0)} KB)`); + continue; + } environment[key] = value; } } + if (skippedLargeVars.length > 0) { + logger.warn(`Skipped ${skippedLargeVars.length} oversized env var(s) from --env-all passthrough (>${(MAX_ENV_VALUE_SIZE / 1024).toFixed(0)} KB each):`); + for (const entry of skippedLargeVars) { + logger.warn(` - ${entry}`); + } + logger.warn('Use --env VAR="$VAR" to explicitly pass large values if needed.'); + } } else { // Default behavior: selectively pass through specific variables if (process.env.GITHUB_TOKEN) environment.GITHUB_TOKEN = process.env.GITHUB_TOKEN; @@ -903,6 +932,21 @@ export function generateDockerCompose( } } + // Warn when total environment size approaches ARG_MAX (~2MB). + // Linux enforces a combined argv+envp limit; large environments can cause E2BIG errors + // when execve() is called inside the container. + if (config.envAll) { + const totalEnvBytes = Object.entries(environment) + .reduce((sum, [k, v]) => sum + k.length + (v?.length ?? 0) + 2, 0); // +2 for '=' and null + if (totalEnvBytes > ENV_SIZE_WARNING_THRESHOLD) { + logger.warn( + `⚠️ Total container environment size is ${(totalEnvBytes / 1024).toFixed(0)} KB — ` + + 'may cause E2BIG (Argument list too long) errors when combined with large command arguments' + ); + logger.warn(' Consider using --exclude-env to remove unnecessary variables'); + } + } + // DNS servers for Docker embedded DNS forwarding (used in docker-compose dns: field) const dnsServers = config.dnsServers || DEFAULT_DNS_SERVERS; // Pass DNS servers to container so setup-iptables.sh can allow Docker DNS forwarding