diff --git a/.github/workflows/sandbox-images-and-e2e.yaml b/.github/workflows/sandbox-images-and-e2e.yaml
index 0a5fba35049..478ae1b8007 100644
--- a/.github/workflows/sandbox-images-and-e2e.yaml
+++ b/.github/workflows/sandbox-images-and-e2e.yaml
@@ -218,6 +218,30 @@ jobs:
cache-from: type=gha,scope=hermes-production-${{ runner.os }}-${{ runner.arch }}
cache-to: type=gha,mode=max,scope=hermes-production-${{ runner.os }}-${{ runner.arch }}
+ # The production build intentionally omits NEMOCLAW_CORPORATE_CA_B64. A
+ # successful final stage therefore proves its registry remediations and
+ # Hermes agent-install phase complete with the base image's default trust.
+ - name: Verify Hermes default-trust final image
+ shell: bash
+ run: |
+ set -euo pipefail
+ docker run --rm \
+ --network none \
+ --read-only \
+ --cap-drop ALL \
+ --security-opt no-new-privileges \
+ --pids-limit 64 \
+ --memory 256m \
+ --entrypoint /bin/sh \
+ nemoclaw-hermes-production -eu -c '
+ test "$NODE_EXTRA_CA_CERTS" = /usr/local/share/nemoclaw/corporate-ca.pem
+ test ! -e /usr/local/share/nemoclaw/corporate-ca.pem
+ test ! -L /usr/local/share/nemoclaw/corporate-ca.pem
+ test -x /usr/local/bin/hermes
+ node -e "const tls = require(\"node:tls\"); if (tls.rootCertificates.length === 0) process.exit(1); tls.createSecureContext()"
+ /opt/hermes/.venv/bin/python -I -c "import ssl; assert ssl.create_default_context().get_ca_certs()"
+ '
+
- name: Scan completed Hermes image for node-tar
id: node-tar-scan
shell: bash
@@ -416,14 +440,32 @@ jobs:
shell: bash
run: |
set -euo pipefail
+ # curl and Python replace their default roots with this build argument.
+ compact_ca_bundle="$(mktemp)"
+ trap 'rm -f "$compact_ca_bundle"' EXIT
+ node --experimental-strip-types scripts/checks/select-ci-endpoint-ca-roots.mts \
+ --output "$compact_ca_bundle"
+ corporate_ca_b64="$(base64 -w 0 "$compact_ca_bundle")"
+ corporate_ca_sha256="$(sha256sum "$compact_ca_bundle" | cut -d ' ' -f 1)"
messaging_plan_b64="$(node --experimental-strip-types scripts/check-messaging-plan-image-boundary.mts plan hermes)"
build_args=(
-f agents/hermes/Dockerfile
--build-arg "BASE_IMAGE=${HERMES_BASE_IMAGE}"
+ --build-arg "NEMOCLAW_CORPORATE_CA_B64=${corporate_ca_b64}"
--build-arg "NEMOCLAW_MESSAGING_PLAN_B64=${messaging_plan_b64}"
)
scripts/check-production-build-args.sh "${build_args[@]}"
docker build "${build_args[@]}" -t nemoclaw-hermes-plan-boundary .
+ installed_ca_sha256="$(
+ docker run --rm --network none --entrypoint sha256sum \
+ nemoclaw-hermes-plan-boundary \
+ /usr/local/share/nemoclaw/corporate-ca.pem |
+ cut -d ' ' -f 1
+ )"
+ test "$installed_ca_sha256" = "$corporate_ca_sha256"
+ docker run --rm --network none --entrypoint openssl \
+ nemoclaw-hermes-plan-boundary crl2pkcs7 -nocrl \
+ -certfile /usr/local/share/nemoclaw/corporate-ca.pem -out /dev/null
node --experimental-strip-types scripts/check-messaging-plan-image-boundary.mts verify \
nemoclaw-hermes-plan-boundary hermes
diff --git a/agents/hermes/Dockerfile b/agents/hermes/Dockerfile
index 977ee162692..635b8dc159e 100644
--- a/agents/hermes/Dockerfile
+++ b/agents/hermes/Dockerfile
@@ -150,24 +150,70 @@ COPY scripts/checks/node-tar-image-scan.mts /scripts/checks/node-tar-image-scan.
# hadolint ignore=DL3006
FROM ${BASE_IMAGE}
+# Base64-encoded host corporate-proxy CA bundle (#6210). Empty by default. When
+# onboard detects an operator-supplied corporate CA on the host it bakes it
+# here; the RUN below decodes it to a root-owned file that the entrypoint
+# appends to the OpenShell trust bundle at runtime. The CA is a public
+# certificate, not a secret, so baking it into an image layer is acceptable.
+ARG NEMOCLAW_CORPORATE_CA_B64
+
+# Decode the host corporate-proxy CA (#6210) to a root-owned, read-only file
+# when onboard baked one in. No-op when NEMOCLAW_CORPORATE_CA_B64 is empty. The
+# ARG is expanded by the shell (not interpolated into source), and its value is
+# base64 sanitized host-side, so this is not an injection vector. Must run as
+# root, before the USER sandbox drop below.
+# hadolint ignore=DL3059,DL4006
+RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then \
+ command -v base64 >/dev/null 2>&1 || { echo "[nemoclaw] base64 is required to decode NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image" >&2; exit 1; }; \
+ command -v openssl >/dev/null 2>&1 || { echo "[nemoclaw] openssl is required to validate NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image (#6210)" >&2; exit 1; }; \
+ mkdir -p /usr/local/share/nemoclaw \
+ && { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /tmp/nemoclaw-corporate-ca.decoded 2>/dev/null \
+ || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; } \
+ && awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{f=0}' /tmp/nemoclaw-corporate-ca.decoded > /usr/local/share/nemoclaw/corporate-ca.pem \
+ && rm -f /tmp/nemoclaw-corporate-ca.decoded \
+ && { grep -qF -- "-----BEGIN CERTIFICATE-----" /usr/local/share/nemoclaw/corporate-ca.pem || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
+ && { openssl crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem >/dev/null 2>&1 || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
+ && chown root:root /usr/local/share/nemoclaw/corporate-ca.pem \
+ && chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem \
+ && echo "[nemoclaw] baked host corporate-proxy CA into image trust (#6210)"; \
+ fi
+
+# Use the decoded CA for Node.js package operations in this final stage. Node.js
+# ignores the path when no CA was baked. At runtime, nemoclaw-start replaces it
+# with the merged OpenShell and corporate bundle.
+ENV NODE_EXTRA_CA_CERTS=/usr/local/share/nemoclaw/corporate-ca.pem
+
# Cross-stage root copies are accepted by Docker's legacy builder and create
# one final-image layer while preserving metadata on existing parent paths.
COPY --from=hermes-npm-patch-payload / /
# The final Hermes image owns the shipped dependency boundary independently of
-# base freshness. Reassert the idempotent npm-private node-tar fix here.
-RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts \
- --npm-root /usr/local/lib/node_modules/npm
+# base freshness. Reassert the idempotent npm-private node-tar fix here. When
+# onboarding supplied a corporate CA, use it for the registry-backed download.
+RUN if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \
+ export CURL_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \
+ fi; \
+ node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts \
+ --npm-root /usr/local/lib/node_modules/npm
# Reassert the npm-private brace-expansion fix for the exact final filesystem.
+# When onboarding supplied a corporate CA, use it for the registry-backed
+# download.
# hadolint ignore=DL3059
-RUN node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts \
- --npm-root /usr/local/lib/node_modules/npm
+RUN if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \
+ export CURL_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \
+ fi; \
+ node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts \
+ --npm-root /usr/local/lib/node_modules/npm
-# Reassert the npm-private ip-address fix for the exact final filesystem.
+# Reassert the npm-private ip-address fix for the exact final filesystem. When
+# onboarding supplied a corporate CA, use it for the registry-backed download.
# hadolint ignore=DL3059
-RUN node --experimental-strip-types /scripts/lib/patch-bundled-npm-ip-address.mts \
- --npm-root /usr/local/lib/node_modules/npm
+RUN if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \
+ export CURL_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \
+ fi; \
+ node --experimental-strip-types /scripts/lib/patch-bundled-npm-ip-address.mts \
+ --npm-root /usr/local/lib/node_modules/npm
# Keep the final image contract explicit even when the published base image
# changes independently of this Dockerfile.
@@ -605,12 +651,6 @@ ARG NEMOCLAW_HERMES_TOOL_GATEWAY_BROKER=0
ARG NEMOCLAW_HERMES_TOOL_GATEWAY_PRESETS_B64=W10=
ARG NEMOCLAW_BUILD_ID=default
ARG NEMOCLAW_DARWIN_VM_COMPAT=0
-# Base64-encoded host corporate-proxy CA bundle (#6210). Empty by default. When
-# onboard detects an operator-supplied corporate CA on the host it bakes it
-# here; the RUN below decodes it to a root-owned file that the entrypoint
-# appends to the OpenShell trust bundle at runtime. The CA is a public
-# certificate, not a secret, so baking it into an image layer is acceptable.
-ARG NEMOCLAW_CORPORATE_CA_B64
# Total model context window (input + output tokens). Empty by default so
# Hermes auto-detects from the endpoint's /v1/models max_model_len; onboard
# rewrites this ARG (via dockerfile-patch) when it probes a runtime value or
@@ -660,34 +700,20 @@ RUN node --experimental-strip-types /src/lib/messaging/applier/build/messaging-b
# Apply messaging agent-install hooks as root so Hermes Python packages can update
# /opt/hermes/.venv before the runtime drops to the sandbox user.
WORKDIR /opt/hermes
+# Clear inherited Python and uv trust overrides before package installation.
+# When the decoded corporate CA exists, use it only for this RUN instruction.
# hadolint ignore=DL3059
-RUN node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent hermes --phase agent-install \
+RUN unset SSL_CERT_FILE REQUESTS_CA_BUNDLE; \
+ if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \
+ export SSL_CERT_FILE=/usr/local/share/nemoclaw/corporate-ca.pem; \
+ export REQUESTS_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \
+ fi; \
+ node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent hermes --phase agent-install \
&& if [ "$NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION" = "1" ]; then \
node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts \
--agent hermes --phase managed-image-capability-union; \
fi
-# Decode the host corporate-proxy CA (#6210) to a root-owned, read-only file
-# when onboard baked one in. No-op when NEMOCLAW_CORPORATE_CA_B64 is empty. The
-# ARG is expanded by the shell (not interpolated into source), and its value is
-# base64 sanitized host-side, so this is not an injection vector. Must run as
-# root, before the USER sandbox drop below.
-# hadolint ignore=DL3059,DL4006
-RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then \
- command -v base64 >/dev/null 2>&1 || { echo "[nemoclaw] base64 is required to decode NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image" >&2; exit 1; }; \
- command -v openssl >/dev/null 2>&1 || { echo "[nemoclaw] openssl is required to validate NEMOCLAW_CORPORATE_CA_B64 but is not installed in the build image (#6210)" >&2; exit 1; }; \
- mkdir -p /usr/local/share/nemoclaw \
- && { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /tmp/nemoclaw-corporate-ca.decoded 2>/dev/null \
- || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; } \
- && awk '/-----BEGIN CERTIFICATE-----/{f=1} f{print} /-----END CERTIFICATE-----/{f=0}' /tmp/nemoclaw-corporate-ca.decoded > /usr/local/share/nemoclaw/corporate-ca.pem \
- && rm -f /tmp/nemoclaw-corporate-ca.decoded \
- && { grep -qF -- "-----BEGIN CERTIFICATE-----" /usr/local/share/nemoclaw/corporate-ca.pem || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
- && { openssl crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem >/dev/null 2>&1 || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates (#6210)" >&2; exit 1; }; } \
- && chown root:root /usr/local/share/nemoclaw/corporate-ca.pem \
- && chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem \
- && echo "[nemoclaw] baked host corporate-proxy CA into image trust (#6210)"; \
- fi
-
WORKDIR /sandbox
USER sandbox
diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json
index 0a8883a340c..25da412c881 100644
--- a/ci/source-shape-test-budget.json
+++ b/ci/source-shape-test-budget.json
@@ -91,6 +91,11 @@
"test": "trusts the corporate CA before the DCode discovery runtime npm install",
"category": "security"
},
+ {
+ "file": "test/corporate-ca-build-tls-anchor.test.ts",
+ "test": "uses the corporate CA conditionally for all Hermes registry remediations",
+ "category": "security"
+ },
{
"file": "test/dcode-base-image-workflow.test.ts",
"test": "accepts every discovered publisher and rejects supply-chain mutations",
diff --git a/docs/security/configure-corporate-ca-trust.mdx b/docs/security/configure-corporate-ca-trust.mdx
index 201d9808465..7b85bd677fe 100644
--- a/docs/security/configure-corporate-ca-trust.mdx
+++ b/docs/security/configure-corporate-ca-trust.mdx
@@ -39,7 +39,12 @@ It sets `NODE_EXTRA_CA_CERTS` before build-time Node.js dependency verification,
-The Hermes Dockerfile decodes the bundle after its managed build-time dependency steps, so the corporate CA does not apply to those earlier operations.
+The Hermes discovery-runtime installer applies the corporate CA before its npm operations.
+The final Hermes image stage decodes the CA immediately after `FROM ${BASE_IMAGE}` and sets `NODE_EXTRA_CA_CERTS` before later npm operations.
+The registry-backed npm remediations set `CURL_CA_BUNDLE` before each download only when the decoded certificate file exists.
+The Hermes package installer clears inherited `SSL_CERT_FILE` and `REQUESTS_CA_BUNDLE` values before its build-time `uv pip install` commands.
+When the decoded CA exists, it sets both variables to that file for those commands.
+If the file does not exist, uv and Python use their default trust configuration.
diff --git a/scripts/checks/select-ci-endpoint-ca-roots.mts b/scripts/checks/select-ci-endpoint-ca-roots.mts
new file mode 100644
index 00000000000..cf503d47ddb
--- /dev/null
+++ b/scripts/checks/select-ci-endpoint-ca-roots.mts
@@ -0,0 +1,322 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { spawnSync } from "node:child_process";
+import { X509Certificate } from "node:crypto";
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import { pathToFileURL } from "node:url";
+
+export const CI_CA_SYSTEM_BUNDLE = "/etc/ssl/certs/ca-certificates.crt";
+export const CI_CA_ENDPOINTS = Object.freeze([
+ "registry.npmjs.org",
+ "pypi.org",
+ "files.pythonhosted.org",
+] as const);
+export const MAX_CI_CA_CERTIFICATES = 24;
+export const MAX_CI_CA_ENCODED_BYTES = 65_536;
+
+const PEM_RE = /-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+?-----END CERTIFICATE-----/gu;
+const OPENSSL_TIMEOUT_MS = 30_000;
+
+type CertificateRecord = { readonly cert: X509Certificate; readonly pem: string };
+type OpenSslResult = {
+ readonly error?: Error;
+ readonly status: number | null;
+ readonly stderr: string;
+ readonly stdout: string;
+};
+export type OpenSslRunner = (args: readonly string[]) => OpenSslResult;
+
+function runOpenSsl(args: readonly string[]): OpenSslResult {
+ const result = spawnSync("openssl", [...args], {
+ encoding: "utf8",
+ input: "",
+ killSignal: "SIGKILL",
+ maxBuffer: 4 * 1024 * 1024,
+ timeout: OPENSSL_TIMEOUT_MS,
+ });
+ return {
+ error: result.error,
+ status: result.status,
+ stderr: result.stderr ?? "",
+ stdout: result.stdout ?? "",
+ };
+}
+
+function parseCertificates(bundle: string, label: string): CertificateRecord[] {
+ const blocks = bundle.match(PEM_RE);
+ if (!blocks?.length) throw new Error(`${label} contains no PEM certificate`);
+ return blocks.map((pem, index) => {
+ try {
+ return { cert: new X509Certificate(pem), pem: pem.trim() };
+ } catch {
+ throw new Error(`${label} certificate ${index + 1} is not valid X.509`);
+ }
+ });
+}
+
+function isSignedBy(cert: X509Certificate, issuer: X509Certificate): boolean {
+ try {
+ return cert.verify(issuer.publicKey);
+ } catch {
+ return false;
+ }
+}
+
+function isSelfSigned(cert: X509Certificate): boolean {
+ return cert.subject === cert.issuer && isSignedBy(cert, cert);
+}
+
+function isCurrentSelfSignedRoot(cert: X509Certificate, nowMs = Date.now()): boolean {
+ const validFromMs = Date.parse(cert.validFrom);
+ const validToMs = Date.parse(cert.validTo);
+ if (
+ !cert.ca ||
+ !isSelfSigned(cert) ||
+ Number.isNaN(validFromMs) ||
+ Number.isNaN(validToMs) ||
+ nowMs < validFromMs ||
+ nowMs > validToMs
+ ) {
+ return false;
+ }
+ return true;
+}
+
+function fingerprint(cert: X509Certificate): string {
+ return cert.fingerprint256.replaceAll(":", "").toLowerCase();
+}
+
+export function normalizeCompactRootBundle(
+ roots: readonly string[],
+ limits: { readonly certificates: number; readonly encodedBytes: number } = {
+ certificates: MAX_CI_CA_CERTIFICATES,
+ encodedBytes: MAX_CI_CA_ENCODED_BYTES,
+ },
+): string {
+ const unique = new Map();
+ for (const [index, pem] of roots.entries()) {
+ const records = parseCertificates(pem, `selected root ${index + 1}`);
+ if (records.length !== 1 || !isCurrentSelfSignedRoot(records[0].cert)) {
+ throw new Error(`selected root ${index + 1} must be a current self-signed CA:TRUE root`);
+ }
+ unique.set(fingerprint(records[0].cert), records[0]);
+ }
+ if (unique.size === 0) throw new Error("selected root bundle is empty");
+ if (unique.size > limits.certificates) {
+ throw new Error(`selected root bundle exceeds ${limits.certificates} certificates`);
+ }
+ const bundle = `${[...unique.values()].map(({ pem }) => pem).join("\n")}\n`;
+ if (Buffer.from(bundle).toString("base64").length > limits.encodedBytes) {
+ throw new Error(`selected root bundle exceeds ${limits.encodedBytes} encoded bytes`);
+ }
+ return bundle;
+}
+
+function opensslOutput(
+ runner: OpenSslRunner,
+ args: readonly string[],
+ label: string,
+ requireVerifyOk = false,
+): string {
+ const result = runner(args);
+ if (result.error || result.status !== 0) {
+ throw new Error(`${label} failed without emitting certificate data`);
+ }
+ const output = `${result.stdout}\n${result.stderr}`;
+ if (requireVerifyOk && !/Verify return code:\s*0\s*\(ok\)/iu.test(output)) {
+ throw new Error(`${label} did not report successful certificate verification`);
+ }
+ return output;
+}
+
+function connectionArgs(endpoint: string, caFile: string, showCerts: boolean): string[] {
+ return [
+ "s_client",
+ "-connect",
+ `${endpoint}:443`,
+ "-servername",
+ endpoint,
+ "-verify_hostname",
+ endpoint,
+ "-verify_return_error",
+ "-CAfile",
+ caFile,
+ "-no-CApath",
+ "-no-CAstore",
+ ...(showCerts ? ["-showcerts"] : []),
+ ];
+}
+
+function systemRoots(systemBundle: string): CertificateRecord[] {
+ const roots = new Map();
+ for (const record of parseCertificates(systemBundle, "system CA bundle")) {
+ if (isCurrentSelfSignedRoot(record.cert)) roots.set(fingerprint(record.cert), record);
+ }
+ if (roots.size === 0) throw new Error("system CA bundle contains no current CA:TRUE root");
+ return [...roots.values()];
+}
+
+function verifiesOffline(
+ runner: OpenSslRunner,
+ endpoint: string,
+ chain: readonly CertificateRecord[],
+ root: CertificateRecord,
+ tempDir: string,
+): boolean {
+ const stem = path.join(tempDir, endpoint);
+ const leaf = `${stem}-leaf.pem`;
+ const intermediates = `${stem}-intermediates.pem`;
+ const rootFile = `${stem}-root.pem`;
+ fs.writeFileSync(leaf, `${chain[0].pem}\n`, { mode: 0o600 });
+ fs.writeFileSync(rootFile, `${root.pem}\n`, { mode: 0o600 });
+ const untrusted = chain.slice(1).filter(({ cert }) => !isSelfSigned(cert));
+ if (untrusted.length) {
+ fs.writeFileSync(intermediates, `${untrusted.map(({ pem }) => pem).join("\n")}\n`, {
+ mode: 0o600,
+ });
+ }
+ const result = runner([
+ "verify",
+ "-purpose",
+ "sslserver",
+ "-verify_hostname",
+ endpoint,
+ "-CAfile",
+ rootFile,
+ "-no-CApath",
+ "-no-CAstore",
+ ...(untrusted.length ? ["-untrusted", intermediates] : []),
+ leaf,
+ ]);
+ return !result.error && result.status === 0;
+}
+
+function selectRoot(
+ runner: OpenSslRunner,
+ endpoint: string,
+ chain: readonly CertificateRecord[],
+ roots: readonly CertificateRecord[],
+ tempDir: string,
+): CertificateRecord {
+ const untrusted = chain.filter(({ cert }) => !isSelfSigned(cert));
+ const candidates = roots
+ .filter(({ cert: root }) =>
+ untrusted.some(({ cert }) => cert.issuer === root.subject && isSignedBy(cert, root)),
+ )
+ .sort((left, right) => fingerprint(left.cert).localeCompare(fingerprint(right.cert)));
+ const selected = candidates.find((root) =>
+ verifiesOffline(runner, endpoint, chain, root, tempDir),
+ );
+ if (!selected) throw new Error(`no system CA root verifies the chain for ${endpoint}`);
+ return selected;
+}
+
+export function writeCiEndpointCaRootsOutput(outputPath: string, bundle: string): void {
+ const noFollow = fs.constants.O_NOFOLLOW;
+ if (typeof noFollow !== "number") {
+ throw new Error("output requires O_NOFOLLOW support");
+ }
+
+ let fd: number;
+ try {
+ // Open without following symlinks or blocking on special files, then validate before writing.
+ fd = fs.openSync(
+ outputPath,
+ fs.constants.O_WRONLY | noFollow | (fs.constants.O_NONBLOCK ?? 0),
+ );
+ } catch (error) {
+ throw new Error("output must be an existing regular file that is not a symlink", {
+ cause: error,
+ });
+ }
+ try {
+ const opened = fs.fstatSync(fd);
+ const afterOpen = fs.lstatSync(outputPath);
+ if (
+ !opened.isFile() ||
+ opened.nlink !== 1 ||
+ !afterOpen.isFile() ||
+ afterOpen.isSymbolicLink() ||
+ afterOpen.nlink !== 1 ||
+ opened.dev !== afterOpen.dev ||
+ opened.ino !== afterOpen.ino
+ ) {
+ throw new Error("output must remain the same regular file with exactly one link");
+ }
+ fs.ftruncateSync(fd, 0);
+ fs.writeFileSync(fd, bundle);
+ fs.fchmodSync(fd, 0o600);
+ } finally {
+ fs.closeSync(fd);
+ }
+}
+
+export function selectCiEndpointCaRoots(
+ outputPath: string,
+ runner: OpenSslRunner = runOpenSsl,
+): { readonly certificates: number; readonly encodedBytes: number } {
+ if (path.resolve(outputPath) === path.resolve(CI_CA_SYSTEM_BUNDLE)) {
+ throw new Error("output must not replace the system CA bundle");
+ }
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-ci-ca-roots-"));
+ try {
+ opensslOutput(runner, ["version"], "OpenSSL availability check");
+ const roots = systemRoots(fs.readFileSync(CI_CA_SYSTEM_BUNDLE, "utf8"));
+ const selected = CI_CA_ENDPOINTS.map((endpoint) => {
+ const chainOutput = opensslOutput(
+ runner,
+ connectionArgs(endpoint, CI_CA_SYSTEM_BUNDLE, true),
+ `system CA verification for ${endpoint}`,
+ true,
+ );
+ return selectRoot(
+ runner,
+ endpoint,
+ parseCertificates(chainOutput, `server chain for ${endpoint}`),
+ roots,
+ tempDir,
+ );
+ });
+ const bundle = normalizeCompactRootBundle(selected.map(({ pem }) => pem));
+ const compactPath = path.join(tempDir, "compact.pem");
+ fs.writeFileSync(compactPath, bundle, { mode: 0o600 });
+ for (const endpoint of CI_CA_ENDPOINTS) {
+ opensslOutput(
+ runner,
+ connectionArgs(endpoint, compactPath, false),
+ `compact CA verification for ${endpoint}`,
+ true,
+ );
+ }
+ writeCiEndpointCaRootsOutput(outputPath, bundle);
+ return {
+ certificates: parseCertificates(bundle, "compact CA bundle").length,
+ encodedBytes: Buffer.from(bundle).toString("base64").length,
+ };
+ } finally {
+ fs.rmSync(tempDir, { recursive: true, force: true });
+ }
+}
+
+function main(argv: readonly string[]): void {
+ if (argv.length !== 2 || argv[0] !== "--output" || !argv[1]) {
+ throw new Error("usage: select-ci-endpoint-ca-roots.mts --output ");
+ }
+ const result = selectCiEndpointCaRoots(argv[1]);
+ process.stdout.write(
+ `Selected CA roots: ${result.certificates} (${result.encodedBytes} encoded bytes).\n`,
+ );
+}
+
+const invokedPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
+if (invokedPath === import.meta.url) {
+ try {
+ main(process.argv.slice(2));
+ } catch (error) {
+ process.stderr.write(`ERROR: ${error instanceof Error ? error.message : String(error)}\n`);
+ process.exitCode = 1;
+ }
+}
diff --git a/test/bundled-npm-brace-expansion-dockerfile-contract.test.ts b/test/bundled-npm-brace-expansion-dockerfile-contract.test.ts
index 5cb814cc124..d8124e411d7 100644
--- a/test/bundled-npm-brace-expansion-dockerfile-contract.test.ts
+++ b/test/bundled-npm-brace-expansion-dockerfile-contract.test.ts
@@ -12,6 +12,7 @@ import {
REVIEWED_NPM_VERSION,
} from "../scripts/patch-bundled-npm-brace-expansion.mts";
import { REVIEWED_NPM_VERSION as UPGRADED_NPM_VERSION } from "../scripts/upgrade-bundled-npm.mts";
+import { requireSingleReviewedDockerfileRunCommand } from "./helpers/dockerfile-run-commands";
const repoRoot = path.resolve(import.meta.dirname, "..");
const baseDockerfiles = [
@@ -27,7 +28,8 @@ const finalDockerfiles = [
const copyInstruction =
"COPY scripts/patch-bundled-npm-brace-expansion.mts /scripts/patch-bundled-npm-brace-expansion.mts";
const patchInstruction =
- "RUN node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts";
+ "node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts";
+const npmRootArguments = ["--npm-root", "/usr/local/lib/node_modules/npm"] as const;
describe("bundled npm brace-expansion image remediation contract", () => {
it("binds the replacement to the reviewed npm and registry artifact", () => {
@@ -43,15 +45,20 @@ describe("bundled npm brace-expansion image remediation contract", () => {
it.each(baseDockerfiles)("patches the reviewed npm tree after upgrading it in %s", (file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
const copy = source.indexOf(copyInstruction);
- const upgrade = source.indexOf(
- "RUN node --experimental-strip-types /scripts/upgrade-bundled-npm.mts",
+ const upgrade = requireSingleReviewedDockerfileRunCommand(
+ source,
+ "node --experimental-strip-types /scripts/upgrade-bundled-npm.mts",
+ npmRootArguments,
+ ).commandStart;
+ const patch = requireSingleReviewedDockerfileRunCommand(
+ source,
+ patchInstruction,
+ npmRootArguments,
);
- const patch = source.indexOf(patchInstruction);
expect(copy, file).toBeGreaterThanOrEqual(0);
expect(upgrade, file).toBeGreaterThan(copy);
- expect(patch, file).toBeGreaterThan(upgrade);
- expect(source.slice(patch)).toContain("--npm-root /usr/local/lib/node_modules/npm");
+ expect(patch.commandStart, file).toBeGreaterThan(upgrade);
});
it.each(
@@ -59,14 +66,19 @@ describe("bundled npm brace-expansion image remediation contract", () => {
)("reasserts the private package fix in the completed %s filesystem", (file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
const copy = source.indexOf(copyInstruction);
- const tarPatch = source.indexOf(
- "RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
+ const tarPatch = requireSingleReviewedDockerfileRunCommand(
+ source,
+ "node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
+ npmRootArguments,
+ ).commandStart;
+ const bracePatch = requireSingleReviewedDockerfileRunCommand(
+ source,
+ patchInstruction,
+ npmRootArguments,
);
- const bracePatch = source.indexOf(patchInstruction);
expect(copy, file).toBeGreaterThanOrEqual(0);
expect(tarPatch, file).toBeGreaterThan(copy);
- expect(bracePatch, file).toBeGreaterThan(tarPatch);
- expect(source.slice(bracePatch)).toContain("--npm-root /usr/local/lib/node_modules/npm");
+ expect(bracePatch.commandStart, file).toBeGreaterThan(tarPatch);
});
});
diff --git a/test/bundled-npm-ip-address-dockerfile-contract.test.ts b/test/bundled-npm-ip-address-dockerfile-contract.test.ts
index 67309121327..01d17988562 100644
--- a/test/bundled-npm-ip-address-dockerfile-contract.test.ts
+++ b/test/bundled-npm-ip-address-dockerfile-contract.test.ts
@@ -12,6 +12,7 @@ import {
REVIEWED_NPM_VERSION,
} from "../scripts/lib/patch-bundled-npm-ip-address.mts";
import { REVIEWED_NPM_VERSION as UPGRADED_NPM_VERSION } from "../scripts/upgrade-bundled-npm.mts";
+import { requireSingleReviewedDockerfileRunCommand } from "./helpers/dockerfile-run-commands";
const repoRoot = path.resolve(import.meta.dirname, "..");
const baseDockerfiles = [
@@ -28,10 +29,7 @@ const copyInstruction =
"COPY scripts/lib/patch-bundled-npm-ip-address.mts /scripts/lib/patch-bundled-npm-ip-address.mts";
const patchCommand =
"node --experimental-strip-types /scripts/lib/patch-bundled-npm-ip-address.mts";
-
-function instructionBody(source: string, start: number): string {
- return source.slice(start).split(/\n(?=\S)/u, 1)[0] ?? "";
-}
+const npmRootArguments = ["--npm-root", "/usr/local/lib/node_modules/npm"] as const;
describe("bundled npm ip-address image remediation contract", () => {
it("binds the replacement to the reviewed npm and registry artifact", () => {
@@ -49,36 +47,40 @@ describe("bundled npm ip-address image remediation contract", () => {
it.each(baseDockerfiles)("patches the reviewed npm tree after upgrading it in %s", (file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
const copy = source.indexOf(copyInstruction);
- const upgrade = source.indexOf(
- "RUN node --experimental-strip-types /scripts/upgrade-bundled-npm.mts",
- );
- const patch = source.indexOf(patchCommand);
+ const upgrade = requireSingleReviewedDockerfileRunCommand(
+ source,
+ "node --experimental-strip-types /scripts/upgrade-bundled-npm.mts",
+ npmRootArguments,
+ ).commandStart;
+ const patch = requireSingleReviewedDockerfileRunCommand(source, patchCommand, npmRootArguments);
expect(copy, file).toBeGreaterThanOrEqual(0);
expect(upgrade, file).toBeGreaterThan(copy);
- expect(patch, file).toBeGreaterThan(upgrade);
- expect(instructionBody(source, patch), file).toContain(
- "--npm-root /usr/local/lib/node_modules/npm",
- );
+ expect(patch.commandStart, file).toBeGreaterThan(upgrade);
});
it.each(finalDockerfiles)("reasserts the private package fix in the completed %s", (file) => {
const source = fs.readFileSync(path.join(repoRoot, file), "utf8");
const copy = source.indexOf(copyInstruction);
- const tarPatch = source.indexOf(
- "RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
+ const tarPatch = requireSingleReviewedDockerfileRunCommand(
+ source,
+ "node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
+ npmRootArguments,
+ ).commandStart;
+ const bracePatch = requireSingleReviewedDockerfileRunCommand(
+ source,
+ "node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts",
+ npmRootArguments,
+ ).commandStart;
+ const ipAddressPatch = requireSingleReviewedDockerfileRunCommand(
+ source,
+ patchCommand,
+ npmRootArguments,
);
- const bracePatch = source.indexOf(
- "RUN node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts",
- );
- const ipAddressPatch = source.indexOf(patchCommand);
expect(copy, file).toBeGreaterThanOrEqual(0);
expect(tarPatch, file).toBeGreaterThan(copy);
expect(bracePatch, file).toBeGreaterThan(tarPatch);
- expect(ipAddressPatch, file).toBeGreaterThan(bracePatch);
- expect(instructionBody(source, ipAddressPatch), file).toContain(
- "--npm-root /usr/local/lib/node_modules/npm",
- );
+ expect(ipAddressPatch.commandStart, file).toBeGreaterThan(bracePatch);
});
});
diff --git a/test/corporate-ca-build-tls-anchor.test.ts b/test/corporate-ca-build-tls-anchor.test.ts
index 1a4ca5174b6..cb168608505 100644
--- a/test/corporate-ca-build-tls-anchor.test.ts
+++ b/test/corporate-ca-build-tls-anchor.test.ts
@@ -4,6 +4,7 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
+import { dockerfileInstructions } from "./helpers/dockerfile-run-commands";
const DOCKERFILE = join(import.meta.dirname, "../Dockerfile");
@@ -173,3 +174,88 @@ describe("DCode corporate proxy CA cold-build trust (#8119)", () => {
expect(curlAnchorIndex).toBeLessThan(ipAddressPatchIndex);
});
});
+
+describe("Hermes corporate proxy CA final-stage trust", () => {
+ const dockerfile = readFileSync(
+ join(import.meta.dirname, "../agents/hermes/Dockerfile"),
+ "utf-8",
+ );
+
+ // source-shape-contract: security -- Hermes final-stage registry clients must trust the decoded corporate CA before making HTTPS requests
+ it("uses the corporate CA conditionally for all Hermes registry remediations", () => {
+ const finalFromIndex = dockerfile.indexOf("FROM ${BASE_IMAGE}");
+ const finalStage = dockerfile.slice(finalFromIndex);
+ const argIndex = finalStage.indexOf("ARG NEMOCLAW_CORPORATE_CA_B64");
+ const decodeIndex = finalStage.indexOf(
+ 'RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then',
+ argIndex,
+ );
+ const nodeAnchorIndex = finalStage.indexOf(
+ "ENV NODE_EXTRA_CA_CERTS=/usr/local/share/nemoclaw/corporate-ca.pem",
+ decodeIndex,
+ );
+ const payloadCopyIndex = finalStage.indexOf(
+ "COPY --from=hermes-npm-patch-payload / /",
+ nodeAnchorIndex,
+ );
+ const conditionalCurlTrust = `RUN if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \\
+ export CURL_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \\
+ fi; \\`;
+ const remediationCommands = [
+ "node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
+ "node --experimental-strip-types /scripts/patch-bundled-npm-brace-expansion.mts",
+ "node --experimental-strip-types /scripts/lib/patch-bundled-npm-ip-address.mts",
+ ];
+ const agentInstallCommand =
+ "node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts --agent hermes --phase agent-install";
+ const packageInstallRun = dockerfileInstructions(finalStage).find(
+ (instruction) =>
+ instruction.keyword === "RUN" && instruction.body.includes(agentInstallCommand),
+ );
+ const expectedPackageInstallRun = [
+ "RUN unset SSL_CERT_FILE REQUESTS_CA_BUNDLE; \\",
+ " if [ -f /usr/local/share/nemoclaw/corporate-ca.pem ]; then \\",
+ " export SSL_CERT_FILE=/usr/local/share/nemoclaw/corporate-ca.pem; \\",
+ " export REQUESTS_CA_BUNDLE=/usr/local/share/nemoclaw/corporate-ca.pem; \\",
+ " fi; \\",
+ ` ${agentInstallCommand} \\`,
+ ' && if [ "$NEMOCLAW_MANAGED_IMAGE_CAPABILITY_UNION" = "1" ]; then \\',
+ " node --experimental-strip-types /src/lib/messaging/applier/build/messaging-build-applier.mts \\",
+ " --agent hermes --phase managed-image-capability-union; \\",
+ " fi",
+ "",
+ ].join("\n");
+ const npmCommandIndexes = [...finalStage.matchAll(/^\s*npm\s+(?:ci|run)\b/gmu)].map(
+ (match) => match.index,
+ );
+
+ for (const [name, index] of Object.entries({
+ finalFromIndex,
+ argIndex,
+ decodeIndex,
+ nodeAnchorIndex,
+ payloadCopyIndex,
+ })) {
+ expect(index, name).toBeGreaterThan(-1);
+ }
+ expect(argIndex).toBeLessThan(decodeIndex);
+ expect(decodeIndex).toBeLessThan(nodeAnchorIndex);
+ expect(nodeAnchorIndex).toBeLessThan(payloadCopyIndex);
+ for (const remediationCommand of remediationCommands) {
+ const remediationIndex = finalStage.indexOf(remediationCommand, payloadCopyIndex);
+ expect(remediationIndex, remediationCommand).toBeGreaterThan(payloadCopyIndex);
+ const runIndex = finalStage.lastIndexOf("\nRUN ", remediationIndex) + 1;
+ expect(runIndex, remediationCommand).toBeGreaterThan(payloadCopyIndex);
+ expect(finalStage.slice(runIndex, remediationIndex).trim(), remediationCommand).toBe(
+ conditionalCurlTrust,
+ );
+ }
+ expect(npmCommandIndexes.length).toBeGreaterThan(0);
+ for (const npmCommandIndex of npmCommandIndexes) {
+ expect(nodeAnchorIndex).toBeLessThan(npmCommandIndex);
+ }
+ expect(packageInstallRun?.text).toBe(expectedPackageInstallRun);
+ expect(packageInstallRun?.text).not.toContain("else");
+ expect(finalStage.match(/^ENV (?:SSL_CERT_FILE|REQUESTS_CA_BUNDLE)=/gmu) ?? []).toEqual([]);
+ });
+});
diff --git a/test/dockerfile-run-commands.test.ts b/test/dockerfile-run-commands.test.ts
new file mode 100644
index 00000000000..1aa4f3eb85b
--- /dev/null
+++ b/test/dockerfile-run-commands.test.ts
@@ -0,0 +1,108 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { describe, expect, it } from "vitest";
+import { requireSingleReviewedDockerfileRunCommand } from "./helpers/dockerfile-run-commands";
+
+const command = "node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts";
+const corporateCaPath = "/usr/local/share/nemoclaw/corporate-ca.pem";
+const requiredArguments = ["--npm-root", "/usr/local/lib/node_modules/npm"] as const;
+const invocation = [command, ...requiredArguments].join(" ");
+const splicedCommand = command.replace("strip-types", "strip-\\\ntypes");
+
+describe("Dockerfile RUN command discovery", () => {
+ it("ignores command text in comments, strings, and non-RUN instructions", () => {
+ const source = [
+ `# ${command}`,
+ `LABEL remediation=\"${command}\"`,
+ `RUN printf '%s\\n' '${command}'`,
+ `RUN printf '%s\\n' complete # ${command}`,
+ "",
+ ].join("\n");
+
+ expect(() =>
+ requireSingleReviewedDockerfileRunCommand(source, command, requiredArguments),
+ ).toThrow("Expected one reviewed RUN command");
+ });
+
+ it("accepts the reviewed command and arguments as a direct RUN instruction", () => {
+ const source = `RUN ${invocation}\nENV NEXT=instruction\n`;
+
+ const match = requireSingleReviewedDockerfileRunCommand(source, command, requiredArguments);
+
+ expect(match.commandStart).toBe(source.indexOf(command));
+ expect(match.instruction.text).toBe(`RUN ${invocation}\n`);
+ });
+
+ it("finds a command after a guard in one complete multiline RUN instruction", () => {
+ const continuation = "\\";
+ const source = [
+ `RUN if [ -f ${corporateCaPath} ]; then ${continuation}`,
+ ` export CURL_CA_BUNDLE=${corporateCaPath}; ${continuation}`,
+ ` fi; ${continuation}`,
+ ` ${command} ${continuation}`,
+ ` ${requiredArguments.join(" ")}`,
+ "ENV NEXT=instruction",
+ "",
+ ].join("\n");
+
+ const match = requireSingleReviewedDockerfileRunCommand(source, command, requiredArguments);
+
+ expect(match.commandStart).toBe(source.indexOf(command));
+ expect(match.instruction.text).toContain(`export CURL_CA_BUNDLE=${corporateCaPath}`);
+ expect(match.instruction.text).toContain("--npm-root /usr/local/lib/node_modules/npm");
+ expect(match.instruction.text).not.toContain("ENV NEXT=instruction");
+ });
+
+ it("reports an extra unguarded command instead of selecting one occurrence", () => {
+ const source = [`RUN ${invocation}`, `RUN ${invocation}`, ""].join("\n");
+
+ expect(() =>
+ requireSingleReviewedDockerfileRunCommand(source, command, requiredArguments),
+ ).toThrow("found 2");
+ });
+
+ it.each([
+ ["inside a command substitution", `RUN printf '%s\\n' "$(${invocation})"\n`],
+ ["inside backticks", `RUN printf '%s\\n' \`${invocation}\`\n`],
+ ["after a parameter-length expansion", `RUN : \${#PATH}; ${invocation}\n`],
+ ["inside a split command substitution", `RUN printf '%s\\n' "$\\\n(${invocation})"\n`],
+ ["after a split parameter-length expansion", `RUN : $\\\r\n{#PATH}; ${invocation}\n`],
+ ["with a spliced command token", `RUN ${splicedCommand} ${requiredArguments.join(" ")}\n`],
+ ])("rejects an extra invocation %s", (_label, hiddenInvocation) => {
+ const source = [`RUN ${invocation}`, hiddenInvocation].join("\n");
+
+ expect(() =>
+ requireSingleReviewedDockerfileRunCommand(source, command, requiredArguments),
+ ).toThrow("unreviewed RUN instruction");
+ });
+
+ it.each([
+ ["a short-circuit branch", `RUN false && ${invocation}\n`],
+ ["an uncalled function", `RUN patch() { ${invocation}; }; true\n`],
+ ["an unreachable conditional branch", `RUN if false; then ${invocation}; fi\n`],
+ ])("rejects the reviewed command inside %s", (_label, source) => {
+ expect(() =>
+ requireSingleReviewedDockerfileRunCommand(source, command, requiredArguments),
+ ).toThrow("unreviewed RUN instruction");
+ });
+
+ it.each([
+ ["before the command", `RUN printf '%s' '${requiredArguments.join(" ")}'; ${command}\n`],
+ ["in one quoted value", `RUN ${command} '${requiredArguments.join(" ")}'\n`],
+ ["in a comment", `RUN ${command} # ${requiredArguments.join(" ")}\n`],
+ ])("rejects required arguments that occur %s", (_label, source) => {
+ expect(() =>
+ requireSingleReviewedDockerfileRunCommand(source, command, requiredArguments),
+ ).toThrow("unreviewed RUN instruction");
+ });
+
+ it.each([
+ ["between the command and arguments", `RUN ${command}\u00a0${requiredArguments.join(" ")}\n`],
+ ["after the arguments", `RUN ${invocation}\u00a0\n`],
+ ])("rejects non-shell whitespace %s", (_label, source) => {
+ expect(() =>
+ requireSingleReviewedDockerfileRunCommand(source, command, requiredArguments),
+ ).toThrow("unreviewed RUN instruction");
+ });
+});
diff --git a/test/e2e/support/sandbox-images-workflow-boundary.test.ts b/test/e2e/support/sandbox-images-workflow-boundary.test.ts
index c7c2c9712e3..b76ac91d0f6 100644
--- a/test/e2e/support/sandbox-images-workflow-boundary.test.ts
+++ b/test/e2e/support/sandbox-images-workflow-boundary.test.ts
@@ -153,6 +153,27 @@ describe("sandbox image workflow boundary", () => {
);
});
+ it("requires the canonical no-CA Hermes build and its default-trust image proof", () => {
+ const { imageWorkflow, mainWorkflow } = readWorkflows();
+ const producer = imageWorkflow.jobs["build-hermes-sandbox-image"];
+ const build = producer.steps!.find((step) => step.name === "Build Hermes production image")!;
+ build.with!["build-args"] = `${build.with!["build-args"]}\nNEMOCLAW_CORPORATE_CA_B64=test`;
+ const proof = producer.steps!.find(
+ (step) => step.name === "Verify Hermes default-trust final image",
+ )!;
+ proof.run = proof.run!.replace(
+ "test ! -e /usr/local/share/nemoclaw/corporate-ca.pem",
+ "test -e /usr/local/share/nemoclaw/corporate-ca.pem",
+ );
+
+ expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toEqual(
+ expect.arrayContaining([
+ "Hermes producer must build the production image exactly once with the canonical local-load Buildx action and OS/architecture-scoped GHA cache",
+ "Hermes producer must prove the no-CA final image uses default trust before completed-image scans",
+ ]),
+ );
+ });
+
it("rejects non-canonical Hermes Buildx action pins", () => {
for (const stepName of ["Set up Docker Buildx", "Build Hermes production image"]) {
const { imageWorkflow, mainWorkflow } = readWorkflows();
@@ -537,10 +558,20 @@ describe("sandbox image workflow boundary", () => {
const hermes = probe.steps!.find(
(step) => step.name === "Build and verify Hermes messaging plan boundary",
)!;
- hermes.run = hermes.run!.replace(
- "check-messaging-plan-image-boundary.mts verify",
- "check-messaging-plan-image-boundary.mts bypass",
- );
+ hermes.run = hermes
+ .run!.replace(
+ 'scripts/check-production-build-args.sh "${build_args[@]}"',
+ 'echo "guard bypassed"',
+ )
+ .replace(
+ '--build-arg "NEMOCLAW_CORPORATE_CA_B64=${corporate_ca_b64}"',
+ '--build-arg "NEMOCLAW_CORPORATE_CA_B64="',
+ )
+ .replace("crl2pkcs7 -nocrl", "version")
+ .replace(
+ "check-messaging-plan-image-boundary.mts verify",
+ "check-messaging-plan-image-boundary.mts bypass",
+ );
expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toEqual(
expect.arrayContaining([
@@ -549,12 +580,103 @@ describe("sandbox image workflow boundary", () => {
"messaging plan image boundary must set up Node exactly once",
"messaging plan image boundary must use Node 22.19.0",
'openclaw messaging plan image boundary must include scripts/check-production-build-args.sh "${build_args[@]}"',
+ 'hermes messaging plan image boundary must include scripts/check-production-build-args.sh "${build_args[@]}"',
+ 'hermes messaging plan image boundary must include --build-arg "NEMOCLAW_CORPORATE_CA_B64=${corporate_ca_b64}"',
+ "hermes messaging plan image boundary must include docker run --rm --network none --entrypoint openssl nemoclaw-hermes-plan-boundary crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem -out /dev/null",
"hermes messaging plan image boundary must include node --experimental-strip-types scripts/check-messaging-plan-image-boundary.mts verify nemoclaw-hermes-plan-boundary hermes",
"messaging plan image boundary must not publish probe image artifacts",
]),
);
});
+ it("requires the exact compact CA root helper invocation", () => {
+ const { imageWorkflow, mainWorkflow } = readWorkflows();
+ const hermes = imageWorkflow.jobs["messaging-plan-image-boundary"].steps!.find(
+ (step) => step.name === "Build and verify Hermes messaging plan boundary",
+ )!;
+ hermes.run = hermes.run!.replace(
+ "select-ci-endpoint-ca-roots.mts",
+ "select-ci-endpoint-ca-roots.mts --endpoint registry.example.invalid",
+ );
+
+ expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toContain(
+ 'hermes messaging plan image boundary must include exactly node --experimental-strip-types scripts/checks/select-ci-endpoint-ca-roots.mts --output "$compact_ca_bundle"',
+ );
+ });
+
+ it("rejects direct base64 encoding of the broad system CA bundle", () => {
+ for (const forbidden of [
+ 'forbidden_ca_b64="$(base64 -w 0 "$system_ca_bundle")"',
+ [
+ "corporate_ca_bundle=/etc/ssl/certs/ca-certificates.crt",
+ 'forbidden_ca_b64="$(base64 -w 0 "$corporate_ca_bundle")"',
+ ].join("\n"),
+ ]) {
+ const { imageWorkflow, mainWorkflow } = readWorkflows();
+ const hermes = imageWorkflow.jobs["messaging-plan-image-boundary"].steps!.find(
+ (step) => step.name === "Build and verify Hermes messaging plan boundary",
+ )!;
+ hermes.run = `${hermes.run}\n${forbidden}`;
+
+ expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toContain(
+ "hermes messaging plan image boundary must not encode the system CA bundle directly",
+ );
+ }
+ });
+
+ it("requires offline equality and parse proofs for the installed Hermes CA bundle", () => {
+ const { imageWorkflow, mainWorkflow } = readWorkflows();
+ const hermes = imageWorkflow.jobs["messaging-plan-image-boundary"].steps!.find(
+ (step) => step.name === "Build and verify Hermes messaging plan boundary",
+ )!;
+ hermes.run = hermes
+ .run!.replace(
+ 'test "$installed_ca_sha256" = "$corporate_ca_sha256"',
+ 'test -n "$installed_ca_sha256"',
+ )
+ .replace("crl2pkcs7 -nocrl", "version");
+
+ expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toEqual(
+ expect.arrayContaining([
+ 'hermes messaging plan image boundary must include test "$installed_ca_sha256" = "$corporate_ca_sha256"',
+ "hermes messaging plan image boundary must include docker run --rm --network none --entrypoint openssl nemoclaw-hermes-plan-boundary crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem -out /dev/null",
+ ]),
+ );
+ });
+
+ it("requires the Hermes build guard before the build and offline CA proofs", () => {
+ const { imageWorkflow, mainWorkflow } = readWorkflows();
+ const hermes = imageWorkflow.jobs["messaging-plan-image-boundary"].steps!.find(
+ (step) => step.name === "Build and verify Hermes messaging plan boundary",
+ )!;
+ const guard = 'scripts/check-production-build-args.sh "${build_args[@]}"';
+ hermes.run = `${hermes.run!.replace(guard, "")}\n${guard}`;
+
+ expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toEqual(
+ expect.arrayContaining([
+ "hermes messaging plan image boundary steps are out of order",
+ "hermes messaging plan image boundary CA fixture steps are out of order",
+ ]),
+ );
+ });
+
+ it("rejects the Hermes CA build argument after the image build", () => {
+ const { imageWorkflow, mainWorkflow } = readWorkflows();
+ const hermes = imageWorkflow.jobs["messaging-plan-image-boundary"].steps!.find(
+ (step) => step.name === "Build and verify Hermes messaging plan boundary",
+ )!;
+ const buildArg = '--build-arg "NEMOCLAW_CORPORATE_CA_B64=${corporate_ca_b64}"';
+ const buildCommand = 'docker build "${build_args[@]}" -t nemoclaw-hermes-plan-boundary .';
+ expect(hermes.run).toContain(buildArg);
+ hermes.run = hermes
+ .run!.replace(buildArg, "")
+ .replace(buildCommand, `${buildCommand}\n${buildArg}`);
+
+ expect(validateSandboxImagesWorkflow(imageWorkflow, mainWorkflow)).toContain(
+ "hermes messaging plan image boundary CA fixture steps are out of order",
+ );
+ });
+
it("requires bounded swap before every hosted Hermes image export", () => {
const { imageWorkflow, mainWorkflow } = readWorkflows();
for (const jobName of ["build-hermes-sandbox-image", "messaging-plan-image-boundary"]) {
diff --git a/test/helpers/dockerfile-run-commands.ts b/test/helpers/dockerfile-run-commands.ts
new file mode 100644
index 00000000000..723d505a633
--- /dev/null
+++ b/test/helpers/dockerfile-run-commands.ts
@@ -0,0 +1,188 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+export interface DockerfileInstruction {
+ readonly body: string;
+ readonly bodyStart: number;
+ readonly end: number;
+ readonly keyword: string;
+ readonly start: number;
+ readonly text: string;
+}
+
+export interface ReviewedDockerfileRunCommand {
+ readonly commandStart: number;
+ readonly instruction: DockerfileInstruction;
+}
+
+const CORPORATE_CA_PATH = "/usr/local/share/nemoclaw/corporate-ca.pem";
+const CORPORATE_CA_GUARD = `if [ -f ${CORPORATE_CA_PATH} ]; then export CURL_CA_BUNDLE=${CORPORATE_CA_PATH}; fi;`;
+
+function lineEnd(source: string, start: number): number {
+ const newline = source.indexOf("\n", start);
+ return newline === -1 ? source.length : newline + 1;
+}
+
+function continuesInstruction(line: string): boolean {
+ const content = line.replace(/\r?\n$/u, "").trimEnd();
+ let escapeCount = 0;
+ for (let index = content.length - 1; index >= 0 && content[index] === "\\"; index -= 1) {
+ escapeCount += 1;
+ }
+ return escapeCount % 2 === 1;
+}
+
+export function dockerfileInstructions(source: string): DockerfileInstruction[] {
+ const instructions: DockerfileInstruction[] = [];
+ let offset = 0;
+
+ while (offset < source.length) {
+ const endOfFirstLine = lineEnd(source, offset);
+ const firstLine = source.slice(offset, endOfFirstLine);
+ const instructionMatch = firstLine.match(/^[ \t]*([A-Za-z]+)(?:[ \t]+|(?=\r?$))/u);
+ if (instructionMatch === null) {
+ offset = endOfFirstLine;
+ continue;
+ }
+
+ let end = endOfFirstLine;
+ let currentLine = firstLine;
+ while (continuesInstruction(currentLine)) {
+ if (end >= source.length) {
+ throw new Error(`Dockerfile ends inside the ${instructionMatch[1]} instruction`);
+ }
+ const nextEnd = lineEnd(source, end);
+ currentLine = source.slice(end, nextEnd);
+ end = nextEnd;
+ }
+
+ const bodyStart = offset + instructionMatch[0].length;
+ instructions.push({
+ body: source.slice(bodyStart, end),
+ bodyStart,
+ end,
+ keyword: instructionMatch[1].toUpperCase(),
+ start: offset,
+ text: source.slice(offset, end),
+ });
+ offset = end;
+ }
+
+ return instructions;
+}
+
+function collapseDockerfileContinuations(source: string): {
+ readonly originalIndexes: readonly number[];
+ readonly text: string;
+} {
+ const characters: string[] = [];
+ const originalIndexes: number[] = [];
+
+ for (let index = 0; index < source.length; index += 1) {
+ if (source[index] === "\\" && source[index + 1] === "\n") {
+ index += 1;
+ continue;
+ }
+ if (source[index] === "\\" && source[index + 1] === "\r" && source[index + 2] === "\n") {
+ index += 2;
+ continue;
+ }
+ characters.push(source[index]);
+ originalIndexes.push(index);
+ }
+
+ return { originalIndexes, text: characters.join("") };
+}
+
+function unquotedTextIndexes(source: string, text: string): number[] {
+ const indexes: number[] = [];
+ let quote: "'" | '"' | "`" | null = null;
+ let comment = false;
+
+ for (let index = 0; index < source.length; index += 1) {
+ const character = source[index];
+ if (comment) {
+ if (character === "\n") comment = false;
+ continue;
+ }
+ if (quote !== null) {
+ if (character === "\\" && quote !== "'") {
+ index += 1;
+ } else if (character === quote) {
+ quote = null;
+ }
+ continue;
+ }
+ if (character === "'" || character === '"' || character === "`") {
+ quote = character;
+ continue;
+ }
+ if (character === "\\") {
+ index += 1;
+ continue;
+ }
+ if (character === "#" && (index === 0 || /[\s;&|(){}]/u.test(source[index - 1]))) {
+ comment = true;
+ continue;
+ }
+ if (!source.startsWith(text, index)) continue;
+ indexes.push(index);
+ index += text.length - 1;
+ }
+
+ return indexes;
+}
+
+function normalizedInstructionBody(source: string): string {
+ return source
+ .replace(/\\\r?\n/gu, " ")
+ .replace(/[ \t\r\n]+/gu, " ")
+ .replace(/^[ \t\r\n]+|[ \t\r\n]+$/gu, "");
+}
+
+export function requireSingleReviewedDockerfileRunCommand(
+ source: string,
+ command: string,
+ requiredArguments: readonly string[],
+): ReviewedDockerfileRunCommand {
+ const invocation = [command, ...requiredArguments].join(" ");
+ const reviewedBodies = new Set([invocation, `${CORPORATE_CA_GUARD} ${invocation}`]);
+ const matches: ReviewedDockerfileRunCommand[] = [];
+ let unreviewedInstructions = 0;
+
+ for (const instruction of dockerfileInstructions(source)) {
+ if (instruction.keyword !== "RUN") continue;
+ const collapsed = collapseDockerfileContinuations(instruction.body);
+ const containsCommand = collapsed.text.includes(command);
+ const hasUnsupportedShellConstruct = ["$(", "${", "`"].some((token) =>
+ collapsed.text.includes(token),
+ );
+ if (containsCommand && hasUnsupportedShellConstruct) {
+ unreviewedInstructions += 1;
+ continue;
+ }
+ const commandIndexes = unquotedTextIndexes(collapsed.text, command);
+ if (commandIndexes.length === 0) continue;
+ if (
+ commandIndexes.length !== 1 ||
+ !reviewedBodies.has(normalizedInstructionBody(instruction.body))
+ ) {
+ unreviewedInstructions += 1;
+ continue;
+ }
+ matches.push({
+ commandStart: instruction.bodyStart + collapsed.originalIndexes[commandIndexes[0]],
+ instruction,
+ });
+ }
+
+ if (unreviewedInstructions > 0) {
+ throw new Error(
+ `Expected '${invocation}' only as a direct RUN or the reviewed corporate CA guarded RUN; found ${unreviewedInstructions} unreviewed RUN instruction(s)`,
+ );
+ }
+ if (matches.length !== 1) {
+ throw new Error(`Expected one reviewed RUN command '${invocation}', found ${matches.length}`);
+ }
+ return matches[0];
+}
diff --git a/test/hermes-final-image-layout.test.ts b/test/hermes-final-image-layout.test.ts
index 93867396d2b..0ed2c5aa3bf 100644
--- a/test/hermes-final-image-layout.test.ts
+++ b/test/hermes-final-image-layout.test.ts
@@ -6,11 +6,13 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
+import { requireSingleReviewedDockerfileRunCommand } from "./helpers/dockerfile-run-commands";
import { dockerRunCommandBetween, runDockerShell } from "./helpers/hermes-dockerfile-run";
import { expectManagedBootstrapNativeImageContract } from "./support/managed-bootstrap-image-contract";
const ROOT = path.resolve(import.meta.dirname, "..");
const HERMES_DOCKERFILE = path.join(ROOT, "agents", "hermes", "Dockerfile");
+const NPM_ROOT_ARGUMENTS = ["--npm-root", "/usr/local/lib/node_modules/npm"] as const;
const HERMES_INTEGRITY_FILES = [
{
arg: "NEMOCLAW_HERMES_IMAGE_BUILD_PROBES_SHA256",
@@ -317,10 +319,11 @@ describe("Hermes final image layout", () => {
const runtime = indexOfRequired(finalStage, runtimeCopy);
const wrapper = indexOfRequired(finalStage, wrapperCopy);
const scan = indexOfRequired(finalStage, scanCopy);
- const tarPatch = indexOfRequired(
+ const tarPatch = requireSingleReviewedDockerfileRunCommand(
finalStage,
- "RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
- );
+ "node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
+ NPM_ROOT_ARGUMENTS,
+ ).commandStart;
const certifiInstall = indexOfRequired(finalStage, "RUN _hermes_certifi=");
const agentChmod = indexOfRequired(
finalStage,
diff --git a/test/node-tar-dockerfile-contract.test.ts b/test/node-tar-dockerfile-contract.test.ts
index 80ee6da1289..0e4eca19c38 100644
--- a/test/node-tar-dockerfile-contract.test.ts
+++ b/test/node-tar-dockerfile-contract.test.ts
@@ -7,6 +7,7 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import { NODE_BASES_REQUIRING_BUNDLED_NPM_TAR_PATCH } from "../scripts/patch-bundled-npm-tar.mts";
+import { requireSingleReviewedDockerfileRunCommand } from "./helpers/dockerfile-run-commands";
const repoRoot = path.resolve(import.meta.dirname, "..");
const dockerfiles = [
@@ -29,6 +30,8 @@ const dockerfiles = [
installsWithNpm: false,
},
] as const;
+const patchCommand = "node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts";
+const npmRootArguments = ["--npm-root", "/usr/local/lib/node_modules/npm"] as const;
function completedStage(source: string): string {
const finalStageStart = [...source.matchAll(/^FROM\b/gmu)].at(-1)?.index;
@@ -61,9 +64,11 @@ describe("node-tar image remediation contract", () => {
])("installs curl before patching the bundled npm tar in $file", (file) => {
const source = completedStage(fs.readFileSync(path.join(repoRoot, file), "utf8"));
const curlInstall = source.indexOf("curl=");
- const patchRun = source.indexOf(
- "RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
- );
+ const patchRun = requireSingleReviewedDockerfileRunCommand(
+ source,
+ patchCommand,
+ npmRootArguments,
+ ).commandStart;
expect(curlInstall, file).toBeGreaterThanOrEqual(0);
expect(patchRun, file).toBeGreaterThan(curlInstall);
@@ -90,9 +95,11 @@ describe("node-tar image remediation contract", () => {
const patchCopy = patchInputStage.indexOf(
"COPY scripts/patch-bundled-npm-tar.mts /scripts/patch-bundled-npm-tar.mts",
);
- const patchRun = source.indexOf(
- "RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
- );
+ const patchRun = requireSingleReviewedDockerfileRunCommand(
+ source,
+ patchCommand,
+ npmRootArguments,
+ ).commandStart;
const scanCopy = scanInputStage.indexOf(
"COPY scripts/checks/node-tar-image-scan.mts /scripts/checks/node-tar-image-scan.mts",
);
@@ -151,15 +158,19 @@ describe("reviewed npm image remediation contract", () => {
{ file: "agents/langchain-deepagents-code/Dockerfile.base", installsWithNpm: false },
])("upgrades npm before use in $file", ({ file, installsWithNpm }) => {
const source = completedStage(fs.readFileSync(path.join(repoRoot, file), "utf8"));
- const patchRun = source.indexOf(
- "RUN node --experimental-strip-types /scripts/patch-bundled-npm-tar.mts",
- );
+ const patchRun = requireSingleReviewedDockerfileRunCommand(
+ source,
+ patchCommand,
+ npmRootArguments,
+ ).commandStart;
const upgradeCopy = source.indexOf(
"COPY scripts/upgrade-bundled-npm.mts /scripts/upgrade-bundled-npm.mts",
);
- const upgradeRun = source.indexOf(
- "RUN node --experimental-strip-types /scripts/upgrade-bundled-npm.mts",
- );
+ const upgradeRun = requireSingleReviewedDockerfileRunCommand(
+ source,
+ "node --experimental-strip-types /scripts/upgrade-bundled-npm.mts",
+ npmRootArguments,
+ ).commandStart;
expect(upgradeCopy, file).toBeGreaterThanOrEqual(0);
expect(patchRun, file).toBeGreaterThan(upgradeCopy);
diff --git a/test/select-ci-endpoint-ca-roots.test.ts b/test/select-ci-endpoint-ca-roots.test.ts
new file mode 100644
index 00000000000..e30a207094e
--- /dev/null
+++ b/test/select-ci-endpoint-ca-roots.test.ts
@@ -0,0 +1,432 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import { spawnSync } from "node:child_process";
+import { X509Certificate } from "node:crypto";
+import fs from "node:fs";
+import path from "node:path";
+
+import { describe, expect, it, vi } from "vitest";
+
+import {
+ CI_CA_ENDPOINTS,
+ CI_CA_SYSTEM_BUNDLE,
+ MAX_CI_CA_CERTIFICATES,
+ MAX_CI_CA_ENCODED_BYTES,
+ normalizeCompactRootBundle,
+ type OpenSslRunner,
+ selectCiEndpointCaRoots,
+ writeCiEndpointCaRootsOutput,
+} from "../scripts/checks/select-ci-endpoint-ca-roots.mts";
+import { LEAF_PEM, PEM, tmpDir } from "../src/lib/onboard/__test-helpers__/corporate-ca-fixtures";
+
+const hasOpenSsl = spawnSync("openssl", ["version"], { encoding: "utf8" }).status === 0;
+
+function openssl(args: readonly string[], cwd: string): void {
+ const result = spawnSync("openssl", [...args], {
+ cwd,
+ encoding: "utf8",
+ killSignal: "SIGKILL",
+ timeout: 10_000,
+ });
+ expect(result.status, `OpenSSL fixture command failed: ${args[0]}`).toBe(0);
+}
+
+function createEndpointCertificate(directory: string): {
+ chain: string;
+ crossSignedRoot: string;
+ root: string;
+} {
+ fs.writeFileSync(
+ path.join(directory, "root.ext"),
+ [
+ "basicConstraints=critical,CA:TRUE",
+ "keyUsage=critical,keyCertSign,cRLSign",
+ "subjectKeyIdentifier=hash",
+ "authorityKeyIdentifier=keyid,issuer",
+ "",
+ ].join("\n"),
+ );
+ fs.writeFileSync(
+ path.join(directory, "leaf.ext"),
+ [
+ "basicConstraints=critical,CA:FALSE",
+ "keyUsage=critical,digitalSignature,keyEncipherment",
+ "extendedKeyUsage=serverAuth",
+ `subjectAltName=${CI_CA_ENDPOINTS.map((endpoint) => `DNS:${endpoint}`).join(",")}`,
+ "",
+ ].join("\n"),
+ );
+ openssl(
+ [
+ "req",
+ "-x509",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-subj",
+ "/CN=NemoClaw CI Root",
+ "-keyout",
+ "root.key",
+ "-out",
+ "root.pem",
+ "-days",
+ "2",
+ "-addext",
+ "basicConstraints=critical,CA:TRUE",
+ "-addext",
+ "keyUsage=critical,keyCertSign,cRLSign",
+ ],
+ directory,
+ );
+ openssl(
+ [
+ "req",
+ "-x509",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-subj",
+ "/CN=NemoClaw Alternate Root",
+ "-keyout",
+ "alternate-root.key",
+ "-out",
+ "alternate-root.pem",
+ "-days",
+ "2",
+ "-addext",
+ "basicConstraints=critical,CA:TRUE",
+ "-addext",
+ "keyUsage=critical,keyCertSign,cRLSign",
+ ],
+ directory,
+ );
+ openssl(
+ ["req", "-new", "-key", "root.key", "-subj", "/CN=NemoClaw CI Root", "-out", "root.csr"],
+ directory,
+ );
+ openssl(
+ [
+ "x509",
+ "-req",
+ "-in",
+ "root.csr",
+ "-CA",
+ "alternate-root.pem",
+ "-CAkey",
+ "alternate-root.key",
+ "-CAcreateserial",
+ "-out",
+ "root-cross-signed.pem",
+ "-days",
+ "2",
+ "-extfile",
+ "root.ext",
+ ],
+ directory,
+ );
+ openssl(
+ [
+ "req",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-subj",
+ `/CN=${CI_CA_ENDPOINTS[0]}`,
+ "-keyout",
+ "leaf.key",
+ "-out",
+ "leaf.csr",
+ ],
+ directory,
+ );
+ openssl(
+ [
+ "x509",
+ "-req",
+ "-in",
+ "leaf.csr",
+ "-CA",
+ "root.pem",
+ "-CAkey",
+ "root.key",
+ "-CAcreateserial",
+ "-out",
+ "leaf.pem",
+ "-days",
+ "2",
+ "-extfile",
+ "leaf.ext",
+ ],
+ directory,
+ );
+ const leaf = fs.readFileSync(path.join(directory, "leaf.pem"), "utf8").trim();
+ const crossSignedRoot = fs
+ .readFileSync(path.join(directory, "root-cross-signed.pem"), "utf8")
+ .trim();
+ return {
+ chain: `${leaf}\n${crossSignedRoot}\n`,
+ crossSignedRoot,
+ root: fs.readFileSync(path.join(directory, "root.pem"), "utf8"),
+ };
+}
+
+describe("CI endpoint CA root selection", () => {
+ it("keeps the endpoint set and build-argument limits fixed", () => {
+ expect(CI_CA_SYSTEM_BUNDLE).toBe("/etc/ssl/certs/ca-certificates.crt");
+ expect(CI_CA_ENDPOINTS).toEqual(["registry.npmjs.org", "pypi.org", "files.pythonhosted.org"]);
+ expect(MAX_CI_CA_CERTIFICATES).toBe(24);
+ expect(MAX_CI_CA_ENCODED_BYTES).toBe(65_536);
+ });
+
+ it("deduplicates CA roots and rejects leaf certificates or oversized output", () => {
+ expect(normalizeCompactRootBundle([PEM, PEM])).toBe(PEM);
+ expect(() => normalizeCompactRootBundle([LEAF_PEM])).toThrow(/CA:TRUE root/u);
+ expect(() =>
+ normalizeCompactRootBundle([PEM], { certificates: 0, encodedBytes: 65_536 }),
+ ).toThrow(/exceeds 0 certificates/u);
+ expect(() => normalizeCompactRootBundle([PEM], { certificates: 24, encodedBytes: 1 })).toThrow(
+ /exceeds 1 encoded bytes/u,
+ );
+ });
+
+ it("validates each CA output identity before truncating it", () => {
+ const output = path.join(tmpDir(), "compact.pem");
+ fs.writeFileSync(output, "unchanged", { mode: 0o644 });
+ const calls: string[] = [];
+ const realLstatSync = fs.lstatSync.bind(fs);
+ const realOpenSync = fs.openSync.bind(fs);
+ const realFstatSync = fs.fstatSync.bind(fs);
+ const realFtruncateSync = fs.ftruncateSync.bind(fs);
+ vi.spyOn(fs, "lstatSync").mockImplementation((file) => {
+ calls.push("lstat");
+ return realLstatSync(file);
+ });
+ vi.spyOn(fs, "openSync").mockImplementation((file, flags, mode) => {
+ calls.push("open");
+ return realOpenSync(file, flags, mode);
+ });
+ vi.spyOn(fs, "fstatSync").mockImplementation((descriptor) => {
+ calls.push("fstat");
+ expect(fs.readFileSync(output, "utf8")).toBe("unchanged");
+ return realFstatSync(descriptor);
+ });
+ vi.spyOn(fs, "ftruncateSync").mockImplementation((descriptor, length) => {
+ calls.push("truncate");
+ expect(fs.readFileSync(output, "utf8")).toBe("unchanged");
+ return realFtruncateSync(descriptor, length);
+ });
+
+ writeCiEndpointCaRootsOutput(output, "replacement");
+
+ expect(calls).toEqual(["open", "fstat", "lstat", "truncate"]);
+ expect(fs.readFileSync(output, "utf8")).toBe("replacement");
+ expect(fs.statSync(output).mode & 0o777).toBe(0o600);
+ });
+
+ it.skipIf(process.platform === "win32")("rejects a FIFO CA output without truncating it", () => {
+ const output = path.join(tmpDir(), "compact.pem");
+ const created = spawnSync("mkfifo", [output], { encoding: "utf8", timeout: 5_000 });
+ expect(created.status, created.stderr).toBe(0);
+ const openSync = vi.spyOn(fs, "openSync");
+ const ftruncateSync = vi.spyOn(fs, "ftruncateSync");
+
+ expect(() => writeCiEndpointCaRootsOutput(output, "replacement")).toThrow(
+ "output must be an existing regular file that is not a symlink",
+ );
+
+ expect(openSync).toHaveBeenCalledOnce();
+ expect(ftruncateSync).not.toHaveBeenCalled();
+ expect(fs.lstatSync(output).isFIFO()).toBe(true);
+ });
+
+ it.skipIf(process.platform === "win32")(
+ "rejects a symlinked CA output without opening its target",
+ () => {
+ const directory = tmpDir();
+ const target = path.join(directory, "target.pem");
+ const output = path.join(directory, "compact.pem");
+ fs.writeFileSync(target, "target", { mode: 0o640 });
+ fs.symlinkSync(target, output);
+ const openSync = vi.spyOn(fs, "openSync");
+ const ftruncateSync = vi.spyOn(fs, "ftruncateSync");
+
+ expect(() => writeCiEndpointCaRootsOutput(output, "replacement")).toThrow(
+ "output must be an existing regular file that is not a symlink",
+ );
+
+ expect(openSync).toHaveBeenCalledOnce();
+ expect(ftruncateSync).not.toHaveBeenCalled();
+ expect(fs.lstatSync(output).isSymbolicLink()).toBe(true);
+ expect(fs.readFileSync(target, "utf8")).toBe("target");
+ expect(fs.statSync(target).mode & 0o777).toBe(0o640);
+ },
+ );
+
+ it.skipIf(process.platform === "win32")(
+ "rejects a device CA output without truncating it",
+ () => {
+ const ftruncateSync = vi.spyOn(fs, "ftruncateSync");
+ const writeFileSync = vi.spyOn(fs, "writeFileSync");
+ const fchmodSync = vi.spyOn(fs, "fchmodSync");
+
+ expect(() => writeCiEndpointCaRootsOutput("/dev/null", "replacement")).toThrow(
+ "output must remain the same regular file with exactly one link",
+ );
+
+ expect(ftruncateSync).not.toHaveBeenCalled();
+ expect(writeFileSync).not.toHaveBeenCalled();
+ expect(fchmodSync).not.toHaveBeenCalled();
+ },
+ );
+
+ it.skipIf(process.platform === "win32")(
+ "rejects a hard-linked CA output without changing either path",
+ () => {
+ const directory = tmpDir();
+ const output = path.join(directory, "compact.pem");
+ const linked = path.join(directory, "linked.pem");
+ fs.writeFileSync(output, "unchanged", { mode: 0o640 });
+ fs.linkSync(output, linked);
+ const openSync = vi.spyOn(fs, "openSync");
+ const ftruncateSync = vi.spyOn(fs, "ftruncateSync");
+
+ expect(() => writeCiEndpointCaRootsOutput(output, "replacement")).toThrow(
+ "output must remain the same regular file with exactly one link",
+ );
+
+ expect(openSync).toHaveBeenCalledOnce();
+ expect(ftruncateSync).not.toHaveBeenCalled();
+ for (const file of [output, linked]) {
+ expect(fs.readFileSync(file, "utf8")).toBe("unchanged");
+ expect(fs.statSync(file).mode & 0o777).toBe(0o640);
+ }
+ },
+ );
+
+ it("rejects a substituted CA output without changing either file", () => {
+ const directory = tmpDir();
+ const output = path.join(directory, "compact.pem");
+ const original = path.join(directory, "original.pem");
+ const replacement = path.join(directory, "replacement.pem");
+ fs.writeFileSync(output, "original", { mode: 0o640 });
+ fs.writeFileSync(replacement, "replacement", { mode: 0o604 });
+ const realFstatSync = fs.fstatSync.bind(fs);
+ vi.spyOn(fs, "fstatSync").mockImplementation((descriptor) => {
+ const stat = realFstatSync(descriptor);
+ fs.renameSync(output, original);
+ fs.renameSync(replacement, output);
+ return stat;
+ });
+ const ftruncateSync = vi.spyOn(fs, "ftruncateSync");
+
+ expect(() => writeCiEndpointCaRootsOutput(output, "written")).toThrow(
+ "output must remain the same regular file with exactly one link",
+ );
+
+ expect(ftruncateSync).not.toHaveBeenCalled();
+ expect(fs.readFileSync(original, "utf8")).toBe("original");
+ expect(fs.statSync(original).mode & 0o777).toBe(0o640);
+ expect(fs.readFileSync(output, "utf8")).toBe("replacement");
+ expect(fs.statSync(output).mode & 0o777).toBe(0o604);
+ });
+
+ it.skipIf(!hasOpenSsl)(
+ "selects a self-signed system root when the server sends its cross-signed form",
+ () => {
+ const directory = tmpDir();
+ const output = path.join(directory, "compact.pem");
+ fs.writeFileSync(output, "", { mode: 0o600 });
+ const fixture = createEndpointCertificate(directory);
+ const systemRoot = new X509Certificate(fixture.root);
+ const crossSignedRoot = new X509Certificate(fixture.crossSignedRoot);
+ expect(crossSignedRoot.subject).toBe(systemRoot.subject);
+ expect(crossSignedRoot.issuer).not.toBe(crossSignedRoot.subject);
+ expect(crossSignedRoot.publicKey.export({ format: "der", type: "spki" })).toEqual(
+ systemRoot.publicKey.export({ format: "der", type: "spki" }),
+ );
+ expect(crossSignedRoot.verify(crossSignedRoot.publicKey)).toBe(false);
+ const realReadFile = fs.readFileSync.bind(fs);
+ vi.spyOn(fs, "readFileSync").mockImplementation(((file, ...args) =>
+ file === CI_CA_SYSTEM_BUNDLE
+ ? fixture.root
+ : realReadFile(file, ...args)) as typeof fs.readFileSync);
+
+ const connections: string[][] = [];
+ const runConnection: OpenSslRunner = (args) => {
+ connections.push([...args]);
+ return {
+ status: 0,
+ stderr: "",
+ stdout: `${fixture.chain}Verify return code: 0 (ok)\n`,
+ };
+ };
+ const runActualOpenSsl: OpenSslRunner = (args) => {
+ const result = spawnSync("openssl", [...args], {
+ encoding: "utf8",
+ killSignal: "SIGKILL",
+ timeout: 10_000,
+ });
+ return {
+ error: result.error,
+ status: result.status,
+ stderr: result.stderr ?? "",
+ stdout: result.stdout ?? "",
+ };
+ };
+ const runner: OpenSslRunner = (args) =>
+ args[0] === "s_client" ? runConnection(args) : runActualOpenSsl(args);
+
+ expect(selectCiEndpointCaRoots(output, runner)).toEqual({
+ certificates: 1,
+ encodedBytes: Buffer.from(fixture.root).toString("base64").length,
+ });
+ expect(fs.readFileSync(output, "utf8")).toBe(fixture.root);
+ expect(connections).toHaveLength(CI_CA_ENDPOINTS.length * 2);
+ for (const endpoint of CI_CA_ENDPOINTS) {
+ const endpointConnections = connections.filter((args) => args.includes(`${endpoint}:443`));
+ expect(endpointConnections).toEqual([
+ [
+ "s_client",
+ "-connect",
+ `${endpoint}:443`,
+ "-servername",
+ endpoint,
+ "-verify_hostname",
+ endpoint,
+ "-verify_return_error",
+ "-CAfile",
+ CI_CA_SYSTEM_BUNDLE,
+ "-no-CApath",
+ "-no-CAstore",
+ "-showcerts",
+ ],
+ [
+ "s_client",
+ "-connect",
+ `${endpoint}:443`,
+ "-servername",
+ endpoint,
+ "-verify_hostname",
+ endpoint,
+ "-verify_return_error",
+ "-CAfile",
+ expect.stringMatching(/\/compact\.pem$/u),
+ "-no-CApath",
+ "-no-CAstore",
+ ],
+ ]);
+ }
+
+ fs.writeFileSync(output, "unchanged", { mode: 0o600 });
+ const rejectCompactVerification: OpenSslRunner = (args) =>
+ args[0] === "s_client" && !args.includes("-showcerts")
+ ? { status: 1, stderr: "verification failed", stdout: "" }
+ : runner(args);
+ expect(() => selectCiEndpointCaRoots(output, rejectCompactVerification)).toThrow(
+ /compact CA verification for registry\.npmjs\.org failed/u,
+ );
+ expect(fs.readFileSync(output, "utf8")).toBe("unchanged");
+ },
+ );
+});
diff --git a/tools/e2e/sandbox-images-workflow-boundary.mts b/tools/e2e/sandbox-images-workflow-boundary.mts
index 85c7a6a7b74..b1058190bc4 100644
--- a/tools/e2e/sandbox-images-workflow-boundary.mts
+++ b/tools/e2e/sandbox-images-workflow-boundary.mts
@@ -27,6 +27,7 @@ const HERMES_SETUP_BUILDX_ACTION =
"docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c";
const HERMES_BUILD_PUSH_ACTION =
"docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a";
+const HERMES_DEFAULT_TRUST_STEP_NAME = "Verify Hermes default-trust final image";
const HERMES_DOWNLOAD_ARTIFACT_ACTION =
"actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c";
const HERMES_UPLOAD_ARTIFACT_ACTION =
@@ -478,6 +479,30 @@ function validateGuardedProductionBuild(
"Hermes producer must build the production image exactly once with the canonical local-load Buildx action and OS/architecture-scoped GHA cache",
);
}
+ const defaultTrust = requireStep(errors, contract.jobName, job, HERMES_DEFAULT_TRUST_STEP_NAME);
+ const defaultTrustRun = normalizedShell(defaultTrust.run);
+ const requiredDefaultTrustFragments = [
+ "set -euo pipefail",
+ "docker run --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges --pids-limit 64 --memory 256m --entrypoint /bin/sh nemoclaw-hermes-production -eu -c",
+ 'test "$NODE_EXTRA_CA_CERTS" = /usr/local/share/nemoclaw/corporate-ca.pem',
+ "test ! -e /usr/local/share/nemoclaw/corporate-ca.pem",
+ "test ! -L /usr/local/share/nemoclaw/corporate-ca.pem",
+ "test -x /usr/local/bin/hermes",
+ 'node -e "const tls = require(\\"node:tls\\"); if (tls.rootCertificates.length === 0) process.exit(1); tls.createSecureContext()"',
+ '/opt/hermes/.venv/bin/python -I -c "import ssl; assert ssl.create_default_context().get_ca_certs()"',
+ ];
+ if (
+ steps(job).filter((step) => step.name === HERMES_DEFAULT_TRUST_STEP_NAME).length !== 1 ||
+ defaultTrust.shell !== "bash" ||
+ requiredDefaultTrustFragments.some((fragment) => !defaultTrustRun.includes(fragment)) ||
+ stepIndex(job, action.name ?? "") >= stepIndex(job, HERMES_DEFAULT_TRUST_STEP_NAME) ||
+ stepIndex(job, HERMES_DEFAULT_TRUST_STEP_NAME) >=
+ stepIndex(job, "Scan completed Hermes image for node-tar")
+ ) {
+ errors.push(
+ "Hermes producer must prove the no-CA final image uses default trust before completed-image scans",
+ );
+ }
return;
}
@@ -605,6 +630,7 @@ function validateMessagingPlanBoundaryBuild(
readonly agent: "hermes" | "openclaw";
readonly baseArgName: "BASE_IMAGE";
readonly baseEnvName: "BASE_IMAGE" | "HERMES_BASE_IMAGE";
+ readonly extraRequiredFragments?: readonly string[];
readonly stepName: string;
readonly target: string;
},
@@ -622,6 +648,7 @@ function validateMessagingPlanBoundaryBuild(
'scripts/check-production-build-args.sh "${build_args[@]}"',
`docker build \"\${build_args[@]}\" -t ${options.target} .`,
`node --experimental-strip-types scripts/check-messaging-plan-image-boundary.mts verify ${options.target} ${options.agent}`,
+ ...(options.extraRequiredFragments ?? []),
];
if (step.shell !== "bash" || !isDeepStrictEqual(record(step.env), expectedEnv)) {
@@ -647,6 +674,64 @@ function validateMessagingPlanBoundaryBuild(
}
}
+function validateHermesMessagingPlanCaFixture(
+ errors: string[],
+ job: SandboxImagesWorkflowJob,
+): void {
+ const step = findStep(job, "Build and verify Hermes messaging plan boundary");
+ const run = normalizedShell(step?.run);
+ const helperInvocation =
+ 'node --experimental-strip-types scripts/checks/select-ci-endpoint-ca-roots.mts --output "$compact_ca_bundle"';
+ const compactEncoding = 'corporate_ca_b64="$(base64 -w 0 "$compact_ca_bundle")"';
+ const sourceHash = 'corporate_ca_sha256="$(sha256sum "$compact_ca_bundle" | cut -d \' \' -f 1)"';
+ const corporateCaBuildArg = '--build-arg "NEMOCLAW_CORPORATE_CA_B64=${corporate_ca_b64}"';
+ const installedHash =
+ "installed_ca_sha256=\"$( docker run --rm --network none --entrypoint sha256sum nemoclaw-hermes-plan-boundary /usr/local/share/nemoclaw/corporate-ca.pem | cut -d ' ' -f 1 )\"";
+ const matchingHash = 'test "$installed_ca_sha256" = "$corporate_ca_sha256"';
+ const parseProof =
+ "docker run --rm --network none --entrypoint openssl nemoclaw-hermes-plan-boundary crl2pkcs7 -nocrl -certfile /usr/local/share/nemoclaw/corporate-ca.pem -out /dev/null";
+ const orderedFragments = [
+ 'compact_ca_bundle="$(mktemp)"',
+ "trap 'rm -f \"$compact_ca_bundle\"' EXIT",
+ helperInvocation,
+ compactEncoding,
+ sourceHash,
+ "check-messaging-plan-image-boundary.mts plan",
+ corporateCaBuildArg,
+ "check-production-build-args.sh",
+ 'docker build "${build_args[@]}" -t nemoclaw-hermes-plan-boundary',
+ installedHash,
+ matchingHash,
+ parseProof,
+ "check-messaging-plan-image-boundary.mts verify",
+ ];
+ for (const fragment of orderedFragments) {
+ if (!run.includes(fragment)) {
+ errors.push(`hermes messaging plan image boundary must include ${fragment}`);
+ }
+ }
+ if (
+ run.split("select-ci-endpoint-ca-roots.mts").length - 1 !== 1 ||
+ !run.includes(`${helperInvocation} ${compactEncoding}`)
+ ) {
+ errors.push(`hermes messaging plan image boundary must include exactly ${helperInvocation}`);
+ }
+ if (
+ run.includes("/etc/ssl/certs/ca-certificates.crt") ||
+ /base64 -w 0 "?\$\{?system_ca_bundle\}?"?/u.test(run)
+ ) {
+ errors.push(
+ "hermes messaging plan image boundary must not encode the system CA bundle directly",
+ );
+ }
+ const positions = orderedFragments.map((fragment) => run.indexOf(fragment));
+ if (
+ positions.some((position, index) => position < 0 || position <= (positions[index - 1] ?? -1))
+ ) {
+ errors.push("hermes messaging plan image boundary CA fixture steps are out of order");
+ }
+}
+
function validateMessagingPlanImageBoundary(
errors: string[],
workflow: SandboxImagesWorkflow,
@@ -685,9 +770,11 @@ function validateMessagingPlanImageBoundary(
agent: "hermes",
baseArgName: "BASE_IMAGE",
baseEnvName: "HERMES_BASE_IMAGE",
+ extraRequiredFragments: ['--build-arg "NEMOCLAW_CORPORATE_CA_B64=${corporate_ca_b64}"'],
stepName: "Build and verify Hermes messaging plan boundary",
target: "nemoclaw-hermes-plan-boundary",
});
+ validateHermesMessagingPlanCaFixture(errors, job);
const builds = dockerBuildLines(job);
if (