Skip to content

fix: pre-flight reclaim of stale root-owned directories before writeConfigs#5983

Closed
lpcox wants to merge 1 commit into
mainfrom
fix/preflight-reclaim-stale-workdir
Closed

fix: pre-flight reclaim of stale root-owned directories before writeConfigs#5983
lpcox wants to merge 1 commit into
mainfrom
fix/preflight-reclaim-stale-workdir

Conversation

@lpcox

@lpcox lpcox commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Problem

On persistent GitHub Actions runners, a previous AWF run (or Docker daemon) can leave /tmp/gh-aw/sandbox/firewall/ owned by root. When the next AWF invocation runs as non-root (rootless/network-isolation mode), mkdirSync fails with EACCES before any container is started:

[ERROR] Fatal error: Error: EACCES: permission denied, mkdir '/tmp/gh-aw/sandbox/firewall/logs'
    at Object.mkdirSync (node:fs:1370:26)
    at wn (/home/runner/.local/lib/awf/awf-bundle.js:372:1743)

Root cause: With --network-isolation now the default, AWF runs without sudo. But Docker containers from previous runs (or the Docker daemon itself) leave directories owned by root. The next non-root AWF invocation cannot create subdirectories under these root-owned paths.

CI failure: https://github.com/github/gh-aw/actions/runs/28856007222/job/85583002595

Solution

Adds a pre-flight reclaim step that detects non-writable ancestor directories and removes them before mkdirSync is called. Applied to:

  1. workDirvalidateAndPrepareWorkDir() in config-writer.ts
  2. All log directoriesprepareLogDirectories() in workdir-setup.ts
  3. /tmp/gh-aw/mcp-logs — hardcoded MCP gateway path

Reclaim strategy

  1. Walk from target path upward to find the first existing non-writable ancestor
  2. Attempt sudo rm -rf (works passwordless on GitHub-hosted runners)
  3. Fall back to fs.rmSync (handles cases where only leaf permissions are wrong)
  4. Log warnings if reclaim fails (writeConfigs will still throw, but with better context)

Safety guards

  • Only activates when running as non-root (root never gets EACCES)
  • Only removes the narrowest non-writable ancestor (not arbitrary paths)
  • Refuses to operate on protected system paths (/, /tmp, /home/runner, etc.)
  • Logs all actions for auditability
  • Falls back gracefully if neither sudo nor rmSync succeeds

Relationship to existing fixes

PR Layer Scope
github/gh-aw#30414 Post-run chmod -R a+rX for artifact upload
github/gh-aw#27742 Post-run Include audit dir in chmod
This PR Pre-run Reclaim stale dirs before writeConfigs
github/gh-aw#42400 Pre-run (closed) Similar approach in gh-aw's bash scripts

