From a262a3ea088aa1616e7447dca607edf53e156276 Mon Sep 17 00:00:00 2001 From: Glucksberg <80581902+Glucksberg@users.noreply.github.com> Date: Sun, 1 Mar 2026 22:07:34 -0400 Subject: [PATCH] fix(docker): ensure agent directory permissions in docker-setup.sh (#28841) * fix(docker): ensure agent directory permissions in docker-setup.sh * fix(docker): restrict chown to config-dir mount, not workspace The previous 'chown -R node:node /home/node/.openclaw' call crossed into the workspace bind mount on Linux hosts, recursively rewriting ownership of all user project files in the workspace directory. Fix: use 'find -xdev' to restrict chown to the config-dir filesystem only (won't cross bind-mount boundaries). Then separately chown only the OpenClaw metadata subdirectory (.openclaw/) within the workspace, leaving the user's project files untouched. Addresses review comment on PR #28841. --- docker-setup.sh | 22 ++++++++++++++++++++-- src/docker-setup.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/docker-setup.sh b/docker-setup.sh index a19573b84bc..61f66ec6d80 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -162,9 +162,11 @@ fi mkdir -p "$OPENCLAW_CONFIG_DIR" mkdir -p "$OPENCLAW_WORKSPACE_DIR" -# Seed device-identity parent eagerly for Docker Desktop/Windows bind mounts -# that reject creating new subdirectories from inside the container. +# Seed directory tree eagerly so bind mounts work even on Docker Desktop/Windows +# where the container (even as root) cannot create new host subdirectories. mkdir -p "$OPENCLAW_CONFIG_DIR/identity" +mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/agent" +mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions" export OPENCLAW_CONFIG_DIR export OPENCLAW_WORKSPACE_DIR @@ -346,6 +348,22 @@ else fi fi +# Ensure bind-mounted data directories are writable by the container's `node` +# user (uid 1000). Host-created dirs inherit the host user's uid which may +# differ, causing EACCES when the container tries to mkdir/write. +# Running a brief root container to chown is the portable Docker idiom -- +# it works regardless of the host uid and doesn't require host-side root. +echo "" +echo "==> Fixing data-directory permissions" +# Use -xdev to restrict chown to the config-dir mount only — without it, +# the recursive chown would cross into the workspace bind mount and rewrite +# ownership of all user project files on Linux hosts. +# After fixing the config dir, only the OpenClaw metadata subdirectory +# (.openclaw/) inside the workspace gets chowned, not the user's project files. +docker compose "${COMPOSE_ARGS[@]}" run --rm --user root --entrypoint sh openclaw-cli -c \ + 'find /home/node/.openclaw -xdev -exec chown node:node {} +; \ + [ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true' + echo "" echo "==> Onboarding (interactive)" echo "Docker setup pins Gateway mode to local." diff --git a/src/docker-setup.test.ts b/src/docker-setup.test.ts index f09f7c7b4e0..defb5a2120a 100644 --- a/src/docker-setup.test.ts +++ b/src/docker-setup.test.ts @@ -171,6 +171,30 @@ describe("docker-setup.sh", () => { expect(identityDirStat.isDirectory()).toBe(true); }); + it("precreates agent data dirs to avoid EACCES in container", async () => { + const activeSandbox = requireSandbox(sandbox); + const configDir = join(activeSandbox.rootDir, "config-agent-dirs"); + const workspaceDir = join(activeSandbox.rootDir, "workspace-agent-dirs"); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_CONFIG_DIR: configDir, + OPENCLAW_WORKSPACE_DIR: workspaceDir, + }); + + expect(result.status).toBe(0); + const agentDirStat = await stat(join(configDir, "agents", "main", "agent")); + expect(agentDirStat.isDirectory()).toBe(true); + const sessionsDirStat = await stat(join(configDir, "agents", "main", "sessions")); + expect(sessionsDirStat.isDirectory()).toBe(true); + + // Verify that a root-user chown step runs before onboarding. + const log = await readFile(activeSandbox.logPath, "utf8"); + const chownIdx = log.indexOf("--user root"); + const onboardIdx = log.indexOf("onboard"); + expect(chownIdx).toBeGreaterThanOrEqual(0); + expect(onboardIdx).toBeGreaterThan(chownIdx); + }); + it("reuses existing config token when OPENCLAW_GATEWAY_TOKEN is unset", async () => { const activeSandbox = requireSandbox(sandbox); const configDir = join(activeSandbox.rootDir, "config-token-reuse");