Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@ const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1";

// ── Helpers ──────────────────────────────────────────────────────

/**
* 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" };
}
// 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 (normalized.length > 64) {
return { valid: false, error: "Sandbox name must be 64 characters or less" };
}
return { valid: true, normalized };
}

/**
* 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}`);
Expand Down Expand Up @@ -150,8 +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 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);
Expand All @@ -162,7 +204,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);
}

Expand All @@ -181,7 +223,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");
Expand All @@ -195,7 +237,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 });
Expand Down
23 changes: 20 additions & 3 deletions nemoclaw-blueprint/orchestrator/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -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": {
Expand All @@ -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],
Expand All @@ -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])

Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion nemoclaw/dist/index.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 20 additions & 3 deletions nemoclaw/dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading