Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/lib/actions/sandbox/rebuild-dcode-artifact-drift.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ describe("rebuildSandbox DCode flow: prepared artifact drift", () => {
agentName: "langchain-deepagents-code",
sandboxEntry: makeDcodeSandboxEntry(),
dcodeRouteResults: [{ ok: true }, { ok: true }, { ok: true }],
dcodeBaseImageIds: ["sha256:dcode-base", "sha256:dcode-base", "sha256:changed"],
dcodeBaseImageIds: [
`sha256:${"a".repeat(64)}`,
`sha256:${"a".repeat(64)}`,
"sha256:changed",
],
});
configureDcodeSession(harness);

Expand Down
78 changes: 67 additions & 11 deletions src/lib/actions/sandbox/rebuild-dcode-base-image-lease.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,34 @@ const trustedLocalOverride = {
provenance: `${"b".repeat(64)}.${"c".repeat(64)}`,
};
const trustedRemoteRef = `ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base@sha256:${"d".repeat(64)}`;
const trustedLocalResolutionMetadata = {
schema: 1,
key: "trusted-local-dcode-base",
imageName: "ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base",
ref: trustedLocalOverride.ref,
digest: null,
source: "local",
imageId: `sha256:${"a".repeat(64)}`,
os: "linux",
architecture: "amd64",
glibcVersion: "2.41",
requireOpenshellSandboxAbi: true,
minGlibcVersion: "2.39",
};
const trustedRemoteResolutionMetadata = {
schema: 1,
key: "trusted-remote-dcode-base",
imageName: "ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base",
ref: trustedRemoteRef,
digest: `sha256:${"d".repeat(64)}`,
source: "source-sha",
imageId: "sha256:dcode-base",
os: "linux",
architecture: "amd64",
glibcVersion: "2.41",
requireOpenshellSandboxAbi: true,
minGlibcVersion: "2.39",
};

