Skip to content

Commit 1fe9e76

Browse files
Mossakaclaude
andauthored
feat(cli): add --memory-limit flag for configurable container memory (#1243)
Reduce default agent container memory from 4GB to 2GB for better DoS protection in shared environments. Add --memory-limit flag to override (e.g., --memory-limit 8g for AI agent workloads). Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cd53588 commit 1fe9e76

File tree

5 files changed

+78
-5
lines changed

5 files changed

+78
-5
lines changed

src/cli.test.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Command } from 'commander';
2-
import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags } from './cli';
2+
import { parseEnvironmentVariables, parseDomains, parseDomainsFile, escapeShellArg, joinShellArgs, parseVolumeMounts, isValidIPv4, isValidIPv6, parseDnsServers, validateAgentImage, isAgentImagePreset, AGENT_IMAGE_PRESETS, processAgentImageOption, processLocalhostKeyword, validateSkipPullWithBuildLocal, validateAllowHostPorts, parseMemoryLimit, validateFormat, validateApiProxyConfig, buildRateLimitConfig, validateRateLimitFlags } from './cli';
33
import { redactSecrets } from './redact-secrets';
44
import * as fs from 'fs';
55
import * as path from 'path';
@@ -1539,4 +1539,26 @@ describe('cli', () => {
15391539
expect(result.error).toBeUndefined();
15401540
});
15411541
});
1542+
1543+
describe('parseMemoryLimit', () => {
1544+
it('accepts valid memory limits', () => {
1545+
expect(parseMemoryLimit('2g')).toEqual({ value: '2g' });
1546+
expect(parseMemoryLimit('4g')).toEqual({ value: '4g' });
1547+
expect(parseMemoryLimit('512m')).toEqual({ value: '512m' });
1548+
expect(parseMemoryLimit('1024k')).toEqual({ value: '1024k' });
1549+
expect(parseMemoryLimit('8G')).toEqual({ value: '8g' });
1550+
});
1551+
1552+
it('rejects invalid formats', () => {
1553+
expect(parseMemoryLimit('abc')).toHaveProperty('error');
1554+
expect(parseMemoryLimit('-1g')).toHaveProperty('error');
1555+
expect(parseMemoryLimit('2x')).toHaveProperty('error');
1556+
expect(parseMemoryLimit('')).toHaveProperty('error');
1557+
expect(parseMemoryLimit('g')).toHaveProperty('error');
1558+
});
1559+
1560+
it('rejects zero', () => {
1561+
expect(parseMemoryLimit('0g')).toHaveProperty('error');
1562+
});
1563+
});
15421564
});

src/cli.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,23 @@ export function validateAllowHostPorts(
422422
return { valid: true };
423423
}
424424

