-
Notifications
You must be signed in to change notification settings - Fork 6
fix: create .copilot/logs mountpoint before docker mount #722
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: create .copilot/logs mountpoint before docker mount #722
Conversation
The agent container mount at ~/.copilot:ro followed by ~/agent-logs:~/.copilot/logs:rw fails in GitHub Actions because Docker cannot create the logs subdirectory inside a read-only mount. Fix by creating ~/.copilot/logs on the host before mounting, so Docker doesn't need to create the mountpoint inside the read-only parent. This affects GitHub Actions runners where ~/.copilot doesn't exist before AWF runs. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
|
💫 TO BE CONTINUED... Smoke Claude failed! Our hero faces unexpected challenges... |
|
Chroot tests failed Smoke Chroot failed - See logs for details. |
|
📰 DEVELOPING STORY: Smoke Copilot reports failed. Our correspondents are investigating the incident... |
* Initial plan * fix: only hide credential files if parent directory exists Prevents Docker mount errors when credential file parent directories don't exist on the host. This fixes the "read-only file system" error when Docker tries to create mountpoints for non-existent parents. The fix checks if the parent directory exists before adding /dev/null mounts for credential files in both normal and chroot modes. This maintains security while avoiding mount failures. Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: replace /dev/null mounts with tmpfs for credential hiding (#738) * Initial plan * fix: replace /dev/null mounts with tmpfs for credential hiding This fixes Docker mount errors like "read-only file system" that occur when mounting /dev/null over credential files whose parent directories don't exist in the container's filesystem. The solution uses tmpfs mounts instead, which create empty in-memory filesystems that overlay directories without requiring the target path to exist first. Changes: - Normal mode: Hide credential directories (~/.docker, ~/.ssh, ~/.aws, etc.) using tmpfs - Chroot mode: Hide credential directories at /host paths using tmpfs - Updated DockerService type to include tmpfs property - Updated tests to verify tmpfs behavior instead of /dev/null mounts - Fixed config mutation bug by using local variable instead of mutating config object Closes the GitHub Actions failure where cargo credentials mounting failed with: "error mounting "/dev/null" to rootfs at "/host/home/runner/.cargo/credentials": create mountpoint ... read-only file system" Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: prevent .cargo volume/tmpfs mount conflict in chroot mode (#740) * Initial plan * fix: prevent .cargo volume/tmpfs mount conflict in chroot mode Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> * fix: handle missing docker-compose.yml during cleanup gracefully (#741) * Initial plan * fix: handle missing docker-compose.yml during cleanup gracefully Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
|
📰 DEVELOPING STORY: Smoke Copilot reports failed. Our correspondents are investigating the incident... |
|
💫 TO BE CONTINUED... Smoke Claude failed! Our hero faces unexpected challenges... |
|
Chroot tests failed Smoke Chroot failed - See logs for details. |
|
🌑 The shadows whisper... Smoke Codex failed. The oracle requires further meditation... |
There was a problem hiding this 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 fixes a Docker mount error that occurs when ~/.copilot/logs doesn't exist on the host. When Docker attempts to mount a read-write subdirectory (~/.copilot/logs) inside an already-mounted read-only parent directory (~/.copilot), it fails if the subdirectory doesn't exist. The solution pre-creates the mountpoint in writeConfigs() before container startup.
Additionally, this PR migrates credential hiding from /dev/null volume mounts to tmpfs mounts, which avoids Docker errors when target paths don't exist in the container filesystem. This architectural change affects how credential directories like ~/.docker, ~/.ssh, and ~/.aws are hidden from the agent.
Changes:
- Pre-create
~/.copilot/logsdirectory inwriteConfigs()to serve as a mountpoint before Docker starts - Migrate credential hiding from
/dev/nullvolume mounts to tmpfs mounts for better reliability - Add conditional logic to prevent mounting
~/.cargowhen credentials are hidden via tmpfs - Add fallback logic in
stopContainers()to stop containers by name when compose file is missing
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/types.ts | Add documentation for tmpfs field in DockerService interface, remove trailing whitespace |
| src/docker-manager.ts | Pre-create .copilot/logs mountpoint; migrate credential hiding from /dev/null to tmpfs; add .cargo mount conditional; add stopContainers() fallback |
| src/docker-manager.test.ts | Update tests to verify tmpfs-based credential hiding instead of /dev/null mounts; add tests for .cargo mount behavior |
Comments suppressed due to low confidence (2)
src/docker-manager.ts:723
- The conditional check on line 719 appears to be ineffective. It checks if any mount starts with the home directory path (e.g., "/home/user:"), but all existing mounts at this point are subdirectories (e.g., "/home/user/.docker:", "/home/user/.ssh:"). Since we never mount the entire home directory via tmpfs, this check will always pass, making the conditional unnecessary. Consider either removing the conditional (since .npmrc should always be hidden), or updating the comment to reflect the actual behavior.
if (!credentialTmpfsMounts.some(mount => mount.startsWith(`${npmrcParent}:`))) {
// Only add if we're not already mounting the entire home directory
// In practice, we'll mount ~/.npmrc as a tmpfs (which will be an empty directory)
credentialTmpfsMounts.push(`${effectiveHome}/.npmrc:rw,noexec,nosuid,size=1m`);
}
src/docker-manager.ts:1333
- The fallback logic for stopping containers by name when the compose file is missing is unrelated to the main purpose of this PR (fixing the .copilot/logs mountpoint issue). This should be either mentioned in the PR description or moved to a separate PR for better traceability. The defensive error handling is valuable, but bundling unrelated changes makes it harder to review and understand the PR's scope.
// 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');
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| logger.debug(`Hidden ${credentialFiles.length} credential file(s) via /dev/null mounts`); | ||
| // Also hide ~/.npmrc file (NPM registry tokens) - needs special handling as it's a file | ||
| // Mount its parent directory as tmpfs to hide it |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment states "Mount its parent directory as tmpfs" but the code actually mounts the file path directly (${effectiveHome}/.npmrc), not its parent directory. Since tmpfs creates a directory (not a file), this will make .npmrc appear as an empty directory in the container, which effectively hides the credential file. The comment should be updated to accurately reflect this behavior: "Mount ~/.npmrc as a tmpfs directory to hide the NPM registry token file".
This issue also appears on line 719 of the same file.
| // Mount its parent directory as tmpfs to hide it | |
| // Mount ~/.npmrc as a tmpfs directory to hide the NPM registry token file |
| // SECURITY: Hide credential directories using tmpfs (empty in-memory filesystem) | ||
| // This prevents prompt-injected commands from reading sensitive tokens | ||
| // even if the attacker knows the file paths | ||
| const credentialFiles = [ | ||
| `${effectiveHome}/.docker/config.json`, // Docker Hub tokens | ||
| `${effectiveHome}/.npmrc`, // NPM registry tokens | ||
| `${effectiveHome}/.cargo/credentials`, // Rust crates.io tokens | ||
| `${effectiveHome}/.composer/auth.json`, // PHP Composer tokens | ||
| `${effectiveHome}/.config/gh/hosts.yml`, // GitHub CLI OAuth tokens | ||
| // SSH private keys (CRITICAL - server access, git operations) | ||
| `${effectiveHome}/.ssh/id_rsa`, | ||
| `${effectiveHome}/.ssh/id_ed25519`, | ||
| `${effectiveHome}/.ssh/id_ecdsa`, | ||
| `${effectiveHome}/.ssh/id_dsa`, | ||
| // Cloud provider credentials (CRITICAL - infrastructure access) | ||
| `${effectiveHome}/.aws/credentials`, | ||
| `${effectiveHome}/.aws/config`, | ||
| `${effectiveHome}/.kube/config`, | ||
| `${effectiveHome}/.azure/credentials`, | ||
| `${effectiveHome}/.config/gcloud/credentials.db`, | ||
| // even if the attacker knows the file paths. | ||
| // Using tmpfs instead of /dev/null mounts avoids Docker errors when parent directories | ||
| // don't exist in the container filesystem. | ||
| const credentialDirs = [ | ||
| `${effectiveHome}/.docker`, // Docker Hub tokens (config.json) | ||
| `${effectiveHome}/.ssh`, // SSH private keys (CRITICAL - server access, git operations) | ||
| `${effectiveHome}/.aws`, // AWS credentials (CRITICAL - infrastructure access) | ||
| `${effectiveHome}/.kube`, // Kubernetes config (CRITICAL - cluster access) | ||
| `${effectiveHome}/.azure`, // Azure credentials | ||
| `${effectiveHome}/.config/gcloud`, // Google Cloud credentials | ||
| `${effectiveHome}/.config/gh`, // GitHub CLI OAuth tokens | ||
| `${effectiveHome}/.cargo`, // Rust crates.io tokens (credentials file) | ||
| `${effectiveHome}/.composer`, // PHP Composer tokens (auth.json) | ||
| ]; | ||
|
|
||
| credentialFiles.forEach(credFile => { | ||
| agentVolumes.push(`/dev/null:${credFile}:ro`); | ||
| // Add tmpfs mounts for credential directories | ||
| credentialDirs.forEach(credDir => { | ||
| credentialTmpfsMounts.push(`${credDir}:rw,noexec,nosuid,size=1m`); | ||
| }); |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR title and description focus on fixing the .copilot/logs mountpoint issue, but this PR also includes a significant architectural change: migrating all credential hiding from /dev/null volume mounts to tmpfs mounts. While this change appears to be intentional and correct (tmpfs avoids errors when target paths don't exist), it represents a substantial scope expansion that should be documented in the PR description. This makes it difficult for reviewers to understand the full scope of changes and their implications.
This issue also appears on line 1304 of the same file.
| // Create the mountpoint directory on the host for agent logs | ||
| // This is required because ~/.copilot is mounted read-only, so Docker cannot | ||
| // create the mountpoint for ~/.copilot/logs inside the read-only mount | ||
| const effectiveHome = config.enableChroot ? getRealUserHome() : (process.env.HOME || '/root'); | ||
| const copilotLogsDir = path.join(effectiveHome, '.copilot', 'logs'); | ||
| if (!fs.existsSync(copilotLogsDir)) { | ||
| fs.mkdirSync(copilotLogsDir, { recursive: true }); | ||
| logger.debug(`Copilot logs mountpoint created at: ${copilotLogsDir}`); | ||
| } |
Copilot
AI
Feb 12, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new functionality to create ~/.copilot/logs mountpoint lacks test coverage, while similar directory creation functionality (agent-logs, squid-logs) has dedicated tests in docker-manager.test.ts. Consider adding a test that verifies the .copilot/logs directory is created in both chroot and non-chroot modes, similar to the existing "should create agent-logs directory" test at line 1511-1528.
Docker mount at
~/.copilot:rofollowed byagent-logs:~/.copilot/logs:rwfails when the logs subdirectory doesn't exist on the host. Docker cannot create the mountpoint inside an already-mounted read-only parent directory.Changes
writeConfigs(): Ensure~/.copilot/logsexists on the host before container startup~/.copilotmountContext
GitHub Actions runners don't have
~/.copilotpre-created, triggering this failure. Local environments with existing.copilotdirectories were unaffected.Error from CI: