From d791bcc056adc36573472942a75856efcce3112c Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Mon, 6 Jul 2026 06:34:22 -0700 Subject: [PATCH 1/2] fix: handle EPERM when chmod'ing pre-existing mcp-logs directory When the MCP Gateway creates /tmp/gh-aw/mcp-logs before AWF starts, the directory may be owned by a different user. The unconditional chmodSync call would crash with EPERM, failing the entire workflow. Wrap the chmod in a try/catch for the 'directory already exists' branch so AWF proceeds gracefully. The directory permissions are best-effort since the container uses a tmpfs overlay for this path anyway. Fixes smoke-chroot CI failures like: EPERM: operation not permitted, chmod '/tmp/gh-aw/mcp-logs' Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/workdir-setup.test.ts | 17 +++++++++++++++++ src/workdir-setup.ts | 11 ++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/workdir-setup.test.ts b/src/workdir-setup.test.ts index 5b4354850..c3c9f01fc 100644 --- a/src/workdir-setup.test.ts +++ b/src/workdir-setup.test.ts @@ -138,6 +138,23 @@ describe('prepareWorkDirectories', () => { expect(mode).toBe(0o777); }); + it('does not throw when chmod on pre-existing mcp-logs dir fails with EPERM', () => { + const mcpLogsDir = '/tmp/gh-aw/mcp-logs'; + fs.mkdirSync(mcpLogsDir, { recursive: true, mode: 0o700 }); + + // Simulate EPERM: directory owned by another user (e.g., MCP gateway) + const eperm = new Error("EPERM: operation not permitted, chmod '/tmp/gh-aw/mcp-logs'") as NodeJS.ErrnoException; + eperm.code = 'EPERM'; + (fs.chmodSync as jest.Mock).mockImplementation((target: string) => { + if (target === mcpLogsDir) throw eperm; + }); + + const config = buildConfig(); + const logPaths = resolveLogPaths(config); + + expect(() => prepareWorkDirectories(config, logPaths)).not.toThrow(); + }); + it('falls back to world-writable squid logs when squid chown fails', () => { const proxyLogsDir = path.join(fixture.tempDir, 'proxy-logs'); (fs.chownSync as unknown as jest.Mock).mockImplementation((targetPath: fs.PathLike) => { diff --git a/src/workdir-setup.ts b/src/workdir-setup.ts index 0c043cc37..0c9b8c105 100644 --- a/src/workdir-setup.ts +++ b/src/workdir-setup.ts @@ -194,9 +194,14 @@ function prepareLogDirectories(logPaths: LogPaths): void { fs.chmodSync(mcpLogsDir, 0o777); logger.debug(`MCP logs directory created at: ${mcpLogsDir}`); } else { - // Fix permissions if directory already exists (e.g., created by a previous run) - fs.chmodSync(mcpLogsDir, 0o777); - logger.debug(`MCP logs directory permissions fixed at: ${mcpLogsDir}`); + // Best-effort permission fix if directory already exists (e.g., created by MCP gateway + // or a previous run). May fail with EPERM if owned by a different user. + try { + fs.chmodSync(mcpLogsDir, 0o777); + logger.debug(`MCP logs directory permissions fixed at: ${mcpLogsDir}`); + } catch { + logger.debug(`MCP logs directory already exists at: ${mcpLogsDir} (chmod skipped, owned by another user)`); + } } } From 64318ae47e3b833a3130701e2286b6e091da1cf1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Jul 2026 14:06:21 +0000 Subject: [PATCH 2/2] fix: narrow mcp-logs chmod EPERM handling --- src/workdir-setup.test.ts | 22 +++++++++++++++++++++- src/workdir-setup.ts | 5 ++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/workdir-setup.test.ts b/src/workdir-setup.test.ts index c3c9f01fc..63e043a98 100644 --- a/src/workdir-setup.test.ts +++ b/src/workdir-setup.test.ts @@ -11,6 +11,8 @@ jest.mock('./host-env', () => require('./test-helpers/fs-mock-factory.test-utils // eslint-disable-next-line @typescript-eslint/no-require-imports jest.mock('./host-identity', () => require('./test-helpers/fs-mock-factory.test-utils').hostIdentityMockFactory()); +const actualFs = jest.requireActual('fs'); + import { prepareWorkDirectories, workdirSetupTestHelpers } from './workdir-setup'; import { resolveLogPaths } from './log-paths'; import { getRealUserHome } from './host-identity'; @@ -145,8 +147,9 @@ describe('prepareWorkDirectories', () => { // Simulate EPERM: directory owned by another user (e.g., MCP gateway) const eperm = new Error("EPERM: operation not permitted, chmod '/tmp/gh-aw/mcp-logs'") as NodeJS.ErrnoException; eperm.code = 'EPERM'; - (fs.chmodSync as jest.Mock).mockImplementation((target: string) => { + (fs.chmodSync as jest.Mock).mockImplementation((target: fs.PathLike, mode: fs.Mode) => { if (target === mcpLogsDir) throw eperm; + actualFs.chmodSync(target, mode); }); const config = buildConfig(); @@ -155,6 +158,23 @@ describe('prepareWorkDirectories', () => { expect(() => prepareWorkDirectories(config, logPaths)).not.toThrow(); }); + it('rethrows non-EPERM errors when chmod on pre-existing mcp-logs dir fails', () => { + const mcpLogsDir = '/tmp/gh-aw/mcp-logs'; + fs.mkdirSync(mcpLogsDir, { recursive: true, mode: 0o700 }); + + const erofs = new Error("EROFS: read-only file system, chmod '/tmp/gh-aw/mcp-logs'") as NodeJS.ErrnoException; + erofs.code = 'EROFS'; + (fs.chmodSync as jest.Mock).mockImplementation((target: fs.PathLike, mode: fs.Mode) => { + if (target === mcpLogsDir) throw erofs; + actualFs.chmodSync(target, mode); + }); + + const config = buildConfig(); + const logPaths = resolveLogPaths(config); + + expect(() => prepareWorkDirectories(config, logPaths)).toThrow(erofs); + }); + it('falls back to world-writable squid logs when squid chown fails', () => { const proxyLogsDir = path.join(fixture.tempDir, 'proxy-logs'); (fs.chownSync as unknown as jest.Mock).mockImplementation((targetPath: fs.PathLike) => { diff --git a/src/workdir-setup.ts b/src/workdir-setup.ts index 0c9b8c105..0574310a1 100644 --- a/src/workdir-setup.ts +++ b/src/workdir-setup.ts @@ -199,7 +199,10 @@ function prepareLogDirectories(logPaths: LogPaths): void { try { fs.chmodSync(mcpLogsDir, 0o777); logger.debug(`MCP logs directory permissions fixed at: ${mcpLogsDir}`); - } catch { + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'EPERM') { + throw error; + } logger.debug(`MCP logs directory already exists at: ${mcpLogsDir} (chmod skipped, owned by another user)`); } }