diff --git a/src/workdir-setup.test.ts b/src/workdir-setup.test.ts index 5b4354850..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'; @@ -138,6 +140,41 @@ 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: fs.PathLike, mode: fs.Mode) => { + if (target === mcpLogsDir) throw eperm; + actualFs.chmodSync(target, mode); + }); + + const config = buildConfig(); + const logPaths = resolveLogPaths(config); + + 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 0c043cc37..0574310a1 100644 --- a/src/workdir-setup.ts +++ b/src/workdir-setup.ts @@ -194,9 +194,17 @@ 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 (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)`); + } } }