Skip to content

Conversation

@lpcox
Copy link
Collaborator

@lpcox lpcox commented Feb 10, 2026

No description provided.

SECURITY: Replace broad $HOME:$HOME mount with specific narrow mounts:
- Mount only ~/.copilot (ro) for MCP config instead of entire home
- Mount only the workspace directory (containerWorkDir) for project files
- Mount ~/.cargo (ro) and ~/.local/bin (ro) only if they exist (chroot mode)

This prevents the agent from accessing sensitive paths like:
- ~/actions-runner (GitHub Actions runner config and credentials)
- ~/work (other repositories being checked out)
- Other sensitive dotfiles and directories

The agent still has access to:
- ~/.copilot/mcp-config.json (read-only) for MCP server configuration
- ~/.copilot/logs (read-write via overlay) for debug logging
- The specific workspace directory being worked on (read-write)
- /tmp for temporary files
SECURITY: The docker-compose.yml file contains sensitive environment
variables (GITHUB_TOKEN, etc.) and was accessible via the /tmp volume
mount. Now deleted immediately after docker compose up succeeds, since
Docker only needs the file during startup.

- Added fs.unlinkSync call after successful container startup
- Added test verifying compose file is deleted
- Logs debug message on successful deletion
@lpcox lpcox requested review from Mossaka and Copilot February 10, 2026 18:15
@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

Chroot tests failed Smoke Chroot failed - See logs for details.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

💫 TO BE CONTINUED... Smoke Claude failed! Our hero faces unexpected challenges...

@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

📰 DEVELOPING STORY: Smoke Copilot reports failed. Our correspondents are investigating the incident...

@github-actions
Copy link
Contributor

github-actions bot commented Feb 10, 2026

🌑 The shadows whisper... Smoke Codex failed. The oracle requires further meditation...

@github-actions
Copy link
Contributor

✅ Coverage Check Passed

Overall Coverage

Metric Base PR Delta
Lines 82.02% 82.16% 📈 +0.14%
Statements 82.06% 82.19% 📈 +0.13%
Functions 81.95% 81.95% ➡️ +0.00%
Branches 75.51% 75.74% 📈 +0.23%
📁 Per-file Coverage Changes (1 files)
File Lines (Before → After) Statements (Before → After)
src/docker-manager.ts 82.6% → 83.1% (+0.54%) 81.9% → 82.5% (+0.56%)

Coverage comparison generated by scripts/ci/compare-coverage.ts

@lpcox
Copy link
Collaborator Author

lpcox commented Feb 10, 2026

@github-actions github-actions bot mentioned this pull request Feb 10, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to harden the Docker execution environment by reducing host filesystem exposure (notably the runner’s home directory) and by removing sensitive artifacts after container startup.

Changes:

  • Replaces full $HOME bind-mounts with selective mounts (e.g., ~/.copilot, agent logs, optional workspace) in both normal and chroot modes.
  • Adds post-start deletion of docker-compose.yml to reduce the lifetime of on-disk secrets.
  • Updates unit tests to reflect the new mount behavior and to assert compose-file deletion.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
src/docker-manager.ts Implements selective home/workspace mounts and deletes docker-compose.yml after startup.
src/docker-manager.test.ts Updates chroot mount expectations and adds a test for compose-file deletion.
Comments suppressed due to low confidence (1)

