fix(team-mode): swallow EPERM/ENOTSUP/EINVAL from chmod on base dir to keep init alive (fixes #4023)
ensureBaseDirs unconditionally called chmod(baseDir, 0o700) on every startup and on every team_create. On filesystems where the OS rejects chmod for the directory (network mounts, SIP-protected locations, non-owner cases on macOS shared by multiple GUI users), the call raises EPERM and the entire team-mode init aborts: [team-mode] init failed: EPERM: operation not permitted, chmod '/Users/<u>/.omo' Wrap chmod through a small safeChmod helper that converts EPERM, ENOTSUP, and EINVAL into a single warning log and continues. mkdir already creates new directories with mode 0o700, and the existing post-creation stat-guard remains in place for the case where the directory pre-exists with a different mode and chmod is permitted, so the security envelope on supported filesystems is unchanged. All other error codes (ENOENT, EACCES, etc.) still propagate. Regression test mocks node:fs/promises.chmod to throw EPERM and asserts that ensureBaseDirs completes successfully and emits exactly the documented warning.
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user