Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,32 @@ 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 \
&& 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 {} + \
Expand Down
27 changes: 27 additions & 0 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -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; }; \
Expand All @@ -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"
35 changes: 35 additions & 0 deletions test/exec-approvals-path-regression.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// @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('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");
});
});