Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 23 additions & 6 deletions src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,12 +477,29 @@ describe('docker-manager', () => {
const squid = result.services['squid-proxy'];

expect(squid.container_name).toBe('awf-squid');
expect(squid.volumes).toContain('/tmp/awf-test/squid.conf:/etc/squid/squid.conf:ro');
// squid.conf is NOT bind-mounted; it's injected via AWF_SQUID_CONFIG_B64 env var
expect(squid.volumes).not.toContainEqual(expect.stringContaining('squid.conf'));
expect(squid.volumes).toContain('/tmp/awf-test/squid-logs:/var/log/squid:rw');
expect(squid.healthcheck).toBeDefined();
expect(squid.ports).toContain('3128:3128');
});

it('should inject squid config via base64 env var when content is provided', () => {
const squidConfig = 'http_port 3128\nacl allowed_domains dstdomain .github.com\n';
const result = generateDockerCompose(mockConfig, mockNetworkConfig, undefined, squidConfig);
const squid = result.services['squid-proxy'] as any;

// Should have AWF_SQUID_CONFIG_B64 env var with base64-encoded config
expect(squid.environment.AWF_SQUID_CONFIG_B64).toBe(
Buffer.from(squidConfig).toString('base64')
);

// Should override entrypoint to decode config before starting squid
expect(squid.entrypoint).toBeDefined();
expect(squid.entrypoint[2]).toContain('base64 -d > /etc/squid/squid.conf');
expect(squid.entrypoint[2]).toContain('entrypoint.sh');
});

