diff --git a/docker-compose.yml b/docker-compose.yml index 8d391e0be43f..25dd89d072e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: volumes: - ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw - ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace + - ${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw ## Uncomment the lines below to enable sandbox isolation ## (agents.defaults.sandbox). Requires Docker CLI in the image ## (build with --build-arg OPENCLAW_INSTALL_DOCKER_CLI=1) or use @@ -112,6 +113,7 @@ services: volumes: - ${OPENCLAW_CONFIG_DIR:-${HOME:-/tmp}/.openclaw}:/home/node/.openclaw - ${OPENCLAW_WORKSPACE_DIR:-${HOME:-/tmp}/.openclaw/workspace}:/home/node/.openclaw/workspace + - ${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw stdin_open: true tty: true init: true diff --git a/scripts/docker/setup.sh b/scripts/docker/setup.sh index b77bc2b1094b..7bf3316df1eb 100755 --- a/scripts/docker/setup.sh +++ b/scripts/docker/setup.sh @@ -240,9 +240,11 @@ fi OPENCLAW_CONFIG_DIR="${OPENCLAW_CONFIG_DIR:-$HOME/.openclaw}" OPENCLAW_WORKSPACE_DIR="${OPENCLAW_WORKSPACE_DIR:-$HOME/.openclaw/workspace}" +OPENCLAW_AUTH_PROFILE_SECRET_DIR="${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-$HOME/.openclaw-auth-profile-secrets}" validate_mount_path_value "OPENCLAW_CONFIG_DIR" "$OPENCLAW_CONFIG_DIR" validate_mount_path_value "OPENCLAW_WORKSPACE_DIR" "$OPENCLAW_WORKSPACE_DIR" +validate_mount_path_value "OPENCLAW_AUTH_PROFILE_SECRET_DIR" "$OPENCLAW_AUTH_PROFILE_SECRET_DIR" if [[ -n "$HOME_VOLUME_NAME" ]]; then if [[ "$HOME_VOLUME_NAME" == *"/"* ]]; then validate_mount_path_value "OPENCLAW_HOME_VOLUME" "$HOME_VOLUME_NAME" @@ -270,6 +272,7 @@ fi mkdir -p "$OPENCLAW_CONFIG_DIR" mkdir -p "$OPENCLAW_WORKSPACE_DIR" +mkdir -p "$OPENCLAW_AUTH_PROFILE_SECRET_DIR" # 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" @@ -278,6 +281,7 @@ mkdir -p "$OPENCLAW_CONFIG_DIR/agents/main/sessions" export OPENCLAW_CONFIG_DIR export OPENCLAW_WORKSPACE_DIR +export OPENCLAW_AUTH_PROFILE_SECRET_DIR export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}" export OPENCLAW_BRIDGE_PORT="${OPENCLAW_BRIDGE_PORT:-18790}" export OPENCLAW_GATEWAY_BIND="${OPENCLAW_GATEWAY_BIND:-lan}" @@ -343,6 +347,7 @@ write_extra_compose() { local gateway_home_mount local gateway_config_mount local gateway_workspace_mount + local gateway_auth_profile_secret_mount cat >"$EXTRA_COMPOSE_FILE" <<'YAML' services: @@ -354,12 +359,15 @@ YAML gateway_home_mount="${home_volume}:/home/node" gateway_config_mount="${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw" gateway_workspace_mount="${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace" + gateway_auth_profile_secret_mount="${OPENCLAW_AUTH_PROFILE_SECRET_DIR}:/home/node/.config/openclaw" validate_mount_spec "$gateway_home_mount" validate_mount_spec "$gateway_config_mount" validate_mount_spec "$gateway_workspace_mount" + validate_mount_spec "$gateway_auth_profile_secret_mount" printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_auth_profile_secret_mount" >>"$EXTRA_COMPOSE_FILE" fi for mount in "$@"; do @@ -376,6 +384,7 @@ YAML printf ' - %s\n' "$gateway_home_mount" >>"$EXTRA_COMPOSE_FILE" printf ' - %s\n' "$gateway_config_mount" >>"$EXTRA_COMPOSE_FILE" printf ' - %s\n' "$gateway_workspace_mount" >>"$EXTRA_COMPOSE_FILE" + printf ' - %s\n' "$gateway_auth_profile_secret_mount" >>"$EXTRA_COMPOSE_FILE" fi for mount in "$@"; do @@ -473,6 +482,7 @@ upsert_env() { upsert_env "$ENV_FILE" \ OPENCLAW_CONFIG_DIR \ OPENCLAW_WORKSPACE_DIR \ + OPENCLAW_AUTH_PROFILE_SECRET_DIR \ OPENCLAW_GATEWAY_PORT \ OPENCLAW_BRIDGE_PORT \ OPENCLAW_GATEWAY_BIND \ @@ -532,6 +542,7 @@ echo "==> Fixing data-directory permissions" # (.openclaw/) inside the workspace gets chowned, not the user's project files. run_prestart_gateway --user root --entrypoint sh openclaw-gateway -c \ 'find /home/node/.openclaw -xdev -exec chown node:node {} +; \ + find /home/node/.config/openclaw -xdev -exec chown node:node {} +; \ [ -d /home/node/.openclaw/workspace/.openclaw ] && chown -R node:node /home/node/.openclaw/workspace/.openclaw || true' echo "" diff --git a/src/docker-setup.e2e.test.ts b/src/docker-setup.e2e.test.ts index 56a3b637f725..f4640285ae32 100644 --- a/src/docker-setup.e2e.test.ts +++ b/src/docker-setup.e2e.test.ts @@ -104,6 +104,7 @@ function createEnv( OPENCLAW_GATEWAY_TOKEN: "test-token", OPENCLAW_CONFIG_DIR: join(sandbox.rootDir, "config"), OPENCLAW_WORKSPACE_DIR: join(sandbox.rootDir, "openclaw"), + OPENCLAW_AUTH_PROFILE_SECRET_DIR: join(sandbox.rootDir, "auth-profile-secrets"), }; for (const [key, value] of Object.entries(overrides)) { @@ -258,11 +259,17 @@ describe("scripts/docker/setup.sh", () => { expect(envFile).toContain("OPENCLAW_EXTRA_MOUNTS="); expect(envFile).toContain("OPENCLAW_HOME_VOLUME=openclaw-home"); // pragma: allowlist secret expect(envFile).toContain("OPENCLAW_DISABLE_BONJOUR="); + expect(envFile).toContain( + `OPENCLAW_AUTH_PROFILE_SECRET_DIR=${join(activeSandbox.rootDir, "auth-profile-secrets")}`, + ); const extraCompose = await readFile( join(activeSandbox.rootDir, "docker-compose.extra.yml"), "utf8", ); expect(extraCompose).toContain("openclaw-home:/home/node"); + expect(extraCompose).toContain( + `${join(activeSandbox.rootDir, "auth-profile-secrets")}:/home/node/.config/openclaw`, + ); expect(extraCompose).toContain("volumes:"); expect(extraCompose).toContain("openclaw-home:"); const log = await readDockerLog(activeSandbox); @@ -383,6 +390,27 @@ describe("scripts/docker/setup.sh", () => { expect(log).toContain("run --rm --no-deps --user root --entrypoint sh openclaw-gateway -c"); }); + it("precreates auth profile secret key dir outside the mounted state dir", async () => { + const activeSandbox = requireSandbox(sandbox); + const configDir = join(activeSandbox.rootDir, "config-auth-profile-key"); + const workspaceDir = join(activeSandbox.rootDir, "workspace-auth-profile-key"); + const secretDir = join(activeSandbox.rootDir, "auth-profile-secret-key"); + + const result = runDockerSetup(activeSandbox, { + OPENCLAW_CONFIG_DIR: configDir, + OPENCLAW_WORKSPACE_DIR: workspaceDir, + OPENCLAW_AUTH_PROFILE_SECRET_DIR: secretDir, + }); + + expect(result.status).toBe(0); + const secretDirStat = await stat(secretDir); + expect(secretDirStat.isDirectory()).toBe(true); + expect(secretDir.startsWith(`${configDir}/`)).toBe(false); + + const log = await readDockerLog(activeSandbox); + expect(log).toContain("find /home/node/.config/openclaw -xdev"); + }); + it("reuses existing config token when OPENCLAW_GATEWAY_TOKEN is unset", async () => { const activeSandbox = requireSandbox(sandbox); const { result, envFile } = await runDockerSetupWithUnsetGatewayToken( @@ -645,6 +673,15 @@ describe("scripts/docker/setup.sh", () => { ); }); + it("keeps docker-compose auth profile secret key source durable outside state", async () => { + const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8"); + expect( + compose.split( + "${OPENCLAW_AUTH_PROFILE_SECRET_DIR:-${HOME:-/tmp}/.openclaw-auth-profile-secrets}:/home/node/.config/openclaw", + ), + ).toHaveLength(3); + }); + it("keeps docker-compose optional env files aligned across services", async () => { const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8"); expect(compose.match(/env_file:\n {6}- path: \.env\n {8}required: false/g)).toHaveLength(2);