Skip to content

Commit 730f28f

Browse files
authored
feat(onboard): add --name flag for explicit sandbox naming (#2591)
## Summary `nemoclaw onboard` had no `--name` flag, so picking a sandbox name in non-interactive or no-TTY runs required the undiscoverable `NEMOCLAW_SANDBOX_NAME` env var, and `--from <Dockerfile>` without a TTY silently fell back to the `my-assistant` default — clobbering the default sandbox. This adds `--name <sandbox>`, plumbed through `OnboardCommandOptions` and `OnboardOptions`. The flag wins over the env var. When prompting is impossible (no TTY or `--non-interactive`) the env var is now seeded into the same path the flag takes, so it actually flows through to `createSandbox` instead of being dropped silently. `--from` runs that cannot prompt and have no resolved name now error cleanly. Both the flag and the env-var seed go through the same `validateName` + reserved-name guard the interactive prompt enforces, so a name like `status` is rejected before it can shadow a CLI command. ## Related Issues Fixes #2543 Fixes #2534 ## Changes - `src/lib/onboard-command.ts`: parse `--name <sandbox>`, validate the value, surface it on `OnboardCommandOptions.sandboxName`, list it in the usage line. - `src/lib/onboard.ts`: add `sandboxName` to `OnboardOptions`; lift `RESERVED_SANDBOX_NAMES` out of `promptValidatedSandboxName` so the flag and env-var paths share it; resolve the requested name early (flag, then env var when prompting is impossible), validate once, and seed it into the local `sandboxName` so `createSandbox` receives the override; reject `--from` runs that cannot prompt and have no resolved name; teach `getRequestedSandboxNameHint` / `getResumeSandboxConflict` / `getResumeConfigConflicts` to prefer `opts.sandboxName` over the env var; thread it through the resume-conflict caller. Re-check `RESERVED_SANDBOX_NAMES` against the resumed `session?.sandboxName` so a stale pre-PR session whose recorded name now collides with a CLI command (e.g. `status`) is rejected before any sandbox operation runs. - `src/lib/onboard-command.test.ts`: positive test for `--name` parsing alongside `--from`, plus the missing-value and flag-as-value error paths. - `test/onboard.test.ts`: cover hint precedence (option over env) and the resume conflict produced when `--name` diverges from the recorded sandbox; static-source assertion that the resumed-session reserved-name guard stays in place. - `docs/reference/commands.md`: add `--name <sandbox>` to the `onboard` usage line, document the flag with its precedence and reserved-name semantics next to `--from`, and update the non-interactive `--from` example to set `NEMOCLAW_SANDBOX_NAME` so the new no-prompt guard is satisfied. ## Type of Change - [x] Code change (feature, bug fix, or refactor) - [x] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Verification - [x] `npx prek run --all-files` passes - [x] `npm test` passes - [x] Tests added or updated for new or changed behavior - [x] No secrets, API keys, or credentials committed - [x] Docs updated for user-facing behavior changes - [ ] `make docs` builds without warnings (doc changes only) - [x] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) ## AI Disclosure - [x] AI-assisted — tool: Claude Code, Codex --- Signed-off-by: Tinson Lai <tinsonl@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a --name <sandbox> flag to specify sandbox names during onboarding; explicit name now takes precedence over env var. * **Bug Fixes** * Onboarding fails early with clear messaging and exit code when a sandbox name is required but missing or invalid (non-interactive/--from cases); reserved names are rejected. * **Tests** * Added tests for parsing, help text, validation, precedence, reserved-name checks, and non-interactive failure cases. * **Documentation** * Updated command reference to document the new flag, validation rules, precedence, and failure behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
1 parent 771edeb commit 730f28f

5 files changed

Lines changed: 292 additions & 34 deletions

File tree

docs/reference/commands.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ The wizard creates an OpenShell gateway, registers inference providers, builds t
6464
Use this command for new installs and for recreating a sandbox after changes to policy or configuration.
6565

6666
```console
67-
$ nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [--agent <name>] [--dangerously-skip-permissions] [--yes-i-accept-third-party-software]
67+
$ nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from <Dockerfile>] [--name <sandbox>] [--agent <name>] [--dangerously-skip-permissions] [--yes-i-accept-third-party-software]
6868
```
6969

7070
:::{warning}
@@ -170,14 +170,28 @@ $ nemoclaw onboard --from path/to/Dockerfile
170170
The file can have any name; if it is not already named `Dockerfile`, onboard copies it to `Dockerfile` inside the staged build context automatically.
171171
All NemoClaw build arguments (`NEMOCLAW_MODEL`, `NEMOCLAW_PROVIDER_KEY`, `NEMOCLAW_INFERENCE_BASE_URL`, etc.) are injected as `ARG` overrides at build time, so declare them in your Dockerfile if you need to reference them.
172172

173-
In non-interactive mode, the path can also be supplied via the `NEMOCLAW_FROM_DOCKERFILE` environment variable:
173+
In non-interactive mode, the path can also be supplied via the `NEMOCLAW_FROM_DOCKERFILE` environment variable.
174+
You must also supply a sandbox name via `--name <sandbox>` or `NEMOCLAW_SANDBOX_NAME` so a `--from` build cannot silently clobber the default `my-assistant` sandbox.
174175

175176
```console
176-
$ NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_FROM_DOCKERFILE=path/to/Dockerfile nemoclaw onboard
177+
$ NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_FROM_DOCKERFILE=path/to/Dockerfile NEMOCLAW_SANDBOX_NAME=my-build nemoclaw onboard
177178
```
178179

179180
If a `--resume` is attempted with a different `--from` path than the original session, onboarding exits with a conflict error rather than silently building from the wrong image.
180181

182+
#### `--name <sandbox>`
183+
184+
Set the sandbox name without going through the interactive prompt.
185+
The same RFC 1123 and reserved-name rules that the wizard enforces apply here too — names that match a NemoClaw CLI command (`status`, `list`, `debug`, etc.) are rejected up front.
186+
187+
```console
188+
$ nemoclaw onboard --non-interactive --name my-build --from path/to/Dockerfile
189+
```
190+
191+
The flag wins over `NEMOCLAW_SANDBOX_NAME`.
192+
When prompting is impossible (no TTY or `--non-interactive`), the env var is also honoured so existing CI scripts keep working.
193+
Combining `--from <Dockerfile>` with non-interactive onboarding requires one of `--name` or `NEMOCLAW_SANDBOX_NAME`; otherwise onboarding exits rather than silently defaulting to `my-assistant` and clobbering the default sandbox.
194+
181195
#### `--dangerously-skip-permissions`
182196

183197
:::{warning}

src/lib/onboard-command.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ describe("onboard command", () => {
4040
fresh: false,
4141
recreateSandbox: false,
4242
fromDockerfile: null,
43+
sandboxName: null,
4344
acceptThirdPartySoftware: true,
4445
agent: null,
4546
dangerouslySkipPermissions: false,
@@ -65,6 +66,7 @@ describe("onboard command", () => {
6566
fresh: false,
6667
recreateSandbox: false,
6768
fromDockerfile: null,
69+
sandboxName: null,
6870
acceptThirdPartySoftware: true,
6971
agent: null,
7072
dangerouslySkipPermissions: false,
@@ -89,6 +91,7 @@ describe("onboard command", () => {
8991
fresh: false,
9092
recreateSandbox: false,
9193
fromDockerfile: null,
94+
sandboxName: null,
9295
acceptThirdPartySoftware: false,
9396
agent: null,
9497
dangerouslySkipPermissions: false,
@@ -112,6 +115,7 @@ describe("onboard command", () => {
112115
expect(runOnboard).not.toHaveBeenCalled();
113116
expect(lines.join("\n")).toContain("Usage: nemoclaw onboard");
114117
expect(lines.join("\n")).toContain("--from <Dockerfile>");
118+
expect(lines.join("\n")).toContain("--name <sandbox>");
115119
expect(lines.join("\n")).toContain("--agent <name>");
116120
expect(lines.join("\n")).toContain("--dangerously-skip-permissions");
117121
});
@@ -138,6 +142,7 @@ describe("onboard command", () => {
138142
fresh: false,
139143
recreateSandbox: false,
140144
fromDockerfile: dockerfilePath,
145+
sandboxName: null,
141146
acceptThirdPartySoftware: false,
142147
agent: null,
143148
dangerouslySkipPermissions: false,
@@ -163,6 +168,7 @@ describe("onboard command", () => {
163168
fresh: true,
164169
recreateSandbox: false,
165170
fromDockerfile: null,
171+
sandboxName: null,
166172
acceptThirdPartySoftware: false,
167173
agent: null,
168174
dangerouslySkipPermissions: false,
@@ -187,6 +193,70 @@ describe("onboard command", () => {
187193
expect(errors.join("\n")).toContain("--resume and --fresh are mutually exclusive");
188194
});
189195

196+
it("parses --name <sandbox>", () => {
197+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-name-parse-"));
198+
const dockerfilePath = path.join(tmpDir, "Custom.Dockerfile");
199+
fs.writeFileSync(dockerfilePath, "FROM scratch\n");
200+
201+
expect(
202+
parseOnboardArgs(
203+
["--non-interactive", "--from", dockerfilePath, "--name", "second-assistant"],
204+
"--yes-i-accept-third-party-software",
205+
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
206+
{
207+
env: {},
208+
error: () => {},
209+
exit: exitWithCode,
210+
},
211+
),
212+
).toEqual({
213+
nonInteractive: true,
214+
resume: false,
215+
fresh: false,
216+
recreateSandbox: false,
217+
fromDockerfile: dockerfilePath,
218+
sandboxName: "second-assistant",
219+
acceptThirdPartySoftware: false,
220+
agent: null,
221+
dangerouslySkipPermissions: false,
222+
controlUiPort: null,
223+
});
224+
});
225+
226+
it("exits when --name is missing its sandbox value", () => {
227+
const errors: string[] = [];
228+
expect(() =>
229+
parseOnboardArgs(
230+
["--name"],
231+
"--yes-i-accept-third-party-software",
232+
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
233+
{
234+
env: {},
235+
error: (message = "") => errors.push(message),
236+
exit: exitWithPrefixedCode,
237+
},
238+
),
239+
).toThrow("exit:1");
240+
expect(errors.join("\n")).toContain("--name requires a sandbox name");
241+
});
242+
243+
it("exits when --name is followed by another flag instead of a value", () => {
244+
const errors: string[] = [];
245+
expect(() =>
246+
parseOnboardArgs(
247+
["--name", "--resume"],
248+
"--yes-i-accept-third-party-software",
249+
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
250+
{
251+
env: {},
252+
error: (message = "") => errors.push(message),
253+
exit: exitWithPrefixedCode,
254+
},
255+
),
256+
).toThrow("exit:1");
257+
expect(errors.join("\n")).toContain("--name requires a sandbox name");
258+
});
259+
190260
it("exits when --from is missing its Dockerfile path", () => {
191261
expect(() =>
192262
parseOnboardArgs(
@@ -260,6 +330,7 @@ describe("onboard command", () => {
260330
fresh: false,
261331
recreateSandbox: false,
262332
fromDockerfile: null,
333+
sandboxName: null,
263334
acceptThirdPartySoftware: false,
264335
agent: "openclaw",
265336
dangerouslySkipPermissions: true,
@@ -394,6 +465,7 @@ describe("onboard command", () => {
394465
fresh: false,
395466
recreateSandbox: false,
396467
fromDockerfile: null,
468+
sandboxName: null,
397469
acceptThirdPartySoftware: false,
398470
agent: null,
399471
dangerouslySkipPermissions: false,

src/lib/onboard-command.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface OnboardCommandOptions {
1010
fresh: boolean;
1111
recreateSandbox: boolean;
1212
fromDockerfile: string | null;
13+
sandboxName: string | null;
1314
acceptThirdPartySoftware: boolean;
1415
agent: string | null;
1516
dangerouslySkipPermissions: boolean;
@@ -42,7 +43,7 @@ const ONBOARD_BASE_ARGS = [
4243

4344
function onboardUsageLines(noticeAcceptFlag: string): string[] {
4445
return [
45-
` Usage: nemoclaw onboard [--non-interactive] [--resume | --fresh] [--recreate-sandbox] [--from <Dockerfile>] [--agent <name>] [--control-ui-port <N>] [--dangerously-skip-permissions] [${noticeAcceptFlag}]`,
46+
` Usage: nemoclaw onboard [--non-interactive] [--resume | --fresh] [--recreate-sandbox] [--from <Dockerfile>] [--name <sandbox>] [--agent <name>] [--control-ui-port <N>] [--dangerously-skip-permissions] [${noticeAcceptFlag}]`,
4647
"",
4748
];
4849
}
@@ -81,6 +82,19 @@ export function parseOnboardArgs(
8182
parsedArgs.splice(fromIdx, 2);
8283
}
8384

85+
let sandboxName: string | null = null;
86+
const nameIdx = parsedArgs.indexOf("--name");
87+
if (nameIdx !== -1) {
88+
const nameValue = parsedArgs[nameIdx + 1];
89+
if (typeof nameValue !== "string" || nameValue.length === 0 || nameValue.startsWith("--")) {
90+
error(" --name requires a sandbox name");
91+
printOnboardUsage(error, noticeAcceptFlag);
92+
exit(1);
93+
}
94+
sandboxName = nameValue;
95+
parsedArgs.splice(nameIdx, 2);
96+
}
97+
8498
let agent: string | null = null;
8599
const agentIdx = parsedArgs.indexOf("--agent");
86100
if (agentIdx !== -1) {
@@ -141,6 +155,7 @@ export function parseOnboardArgs(
141155
fresh,
142156
recreateSandbox: parsedArgs.includes("--recreate-sandbox"),
143157
fromDockerfile,
158+
sandboxName,
144159
acceptThirdPartySoftware:
145160
parsedArgs.includes(noticeAcceptFlag) || String(deps.env[noticeAcceptEnv] || "") === "1",
146161
agent,

0 commit comments

Comments
 (0)