diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index 067cf4b7d..a2dce6b1d 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -655,6 +655,44 @@ describe('docker-manager', () => { expect(volumes).not.toContain(`${homeDir}:/host${homeDir}:rw`); }); + it('should not mount .cargo when enableChroot is true and allowFullFilesystemAccess is false', () => { + const configWithChroot = { + ...mockConfig, + enableChroot: true, + allowFullFilesystemAccess: false + }; + const result = generateDockerCompose(configWithChroot, mockNetworkConfig); + const agent = result.services.agent; + const volumes = agent.volumes as string[]; + const tmpfs = agent.tmpfs as string[]; + + // Should NOT mount .cargo as volume (it's hidden via tmpfs) + const homeDir = process.env.HOME || '/root'; + const cargoVolumePattern = new RegExp(`${homeDir.replace(/\//g, '\\/')}.*\\.cargo.*:/host.*\\.cargo`); + expect(volumes.some((v: string) => cargoVolumePattern.test(v))).toBe(false); + + // Should have .cargo hidden via tmpfs + expect(tmpfs.some((t: string) => t.includes('.cargo'))).toBe(true); + }); + + it('should mount .cargo when enableChroot is true and allowFullFilesystemAccess is true', () => { + const configWithChroot = { + ...mockConfig, + enableChroot: true, + allowFullFilesystemAccess: true + }; + const result = generateDockerCompose(configWithChroot, mockNetworkConfig); + const agent = result.services.agent; + const volumes = agent.volumes as string[]; + const tmpfs = agent.tmpfs as string[]; + + // With allowFullFilesystemAccess, should have blanket mount + expect(volumes).toContain('/:/host:rw'); + + // Should NOT have credential hiding tmpfs (only MCP logs tmpfs) + expect(tmpfs.some((t: string) => t.includes('.cargo'))).toBe(false); + }); + it('should add SYS_CHROOT and SYS_ADMIN capabilities when enableChroot is true', () => { const configWithChroot = { ...mockConfig, diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 945176e3e..0cdc409f3 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -502,8 +502,9 @@ export function generateDockerCompose( } // Mount ~/.cargo for Rust binaries (read-only) if it exists + // SKIP if allowFullFilesystemAccess is false (credentials will be hidden via tmpfs) const hostCargoDir = path.join(userHome, '.cargo'); - if (fs.existsSync(hostCargoDir)) { + if (fs.existsSync(hostCargoDir) && config.allowFullFilesystemAccess) { agentVolumes.push(`${hostCargoDir}:/host${hostCargoDir}:ro`); } @@ -1300,11 +1301,36 @@ export async function stopContainers(workDir: string, keepContainers: boolean): logger.info('Stopping containers...'); try { - await execa('docker', ['compose', 'down', '-v'], { - cwd: workDir, - stdio: 'inherit', - }); - logger.success('Containers stopped successfully'); + // Check if workDir and docker-compose.yml exist before using docker compose + const composeFile = path.join(workDir, 'docker-compose.yml'); + if (fs.existsSync(workDir) && fs.existsSync(composeFile)) { + // Normal path: use docker compose down + await execa('docker', ['compose', 'down', '-v'], { + cwd: workDir, + stdio: 'inherit', + }); + logger.success('Containers stopped successfully'); + } else { + // Fallback: compose file missing, stop containers by name + logger.debug('Compose file not found, stopping containers by name'); + + // Stop and remove containers by name + const containerNames = ['awf-agent', 'awf-squid']; + for (const name of containerNames) { + try { + // Check if container exists + const { stdout } = await execa('docker', ['ps', '-aq', '-f', `name=^${name}$`]); + if (stdout.trim()) { + logger.debug(`Stopping container: ${name}`); + await execa('docker', ['rm', '-f', name], { stdio: 'inherit' }); + } + } catch (err) { + logger.debug(`Could not stop container ${name}:`, err); + } + } + + logger.success('Containers stopped successfully'); + } } catch (error) { logger.error('Failed to stop containers:', error); throw error;