src/docker-manager.ts:520

  • Same issue as the non-chroot case: mounting ${config.containerWorkDir} under /host assumes containerWorkDir is a host path. In chroot mode this is especially likely to break when containerWorkDir is something like /workspace/... (container path) rather than /home/runner/... (host path). Consider separating “container working dir” from “host workspace path to mount into /host”.
    // Mount the workspace directory under /host if specified
    if (config.containerWorkDir && config.containerWorkDir !== '/workspace') {
      agentVolumes.push(`${config.containerWorkDir}:/host${config.containerWorkDir}:rw`);
      logger.debug(`Mounting workspace directory under /host: ${config.containerWorkDir}`);
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +432 to 438
// SECURITY: Mount only specific paths needed, NOT the entire home directory
// This prevents access to sensitive paths like ~/actions-runner, ~/work (other repos), etc.
const effectiveHome = config.enableChroot ? getRealUserHome() : (process.env.HOME || '/root');
const agentVolumes: string[] = [
// Essential mounts that are always included
'/tmp:/tmp:rw',
`${effectiveHome}:${effectiveHome}:rw`,
// Mount agent logs directory to workDir for persistence
`${config.workDir}/agent-logs:${effectiveHome}/.copilot/logs:rw`,
];
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new comment says we mount only specific paths and not the entire home directory, but in the non-chroot default path the code still prepends the blanket /:/host:rw mount (later in this function), which exposes the full home directory under /host/.... Either tighten the default mounts to actually enforce this security property, or adjust the comment to avoid overstating the protection provided in the default (non-chroot, no custom mounts) mode.

See below for a potential fix:

  // SECURITY: Restrict direct bind mounts from the home directory to specific paths
  // NOTE: In non-chroot mode, a broader host filesystem mount (e.g. /:/host:rw) may still exist
  const effectiveHome = config.enableChroot ? getRealUserHome() : (process.env.HOME || '/root');
  const agentVolumes: string[] = [
    // Essential mounts that are always included
    '/tmp:/tmp:rw',
  ];

  // Directly mount only specific subdirectories of the home directory, not the entire home path
  // This reduces direct exposure of paths like ~/actions-runner or other repositories via bind mounts

Copilot uses AI. Check for mistakes.

// Ensure .copilot directory exists on host before mounting
if (!fs.existsSync(copilotConfigDir)) {
fs.mkdirSync(copilotConfigDir, { recursive: true });
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateDockerCompose() now creates ~/.copilot on the host unconditionally. This introduces a side effect and can throw (e.g., if $HOME resolves to an unwritable path), which would prevent config generation. Consider avoiding host filesystem mutation here (let the caller ensure the directory exists), or wrap mkdirSync in a try/catch and emit a clear error with the path that failed.

Suggested change
fs.mkdirSync(copilotConfigDir, { recursive: true });
try {
fs.mkdirSync(copilotConfigDir, { recursive: true });
} catch (err) {
const message = `Failed to create Copilot config directory on host: ${copilotConfigDir}. ${err instanceof Error ? err.message : String(err)}`;
logger.error(message);
throw new Error(message);
}

Copilot uses AI. Check for mistakes.
Comment on lines +456 to +458
// Only mount if it's a real path (not the default /workspace)
agentVolumes.push(`${config.containerWorkDir}:${config.containerWorkDir}:rw`);
logger.debug(`Mounting workspace directory: ${config.containerWorkDir}`);
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

containerWorkDir is defined/used as a container working directory, but this change also treats it as a host bind-mount source (${config.containerWorkDir}:${config.containerWorkDir}). If containerWorkDir is set to a container-only path (e.g. /workspace/project, which is already covered by existing tests), docker compose up will fail because the host path likely doesn't exist. Consider introducing a separate hostWorkDir/workspaceMount option, or only adding this bind mount when the host path exists (and otherwise rely on volumeMounts).

This issue also appears on line 516 of the same file.

Suggested change
// Only mount if it's a real path (not the default /workspace)
agentVolumes.push(`${config.containerWorkDir}:${config.containerWorkDir}:rw`);
logger.debug(`Mounting workspace directory: ${config.containerWorkDir}`);
// Only mount if it's a real host path (not the default /workspace or a container-only path)
if (fs.existsSync(config.containerWorkDir)) {
agentVolumes.push(`${config.containerWorkDir}:${config.containerWorkDir}:rw`);
logger.debug(`Mounting workspace directory: ${config.containerWorkDir}`);
} else {
logger.debug(
`Not mounting workspace directory '${config.containerWorkDir}' because it does not exist on the host; assuming container-only path.`,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +935 to +948

// SECURITY: Immediately delete docker-compose.yml after containers start
// This file contains sensitive environment variables (tokens, secrets) that
// would otherwise be readable by the agent via the /tmp mount until cleanup.
// Docker Compose only needs the file at startup, not during execution.
const composeFile = path.join(workDir, 'docker-compose.yml');
try {
if (fs.existsSync(composeFile)) {
fs.unlinkSync(composeFile);
logger.debug('Deleted docker-compose.yml (contained sensitive environment variables)');
}
} catch (err) {
logger.debug('Could not delete docker-compose.yml:', err);
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleting docker-compose.yml immediately after docker compose up will break later cleanup: stopContainers() currently runs docker compose down -v with cwd: workDir, which requires the compose file to still exist. It also conflicts with --keep-containers, which currently promises to preserve config files. If you want to eliminate secrets from disk, consider switching cleanup to container-name based teardown (docker rm -f awf-squid awf-agent, plus any needed network/volume cleanup), or keep a compose file outside the /tmp bind mount and pass it via docker compose -f <path> for both up/down.

Suggested change
// SECURITY: Immediately delete docker-compose.yml after containers start
// This file contains sensitive environment variables (tokens, secrets) that
// would otherwise be readable by the agent via the /tmp mount until cleanup.
// Docker Compose only needs the file at startup, not during execution.
const composeFile = path.join(workDir, 'docker-compose.yml');
try {
if (fs.existsSync(composeFile)) {
fs.unlinkSync(composeFile);
logger.debug('Deleted docker-compose.yml (contained sensitive environment variables)');
}
} catch (err) {
logger.debug('Could not delete docker-compose.yml:', err);
}

Copilot uses AI. Check for mistakes.
Comment on lines +1532 to +1544
it('should delete docker-compose.yml after containers start (security: removes sensitive env vars)', async () => {
// Create a docker-compose.yml file with sensitive data
const composeFile = path.join(testDir, 'docker-compose.yml');
fs.writeFileSync(composeFile, 'environment:\n GITHUB_TOKEN: secret123\n');

mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);
mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

await startContainers(testDir, ['github.com']);

// docker-compose.yml should be deleted after containers start
expect(fs.existsSync(composeFile)).toBe(false);
});
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new test verifies deletion of docker-compose.yml after startup, but there is no corresponding coverage ensuring teardown still works. Given stopContainers() uses docker compose down (which requires the compose file), please add a test that exercises the full start→stop flow (or update the stop logic) so we don’t regress cleanup behavior.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant