Merge pull request #4172 from MoerAI/fix/team-mode-base-dir-chmod-eperm

This commit is contained in:
YeonGyu-Kim
2026-05-20 18:59:59 +09:00
committed by GitHub
2 changed files with 68 additions and 2 deletions
@@ -117,4 +117,53 @@ describe("paths", () => {
expect(directoryStat.mode & 0o777).toBe(0o700)
}
})
test("ensureBaseDirs swallows EPERM from chmod and logs a warning instead of aborting team-mode init", async () => {
// given: directories exist with permissive mode that chmod cannot tighten
// (mirrors macOS network mount / non-owner / SIP cases reported in #4023).
const baseDir = path.join(tmpdir(), `omo-test-eperm-${randomUUID()}`)
temporaryDirectories.push(baseDir)
await mkdir(baseDir, { recursive: true })
await mkdir(path.join(baseDir, "teams"), { recursive: true })
await mkdir(path.join(baseDir, "runtime"), { recursive: true })
await mkdir(path.join(baseDir, "worktrees"), { recursive: true })
const realFs = await import("node:fs/promises")
let chmodCalls = 0
mock.module("node:fs/promises", () => ({
...realFs,
chmod: async (target: string) => {
chmodCalls += 1
const eperm = Object.assign(new Error(`EPERM: operation not permitted, chmod '${target}'`), {
code: "EPERM",
syscall: "chmod",
path: target,
errno: -1,
})
throw eperm
},
}))
const { ensureBaseDirs: ensureBaseDirsWithMockedChmod } = await import("./paths")
logCalls.splice(0)
// when
let thrown: unknown = null
try {
await ensureBaseDirsWithMockedChmod(baseDir)
} catch (error) {
thrown = error
}
// then: function does not throw, EPERM was reached, and one warning was logged.
expect(thrown).toBeNull()
expect(chmodCalls).toBeGreaterThan(0)
const warnings = logCalls.filter(([message]) =>
message === "team-mode: chmod refused on base directory; continuing with existing permissions"
)
expect(warnings.length).toBeGreaterThan(0)
const firstWarning = warnings[0]?.[1] as { code?: string; path?: string } | undefined
expect(firstWarning?.code).toBe("EPERM")
expect(firstWarning?.path).toContain(baseDir)
})
})
+19 -2
View File
@@ -100,6 +100,23 @@ export async function discoverTeamSpecs(
return discoveredTeamSpecs
}
async function safeChmod(directoryPath: string, mode: number): Promise<void> {
try {
await chmod(directoryPath, mode)
} catch (error) {
const errnoError = error as NodeJS.ErrnoException
if (errnoError?.code === "EPERM" || errnoError?.code === "ENOTSUP" || errnoError?.code === "EINVAL") {
log("team-mode: chmod refused on base directory; continuing with existing permissions", {
path: directoryPath,
code: errnoError.code,
syscall: errnoError.syscall,
})
return
}
throw error
}
}
export async function ensureBaseDirs(baseDir: string): Promise<void> {
const directories = [
baseDir,
@@ -110,13 +127,13 @@ export async function ensureBaseDirs(baseDir: string): Promise<void> {
for (const directoryPath of directories) {
await mkdir(directoryPath, { recursive: true, mode: 0o700 })
await chmod(directoryPath, 0o700)
await safeChmod(directoryPath, 0o700)
}
await Promise.all(directories.map(async (directoryPath) => {
const directoryStat = await stat(directoryPath)
if ((directoryStat.mode & 0o777) !== 0o700) {
await chmod(directoryPath, 0o700)
await safeChmod(directoryPath, 0o700)
}
}))
}