425+
/**
426+
* Parses and validates a Docker memory limit string.
427+
* Valid formats: positive integer followed by b, k, m, or g (e.g., "2g", "512m", "4g").
428+
*/
429+
export function parseMemoryLimit(input: string): { value: string; error?: undefined } | { value?: undefined; error: string } {
430+
const pattern = /^(\d+)([bkmg])$/i;
431+
const match = input.match(pattern);
432+
if (!match) {
433+
return { error: `Invalid --memory-limit value "${input}". Expected format: <number><unit> (e.g., 2g, 512m, 4g)` };
434+
}
435+
const num = parseInt(match[1], 10);
436+
if (num <= 0) {
437+
return { error: `Invalid --memory-limit value "${input}". Memory limit must be a positive number.` };
438+
}
439+
return { value: input.toLowerCase() };
440+
}
441+
425442
/**
426443
* Parses and validates DNS servers from a comma-separated string
427444
* @param input - Comma-separated DNS server string (e.g., "8.8.8.8,1.1.1.1")
@@ -780,6 +797,11 @@ program
780797
'--container-workdir <dir>',
781798
'Working directory inside the container (should match GITHUB_WORKSPACE for path consistency)'
782799
)
800+
.option(
801+
'--memory-limit <limit>',
802+
'Memory limit for the agent container (e.g., 1g, 2g, 4g, 512m). Default: 2g',
803+
'2g'
804+
)
783805
.option(
784806
'--dns-servers <servers>',
785807
'Comma-separated list of trusted DNS servers. DNS traffic is ONLY allowed to these servers (default: 8.8.8.8,8.8.4.4)',
@@ -1067,6 +1089,13 @@ program
10671089
logger.warn('⚠️ SSL Bump intercepts HTTPS traffic. Only use for trusted workloads.');
10681090
}
10691091

1092+
// Validate memory limit
1093+
const memoryLimit = parseMemoryLimit(options.memoryLimit);
1094+
if (memoryLimit.error) {
1095+
logger.error(memoryLimit.error);
1096+
process.exit(1);
1097+
}
1098+
10701099
// Validate agent image option
10711100
const agentImageResult = processAgentImageOption(options.agentImage, options.buildLocal);
10721101
if (agentImageResult.error) {
@@ -1096,6 +1125,7 @@ program
10961125
volumeMounts,
10971126
containerWorkDir: options.containerWorkdir,
10981127
dnsServers,
1128+
memoryLimit: memoryLimit.value,
10991129
proxyLogsDir: options.proxyLogsDir,
11001130
enableHostAccess: options.enableHostAccess,
11011131
allowHostPorts: options.allowHostPorts,

src/docker-manager.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,12 +1022,21 @@ describe('docker-manager', () => {
10221022
expect(agent.security_opt).toContain('no-new-privileges:true');
10231023

10241024
// Verify resource limits
1025-
expect(agent.mem_limit).toBe('4g');
1026-
expect(agent.memswap_limit).toBe('4g');
1025+
expect(agent.mem_limit).toBe('2g');
1026+
expect(agent.memswap_limit).toBe('2g');
10271027
expect(agent.pids_limit).toBe(1000);
10281028
expect(agent.cpu_shares).toBe(1024);
10291029
});
10301030

1031+
it('should use custom memory limit when specified', () => {
1032+
const customConfig = { ...mockConfig, memoryLimit: '8g' };
1033+
const result = generateDockerCompose(customConfig, mockNetworkConfig);
1034+
const agent = result.services.agent;
1035+
1036+
expect(agent.mem_limit).toBe('8g');
1037+
expect(agent.memswap_limit).toBe('8g');
1038+
});
1039+
10311040
it('should disable TTY by default to prevent ANSI escape sequences', () => {
10321041
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
10331042
const agent = result.services.agent;

src/docker-manager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -912,8 +912,8 @@ export function generateDockerCompose(
912912
'apparmor:unconfined',
913913
],
914914
// Resource limits to prevent DoS attacks (conservative defaults)
915-
mem_limit: '4g', // 4GB memory limit
916-
memswap_limit: '4g', // No swap (same as mem_limit)
915+
mem_limit: config.memoryLimit || '2g',
916+
memswap_limit: config.memoryLimit || '2g', // No swap (same as mem_limit)
917917
pids_limit: 1000, // Max 1000 processes
918918
cpu_shares: 1024, // Default CPU share
919919
stdin_open: true,

src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,18 @@ export interface WrapperConfig {
292292
*/
293293
dnsServers?: string[];
294294

295+
/**
296+
* Memory limit for the agent execution container
297+
*
298+
* Accepts Docker memory format: a positive integer followed by a unit suffix
299+
* (b, k, m, g). Controls the maximum amount of memory the container can use.
300+
*
301+
* @default '2g'
302+
* @example '4g'
303+
* @example '512m'
304+
*/
305+
memoryLimit?: string;
306+
295307
/**
296308
* Custom directory for Squid proxy logs (written directly during runtime)
297309
*

0 commit comments

Comments
 (0)