Skip to content

Conversation

@Claude
Copy link
Contributor

@Claude Claude AI commented Feb 12, 2026

stopContainers() fails with "no configuration file provided: not found" when the work directory or docker-compose.yml is missing during cleanup, causing GitHub Actions workflows to exit with errors.

Changes

  • Added defensive checks in stopContainers() to verify workDir and docker-compose.yml exist before running docker compose down -v
  • Implemented fallback mechanism to stop containers by name (awf-agent, awf-squid) using docker rm -f when compose file is unavailable
  • Each container cleanup uses independent try-catch to prevent cascading failures

Implementation

// Check if compose file exists 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 });
} else {
  // Fallback: stop containers by name
  for (const name of ['awf-agent', 'awf-squid']) {
    const { stdout } = await execa('docker', ['ps', '-aq', '-f', `name=^${name}$`]);
    if (stdout.trim()) {
      await execa('docker', ['rm', '-f', name]);
    }
  }
}

This ensures cleanup succeeds regardless of workDir state, preventing workflow failures when the environment is in an unexpected state.

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
@Claude Claude AI changed the title [WIP] Fix failing GitHub Actions workflow agent fix: handle missing docker-compose.yml during cleanup gracefully Feb 12, 2026
@Claude Claude AI requested a review from lpcox February 12, 2026 06:24
@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

Chroot tests passed! Smoke Chroot - All security and functionality tests succeeded.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 12, 2026

📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤

@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

C++ Build Test Results

Project CMake Build Status
fmt PASS
json PASS

Overall: PASS

All C++ projects built successfully.

AI generated by Build Test C++

@github-actions
Copy link
Contributor

Build Test: Deno - BLOCKED

Unable to complete Deno tests due to AWF credential hiding incompatibility

Error Details

Deno tests fail with the following error:

error: Error loading .npmrc at /home/runner/.npmrc.

Caused by:
    Is a directory (os error 21)

Root Cause

AWF's credential hiding mechanism mounts /home/runner/.npmrc as /dev/null (which appears as a directory), but Deno expects .npmrc to either:

  • Be a regular file
  • Not exist

This causes Deno to fail during initialization before any tests can run.

Test Status

Project Tests Status
oak N/A BLOCKED
std N/A BLOCKED

Overall: BLOCKED

Recommended Solutions

  1. Update AWF to use a different credential hiding method that doesn't break Deno (e.g., mount an empty file instead of /dev/null)
  2. Add --allow-npmrc flag to temporarily disable .npmrc hiding for Deno compatibility
  3. Run Deno tests outside the AWF firewall

Note: Label build-test-deno NOT added due to test failure.

AI generated by Build Test Deno

@github-actions
Copy link
Contributor

Node.js Build Test Results ✅

All Node.js projects tested successfully!

Project Install Tests Status
clsx PASS PASS
execa PASS PASS
p-limit PASS PASS

Overall: PASS

All projects installed dependencies and ran tests successfully.

AI generated by Build Test Node.js

@github-actions
Copy link
Contributor

Smoke Test Results (Run 21935968806)

Last 2 merged PRs:

✅ GitHub MCP: Retrieved PR data
✅ Playwright: Title verified ("GitHub")
✅ File Write: Created test file
✅ Bash Tool: File verified

Status: PASS

cc @Mossaka @lpcox @Copilot

AI generated by Smoke Copilot

@github-actions
Copy link
Contributor

Build Test: Bun - Results

Project Install Tests Status
elysia 1/1 PASS
hono 1/1 PASS

Overall: PASS

All Bun projects built and tested successfully.

AI generated by Build Test Bun

@github-actions
Copy link
Contributor

🔴 Rust Build Test - FAILED

Error: Rust toolchain not available in workflow environment.

Project Build Tests Status
fd N/A FAILED
zoxide N/A FAILED

Overall: FAIL

Error Details

The workflow environment does not have the Rust toolchain installed. cargo command not found.

Required Fix: The workflow needs to install Rust before running tests:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env

Or use the actions-rs/toolchain GitHub Action in the workflow setup.

AI generated by Build Test Rust

@github-actions
Copy link
Contributor

Go Build Test Results

Project Download Tests Status
color 1/1 PASS
env 1/1 PASS
uuid 1/1 PASS

Overall: PASS

All Go projects built and tested successfully.

AI generated by Build Test Go

@github-actions
Copy link
Contributor

.NET Build Test Results

Project Restore Build Run Status
hello-world PASS
json-parse PASS

Overall: PASS

All .NET projects successfully restored, built, and ran with expected output.

AI generated by Build Test .NET

@github-actions
Copy link
Contributor

Chroot Version Comparison Test Results

Runtime Host Version Chroot Version Match?
Python 3.12.12 3.12.3 ❌ NO
Node.js v24.13.0 v20.20.0 ❌ NO
Go go1.22.12 go1.22.12 ✅ YES

Overall Result: ❌ Tests Failed

Chroot mode successfully binds host runtimes, but version mismatches indicate the container is using its own bundled versions instead of host binaries. Only Go matched between host and chroot environments.

AI generated by Smoke Chroot

@github-actions
Copy link
Contributor

Security Review: Network Cleanup Gap in Fallback Path

Issue: Network Resource Leak in Fallback Cleanup

The fallback cleanup mechanism added in this PR has a network cleanup gap that could create security vulnerabilities.

Problem

When docker-compose.yml is missing, the code falls back to removing containers by name:

File: src/docker-manager.ts:1318-1333

