diff --git a/src/lib/sandbox/build-context.ts b/src/lib/sandbox/build-context.ts index 9f265ed00d5..5c34a282d4f 100644 --- a/src/lib/sandbox/build-context.ts +++ b/src/lib/sandbox/build-context.ts @@ -73,6 +73,7 @@ function stageLegacySandboxBuildContext( fs.cpSync(path.join(rootDir, "scripts"), path.join(buildCtx, "scripts"), { recursive: true, }); + normalizeReadModesForDockerCopy(path.join(buildCtx, "scripts")); fs.cpSync( path.join(rootDir, "src", "lib", "messaging"), path.join(buildCtx, "src", "lib", "messaging"), @@ -260,6 +261,7 @@ function stageOptimizedSandboxBuildContext( path.join(rootDir, "scripts", "lib", "reviewed-npm-archive.mts"), path.join(stagedScriptsDir, "lib", "reviewed-npm-archive.mts"), ); + normalizeReadModesForDockerCopy(stagedScriptsDir); return { buildCtx, stagedDockerfile }; } diff --git a/test/sandbox-build-context.test.ts b/test/sandbox-build-context.test.ts index 9852e9b06a9..0914808b3e4 100644 --- a/test/sandbox-build-context.test.ts +++ b/test/sandbox-build-context.test.ts @@ -113,7 +113,9 @@ describe("sandbox build context staging", () => { writeFixture(path.join("scripts", "patch-openclaw-issue-4434-diagnostics.mts")); writeFixture(path.join("scripts", "patch-openclaw-device-self-approval.mts")); writeFixture(path.join("scripts", "verify-wechat-runtime-lock.mts")); - writeFixture(path.join("scripts", "lib", "reviewed-npm-archive.mts")); + writeFixture(path.join("scripts", "lib", "reviewed-npm-archive.mts"), "fixture\n", 0o700); + fs.chmodSync(path.join(sourceRoot, "scripts"), 0o700); + fs.chmodSync(path.join(sourceRoot, "scripts", "lib"), 0o700); } function expectDockerfileScriptCopiesExist(buildCtx: string, stagedDockerfile: string) { @@ -187,6 +189,16 @@ describe("sandbox build context staging", () => { expect(fs.existsSync(path.join(buildCtx, "src", "lib", "tool-disclosure.ts"))).toBe(true); } + function expectStagedScriptModes(buildCtx: string) { + const stagedScripts = path.join(buildCtx, "scripts"); + const stagedLib = path.join(stagedScripts, "lib"); + const stagedHelper = path.join(stagedLib, "reviewed-npm-archive.mts"); + + expect((fs.statSync(stagedScripts).mode & 0o777).toString(8)).toBe("755"); + expect((fs.statSync(stagedLib).mode & 0o777).toString(8)).toBe("755"); + expect((fs.statSync(stagedHelper).mode & 0o777).toString(8)).toBe("755"); + } + it("normalizes copied blueprint modes with chmod a+rX semantics", () => { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-build-context-unit-")); const blueprintDir = path.join(tmpDir, "nemoclaw-blueprint"); @@ -281,6 +293,46 @@ describe("sandbox build context staging", () => { } }); + it("optimized staging makes copied scripts readable under a restrictive umask (#7071)", () => { + const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-build-context-source-")); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-build-context-script-mode-")); + + try { + writeBuildContextFixture(sourceRoot); + const previousUmask = process.umask(0o077); + try { + const { buildCtx } = stageOptimizedSandboxBuildContext(sourceRoot, tmpDir); + expectStagedScriptModes(buildCtx); + } finally { + process.umask(previousUmask); + } + } finally { + fs.rmSync(sourceRoot, { recursive: true, force: true }); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it("legacy staging makes copied scripts readable under a restrictive umask (#7071)", () => { + const sourceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-build-context-source-")); + const tmpDir = fs.mkdtempSync( + path.join(os.tmpdir(), "nemoclaw-build-context-legacy-script-mode-"), + ); + + try { + writeBuildContextFixture(sourceRoot); + const previousUmask = process.umask(0o077); + try { + const { buildCtx } = stageLegacySandboxBuildContext(sourceRoot, tmpDir); + expectStagedScriptModes(buildCtx); + } finally { + process.umask(previousUmask); + } + } finally { + fs.rmSync(sourceRoot, { recursive: true, force: true }); + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + it("optimized staging excludes blueprint .venv and extra scripts while preserving required files", () => { const repoRoot = path.join(import.meta.dirname, ".."); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-build-context-opt-"));