it('should configure agent container with proxy settings', () => {
const result = generateDockerCompose(mockConfig, mockNetworkConfig);
const agent = result.services.agent;
Expand Down Expand Up @@ -2267,7 +2284,7 @@ describe('docker-manager', () => {
expect(mockExecaFn).toHaveBeenCalledWith(
'docker',
['compose', 'up', '-d'],
{ cwd: testDir, stdio: 'inherit' }
{ cwd: testDir, stdout: process.stderr, stderr: 'inherit' }
);
});

Expand All @@ -2280,7 +2297,7 @@ describe('docker-manager', () => {
expect(mockExecaFn).toHaveBeenCalledWith(
'docker',
['compose', 'up', '-d'],
{ cwd: testDir, stdio: 'inherit' }
{ cwd: testDir, stdout: process.stderr, stderr: 'inherit' }
);
});

Expand All @@ -2293,7 +2310,7 @@ describe('docker-manager', () => {
expect(mockExecaFn).toHaveBeenCalledWith(
'docker',
['compose', 'up', '-d', '--pull', 'never'],
{ cwd: testDir, stdio: 'inherit' }
{ cwd: testDir, stdout: process.stderr, stderr: 'inherit' }
);
});

Expand All @@ -2306,7 +2323,7 @@ describe('docker-manager', () => {
expect(mockExecaFn).toHaveBeenCalledWith(
'docker',
['compose', 'up', '-d'],
{ cwd: testDir, stdio: 'inherit' }
{ cwd: testDir, stdout: process.stderr, stderr: 'inherit' }
);
});

Expand Down Expand Up @@ -2361,7 +2378,7 @@ describe('docker-manager', () => {
expect(mockExecaFn).toHaveBeenCalledWith(
'docker',
['compose', 'down', '-v'],
{ cwd: testDir, stdio: 'inherit' }
{ cwd: testDir, stdout: process.stderr, stderr: 'inherit' }
);
});

Expand Down
50 changes: 44 additions & 6 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,8 @@ export interface SslConfig {
export function generateDockerCompose(
config: WrapperConfig,
networkConfig: { subnet: string; squidIp: string; agentIp: string; proxyIp?: string },
sslConfig?: SslConfig
sslConfig?: SslConfig,
squidConfigContent?: string
): DockerComposeConfig {
const projectRoot = path.join(__dirname, '..');

Expand All @@ -249,8 +250,12 @@ export function generateDockerCompose(
: path.join(config.workDir, 'api-proxy-logs');

// Build Squid volumes list
// Note: squid.conf is NOT bind-mounted. Instead, it's passed as a base64-encoded
// environment variable (AWF_SQUID_CONFIG_B64) and decoded by the entrypoint override.
// This supports Docker-in-Docker (DinD) environments where the Docker daemon runs
// in a separate container and cannot access files on the host filesystem.
// See: https://github.com/github/gh-aw/issues/18385
const squidVolumes = [
`${config.workDir}/squid.conf:/etc/squid/squid.conf:ro`,
`${squidLogsPath}:/var/log/squid:rw`,
];

Expand Down Expand Up @@ -292,6 +297,29 @@ export function generateDockerCompose(
],
};

// Inject squid.conf via environment variable instead of bind mount.
// In Docker-in-Docker (DinD) environments, the Docker daemon runs in a separate
// container and cannot access files on the host filesystem. Bind-mounting
// squid.conf fails because the daemon creates a directory at the missing path.
// Passing the config as a base64-encoded env var works universally because
// env vars are part of the container spec sent via the Docker API.
if (squidConfigContent) {
const configB64 = Buffer.from(squidConfigContent).toString('base64');
squidService.environment = {
...squidService.environment,
AWF_SQUID_CONFIG_B64: configB64,
};
// Override entrypoint to decode the config before starting squid.
// The original entrypoint (/usr/local/bin/entrypoint.sh) is called after decoding.
// Use $$ to escape $ for Docker Compose variable interpolation.
// Docker Compose interprets $VAR as variable substitution in YAML values;
// $$ produces a literal $ that the shell inside the container will expand.
squidService.entrypoint = [
'/bin/bash', '-c',
'echo "$$AWF_SQUID_CONFIG_B64" | base64 -d > /etc/squid/squid.conf && exec /usr/local/bin/entrypoint.sh',
];
}

// Only enable host.docker.internal when explicitly requested via --enable-host-access
// This allows containers to reach services on the host machine (e.g., MCP gateways)
// Security note: When combined with allowing host.docker.internal domain,
Expand Down Expand Up @@ -1294,9 +1322,11 @@ export async function writeConfigs(config: WrapperConfig): Promise<void> {
// Write Docker Compose config
// Uses mode 0o600 (owner-only read/write) because this file contains sensitive
// environment variables (tokens, API keys) in plaintext
const dockerCompose = generateDockerCompose(config, networkConfig, sslConfig);
const dockerCompose = generateDockerCompose(config, networkConfig, sslConfig, squidConfig);
const dockerComposePath = path.join(config.workDir, 'docker-compose.yml');
fs.writeFileSync(dockerComposePath, yaml.dump(dockerCompose), { mode: 0o600 });
// lineWidth: -1 disables line wrapping to prevent base64-encoded values
// (like AWF_SQUID_CONFIG_B64) from being split across multiple lines
fs.writeFileSync(dockerComposePath, yaml.dump(dockerCompose, { lineWidth: -1 }), { mode: 0o600 });
logger.debug(`Docker Compose config written to: ${dockerComposePath}`);
}

Expand Down Expand Up @@ -1395,9 +1425,16 @@ export async function startContainers(workDir: string, allowedDomains: string[],
composeArgs.push('--pull', 'never');
logger.debug('Using --pull never (skip-pull mode)');
}
// Redirect Docker Compose stdout to stderr so it doesn't pollute the
// agent command's stdout. Docker Compose outputs build progress and
// container creation status to stdout, which would be captured by test
// runners and break assertions that check for agent command output.
// All AWF informational output goes to stderr (via logger), so this
// keeps the output consistent. Users still see progress in their terminal.
await execa('docker', composeArgs, {
cwd: workDir,
stdio: 'inherit',
stdout: process.stderr,
stderr: 'inherit',
});
logger.success('Containers started successfully');
} catch (error) {
Expand Down Expand Up @@ -1551,7 +1588,8 @@ export async function stopContainers(workDir: string, keepContainers: boolean):
try {
await execa('docker', ['compose', 'down', '-v'], {
cwd: workDir,
stdio: 'inherit',
stdout: process.stderr,
stderr: 'inherit',
});
logger.success('Containers stopped successfully');
} catch (error) {
Expand Down
Loading