Skip to content
20 changes: 17 additions & 3 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ The wizard creates an OpenShell gateway, registers inference providers, builds t
Use this command for new installs and for recreating a sandbox after changes to policy or configuration.

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

:::{warning}
Expand Down Expand Up @@ -170,14 +170,28 @@ $ nemoclaw onboard --from path/to/Dockerfile
The file can have any name; if it is not already named `Dockerfile`, onboard copies it to `Dockerfile` inside the staged build context automatically.
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.

In non-interactive mode, the path can also be supplied via the `NEMOCLAW_FROM_DOCKERFILE` environment variable:
In non-interactive mode, the path can also be supplied via the `NEMOCLAW_FROM_DOCKERFILE` environment variable.
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.

```console
$ NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_FROM_DOCKERFILE=path/to/Dockerfile nemoclaw onboard
$ NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_FROM_DOCKERFILE=path/to/Dockerfile NEMOCLAW_SANDBOX_NAME=my-build nemoclaw onboard
```

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.

#### `--name <sandbox>`

Set the sandbox name without going through the interactive prompt.
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.

```console
$ nemoclaw onboard --non-interactive --name my-build --from path/to/Dockerfile
```

The flag wins over `NEMOCLAW_SANDBOX_NAME`.
When prompting is impossible (no TTY or `--non-interactive`), the env var is also honoured so existing CI scripts keep working.
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.

#### `--dangerously-skip-permissions`

:::{warning}
Expand Down
72 changes: 72 additions & 0 deletions src/lib/onboard-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe("onboard command", () => {
fresh: false,
recreateSandbox: false,
fromDockerfile: null,
sandboxName: null,
acceptThirdPartySoftware: true,
agent: null,
dangerouslySkipPermissions: false,
Expand All @@ -65,6 +66,7 @@ describe("onboard command", () => {
fresh: false,
recreateSandbox: false,
fromDockerfile: null,
sandboxName: null,
acceptThirdPartySoftware: true,
agent: null,
dangerouslySkipPermissions: false,
Expand All @@ -89,6 +91,7 @@ describe("onboard command", () => {
fresh: false,
recreateSandbox: false,
fromDockerfile: null,
sandboxName: null,
acceptThirdPartySoftware: false,
agent: null,
dangerouslySkipPermissions: false,
Expand All @@ -112,6 +115,7 @@ describe("onboard command", () => {
expect(runOnboard).not.toHaveBeenCalled();
expect(lines.join("\n")).toContain("Usage: nemoclaw onboard");
expect(lines.join("\n")).toContain("--from <Dockerfile>");
expect(lines.join("\n")).toContain("--name <sandbox>");
expect(lines.join("\n")).toContain("--agent <name>");
expect(lines.join("\n")).toContain("--dangerously-skip-permissions");
});
Expand All @@ -138,6 +142,7 @@ describe("onboard command", () => {
fresh: false,
recreateSandbox: false,
fromDockerfile: dockerfilePath,
sandboxName: null,
acceptThirdPartySoftware: false,
agent: null,
dangerouslySkipPermissions: false,
Expand All @@ -163,6 +168,7 @@ describe("onboard command", () => {
fresh: true,
recreateSandbox: false,
fromDockerfile: null,
sandboxName: null,
acceptThirdPartySoftware: false,
agent: null,
dangerouslySkipPermissions: false,
Expand All @@ -187,6 +193,70 @@ describe("onboard command", () => {
expect(errors.join("\n")).toContain("--resume and --fresh are mutually exclusive");
});

it("parses --name <sandbox>", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-name-parse-"));
const dockerfilePath = path.join(tmpDir, "Custom.Dockerfile");
fs.writeFileSync(dockerfilePath, "FROM scratch\n");

expect(
parseOnboardArgs(
["--non-interactive", "--from", dockerfilePath, "--name", "second-assistant"],
"--yes-i-accept-third-party-software",
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
{
env: {},
error: () => {},
exit: exitWithCode,
},
),
).toEqual({
nonInteractive: true,
resume: false,
fresh: false,
recreateSandbox: false,
fromDockerfile: dockerfilePath,
sandboxName: "second-assistant",
acceptThirdPartySoftware: false,
agent: null,
dangerouslySkipPermissions: false,
controlUiPort: null,
});
});

it("exits when --name is missing its sandbox value", () => {
const errors: string[] = [];
expect(() =>
parseOnboardArgs(
["--name"],
"--yes-i-accept-third-party-software",
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
{
env: {},
error: (message = "") => errors.push(message),
exit: exitWithPrefixedCode,
},
),
).toThrow("exit:1");
expect(errors.join("\n")).toContain("--name requires a sandbox name");
});

it("exits when --name is followed by another flag instead of a value", () => {
const errors: string[] = [];
expect(() =>
parseOnboardArgs(
["--name", "--resume"],
"--yes-i-accept-third-party-software",
"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE",
{
env: {},
error: (message = "") => errors.push(message),
exit: exitWithPrefixedCode,
},
),
).toThrow("exit:1");
expect(errors.join("\n")).toContain("--name requires a sandbox name");
});

it("exits when --from is missing its Dockerfile path", () => {
expect(() =>
parseOnboardArgs(
Expand Down Expand Up @@ -260,6 +330,7 @@ describe("onboard command", () => {
fresh: false,
recreateSandbox: false,
fromDockerfile: null,
sandboxName: null,
acceptThirdPartySoftware: false,
agent: "openclaw",
dangerouslySkipPermissions: true,
Expand Down Expand Up @@ -394,6 +465,7 @@ describe("onboard command", () => {
fresh: false,
recreateSandbox: false,
fromDockerfile: null,
sandboxName: null,
acceptThirdPartySoftware: false,
agent: null,
dangerouslySkipPermissions: false,
Expand Down
17 changes: 16 additions & 1 deletion src/lib/onboard-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface OnboardCommandOptions {
fresh: boolean;
recreateSandbox: boolean;
fromDockerfile: string | null;
sandboxName: string | null;
acceptThirdPartySoftware: boolean;
agent: string | null;
dangerouslySkipPermissions: boolean;
Expand Down Expand Up @@ -42,7 +43,7 @@ const ONBOARD_BASE_ARGS = [

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

let sandboxName: string | null = null;
const nameIdx = parsedArgs.indexOf("--name");
if (nameIdx !== -1) {
const nameValue = parsedArgs[nameIdx + 1];
if (typeof nameValue !== "string" || nameValue.length === 0 || nameValue.startsWith("--")) {
error(" --name requires a sandbox name");
printOnboardUsage(error, noticeAcceptFlag);
exit(1);
}
sandboxName = nameValue;
parsedArgs.splice(nameIdx, 2);
}

let agent: string | null = null;
const agentIdx = parsedArgs.indexOf("--agent");
if (agentIdx !== -1) {
Expand Down Expand Up @@ -141,6 +155,7 @@ export function parseOnboardArgs(
fresh,
recreateSandbox: parsedArgs.includes("--recreate-sandbox"),
fromDockerfile,
sandboxName,
acceptThirdPartySoftware:
parsedArgs.includes(noticeAcceptFlag) || String(deps.env[noticeAcceptEnv] || "") === "1",
agent,
Expand Down
Loading
Loading