diff --git a/.agents/skills/nemoclaw-user-reference/references/commands.md b/.agents/skills/nemoclaw-user-reference/references/commands.md index 7f8e9c5fc74..22a4a8c8fa0 100644 --- a/.agents/skills/nemoclaw-user-reference/references/commands.md +++ b/.agents/skills/nemoclaw-user-reference/references/commands.md @@ -44,7 +44,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 ] [--agent ] [--yes-i-accept-third-party-software] +$ nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from ] [--provider ] [--agent ] [--yes-i-accept-third-party-software] ``` > **Warning:** For NemoClaw-managed environments, use `nemoclaw onboard` when you need to create or recreate the OpenShell gateway or sandbox. @@ -97,6 +97,12 @@ or: $ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 nemoclaw onboard --non-interactive ``` +To pick a non-default provider in scripted installs, pass `--provider` or set `NEMOCLAW_PROVIDER`: + +```console +$ nemoclaw onboard --non-interactive --provider openai --yes-i-accept-third-party-software +``` + To enable Brave Search in non-interactive mode, set: ```console diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 6304e9f129f..396ec1165f3 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -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 ] [--agent ] [--yes-i-accept-third-party-software] +$ nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from ] [--provider ] [--agent ] [--yes-i-accept-third-party-software] ``` :::{warning} @@ -119,6 +119,12 @@ or: $ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 nemoclaw onboard --non-interactive ``` +To pick a non-default provider in scripted installs, pass `--provider` or set `NEMOCLAW_PROVIDER`: + +```console +$ nemoclaw onboard --non-interactive --provider openai --yes-i-accept-third-party-software +``` + To enable Brave Search in non-interactive mode, set: ```console diff --git a/install.sh b/install.sh index bf33782e456..74a8b385b98 100755 --- a/install.sh +++ b/install.sh @@ -74,6 +74,7 @@ bootstrap_usage() { printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash -s -- [options]\n\n" printf " Options:\n" printf " --non-interactive Skip prompts (uses env vars / defaults)\n" + printf " --provider Default inference provider for non-interactive onboard\n" printf " --yes-i-accept-third-party-software Accept the third-party software notice in non-interactive mode\n" printf " --version, -v Print installer version and exit\n" printf " --help, -h Show this help message and exit\n\n" diff --git a/scripts/install.sh b/scripts/install.sh index 64c0738baef..b6e68a1726e 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -301,6 +301,7 @@ usage() { printf " curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash -s -- [options]\n\n" printf " ${C_DIM}Options:${C_RESET}\n" printf " --non-interactive Skip prompts (uses env vars / defaults)\n" + printf " --provider Default inference provider for non-interactive onboard\n" printf " --yes-i-accept-third-party-software Accept the third-party software notice in non-interactive mode\n" printf " --version, -v Print installer version and exit\n" printf " --help, -h Show this help message and exit\n\n" @@ -1180,10 +1181,19 @@ post_install_message() { main() { # Parse flags NON_INTERACTIVE="" + PROVIDER="" ACCEPT_THIRD_PARTY_SOFTWARE="" - for arg in "$@"; do - case "$arg" in + while [[ $# -gt 0 ]]; do + case "$1" in --non-interactive) NON_INTERACTIVE=1 ;; + --provider) + shift + if [[ $# -eq 0 || "$1" == --* ]]; then + usage + error "--provider requires a provider name" + fi + PROVIDER="$1" + ;; --yes-i-accept-third-party-software) ACCEPT_THIRD_PARTY_SOFTWARE=1 ;; --version | -v) local version_suffix @@ -1197,14 +1207,17 @@ main() { ;; *) usage - error "Unknown option: $arg" + error "Unknown option: $1" ;; esac + shift done # Also honor env var NON_INTERACTIVE="${NON_INTERACTIVE:-${NEMOCLAW_NON_INTERACTIVE:-}}" + PROVIDER="${PROVIDER:-${NEMOCLAW_PROVIDER:-}}" ACCEPT_THIRD_PARTY_SOFTWARE="${ACCEPT_THIRD_PARTY_SOFTWARE:-${NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE:-}}" export NEMOCLAW_NON_INTERACTIVE="${NON_INTERACTIVE}" + export NEMOCLAW_PROVIDER="${PROVIDER}" export NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE="${ACCEPT_THIRD_PARTY_SOFTWARE}" _INSTALL_START=$SECONDS diff --git a/src/lib/onboard-command.test.ts b/src/lib/onboard-command.test.ts index c6889d322b2..543bce65c77 100644 --- a/src/lib/onboard-command.test.ts +++ b/src/lib/onboard-command.test.ts @@ -29,6 +29,7 @@ describe("onboard command", () => { resume: true, recreateSandbox: false, fromDockerfile: null, + provider: null, acceptThirdPartySoftware: true, agent: null, dangerouslySkipPermissions: false, @@ -54,6 +55,7 @@ describe("onboard command", () => { resume: false, recreateSandbox: false, fromDockerfile: null, + provider: null, acceptThirdPartySoftware: true, agent: null, dangerouslySkipPermissions: false, @@ -78,6 +80,7 @@ describe("onboard command", () => { resume: true, recreateSandbox: false, fromDockerfile: null, + provider: null, acceptThirdPartySoftware: false, agent: null, dangerouslySkipPermissions: false, @@ -102,6 +105,7 @@ describe("onboard command", () => { expect(runOnboard).not.toHaveBeenCalled(); expect(lines.join("\n")).toContain("Usage: nemoclaw onboard"); expect(lines.join("\n")).toContain("--from "); + expect(lines.join("\n")).toContain("--provider "); expect(lines.join("\n")).toContain("--agent "); expect(lines.join("\n")).toContain("--dangerously-skip-permissions"); }); @@ -125,12 +129,56 @@ describe("onboard command", () => { resume: true, recreateSandbox: false, fromDockerfile: "/tmp/Custom.Dockerfile", + provider: null, acceptThirdPartySoftware: false, agent: null, dangerouslySkipPermissions: false, }); }); + it("parses --provider ", () => { + expect( + parseOnboardArgs( + ["--non-interactive", "--provider", "openai"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: {}, + error: () => {}, + exit: ((code: number) => { + throw new Error(String(code)); + }) as never, + }, + ), + ).toEqual({ + nonInteractive: true, + resume: false, + recreateSandbox: false, + fromDockerfile: null, + provider: "openai", + acceptThirdPartySoftware: false, + agent: null, + dangerouslySkipPermissions: false, + }); + }); + + it("exits when --provider is missing its provider name", () => { + expect(() => + parseOnboardArgs( + ["--provider"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: {}, + error: () => {}, + exit: ((code: number) => { + throw new Error(`exit:${code}`); + }) as never, + }, + ), + ).toThrow("exit:1"); + }); + it("exits when --from is missing its Dockerfile path", () => { expect(() => parseOnboardArgs( @@ -188,6 +236,7 @@ describe("onboard command", () => { resume: false, recreateSandbox: false, fromDockerfile: null, + provider: null, acceptThirdPartySoftware: false, agent: "openclaw", dangerouslySkipPermissions: true, @@ -239,6 +288,7 @@ describe("onboard command", () => { resume: false, recreateSandbox: false, fromDockerfile: null, + provider: null, acceptThirdPartySoftware: false, agent: null, dangerouslySkipPermissions: false, diff --git a/src/lib/onboard-command.ts b/src/lib/onboard-command.ts index f3f4c5d8f9f..637ec746eca 100644 --- a/src/lib/onboard-command.ts +++ b/src/lib/onboard-command.ts @@ -6,6 +6,7 @@ export interface OnboardCommandOptions { resume: boolean; recreateSandbox: boolean; fromDockerfile: string | null; + provider: string | null; acceptThirdPartySoftware: boolean; agent: string | null; dangerouslySkipPermissions: boolean; @@ -36,7 +37,7 @@ const ONBOARD_BASE_ARGS = [ function onboardUsageLines(noticeAcceptFlag: string): string[] { return [ - ` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from ] [--agent ] [--dangerously-skip-permissions] [${noticeAcceptFlag}]`, + ` Usage: nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from ] [--provider ] [--agent ] [--dangerously-skip-permissions] [${noticeAcceptFlag}]`, "", ]; } @@ -69,6 +70,18 @@ export function parseOnboardArgs( parsedArgs.splice(fromIdx, 2); } + let provider: string | null = null; + const providerIdx = parsedArgs.indexOf("--provider"); + if (providerIdx !== -1) { + provider = parsedArgs[providerIdx + 1] || null; + if (!provider || provider.startsWith("--")) { + error(" --provider requires a provider name"); + printOnboardUsage(error, noticeAcceptFlag); + exit(1); + } + parsedArgs.splice(providerIdx, 2); + } + let agent: string | null = null; const agentIdx = parsedArgs.indexOf("--agent"); if (agentIdx !== -1) { @@ -101,6 +114,7 @@ export function parseOnboardArgs( resume: parsedArgs.includes("--resume"), recreateSandbox: parsedArgs.includes("--recreate-sandbox"), fromDockerfile, + provider, acceptThirdPartySoftware: parsedArgs.includes(noticeAcceptFlag) || String(deps.env[noticeAcceptEnv] || "") === "1", agent, diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index ccb6f3392a7..0a303b30a0e 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -5827,6 +5827,9 @@ function skippedStepMessage(stepName, detail, reason = "resume") { async function onboard(opts = {}) { NON_INTERACTIVE = opts.nonInteractive || process.env.NEMOCLAW_NON_INTERACTIVE === "1"; RECREATE_SANDBOX = opts.recreateSandbox || process.env.NEMOCLAW_RECREATE_SANDBOX === "1"; + if (opts.provider) { + process.env.NEMOCLAW_PROVIDER = opts.provider; + } const dangerouslySkipPermissions = opts.dangerouslySkipPermissions || process.env.NEMOCLAW_DANGEROUSLY_SKIP_PERMISSIONS === "1"; if (dangerouslySkipPermissions) { diff --git a/test/cli.test.ts b/test/cli.test.ts index 6f8f3592f9c..02ea9965f68 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -122,6 +122,7 @@ describe("CLI dispatch", () => { expect(r.code).toBe(0); expect(r.out.includes("Usage: nemoclaw onboard")).toBeTruthy(); expect(r.out.includes("--from ")).toBeTruthy(); + expect(r.out.includes("--provider ")).toBeTruthy(); }); it("unknown onboard option exits 1", () => { @@ -142,6 +143,13 @@ describe("CLI dispatch", () => { expect(r.out.includes("Unknown onboard option(s): --non-interactiv")).toBeTruthy(); }); + it("accepts onboard --provider in CLI parsing", () => { + const r = run("onboard --provider openai --non-interactiv"); + expect(r.code).toBe(1); + expect(r.out.includes("Unknown onboard option(s): --non-interactiv")).toBeTruthy(); + expect(r.out.includes("Unknown onboard option(s): --provider")).toBeFalsy(); + }); + it("setup --help exits 0 and shows onboard usage", () => { const r = run("setup --help"); expect(r.code).toBe(0); diff --git a/test/install-preflight.test.ts b/test/install-preflight.test.ts index 52fe703ef24..082402d5a2c 100644 --- a/test/install-preflight.test.ts +++ b/test/install-preflight.test.ts @@ -1805,6 +1805,110 @@ describe("installer flag parsing", () => { expect(result.status).toBe(0); expect(`${result.stdout}${result.stderr}`).toMatch(/NEMOCLAW_INSTALL_TAG/); }); + + it("--help documents the provider flag", () => { + const result = spawnSync("bash", [INSTALLER, "--help"], { + cwd: path.join(import.meta.dirname, ".."), + encoding: "utf-8", + }); + + expect(result.status).toBe(0); + expect(`${result.stdout}${result.stderr}`).toMatch(/--provider /); + }); + + it("rejects --provider without a provider name", () => { + const result = spawnSync("bash", [INSTALLER, "--provider"], { + cwd: path.join(import.meta.dirname, ".."), + encoding: "utf-8", + }); + + expect(result.status).not.toBe(0); + const output = `${result.stdout}${result.stderr}`; + expect(output).toMatch(/--provider requires a provider name/); + expect(output).toMatch(/NemoClaw Installer/); + }); + + it("forwards --provider into the onboard environment for non-interactive installs", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-provider-forward-")); + const fakeBin = path.join(tmp, "bin"); + const prefix = path.join(tmp, "prefix"); + const onboardLog = path.join(tmp, "onboard.log"); + fs.mkdirSync(fakeBin); + fs.mkdirSync(path.join(prefix, "bin"), { recursive: true }); + + writeNodeStub(fakeBin); + writeExecutable( + path.join(fakeBin, "docker"), + `#!/usr/bin/env bash +if [ "$1" = "info" ]; then + echo '{"ServerVersion":"29.3.1","OperatingSystem":"Ubuntu 24.04","CgroupVersion":"2"}' + exit 0 +fi +exit 0 +`, + ); + writeExecutable( + path.join(fakeBin, "openshell"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "openshell 0.0.22" + exit 0 +fi +exit 0 +`, + ); + writeNpmStub( + fakeBin, + `if [ "$1" = "pack" ]; then + tmpdir="$4" + mkdir -p "$tmpdir/package" + tar -czf "$tmpdir/openclaw-2026.3.11.tgz" -C "$tmpdir" package + exit 0 +fi +if [ "$1" = "install" ]; then exit 0; fi +if [ "$1" = "run" ] && { [ "$2" = "build" ] || [ "$2" = "build:cli" ] || [ "$2" = "--if-present" ]; }; then exit 0; fi +if [ "$1" = "link" ]; then + cat > "$NPM_PREFIX/bin/nemoclaw" <<'EOS' +#!/usr/bin/env bash +if [ "$1" = "--version" ]; then echo "nemoclaw v0.1.0-test"; exit 0; fi +printf 'args:%s\\n' "$*" >> "$NEMOCLAW_ONBOARD_LOG" +printf 'provider:%s\\n' "\${NEMOCLAW_PROVIDER:-}" >> "$NEMOCLAW_ONBOARD_LOG" +exit 0 +EOS + chmod +x "$NPM_PREFIX/bin/nemoclaw" + exit 0 +fi`, + ); + + const result = spawnSync( + "bash", + [ + INSTALLER, + "--non-interactive", + "--provider", + "openai", + "--yes-i-accept-third-party-software", + ], + { + cwd: path.join(import.meta.dirname, ".."), + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, + NPM_PREFIX: prefix, + NEMOCLAW_ONBOARD_LOG: onboardLog, + }, + }, + ); + + expect(result.status).toBe(0); + const onboardInvocation = fs.readFileSync(onboardLog, "utf-8"); + expect(onboardInvocation).toMatch( + /^args:onboard --non-interactive --yes-i-accept-third-party-software$/m, + ); + expect(onboardInvocation).toMatch(/^provider:openai$/m); + }); }); // ---------------------------------------------------------------------------