From d0ec16b7b2e85784b0b7e544f8a947985d5dce0c Mon Sep 17 00:00:00 2001 From: Hakancan Date: Tue, 17 Mar 2026 14:49:06 +0000 Subject: [PATCH 1/2] fix: quote sandbox names in shell commands and add validation Fixes #166 - Add shellEscape() function to properly escape sandbox names in shell commands - Add validateSandboxName() function to enforce alphanumeric + hyphens only - Apply shell escaping to all shell commands that use sandboxName: - openshell sandbox delete - openshell sandbox create --name - openshell forward start - Exit with error if sandbox name fails validation This prevents shell injection and failures when sandbox names contain spaces or special characters. --- bin/lib/onboard.js | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index cb3b804eaa6..8c24bec59ce 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -16,6 +16,36 @@ const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1"; // ── Helpers ────────────────────────────────────────────────────── +/** + * Validates sandbox name - only alphanumeric characters and hyphens allowed. + * Returns { valid: boolean, error?: string } + */ +function validateSandboxName(name) { + if (!name || typeof name !== "string") { + return { valid: false, error: "Sandbox name is required" }; + } + if (!/^[a-zA-Z0-9-]+$/.test(name)) { + return { valid: false, error: "Sandbox name must contain only letters, numbers, and hyphens" }; + } + if (name.length > 64) { + return { valid: false, error: "Sandbox name must be 64 characters or less" }; + } + return { valid: true }; +} + +/** + * Escapes a string for safe use in shell commands. + * Wraps in single quotes and handles embedded single quotes. + */ +function shellEscape(str) { + if (typeof str !== "string") { + throw new Error("shellEscape: expected string argument"); + } + // Use single quotes and escape any embedded single quotes + // by ending the quote, adding an escaped quote, and starting a new quote + return "'" + str.replace(/'/g, "'\"'\"'") + "'"; +} + function step(n, total, msg) { console.log(""); console.log(` [${n}/${total}] ${msg}`); @@ -153,6 +183,13 @@ async function createSandbox(gpu) { const nameAnswer = await prompt(" Sandbox name [my-assistant]: "); const sandboxName = nameAnswer || "my-assistant"; + // Validate sandbox name + const validation = validateSandboxName(sandboxName); + if (!validation.valid) { + console.error(` Error: ${validation.error}`); + process.exit(1); + } + // Check if sandbox already exists in registry const existing = registry.getSandbox(sandboxName); if (existing) { @@ -162,7 +199,7 @@ async function createSandbox(gpu) { return sandboxName; } // Destroy old sandbox - run(`openshell sandbox delete ${sandboxName} 2>/dev/null || true`, { ignoreError: true }); + run(`openshell sandbox delete ${shellEscape(sandboxName)} 2>/dev/null || true`, { ignoreError: true }); registry.removeSandbox(sandboxName); } @@ -181,7 +218,7 @@ async function createSandbox(gpu) { const basePolicyPath = path.join(ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"); const createArgs = [ `--from "${buildCtx}/Dockerfile"`, - `--name ${sandboxName}`, + `--name ${shellEscape(sandboxName)}`, `--policy "${basePolicyPath}"`, ]; if (gpu && gpu.nimCapable) createArgs.push("--gpu"); @@ -195,7 +232,7 @@ async function createSandbox(gpu) { run(`openshell sandbox create ${createArgs.join(" ")} -- env ${envArgs.join(" ")} nemoclaw-start 2>&1 | awk '/Sandbox allocated/{if(!seen){print;seen=1}next}1'`); // Forward dashboard port separately - run(`openshell forward start --background 18789 ${sandboxName}`, { ignoreError: true }); + run(`openshell forward start --background 18789 ${shellEscape(sandboxName)}`, { ignoreError: true }); // Clean up build context run(`rm -rf "${buildCtx}"`, { ignoreError: true }); From 917d9b068e162c4bab23870d35a14ff66afe47db Mon Sep 17 00:00:00 2001 From: Hakancan Date: Tue, 17 Mar 2026 14:52:49 +0000 Subject: [PATCH 2/2] fix: normalize sandbox names to lowercase to prevent creation failures --- bin/lib/onboard.js | 21 +++-- nemoclaw-blueprint/orchestrator/runner.py | 23 +++++- nemoclaw/dist/index.d.ts.map | 2 +- nemoclaw/dist/index.js | 23 +++++- nemoclaw/dist/index.js.map | 2 +- nemoclaw/src/index.ts | 28 ++++++- test/sandbox-name.test.js | 93 +++++++++++++++++++++++ 7 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 test/sandbox-name.test.js diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index 8c24bec59ce..d3f991497be 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -17,20 +17,23 @@ const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1"; // ── Helpers ────────────────────────────────────────────────────── /** - * Validates sandbox name - only alphanumeric characters and hyphens allowed. - * Returns { valid: boolean, error?: string } + * Validates sandbox name - only lowercase alphanumeric characters and hyphens allowed. + * Names are normalized to lowercase. + * Returns { valid: boolean, normalized?: string, error?: string } */ function validateSandboxName(name) { if (!name || typeof name !== "string") { return { valid: false, error: "Sandbox name is required" }; } - if (!/^[a-zA-Z0-9-]+$/.test(name)) { - return { valid: false, error: "Sandbox name must contain only letters, numbers, and hyphens" }; + // Normalize to lowercase + const normalized = name.toLowerCase(); + if (!/^[a-z0-9-]+$/.test(normalized)) { + return { valid: false, error: "Sandbox name must contain only lowercase letters, numbers, and hyphens" }; } - if (name.length > 64) { + if (normalized.length > 64) { return { valid: false, error: "Sandbox name must be 64 characters or less" }; } - return { valid: true }; + return { valid: true, normalized }; } /** @@ -180,15 +183,17 @@ async function startGateway(gpu) { async function createSandbox(gpu) { step(3, 7, "Creating sandbox"); + console.log(" Naming rules: lowercase letters, numbers, and hyphens only (e.g., my-assistant)"); const nameAnswer = await prompt(" Sandbox name [my-assistant]: "); - const sandboxName = nameAnswer || "my-assistant"; + let sandboxName = nameAnswer || "my-assistant"; - // Validate sandbox name + // Validate and normalize sandbox name const validation = validateSandboxName(sandboxName); if (!validation.valid) { console.error(` Error: ${validation.error}`); process.exit(1); } + sandboxName = validation.normalized; // Check if sandbox already exists in registry const existing = registry.getSandbox(sandboxName); diff --git a/nemoclaw-blueprint/orchestrator/runner.py b/nemoclaw-blueprint/orchestrator/runner.py index 432c228c3be..282babc21ef 100644 --- a/nemoclaw-blueprint/orchestrator/runner.py +++ b/nemoclaw-blueprint/orchestrator/runner.py @@ -17,6 +17,7 @@ import argparse import json import os +import re import shutil import subprocess import sys @@ -109,12 +110,15 @@ def action_plan( if endpoint_url: inference_cfg = {**inference_cfg, "endpoint": endpoint_url} + # Normalize sandbox name to lowercase + sandbox_name = normalize_sandbox_name(sandbox_cfg.get("name", "openclaw")) + plan: dict[str, Any] = { "run_id": rid, "profile": profile, "sandbox": { "image": sandbox_cfg.get("image", "openclaw"), - "name": sandbox_cfg.get("name", "openclaw"), + "name": sandbox_name, "forward_ports": sandbox_cfg.get("forward_ports", [18789]), }, "inference": { @@ -135,6 +139,17 @@ def action_plan( return plan +def normalize_sandbox_name(name: str) -> str: + """Normalize sandbox name to lowercase and validate it.""" + normalized = name.lower() + # Only lowercase letters, numbers, and hyphens allowed + if not re.match(r'^[a-z0-9-]+$', normalized): + raise ValueError(f"Invalid sandbox name: '{name}'. Only lowercase letters, numbers, and hyphens are allowed.") + if len(normalized) > 64: + raise ValueError(f"Sandbox name too long: '{name}'. Must be 64 characters or less.") + return normalized + + def action_apply( profile: str, blueprint: dict[str, Any], @@ -160,7 +175,8 @@ def action_apply( sandbox_cfg: dict[str, Any] = blueprint.get("components", {}).get("sandbox", {}) - sandbox_name: str = sandbox_cfg.get("name", "openclaw") + # Normalize sandbox name to lowercase + sandbox_name: str = normalize_sandbox_name(sandbox_cfg.get("name", "openclaw")) sandbox_image: str = sandbox_cfg.get("image", "openclaw") forward_ports: list[int] = sandbox_cfg.get("forward_ports", [18789]) @@ -282,7 +298,8 @@ def action_rollback(rid: str) -> None: plan_file = state_dir / "plan.json" if plan_file.exists(): plan = json.loads(plan_file.read_text()) - sandbox_name = plan.get("sandbox_name", "openclaw") + # Normalize sandbox name from plan (in case it was saved before normalization) + sandbox_name = normalize_sandbox_name(plan.get("sandbox_name", "openclaw")) progress(30, f"Stopping sandbox {sandbox_name}") run_cmd( diff --git a/nemoclaw/dist/index.d.ts.map b/nemoclaw/dist/index.d.ts.map index d775191d583..d53d959925f 100644 --- a/nemoclaw/dist/index.d.ts.map +++ b/nemoclaw/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nemoclaw/dist/index.js b/nemoclaw/dist/index.js index 9268d4b7c60..06097584d3c 100644 --- a/nemoclaw/dist/index.js +++ b/nemoclaw/dist/index.js @@ -13,6 +13,25 @@ const DEFAULT_PLUGIN_CONFIG = { sandboxName: "openclaw", inferenceProvider: "nvidia", }; +/** + * Normalizes a sandbox name to lowercase and validates it. + * Returns the normalized name or the default if invalid. + */ +function normalizeSandboxName(name, defaultName) { + if (typeof name !== "string" || !name) { + return defaultName; + } + // Normalize to lowercase + const normalized = name.toLowerCase(); + // Validate: only lowercase letters, numbers, and hyphens allowed + if (!/^[a-z0-9-]+$/.test(normalized)) { + return defaultName; + } + if (normalized.length > 64) { + return defaultName; + } + return normalized; +} function getPluginConfig(api) { const raw = api.pluginConfig ?? {}; return { @@ -22,9 +41,7 @@ function getPluginConfig(api) { blueprintRegistry: typeof raw["blueprintRegistry"] === "string" ? raw["blueprintRegistry"] : DEFAULT_PLUGIN_CONFIG.blueprintRegistry, - sandboxName: typeof raw["sandboxName"] === "string" - ? raw["sandboxName"] - : DEFAULT_PLUGIN_CONFIG.sandboxName, + sandboxName: normalizeSandboxName(raw["sandboxName"], DEFAULT_PLUGIN_CONFIG.sandboxName), inferenceProvider: typeof raw["inferenceProvider"] === "string" ? raw["inferenceProvider"] : DEFAULT_PLUGIN_CONFIG.inferenceProvider, diff --git a/nemoclaw/dist/index.js.map b/nemoclaw/dist/index.js.map index e45a1adab07..f416a8b1c6a 100644 --- a/nemoclaw/dist/index.js.map +++ b/nemoclaw/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nemoclaw/src/index.ts b/nemoclaw/src/index.ts index 796564d8be2..2cc13a93931 100644 --- a/nemoclaw/src/index.ts +++ b/nemoclaw/src/index.ts @@ -150,6 +150,26 @@ const DEFAULT_PLUGIN_CONFIG: NemoClawConfig = { inferenceProvider: "nvidia", }; +/** + * Normalizes a sandbox name to lowercase and validates it. + * Returns the normalized name or the default if invalid. + */ +function normalizeSandboxName(name: string | unknown, defaultName: string): string { + if (typeof name !== "string" || !name) { + return defaultName; + } + // Normalize to lowercase + const normalized = name.toLowerCase(); + // Validate: only lowercase letters, numbers, and hyphens allowed + if (!/^[a-z0-9-]+$/.test(normalized)) { + return defaultName; + } + if (normalized.length > 64) { + return defaultName; + } + return normalized; +} + export function getPluginConfig(api: OpenClawPluginApi): NemoClawConfig { const raw = api.pluginConfig ?? {}; return { @@ -161,10 +181,10 @@ export function getPluginConfig(api: OpenClawPluginApi): NemoClawConfig { typeof raw["blueprintRegistry"] === "string" ? raw["blueprintRegistry"] : DEFAULT_PLUGIN_CONFIG.blueprintRegistry, - sandboxName: - typeof raw["sandboxName"] === "string" - ? raw["sandboxName"] - : DEFAULT_PLUGIN_CONFIG.sandboxName, + sandboxName: normalizeSandboxName( + raw["sandboxName"], + DEFAULT_PLUGIN_CONFIG.sandboxName, + ), inferenceProvider: typeof raw["inferenceProvider"] === "string" ? raw["inferenceProvider"] diff --git a/test/sandbox-name.test.js b/test/sandbox-name.test.js new file mode 100644 index 00000000000..89def65edba --- /dev/null +++ b/test/sandbox-name.test.js @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); + +// Import the validateSandboxName function from onboard.js +// We need to extract it for testing +const fs = require("fs"); +const path = require("path"); + +// Read and evaluate the onboard.js file to extract the validateSandboxName function +const onboardPath = path.join(__dirname, "..", "bin", "lib", "onboard.js"); +const onboardCode = fs.readFileSync(onboardPath, "utf-8"); + +// Extract just the validateSandboxName function using regex +const validateSandboxNameMatch = onboardCode.match( + /function validateSandboxName\(name\) \{[\s\S]*?\n\}/ +); +assert.ok(validateSandboxNameMatch, "validateSandboxName function not found"); + +// Create a function from the extracted code +const validateSandboxName = new Function( + "return " + validateSandboxNameMatch[0] +)(); + +describe("validateSandboxName", () => { + it("accepts valid lowercase names", () => { + const result = validateSandboxName("my-assistant"); + assert.equal(result.valid, true); + assert.equal(result.normalized, "my-assistant"); + }); + + it("normalizes uppercase letters to lowercase", () => { + const result = validateSandboxName("My-Assistant"); + assert.equal(result.valid, true); + assert.equal(result.normalized, "my-assistant"); + }); + + it("normalizes all uppercase names to lowercase", () => { + const result = validateSandboxName("MY-ASSISTANT"); + assert.equal(result.valid, true); + assert.equal(result.normalized, "my-assistant"); + }); + + it("accepts names with numbers", () => { + const result = validateSandboxName("assistant-123"); + assert.equal(result.valid, true); + assert.equal(result.normalized, "assistant-123"); + }); + + it("accepts names starting with numbers", () => { + const result = validateSandboxName("123-assistant"); + assert.equal(result.valid, true); + assert.equal(result.normalized, "123-assistant"); + }); + + it("rejects names with special characters", () => { + const result = validateSandboxName("my_assistant"); + assert.equal(result.valid, false); + assert.ok(result.error.includes("lowercase letters, numbers, and hyphens")); + }); + + it("rejects names with spaces", () => { + const result = validateSandboxName("my assistant"); + assert.equal(result.valid, false); + }); + + it("rejects empty names", () => { + const result = validateSandboxName(""); + assert.equal(result.valid, false); + }); + + it("rejects names longer than 64 characters", () => { + const longName = "a".repeat(65); + const result = validateSandboxName(longName); + assert.equal(result.valid, false); + assert.ok(result.error.includes("64 characters")); + }); + + it("accepts names exactly 64 characters", () => { + const name64 = "a".repeat(64); + const result = validateSandboxName(name64); + assert.equal(result.valid, true); + assert.equal(result.normalized, name64); + }); + + it("normalizes mixed case with numbers and hyphens", () => { + const result = validateSandboxName("My-Assistant-123-Test"); + assert.equal(result.valid, true); + assert.equal(result.normalized, "my-assistant-123-test"); + }); +});