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..f012793d3c 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 +$ 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/src/lib/onboard-command.test.ts b/src/lib/onboard-command.test.ts index 8257651cdf..1227eed7a0 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,179 @@ 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"); + }); + + 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 4497414686..9df14ae641 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) { @@ -120,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"), @@ -137,6 +175,7 @@ export function parseOnboardArgs( agent, dangerouslySkipPermissions: parsedArgs.includes("--dangerously-skip-permissions"), controlUiPort, + sandboxName, }; } @@ -148,6 +187,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); } diff --git a/test/e2e/brev-e2e.test.ts b/test/e2e/brev-e2e.test.ts index 087ffca26f..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}`, - `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, ); 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