From 863f8d22394843a210d7908fc5b76a5956fb51f4 Mon Sep 17 00:00:00 2001 From: San Dang Date: Mon, 10 Aug 2026 13:38:43 +0700 Subject: [PATCH 1/5] fix(images): normalize managed runtime bundle metadata Signed-off-by: San Dang --- Dockerfile | 53 ++++++++++++------- agents/hermes/Dockerfile | 53 ++++++++++++------- agents/langchain-deepagents-code/Dockerfile | 35 +++++++----- test/hermes-final-image-layout.test.ts | 4 +- test/langchain-deepagents-code-image.test.ts | 5 +- .../mcp-tool-discovery-image-contract.test.ts | 9 ++-- test/openclaw-final-image-layout.test.ts | 4 +- .../managed-bootstrap-image-contract.ts | 22 +++++++- 8 files changed, 127 insertions(+), 58 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8526c8bdda6..83a9e3f0c1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,11 +53,12 @@ RUN ln -s /opt/nemoclaw/node_modules /opt/nemoclaw-root/node_modules \ # graph during image assembly. Protected rebuilds remain network-free and the # final image still receives only the generated bundles. FROM scratch AS mcp-tool-discovery-runtime -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs +# Normalize bundle metadata so the build-context umask cannot make reviewed runtime files writable. +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs FROM scratch AS managed-startup-runtime-builder -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs # Compile the bootstrap boundary on the target platform. The output is a # freestanding static ELF; only its reviewed, non-executable Bash body remains @@ -1502,21 +1503,37 @@ RUN managed_runtime_assertion_failed() { \ exit 1; \ }; \ discovery_contract="$(node /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs)" \ - && node -e "const result = JSON.parse(process.argv[1]); if (result.protocol !== 1 || result.ok !== false || result.detail !== \"tool discovery received invalid runtime arguments\") process.exit(1);" "$discovery_contract" \ - && discovery_unsafe="$(find -L /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime \( ! -user root -o -perm /022 \) -print -quit)" \ - && test -z "$discovery_unsafe" \ - && { test -f /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs || managed_runtime_assertion_failed regular-file /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { test ! -L /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs || managed_runtime_assertion_failed non-symlink /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { chown root:root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null || managed_runtime_assertion_failed owner-root-root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { chmod 0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null || managed_runtime_assertion_failed mode-0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null)" = '0:0:444' || managed_runtime_assertion_failed metadata-0:0:444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && test -f /usr/local/bin/nemoclaw-managed-bootstrap \ - && test ! -L /usr/local/bin/nemoclaw-managed-bootstrap \ - && test "$(stat -c '%u:%g:%a' /usr/local/bin/nemoclaw-managed-bootstrap)" = '0:0:755' \ - && test -f /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ - && test ! -L /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ - && test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh)" = '0:0:444' \ - && install -d -o root -g root -m 0755 /run/nemoclaw + || managed_runtime_assertion_failed discovery-exec /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs; \ + node -e "const result = JSON.parse(process.argv[1]); if (result.protocol !== 1 || result.ok !== false || result.detail !== \"tool discovery received invalid runtime arguments\") process.exit(1);" "$discovery_contract" \ + || managed_runtime_assertion_failed discovery-contract /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs; \ + discovery_unsafe="$(find -L /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime \( ! -user root -o -perm /022 \) -print -quit)" \ + || managed_runtime_assertion_failed discovery-tree-scan /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime; \ + test -z "$discovery_unsafe" \ + || managed_runtime_assertion_failed discovery-tree-owner-mode "$discovery_unsafe"; \ + test -f /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs \ + || managed_runtime_assertion_failed regular-file /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + test ! -L /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs \ + || managed_runtime_assertion_failed non-symlink /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + chown root:root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null \ + || managed_runtime_assertion_failed owner-root-root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + chmod 0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null \ + || managed_runtime_assertion_failed mode-0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null)" = '0:0:444' \ + || managed_runtime_assertion_failed metadata-0:0:444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + test -f /usr/local/bin/nemoclaw-managed-bootstrap \ + || managed_runtime_assertion_failed managed-bootstrap-regular-file /usr/local/bin/nemoclaw-managed-bootstrap; \ + test ! -L /usr/local/bin/nemoclaw-managed-bootstrap \ + || managed_runtime_assertion_failed managed-bootstrap-non-symlink /usr/local/bin/nemoclaw-managed-bootstrap; \ + test "$(stat -c '%u:%g:%a' /usr/local/bin/nemoclaw-managed-bootstrap)" = '0:0:755' \ + || managed_runtime_assertion_failed managed-bootstrap-metadata-0:0:755 /usr/local/bin/nemoclaw-managed-bootstrap; \ + test -f /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ + || managed_runtime_assertion_failed managed-bootstrap-trampoline-regular-file /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh; \ + test ! -L /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ + || managed_runtime_assertion_failed managed-bootstrap-trampoline-non-symlink /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh; \ + test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh)" = '0:0:444' \ + || managed_runtime_assertion_failed managed-bootstrap-trampoline-metadata-0:0:444 /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh; \ + install -d -o root -g root -m 0755 /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw # Copy startup script and shared sandbox initialisation library. RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-codex-acp \ diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile index 307fb207287..d7dba0be0a0 100644 --- a/agents/hermes/Dockerfile +++ b/agents/hermes/Dockerfile @@ -13,11 +13,12 @@ ARG NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=0 # The reviewed npm graph is audited in CI; image assembly copies only its # generated runtime artifacts and therefore needs neither npm nor network. FROM scratch AS mcp-tool-discovery-runtime -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs +# Normalize bundle metadata so the build-context umask cannot make reviewed runtime files writable. +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs FROM scratch AS managed-startup-runtime-builder -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs # Compile the bootstrap boundary on the target platform. The output is a # freestanding static ELF; only its reviewed, non-executable Bash body remains @@ -364,21 +365,37 @@ RUN managed_runtime_assertion_failed() { \ exit 1; \ }; \ discovery_contract="$(node /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs)" \ - && node -e 'const result = JSON.parse(process.argv[1]); if (result.protocol !== 1 || result.ok !== false || result.detail !== "tool discovery received invalid runtime arguments") process.exit(1);' "$discovery_contract" \ - && discovery_unsafe="$(find -L /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime \( ! -user root -o -perm /022 \) -print -quit)" \ - && test -z "$discovery_unsafe" \ - && { test -f /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs || managed_runtime_assertion_failed regular-file /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { test ! -L /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs || managed_runtime_assertion_failed non-symlink /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { chown root:root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null || managed_runtime_assertion_failed owner-root-root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { chmod 0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null || managed_runtime_assertion_failed mode-0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null)" = '0:0:444' || managed_runtime_assertion_failed metadata-0:0:444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && test -f /usr/local/bin/nemoclaw-managed-bootstrap \ - && test ! -L /usr/local/bin/nemoclaw-managed-bootstrap \ - && test "$(stat -c '%u:%g:%a' /usr/local/bin/nemoclaw-managed-bootstrap)" = '0:0:755' \ - && test -f /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ - && test ! -L /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ - && test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh)" = '0:0:444' \ - && install -d -o root -g root -m 0755 /run/nemoclaw + || managed_runtime_assertion_failed discovery-exec /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs; \ + node -e 'const result = JSON.parse(process.argv[1]); if (result.protocol !== 1 || result.ok !== false || result.detail !== "tool discovery received invalid runtime arguments") process.exit(1);' "$discovery_contract" \ + || managed_runtime_assertion_failed discovery-contract /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs; \ + discovery_unsafe="$(find -L /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime \( ! -user root -o -perm /022 \) -print -quit)" \ + || managed_runtime_assertion_failed discovery-tree-scan /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime; \ + test -z "$discovery_unsafe" \ + || managed_runtime_assertion_failed discovery-tree-owner-mode "$discovery_unsafe"; \ + test -f /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs \ + || managed_runtime_assertion_failed regular-file /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + test ! -L /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs \ + || managed_runtime_assertion_failed non-symlink /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + chown root:root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null \ + || managed_runtime_assertion_failed owner-root-root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + chmod 0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null \ + || managed_runtime_assertion_failed mode-0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null)" = '0:0:444' \ + || managed_runtime_assertion_failed metadata-0:0:444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + test -f /usr/local/bin/nemoclaw-managed-bootstrap \ + || managed_runtime_assertion_failed managed-bootstrap-regular-file /usr/local/bin/nemoclaw-managed-bootstrap; \ + test ! -L /usr/local/bin/nemoclaw-managed-bootstrap \ + || managed_runtime_assertion_failed managed-bootstrap-non-symlink /usr/local/bin/nemoclaw-managed-bootstrap; \ + test "$(stat -c '%u:%g:%a' /usr/local/bin/nemoclaw-managed-bootstrap)" = '0:0:755' \ + || managed_runtime_assertion_failed managed-bootstrap-metadata-0:0:755 /usr/local/bin/nemoclaw-managed-bootstrap; \ + test -f /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ + || managed_runtime_assertion_failed managed-bootstrap-trampoline-regular-file /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh; \ + test ! -L /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh \ + || managed_runtime_assertion_failed managed-bootstrap-trampoline-non-symlink /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh; \ + test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh)" = '0:0:444' \ + || managed_runtime_assertion_failed managed-bootstrap-trampoline-metadata-0:0:444 /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh; \ + install -d -o root -g root -m 0755 /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw # Ensure sandbox user can read blueprint files copied as root RUN chmod -R a+rX /opt/nemoclaw-blueprint/ diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index 914748ac335..c9eaec70cd8 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -11,11 +11,12 @@ ARG NEMOCLAW_CORPORATE_CA_B64= # The reviewed npm graph is audited in CI; image assembly copies only its # generated runtime artifacts and therefore needs neither npm nor network. FROM scratch AS mcp-tool-discovery-runtime -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs +# Normalize bundle metadata so the build-context umask cannot make reviewed runtime files writable. +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs FROM scratch AS managed-startup-runtime-builder -COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs +COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs # Compile the bootstrap boundary on the target platform. The output is a # freestanding static ELF; only its reviewed, non-executable Bash body remains @@ -114,15 +115,25 @@ RUN managed_runtime_assertion_failed() { \ exit 1; \ }; \ discovery_contract="$(node /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs)" \ - && node -e 'const result = JSON.parse(process.argv[1]); if (result.protocol !== 1 || result.ok !== false || result.detail !== "tool discovery received invalid runtime arguments") process.exit(1);' "$discovery_contract" \ - && discovery_unsafe="$(find -L /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime \( ! -user root -o -perm /022 \) -print -quit)" \ - && test -z "$discovery_unsafe" \ - && { test -f /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs || managed_runtime_assertion_failed regular-file /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { test ! -L /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs || managed_runtime_assertion_failed non-symlink /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { chown root:root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null || managed_runtime_assertion_failed owner-root-root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { chmod 0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null || managed_runtime_assertion_failed mode-0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && { test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null)" = '0:0:444' || managed_runtime_assertion_failed metadata-0:0:444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; } \ - && install -d -o root -g root -m 0755 /run/nemoclaw + || managed_runtime_assertion_failed discovery-exec /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs; \ + node -e 'const result = JSON.parse(process.argv[1]); if (result.protocol !== 1 || result.ok !== false || result.detail !== "tool discovery received invalid runtime arguments") process.exit(1);' "$discovery_contract" \ + || managed_runtime_assertion_failed discovery-contract /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs; \ + discovery_unsafe="$(find -L /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime \( ! -user root -o -perm /022 \) -print -quit)" \ + || managed_runtime_assertion_failed discovery-tree-scan /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime; \ + test -z "$discovery_unsafe" \ + || managed_runtime_assertion_failed discovery-tree-owner-mode "$discovery_unsafe"; \ + test -f /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs \ + || managed_runtime_assertion_failed regular-file /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + test ! -L /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs \ + || managed_runtime_assertion_failed non-symlink /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + chown root:root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null \ + || managed_runtime_assertion_failed owner-root-root /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + chmod 0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null \ + || managed_runtime_assertion_failed mode-0444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null)" = '0:0:444' \ + || managed_runtime_assertion_failed metadata-0:0:444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ + install -d -o root -g root -m 0755 /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts diff --git a/test/hermes-final-image-layout.test.ts b/test/hermes-final-image-layout.test.ts index a94567dd52d..ee7f2022e3d 100644 --- a/test/hermes-final-image-layout.test.ts +++ b/test/hermes-final-image-layout.test.ts @@ -393,7 +393,7 @@ describe("Hermes final image layout", () => { ); const managedRuntimeDirectory = indexOfRequired( finalStage, - "&& install -d -o root -g root -m 0755 /run/nemoclaw", + "install -d -o root -g root -m 0755 /run/nemoclaw", ); const runtimeModeReplay = indexOfRequired( finalStage, @@ -425,7 +425,7 @@ describe("Hermes final image layout", () => { expect(managedRuntimeDirectory).toBeLessThan(runtimeModeReplay); expect(finalStage).toContain("/usr/local/bin/nemoclaw-managed-bootstrap"); expect(dockerfile).toContain( - "COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs", + "COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs", ); expect(dockerfile).not.toContain( "COPY src/lib/onboard/managed-bootstrap/ ./src/lib/onboard/managed-bootstrap/", diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index c3e432f0f92..9572b76cd47 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -162,7 +162,8 @@ describe("LangChain Deep Agents Code image contracts", () => { "# explicitly before installing the root-owned managed-startup handoff.", "USER root", ].join("\n"); - const managedRuntimeDirectory = "&& install -d -o root -g root -m 0755 /run/nemoclaw"; + const managedRuntimeDirectory = + "install -d -o root -g root -m 0755 /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw"; const runtimeModeReplay = "&& chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts"; expect(dockerfile).toContain("ARG BASE_IMAGE\n"); @@ -201,7 +202,7 @@ describe("LangChain Deep Agents Code image contracts", () => { dockerfile.indexOf(runtimeModeReplay), ); expect(dockerfile).toContain( - "COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs", + "COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs", ); expect(dockerfile).not.toContain( "COPY src/lib/onboard/managed-bootstrap/ ./src/lib/onboard/managed-bootstrap/", diff --git a/test/mcp-tool-discovery-image-contract.test.ts b/test/mcp-tool-discovery-image-contract.test.ts index 3b9030eab95..0db876a7250 100644 --- a/test/mcp-tool-discovery-image-contract.test.ts +++ b/test/mcp-tool-discovery-image-contract.test.ts @@ -199,7 +199,7 @@ describe("MCP tool discovery image contract", () => { "COPY tools/mcp-tool-discovery-runtime/npm-cache-seed/ /usr/local/lib/nemoclaw-build-tools/npm-cache-seed/", ); expect(openClawDockerfile).toContain( - "COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs", + "COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs", ); expect(openClawDockerfile).not.toContain("mcp-runtime-npm-cache-seed/"); expect(openClawDockerfile).not.toContain("install-reviewed-runtime.sh"); @@ -434,10 +434,13 @@ describe("MCP tool discovery image contract", () => { const dockerfile = fs.readFileSync(path.join(repoRoot, relativePath), "utf8"); expect(dockerfile).toContain( - "COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs", + "COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs", ); expect(dockerfile).toContain( - "COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs", + "COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs", + ); + expect(dockerfile).toContain( + "COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/", ); expect(dockerfile).toContain( `COPY --from=mcp-tool-discovery-runtime /opt/mcp-tool-discovery-runtime/dist/ ${runtimeRoot}/`, diff --git a/test/openclaw-final-image-layout.test.ts b/test/openclaw-final-image-layout.test.ts index 16230300267..9ec4ccc8cf6 100644 --- a/test/openclaw-final-image-layout.test.ts +++ b/test/openclaw-final-image-layout.test.ts @@ -222,7 +222,7 @@ describe("OpenClaw final image layout", () => { ); const managedRuntimeDirectory = indexOfRequired( finalStage, - "&& install -d -o root -g root -m 0755 /run/nemoclaw", + "install -d -o root -g root -m 0755 /run/nemoclaw", ); const runtimeChmod = indexOfRequired(finalStage, "RUN chmod 755 /usr/local/bin/nemoclaw-start"); @@ -247,7 +247,7 @@ describe("OpenClaw final image layout", () => { expect(managedRuntimeDirectory).toBeLessThan(runtimeChmod); expect(finalStage).toContain("/usr/local/bin/nemoclaw-managed-bootstrap"); expect(dockerfile).toContain( - "COPY tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs", + "COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /out/managed-startup-image-runtime.cjs", ); expect(dockerfile).not.toContain( "COPY src/lib/onboard/managed-bootstrap/ ./src/lib/onboard/managed-bootstrap/", diff --git a/test/support/managed-bootstrap-image-contract.ts b/test/support/managed-bootstrap-image-contract.ts index 4b12be7c2ef..04e01f6246d 100644 --- a/test/support/managed-bootstrap-image-contract.ts +++ b/test/support/managed-bootstrap-image-contract.ts @@ -31,6 +31,8 @@ const COMPILER_FLAGS = [ const MANAGED_BOOTSTRAP_BUILDER_IMAGE = "node:22-trixie@sha256:a566dd560283ae5615c8bb86b58fa8a1b6f3c82b492473a061672416266625da"; +const DISCOVERY_RUNTIME_ROOT = "/usr/local/lib/nemoclaw/mcp-tool-discovery-runtime"; +const DISCOVERY_RUNTIME_PATH = `${DISCOVERY_RUNTIME_ROOT}/mcp-tool-discovery.mjs`; const MANAGED_STARTUP_RUNTIME_PATH = "/usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs"; function expectManagedRuntimeDiagnostic(dockerfile: string): void { @@ -40,7 +42,9 @@ function expectManagedRuntimeDiagnostic(dockerfile: string): void { instruction.body.includes("managed_runtime_assertion_failed()"), ); expect(instructions).toHaveLength(1); - const logicalInstruction = (instructions[0]?.body ?? "").replace(/\\\r?\n[ \t]*/gu, " "); + const logicalInstruction = (instructions[0]?.body ?? "") + .replace(/\\\r?\n[ \t]*/gu, " ") + .replace(/[ \t]+/gu, " "); const discoveryStart = logicalInstruction.indexOf("discovery_contract="); expect(discoveryStart).toBeGreaterThan(0); const functionSource = logicalInstruction.slice(0, discoveryStart).trim(); @@ -54,11 +58,16 @@ function expectManagedRuntimeDiagnostic(dockerfile: string): void { } for (const assertion of [ + `discovery_contract="$(node ${DISCOVERY_RUNTIME_PATH})" || managed_runtime_assertion_failed discovery-exec ${DISCOVERY_RUNTIME_PATH}`, + `managed_runtime_assertion_failed discovery-contract ${DISCOVERY_RUNTIME_PATH}`, + `managed_runtime_assertion_failed discovery-tree-scan ${DISCOVERY_RUNTIME_ROOT}`, + 'managed_runtime_assertion_failed discovery-tree-owner-mode "$discovery_unsafe"', `test -f ${MANAGED_STARTUP_RUNTIME_PATH} || managed_runtime_assertion_failed regular-file ${MANAGED_STARTUP_RUNTIME_PATH}`, `test ! -L ${MANAGED_STARTUP_RUNTIME_PATH} || managed_runtime_assertion_failed non-symlink ${MANAGED_STARTUP_RUNTIME_PATH}`, `chown root:root ${MANAGED_STARTUP_RUNTIME_PATH} 2>/dev/null || managed_runtime_assertion_failed owner-root-root ${MANAGED_STARTUP_RUNTIME_PATH}`, `chmod 0444 ${MANAGED_STARTUP_RUNTIME_PATH} 2>/dev/null || managed_runtime_assertion_failed mode-0444 ${MANAGED_STARTUP_RUNTIME_PATH}`, `test \"$(stat -c '%u:%g:%a' ${MANAGED_STARTUP_RUNTIME_PATH} 2>/dev/null)\" = '0:0:444' || managed_runtime_assertion_failed metadata-0:0:444 ${MANAGED_STARTUP_RUNTIME_PATH}`, + "install -d -o root -g root -m 0755 /run/nemoclaw || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw", ]) { expect(logicalInstruction.split(assertion)).toHaveLength(2); } @@ -109,6 +118,17 @@ function expectManagedRuntimeDiagnostic(dockerfile: string): void { expect(symlink.stderr).toBe( `ERROR: managed image assertion failed: non-symlink path=${linkPath} uid=0 gid=0 type=symbolic link mode=777 symlink=yes\n`, ); + + const writable = runDiagnostic( + targetPath, + "discovery-tree-owner-mode", + "uid=0 gid=0 type=regular file mode=664", + ); + expect(writable.status).toBe(1); + expect(writable.stdout).toBe(""); + expect(writable.stderr).toBe( + `ERROR: managed image assertion failed: discovery-tree-owner-mode path=${targetPath} uid=0 gid=0 type=regular file mode=664 symlink=no\n`, + ); } finally { fs.rmSync(tmp, { force: true, recursive: true }); } From dd3d98ca1ad63900f330d7f27a0cf113e462326d Mon Sep 17 00:00:00 2001 From: San Dang Date: Mon, 10 Aug 2026 13:53:04 +0700 Subject: [PATCH 2/5] test(images): exercise writable bundle metadata Signed-off-by: San Dang --- .github/workflows/managed-images.yaml | 10 ++++++++ Dockerfile | 2 +- agents/hermes/Dockerfile | 2 +- agents/langchain-deepagents-code/Dockerfile | 2 +- ci/source-shape-test-budget.json | 5 ++++ .../mcp-tool-discovery-image-contract.test.ts | 23 +++++++++++++++++++ 6 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index c66ffa61b82..348d0b11370 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -94,6 +94,16 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} persist-credentials: false + - name: Make reviewed runtime bundle files group-writable + shell: bash + run: | + set -euo pipefail + chmod 0664 \ + tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle \ + tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json \ + tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt \ + tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle + - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 diff --git a/Dockerfile b/Dockerfile index 83a9e3f0c1d..b61a328c630 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,7 +53,7 @@ RUN ln -s /opt/nemoclaw/node_modules /opt/nemoclaw-root/node_modules \ # graph during image assembly. Protected rebuilds remain network-free and the # final image still receives only the generated bundles. FROM scratch AS mcp-tool-discovery-runtime -# Normalize bundle metadata so the build-context umask cannot make reviewed runtime files writable. +# Set root ownership and read-only modes because scratch-stage COPY preserves source metadata. COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile index d7dba0be0a0..600d649ee15 100644 --- a/agents/hermes/Dockerfile +++ b/agents/hermes/Dockerfile @@ -13,7 +13,7 @@ ARG NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION=0 # The reviewed npm graph is audited in CI; image assembly copies only its # generated runtime artifacts and therefore needs neither npm nor network. FROM scratch AS mcp-tool-discovery-runtime -# Normalize bundle metadata so the build-context umask cannot make reviewed runtime files writable. +# Set root ownership and read-only modes because scratch-stage COPY preserves source metadata. COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index c9eaec70cd8..e840bac5697 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -11,7 +11,7 @@ ARG NEMOCLAW_CORPORATE_CA_B64= # The reviewed npm graph is audited in CI; image assembly copies only its # generated runtime artifacts and therefore needs neither npm nor network. FROM scratch AS mcp-tool-discovery-runtime -# Normalize bundle metadata so the build-context umask cannot make reviewed runtime files writable. +# Set root ownership and read-only modes because scratch-stage COPY preserves source metadata. COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /opt/mcp-tool-discovery-runtime/dist/ COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /opt/mcp-tool-discovery-runtime/dist/mcp-tool-discovery.mjs diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json index 7ae023b4644..02299290418 100644 --- a/ci/source-shape-test-budget.json +++ b/ci/source-shape-test-budget.json @@ -286,6 +286,11 @@ "test": "%s keeps the full plan in build processes but not final runtime environments (#5896)", "category": "security" }, + { + "file": "test/mcp-tool-discovery-image-contract.test.ts", + "test": "builds managed images when reviewed runtime bundle files are group-writable (#8665)", + "category": "security" + }, { "file": "test/mcp-tool-discovery-image-contract.test.ts", "test": "pins reviewed packages and audits their lock outside image builds (#8253)", diff --git a/test/mcp-tool-discovery-image-contract.test.ts b/test/mcp-tool-discovery-image-contract.test.ts index 0db876a7250..7a9f455f150 100644 --- a/test/mcp-tool-discovery-image-contract.test.ts +++ b/test/mcp-tool-discovery-image-contract.test.ts @@ -11,12 +11,19 @@ import { stripVTControlCharacters } from "node:util"; import { describe, expect, it } from "vitest"; const repoRoot = path.join(import.meta.dirname, ".."); +const managedImagesWorkflowPath = path.join(repoRoot, ".github/workflows/managed-images.yaml"); const runtimeRoot = "/usr/local/lib/nemoclaw/mcp-tool-discovery-runtime"; const dockerfiles = [ "Dockerfile", "agents/hermes/Dockerfile", "agents/langchain-deepagents-code/Dockerfile", ] as const; +const reviewedRuntimeBundleFiles = [ + "tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle", + "tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json", + "tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt", + "tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle", +] as const; function createCacheSeedFixture(): { cache: string; @@ -103,6 +110,22 @@ function createCacheSeedFixture(): { } describe("MCP tool discovery image contract", () => { + // source-shape-contract: security -- The PR image build must reproduce group-writable inputs before validating the immutable bundle boundary + it("builds managed images when reviewed runtime bundle files are group-writable (#8665)", () => { + const workflow = fs.readFileSync(managedImagesWorkflowPath, "utf8"); + const metadataStep = workflow.indexOf( + "- name: Make reviewed runtime bundle files group-writable", + ); + const buildStep = workflow.indexOf("- name: Build PR managed image locally"); + + expect(metadataStep).toBeGreaterThan(0); + expect(metadataStep).toBeLessThan(buildStep); + expect(workflow.slice(metadataStep, buildStep)).toContain("chmod 0664 \\"); + for (const bundleFile of reviewedRuntimeBundleFiles) { + expect(workflow.slice(metadataStep, buildStep)).toContain(bundleFile); + } + }); + // source-shape-contract: security -- Exact package pins and the CI audit mapping protect the shipped runtime graph it("pins reviewed packages and audits their lock outside image builds (#8253)", () => { const packageRoot = path.join(repoRoot, "tools", "mcp-tool-discovery-runtime"); From 6e4d1ec80229babb7158fb23fa952ebe11c82e98 Mon Sep 17 00:00:00 2001 From: San Dang Date: Mon, 10 Aug 2026 16:42:30 +0700 Subject: [PATCH 3/5] fix(images): reject unsafe runtime paths Signed-off-by: San Dang --- .github/workflows/managed-images.yaml | 16 ++- Dockerfile | 8 +- agents/hermes/Dockerfile | 8 +- agents/langchain-deepagents-code/Dockerfile | 8 +- test/hermes-final-image-layout.test.ts | 2 +- test/langchain-deepagents-code-image.test.ts | 2 +- .../mcp-tool-discovery-image-contract.test.ts | 99 +++++++++++++++++-- test/openclaw-final-image-layout.test.ts | 2 +- .../managed-bootstrap-image-contract.ts | 20 +++- 9 files changed, 145 insertions(+), 20 deletions(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index 348d0b11370..e0e4bc91001 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -98,11 +98,19 @@ jobs: shell: bash run: | set -euo pipefail - chmod 0664 \ - tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle \ - tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json \ - tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt \ + runtime_bundle_files=( + tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle + tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json + tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle + ) + for path in "${runtime_bundle_files[@]}"; do + if [[ ! -f "$path" || -L "$path" ]]; then + printf 'ERROR: reviewed runtime bundle input must be a regular non-symlink: %s\n' "$path" >&2 + exit 1 + fi + done + chmod 0664 "${runtime_bundle_files[@]}" - name: Set up Docker Buildx uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 diff --git a/Dockerfile b/Dockerfile index b61a328c630..2fb7385d04d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1533,7 +1533,13 @@ RUN managed_runtime_assertion_failed() { \ test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh)" = '0:0:444' \ || managed_runtime_assertion_failed managed-bootstrap-trampoline-metadata-0:0:444 /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh; \ install -d -o root -g root -m 0755 /run/nemoclaw \ - || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw + || managed_runtime_assertion_failed runtime-directory-create /run/nemoclaw; \ + test -d /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory /run/nemoclaw; \ + test ! -L /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory-non-symlink /run/nemoclaw; \ + test "$(stat -c '%u:%g:%a' /run/nemoclaw 2>/dev/null)" = '0:0:755' \ + || managed_runtime_assertion_failed runtime-directory-metadata-0:0:755 /run/nemoclaw # Copy startup script and shared sandbox initialisation library. RUN chmod 755 /usr/local/bin/nemoclaw-start /usr/local/bin/nemoclaw-codex-acp \ diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile index 600d649ee15..2e8b2ef5206 100644 --- a/agents/hermes/Dockerfile +++ b/agents/hermes/Dockerfile @@ -395,7 +395,13 @@ RUN managed_runtime_assertion_failed() { \ test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh)" = '0:0:444' \ || managed_runtime_assertion_failed managed-bootstrap-trampoline-metadata-0:0:444 /usr/local/lib/nemoclaw/managed-bootstrap-trampoline.sh; \ install -d -o root -g root -m 0755 /run/nemoclaw \ - || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw + || managed_runtime_assertion_failed runtime-directory-create /run/nemoclaw; \ + test -d /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory /run/nemoclaw; \ + test ! -L /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory-non-symlink /run/nemoclaw; \ + test "$(stat -c '%u:%g:%a' /run/nemoclaw 2>/dev/null)" = '0:0:755' \ + || managed_runtime_assertion_failed runtime-directory-metadata-0:0:755 /run/nemoclaw # Ensure sandbox user can read blueprint files copied as root RUN chmod -R a+rX /opt/nemoclaw-blueprint/ diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile index e840bac5697..c67103d645a 100644 --- a/agents/langchain-deepagents-code/Dockerfile +++ b/agents/langchain-deepagents-code/Dockerfile @@ -133,7 +133,13 @@ RUN managed_runtime_assertion_failed() { \ test "$(stat -c '%u:%g:%a' /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs 2>/dev/null)" = '0:0:444' \ || managed_runtime_assertion_failed metadata-0:0:444 /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs; \ install -d -o root -g root -m 0755 /run/nemoclaw \ - || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw + || managed_runtime_assertion_failed runtime-directory-create /run/nemoclaw; \ + test -d /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory /run/nemoclaw; \ + test ! -L /run/nemoclaw \ + || managed_runtime_assertion_failed runtime-directory-non-symlink /run/nemoclaw; \ + test "$(stat -c '%u:%g:%a' /run/nemoclaw 2>/dev/null)" = '0:0:755' \ + || managed_runtime_assertion_failed runtime-directory-metadata-0:0:755 /run/nemoclaw COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts diff --git a/test/hermes-final-image-layout.test.ts b/test/hermes-final-image-layout.test.ts index ee7f2022e3d..125ef042679 100644 --- a/test/hermes-final-image-layout.test.ts +++ b/test/hermes-final-image-layout.test.ts @@ -393,7 +393,7 @@ describe("Hermes final image layout", () => { ); const managedRuntimeDirectory = indexOfRequired( finalStage, - "install -d -o root -g root -m 0755 /run/nemoclaw", + "install -d -o root -g root -m 0755 /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory-create /run/nemoclaw; \\\n test -d /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory /run/nemoclaw; \\\n test ! -L /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory-non-symlink /run/nemoclaw; \\\n test \"$(stat -c '%u:%g:%a' /run/nemoclaw 2>/dev/null)\" = '0:0:755' \\\n || managed_runtime_assertion_failed runtime-directory-metadata-0:0:755 /run/nemoclaw", ); const runtimeModeReplay = indexOfRequired( finalStage, diff --git a/test/langchain-deepagents-code-image.test.ts b/test/langchain-deepagents-code-image.test.ts index 9572b76cd47..50aecc83042 100644 --- a/test/langchain-deepagents-code-image.test.ts +++ b/test/langchain-deepagents-code-image.test.ts @@ -163,7 +163,7 @@ describe("LangChain Deep Agents Code image contracts", () => { "USER root", ].join("\n"); const managedRuntimeDirectory = - "install -d -o root -g root -m 0755 /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw"; + "install -d -o root -g root -m 0755 /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory-create /run/nemoclaw; \\\n test -d /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory /run/nemoclaw; \\\n test ! -L /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory-non-symlink /run/nemoclaw; \\\n test \"$(stat -c '%u:%g:%a' /run/nemoclaw 2>/dev/null)\" = '0:0:755' \\\n || managed_runtime_assertion_failed runtime-directory-metadata-0:0:755 /run/nemoclaw"; const runtimeModeReplay = "&& chmod 444 /opt/nemoclaw-deepagents-code/generate-config.ts"; expect(dockerfile).toContain("ARG BASE_IMAGE\n"); diff --git a/test/mcp-tool-discovery-image-contract.test.ts b/test/mcp-tool-discovery-image-contract.test.ts index 7a9f455f150..6f49816c1b4 100644 --- a/test/mcp-tool-discovery-image-contract.test.ts +++ b/test/mcp-tool-discovery-image-contract.test.ts @@ -9,6 +9,7 @@ import path from "node:path"; import { pathToFileURL } from "node:url"; import { stripVTControlCharacters } from "node:util"; import { describe, expect, it } from "vitest"; +import YAML from "yaml"; const repoRoot = path.join(import.meta.dirname, ".."); const managedImagesWorkflowPath = path.join(repoRoot, ".github/workflows/managed-images.yaml"); @@ -25,6 +26,21 @@ const reviewedRuntimeBundleFiles = [ "tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle", ] as const; +type ManagedImagesWorkflow = { + jobs?: Record }>; +}; + +function createReviewedRuntimeBundleFixture(): string { + const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-reviewed-runtime-bundle-")); + for (const bundleFile of reviewedRuntimeBundleFiles) { + const fixturePath = path.join(fixture, bundleFile); + fs.mkdirSync(path.dirname(fixturePath), { recursive: true }); + fs.writeFileSync(fixturePath, "reviewed runtime fixture\n"); + fs.chmodSync(fixturePath, 0o644); + } + return fixture; +} + function createCacheSeedFixture(): { cache: string; fixture: string; @@ -112,17 +128,82 @@ function createCacheSeedFixture(): { describe("MCP tool discovery image contract", () => { // source-shape-contract: security -- The PR image build must reproduce group-writable inputs before validating the immutable bundle boundary it("builds managed images when reviewed runtime bundle files are group-writable (#8665)", () => { - const workflow = fs.readFileSync(managedImagesWorkflowPath, "utf8"); - const metadataStep = workflow.indexOf( - "- name: Make reviewed runtime bundle files group-writable", + const workflow = YAML.parse( + fs.readFileSync(managedImagesWorkflowPath, "utf8"), + ) as ManagedImagesWorkflow; + const steps = workflow.jobs?.["pr-build-and-entrypoint"]?.steps ?? []; + const metadataSteps = steps.filter( + (step) => step.name === "Make reviewed runtime bundle files group-writable", ); - const buildStep = workflow.indexOf("- name: Build PR managed image locally"); + const metadataStep = metadataSteps[0]; + const buildStep = steps.find((step) => step.name === "Build PR managed image locally"); + const expectedRun = [ + "set -euo pipefail", + "runtime_bundle_files=(", + ...reviewedRuntimeBundleFiles.map((bundleFile) => ` ${bundleFile}`), + ")", + 'for path in "${runtime_bundle_files[@]}"; do', + ' if [[ ! -f "$path" || -L "$path" ]]; then', + " printf 'ERROR: reviewed runtime bundle input must be a regular non-symlink: %s\\n' \"$path\" >&2", + " exit 1", + " fi", + "done", + 'chmod 0664 "${runtime_bundle_files[@]}"', + "", + ].join("\n"); + + expect(metadataSteps).toHaveLength(1); + expect(metadataStep?.run).toBe(expectedRun); + expect(buildStep).toBeDefined(); + if (!metadataStep?.run || !buildStep) { + throw new Error("managed image workflow is missing the reviewed bundle or build step"); + } + expect(steps.indexOf(metadataStep)).toBeLessThan(steps.indexOf(buildStep)); + + const validFixture = createReviewedRuntimeBundleFixture(); + try { + const valid = spawnSync("bash", ["-c", metadataStep.run], { + cwd: validFixture, + encoding: "utf8", + }); + expect(valid.status, valid.stderr).toBe(0); + expect(valid.stderr).toBe(""); + for (const bundleFile of reviewedRuntimeBundleFiles) { + expect(fs.statSync(path.join(validFixture, bundleFile)).mode & 0o777).toBe(0o664); + } + } finally { + fs.rmSync(validFixture, { force: true, recursive: true }); + } + + for (const invalidType of ["missing", "directory", "symlink"] as const) { + const invalidFixture = createReviewedRuntimeBundleFixture(); + const invalidBundleFile = reviewedRuntimeBundleFiles.at(-1)!; + const invalidPath = path.join(invalidFixture, invalidBundleFile); + fs.unlinkSync(invalidPath); + if (invalidType === "directory") { + fs.mkdirSync(invalidPath); + } else if (invalidType === "symlink") { + const targetPath = path.join(invalidFixture, "symlink-target.bundle"); + fs.writeFileSync(targetPath, "unreviewed target\n", { mode: 0o644 }); + fs.symlinkSync(targetPath, invalidPath); + } - expect(metadataStep).toBeGreaterThan(0); - expect(metadataStep).toBeLessThan(buildStep); - expect(workflow.slice(metadataStep, buildStep)).toContain("chmod 0664 \\"); - for (const bundleFile of reviewedRuntimeBundleFiles) { - expect(workflow.slice(metadataStep, buildStep)).toContain(bundleFile); + try { + const invalid = spawnSync("bash", ["-c", metadataStep.run], { + cwd: invalidFixture, + encoding: "utf8", + }); + expect(invalid.status).toBe(1); + expect(invalid.stdout).toBe(""); + expect(invalid.stderr).toBe( + `ERROR: reviewed runtime bundle input must be a regular non-symlink: ${invalidBundleFile}\n`, + ); + expect( + fs.statSync(path.join(invalidFixture, reviewedRuntimeBundleFiles[0])).mode & 0o777, + ).toBe(0o644); + } finally { + fs.rmSync(invalidFixture, { force: true, recursive: true }); + } } }); diff --git a/test/openclaw-final-image-layout.test.ts b/test/openclaw-final-image-layout.test.ts index 9ec4ccc8cf6..ef49009eb45 100644 --- a/test/openclaw-final-image-layout.test.ts +++ b/test/openclaw-final-image-layout.test.ts @@ -222,7 +222,7 @@ describe("OpenClaw final image layout", () => { ); const managedRuntimeDirectory = indexOfRequired( finalStage, - "install -d -o root -g root -m 0755 /run/nemoclaw", + "install -d -o root -g root -m 0755 /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory-create /run/nemoclaw; \\\n test -d /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory /run/nemoclaw; \\\n test ! -L /run/nemoclaw \\\n || managed_runtime_assertion_failed runtime-directory-non-symlink /run/nemoclaw; \\\n test \"$(stat -c '%u:%g:%a' /run/nemoclaw 2>/dev/null)\" = '0:0:755' \\\n || managed_runtime_assertion_failed runtime-directory-metadata-0:0:755 /run/nemoclaw", ); const runtimeChmod = indexOfRequired(finalStage, "RUN chmod 755 /usr/local/bin/nemoclaw-start"); diff --git a/test/support/managed-bootstrap-image-contract.ts b/test/support/managed-bootstrap-image-contract.ts index 04e01f6246d..b9117e49600 100644 --- a/test/support/managed-bootstrap-image-contract.ts +++ b/test/support/managed-bootstrap-image-contract.ts @@ -67,7 +67,10 @@ function expectManagedRuntimeDiagnostic(dockerfile: string): void { `chown root:root ${MANAGED_STARTUP_RUNTIME_PATH} 2>/dev/null || managed_runtime_assertion_failed owner-root-root ${MANAGED_STARTUP_RUNTIME_PATH}`, `chmod 0444 ${MANAGED_STARTUP_RUNTIME_PATH} 2>/dev/null || managed_runtime_assertion_failed mode-0444 ${MANAGED_STARTUP_RUNTIME_PATH}`, `test \"$(stat -c '%u:%g:%a' ${MANAGED_STARTUP_RUNTIME_PATH} 2>/dev/null)\" = '0:0:444' || managed_runtime_assertion_failed metadata-0:0:444 ${MANAGED_STARTUP_RUNTIME_PATH}`, - "install -d -o root -g root -m 0755 /run/nemoclaw || managed_runtime_assertion_failed runtime-directory-0:0:755 /run/nemoclaw", + "install -d -o root -g root -m 0755 /run/nemoclaw || managed_runtime_assertion_failed runtime-directory-create /run/nemoclaw", + "test -d /run/nemoclaw || managed_runtime_assertion_failed runtime-directory /run/nemoclaw", + "test ! -L /run/nemoclaw || managed_runtime_assertion_failed runtime-directory-non-symlink /run/nemoclaw", + "test \"$(stat -c '%u:%g:%a' /run/nemoclaw 2>/dev/null)\" = '0:0:755' || managed_runtime_assertion_failed runtime-directory-metadata-0:0:755 /run/nemoclaw", ]) { expect(logicalInstruction.split(assertion)).toHaveLength(2); } @@ -76,8 +79,12 @@ function expectManagedRuntimeDiagnostic(dockerfile: string): void { const missingPath = path.join(tmp, "missing-runtime.cjs"); const targetPath = path.join(tmp, "runtime-target.cjs"); const linkPath = path.join(tmp, "runtime-link.cjs"); + const runtimeDirectoryTarget = path.join(tmp, "runtime-directory-target"); + const runtimeDirectoryLink = path.join(tmp, "runtime-directory-link"); fs.writeFileSync(targetPath, "fixture\n", { mode: 0o444 }); fs.symlinkSync(targetPath, linkPath); + fs.mkdirSync(runtimeDirectoryTarget); + fs.symlinkSync(runtimeDirectoryTarget, runtimeDirectoryLink); const runDiagnostic = (artifactPath: string, invariant: string, statOutput: string) => spawnSync( "sh", @@ -119,6 +126,17 @@ function expectManagedRuntimeDiagnostic(dockerfile: string): void { `ERROR: managed image assertion failed: non-symlink path=${linkPath} uid=0 gid=0 type=symbolic link mode=777 symlink=yes\n`, ); + const runtimeDirectorySymlink = runDiagnostic( + runtimeDirectoryLink, + "runtime-directory-non-symlink", + "uid=0 gid=0 type=symbolic link mode=777", + ); + expect(runtimeDirectorySymlink.status).toBe(1); + expect(runtimeDirectorySymlink.stdout).toBe(""); + expect(runtimeDirectorySymlink.stderr).toBe( + `ERROR: managed image assertion failed: runtime-directory-non-symlink path=${runtimeDirectoryLink} uid=0 gid=0 type=symbolic link mode=777 symlink=yes\n`, + ); + const writable = runDiagnostic( targetPath, "discovery-tree-owner-mode", From bdf2902e89c13ffb43325bdfb297b7cf7626baa1 Mon Sep 17 00:00:00 2001 From: San Dang Date: Mon, 10 Aug 2026 16:50:37 +0700 Subject: [PATCH 4/5] test(images): keep workflow contract linear Signed-off-by: San Dang --- .../mcp-tool-discovery-image-contract.test.ts | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/test/mcp-tool-discovery-image-contract.test.ts b/test/mcp-tool-discovery-image-contract.test.ts index 6f49816c1b4..2bd88a79a18 100644 --- a/test/mcp-tool-discovery-image-contract.test.ts +++ b/test/mcp-tool-discovery-image-contract.test.ts @@ -30,6 +30,27 @@ type ManagedImagesWorkflow = { jobs?: Record }>; }; +const invalidReviewedBundlePreparers: ReadonlyArray< + (fixture: string, invalidPath: string) => void +> = [ + () => undefined, + (_fixture, invalidPath) => fs.mkdirSync(invalidPath), + (fixture, invalidPath) => { + const targetPath = path.join(fixture, "symlink-target.bundle"); + fs.writeFileSync(targetPath, "unreviewed target\n", { mode: 0o644 }); + fs.symlinkSync(targetPath, invalidPath); + }, +]; + +function required(value: T | undefined, message: string): T { + return ( + value ?? + (() => { + throw new Error(message); + })() + ); +} + function createReviewedRuntimeBundleFixture(): string { const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-reviewed-runtime-bundle-")); for (const bundleFile of reviewedRuntimeBundleFiles) { @@ -135,8 +156,14 @@ describe("MCP tool discovery image contract", () => { const metadataSteps = steps.filter( (step) => step.name === "Make reviewed runtime bundle files group-writable", ); - const metadataStep = metadataSteps[0]; - const buildStep = steps.find((step) => step.name === "Build PR managed image locally"); + const metadataStep = required( + metadataSteps[0], + "managed image workflow is missing the reviewed bundle step", + ); + const buildStep = required( + steps.find((step) => step.name === "Build PR managed image locally"), + "managed image workflow is missing the build step", + ); const expectedRun = [ "set -euo pipefail", "runtime_bundle_files=(", @@ -153,16 +180,13 @@ describe("MCP tool discovery image contract", () => { ].join("\n"); expect(metadataSteps).toHaveLength(1); - expect(metadataStep?.run).toBe(expectedRun); - expect(buildStep).toBeDefined(); - if (!metadataStep?.run || !buildStep) { - throw new Error("managed image workflow is missing the reviewed bundle or build step"); - } + const metadataStepRun = required(metadataStep.run, "reviewed bundle step is missing run"); + expect(metadataStepRun).toBe(expectedRun); expect(steps.indexOf(metadataStep)).toBeLessThan(steps.indexOf(buildStep)); const validFixture = createReviewedRuntimeBundleFixture(); try { - const valid = spawnSync("bash", ["-c", metadataStep.run], { + const valid = spawnSync("bash", ["-c", metadataStepRun], { cwd: validFixture, encoding: "utf8", }); @@ -175,21 +199,15 @@ describe("MCP tool discovery image contract", () => { fs.rmSync(validFixture, { force: true, recursive: true }); } - for (const invalidType of ["missing", "directory", "symlink"] as const) { + for (const prepareInvalidBundle of invalidReviewedBundlePreparers) { const invalidFixture = createReviewedRuntimeBundleFixture(); - const invalidBundleFile = reviewedRuntimeBundleFiles.at(-1)!; + const invalidBundleFile = reviewedRuntimeBundleFiles[3]; const invalidPath = path.join(invalidFixture, invalidBundleFile); fs.unlinkSync(invalidPath); - if (invalidType === "directory") { - fs.mkdirSync(invalidPath); - } else if (invalidType === "symlink") { - const targetPath = path.join(invalidFixture, "symlink-target.bundle"); - fs.writeFileSync(targetPath, "unreviewed target\n", { mode: 0o644 }); - fs.symlinkSync(targetPath, invalidPath); - } + prepareInvalidBundle(invalidFixture, invalidPath); try { - const invalid = spawnSync("bash", ["-c", metadataStep.run], { + const invalid = spawnSync("bash", ["-c", metadataStepRun], { cwd: invalidFixture, encoding: "utf8", }); From 6e3558659100e7e8e82e9a343de0ec14168a7c03 Mon Sep 17 00:00:00 2001 From: San Dang Date: Mon, 10 Aug 2026 17:04:52 +0700 Subject: [PATCH 5/5] test(images): exercise symlinked runtime directory Signed-off-by: San Dang --- .github/workflows/managed-images.yaml | 50 +++++++++++++++++++ ci/source-shape-test-budget.json | 5 ++ ...managed-image-publication-workflow.test.ts | 24 ++++++++- 3 files changed, 77 insertions(+), 2 deletions(-) diff --git a/.github/workflows/managed-images.yaml b/.github/workflows/managed-images.yaml index e0e4bc91001..9776c02352b 100644 --- a/.github/workflows/managed-images.yaml +++ b/.github/workflows/managed-images.yaml @@ -208,6 +208,56 @@ jobs: provenance: false sbom: false + - name: Reject a symlinked runtime directory during image assembly + if: ${{ matrix.agent == 'langchain-deepagents-code' }} + shell: bash + env: + BASE_REFERENCE: ${{ steps.base.outputs.ref }} + DOCKERFILE: ${{ matrix.dockerfile }} + run: | + set -euo pipefail + probe_dockerfile="$RUNNER_TEMP/runtime-directory-symlink.Dockerfile" + probe_log="$RUNNER_TEMP/runtime-directory-symlink.log" + { + cat <<'PROBE_HEADER' + ARG BASE_IMAGE + FROM ${BASE_IMAGE} + USER root + COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/managed-startup-image-runtime.bundle /usr/local/lib/nemoclaw/managed-startup-image-runtime.cjs + COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/BUNDLED_PACKAGES.json /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/BUNDLED_PACKAGES.json + COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/THIRD_PARTY_LICENSES.txt /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/THIRD_PARTY_LICENSES.txt + COPY --chown=0:0 --chmod=0444 tools/mcp-tool-discovery-runtime/reviewed-runtime-bundle/mcp-tool-discovery/mcp-tool-discovery.bundle /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/mcp-tool-discovery.mjs + RUN rm -rf /run/nemoclaw \ + && ln -s /tmp /run/nemoclaw + PROBE_HEADER + awk ' + /^RUN managed_runtime_assertion_failed\(\)/ { copying = 1 } + copying { print } + copying && /managed_runtime_assertion_failed runtime-directory-metadata-0:0:755 \/run\/nemoclaw$/ { exit } + ' "$DOCKERFILE" + } > "$probe_dockerfile" + + if [ "$(grep -c '^RUN managed_runtime_assertion_failed()' "$probe_dockerfile")" -ne 1 ]; then + echo "ERROR: could not extract one managed runtime assertion from $DOCKERFILE." >&2 + exit 1 + fi + if docker buildx build \ + --platform linux/amd64 \ + --build-arg "BASE_IMAGE=${BASE_REFERENCE}" \ + --progress plain \ + --file "$probe_dockerfile" \ + . > "$probe_log" 2>&1; then + echo "ERROR: image assembly accepted a symlinked /run/nemoclaw directory." >&2 + exit 1 + fi + if ! grep -Fq \ + 'ERROR: managed image assertion failed: runtime-directory-non-symlink path=/run/nemoclaw' \ + "$probe_log"; then + tail -80 "$probe_log" >&2 + echo "ERROR: image assembly failed without the runtime-directory-non-symlink diagnostic." >&2 + exit 1 + fi + - name: Validate exact PR managed image contract id: contract shell: bash diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json index 02299290418..9952d638d63 100644 --- a/ci/source-shape-test-budget.json +++ b/ci/source-shape-test-budget.json @@ -281,6 +281,11 @@ "test": "keeps fork-safe labeling inside the trusted metadata boundary", "category": "security" }, + { + "file": "test/managed-image-publication-workflow.test.ts", + "test": "builds and exercises every shipped agent from an exact PR image before merge (#7744)", + "category": "security" + }, { "file": "test/messaging-image-env-contract.test.ts", "test": "%s keeps the full plan in build processes but not final runtime environments (#5896)", diff --git a/test/managed-image-publication-workflow.test.ts b/test/managed-image-publication-workflow.test.ts index 44b6c9cdc69..60d543e36ad 100644 --- a/test/managed-image-publication-workflow.test.ts +++ b/test/managed-image-publication-workflow.test.ts @@ -435,8 +435,11 @@ describe("complete managed-image publication workflow", () => { } }); + // source-shape-contract: security -- PR image builds must reject a symlinked runtime path before final image contract validation it("builds and exercises every shipped agent from an exact PR image before merge (#7744)", () => { - const workflow = readWorkflow("managed-images.yaml"); + const workflow = YAML.parse( + fs.readFileSync(path.join(repoRoot, ".github/workflows/managed-images.yaml"), "utf8"), + ) as Workflow; const prBuilder = managedPrBuilder(workflow); const matrix = prBuilder.strategy?.matrix?.include ?? []; const steps = prBuilder.steps ?? []; @@ -466,11 +469,28 @@ describe("complete managed-image publication workflow", () => { expect(resolveBase).toContain('reference="${BASE_REPOSITORY}@${digest}"'); expect(resolveBase).toContain('actual="sha256:$(sha256sum "$exact_raw"'); - expect(step(prBuilder, "Build PR managed image locally").with).toMatchObject({ + const build = step(prBuilder, "Build PR managed image locally"); + expect(build.with).toMatchObject({ platforms: "linux/amd64", load: true, push: false, }); + const runtimeDirectoryProbe = step( + prBuilder, + "Reject a symlinked runtime directory during image assembly", + ); + expect(runtimeDirectoryProbe.if).toBe("${{ matrix.agent == 'langchain-deepagents-code' }}"); + expect(runtimeDirectoryProbe.run).toContain("docker buildx build"); + expect(runtimeDirectoryProbe.run).toContain( + "managed_runtime_assertion_failed runtime-directory-metadata-0:0:755", + ); + expect(runtimeDirectoryProbe.run).toContain( + "ERROR: managed image assertion failed: runtime-directory-non-symlink path=/run/nemoclaw", + ); + expect(steps.indexOf(build)).toBeLessThan(steps.indexOf(runtimeDirectoryProbe)); + expect(steps.indexOf(runtimeDirectoryProbe)).toBeLessThan( + steps.indexOf(step(prBuilder, "Validate exact PR managed image contract")), + ); expect(step(prBuilder, "Exercise managed startup root stdin and hold").run).toContain( "run-managed-image-direct-e2e.ts", );