From c0dd95f7607675087b8981d828935450ee86bcdf Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Tue, 28 Apr 2026 12:31:19 +0000 Subject: [PATCH 1/5] feat(cli): add --name flag to nemoclaw onboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wizard already honors NEMOCLAW_SANDBOX_NAME via promptValidatedSandboxName (non-interactive: used as the sandbox name; interactive: used as the prompt default), but the env var is undiscoverable from `nemoclaw onboard --help`. This blocks scripted/CI workflows that pass `--from ` and need to spin up a second sandbox alongside an existing one — the only documented path is openshell sandbox create, which leaves the new sandbox unregistered in ~/.nemoclaw/sandboxes.json (invisible to nemoclaw status). Surface the existing env var as a discoverable CLI flag: - Add --name to the onboard parser, with fail-fast rejection for missing values or values that look like another flag (--name --from ...). - Detailed validation (lowercase, leading-letter, hyphen rules, reserved-name list) still happens in promptValidatedSandboxName, so error messages stay consistent across CLI-flag and env-var entry paths. - runOnboardCommand assigns deps.env.NEMOCLAW_SANDBOX_NAME from the parsed flag before calling runOnboard so the existing wizard pickup keeps working unchanged — no new code path on the inner onboard side. Tests cover: parsing, missing-value rejection, --name rejection, env propagation, and that --name absent leaves an existing NEMOCLAW_SANDBOX_NAME untouched. Closes #2543. Signed-off-by: latenighthackathon --- src/lib/onboard-command.test.ts | 94 +++++++++++++++++++++++++++++++++ src/lib/onboard-command.ts | 31 ++++++++++- 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/lib/onboard-command.test.ts b/src/lib/onboard-command.test.ts index 8257651cdf..5b762d7945 100644 --- a/src/lib/onboard-command.test.ts +++ b/src/lib/onboard-command.test.ts @@ -40,6 +40,7 @@ describe("onboard command", () => { agent: null, dangerouslySkipPermissions: false, controlUiPort: null, + sandboxName: null, }); }); @@ -65,6 +66,7 @@ describe("onboard command", () => { agent: null, dangerouslySkipPermissions: false, controlUiPort: null, + sandboxName: null, }); }); @@ -89,6 +91,7 @@ describe("onboard command", () => { agent: null, dangerouslySkipPermissions: false, controlUiPort: null, + sandboxName: null, }); }); @@ -134,6 +137,7 @@ describe("onboard command", () => { agent: null, dangerouslySkipPermissions: false, controlUiPort: null, + sandboxName: null, }); }); @@ -159,6 +163,7 @@ describe("onboard command", () => { agent: null, dangerouslySkipPermissions: false, controlUiPort: null, + sandboxName: null, }); }); @@ -235,6 +240,7 @@ describe("onboard command", () => { agent: "openclaw", dangerouslySkipPermissions: true, controlUiPort: null, + sandboxName: null, }); }); @@ -369,6 +375,7 @@ describe("onboard command", () => { agent: null, dangerouslySkipPermissions: false, controlUiPort: null, + sandboxName: null, }); }); @@ -390,4 +397,91 @@ describe("onboard command", () => { expect(lines.join("\n")).toContain("Use `nemoclaw onboard` instead"); expect(runOnboard).toHaveBeenCalledTimes(1); }); + it("parses --name ", () => { + const result = parseOnboardArgs( + ["--non-interactive", "--name", "deepobs", "--from", "/path/to/Dockerfile"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: {}, + error: () => {}, + exit: exitWithCode, + }, + ); + expect(result.sandboxName).toBe("deepobs"); + expect(result.fromDockerfile).toBe("/path/to/Dockerfile"); + expect(result.nonInteractive).toBe(true); + }); + + it("rejects --name with no following value", () => { + const errorMessages: string[] = []; + expect(() => + parseOnboardArgs( + ["--name"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: {}, + error: (msg) => { + errorMessages.push(String(msg ?? "")); + }, + exit: exitWithCode, + }, + ), + ).toThrow(); + expect(errorMessages.join("\n")).toMatch(/--name requires a sandbox name/); + }); + + it("rejects --name (treats next flag as missing value)", () => { + const errorMessages: string[] = []; + expect(() => + parseOnboardArgs( + ["--name", "--from", "/path/to/Dockerfile"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: {}, + error: (msg) => { + errorMessages.push(String(msg ?? "")); + }, + exit: exitWithCode, + }, + ), + ).toThrow(); + expect(errorMessages.join("\n")).toMatch(/--name requires a sandbox name/); + }); + + it("propagates --name to NEMOCLAW_SANDBOX_NAME so promptValidatedSandboxName picks it up", async () => { + const env: NodeJS.ProcessEnv = {}; + const runOnboard = vi.fn(async () => {}); + await runOnboardCommand({ + args: ["--non-interactive", "--name", "my-deepobs"], + noticeAcceptFlag: "--yes-i-accept-third-party-software", + noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + env, + runOnboard, + error: () => {}, + exit: exitWithCode, + }); + expect(env.NEMOCLAW_SANDBOX_NAME).toBe("my-deepobs"); + expect(runOnboard).toHaveBeenCalledWith( + expect.objectContaining({ sandboxName: "my-deepobs" }), + ); + }); + + it("does not touch NEMOCLAW_SANDBOX_NAME when --name is absent", async () => { + const env: NodeJS.ProcessEnv = { NEMOCLAW_SANDBOX_NAME: "existing-default" }; + const runOnboard = vi.fn(async () => {}); + await runOnboardCommand({ + args: ["--non-interactive"], + noticeAcceptFlag: "--yes-i-accept-third-party-software", + noticeAcceptEnv: "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + env, + runOnboard, + error: () => {}, + exit: exitWithCode, + }); + expect(env.NEMOCLAW_SANDBOX_NAME).toBe("existing-default"); + }); + }); diff --git a/src/lib/onboard-command.ts b/src/lib/onboard-command.ts index 4497414686..d30b59027e 100644 --- a/src/lib/onboard-command.ts +++ b/src/lib/onboard-command.ts @@ -11,6 +11,7 @@ export interface OnboardCommandOptions { agent: string | null; dangerouslySkipPermissions: boolean; controlUiPort: number | null; + sandboxName: string | null; } export interface RunOnboardCommandDeps { @@ -39,7 +40,7 @@ const ONBOARD_BASE_ARGS = [ function onboardUsageLines(noticeAcceptFlag: string): string[] { return [ - ` Usage: nemoclaw onboard [--non-interactive] [--resume | --fresh] [--recreate-sandbox] [--from ] [--agent ] [--control-ui-port ] [--dangerously-skip-permissions] [${noticeAcceptFlag}]`, + ` Usage: nemoclaw onboard [--non-interactive] [--resume | --fresh] [--recreate-sandbox] [--from ] [--name ] [--agent ] [--control-ui-port ] [--dangerously-skip-permissions] [${noticeAcceptFlag}]`, "", ]; } @@ -110,6 +111,24 @@ export function parseOnboardArgs( parsedArgs.splice(portIdx, 2); } + // --name — surface NEMOCLAW_SANDBOX_NAME via a discoverable + // CLI flag. Detailed validation (lowercase, leading-letter, hyphen rules, + // reserved-name list) still happens later in promptValidatedSandboxName; + // here we only reject the obviously-broken cases so the flag fails fast. + // See #2543. + let sandboxName: string | null = null; + const nameIdx = parsedArgs.indexOf("--name"); + if (nameIdx !== -1) { + const nameValue = parsedArgs[nameIdx + 1]; + if (typeof nameValue !== "string" || nameValue.startsWith("--") || nameValue.length === 0) { + error(" --name requires a sandbox name"); + printOnboardUsage(error, noticeAcceptFlag); + exit(1); + } + sandboxName = nameValue; + parsedArgs.splice(nameIdx, 2); + } + const allowedArgs = new Set([...ONBOARD_BASE_ARGS, noticeAcceptFlag]); const unknownArgs = parsedArgs.filter((arg) => !allowedArgs.has(arg)); if (unknownArgs.length > 0) { @@ -137,6 +156,7 @@ export function parseOnboardArgs( agent, dangerouslySkipPermissions: parsedArgs.includes("--dangerously-skip-permissions"), controlUiPort, + sandboxName, }; } @@ -148,6 +168,15 @@ export async function runOnboardCommand(deps: RunOnboardCommandDeps): Promise piggybacks on the existing NEMOCLAW_SANDBOX_NAME env-var + // path that promptValidatedSandboxName already honors in non-interactive + // mode and uses as the prompt default in interactive mode. Setting the + // env var here keeps the wizard's validation/reserved-name checks intact + // while making the name discoverable from `nemoclaw onboard --help`. + // See #2543. + if (options.sandboxName) { + deps.env.NEMOCLAW_SANDBOX_NAME = options.sandboxName; + } await deps.runOnboard(options); } From 2a24aec21955fc804e717c420bb54fd63a2e8f52 Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Tue, 28 Apr 2026 13:03:39 +0000 Subject: [PATCH 2/5] fix(cli): require sandbox name for --non-interactive (without --resume) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this guard, `nemoclaw onboard --non-interactive` silently fell through to the "my-assistant" default — which collides with the most common existing sandbox name and is exactly the surprise #2543 reported when trying to spin up a second sandbox via --from in non-interactive mode. CodeRabbit follow-up on #2618 asked us to fail-fast in this case. Reject when --non-interactive is set without --name and without NEMOCLAW_SANDBOX_NAME (whitespace-only env values count as missing). Resume flows are exempt — the sandbox name is recorded in the session and re-derived from there, so `--non-interactive --resume` keeps working unchanged. Test surfaces the new guard from four angles: - error when both --name and env are missing - accept when --name is provided - accept when NEMOCLAW_SANDBOX_NAME is set - whitespace-only env value still triggers the guard - --resume bypasses the guard Updated E2E callers that didn't set NEMOCLAW_SANDBOX_NAME so they keep working post-guard: - test-port8080-conflict.sh — added NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" to both onboard invocations (preflight failure path is what the test exercises; the new guard would short-circuit before the port check) - test-inference-routing.sh — added explicit NEMOCLAW_SANDBOX_NAME for five test cases that didn't previously specify one (TC-INF-05, unreachable, OpenAI, Anthropic, custom endpoint) - brev-e2e.test.ts — added NEMOCLAW_SANDBOX_NAME="e2e-brev" to the Launchable nohup invocation Signed-off-by: latenighthackathon --- src/lib/onboard-command.test.ts | 88 +++++++++++++++++++ src/lib/onboard-command.ts | 21 ++++- test/e2e/brev-e2e.test.ts | 2 +- .../test-port8080-conflict.sh | 4 +- test/e2e/test-inference-routing.sh | 5 ++ 5 files changed, 116 insertions(+), 4 deletions(-) diff --git a/src/lib/onboard-command.test.ts b/src/lib/onboard-command.test.ts index 5b762d7945..1227eed7a0 100644 --- a/src/lib/onboard-command.test.ts +++ b/src/lib/onboard-command.test.ts @@ -484,4 +484,92 @@ describe("onboard command", () => { expect(env.NEMOCLAW_SANDBOX_NAME).toBe("existing-default"); }); + it("rejects --non-interactive without --name and without NEMOCLAW_SANDBOX_NAME", () => { + const errorMessages: string[] = []; + expect(() => + parseOnboardArgs( + ["--non-interactive", "--from", "/path/to/Dockerfile"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: {}, + error: (msg) => { + errorMessages.push(String(msg ?? "")); + }, + exit: exitWithCode, + }, + ), + ).toThrow(); + expect(errorMessages.join("\n")).toMatch( + /--non-interactive requires --name /, + ); + }); + + it("accepts --non-interactive when --name is provided", () => { + const result = parseOnboardArgs( + ["--non-interactive", "--name", "my-deepobs"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: {}, + error: () => {}, + exit: exitWithCode, + }, + ); + expect(result.nonInteractive).toBe(true); + expect(result.sandboxName).toBe("my-deepobs"); + }); + + it("accepts --non-interactive when NEMOCLAW_SANDBOX_NAME env var is set", () => { + const result = parseOnboardArgs( + ["--non-interactive"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: { NEMOCLAW_SANDBOX_NAME: "from-env" }, + error: () => {}, + exit: exitWithCode, + }, + ); + expect(result.nonInteractive).toBe(true); + expect(result.sandboxName).toBe(null); + }); + + it("treats whitespace-only NEMOCLAW_SANDBOX_NAME as missing", () => { + const errorMessages: string[] = []; + expect(() => + parseOnboardArgs( + ["--non-interactive"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: { NEMOCLAW_SANDBOX_NAME: " " }, + error: (msg) => { + errorMessages.push(String(msg ?? "")); + }, + exit: exitWithCode, + }, + ), + ).toThrow(); + expect(errorMessages.join("\n")).toMatch( + /--non-interactive requires --name /, + ); + }); + + it("exempts --resume from the non-interactive name requirement (session has the name)", () => { + const result = parseOnboardArgs( + ["--non-interactive", "--resume"], + "--yes-i-accept-third-party-software", + "NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE", + { + env: {}, + error: () => {}, + exit: exitWithCode, + }, + ); + expect(result.nonInteractive).toBe(true); + expect(result.resume).toBe(true); + expect(result.sandboxName).toBe(null); + }); + }); diff --git a/src/lib/onboard-command.ts b/src/lib/onboard-command.ts index d30b59027e..9df14ae641 100644 --- a/src/lib/onboard-command.ts +++ b/src/lib/onboard-command.ts @@ -139,14 +139,33 @@ export function parseOnboardArgs( const resume = parsedArgs.includes("--resume"); const fresh = parsedArgs.includes("--fresh"); + const nonInteractive = parsedArgs.includes("--non-interactive"); if (resume && fresh) { error(" --resume and --fresh are mutually exclusive."); printOnboardUsage(error, noticeAcceptFlag); exit(1); } + // Non-interactive without --name (and without NEMOCLAW_SANDBOX_NAME) silently + // defaulted to "my-assistant", which collides with the most common existing + // sandbox name when the user is trying to spin up a second one (the case + // #2543 reports). Resume flows are exempt — the sandbox name is recorded in + // the session and re-derived from there. CodeRabbit follow-up on #2618. + if ( + nonInteractive && + !resume && + !sandboxName && + !String(deps.env.NEMOCLAW_SANDBOX_NAME ?? "").trim() + ) { + error( + " --non-interactive requires --name (or NEMOCLAW_SANDBOX_NAME).", + ); + printOnboardUsage(error, noticeAcceptFlag); + exit(1); + } + return { - nonInteractive: parsedArgs.includes("--non-interactive"), + nonInteractive, resume, fresh, recreateSandbox: parsedArgs.includes("--recreate-sandbox"), diff --git a/test/e2e/brev-e2e.test.ts b/test/e2e/brev-e2e.test.ts index 087ffca26f..f16456754b 100644 --- a/test/e2e/brev-e2e.test.ts +++ b/test/e2e/brev-e2e.test.ts @@ -511,7 +511,7 @@ function pollForSandboxReady(elapsed: () => string): void { [ `source ~/.nvm/nvm.sh 2>/dev/null || true`, `cd ${remoteDir}`, - `nohup nemoclaw onboard --non-interactive /tmp/nemoclaw-onboard.log 2>&1 & disown`, + `NEMOCLAW_SANDBOX_NAME="e2e-brev" nohup nemoclaw onboard --non-interactive /tmp/nemoclaw-onboard.log 2>&1 & disown`, `sleep 2`, `echo "onboard launched"`, ].join(" && "), diff --git a/test/e2e/e2e-cloud-experimental/test-port8080-conflict.sh b/test/e2e/e2e-cloud-experimental/test-port8080-conflict.sh index b35a4d0008..717eb81ad8 100755 --- a/test/e2e/e2e-cloud-experimental/test-port8080-conflict.sh +++ b/test/e2e/e2e-cloud-experimental/test-port8080-conflict.sh @@ -75,7 +75,7 @@ PASS "Port 8080 occupied by dummy process (PID ${occupier_pid})" P4_LOG="$(mktemp)" INFO "Running nemoclaw onboard --non-interactive (expect preflight to fail on port 8080)..." set +e -NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 nemoclaw onboard --non-interactive >"$P4_LOG" 2>&1 +NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" nemoclaw onboard --non-interactive >"$P4_LOG" 2>&1 p4_exit=$? set -euo pipefail p4_out="$(cat "$P4_LOG")" @@ -124,7 +124,7 @@ else INFO "Sandbox missing after gateway destroy/recreate — re-onboarding with NEMOCLAW_RECREATE_SANDBOX=1..." P4R_LOG="$(mktemp)" set +e - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 NEMOCLAW_RECREATE_SANDBOX=1 nemoclaw onboard --non-interactive >"$P4R_LOG" 2>&1 + NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 NEMOCLAW_RECREATE_SANDBOX=1 NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" nemoclaw onboard --non-interactive >"$P4R_LOG" 2>&1 p4r_exit=$? set -euo pipefail if [ "$p4r_exit" -ne 0 ]; then diff --git a/test/e2e/test-inference-routing.sh b/test/e2e/test-inference-routing.sh index 0dabd5bbc9..611feb190d 100755 --- a/test/e2e/test-inference-routing.sh +++ b/test/e2e/test-inference-routing.sh @@ -205,6 +205,7 @@ test_inf_05_credential_isolation() { NEMOCLAW_NON_INTERACTIVE=1 \ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ NEMOCLAW_POLICY_TIER="open" \ + NEMOCLAW_SANDBOX_NAME="e2e-inf-routing" \ nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1 | redact_stream "$real_key" | tee -a "$LOG_FILE" || onboard_exit=$? if [[ $onboard_exit -ne 0 ]]; then @@ -366,6 +367,7 @@ test_inf_07_unreachable_endpoint() { NEMOCLAW_ENDPOINT_URL="https://nemoclaw-e2e.invalid/v1" \ NEMOCLAW_MODEL="test-model" \ COMPATIBLE_API_KEY="fake-key-for-unreachable-test" \ + NEMOCLAW_SANDBOX_NAME="e2e-unreachable" \ run_with_timeout 120 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1) || exit_code=$? @@ -433,6 +435,7 @@ test_inf_02_openai() { NEMOCLAW_PROVIDER="openai" \ NEMOCLAW_MODEL="$model" \ OPENAI_API_KEY="$api_key" \ + NEMOCLAW_SANDBOX_NAME="e2e-inf-openai" \ run_with_timeout 300 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1 | redact_stream "$api_key" | tee -a "$LOG_FILE" || onboard_exit=$? @@ -507,6 +510,7 @@ test_inf_03_anthropic() { NEMOCLAW_PROVIDER="anthropic" \ NEMOCLAW_MODEL="$model" \ ANTHROPIC_API_KEY="$api_key" \ + NEMOCLAW_SANDBOX_NAME="e2e-inf-anthropic" \ run_with_timeout 300 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1 | redact_stream "$api_key" | tee -a "$LOG_FILE" || onboard_exit=$? @@ -594,6 +598,7 @@ test_inf_09_compatible_endpoint() { NEMOCLAW_ENDPOINT_URL="$endpoint_url" \ NEMOCLAW_MODEL="$endpoint_model" \ COMPATIBLE_API_KEY="$endpoint_key" \ + NEMOCLAW_SANDBOX_NAME="e2e-inf-custom" \ run_with_timeout 300 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1 | redact_stream "$endpoint_key" | tee -a "$LOG_FILE" || onboard_exit=$? From 4bc1d2ef1690ce80f7e0f2f407d0dc235f0bbba3 Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Tue, 28 Apr 2026 14:23:39 +0000 Subject: [PATCH 3/5] docs: surface --name / NEMOCLAW_SANDBOX_NAME in non-interactive examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit makes nemoclaw onboard --non-interactive (without --resume) reject when neither --name nor NEMOCLAW_SANDBOX_NAME is set, to close the silent fall-through to "my-assistant" that #2543 reported. Update the doc examples that copy verbatim into terminals so they keep working after the guard lands. - docs/reference/commands.md — usage line gets [--name ]; six console examples get NEMOCLAW_SANDBOX_NAME=my-assistant prepended (or --name my-assistant inline) so each is runnable as-is. - docs/inference/use-local-inference.md — five non-interactive setup recipes (Ollama, llama.cpp, vLLM-OpenAI, vLLM-Anthropic, NIM) gain NEMOCLAW_SANDBOX_NAME=my-assistant alongside their existing env-arg block. The Docker-login retry prose mentions the new requirement. - docs/deployment/set-up-telegram-bridge.md — credential-rotation prose cites --name in its re-run example. - docs/reference/network-policies.md — restricted-tier example uses the env var. - docs/reference/troubleshooting.md — credential rotation prose + the bare $ console example get the env var / flag. Signed-off-by: latenighthackathon --- docs/deployment/set-up-telegram-bridge.md | 2 +- docs/inference/use-local-inference.md | 7 ++++++- docs/reference/commands.md | 11 ++++++----- docs/reference/network-policies.md | 2 +- docs/reference/troubleshooting.md | 4 ++-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/docs/deployment/set-up-telegram-bridge.md b/docs/deployment/set-up-telegram-bridge.md index 577a85b28f..091a0ff13b 100644 --- a/docs/deployment/set-up-telegram-bridge.md +++ b/docs/deployment/set-up-telegram-bridge.md @@ -72,7 +72,7 @@ Channel entries in `/sandbox/.openclaw/openclaw.json` are fixed at image build t If you add or change `TELEGRAM_BOT_TOKEN` (or toggle channels) after a sandbox already exists, you typically need to run `nemoclaw onboard` again so the image and provider attachments are rebuilt with the new settings. NemoClaw stores a SHA-256 hash of each messaging token in the sandbox registry at creation time. -When you re-run `nemoclaw onboard --non-interactive` with a new token, NemoClaw detects the change, backs up workspace state, deletes the sandbox, recreates it with the new credential, and restores the backup. +When you re-run `nemoclaw onboard --non-interactive --name ` with a new token, NemoClaw detects the change, backs up workspace state, deletes the sandbox, recreates it with the new credential, and restores the backup. This makes credential rotation safe to script. Telegram, Discord, and Slack each allow only one active consumer per bot token. diff --git a/docs/inference/use-local-inference.md b/docs/inference/use-local-inference.md index c536922cf9..41039c7cbc 100644 --- a/docs/inference/use-local-inference.md +++ b/docs/inference/use-local-inference.md @@ -88,6 +88,7 @@ path to the model server. ```console $ NEMOCLAW_PROVIDER=ollama \ NEMOCLAW_MODEL=qwen2.5:14b \ + NEMOCLAW_SANDBOX_NAME=my-assistant \ nemoclaw onboard --non-interactive ``` @@ -137,6 +138,7 @@ $ NEMOCLAW_PROVIDER=custom \ NEMOCLAW_ENDPOINT_URL=http://localhost:8000/v1 \ NEMOCLAW_MODEL=meta-llama/Llama-3.1-8B-Instruct \ COMPATIBLE_API_KEY=dummy \ + NEMOCLAW_SANDBOX_NAME=my-assistant \ nemoclaw onboard --non-interactive ``` @@ -189,6 +191,7 @@ $ NEMOCLAW_PROVIDER=anthropicCompatible \ NEMOCLAW_ENDPOINT_URL=http://localhost:8080 \ NEMOCLAW_MODEL=my-model \ COMPATIBLE_ANTHROPIC_API_KEY=dummy \ + NEMOCLAW_SANDBOX_NAME=my-assistant \ nemoclaw onboard --non-interactive ``` @@ -215,6 +218,7 @@ The vLLM `/v1/responses` endpoint does not run the `--tool-call-parser`, so tool ```console $ NEMOCLAW_EXPERIMENTAL=1 \ NEMOCLAW_PROVIDER=vllm \ + NEMOCLAW_SANDBOX_NAME=my-assistant \ nemoclaw onboard --non-interactive ``` @@ -237,7 +241,7 @@ NemoClaw filters available models by GPU VRAM, pulls the NIM container image, st NIM container images are hosted on `nvcr.io` and require NGC registry authentication before `docker pull` succeeds. If Docker is not already logged in to `nvcr.io`, onboard prompts for an [NGC API key](https://org.ngc.nvidia.com/setup/api-key) and runs `docker login nvcr.io` over `--password-stdin` so the key is never written to disk or shell history. The prompt masks the key during input and retries once on a bad key before failing. -In non-interactive mode, onboard exits with login instructions if Docker is not already authenticated; run `docker login nvcr.io` yourself, then re-run `nemoclaw onboard --non-interactive`. +In non-interactive mode, onboard exits with login instructions if Docker is not already authenticated; run `docker login nvcr.io` yourself, then re-run `nemoclaw onboard --non-interactive` (with `--name ` or `NEMOCLAW_SANDBOX_NAME` set). :::{note} NIM uses vLLM internally. @@ -249,6 +253,7 @@ The same `chat/completions` API path restriction applies. ```console $ NEMOCLAW_EXPERIMENTAL=1 \ NEMOCLAW_PROVIDER=nim \ + NEMOCLAW_SANDBOX_NAME=my-assistant \ nemoclaw onboard --non-interactive ``` diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 8aca2ae456..b5f620ffec 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 ] [--dangerously-skip-permissions] [--yes-i-accept-third-party-software] +$ nemoclaw onboard [--non-interactive] [--resume] [--recreate-sandbox] [--from ] [--name ] [--agent ] [--dangerously-skip-permissions] [--yes-i-accept-third-party-software] ``` :::{warning} @@ -99,7 +99,7 @@ For details on tiers and the presets each includes, see [Network Policies](netwo In non-interactive mode, set the tier with `NEMOCLAW_POLICY_TIER` (default: `balanced`): ```console -$ NEMOCLAW_POLICY_TIER=restricted nemoclaw onboard --non-interactive --yes-i-accept-third-party-software +$ NEMOCLAW_POLICY_TIER=restricted NEMOCLAW_SANDBOX_NAME=my-assistant nemoclaw onboard --non-interactive --yes-i-accept-third-party-software ``` If you enable Brave Search during onboarding, NemoClaw currently stores the Brave API key in the sandbox's OpenClaw configuration. @@ -110,19 +110,20 @@ Treat Brave Search as an explicit opt-in and use a dedicated low-privilege Brave For non-interactive onboarding, you must explicitly accept the third-party software notice: ```console -$ nemoclaw onboard --non-interactive --yes-i-accept-third-party-software +$ nemoclaw onboard --non-interactive --name my-assistant --yes-i-accept-third-party-software ``` or: ```console -$ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 nemoclaw onboard --non-interactive +$ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 NEMOCLAW_SANDBOX_NAME=my-assistant nemoclaw onboard --non-interactive ``` To enable Brave Search in non-interactive mode, set: ```console $ BRAVE_API_KEY=... \ + NEMOCLAW_SANDBOX_NAME=my-assistant \ nemoclaw onboard --non-interactive ``` @@ -193,7 +194,7 @@ $ nemoclaw onboard --dangerously-skip-permissions Onboarding prints an explicit warning at start so the reduced security posture is visible in logs. The flag is also honored via `NEMOCLAW_DANGEROUSLY_SKIP_PERMISSIONS=1` for non-interactive runs: ```console -$ NEMOCLAW_DANGEROUSLY_SKIP_PERMISSIONS=1 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software +$ NEMOCLAW_DANGEROUSLY_SKIP_PERMISSIONS=1 NEMOCLAW_SANDBOX_NAME=my-assistant nemoclaw onboard --non-interactive --yes-i-accept-third-party-software ``` The flag is persisted on the sandbox registry entry, so `nemoclaw status` surfaces `Permissions: dangerously-skip-permissions (shields permanently down)` for sandboxes created this way. To tighten a sandbox after the fact, re-run `nemoclaw onboard` without the flag. diff --git a/docs/reference/network-policies.md b/docs/reference/network-policies.md index e288309729..9442566ef3 100644 --- a/docs/reference/network-policies.md +++ b/docs/reference/network-policies.md @@ -114,7 +114,7 @@ Tier definitions are stored in `nemoclaw-blueprint/policies/tiers.yaml`. In non-interactive mode, set the tier with `NEMOCLAW_POLICY_TIER`: ```console -$ NEMOCLAW_POLICY_TIER=open nemoclaw onboard --non-interactive --yes-i-accept-third-party-software +$ NEMOCLAW_POLICY_TIER=open NEMOCLAW_SANDBOX_NAME=my-assistant nemoclaw onboard --non-interactive --yes-i-accept-third-party-software ``` If the value does not match a known tier, onboarding exits with an error listing the valid options. diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md index 15ca312f60..d94a3095f1 100644 --- a/docs/reference/troubleshooting.md +++ b/docs/reference/troubleshooting.md @@ -337,7 +337,7 @@ Then re-run `nemoclaw onboard`. ### Updated messaging token is not picked up -Re-running `nemoclaw onboard --non-interactive` with a new +Re-running `nemoclaw onboard --non-interactive --name ` with a new `TELEGRAM_BOT_TOKEN`, `DISCORD_BOT_TOKEN`, or `SLACK_BOT_TOKEN` previously reported success while the sandbox kept polling with the old credential. Current NemoClaw stores SHA-256 hashes of messaging credentials in the @@ -350,7 +350,7 @@ If you suspect a sandbox is still using a stale token, re-run onboarding so the credential check runs: ```console -$ nemoclaw onboard --non-interactive +$ NEMOCLAW_SANDBOX_NAME=my-assistant nemoclaw onboard --non-interactive ``` ### Sandbox creation killed by OOM (exit 137) From 6ae832847ff71535629d6a4e114cc5b9fa0584f5 Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Tue, 28 Apr 2026 14:30:17 +0000 Subject: [PATCH 4/5] fix(test/docs): address CodeRabbit follow-ups on previous two commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test/e2e/test-inference-routing.sh — drop the five NEMOCLAW_SANDBOX_NAME additions from the previous commit. Each block already had the right name set on an earlier line ($SANDBOX_NAME / $sbx_name); the redundant trailing assignments would have last-won under bash inline-assignment semantics and onboarded a different sandbox than the one the test then SSHes into and asserts on. - test/e2e/brev-e2e.test.ts — switch the sandbox name from "e2e-brev" back to "e2e-test" so the launchable onboard path keeps matching the downstream connectivity / health / destroy checks at lines 552, 591, 626, 703. - docs/reference/troubleshooting.md — credential rotation example appends --yes-i-accept-third-party-software so the copy-paste form is complete. Signed-off-by: latenighthackathon --- docs/reference/troubleshooting.md | 2 +- test/e2e/brev-e2e.test.ts | 2 +- test/e2e/test-inference-routing.sh | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md index d94a3095f1..5305249160 100644 --- a/docs/reference/troubleshooting.md +++ b/docs/reference/troubleshooting.md @@ -350,7 +350,7 @@ If you suspect a sandbox is still using a stale token, re-run onboarding so the credential check runs: ```console -$ NEMOCLAW_SANDBOX_NAME=my-assistant nemoclaw onboard --non-interactive +$ NEMOCLAW_SANDBOX_NAME=my-assistant nemoclaw onboard --non-interactive --yes-i-accept-third-party-software ``` ### Sandbox creation killed by OOM (exit 137) diff --git a/test/e2e/brev-e2e.test.ts b/test/e2e/brev-e2e.test.ts index f16456754b..da3574fd39 100644 --- a/test/e2e/brev-e2e.test.ts +++ b/test/e2e/brev-e2e.test.ts @@ -511,7 +511,7 @@ function pollForSandboxReady(elapsed: () => string): void { [ `source ~/.nvm/nvm.sh 2>/dev/null || true`, `cd ${remoteDir}`, - `NEMOCLAW_SANDBOX_NAME="e2e-brev" nohup nemoclaw onboard --non-interactive /tmp/nemoclaw-onboard.log 2>&1 & disown`, + `NEMOCLAW_SANDBOX_NAME="e2e-test" nohup nemoclaw onboard --non-interactive /tmp/nemoclaw-onboard.log 2>&1 & disown`, `sleep 2`, `echo "onboard launched"`, ].join(" && "), diff --git a/test/e2e/test-inference-routing.sh b/test/e2e/test-inference-routing.sh index 611feb190d..0dabd5bbc9 100755 --- a/test/e2e/test-inference-routing.sh +++ b/test/e2e/test-inference-routing.sh @@ -205,7 +205,6 @@ test_inf_05_credential_isolation() { NEMOCLAW_NON_INTERACTIVE=1 \ NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ NEMOCLAW_POLICY_TIER="open" \ - NEMOCLAW_SANDBOX_NAME="e2e-inf-routing" \ nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1 | redact_stream "$real_key" | tee -a "$LOG_FILE" || onboard_exit=$? if [[ $onboard_exit -ne 0 ]]; then @@ -367,7 +366,6 @@ test_inf_07_unreachable_endpoint() { NEMOCLAW_ENDPOINT_URL="https://nemoclaw-e2e.invalid/v1" \ NEMOCLAW_MODEL="test-model" \ COMPATIBLE_API_KEY="fake-key-for-unreachable-test" \ - NEMOCLAW_SANDBOX_NAME="e2e-unreachable" \ run_with_timeout 120 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1) || exit_code=$? @@ -435,7 +433,6 @@ test_inf_02_openai() { NEMOCLAW_PROVIDER="openai" \ NEMOCLAW_MODEL="$model" \ OPENAI_API_KEY="$api_key" \ - NEMOCLAW_SANDBOX_NAME="e2e-inf-openai" \ run_with_timeout 300 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1 | redact_stream "$api_key" | tee -a "$LOG_FILE" || onboard_exit=$? @@ -510,7 +507,6 @@ test_inf_03_anthropic() { NEMOCLAW_PROVIDER="anthropic" \ NEMOCLAW_MODEL="$model" \ ANTHROPIC_API_KEY="$api_key" \ - NEMOCLAW_SANDBOX_NAME="e2e-inf-anthropic" \ run_with_timeout 300 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1 | redact_stream "$api_key" | tee -a "$LOG_FILE" || onboard_exit=$? @@ -598,7 +594,6 @@ test_inf_09_compatible_endpoint() { NEMOCLAW_ENDPOINT_URL="$endpoint_url" \ NEMOCLAW_MODEL="$endpoint_model" \ COMPATIBLE_API_KEY="$endpoint_key" \ - NEMOCLAW_SANDBOX_NAME="e2e-inf-custom" \ run_with_timeout 300 nemoclaw onboard --non-interactive --yes-i-accept-third-party-software \ 2>&1 | redact_stream "$endpoint_key" | tee -a "$LOG_FILE" || onboard_exit=$? From dbd25ae068234077c920c0e8a0d494da0aabe5a1 Mon Sep 17 00:00:00 2001 From: latenighthackathon Date: Tue, 28 Apr 2026 17:11:39 +0000 Subject: [PATCH 5/5] fix(test/docs): address CodeRabbit follow-ups on previous commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - docs/reference/troubleshooting.md — credential rotation example was re-targeting the sandbox without setting the new token, so the rotation check had nothing fresh to detect. Show TELEGRAM_BOT_TOKEN alongside NEMOCLAW_SANDBOX_NAME in the rerun command and use a placeholder so the example is generic across the three messaging providers (TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN, SLACK_BOT_TOKEN) named in the surrounding prose. - test/e2e/brev-e2e.test.ts — extract E2E_SANDBOX_NAME constant and replace the 11 inline "e2e-test" literals across env exports, registry construction, polling, and assertions. The previous commit added another inline literal at line 514 to a file that already duplicated the name, which made future renames easy to miss. Signed-off-by: latenighthackathon --- docs/reference/troubleshooting.md | 2 +- test/e2e/brev-e2e.test.ts | 39 ++++++++++++++++++------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md index 5305249160..f012793d3c 100644 --- a/docs/reference/troubleshooting.md +++ b/docs/reference/troubleshooting.md @@ -350,7 +350,7 @@ If you suspect a sandbox is still using a stale token, re-run onboarding so the credential check runs: ```console -$ NEMOCLAW_SANDBOX_NAME=my-assistant nemoclaw onboard --non-interactive --yes-i-accept-third-party-software +$ TELEGRAM_BOT_TOKEN= NEMOCLAW_SANDBOX_NAME= nemoclaw onboard --non-interactive --yes-i-accept-third-party-software ``` ### Sandbox creation killed by OOM (exit 137) diff --git a/test/e2e/brev-e2e.test.ts b/test/e2e/brev-e2e.test.ts index da3574fd39..26f3904562 100644 --- a/test/e2e/brev-e2e.test.ts +++ b/test/e2e/brev-e2e.test.ts @@ -51,6 +51,13 @@ const TEST_SUITE = process.env.TEST_SUITE || "full"; const REPO_DIR = path.resolve(import.meta.dirname, "../.."); const CLI_PATH = path.join(REPO_DIR, "bin", "nemoclaw.js"); +// Single source of truth for the sandbox name used throughout this suite — +// shell exports, registry writes, polling, assertions, and the launchable +// background onboard path all reference it. Keep them aligned through this +// constant rather than inline literals so renames stay safe (CR follow-up +// on #2618). +const E2E_SANDBOX_NAME = "e2e-test"; + function requireInstanceName(): string { if (!INSTANCE_NAME) { throw new Error("INSTANCE_NAME is required for Brev E2E tests"); @@ -158,7 +165,7 @@ function sshEnv( `export GITHUB_TOKEN='${shellEscape(process.env.GITHUB_TOKEN)}'`, `export NEMOCLAW_NON_INTERACTIVE=1`, `export NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1`, - `export NEMOCLAW_SANDBOX_NAME=e2e-test`, + `export NEMOCLAW_SANDBOX_NAME=${E2E_SANDBOX_NAME}`, ]; // Forward optional messaging tokens for the messaging-providers test for (const key of [ @@ -284,7 +291,7 @@ function runLocalDeploy(instanceName: string): void { ...process.env, NEMOCLAW_NON_INTERACTIVE: "1", NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", - NEMOCLAW_SANDBOX_NAME: "e2e-test", + NEMOCLAW_SANDBOX_NAME: E2E_SANDBOX_NAME, NEMOCLAW_PROVIDER: process.env.NEMOCLAW_PROVIDER || "build", NEMOCLAW_DEPLOY_NO_CONNECT: "1", NEMOCLAW_DEPLOY_NO_START_SERVICES: "1", @@ -511,7 +518,7 @@ function pollForSandboxReady(elapsed: () => string): void { [ `source ~/.nvm/nvm.sh 2>/dev/null || true`, `cd ${remoteDir}`, - `NEMOCLAW_SANDBOX_NAME="e2e-test" nohup nemoclaw onboard --non-interactive /tmp/nemoclaw-onboard.log 2>&1 & disown`, + `NEMOCLAW_SANDBOX_NAME="${E2E_SANDBOX_NAME}" nohup nemoclaw onboard --non-interactive /tmp/nemoclaw-onboard.log 2>&1 & disown`, `sleep 2`, `echo "onboard launched"`, ].join(" && "), @@ -549,8 +556,8 @@ function pollForSandboxReady(elapsed: () => string): void { const sandboxList = ssh(`openshell sandbox list 2>/dev/null || true`, { timeout: 15_000, }); - if (sandboxList.includes("e2e-test") && sandboxList.includes("Ready")) { - console.log(`[${onboardElapsed()}] Sandbox e2e-test is Ready!`); + if (sandboxList.includes(E2E_SANDBOX_NAME) && sandboxList.includes("Ready")) { + console.log(`[${onboardElapsed()}] Sandbox ${E2E_SANDBOX_NAME} is Ready!`); break; } // Show onboard progress from the log @@ -588,7 +595,7 @@ function pollForSandboxReady(elapsed: () => string): void { // Verify sandbox is actually ready const finalList = ssh(`openshell sandbox list 2>/dev/null`, { timeout: 15_000 }); - if (!finalList.includes("e2e-test") || !finalList.includes("Ready")) { + if (!finalList.includes(E2E_SANDBOX_NAME) || !finalList.includes("Ready")) { const failLog = ssh("cat /tmp/nemoclaw-onboard.log 2>/dev/null || echo 'no log'", { timeout: 10_000, }); @@ -623,10 +630,10 @@ function writeManualRegistry(elapsed: () => string): void { // Write the sandbox registry using printf to avoid heredoc quoting issues over SSH const registryJson = JSON.stringify( { - defaultSandbox: "e2e-test", + defaultSandbox: E2E_SANDBOX_NAME, sandboxes: { - "e2e-test": { - name: "e2e-test", + [E2E_SANDBOX_NAME]: { + name: E2E_SANDBOX_NAME, createdAt: new Date().toISOString(), model: null, nimContainer: null, @@ -700,11 +707,11 @@ describe.runIf(hasRequiredVars && hasAuthenticatedBrev)("Brev E2E", () => { if (TEST_SUITE !== "full") { console.log(`[${elapsed()}] Verifying sandbox registry...`); const registry = JSON.parse(ssh(`cat ~/.nemoclaw/sandboxes.json`, { timeout: 10_000 })); - expect(registry.defaultSandbox).toBe("e2e-test"); - expect(registry.sandboxes).toHaveProperty("e2e-test"); - const sandbox = registry.sandboxes["e2e-test"]; + expect(registry.defaultSandbox).toBe(E2E_SANDBOX_NAME); + expect(registry.sandboxes).toHaveProperty(E2E_SANDBOX_NAME); + const sandbox = registry.sandboxes[E2E_SANDBOX_NAME]; expect(sandbox).toMatchObject({ - name: "e2e-test", + name: E2E_SANDBOX_NAME, gpuEnabled: false, policies: ["pypi", "npm"], }); @@ -766,12 +773,12 @@ describe.runIf(hasRequiredVars && hasAuthenticatedBrev)("Brev E2E", () => { "export PATH=$HOME/.local/bin:$PATH && openshell sandbox list 2>/dev/null", { timeout: 30_000 }, ); - expect(sandboxList).toContain("e2e-test"); + expect(sandboxList).toContain(E2E_SANDBOX_NAME); expect(sandboxList).toContain("Ready"); const registry = JSON.parse(ssh("cat ~/.nemoclaw/sandboxes.json", { timeout: 10_000 })); - expect(registry.defaultSandbox).toBe("e2e-test"); - expect(registry.sandboxes).toHaveProperty("e2e-test"); + expect(registry.defaultSandbox).toBe(E2E_SANDBOX_NAME); + expect(registry.sandboxes).toHaveProperty(E2E_SANDBOX_NAME); }, 120_000, );