diff --git a/Dockerfile b/Dockerfile index 70815cc74..54a8fe1b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -216,6 +216,35 @@ RUN mkdir -p /sandbox/.openclaw-data/logs \ fi; \ done +# Ensure exec approvals path compatibility when using a stale published base +# image that still points to ~/.openclaw/exec-approvals.json. +RUN OPENCLAW_DIST_DIR="$(npm root -g)/openclaw/dist" \ + && if [ ! -d "$OPENCLAW_DIST_DIR" ]; then \ + echo "Error: OpenClaw dist directory not found: $OPENCLAW_DIST_DIR"; \ + exit 1; \ + fi \ + && mkdir -p /sandbox/.openclaw-data \ + && chown sandbox:sandbox /sandbox/.openclaw-data \ + && chmod 755 /sandbox/.openclaw-data \ + && LEGACY_EXEC_APPROVALS_PATH="$(printf '%b' '\176/.openclaw/exec-approvals.json')" \ + && DATA_EXEC_APPROVALS_PATH="$(printf '%b' '\176/.openclaw-data/exec-approvals.json')" \ + && files_with_old_path="$(grep -R --include='*.js' -l "$LEGACY_EXEC_APPROVALS_PATH" "$OPENCLAW_DIST_DIR" || true)" \ + && if [ -n "$files_with_old_path" ]; then \ + files_with_old_path_file="$(mktemp)"; \ + printf '%s\n' "$files_with_old_path" > "$files_with_old_path_file"; \ + while IFS= read -r file; do \ + sed -i "s#${LEGACY_EXEC_APPROVALS_PATH}#${DATA_EXEC_APPROVALS_PATH}#g" "$file"; \ + done < "$files_with_old_path_file"; \ + rm -f "$files_with_old_path_file"; \ + elif ! grep -R --include='*.js' -q "$DATA_EXEC_APPROVALS_PATH" "$OPENCLAW_DIST_DIR"; then \ + echo "Error: Unable to verify OpenClaw exec approvals path in dist"; \ + exit 1; \ + fi \ + && if grep -R --include='*.js' -n "$LEGACY_EXEC_APPROVALS_PATH" "$OPENCLAW_DIST_DIR"; then \ + echo "Error: OpenClaw exec approvals path patch failed"; \ + exit 1; \ + fi + RUN chown root:root /sandbox/.openclaw \ && rm -rf /root/.npm /sandbox/.npm \ && find /sandbox/.openclaw -mindepth 1 -maxdepth 1 -exec chown -h root:root {} + \ diff --git a/Dockerfile.base b/Dockerfile.base index 53e10009e..02ee4f648 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -154,6 +154,10 @@ ARG OPENCLAW_VERSION=2026.4.2 SHELL ["/bin/bash", "-o", "pipefail", "-c"] +# OpenClaw rejects symlink writes for exec approvals, but NemoClaw keeps +# /sandbox/.openclaw immutable and redirects writable state to .openclaw-data. +# Patch OpenClaw dist defaults so approvals write directly to .openclaw-data. +# Ref: https://github.com/NVIDIA/NemoClaw/issues/1785 RUN --mount=type=bind,source=nemoclaw-blueprint/blueprint.yaml,target=/tmp/blueprint.yaml \ echo "$OPENCLAW_VERSION" | grep -qxE '[0-9]+(\.[0-9]+)*' \ || { echo "Error: OPENCLAW_VERSION='$OPENCLAW_VERSION' is invalid (expected e.g. 2026.3.11)."; exit 1; }; \ @@ -169,4 +173,27 @@ RUN --mount=type=bind,source=nemoclaw-blueprint/blueprint.yaml,target=/tmp/bluep echo "Hint: Check available versions with: npm view openclaw versions"; exit 1; \ fi; \ npm install -g "openclaw@${OPENCLAW_VERSION}" \ + && OPENCLAW_DIST_DIR="$(npm root -g)/openclaw/dist" \ + && if [ ! -d "$OPENCLAW_DIST_DIR" ]; then \ + echo "Error: OpenClaw dist directory not found: $OPENCLAW_DIST_DIR"; \ + exit 1; \ + fi \ + && LEGACY_EXEC_APPROVALS_PATH="$(printf '%b' '\176/.openclaw/exec-approvals.json')" \ + && DATA_EXEC_APPROVALS_PATH="$(printf '%b' '\176/.openclaw-data/exec-approvals.json')" \ + && files_with_old_path="$(grep -R --include='*.js' -l "$LEGACY_EXEC_APPROVALS_PATH" "$OPENCLAW_DIST_DIR" || true)" \ + && if [ -n "$files_with_old_path" ]; then \ + files_with_old_path_file="$(mktemp)"; \ + printf '%s\n' "$files_with_old_path" > "$files_with_old_path_file"; \ + while IFS= read -r file; do \ + sed -i "s#${LEGACY_EXEC_APPROVALS_PATH}#${DATA_EXEC_APPROVALS_PATH}#g" "$file"; \ + done < "$files_with_old_path_file"; \ + rm -f "$files_with_old_path_file"; \ + elif ! grep -R --include='*.js' -q "$DATA_EXEC_APPROVALS_PATH" "$OPENCLAW_DIST_DIR"; then \ + echo "Error: Unable to verify OpenClaw exec approvals path in dist"; \ + exit 1; \ + fi \ + && if grep -R --include='*.js' -n "$LEGACY_EXEC_APPROVALS_PATH" "$OPENCLAW_DIST_DIR"; then \ + echo "Error: OpenClaw exec approvals path patch failed"; \ + exit 1; \ + fi \ && pip3 install --no-cache-dir --break-system-packages "pyyaml==6.0.3" diff --git a/test/exec-approvals-path-regression.test.ts b/test/exec-approvals-path-regression.test.ts new file mode 100644 index 000000000..516c0c28a --- /dev/null +++ b/test/exec-approvals-path-regression.test.ts @@ -0,0 +1,38 @@ +// @ts-nocheck +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("exec approvals path regression guard", () => { + it("Dockerfile.base patches and validates OpenClaw exec approvals path across dist bundles", () => { + const dockerfileBase = path.join(import.meta.dirname, "..", "Dockerfile.base"); + const src = fs.readFileSync(dockerfileBase, "utf-8"); + + expect(src).toContain('LEGACY_EXEC_APPROVALS_PATH="$(printf \'%b\''); + expect(src).toContain('DATA_EXEC_APPROVALS_PATH="$(printf \'%b\''); + expect(src).toContain('files_with_old_path_file="$(mktemp)"'); + expect(src).toContain("--include='*.js'"); + expect(src).toContain("OpenClaw dist directory not found:"); + expect(src).toContain("Unable to verify OpenClaw exec approvals path in dist"); + }); + + it("Dockerfile applies a runtime compatibility patch for stale base images", () => { + const dockerfile = path.join(import.meta.dirname, "..", "Dockerfile"); + const src = fs.readFileSync(dockerfile, "utf-8"); + + expect(src).toContain('[ ! -d "$OPENCLAW_DIST_DIR" ]'); + expect(src).toContain("mkdir -p /sandbox/.openclaw-data"); + expect(src).toContain("chown sandbox:sandbox /sandbox/.openclaw-data"); + expect(src).toContain("chmod 755 /sandbox/.openclaw-data"); + expect(src).toContain('LEGACY_EXEC_APPROVALS_PATH="$(printf \'%b\''); + expect(src).toContain('DATA_EXEC_APPROVALS_PATH="$(printf \'%b\''); + expect(src).toContain('files_with_old_path_file="$(mktemp)"'); + expect(src).toContain("--include='*.js'"); + expect(src).toContain("Unable to verify OpenClaw exec approvals path in dist"); + expect(src).toContain("OpenClaw dist directory not found:"); + expect(src).toContain("OpenClaw exec approvals path patch failed"); + }); +});