This is the AWF-side fix. The previous approach (github/gh-aw#42400) added a bash pre-flight in gh-aw's setup scripts, but was closed without merge. Fixing it in AWF itself is more robust — it covers all invocations regardless of how AWF is called.

Testing

  • 15 new unit tests for preflight-reclaim.ts covering:
    • Protected path detection
    • Non-writable ancestor detection
    • Reclaim via sudo fallback to rmSync
    • Root user bypass
    • Writable path no-op
  • All 3520 existing tests continue to pass
  • Type-checks cleanly (tsc --noEmit)

…onfigs

On persistent GitHub Actions runners, a previous AWF run (or Docker daemon)
can leave /tmp/gh-aw/sandbox/firewall/ owned by root. When the next AWF
invocation runs as non-root (rootless/network-isolation mode), mkdirSync
fails with EACCES before any container is started:

  EACCES: permission denied, mkdir '/tmp/gh-aw/sandbox/firewall/logs'

This adds a pre-flight reclaim step that detects non-writable ancestor
directories and removes them (via sudo rm -rf, then fs.rmSync fallback)
before mkdirSync is called. The reclaim is applied to:

- workDir (validateAndPrepareWorkDir in config-writer.ts)
- All log directories (prepareLogDirectories in workdir-setup.ts)
- /tmp/gh-aw/mcp-logs (hardcoded MCP gateway path)

Safety guards:
- Only acts when running as non-root (root never gets EACCES)
- Only removes the narrowest non-writable ancestor
- Refuses to operate on protected system paths (/, /tmp, /home/runner, etc.)
- Logs all actions for auditability
- Falls back gracefully if neither sudo nor rmSync succeeds

This is the AWF-side complement to gh-aw's compiled workflow chmod steps
(#30414, #27742) which fix artifact upload permissions post-run. This fix
addresses the pre-run failure path that those PRs cannot cover.

Fixes: github/gh-aw#42400 (supersedes the closed, unmerged approach)
Ref: https://github.com/github/gh-aw/actions/runs/28856007222/job/85583002595

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 7, 2026 12:48
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

⚠️ Coverage Regression Detected

This PR decreases test coverage. Please add tests to maintain coverage levels.

Overall Coverage

Metric Base PR Delta
Lines 98.82% 98.70% 📉 -0.12%
Statements 98.76% 98.63% 📉 -0.13%
Functions 99.72% 99.72% ➡️ +0.00%
Branches 95.19% 95.05% 📉 -0.14%
📁 Per-file Coverage Changes (2 files)
File Lines (Before → After) Statements (Before → After)
src/workdir-setup.ts 97.8% → 97.9% (+0.06%) 97.8% → 97.9% (+0.06%)
src/config-writer.ts 91.4% → 91.6% (+0.18%) 91.4% → 91.6% (+0.18%)
✨ New Files (1 files)
  • src/preflight-reclaim.ts: 85.1% lines

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

@lpcox

lpcox commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: the pre-flight reclaim belongs in gh-aw (which owns the paths and has the setup phase), not in AWF. AWF will get a defensive guard with clear error messages instead. The actual fix will be a PR in gh-aw.

@lpcox lpcox closed this Jul 7, 2026
@lpcox lpcox deleted the fix/preflight-reclaim-stale-workdir branch July 7, 2026 12:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 a pre-flight “stale directory reclaim” mechanism intended to prevent EACCES failures on persistent GitHub Actions runners when previously root-owned paths under /tmp/gh-aw/... block subsequent non-root AWF runs, before writeConfigs() and log directory setup occur.

Changes:

  • Introduces reclaimStaleDirectory() to detect and remove the narrowest non-writable ancestor directory via sudo rm -rf with a fs.rmSync fallback.
  • Invokes the pre-flight reclaim step before creating the workDir and before creating the various log/session directories (including /tmp/gh-aw/mcp-logs).
  • Adds unit tests covering protected-path handling and reclaim flows.
Show a summary per file
File Description
src/workdir-setup.ts Runs pre-flight reclaim on log/session/MCP log directories before ensuring they exist.
src/preflight-reclaim.ts New reclaim implementation (ancestor detection, protected-path checks, sudo/fs removal attempts).
src/preflight-reclaim.test.ts New unit tests for reclaim behavior and guardrails.
src/config-writer.ts Runs pre-flight reclaim on workDir before creating it with 0o700.

Review details

Tip

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

  • Files reviewed: 4/4 changed files
  • Comments generated: 3
  • Review effort level: Low

Comment thread src/preflight-reclaim.ts
Comment on lines +96 to +108
// Collect path segments from target down to the first existing directory
const nonExistentSegments: string[] = [];
while (!fs.existsSync(current)) {
nonExistentSegments.unshift(path.basename(current));
const parent = path.dirname(current);
if (parent === current) break; // reached root
current = parent;
}

// `current` is now the first existing ancestor of targetDir
if (!fs.existsSync(current)) {
return undefined;
}
Comment thread src/preflight-reclaim.ts
Comment on lines +14 to +18
* This function detects non-writable ancestors in the workDir path and attempts
* to reclaim them via:
* 1. `sudo rm -rf` (available on GitHub-hosted runners without a password)
* 2. Plain `rm -rf` fallback (may work if only leaf permissions are wrong)
*
Comment thread src/preflight-reclaim.ts
Comment on lines +127 to +151
const protectedPaths = new Set([
'/',
'/tmp',
'/var',
'/var/tmp',
'/home',
'/root',
'/usr',
'/etc',
'/opt',
'/bin',
'/sbin',
'/lib',
'/lib64',
'/dev',
'/sys',
'/proc',
'/run',
// GitHub Actions runner paths (protect the runner itself)
'/home/runner',
'/home/runner/work',
]);

return protectedPaths.has(resolved);
}
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