diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index d9de71fa78c2..e056cf160b0c 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -1385,7 +1385,6 @@ export async function downloadClawHubPackageArchive(params: { const target = await createTempDownloadTarget({ prefix: "openclaw-clawhub-clawpack", fileName: npmTarballName, - tmpDir: os.tmpdir(), }); await fs.writeFile(target.path, bytes); return { @@ -1428,7 +1427,6 @@ export async function downloadClawHubPackageArchive(params: { const target = await createTempDownloadTarget({ prefix: "openclaw-clawhub-package", fileName: `${params.name}.zip`, - tmpDir: os.tmpdir(), }); await fs.writeFile(target.path, bytes); return { @@ -1475,7 +1473,6 @@ export async function downloadClawHubSkillArchive(params: { const target = await createTempDownloadTarget({ prefix: "openclaw-clawhub-skill", fileName: `${params.slug}.zip`, - tmpDir: os.tmpdir(), }); await fs.writeFile(target.path, bytes); return { @@ -1518,7 +1515,6 @@ export async function downloadClawHubSkillArchiveUrl(params: { const target = await createTempDownloadTarget({ prefix: "openclaw-clawhub-skill", fileName: "skill.zip", - tmpDir: os.tmpdir(), }); await fs.writeFile(target.path, bytes); return { @@ -1555,7 +1551,6 @@ export async function downloadClawHubGitHubSkillArchive(params: { const target = await createTempDownloadTarget({ prefix: "openclaw-clawhub-github-skill", fileName: `${params.commit}.zip`, - tmpDir: os.tmpdir(), }); await fs.writeFile(target.path, bytes); return { diff --git a/src/infra/install-source-utils.chmod.test.ts b/src/infra/install-source-utils.chmod.test.ts new file mode 100644 index 000000000000..5b64e65fc891 --- /dev/null +++ b/src/infra/install-source-utils.chmod.test.ts @@ -0,0 +1,57 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js"; + +const resolvePreferredOpenClawTmpDirMock = vi.hoisted(() => vi.fn()); + +vi.mock("./tmp-openclaw-dir.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolvePreferredOpenClawTmpDir: resolvePreferredOpenClawTmpDirMock, + }; +}); + +import { withTempDir } from "./install-source-utils.js"; + +describe("withTempDir private root", () => { + const tempDirs = useAutoCleanupTempDirTracker(afterEach); + + it.runIf(process.platform !== "win32")( + "preserves parent temp root permissions when using private OpenClaw temp root", + async () => { + const mockParentRoot = tempDirs.make("openclaw-chmod-test-"); + const mockOpenClawDir = path.join(mockParentRoot, "openclaw"); + + await fs.mkdir(mockOpenClawDir, { recursive: true }); + await fs.chmod(mockParentRoot, 0o1777); + const canonicalOpenClawDir = await fs.realpath(mockOpenClawDir); + + resolvePreferredOpenClawTmpDirMock.mockReturnValue(mockOpenClawDir); + + let observedDir = ""; + const value = await withTempDir("openclaw-test-", async (tmpDir) => { + observedDir = tmpDir; + expect(path.dirname(tmpDir)).toBe(canonicalOpenClawDir); + await fs.writeFile(path.join(tmpDir, "marker.txt"), "ok"); + return "done"; + }); + + expect(value).toBe("done"); + + await expect( + fs.stat(observedDir).then( + () => true, + () => false, + ), + ).resolves.toBe(false); + + const privateRootStat = await fs.stat(mockOpenClawDir); + expect(privateRootStat.mode & 0o7777).toBe(0o700); + + const parentStat = await fs.stat(mockParentRoot); + expect(parentStat.mode & 0o7777).toBe(0o1777); + }, + ); +}); diff --git a/src/infra/install-source-utils.ts b/src/infra/install-source-utils.ts index 4f9474e58d95..6b77866f3ee4 100644 --- a/src/infra/install-source-utils.ts +++ b/src/infra/install-source-utils.ts @@ -1,6 +1,5 @@ // Resolves and packages install sources for plugin installs. import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { isRecord } from "@openclaw/normalization-core/record-coerce"; import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; @@ -11,6 +10,7 @@ import { resolveArchiveKind } from "./archive.js"; import { pathExists } from "./fs-safe.js"; import { applyNpmFreshnessBypassEnv, type NpmProjectInstallEnvOptions } from "./npm-install-env.js"; import { withTempWorkspace } from "./private-temp-workspace.js"; +import { resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js"; /** Metadata npm reports when resolving a registry spec or packed archive. */ export type NpmSpecResolution = { @@ -141,7 +141,7 @@ export async function withTempDir( fn: (tmpDir: string) => Promise, options?: { rootDir?: string }, ): Promise { - const rootDir = options?.rootDir ?? os.tmpdir(); + const rootDir = options?.rootDir ?? resolvePreferredOpenClawTmpDir(); return await withTempWorkspace({ rootDir, prefix }, async (tmp) => fn(tmp.dir)); }