diff --git a/src/lib/inference/local-adapter-lifecycle.test.ts b/src/lib/inference/local-adapter-lifecycle.test.ts index 76964f04e7a..d3206ca0a5f 100644 --- a/src/lib/inference/local-adapter-lifecycle.test.ts +++ b/src/lib/inference/local-adapter-lifecycle.test.ts @@ -9,6 +9,7 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { + ensureLocalAdapterStateDir, isLocalAdapterProcess, killLocalAdapterPid, loadLocalAdapterPid, @@ -129,3 +130,24 @@ describe("local adapter lifecycle", () => { ).resolves.toBe(false); }); }); + +describe("ensureLocalAdapterStateDir", () => { + it("creates directory with owner-only permissions (0o700)", () => { + if (process.platform === "win32") return; + const dir = tempDir(); + const stateDir = path.join(dir, "nested", "state"); + ensureLocalAdapterStateDir(stateDir); + const stat = fs.statSync(stateDir); + expect(stat.mode & 0o777).toBe(0o700); + }); + + it("tightens permissions on an existing world-readable directory", () => { + if (process.platform === "win32") return; + const dir = tempDir(); + const stateDir = path.join(dir, "lax"); + fs.mkdirSync(stateDir, { mode: 0o755 }); + ensureLocalAdapterStateDir(stateDir); + const stat = fs.statSync(stateDir); + expect(stat.mode & 0o777).toBe(0o700); + }); +}); diff --git a/src/lib/inference/local-adapter-lifecycle.ts b/src/lib/inference/local-adapter-lifecycle.ts index f0fb9dd15d2..472f1e26e52 100644 --- a/src/lib/inference/local-adapter-lifecycle.ts +++ b/src/lib/inference/local-adapter-lifecycle.ts @@ -26,7 +26,16 @@ export function ensureLocalAdapterStateDir( stateDir = DEFAULT_LOCAL_ADAPTER_STATE_DIR, ): void { if (!fs.existsSync(stateDir)) { - fs.mkdirSync(stateDir, { recursive: true }); + fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 }); + } + // Tighten permissions in case the directory was created with a lax umask. + try { + const stat = fs.statSync(stateDir); + if ((stat.mode & 0o077) !== 0) { + fs.chmodSync(stateDir, 0o700); + } + } catch { + // Best effort — stat/chmod may fail on non-POSIX or read-only fs. } } diff --git a/src/lib/onboard/config-sync.test.ts b/src/lib/onboard/config-sync.test.ts index 1e67c646601..56af39a0cb0 100644 --- a/src/lib/onboard/config-sync.test.ts +++ b/src/lib/onboard/config-sync.test.ts @@ -22,7 +22,10 @@ describe("sandbox config sync helpers", () => { providerLabel: "Other OpenAI-compatible endpoint", }); + expect(script).toMatch(/mkdir -p -m 700 ~\/\.nemoclaw/); + expect(script).toMatch(/chmod 700 ~\/\.nemoclaw/); expect(script).toMatch(/cat > ~\/\.nemoclaw\/config\.json/); + expect(script).toMatch(/chmod 600 ~\/\.nemoclaw\/config\.json/); expect(script).toContain('"model": "nemotron-3-nano:30b"'); expect(script).toContain('"credentialEnv": "OPENAI_API_KEY"'); expect(script).not.toMatch(/cat > ~\/\.openclaw\/openclaw\.json/); diff --git a/src/lib/onboard/config-sync.ts b/src/lib/onboard/config-sync.ts index 55aa87ee17e..410c82f97bc 100644 --- a/src/lib/onboard/config-sync.ts +++ b/src/lib/onboard/config-sync.ts @@ -37,10 +37,12 @@ export function buildSandboxConfigSyncScript(selectionConfig: ProviderSelectionC // chance to perform its own startup initialization. return ` set -euo pipefail -mkdir -p ~/.nemoclaw +mkdir -p -m 700 ~/.nemoclaw +chmod 700 ~/.nemoclaw cat > ~/.nemoclaw/config.json <<'EOF_NEMOCLAW_CFG' ${JSON.stringify(selectionConfig, null, 2)} EOF_NEMOCLAW_CFG +chmod 600 ~/.nemoclaw/config.json config_dir=/sandbox/.openclaw if [ -d "$config_dir" ]; then config_dir_owner="$(stat -c '%U' "$config_dir" 2>/dev/null || echo unknown)"