Skip to content
155 changes: 155 additions & 0 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,7 @@ const trustedLocalOverride = {
ref: `nemoclaw-langchain-deepagents-code-sandbox-base-local:image-${"a".repeat(64)}`,
provenance: `${"b".repeat(64)}.${"c".repeat(64)}`,
};
const trustedRemoteRef = `ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base@sha256:${"d".repeat(64)}`;

describe("rebuildSandbox DCode flow: base-image trust lease", () => {
beforeEach(resetRebuildFlowTestEnvironment);
Expand Down Expand Up @@ -55,6 +56,160 @@ describe("rebuildSandbox DCode flow: base-image trust lease", () => {
expect(leaseActive).toBe(false);
});

it("uses the refreshed published base without recompiling native libraries (#8120)", async () => {
const harness = createRebuildFlowHarness({
agentName: "langchain-deepagents-code",
sandboxEntry: makeDcodeSandboxEntry(),
});
configureDcodeSession(harness);
const resolutionMetadata = {
ref: trustedRemoteRef,
source: "source-sha",
};
harness.ensureAgentBaseImageSpy.mockReturnValue({
imageTag: trustedRemoteRef,
built: false,
resolutionMetadata,
});
let leaseActive = false;
harness.restoreTrustedAgentRemoteBaseImageOverrideSpy.mockImplementation(() => {
leaseActive = false;
});
harness.pinTrustedAgentRemoteBaseImageOverrideForOperationSpy.mockImplementation(() => {
leaseActive = true;
return harness.restoreTrustedAgentRemoteBaseImageOverrideSpy;
});
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async () => {
expect(leaseActive).toBe(true);
expect(process.env[overrideEnvName]).toBe(trustedRemoteRef);
return { ok: true, prepared: harness.preparedDcodeBuildContext };
});

await expect(
harness.rebuildSandbox("alpha", ["--yes"], { throwOnError: true }),
).resolves.toBeUndefined();

expect(harness.ensureAgentBaseImageSpy).toHaveBeenCalledWith(
expect.objectContaining({ name: "langchain-deepagents-code" }),
{ forceBaseImageRefresh: true },
);
expect(harness.ensureAgentBaseImageSpy).not.toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ forceBaseImageRebuild: true }),
);
expect(harness.pinTrustedAgentRemoteBaseImageOverrideForOperationSpy).toHaveBeenCalledWith(
overrideEnvName,
{ ref: trustedRemoteRef, resolutionMetadata },
);
expect(harness.pinTrustedAgentBaseImageOverrideForOperationSpy).not.toHaveBeenCalled();
expect(harness.restoreTrustedAgentRemoteBaseImageOverrideSpy).toHaveBeenCalledOnce();
expect(harness.dockerRmiSpy).not.toHaveBeenCalledWith(trustedRemoteRef, expect.anything());
expect(leaseActive).toBe(false);
});

it("forces a trusted local build when refresh returns a mutable reference (#8120)", async () => {
const harness = createRebuildFlowHarness({
agentName: "langchain-deepagents-code",
sandboxEntry: makeDcodeSandboxEntry(),
});
configureDcodeSession(harness);
harness.ensureAgentBaseImageSpy
.mockReturnValueOnce({
imageTag: "ghcr.io/nvidia/nemoclaw/langchain-deepagents-code-sandbox-base:latest",
built: false,
})
.mockReturnValueOnce({
imageTag: trustedLocalOverride.ref,
built: true,
trustedLocalOverride,
});
let leaseActive = false;
harness.restoreTrustedAgentBaseImageOverrideSpy.mockImplementation(() => {
leaseActive = false;
});
harness.pinTrustedAgentBaseImageOverrideForOperationSpy.mockImplementation(() => {
leaseActive = true;
return harness.restoreTrustedAgentBaseImageOverrideSpy;
});
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async () => {
expect(leaseActive).toBe(true);
expect(process.env[overrideEnvName]).toBe(trustedLocalOverride.ref);
return { ok: true, prepared: harness.preparedDcodeBuildContext };
});

await expect(
harness.rebuildSandbox("alpha", ["--yes"], { throwOnError: true }),
).resolves.toBeUndefined();

expect(harness.ensureAgentBaseImageSpy).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ name: "langchain-deepagents-code" }),
{ forceBaseImageRefresh: true },
);
expect(harness.ensureAgentBaseImageSpy).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ name: "langchain-deepagents-code" }),
{ forceBaseImageRebuild: true },
);
expect(harness.pinTrustedAgentBaseImageOverrideForOperationSpy).toHaveBeenCalledWith(
overrideEnvName,
trustedLocalOverride,
);
expect(harness.pinTrustedAgentRemoteBaseImageOverrideForOperationSpy).not.toHaveBeenCalled();
expect(harness.restoreTrustedAgentBaseImageOverrideSpy).toHaveBeenCalledOnce();
expect(leaseActive).toBe(false);
});

it("restores the published base-image trust lease when replacement preparation throws (#8120)", async () => {
const restoreEnv = snapshotEnv([overrideEnvName]);
process.env[overrideEnvName] = "caller-selected-base:current";
const harness = createRebuildFlowHarness({
agentName: "langchain-deepagents-code",
sandboxEntry: makeDcodeSandboxEntry(),
});
configureDcodeSession(harness);
const resolutionMetadata = {
ref: trustedRemoteRef,
source: "source-sha",
};
harness.ensureAgentBaseImageSpy.mockReturnValue({
imageTag: trustedRemoteRef,
built: false,
resolutionMetadata,
});
let leaseActive = false;
harness.restoreTrustedAgentRemoteBaseImageOverrideSpy.mockImplementation(() => {
leaseActive = false;
});
harness.pinTrustedAgentRemoteBaseImageOverrideForOperationSpy.mockImplementation(() => {
leaseActive = true;
return harness.restoreTrustedAgentRemoteBaseImageOverrideSpy;
});
harness.prepareManagedDcodeRebuildImageSpy.mockImplementation(async () => {
expect(leaseActive).toBe(true);
expect(process.env[overrideEnvName]).toBe(trustedRemoteRef);
throw new Error("fixture preparation failed");
});

try {
await expect(
harness.rebuildSandbox("alpha", ["--yes"], { throwOnError: true }),
).rejects.toThrow("fixture preparation failed");

expect(harness.pinTrustedAgentRemoteBaseImageOverrideForOperationSpy).toHaveBeenCalledWith(
overrideEnvName,
{ ref: trustedRemoteRef, resolutionMetadata },
);
expect(harness.pinTrustedAgentBaseImageOverrideForOperationSpy).not.toHaveBeenCalled();
expect(harness.restoreTrustedAgentRemoteBaseImageOverrideSpy).toHaveBeenCalledOnce();
expect(harness.dockerRmiSpy).not.toHaveBeenCalledWith(trustedRemoteRef, expect.anything());
expect(leaseActive).toBe(false);
expect(process.env[overrideEnvName]).toBe("caller-selected-base:current");
} finally {
restoreEnv();
}
});

it("restores the base-image trust lease when replacement preparation throws (#6195)", async () => {
const restoreEnv = snapshotEnv([overrideEnvName]);
process.env[overrideEnvName] = "caller-selected-base:current";
Expand Down
89 changes: 65 additions & 24 deletions src/lib/actions/sandbox/rebuild-dcode-preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import { isDeepStrictEqual } from "node:util";

import { dockerImageInspectFormat, dockerRmi } from "../../adapters/docker";
import type { TrustedRemoteBaseImageOverride } from "../../agent/base-image";
import { loadAgent } from "../../agent/defs";
import {
ensureAgentBaseImage,
pinTrustedAgentBaseImageOverrideForOperation,
pinTrustedAgentRemoteBaseImageOverrideForOperation,
} from "../../agent/onboard";
import { RD as _RD, R } from "../../cli/terminal-style";
import { recoverNamedGatewayRuntime } from "../../gateway-runtime-action";
Expand All @@ -19,8 +21,8 @@ import {
getResumeSandboxGpuOverrides,
resolveSandboxGpuConfig,
} from "../../onboard/sandbox-gpu-mode";
import { redact } from "../../security/redact";
import type { TrustedLocalBaseImageOverride } from "../../sandbox-base-image";
import { redact } from "../../security/redact";
import * as onboardSession from "../../state/onboard-session";
import * as registry from "../../state/registry";
import * as sandboxState from "../../state/sandbox";
Expand All @@ -44,7 +46,8 @@ export type DcodeRebuildPreflightBail = (message: string, code?: number) => neve

type PinnedDcodeBaseImage = {
readonly imageRef: string;
readonly trustedLocalOverride: TrustedLocalBaseImageOverride;
readonly trustedLocalOverride?: TrustedLocalBaseImageOverride;
readonly trustedRemoteOverride?: TrustedRemoteBaseImageOverride;
dispose(): boolean;
verify(): boolean;
};
Expand Down Expand Up @@ -270,43 +273,77 @@ function inspectLocalImageId(imageRef: string): string {
}
}

function buildPinnedDcodeBaseImage(bail: DcodeRebuildPreflightBail): PinnedDcodeBaseImage {
function isImmutableRemoteImageRef(imageRef: string): boolean {
return /^[^\s@]+@sha256:[0-9a-f]{64}$/i.test(imageRef);
}

function resolvePinnedDcodeBaseImage(bail: DcodeRebuildPreflightBail): PinnedDcodeBaseImage {
const agent = loadAgent(DCODE_AGENT_NAME);
if (!agent.dockerfileBasePath) {
fail("DCode is missing its sandbox base Dockerfile", bail);
}
let result: ReturnType<typeof ensureAgentBaseImage>;
try {
result = ensureAgentBaseImage(agent, { forceBaseImageRebuild: true });
result = ensureAgentBaseImage(agent, { forceBaseImageRefresh: true });
} catch (error) {
fail(
`DCode base image could not be built: ${error instanceof Error ? error.message : String(error)}`,
bail,
);
try {
result = ensureAgentBaseImage(agent, { forceBaseImageRebuild: true });
} catch (buildError) {
fail(
`DCode base image could not be resolved or built: ${buildError instanceof Error ? buildError.message : String(buildError)}`,
bail,
);
}
}
if (
result.imageTag &&
!result.trustedLocalOverride &&
!isImmutableRemoteImageRef(result.imageTag)
) {
try {
result = ensureAgentBaseImage(agent, { forceBaseImageRebuild: true });
} catch (error) {
fail(
`DCode base image could not be built: ${error instanceof Error ? error.message : String(error)}`,
bail,
);
}
}
const imageRef = result.imageTag;
const trustedLocalOverride = result.trustedLocalOverride;
if (!imageRef || !trustedLocalOverride || trustedLocalOverride.ref !== imageRef) {
fail("DCode base image did not retain its current build-operation proof", bail);
const trustedRemoteOverride =
imageRef &&
isImmutableRemoteImageRef(imageRef) &&
result.resolutionMetadata?.ref === imageRef &&
result.resolutionMetadata.source !== "local"
? { ref: imageRef, resolutionMetadata: result.resolutionMetadata }
: undefined;
if (
!imageRef ||
(trustedLocalOverride?.ref !== imageRef && trustedRemoteOverride?.ref !== imageRef)
) {
fail("DCode base image did not retain its current resolution proof", bail);
}
const imageId = inspectLocalImageId(imageRef);
if (!imageId) {
try {
dockerRmi(imageRef, { ignoreError: true, suppressOutput: true });
} catch {
// The identity failure is the actionable error.
if (trustedLocalOverride) {
try {
dockerRmi(imageRef, { ignoreError: true, suppressOutput: true });
} catch {
// The identity failure is the actionable error.
}
}
fail("DCode base image identity could not be verified", bail);
}

let removed = false;
let disposed = trustedRemoteOverride !== undefined;
let warned = false;
const dispose = (): boolean => {
if (removed) return true;
if (disposed) return true;
try {
const removal = dockerRmi(imageRef, { ignoreError: true, suppressOutput: true });
if (removal.status === 0) {
removed = true;
disposed = true;
process.removeListener("exit", dispose);
return true;
}
Expand All @@ -319,10 +356,11 @@ function buildPinnedDcodeBaseImage(bail: DcodeRebuildPreflightBail): PinnedDcode
}
return false;
};
process.on("exit", dispose);
if (trustedLocalOverride) process.on("exit", dispose);
return {
imageRef,
trustedLocalOverride,
trustedRemoteOverride,
dispose,
verify: () => inspectLocalImageId(imageRef) === imageId,
};
Expand All @@ -335,15 +373,18 @@ async function withPinnedBaseImage<T>(
const envName = "NEMOCLAW_LANGCHAIN_DEEPAGENTS_CODE_SANDBOX_BASE_IMAGE_REF";
const hadPrevious = Object.hasOwn(process.env, envName);
const previous = process.env[envName];
const restoreTrustedOverride = pinTrustedAgentBaseImageOverrideForOperation(
envName,
pinned.trustedLocalOverride,
);
const restoreTrustedLocalOverride = pinned.trustedLocalOverride
? pinTrustedAgentBaseImageOverrideForOperation(envName, pinned.trustedLocalOverride)
: () => undefined;
const restoreTrustedRemoteOverride = pinned.trustedRemoteOverride
? pinTrustedAgentRemoteBaseImageOverrideForOperation(envName, pinned.trustedRemoteOverride)
: () => undefined;
process.env[envName] = pinned.imageRef;
try {
return await action();
} finally {
restoreTrustedOverride();
restoreTrustedRemoteOverride();
restoreTrustedLocalOverride();
if (hadPrevious && previous !== undefined) process.env[envName] = previous;
else delete process.env[envName];
}
Expand Down Expand Up @@ -389,7 +430,7 @@ export async function prepareDcodeReplacementBeforeMutation(
const target = resolveTarget(entry, resumeConfig, bail, gatewayPort);
if (!skipLiveRoute) requireInferenceRoute(sandboxName, target, bail);

pinnedBase = buildPinnedDcodeBaseImage(bail);
pinnedBase = resolvePinnedDcodeBaseImage(bail);
const sandboxGpuConfig = getRecordedGpuConfig(sandboxName, entry, session);
if (sandboxGpuConfig.errors.length > 0) fail(sandboxGpuConfig.errors.join(" "), bail);
const imageResult = await withPinnedBaseImage(pinnedBase, () =>
Expand Down
12 changes: 11 additions & 1 deletion test/helpers/rebuild-flow-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,13 @@ export type RebuildFlowHarness = {
applyPresetContentSpy: MockInstance;
backupSandboxStateSpy: MockInstance;
disposePreparedDcodeRebuildImageSpy: MockInstance;
dockerRmiSpy: MockInstance;
errorSpy: MockInstance;
ensureAgentBaseImageSpy: MockInstance;
pinTrustedAgentBaseImageOverrideForOperationSpy: MockInstance;
pinTrustedAgentRemoteBaseImageOverrideForOperationSpy: MockInstance;
restoreTrustedAgentBaseImageOverrideSpy: MockInstance;
restoreTrustedAgentRemoteBaseImageOverrideSpy: MockInstance;
executeSandboxCommandSpy: MockInstance;
checkAndRecoverSandboxProcessesSpy: MockInstance;
ensureMessagingHostForwardAfterRebuildSpy: MockInstance;
Expand Down Expand Up @@ -378,7 +381,7 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}):
}
return dcodeBaseImageIds.shift() ?? "sha256:dcode-base";
});
vi.spyOn(dockerImage, "dockerRmi").mockReturnValue({ status: 0 });
const dockerRmiSpy = vi.spyOn(dockerImage, "dockerRmi").mockReturnValue({ status: 0 });
vi.spyOn(dockerImage, "dockerTag").mockImplementation((source: unknown, target: unknown) => {
const sourceRef = String(source);
const sourceId =
Expand All @@ -400,6 +403,10 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}):
const pinTrustedAgentBaseImageOverrideForOperationSpy = vi
.spyOn(agentOnboard, "pinTrustedAgentBaseImageOverrideForOperation")
.mockReturnValue(restoreTrustedAgentBaseImageOverrideSpy);
const restoreTrustedAgentRemoteBaseImageOverrideSpy = vi.fn();
const pinTrustedAgentRemoteBaseImageOverrideForOperationSpy = vi
.spyOn(agentOnboard, "pinTrustedAgentRemoteBaseImageOverrideForOperation")
.mockReturnValue(restoreTrustedAgentRemoteBaseImageOverrideSpy);
const sessionAgentName =
overrides.sessionAgentName === undefined ? agentName : overrides.sessionAgentName;
vi.spyOn(agentRuntime, "getSessionAgent").mockReturnValue(
Expand Down Expand Up @@ -817,10 +824,13 @@ export function createRebuildFlowHarness(overrides: RebuildFlowOverrides = {}):
applyPresetContentSpy,
backupSandboxStateSpy,
disposePreparedDcodeRebuildImageSpy,
dockerRmiSpy,
errorSpy,
ensureAgentBaseImageSpy,
pinTrustedAgentBaseImageOverrideForOperationSpy,
pinTrustedAgentRemoteBaseImageOverrideForOperationSpy,
restoreTrustedAgentBaseImageOverrideSpy,
restoreTrustedAgentRemoteBaseImageOverrideSpy,
executeSandboxCommandSpy,
checkAndRecoverSandboxProcessesSpy,
ensureMessagingHostForwardAfterRebuildSpy,
Expand Down
Loading