Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
d2689c5
refactor(onboard): extract docker gateway runtime helpers
cv Jun 10, 2026
cfae232
test(onboard): cover docker gateway runtime helpers
cv Jun 10, 2026
75f5a52
docs(onboard): explain docker gateway runtime boundary
cv Jun 10, 2026
128ef56
refactor(onboard): extract dashboard port create resolver
cv Jun 10, 2026
8d9f4c1
test(onboard): document malformed dashboard URL resolution
cv Jun 10, 2026
b6ea38f
refactor(onboard): centralize reused dashboard metadata
cv Jun 10, 2026
2d9d81a
test(onboard): cover disabled Hermes dashboard reuse metadata
cv Jun 10, 2026
318a002
refactor(onboard): extract sandbox registration payload
cv Jun 10, 2026
106d27d
refactor(onboard): extract messaging preparation
cv Jun 10, 2026
45b449b
test(onboard): cover messaging preparation edge cases
cv Jun 10, 2026
97b99bc
refactor(onboard): extract build context staging
cv Jun 10, 2026
a928abd
test(onboard): cover build context staging edge cases
cv Jun 10, 2026
1521702
refactor(onboard): extract sandbox launch envelope
cv Jun 10, 2026
d2c9122
test(onboard): cover sandbox launch shell boundaries
cv Jun 10, 2026
4beb640
refactor(onboard): extract build patch config
cv Jun 10, 2026
6bca4c8
refactor(onboard): extract dockerfile patch flow
cv Jun 10, 2026
06a7cf2
fix(onboard): harden dockerfile patch writes
cv Jun 10, 2026
ebf0c47
fix(onboard): satisfy secure dockerfile open
cv Jun 10, 2026
979706f
test(onboard): isolate dockerfile security guard
cv Jun 10, 2026
3513878
test(onboard): cover dockerfile write guards
cv Jun 10, 2026
b793d63
fix(onboard): validate dockerfile before truncating
cv Jun 10, 2026
a08fce9
refactor(onboard): extract sandbox create plan
cv Jun 10, 2026
f959ccc
Merge remote-tracking branch 'origin/main' into codex/tmp-dashboard-p…
cv Jun 11, 2026
c06aae6
Merge branch 'codex/tmp-dashboard-port-merge' into codex/tmp-reuse-da…
cv Jun 11, 2026
c66b889
Merge branch 'codex/tmp-reuse-dashboard-merge' into HEAD
cv Jun 11, 2026
c35baff
Merge branch 'codex/tmp-registration-merge' into HEAD
cv Jun 11, 2026
9e0ded0
Merge branch 'codex/tmp-messaging-prep-merge' into HEAD
cv Jun 11, 2026
a59867f
Merge branch 'codex/tmp-build-context-merge' into HEAD
cv Jun 11, 2026
6d40787
Merge branch 'codex/tmp-sandbox-launch-merge' into HEAD
cv Jun 11, 2026
89d74e5
Merge branch 'codex/tmp-build-patch-config-merge' into HEAD
cv Jun 11, 2026
5cccd4b
Merge branch 'codex/tmp-dockerfile-patch-merge' into HEAD
cv Jun 11, 2026
a90589f
fix(onboard): drop unused build patch config bindings
cv Jun 11, 2026
a42525e
Merge branch 'codex/tmp-build-patch-config-merge-v2' into HEAD
cv Jun 11, 2026
3f469f0
Merge branch 'codex/tmp-dockerfile-patch-merge-v2' into HEAD
cv Jun 11, 2026
57ed3dc
Merge remote-tracking branch 'origin/main' into codex/pr-5148-update
cv Jun 11, 2026
491a555
Merge remote-tracking branch 'origin/codex/onboard-build-patch-config…
cv Jun 11, 2026
bf9b269
Merge remote-tracking branch 'origin/codex/onboard-dockerfile-patch-f…
cv Jun 11, 2026
eb1c166
merge(onboard): main into build patch config flow
cv Jun 11, 2026
e9d1600
fix(onboard): narrow build patch config helper
cv Jun 11, 2026
b239831
fix(onboard): clarify build patch config boundary
cv Jun 11, 2026
d183ff0
Merge remote-tracking branch 'origin/codex/onboard-build-patch-config…
cv Jun 11, 2026
02d6ea2
Merge remote-tracking branch 'origin/codex/onboard-dockerfile-patch-f…
cv Jun 11, 2026
d649b2b
Merge remote-tracking branch 'origin/main' into codex/pr-5154-update
cv Jun 11, 2026
c35627b
Merge remote-tracking branch 'origin/codex/onboard-dockerfile-patch-f…
cv Jun 11, 2026
10d775d
Merge remote-tracking branch 'origin/main' into codex/pr-5158-update
cv Jun 11, 2026
1e99e50
refactor(onboard): extract sandbox messaging preflight (#5159)
cv Jun 11, 2026
a59e1bb
Merge branch 'main' into codex/onboard-create-plan-flow
cv Jun 12, 2026
2de593c
refactor(onboard): extract entry option resolution (#5161)
cv Jun 12, 2026
3f972b1
fix(onboard): honor resolved messaging channels
cv Jun 12, 2026
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
301 changes: 91 additions & 210 deletions src/lib/onboard.ts

Large diffs are not rendered by default.

162 changes: 162 additions & 0 deletions src/lib/onboard/entry-options.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

import {
resolveOnboardEntryOptions,
type OnboardEntryOptionsDeps,
} from "../../../dist/lib/onboard/entry-options";

class ExitError extends Error {
constructor(readonly code: number) {
super(`exit ${code}`);
}
}

function createDeps(overrides: Partial<OnboardEntryOptionsDeps> = {}): OnboardEntryOptionsDeps {
return {
isNonInteractive: vi.fn(() => false),
validateName: vi.fn((name: string) => name.trim().toLowerCase()),
reservedSandboxNames: new Set(["status"]),
cliDisplayName: vi.fn(() => "NemoClaw"),
getNameValidationGuidance: vi.fn(() => ["Use lowercase letters, numbers, and hyphens."]),
error: vi.fn(),
exitProcess: vi.fn((code: number) => {
throw new ExitError(code);
}) as (code: number) => never,
...overrides,
};
}

describe("resolveOnboardEntryOptions", () => {
it("rejects mutually exclusive resume and fresh flags", () => {
const deps = createDeps();

expect(() =>
resolveOnboardEntryOptions(
{
opts: { resume: true, fresh: true },
env: {},
stdinIsTty: true,
stdoutIsTty: true,
},
deps,
),
).toThrow(ExitError);
expect(deps.error).toHaveBeenCalledWith(" --resume and --fresh cannot both be set.");
});

it("uses non-interactive env defaults for Dockerfile and sandbox name", () => {
const deps = createDeps({
isNonInteractive: vi.fn(() => true),
});

const result = resolveOnboardEntryOptions(
{
opts: {},
env: {
NEMOCLAW_FROM_DOCKERFILE: "Dockerfile.custom",
NEMOCLAW_SANDBOX_NAME: " Demo-Box ",
},
stdinIsTty: false,
stdoutIsTty: false,
},
deps,
);

expect(result).toMatchObject({
resume: false,
fresh: false,
requestedFromDockerfile: "Dockerfile.custom",
requestedSandboxName: "demo-box",
cannotPrompt: true,
});
expect(deps.validateName).toHaveBeenCalledWith("Demo-Box", "sandbox name");
});

it("requires a sandbox name for --from when prompts are unavailable", () => {
const deps = createDeps();

expect(() =>
resolveOnboardEntryOptions(
{
opts: { fromDockerfile: "Dockerfile.custom" },
env: {},
stdinIsTty: false,
stdoutIsTty: true,
},
deps,
),
).toThrow(ExitError);
expect(deps.error).toHaveBeenCalledWith(
" --from <Dockerfile> requires --name <sandbox> (or NEMOCLAW_SANDBOX_NAME) when running without a TTY or with --non-interactive.",
);
expect(deps.error).toHaveBeenCalledWith(
" A sandbox name cannot be prompted for in this context.",
);
});

it("allows resume with --from and no recovered sandbox name so later resume guards can decide", () => {
const deps = createDeps();

const result = resolveOnboardEntryOptions(
{
opts: { resume: true, fromDockerfile: "Dockerfile.custom" },
env: {},
stdinIsTty: false,
stdoutIsTty: true,
},
deps,
);

expect(result.resume).toBe(true);
expect(result.requestedFromDockerfile).toBe("Dockerfile.custom");
expect(result.requestedSandboxName).toBeNull();
});

it("rejects reserved sandbox command names with the original request source", () => {
const deps = createDeps();

expect(() =>
resolveOnboardEntryOptions(
{
opts: { sandboxName: "Status" },
env: {},
stdinIsTty: true,
stdoutIsTty: true,
},
deps,
),
).toThrow(ExitError);
expect(deps.error).toHaveBeenCalledWith(" Reserved name: 'status' is a NemoClaw CLI command.");
expect(deps.error).toHaveBeenCalledWith(
" Choose a different sandbox name (passed via --name) to avoid routing conflicts.",
);
expect(deps.error).not.toHaveBeenCalledWith(" Use lowercase letters, numbers, and hyphens.");
expect(deps.getNameValidationGuidance).not.toHaveBeenCalled();
expect(deps.exitProcess).toHaveBeenCalledTimes(1);
});

it("prints validation guidance for invalid sandbox names", () => {
const deps = createDeps({
validateName: vi.fn(() => {
throw new Error("Invalid sandbox name");
}),
});

expect(() =>
resolveOnboardEntryOptions(
{
opts: { sandboxName: "bad name" },
env: {},
stdinIsTty: true,
stdoutIsTty: true,
},
deps,
),
).toThrow(ExitError);
expect(deps.error).toHaveBeenCalledWith(" Invalid sandbox name");
expect(deps.error).toHaveBeenCalledWith(" Use lowercase letters, numbers, and hyphens.");
});
});
104 changes: 104 additions & 0 deletions src/lib/onboard/entry-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export interface OnboardEntryOptionsInput {
opts: {
resume?: boolean;
fresh?: boolean;
fromDockerfile?: string | null;
sandboxName?: string | null;
};
env: NodeJS.ProcessEnv | Record<string, string | undefined>;
stdinIsTty: boolean;
stdoutIsTty: boolean;
}

export interface OnboardEntryOptionsDeps {
isNonInteractive(): boolean;
validateName(name: string, kind: string): string;
reservedSandboxNames: ReadonlySet<string>;
cliDisplayName(): string;
getNameValidationGuidance(
kind: string,
value: string | null | undefined,
options?: { includeAllowedFormat?: boolean },
): string[];
error(message: string): void;
exitProcess(code: number): never;
}

export interface ResolvedOnboardEntryOptions {
resume: boolean;
fresh: boolean;
requestedFromDockerfile: string | null;
requestedSandboxName: string | null;
cannotPrompt: boolean;
}

export function resolveOnboardEntryOptions(
input: OnboardEntryOptionsInput,
deps: OnboardEntryOptionsDeps,
): ResolvedOnboardEntryOptions {
const resume = input.opts.resume === true;
const fresh = input.opts.fresh === true;
if (resume && fresh) {
deps.error(" --resume and --fresh cannot both be set.");
deps.exitProcess(1);
}

const requestedFromDockerfile =
input.opts.fromDockerfile ||
(deps.isNonInteractive() ? input.env.NEMOCLAW_FROM_DOCKERFILE || null : null);
const cannotPrompt = deps.isNonInteractive() || !input.stdinIsTty || !input.stdoutIsTty;
let requestedSandboxName: string | null =
typeof input.opts.sandboxName === "string" && input.opts.sandboxName.length > 0
? input.opts.sandboxName
: null;
let requestedSandboxSource: "--name" | "NEMOCLAW_SANDBOX_NAME" | null = requestedSandboxName
? "--name"
: null;
if (!requestedSandboxName && cannotPrompt) {
const envName = input.env.NEMOCLAW_SANDBOX_NAME;
if (typeof envName === "string" && envName.trim().length > 0) {
requestedSandboxName = envName.trim();
requestedSandboxSource = "NEMOCLAW_SANDBOX_NAME";
}
}
if (requestedSandboxName) {
let validated: string;
try {
validated = deps.validateName(requestedSandboxName, "sandbox name");
} catch (error) {
deps.error(` ${error instanceof Error ? error.message : String(error)}`);
for (const line of deps.getNameValidationGuidance("sandbox name", requestedSandboxName, {
includeAllowedFormat: false,
})) {
deps.error(` ${line}`);
}
deps.exitProcess(1);
}
if (deps.reservedSandboxNames.has(validated)) {
deps.error(` Reserved name: '${validated}' is a ${deps.cliDisplayName()} CLI command.`);
deps.error(
` Choose a different sandbox name (passed via ${requestedSandboxSource}) to avoid routing conflicts.`,
);
deps.exitProcess(1);
}
requestedSandboxName = validated;
}
if (cannotPrompt && !resume && requestedFromDockerfile && !requestedSandboxName) {
deps.error(
" --from <Dockerfile> requires --name <sandbox> (or NEMOCLAW_SANDBOX_NAME) when running without a TTY or with --non-interactive.",
);
deps.error(" A sandbox name cannot be prompted for in this context.");
deps.exitProcess(1);
}

return {
resume,
fresh,
requestedFromDockerfile,
requestedSandboxName,
cannotPrompt,
};
}
Loading
Loading