describe("rebuildSandbox DCode flow: base-image trust lease", () => {
installRebuildFlowTestHooks({ acceptThirdPartySoftware: true });
Expand All @@ -36,9 +64,13 @@ describe("rebuildSandbox DCode flow: base-image trust lease", () => {
leaseActive = true;
return harness.restoreTrustedAgentBaseImageOverrideSpy;
});
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async () => {
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async (input) => {
expect(leaseActive).toBe(true);
expect(process.env[overrideEnvName]).toBe(trustedLocalOverride.ref);
expect(input.preResolvedBaseImageMetadata).toMatchObject({
ref: trustedLocalOverride.ref,
imageId: `sha256:${"a".repeat(64)}`,
});
return { ok: true, prepared: harness.preparedDcodeBuildContext };
});

Expand All @@ -60,10 +92,7 @@ describe("rebuildSandbox DCode flow: base-image trust lease", () => {
sandboxEntry: makeDcodeSandboxEntry(),
});
configureDcodeSession(harness);
const resolutionMetadata = {
ref: trustedRemoteRef,
source: "source-sha",
};
const resolutionMetadata = trustedRemoteResolutionMetadata;
harness.ensureAgentBaseImageSpy.mockReturnValue({
imageTag: trustedRemoteRef,
built: false,
Expand All @@ -77,9 +106,10 @@ describe("rebuildSandbox DCode flow: base-image trust lease", () => {
leaseActive = true;
return harness.restoreTrustedAgentRemoteBaseImageOverrideSpy;
});
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async () => {
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async (input) => {
expect(leaseActive).toBe(true);
expect(process.env[overrideEnvName]).toBe(trustedRemoteRef);
expect(input.preResolvedBaseImageMetadata).toBe(resolutionMetadata);
return { ok: true, prepared: harness.preparedDcodeBuildContext };
});

Expand Down Expand Up @@ -120,6 +150,7 @@ describe("rebuildSandbox DCode flow: base-image trust lease", () => {
imageTag: trustedLocalOverride.ref,
built: true,
trustedLocalOverride,
resolutionMetadata: trustedLocalResolutionMetadata,
});
let leaseActive = false;
harness.restoreTrustedAgentBaseImageOverrideSpy.mockImplementation(() => {
Expand All @@ -129,9 +160,10 @@ describe("rebuildSandbox DCode flow: base-image trust lease", () => {
leaseActive = true;
return harness.restoreTrustedAgentBaseImageOverrideSpy;
});
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async () => {
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async (input) => {
expect(leaseActive).toBe(true);
expect(process.env[overrideEnvName]).toBe(trustedLocalOverride.ref);
expect(input.preResolvedBaseImageMetadata).toBe(trustedLocalResolutionMetadata);
return { ok: true, prepared: harness.preparedDcodeBuildContext };
});

Expand Down Expand Up @@ -166,10 +198,7 @@ describe("rebuildSandbox DCode flow: base-image trust lease", () => {
sandboxEntry: makeDcodeSandboxEntry(),
});
configureDcodeSession(harness);
const resolutionMetadata = {
ref: trustedRemoteRef,
source: "source-sha",
};
const resolutionMetadata = trustedRemoteResolutionMetadata;
harness.ensureAgentBaseImageSpy.mockReturnValue({
imageTag: trustedRemoteRef,
built: false,
Expand Down Expand Up @@ -246,4 +275,31 @@ describe("rebuildSandbox DCode flow: base-image trust lease", () => {
restoreEnv();
}
});

it("rejects base-image resolution metadata for a different local image (#9386)", async () => {
const harness = createRebuildFlowHarness({
agentName: "langchain-deepagents-code",
sandboxEntry: makeDcodeSandboxEntry(),
});
configureDcodeSession(harness);
harness.ensureAgentBaseImageSpy.mockReturnValue({
imageTag: trustedLocalOverride.ref,
built: true,
trustedLocalOverride,
resolutionMetadata: {
...trustedLocalResolutionMetadata,
imageId: `sha256:${"e".repeat(64)}`,
},
});

await expect(
harness.rebuildSandbox("alpha", ["--yes"], { throwOnError: true }),
).rejects.toThrow("DCode base-image resolution metadata does not match the pinned image");

expect(harness.prepareManagedDcodeRebuildImageSpy).not.toHaveBeenCalled();
expect(harness.dockerRmiSpy).toHaveBeenCalledWith(trustedLocalOverride.ref, {
ignoreError: true,
suppressOutput: true,
});
});
});
26 changes: 24 additions & 2 deletions src/lib/actions/sandbox/rebuild-dcode-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import {
getResumeSandboxGpuOverrides,
resolveSandboxGpuConfig,
} from "../../onboard/sandbox-gpu-mode";
import type { TrustedLocalBaseImageOverride } from "../../sandbox-base-image";
import type {
SandboxBaseImageResolutionMetadata,
TrustedLocalBaseImageOverride,
} from "../../sandbox-base-image";
import { redact } from "../../security/redact";
import * as onboardSession from "../../state/onboard-session";
import * as registry from "../../state/registry";
Expand All @@ -46,6 +49,7 @@ export type DcodeRebuildPreflightBail = (message: string, code?: number) => neve

type PinnedDcodeBaseImage = {
readonly imageRef: string;
readonly resolutionMetadata: SandboxBaseImageResolutionMetadata;
readonly trustedLocalOverride?: TrustedLocalBaseImageOverride;
readonly trustedRemoteOverride?: TrustedRemoteBaseImageOverride;
dispose(): boolean;
Expand Down Expand Up @@ -335,6 +339,21 @@ function resolvePinnedDcodeBaseImage(bail: DcodeRebuildPreflightBail): PinnedDco
}
fail("DCode base image identity could not be verified", bail);
}
const resolutionMetadata = result.resolutionMetadata;
if (
!resolutionMetadata ||
resolutionMetadata.ref !== imageRef ||
resolutionMetadata.imageId !== imageId
) {
if (trustedLocalOverride) {
try {
dockerRmi(imageRef, { ignoreError: true, suppressOutput: true });
} catch {
// Report the metadata mismatch instead of the Docker cleanup failure.
}
}
fail("DCode base-image resolution metadata does not match the pinned image", bail);
}

let disposed = trustedRemoteOverride !== undefined;
let warned = false;
Expand All @@ -359,6 +378,7 @@ function resolvePinnedDcodeBaseImage(bail: DcodeRebuildPreflightBail): PinnedDco
if (trustedLocalOverride) process.on("exit", dispose);
return {
imageRef,
resolutionMetadata,
trustedLocalOverride,
trustedRemoteOverride,
dispose,
Expand Down Expand Up @@ -433,7 +453,8 @@ export async function prepareDcodeReplacementBeforeMutation(
pinnedBase = resolvePinnedDcodeBaseImage(bail);
const sandboxGpuConfig = getRecordedGpuConfig(sandboxName, entry, session);
if (sandboxGpuConfig.errors.length > 0) fail(sandboxGpuConfig.errors.join(" "), bail);
const imageResult = await withPinnedBaseImage(pinnedBase, () =>
const pinnedBaseForPreparation = pinnedBase;
const imageResult = await withPinnedBaseImage(pinnedBaseForPreparation, () =>
prepareManagedDcodeRebuildImage({
agent: loadAgent(DCODE_AGENT_NAME),
provider: target.provider,
Expand All @@ -444,6 +465,7 @@ export async function prepareDcodeReplacementBeforeMutation(
webSearchConfig,
toolDisclosure: input.toolDisclosure,
dcodeAutoApprovalMode: input.dcodeAutoApprovalMode,
preResolvedBaseImageMetadata: pinnedBaseForPreparation.resolutionMetadata,
sandboxGpuConfig,
gatewayPort,
}),
Expand Down
111 changes: 107 additions & 4 deletions src/lib/actions/sandbox/rebuild-managed-image-configuration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ import { restoreEnv } from "../../../../test/helpers/env-test-helpers";
import {
dcodeInput,
expectPreparedImage,
writeDcodeRebuildDockerfile,
} from "../../../../test/helpers/rebuild-managed-image-preflight-harness";
import {
disposePreparedDcodeRebuildImage,
prepareManagedDcodeRebuildImage,
} from "./rebuild-managed-image-preflight";
import type { SandboxBaseImageResolutionMetadata } from "../../sandbox-base-image";

describe("managed DCode rebuild image configuration", () => {
it("pins recorded reasoning and web search while restoring ambient state (#6195)", async () => {
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dcode-rebuild-fidelity-"));
const stagedDockerfile = path.join(testRoot, "Dockerfile");
fs.writeFileSync(stagedDockerfile, "FROM scratch\n");
writeDcodeRebuildDockerfile(stagedDockerfile);
const previousReasoning = process.env.NEMOCLAW_REASONING;
process.env.NEMOCLAW_REASONING = "false";
let reasoningDuringPatch: string | undefined;
Expand Down Expand Up @@ -74,7 +76,7 @@ describe("managed DCode rebuild image configuration", () => {
it("binds DCode auto-approval mode into the prepared image configuration (#6478)", async () => {
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dcode-rebuild-auto-approval-"));
const stagedDockerfile = path.join(testRoot, "Dockerfile");
fs.writeFileSync(stagedDockerfile, "FROM scratch\n");
writeDcodeRebuildDockerfile(stagedDockerfile);
const prepareDockerfilePatch = vi.fn(async () => ({
buildId: "dcode-auto-approval",
dashboardRemoteBindPrepared: false,
Expand Down Expand Up @@ -111,10 +113,111 @@ describe("managed DCode rebuild image configuration", () => {
}
});

it("passes pinned base-image resolution metadata to DCode Dockerfile patching (#9386)", async () => {
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dcode-rebuild-base-resolution-"));
const stagedDockerfile = path.join(testRoot, "Dockerfile");
const digest = `sha256:${"a".repeat(64)}`;
const metadata = {
schema: 1,
key: "dcode-platform-base",
imageName: "ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base",
ref: `ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base@${digest}`,
digest,
source: "override",
imageId: `sha256:${"b".repeat(64)}`,
os: "linux",
architecture: "amd64",
glibcVersion: "2.41",
requireOpenshellSandboxAbi: true,
minGlibcVersion: "2.39",
} satisfies SandboxBaseImageResolutionMetadata;
writeDcodeRebuildDockerfile(stagedDockerfile, metadata);
const prepareDockerfilePatch = vi.fn(async () => ({
buildId: "dcode-base-resolution",
dashboardRemoteBindPrepared: false,
resolvedBaseImage: null,
}));

try {
const result = await prepareManagedDcodeRebuildImage(
dcodeInput({
dcodeAutoApprovalMode: "thread-opt-in",
preResolvedBaseImageMetadata: metadata,
}),
{
stageBuildContext: () => ({
buildCtx: testRoot,
stagedDockerfile,
origin: "generated" as const,
cleanupBuildCtx: () => {
fs.rmSync(testRoot, { recursive: true, force: true });
return true;
},
}),
prepareDockerfilePatch,
buildImage: () => ({ status: 0 }) as never,
removeImage: () => ({ status: 0 }) as never,
},
);

expect(result.ok).toBe(true);
expect(prepareDockerfilePatch).toHaveBeenCalledWith(
expect.objectContaining({
dcodeAutoApprovalMode: "thread-opt-in",
preResolvedBaseImageMetadata: metadata,
}),
);
disposePreparedDcodeRebuildImage(expectPreparedImage(result));
} finally {
fs.rmSync(testRoot, { recursive: true, force: true });
}
});

it("rejects a staged DCode base image that differs from pinned base-image resolution metadata (#9386)", async () => {
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dcode-rebuild-base-mismatch-"));
const stagedDockerfile = path.join(testRoot, "Dockerfile");
const input = dcodeInput();
writeDcodeRebuildDockerfile(stagedDockerfile, {
...input.preResolvedBaseImageMetadata,
ref: `ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base@sha256:${"c".repeat(64)}`,
});
const cleanupBuildCtx = vi.fn(() => {
fs.rmSync(testRoot, { recursive: true, force: true });
return true;
});
const prepareDockerfilePatch = vi.fn();
const buildImage = vi.fn();

try {
await expect(
prepareManagedDcodeRebuildImage(input, {
stageBuildContext: () => ({
buildCtx: testRoot,
stagedDockerfile,
origin: "generated" as const,
cleanupBuildCtx,
}),
prepareDockerfilePatch,
buildImage,
removeImage: () => ({ status: 0 }) as never,
}),
).resolves.toEqual({
ok: false,
detail:
"managed DCode staged base image does not match pinned base-image resolution metadata",
});
expect(prepareDockerfilePatch).not.toHaveBeenCalled();
expect(buildImage).not.toHaveBeenCalled();
expect(cleanupBuildCtx).toHaveBeenCalledOnce();
} finally {
fs.rmSync(testRoot, { recursive: true, force: true });
}
});

it("preserves remote dashboard bind preparation from the managed Dockerfile patch (#6024)", async () => {
const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "dcode-rebuild-remote-bind-"));
const stagedDockerfile = path.join(testRoot, "Dockerfile");
fs.writeFileSync(stagedDockerfile, "FROM scratch\n");
writeDcodeRebuildDockerfile(stagedDockerfile);

try {
const result = await prepareManagedDcodeRebuildImage(dcodeInput(), {
Expand Down Expand Up @@ -147,7 +250,7 @@ describe("managed DCode rebuild image configuration", () => {
it("defaults missing compatible-endpoint reasoning without borrowing ambient state (#6195)", async () => {
const buildCtx = fs.mkdtempSync(path.join(os.tmpdir(), "dcode-rebuild-reasoning-"));
const stagedDockerfile = path.join(buildCtx, "Dockerfile");
fs.writeFileSync(stagedDockerfile, "FROM scratch\n");
writeDcodeRebuildDockerfile(stagedDockerfile);
const previousReasoning = process.env.NEMOCLAW_REASONING;
process.env.NEMOCLAW_REASONING = "true";
let reasoningDuringPatch: string | undefined;
Expand Down
Loading
Loading