Skip to content

Conversation

@Claude
Copy link
Contributor

@Claude Claude AI commented Feb 12, 2026

Docker mount at ~/.copilot:ro followed by agent-logs:~/.copilot/logs:rw fails when the logs subdirectory doesn't exist on the host. Docker cannot create the mountpoint inside an already-mounted read-only parent directory.

Changes

  • Pre-create mountpoint in writeConfigs(): Ensure ~/.copilot/logs exists on the host before container startup
  • Prevents Docker from attempting directory creation inside the read-only ~/.copilot mount

Context

GitHub Actions runners don't have ~/.copilot pre-created, triggering this failure. Local environments with existing .copilot directories were unaffected.

Error from CI:

error mounting "/tmp/awf-1770874478628/agent-logs" to rootfs at "/home/runner/.copilot/logs": 
create mountpoint: mkdirat .../merged/home/runner/.copilot/logs: read-only file system

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>
@Claude Claude AI changed the title [WIP] Fix failing GitHub Actions workflow agent fix: create .copilot/logs mountpoint before docker mount Feb 12, 2026
@Claude Claude AI requested a review from lpcox February 12, 2026 05:42
@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

📰 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>
@lpcox lpcox marked this pull request as ready for review February 12, 2026 06:33
Copilot AI review requested due to automatic review settings February 12, 2026 06:33
@lpcox lpcox merged commit c20d6a3 into lpcox/port-one-shot-token-rust Feb 12, 2026
13 checks passed
@lpcox lpcox deleted the claude/fix-gh-actions-workflow branch February 12, 2026 06:33
@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

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

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

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

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 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/logs directory in writeConfigs() to serve as a mountpoint before Docker starts
  • Migrate credential hiding from /dev/null volume mounts to tmpfs mounts for better reliability
  • Add conditional logic to prevent mounting ~/.cargo when 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
Copy link

Copilot AI Feb 12, 2026

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.

Suggested change
// Mount its parent directory as tmpfs to hide it
// Mount ~/.npmrc as a tmpfs directory to hide the NPM registry token file

Copilot uses AI. Check for mistakes.
Comment on lines +694 to 714
// 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`);
});
Copy link

Copilot AI Feb 12, 2026

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.

Copilot uses AI. Check for mistakes.
Comment on lines +925 to +933
// 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}`);
}
Copy link

Copilot AI Feb 12, 2026

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.

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.

2 participants