diff --git a/agents/langchain-deepagents-code/Dockerfile b/agents/langchain-deepagents-code/Dockerfile
index 004fc7b1faa..788ef50d6e4 100644
--- a/agents/langchain-deepagents-code/Dockerfile
+++ b/agents/langchain-deepagents-code/Dockerfile
@@ -15,7 +15,17 @@ ENV NPM_CONFIG_AUDIT=false \
NPM_CONFIG_UPDATE_NOTIFIER=false
WORKDIR /opt/mcp-tool-discovery-runtime
COPY tools/mcp-tool-discovery-runtime/package.json tools/mcp-tool-discovery-runtime/package-lock.json tools/mcp-tool-discovery-runtime/tsconfig.json tools/mcp-tool-discovery-runtime/install-reviewed-runtime.sh tools/mcp-tool-discovery-runtime/*.ts ./
-RUN ./install-reviewed-runtime.sh \
+# hadolint ignore=DL4006
+RUN set -eu; \
+ if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then \
+ { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /tmp/nemoclaw-corporate-ca.pem 2>/dev/null \
+ || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; }; \
+ node -e 'const fs = require("node:fs"); const { X509Certificate } = require("node:crypto"); const pem = fs.readFileSync(process.argv[1], "utf8"); const certificates = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g); if (!certificates?.length) process.exit(1); for (const certificate of certificates) if (!new X509Certificate(certificate).ca) process.exit(1);' /tmp/nemoclaw-corporate-ca.pem \
+ || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates with basicConstraints CA:TRUE (#6210)" >&2; exit 1; }; \
+ export NODE_EXTRA_CA_CERTS=/tmp/nemoclaw-corporate-ca.pem; \
+ fi; \
+ ./install-reviewed-runtime.sh \
+ && rm -f /tmp/nemoclaw-corporate-ca.pem \
&& rm -f ./install-reviewed-runtime.sh
RUN chown -R root:root /opt/mcp-tool-discovery-runtime \
&& chmod -R a=rX /opt/mcp-tool-discovery-runtime
@@ -23,6 +33,26 @@ RUN chown -R root:root /opt/mcp-tool-discovery-runtime \
# hadolint ignore=DL3006
FROM ${BASE_IMAGE}
+ARG NEMOCLAW_CORPORATE_CA_B64
+
+# Decode the host corporate-proxy CA (#6210) for runtime trust when onboarding
+# includes one in the final DCode image. Published or cached bases may not carry
+# the host-specific CA, so decode the argument again when it is present.
+# 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; }; \
+ install -d -o root -g root -m 0755 /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 \
+ && { node -e 'const fs = require("node:fs"); const { X509Certificate } = require("node:crypto"); const pem = fs.readFileSync(process.argv[1], "utf8"); const certificates = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g); if (!certificates?.length) process.exit(1); for (const certificate of certificates) if (!new X509Certificate(certificate).ca) process.exit(1);' /usr/local/share/nemoclaw/corporate-ca.pem \
+ || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates with basicConstraints CA:TRUE (#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 DCode image trust (#6210)"; \
+ fi
+
COPY --from=mcp-tool-discovery-runtime /opt/mcp-tool-discovery-runtime/dist/ /usr/local/lib/nemoclaw/mcp-tool-discovery-runtime/
RUN 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" \
diff --git a/agents/langchain-deepagents-code/Dockerfile.base b/agents/langchain-deepagents-code/Dockerfile.base
index c4396054206..0518a51ea6b 100644
--- a/agents/langchain-deepagents-code/Dockerfile.base
+++ b/agents/langchain-deepagents-code/Dockerfile.base
@@ -11,8 +11,13 @@ ARG PERL_VERSION=5.44.0
ARG PERL_SHA256=505cf43912e9480495c344c70260452e32aa2a73c546a026b3f100053b23ce91
ARG PERL_PACKAGE_REVISION=1nemoclaw1
+ARG NEMOCLAW_CORPORATE_CA_B64=
+
FROM node:22-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba AS native-security-builder
+ARG NEMOCLAW_CORPORATE_CA_B64
+
+# hadolint ignore=DL4006
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential=12.12 \
ca-certificates=20250419 \
@@ -22,6 +27,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
openssh-server=1:10.0p1-7+deb13u4 \
xz-utils=5.8.1-1+deb13u1 \
zlib1g-dev=1:1.3.dfsg+really1.3.1-1+b1 \
+ && if [ -n "${NEMOCLAW_CORPORATE_CA_B64:-}" ]; then \
+ install -d -o root -g root -m 0755 /usr/local/share/nemoclaw /usr/local/share/ca-certificates; \
+ { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /usr/local/share/nemoclaw/corporate-ca.pem 2>/dev/null \
+ || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; }; \
+ node -e 'const fs = require("node:fs"); const { X509Certificate } = require("node:crypto"); const pemPath = process.argv[1]; const anchorDir = process.argv[2]; const pem = fs.readFileSync(pemPath, "utf8"); const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g); if (!blocks?.length) process.exit(1); fs.writeFileSync(pemPath, blocks.map((block) => block.trim()).join("\n") + "\n"); blocks.forEach((block, index) => { if (!new X509Certificate(block).ca) process.exit(1); const name = anchorDir + "/nemoclaw-corporate-ca-" + String(index + 1).padStart(2, "0") + ".crt"; fs.writeFileSync(name, block.trim() + "\n"); });' /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates \
+ || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates with basicConstraints CA:TRUE (#6210)" >&2; exit 1; }; \
+ chown root:root /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt; \
+ chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt; \
+ update-ca-certificates; \
+ echo "[nemoclaw] trusted host corporate-proxy CA for native security builders (#6210)"; \
+ fi \
&& rm -rf /var/lib/apt/lists/*
COPY scripts/security/build-native-security-packages.sh /scripts/security/build-native-security-packages.sh
@@ -47,6 +63,8 @@ RUN bash /scripts/security/build-perl-security-packages.sh \
FROM node:22-trixie-slim@sha256:e6d9a389d34ff9678438af985c9913fbd1eb6ed36e80fea56644f4b4f6dd70ba
+ARG NEMOCLAW_CORPORATE_CA_B64
+
COPY --from=perl-builder /out /tmp/nemoclaw-native-security
COPY scripts/lib/reviewed-npm-archive.mts /scripts/lib/reviewed-npm-archive.mts
@@ -76,6 +94,18 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
e2fsprogs=1.47.2-3+b11 \
openssh-sftp-server=1:10.0p1-7+deb13u4 \
ripgrep=14.1.1-1+b4 \
+ && 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 base build image" >&2; exit 1; }; \
+ install -d -o root -g root -m 0755 /usr/local/share/nemoclaw /usr/local/share/ca-certificates; \
+ { printf '%s' "${NEMOCLAW_CORPORATE_CA_B64}" | base64 --decode > /usr/local/share/nemoclaw/corporate-ca.pem 2>/dev/null \
+ || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 is not valid base64; expected a single-line base64-encoded PEM (#6210)" >&2; exit 1; }; }; \
+ node -e 'const fs = require("node:fs"); const { X509Certificate } = require("node:crypto"); const pemPath = process.argv[1]; const anchorDir = process.argv[2]; const pem = fs.readFileSync(pemPath, "utf8"); const blocks = pem.match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g); if (!blocks?.length) process.exit(1); fs.writeFileSync(pemPath, blocks.map((block) => block.trim()).join("\n") + "\n"); blocks.forEach((block, index) => { if (!new X509Certificate(block).ca) process.exit(1); const name = anchorDir + "/nemoclaw-corporate-ca-" + String(index + 1).padStart(2, "0") + ".crt"; fs.writeFileSync(name, block.trim() + "\n"); });' /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates \
+ || { echo "[nemoclaw] NEMOCLAW_CORPORATE_CA_B64 did not decode to a bundle of valid X.509 certificates with basicConstraints CA:TRUE (#6210)" >&2; exit 1; }; \
+ chown root:root /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt; \
+ chmod 0444 /usr/local/share/nemoclaw/corporate-ca.pem /usr/local/share/ca-certificates/nemoclaw-corporate-ca-*.crt; \
+ update-ca-certificates; \
+ echo "[nemoclaw] baked host corporate-proxy CA into base image trust (#6210)"; \
+ fi \
&& arch="$(dpkg --print-architecture)" \
&& case "$arch" in \
amd64) \
diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json
index fd0e5a9da21..b85ee0a3c64 100644
--- a/ci/source-shape-test-budget.json
+++ b/ci/source-shape-test-budget.json
@@ -76,6 +76,21 @@
"test": "decodes the CA and exports NODE_EXTRA_CA_CERTS before the reinstall audit-signatures step",
"category": "security"
},
+ {
+ "file": "test/corporate-ca-build-tls-anchor.test.ts",
+ "test": "accepts the corporate CA build arg in the DCode base image before HTTPS fetches",
+ "category": "security"
+ },
+ {
+ "file": "test/corporate-ca-build-tls-anchor.test.ts",
+ "test": "decodes the corporate CA again in the DCode final image",
+ "category": "security"
+ },
+ {
+ "file": "test/corporate-ca-build-tls-anchor.test.ts",
+ "test": "trusts the corporate CA before the DCode discovery runtime npm install",
+ "category": "security"
+ },
{
"file": "test/dcode-base-image-workflow.test.ts",
"test": "accepts every discovered publisher and rejects supply-chain mutations",
diff --git a/docs/index.yml b/docs/index.yml
index ea0ad6939e0..8a6ee291620 100644
--- a/docs/index.yml
+++ b/docs/index.yml
@@ -675,6 +675,9 @@ navigation:
- page: "Security Best Practices"
path: _build/agent-variants/security/best-practices.deepagents.generated.mdx
slug: best-practices
+ - page: "Configure Corporate CA Trust"
+ path: _build/agent-variants/security/configure-corporate-ca-trust.deepagents.generated.mdx
+ slug: configure-corporate-ca-trust
- page: "Credential Storage"
path: _build/agent-variants/security/credential-storage.deepagents.generated.mdx
slug: credential-storage
diff --git a/docs/security/configure-corporate-ca-trust.mdx b/docs/security/configure-corporate-ca-trust.mdx
index 0f0291aa0df..201d9808465 100644
--- a/docs/security/configure-corporate-ca-trust.mdx
+++ b/docs/security/configure-corporate-ca-trust.mdx
@@ -8,7 +8,7 @@ description-agent: "Configures corporate proxy CA trust for runtime TLS and supp
keywords: ["nemoclaw corporate ca", "corporate proxy tls", "certificate verification"]
content:
type: "how_to"
-agent-variants: ["openclaw", "hermes"]
+agent-variants: ["openclaw", "hermes", "deepagents"]
---
Configure a corporate Certificate Authority (CA) before onboarding when an enterprise proxy re-signs external TLS with a root that OpenShell does not provide.
@@ -43,6 +43,13 @@ The Hermes Dockerfile decodes the bundle after its managed build-time dependency
+
+
+Deep Agents Code applies the corporate CA to its local base-image build before HTTPS dependency fetches and decodes it again in the final sandbox image.
+This supports cold builds on hosts where the base image is not cached.
+
+
+
At runtime, NemoClaw appends the corporate CA to the OpenShell trust bundle instead of replacing it.
It points `SSL_CERT_FILE`, `CURL_CA_BUNDLE`, `REQUESTS_CA_BUNDLE`, `GIT_SSL_CAINFO`, and `NODE_EXTRA_CA_CERTS` at the merged bundle so curl, Python, Git, and Node.js trust both roots.
@@ -91,6 +98,10 @@ Set `NEMOCLAW_CORPORATE_CA_IMPORT=0` to disable corporate CA import entirely.
## Related Topics
+
+
- [Troubleshooting](../reference/troubleshooting#external-channel-tls-fails-behind-a-corporate-mitm-proxy-netfail) for `NET:FAIL` symptoms and certificate diagnostics.
+
+
- [Security Best Practices](best-practices) for the broader sandbox trust model.
- [Credential Storage](credential-storage) for the OpenShell provider credential boundary.
diff --git a/src/lib/adapters/docker/image.ts b/src/lib/adapters/docker/image.ts
index 743b9ec3765..a979f8adde3 100644
--- a/src/lib/adapters/docker/image.ts
+++ b/src/lib/adapters/docker/image.ts
@@ -11,6 +11,8 @@ import {
} from "./run";
export type DockerBuildOptions = DockerRunOptions & {
+ buildArgs?: Record;
+
labels?: Record;
quiet?: boolean;
};
@@ -21,7 +23,7 @@ export function dockerBuild(
contextDir: string = ROOT,
opts: DockerBuildOptions = {},
): DockerRunResult {
- const { labels, quiet, ...rest } = opts;
+ const { buildArgs, labels, quiet, ...rest } = opts;
// Dockerfile.base relies on `RUN --mount=type=bind`, which is BuildKit-only.
// Hosts whose Docker daemon defaults to the legacy builder (e.g. fresh
// Debian/Ubuntu Docker 29 without /etc/docker/daemon.json) abort the
@@ -33,6 +35,10 @@ export function dockerBuild(
const args = [
"build",
...(quiet ? ["--quiet"] : []),
+
+ ...Object.entries(buildArgs ?? {})
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
+ .flatMap(([key, value]) => ["--build-arg", `${key}=${value}`]),
...Object.entries(labels ?? {})
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
.flatMap(([key, value]) => ["--label", `${key}=${value}`]),
diff --git a/src/lib/adapters/docker/index.test.ts b/src/lib/adapters/docker/index.test.ts
index a54f7774cae..4a1dde9a318 100644
--- a/src/lib/adapters/docker/index.test.ts
+++ b/src/lib/adapters/docker/index.test.ts
@@ -84,6 +84,30 @@ describe("docker helpers", () => {
);
});
+ it("adds sorted build args to dockerBuild argv and drops them from options", () => {
+ dockerBuild("Dockerfile.base", "sandbox-base:latest", "/repo/root", {
+ buildArgs: { Z_ARG: "last", A_ARG: "first" },
+ ignoreError: true,
+ });
+
+ expect(runMock).toHaveBeenCalledWith(
+ [
+ "docker",
+ "build",
+ "--build-arg",
+ "A_ARG=first",
+ "--build-arg",
+ "Z_ARG=last",
+ "-f",
+ "Dockerfile.base",
+ "-t",
+ "sandbox-base:latest",
+ "/repo/root",
+ ],
+ { ignoreError: true, env: { DOCKER_BUILDKIT: "1" } },
+ );
+ });
+
it("adds sorted image labels to dockerBuild argv and drops them from options", () => {
dockerBuild("Dockerfile.base", "sandbox-base:latest", "/repo/root", {
labels: { "com.example.z": "last", "com.example.a": "first" },
diff --git a/src/lib/agent/base-image.test.ts b/src/lib/agent/base-image.test.ts
index 4cfa05d850c..eb2d2375e74 100644
--- a/src/lib/agent/base-image.test.ts
+++ b/src/lib/agent/base-image.test.ts
@@ -6,6 +6,8 @@ import fs from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { makeAgent, withMockedDocker } from "../../../test/helpers/base-image-test-harness";
+
+import { tmpDir, writeCa } from "../onboard/__test-helpers__/corporate-ca-fixtures";
import { testTimeout } from "../../../test/helpers/timeouts";
import {
createSandboxBaseImageBuildProvenanceKey,
@@ -50,6 +52,7 @@ function makeDifferingImageInspection(
describe("agent base image provisioning", () => {
beforeEach(() => {
+ vi.stubEnv("NEMOCLAW_CORPORATE_CA_ANCHOR_DIRS", "");
vi.restoreAllMocks();
});
@@ -369,6 +372,67 @@ describe("agent base image provisioning", () => {
});
});
+ it("passes the resolved corporate CA into local agent base image builds (#8119)", () => {
+ vi.stubEnv("NEMOCLAW_CORPORATE_CA_BUNDLE", writeCa(tmpDir()));
+ withMockedDocker(({ ensureAgentBaseImage, dockerBuildMock, resolveSandboxBaseImageMock }) => {
+ resolveSandboxBaseImageMock.mockReturnValue({
+ ref: "nemoclaw-dcode-sandbox-base-local:compatible",
+ digest: null,
+ source: "local",
+ glibcVersion: "2.41",
+ });
+
+ ensureAgentBaseImage(
+ makeAgent({
+ name: "langchain-deepagents-code",
+ displayName: "LangChain Deep Agents Code",
+ expectedVersion: "0.1.34",
+ dockerfileBasePath: "/test/root/agents/langchain-deepagents-code/Dockerfile.base",
+ dockerfilePath: "/test/root/agents/langchain-deepagents-code/Dockerfile",
+ }),
+ { forceBaseImageRebuild: true },
+ );
+
+ const options = dockerBuildMock.mock.calls[0]?.[3] as {
+ buildArgs?: Record;
+ };
+ const encoded = options.buildArgs?.NEMOCLAW_CORPORATE_CA_B64;
+ expect(encoded).toBeTypeOf("string");
+ expect(Buffer.from(encoded ?? "", "base64").toString("utf8")).toContain("BEGIN CERTIFICATE");
+ });
+ });
+
+ it("omits corporate CA build inputs when corporate CA import is disabled (#8119)", () => {
+ vi.stubEnv("NEMOCLAW_CORPORATE_CA_BUNDLE", writeCa(tmpDir()));
+ vi.stubEnv("NEMOCLAW_CORPORATE_CA_IMPORT", "0");
+ withMockedDocker(({ ensureAgentBaseImage, dockerBuildMock, resolveSandboxBaseImageMock }) => {
+ resolveSandboxBaseImageMock.mockReturnValue({
+ ref: "nemoclaw-dcode-sandbox-base-local:compatible",
+ digest: null,
+ source: "local",
+ glibcVersion: "2.41",
+ });
+
+ ensureAgentBaseImage(
+ makeAgent({
+ name: "langchain-deepagents-code",
+ displayName: "LangChain Deep Agents Code",
+ expectedVersion: "0.1.34",
+ dockerfileBasePath: "/test/root/agents/langchain-deepagents-code/Dockerfile.base",
+ dockerfilePath: "/test/root/agents/langchain-deepagents-code/Dockerfile",
+ }),
+ { forceBaseImageRebuild: true },
+ );
+
+ expect(resolveSandboxBaseImageMock).toHaveBeenCalledWith(
+ expect.objectContaining({ buildArgs: undefined }),
+ );
+ expect(dockerBuildMock.mock.calls[0]?.[3]).toEqual(
+ expect.objectContaining({ buildArgs: undefined }),
+ );
+ });
+ });
+
it("fails closed when the Deep Agents Code manifest omits its base-image version", () => {
withMockedDocker(({ ensureAgentBaseImage, resolveSandboxBaseImageMock }) => {
expect(() =>
diff --git a/src/lib/agent/base-image.ts b/src/lib/agent/base-image.ts
index 8a6cd8d0083..cf196552dae 100644
--- a/src/lib/agent/base-image.ts
+++ b/src/lib/agent/base-image.ts
@@ -15,6 +15,8 @@ import {
dockerTag,
} from "../adapters/docker";
import { createCustomBuildContextFilter } from "../onboard/custom-build-context";
+
+import { encodeCorporateCaArg, resolveCorporateCa } from "../onboard/corporate-ca";
import { ROOT } from "../runner";
import { SANDBOX_BUILD_CONTEXT_PREFIX } from "../sandbox/build-context";
import {
@@ -42,6 +44,15 @@ import {
import { createDeepAgentsCodeBaseImageResolutionOptions } from "./deep-agents-code-base-image";
import type { AgentDefinition } from "./defs";
+function corporateCaBuildArgs(
+ env: NodeJS.ProcessEnv = process.env,
+): Record | undefined {
+ const corporateCa = resolveCorporateCa(env);
+ return corporateCa
+ ? { NEMOCLAW_CORPORATE_CA_B64: encodeCorporateCaArg(corporateCa.pem) }
+ : undefined;
+}
+
const HERMES_MCP_RUNTIME_PROBE_OK = "nemoclaw-hermes-mcp-runtime-ok";
// Matches the official Hermes base repository for both Dockerfile manifest-list
// pins and Docker-normalized platform manifest digests.
@@ -263,6 +274,7 @@ function createAgentBaseImageResolutionOptions(
return {
imageName,
dockerfilePath,
+ buildArgs: agent.name === "langchain-deepagents-code" ? corporateCaBuildArgs() : undefined,
localTag: buildLocalBaseTag(`nemoclaw-${agent.name}-sandbox-base-local`, ROOT),
envVar: getAgentSandboxBaseImageEnvVar(agent.name),
label: `${agent.displayName} sandbox base image`,
@@ -501,6 +513,8 @@ export function ensureAgentBaseImage(
const buildProvenance = localBaseImageBuildProvenance(resolutionOptions);
console.log(` Rebuilding ${agent.displayName} base image...`);
const buildResult = dockerBuild(baseDockerfile, forceBuildTag, ROOT, {
+ buildArgs: resolutionOptions.buildArgs,
+
ignoreError: true,
labels: buildProvenance.labels,
stdio: ["ignore", "inherit", "inherit"],
@@ -610,6 +624,8 @@ export function ensureAgentBaseImage(
console.log(` Building ${agent.displayName} base image (first time only)...`);
const buildProvenance = localBaseImageBuildProvenance(resolutionOptions);
const buildResult = dockerBuild(baseDockerfile, baseImageTag, ROOT, {
+ buildArgs: resolutionOptions.buildArgs,
+
ignoreError: true,
labels: buildProvenance.labels,
stdio: ["ignore", "inherit", "inherit"],
diff --git a/src/lib/sandbox-base-image-resolution.test.ts b/src/lib/sandbox-base-image-resolution.test.ts
index 16cff52cf58..c47a12de865 100644
--- a/src/lib/sandbox-base-image-resolution.test.ts
+++ b/src/lib/sandbox-base-image-resolution.test.ts
@@ -71,6 +71,29 @@ function resolutionOptions() {
};
}
+function mockLocalFallback(
+ options: ReturnType,
+ provenance: string,
+): void {
+ dockerMocks.imageInspect.mockImplementation((imageRef: string) => ({
+ status: imageRef === options.localTag ? 0 : 1,
+ }));
+ dockerMocks.imageInspectFormat.mockReturnValue(
+ JSON.stringify({
+ Id: IMAGE_ID,
+ RepoDigests: [],
+ Os: "linux",
+ Architecture: "amd64",
+ Config: {
+ Labels: {
+ [SANDBOX_BASE_BUILD_PROVENANCE_LABEL]: provenance,
+ },
+ },
+ }),
+ );
+ dockerMocks.pull.mockReturnValue({ status: 1 });
+}
+
describe("sandbox base-image warm resolution", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -503,6 +526,86 @@ describe("sandbox base-image warm resolution", () => {
});
});
+ it("rebuilds a local fallback when corporate CA build inputs change (#8119)", () => {
+ const buildArgs = { NEMOCLAW_CORPORATE_CA_B64: "second-public-ca" };
+ const options = {
+ ...resolutionOptions(),
+ buildArgs,
+ env: {
+ ...resolutionOptions().env,
+ NEMOCLAW_SANDBOX_BASE_LOCAL_BUILD: "1",
+ },
+ validateImage: () => true,
+ };
+ const previousProvenance = `${createSandboxBaseImageBuildProvenanceKey({
+ ...options,
+ buildArgs: { NEMOCLAW_CORPORATE_CA_B64: "first-public-ca" },
+ })}.${"c".repeat(64)}`;
+ mockLocalFallback(options, previousProvenance);
+ dockerMocks.build.mockReturnValue({ status: 0 });
+
+ expect(resolveSandboxBaseImage(options)).toMatchObject({ source: "local" });
+ expect(dockerMocks.build).toHaveBeenCalledWith(
+ options.dockerfilePath,
+ options.localTag,
+ options.rootDir,
+ expect.objectContaining({
+ buildArgs,
+ labels: {
+ [SANDBOX_BASE_BUILD_PROVENANCE_LABEL]: expect.stringMatching(
+ new RegExp(`^${createSandboxBaseImageBuildProvenanceKey(options)}\\.[0-9a-f]{64}$`),
+ ),
+ },
+ }),
+ );
+ });
+
+ it("rebuilds a local fallback when the current build omits the previous corporate CA input (#8119)", () => {
+ const options = {
+ ...resolutionOptions(),
+ env: {
+ ...resolutionOptions().env,
+ NEMOCLAW_SANDBOX_BASE_LOCAL_BUILD: "1",
+ },
+ validateImage: () => true,
+ };
+ const previousProvenance = `${createSandboxBaseImageBuildProvenanceKey({
+ ...options,
+ buildArgs: { NEMOCLAW_CORPORATE_CA_B64: "first-public-ca" },
+ })}.${"c".repeat(64)}`;
+ mockLocalFallback(options, previousProvenance);
+ dockerMocks.build.mockReturnValue({ status: 0 });
+
+ expect(resolveSandboxBaseImage(options)).toMatchObject({ source: "local" });
+ expect(dockerMocks.build).toHaveBeenCalledWith(
+ options.dockerfilePath,
+ options.localTag,
+ options.rootDir,
+ expect.objectContaining({
+ buildArgs: undefined,
+ labels: {
+ [SANDBOX_BASE_BUILD_PROVENANCE_LABEL]: expect.stringMatching(
+ new RegExp(`^${createSandboxBaseImageBuildProvenanceKey(options)}\\.[0-9a-f]{64}$`),
+ ),
+ },
+ }),
+ );
+ });
+
+ it("reuses a local fallback with current build provenance (#8119)", () => {
+ const options = {
+ ...resolutionOptions(),
+ buildArgs: { NEMOCLAW_CORPORATE_CA_B64: "current-public-ca" },
+ validateImage: () => true,
+ };
+ const provenance = `${createSandboxBaseImageBuildProvenanceKey(options)}.${"c".repeat(64)}`;
+ mockLocalFallback(options, provenance);
+
+ expect(resolveSandboxBaseImage(options)).toMatchObject({ source: "local" });
+ expect(dockerMocks.build).not.toHaveBeenCalled();
+ expect(traceMocks.add).toHaveBeenCalledWith("nemoclaw.sandbox_base_image.local_fallback_reuse");
+ });
+
it("fails closed instead of trusting an existing local tag when base inputs are dirty (#4680)", () => {
sourceMocks.inputsDirty.mockReturnValue(true);
dockerMocks.imageInspect.mockReturnValue({ status: 0 });
diff --git a/src/lib/sandbox-base-image.ts b/src/lib/sandbox-base-image.ts
index fd786439179..d628490687c 100644
--- a/src/lib/sandbox-base-image.ts
+++ b/src/lib/sandbox-base-image.ts
@@ -95,6 +95,24 @@ function localBuildAllowed(env: NodeJS.ProcessEnv = process.env): boolean {
return env.NODE_ENV !== "test" && env.VITEST !== "true";
}
+function hasCurrentLocalBuildProvenance(
+ imageRef: string,
+ options: ResolveBaseImageOptions,
+): boolean {
+ const inspected = inspectLocalImageMetadata(imageRef);
+ const labels =
+ inspected?.Config?.Labels && typeof inspected.Config.Labels === "object"
+ ? (inspected.Config.Labels as Record)
+ : {};
+ const provenance = labels[SANDBOX_BASE_BUILD_PROVENANCE_LABEL];
+ const expectedProvenance = createSandboxBaseImageBuildProvenanceKey(options);
+ return (
+ typeof provenance === "string" &&
+ provenance.startsWith(`${expectedProvenance}.`) &&
+ /^[0-9a-f]{64}\.[0-9a-f]{64}$/.test(provenance)
+ );
+}
+
function getRepoDigest(
imageName: string,
imageRef: string,
@@ -353,7 +371,7 @@ function resolveLocalCandidate(
const imageRef = options.localTag;
if (!forceBuild) {
const inspectResult = dockerImageInspect(imageRef, { ignoreError: true, suppressOutput: true });
- if (inspectResult.status === 0) {
+ if (inspectResult.status === 0 && hasCurrentLocalBuildProvenance(imageRef, options)) {
const check = options.requireOpenshellSandboxAbi
? imageMeetsMinimumGlibc(imageRef, options.minGlibcVersion || OPENSHELL_SANDBOX_MIN_GLIBC)
: { ok: true, version: null };
@@ -378,6 +396,8 @@ function resolveLocalCandidate(
// useful diagnostic.
const buildResult = withLocalBuildHeartbeat(() =>
dockerBuild(options.dockerfilePath, imageRef, options.rootDir || ROOT, {
+ buildArgs: options.buildArgs,
+
labels: {
[SANDBOX_BASE_BUILD_PROVENANCE_LABEL]: createSandboxBaseImageBuildProvenance(options),
},
diff --git a/src/lib/sandbox-base-image/resolution-key.test.ts b/src/lib/sandbox-base-image/resolution-key.test.ts
index a150c4785b1..0437283b450 100644
--- a/src/lib/sandbox-base-image/resolution-key.test.ts
+++ b/src/lib/sandbox-base-image/resolution-key.test.ts
@@ -64,6 +64,21 @@ describe("sandbox base-image resolution key", () => {
expect(createSandboxBaseImageResolutionKey(options(root))).not.toBe(before);
});
+ it("isolates build args without exposing their values (#8119)", () => {
+ const root = fixture();
+ const base = options(root);
+ const first = createSandboxBaseImageResolutionKey({
+ ...base,
+ buildArgs: { NEMOCLAW_CORPORATE_CA_B64: "first-public-ca" },
+ });
+ const second = createSandboxBaseImageResolutionKey({
+ ...base,
+ buildArgs: { NEMOCLAW_CORPORATE_CA_B64: "second-public-ca" },
+ });
+
+ expect(second).not.toBe(first);
+ });
+
it("changes when a Dockerfile-copied runtime helper changes", () => {
const root = fixture();
const helper = path.join(root, "scripts", "lib", "sandbox-rlimits.sh");
diff --git a/src/lib/sandbox-base-image/resolution-key.ts b/src/lib/sandbox-base-image/resolution-key.ts
index ffbdfcd6f58..dce8be9cd8e 100644
--- a/src/lib/sandbox-base-image/resolution-key.ts
+++ b/src/lib/sandbox-base-image/resolution-key.ts
@@ -40,6 +40,20 @@ function hashBaseImageInputs(
return hash.digest("hex");
}
+function hashBuildArgs(buildArgs: Record | undefined): string | null {
+ if (!buildArgs || Object.keys(buildArgs).length === 0) return null;
+ const hash = crypto.createHash("sha256");
+ for (const [key, value] of Object.entries(buildArgs).sort(([left], [right]) =>
+ left < right ? -1 : left > right ? 1 : 0,
+ )) {
+ hash.update(key);
+ hash.update("\0");
+ hash.update(value);
+ hash.update("\0");
+ }
+ return hash.digest("hex");
+}
+
function dockerPlatform(): string {
const reported = dockerInfoFormat("{{.OSType}}/{{.Architecture}}", {
ignoreError: true,
@@ -56,6 +70,8 @@ export function createSandboxBaseImageBuildProvenanceKey(options: ResolveBaseIma
imageName: options.imageName,
sourceRevisions: getSourceRevisionIds(rootDir, env),
inputFingerprint: hashBaseImageInputs(rootDir, options.dockerfilePath, options.inputPaths),
+
+ buildArgsFingerprint: hashBuildArgs(options.buildArgs),
};
return crypto.createHash("sha256").update(JSON.stringify(material)).digest("hex");
}
@@ -79,6 +95,8 @@ export function createSandboxBaseImageResolutionKey(options: ResolveBaseImageOpt
sourceTags: getSourceShortShaTags(rootDir, env),
localTag: options.localTag,
inputFingerprint: hashBaseImageInputs(rootDir, options.dockerfilePath, options.inputPaths),
+
+ buildArgsFingerprint: hashBuildArgs(options.buildArgs),
platform: dockerPlatform(),
requireOpenshellSandboxAbi: options.requireOpenshellSandboxAbi === true,
minGlibcVersion: options.minGlibcVersion || OPENSHELL_SANDBOX_MIN_GLIBC,
diff --git a/src/lib/sandbox-base-image/types.ts b/src/lib/sandbox-base-image/types.ts
index b84b31c00dd..4f81f6faa8d 100644
--- a/src/lib/sandbox-base-image/types.ts
+++ b/src/lib/sandbox-base-image/types.ts
@@ -40,6 +40,8 @@ export type ResolveBaseImageOptions = {
imageName: string;
dockerfilePath: string;
inputPaths?: string[];
+
+ buildArgs?: Record;
localTag: string;
envVar?: string;
label?: string;
diff --git a/test/corporate-ca-build-tls-anchor.test.ts b/test/corporate-ca-build-tls-anchor.test.ts
index 5f37b482ff0..9fb48520bd5 100644
--- a/test/corporate-ca-build-tls-anchor.test.ts
+++ b/test/corporate-ca-build-tls-anchor.test.ts
@@ -38,3 +38,114 @@ describe("corporate proxy CA build-time TLS anchor (#6839)", () => {
expect(anchorIndex).toBeLessThan(auditSignaturesIndex);
});
});
+
+describe("DCode corporate proxy CA cold-build trust (#8119)", () => {
+ const baseDockerfile = readFileSync(
+ join(import.meta.dirname, "../agents/langchain-deepagents-code/Dockerfile.base"),
+ "utf-8",
+ );
+ const finalDockerfile = readFileSync(
+ join(import.meta.dirname, "../agents/langchain-deepagents-code/Dockerfile"),
+ "utf-8",
+ );
+
+ // source-shape-contract: security -- DCode cold base builds must establish corporate CA trust before HTTPS dependency fetches
+ it("accepts the corporate CA build arg in the DCode base image before HTTPS fetches", () => {
+ const argIndex = baseDockerfile.indexOf("ARG NEMOCLAW_CORPORATE_CA_B64=");
+ const nativeBuilderIndex = baseDockerfile.indexOf("AS native-security-builder", argIndex);
+ const nativeArgIndex = baseDockerfile.indexOf(
+ "ARG NEMOCLAW_CORPORATE_CA_B64",
+ nativeBuilderIndex,
+ );
+ const nativeTrustIndex = baseDockerfile.indexOf("update-ca-certificates", nativeArgIndex);
+ const nativeFetchIndex = baseDockerfile.indexOf(
+ "build-native-security-packages.sh /out",
+ nativeTrustIndex,
+ );
+ const perlFetchIndex = baseDockerfile.indexOf(
+ "RUN bash /scripts/security/build-perl-security-packages.sh",
+ nativeFetchIndex,
+ );
+ const finalFromIndex = baseDockerfile.indexOf("FROM node:22-trixie-slim", perlFetchIndex);
+ const finalArgIndex = baseDockerfile.indexOf("ARG NEMOCLAW_CORPORATE_CA_B64", finalFromIndex);
+ const finalTrustIndex = baseDockerfile.indexOf("update-ca-certificates", finalArgIndex);
+ const firstSnapshotCurlIndex = baseDockerfile.indexOf(
+ "https://snapshot.debian.org/archive",
+ finalTrustIndex,
+ );
+
+ for (const [name, index] of Object.entries({
+ argIndex,
+ nativeBuilderIndex,
+ nativeArgIndex,
+ nativeTrustIndex,
+ nativeFetchIndex,
+ perlFetchIndex,
+ finalFromIndex,
+ finalArgIndex,
+ finalTrustIndex,
+ firstSnapshotCurlIndex,
+ })) {
+ expect(index, name).toBeGreaterThan(-1);
+ }
+ expect(nativeArgIndex).toBeLessThan(nativeTrustIndex);
+ expect(nativeTrustIndex).toBeLessThan(nativeFetchIndex);
+ expect(nativeFetchIndex).toBeLessThan(perlFetchIndex);
+ expect(finalArgIndex).toBeLessThan(finalTrustIndex);
+ expect(finalTrustIndex).toBeLessThan(firstSnapshotCurlIndex);
+ });
+
+ // source-shape-contract: security -- DCode discovery npm installs must trust the host corporate CA before registry access
+ it("trusts the corporate CA before the DCode discovery runtime npm install", () => {
+ const discoveryStageIndex = finalDockerfile.indexOf("AS mcp-tool-discovery-runtime");
+ const discoveryArgIndex = finalDockerfile.indexOf(
+ "ARG NEMOCLAW_CORPORATE_CA_B64",
+ discoveryStageIndex,
+ );
+ const discoveryTrustIndex = finalDockerfile.indexOf(
+ "export NODE_EXTRA_CA_CERTS=/tmp/nemoclaw-corporate-ca.pem",
+ discoveryArgIndex,
+ );
+ const discoveryInstallIndex = finalDockerfile.indexOf(
+ "./install-reviewed-runtime.sh",
+ discoveryTrustIndex,
+ );
+
+ expect(discoveryStageIndex).toBeGreaterThan(-1);
+ expect(discoveryArgIndex).toBeGreaterThan(discoveryStageIndex);
+ expect(discoveryTrustIndex).toBeGreaterThan(discoveryArgIndex);
+ expect(discoveryInstallIndex).toBeGreaterThan(discoveryTrustIndex);
+ });
+
+ // source-shape-contract: security -- DCode final images must decode the sandbox-specific corporate CA even when the base is reused
+ it("decodes the corporate CA again in the DCode final image", () => {
+ const finalFromIndex = finalDockerfile.indexOf("FROM ${BASE_IMAGE}");
+ const finalArgIndex = finalDockerfile.indexOf("ARG NEMOCLAW_CORPORATE_CA_B64", finalFromIndex);
+ const finalDecodeIndex = finalDockerfile.indexOf(
+ 'RUN if [ -n "${NEMOCLAW_CORPORATE_CA_B64}" ]; then',
+ finalArgIndex,
+ );
+ const trustDirectoryIndex = finalDockerfile.indexOf(
+ "install -d -o root -g root -m 0755 /usr/local/share/nemoclaw",
+ finalDecodeIndex,
+ );
+ const runtimeProbeIndex = finalDockerfile.indexOf(
+ "mcp-tool-discovery-runtime",
+ finalDecodeIndex,
+ );
+
+ for (const [name, index] of Object.entries({
+ finalFromIndex,
+ finalArgIndex,
+ finalDecodeIndex,
+ trustDirectoryIndex,
+ runtimeProbeIndex,
+ })) {
+ expect(index, name).toBeGreaterThan(-1);
+ }
+ expect(finalFromIndex).toBeLessThan(finalArgIndex);
+ expect(finalArgIndex).toBeLessThan(finalDecodeIndex);
+ expect(finalDecodeIndex).toBeLessThan(trustDirectoryIndex);
+ expect(trustDirectoryIndex).toBeLessThan(runtimeProbeIndex);
+ });
+});
diff --git a/test/corporate-ca-dockerfile-decode.test.ts b/test/corporate-ca-dockerfile-decode.test.ts
index 6a19803b6e1..2cc6e2eec2f 100644
--- a/test/corporate-ca-dockerfile-decode.test.ts
+++ b/test/corporate-ca-dockerfile-decode.test.ts
@@ -11,6 +11,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
+import { LEAF_PEM } from "../src/lib/onboard/__test-helpers__/corporate-ca-fixtures";
import {
hasGnuBase64Decode,
hasOpenssl,
@@ -48,6 +49,12 @@ J0N7VBg2CdK6jRjKLQOSOPq3ySCicHhVRI8hxIWotif7mK3jj6D8NRalwmlHgNM=
const DOCKERFILES = [
["OpenClaw", join(import.meta.dirname, "../Dockerfile")],
["Hermes", join(import.meta.dirname, "../agents/hermes/Dockerfile")],
+ ["Deep Agents Code", join(import.meta.dirname, "../agents/langchain-deepagents-code/Dockerfile")],
+] as const;
+
+const DEEP_AGENTS_DOCKERFILES = [
+ join(import.meta.dirname, "../agents/langchain-deepagents-code/Dockerfile"),
+ join(import.meta.dirname, "../agents/langchain-deepagents-code/Dockerfile.base"),
] as const;
const tmpRoots: string[] = [];
@@ -64,6 +71,30 @@ afterEach(() => {
}
});
+describe("Deep Agents Code Dockerfile corporate CA validation", () => {
+ it("requires CA:TRUE at every X.509 certificate parser (#8119)", () => {
+ for (const dockerfile of DEEP_AGENTS_DOCKERFILES) {
+ const source = readFileSync(dockerfile, "utf8");
+ const parsedCertificates = source.match(/new X509Certificate\(/g) ?? [];
+ const caChecks = source.match(/new X509Certificate\([^)]*\)\.ca/g) ?? [];
+ expect(parsedCertificates.length).toBeGreaterThan(0);
+ expect(caChecks).toHaveLength(parsedCertificates.length);
+ }
+ });
+});
+
+describe.skipIf(!canRunDecodeBlock)("Deep Agents Code corporate CA constraint", () => {
+ it("rejects a valid certificate without CA:TRUE (#8119)", () => {
+ const res = runDockerfileCorporateCaDecode(
+ DEEP_AGENTS_DOCKERFILES[0],
+ Buffer.from(LEAF_PEM).toString("base64"),
+ tmpDir(),
+ );
+ expect(res.status).not.toBe(0);
+ expect(res.stderr).toContain("basicConstraints CA:TRUE");
+ });
+});
+
for (const [label, dockerfile] of DOCKERFILES) {
describe.skipIf(!canRunDecodeBlock)(
`corporate CA Dockerfile decode guard — ${label} (#6210)`,
diff --git a/test/helpers/corporate-ca-support.ts b/test/helpers/corporate-ca-support.ts
index e467c5f6bfc..4eebb6dc8c8 100644
--- a/test/helpers/corporate-ca-support.ts
+++ b/test/helpers/corporate-ca-support.ts
@@ -283,9 +283,13 @@ export function runDockerfileCorporateCaDecode(
// Redirect the fixed /tmp decode scratch path into the per-test dir so
// concurrent test runs never collide.
.replaceAll("/tmp/nemoclaw-corporate-ca.decoded", path.join(outDir, "decoded"))
- // Root ownership requires root; the test only exercises the base64/cert
- // guards, so chown to the current user keeps the shipped fail-fast `&&`
- // chain intact while running unprivileged.
+ // Root ownership requires root. The contract test preserves the exact
+ // production command; this extracted-script test substitutes the current
+ // user and group so the certificate guards run unprivileged.
+ .replaceAll(
+ "install -d -o root -g root -m 0755",
+ 'install -d -o "$(id -u)" -g "$(id -g)" -m 0755',
+ )
.replaceAll("chown root:root", 'chown "$(id -u):$(id -g)"');
const wrapper = path.join(outDir, "decode.sh");
fs.writeFileSync(