// 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);
  }
}

However, the network is not removed in this fallback path.

Security Impact

  1. Network Persistence: The awf-net Docker network (172.30.0.0/24) remains active after fallback cleanup
  2. Stale Firewall Rules: The network bridge interface persists, potentially leaving host-level iptables rules active
  3. Inconsistent Security State: Future runs may inherit stale network configuration with residual firewall rules, creating gaps in egress filtering

Comparison with Normal Path

Normal cleanup (src/docker-manager.ts:1307-1311):

await execa('docker', ['compose', 'down', '-v'], {
  cwd: workDir,
  stdio: 'inherit',
});

docker compose down -v removes:

  • ✅ Containers
  • ✅ Volumes
  • Networks defined in the compose file

Fallback cleanup:

  • ✅ Containers
  • Networks NOT removed

Recommendation

Add network cleanup to the fallback path to maintain security parity:

} 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 {
      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);
    }
  }
  
  // Also remove the network to match docker compose down behavior
  try {
    await execa('docker', ['network', 'rm', 'awf-net'], { reject: false });
    logger.debug('Removed awf-net network');
  } catch (err) {
    logger.debug('Could not remove network:', err);
  }

  logger.success('Containers stopped successfully');
}

Severity

Medium - While not an immediate exploit vector, inconsistent cleanup could leave the firewall in an undefined state. The security boundary relies on proper teardown of all Docker resources and iptables rules. Network persistence breaks this assumption.


Note: The existing code intentionally keeps the network across runs for performance (see comment in src/cli.ts:1006), but this is only safe when cleanupHostIptables() is called. The fallback path doesn't guarantee consistent iptables cleanup if the network persists without the compose file.

AI generated by Security Guard

@github-actions
Copy link
Contributor

Java Build Test Results ✅

All Java projects compiled and tested successfully through the AWF firewall.

Project Compile Tests Status
gson 1/1 PASS
caffeine 1/1 PASS

Overall: PASS

All compilation and test execution completed successfully with Maven proxy configuration.

AI generated by Build Test Java

@lpcox lpcox marked this pull request as ready for review February 12, 2026 06:32
Copilot AI review requested due to automatic review settings February 12, 2026 06:32
@lpcox lpcox merged commit c7ad1ac into claude/fix-github-actions-workflow-yet-again Feb 12, 2026
66 of 67 checks passed
@lpcox lpcox deleted the claude/fix-github-actions-workflow-one-more-time branch February 12, 2026 06:32
lpcox added a commit that referenced this pull request Feb 12, 2026
* 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>
lpcox added a commit that referenced this pull request Feb 12, 2026
* 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>
lpcox added a commit that referenced this pull request Feb 12, 2026
* 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 added a commit that referenced this pull request Feb 12, 2026
* Initial plan

* fix: create .copilot/logs mountpoint before docker mount

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>

* fix: only hide credential files if parent directory exists (#737)

* 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>

---------

Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
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 adds defensive error handling to stopContainers() to prevent failures when docker-compose.yml is missing during cleanup. The changes implement a fallback mechanism that stops containers by name using docker rm -f when the compose file is unavailable.

Changes:

  • Added existence checks for workDir and docker-compose.yml before attempting docker compose down
  • Implemented fallback to stop containers by name (awf-agent, awf-squid) when compose file is missing
  • Added individual try-catch blocks for each container to prevent cascading failures
Comments suppressed due to low confidence (1)

src/docker-manager.ts:1314

  • The comment "Normal path: use docker compose down" is misleading. Due to the security fix at line 1149 that deletes docker-compose.yml immediately after containers start, this path will rarely execute during normal operation. The "fallback" path (lines 1314-1332) is actually the normal execution path.

Consider updating comments to reflect this reality:

  • Line 1307: "Compose file exists (e.g., startup failed before deletion): use docker compose down"
  • Line 1314: "Normal path: compose file was deleted for security, stop containers by name"
      // 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

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

Comment on lines +1304 to +1333
// 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');
}
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 logic assumes the fallback path (stopping containers by name) is for error scenarios, but due to the security fix at line 1149 that deletes docker-compose.yml immediately after containers start, the compose file will NOT exist during normal cleanup. This makes the fallback path the primary execution path, not a true fallback.

Consider either:

  1. Removing the compose file check and always using docker rm to stop containers by name (simpler, more predictable)
  2. Keeping the compose file but restricting its permissions instead of deleting it
  3. Adding a comment explaining that this is the expected normal path, not just a fallback

The current implementation works correctly but the code structure and comments are misleading about which path is the normal case versus the exceptional case.

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

Copilot uses AI. Check for mistakes.
Comment on lines +1317 to +1332
// 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');
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 error handling in the fallback path swallows all errors with individual try-catch blocks (lines 1320-1329). This means if both containers fail to stop (e.g., Docker daemon is down), the function will still log "Containers stopped successfully" and return without error.

Consider tracking whether at least one container operation succeeded, or accumulating errors and throwing if no containers could be stopped. For example:

let stoppedAny = false;
const errors: Error[] = [];
for (const name of containerNames) {
  try {
    // ... existing logic ...
    stoppedAny = true;
  } catch (err) {
    errors.push(err);
    logger.debug(`Could not stop container ${name}:`, err);
  }
}
if (!stoppedAny && errors.length > 0) {
  throw new Error(`Failed to stop any containers: ${errors.map(e => e.message).join(', ')}`);
}

This ensures the outer error handler and calling code are aware when cleanup truly fails.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants