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
78 changes: 74 additions & 4 deletions src/lib/onboard/build-context-stage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import path from "node:path";

import { afterEach, describe, expect, it, vi } from "vitest";
import { createAgentSandbox as createManagedAgentSandbox } from "../agent/base-image";
import { SandboxBaseImageResolutionError } from "../sandbox-base-image";
import { stageCreateSandboxBuildContext } from "./build-context-stage";
import { CUSTOM_BUILD_CONTEXT_WARN_BYTES } from "./custom-build-context";

Expand Down Expand Up @@ -84,7 +85,7 @@ describe("stageCreateSandboxBuildContext", () => {
fs.writeFileSync(agentDockerfile, "FROM scratch\nCOPY agents/hermes/plugin/ /opt/plugin/\n");
const agentBuild = {
buildCtx: makeTmpDir("nemoclaw-agent-staged-"),
stagedDockerfile: path.join(os.tmpdir(), "agent.Dockerfile"),
stagedDockerfile: path.join(makeTmpDir("nemoclaw-agent-staged-df-"), "agent.Dockerfile"),
};
const createAgentSandbox = vi.fn(() => agentBuild);
const agent = { name: "hermes", displayName: "Hermes", dockerfilePath: agentDockerfile } as any;
Expand Down Expand Up @@ -187,7 +188,7 @@ describe("stageCreateSandboxBuildContext", () => {
fs.symlinkSync(agentDockerfile, linkedDockerfile);
const agentBuild = {
buildCtx: makeTmpDir("nemoclaw-agent-staged-link-"),
stagedDockerfile: path.join(os.tmpdir(), "agent.Dockerfile"),
stagedDockerfile: path.join(makeTmpDir("nemoclaw-agent-staged-link-df-"), "agent.Dockerfile"),
};
const createAgentSandbox = vi.fn(() => agentBuild);
const agent = { name: "hermes", displayName: "Hermes", dockerfilePath: agentDockerfile } as any;
Expand Down Expand Up @@ -346,14 +347,83 @@ describe("stageCreateSandboxBuildContext", () => {
expect(fs.existsSync(stagedBuildCtx)).toBe(false);
});

it("converts a SandboxBaseImageResolutionError from createAgentSandbox to a clean exit (#8102)", () => {
const errors: string[] = [];
const resolutionError = new SandboxBaseImageResolutionError(
"Hermes Agent sandbox base image override 'ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:deadbeef' could not be resolved to an immutable trusted digest or failed required compatibility checks.",
);

expect(() =>
stageCreateSandboxBuildContext({
root: "/repo",
fromDockerfile: null,
agent: { name: "hermes", displayName: "Hermes Agent" } as any,
createAgentSandbox: () => {
throw resolutionError;
},
error: (msg) => errors.push(msg),
exit: throwingExit,
}),
).toThrow("exit 1");

expect(errors).toEqual([` ${resolutionError.message}`]);
});

it("converts a SandboxBaseImageResolutionError from the --from=<agent Dockerfile> path to a clean exit (#8102)", () => {
const agentDockerfilePath = path.join(makeTmpDir("nemoclaw-8102-from-"), "agent.Dockerfile");
fs.writeFileSync(agentDockerfilePath, "FROM scratch\n");
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
const errors: string[] = [];
const resolutionError = new SandboxBaseImageResolutionError(
"Hermes Agent sandbox base image override 'ghcr.io/nvidia/nemoclaw/hermes-sandbox-base@sha256:deadbeef' is outside the trusted repository 'ghcr.io/nvidia/nemoclaw/hermes-sandbox-base'.",
);

expect(() =>
stageCreateSandboxBuildContext({
root: "/repo",
fromDockerfile: agentDockerfilePath,
agent: {
name: "hermes",
displayName: "Hermes Agent",
dockerfilePath: agentDockerfilePath,
} as any,
createAgentSandbox: () => {
throw resolutionError;
},
log: vi.fn(),
error: (msg) => errors.push(msg),
exit: throwingExit,
}),
).toThrow("exit 1");

expect(errors).toEqual([` ${resolutionError.message}`]);
fs.rmSync(agentDockerfilePath, { force: true });
});

it("re-throws non-SandboxBaseImageResolutionError from createAgentSandbox (#8102)", () => {
const unexpected = new Error("unexpected internal failure");

expect(() =>
stageCreateSandboxBuildContext({
root: "/repo",
fromDockerfile: null,
agent: { name: "hermes", displayName: "Hermes Agent" } as any,
createAgentSandbox: () => {
throw unexpected;
},
error: vi.fn(),
exit: throwingExit,
}),
).toThrow("unexpected internal failure");
});

it("delegates to agent or default build-context staging when no custom Dockerfile is supplied", () => {
const agentBuild = {
buildCtx: makeTmpDir("nemoclaw-agent-build-"),
stagedDockerfile: path.join(os.tmpdir(), "agent.Dockerfile"),
stagedDockerfile: path.join(makeTmpDir("nemoclaw-agent-build-df-"), "agent.Dockerfile"),
};
const defaultBuild = {
buildCtx: makeTmpDir("nemoclaw-default-build-"),
stagedDockerfile: path.join(os.tmpdir(), "default.Dockerfile"),
stagedDockerfile: path.join(makeTmpDir("nemoclaw-default-build-df-"), "default.Dockerfile"),
};
const createAgentSandbox = vi.fn(() => agentBuild);
const stageDefaultSandboxBuildContext = vi.fn(() => defaultBuild);
Expand Down
21 changes: 19 additions & 2 deletions src/lib/onboard/build-context-stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type StagedBuildContext,
stageOptimizedSandboxBuildContext,
} from "../sandbox/build-context";
import { SandboxBaseImageResolutionError } from "../sandbox-base-image";
import {
CUSTOM_BUILD_CONTEXT_WARN_BYTES,
createCustomBuildContextFilter,
Expand Down Expand Up @@ -100,7 +101,15 @@ export function stageCreateSandboxBuildContext(
log(
` This is the managed ${input.agent.displayName} Dockerfile; staging the repository root as the Docker build context.`,
);
build = input.createAgentSandbox(input.agent);
try {
build = input.createAgentSandbox(input.agent);
} catch (err) {
if (err instanceof SandboxBaseImageResolutionError) {
error(` ${err.message}`);
exit(1);
}
throw err;
}
return {
...build,
origin,
Expand Down Expand Up @@ -163,7 +172,15 @@ export function stageCreateSandboxBuildContext(
}
build = { buildCtx, stagedDockerfile };
} else if (input.agent) {
build = input.createAgentSandbox(input.agent);
try {
build = input.createAgentSandbox(input.agent);
} catch (err) {
if (err instanceof SandboxBaseImageResolutionError) {
error(` ${err.message}`);
exit(1);
}
throw err;
}
} else {
build = (input.stageDefaultSandboxBuildContext ?? stageOptimizedSandboxBuildContext)(
input.root,
Expand Down
Loading