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
24 changes: 20 additions & 4 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ const HOST_GATEWAY_URL = "http://host.openshell.internal";

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

/**
* Validate a sandbox name to prevent shell injection and OpenShell argument
* parsing issues (e.g., WSL truncation on hyphens).
* Allowed: lowercase alphanumeric, hyphens, underscores. Must start with a letter.
*/
function validateSandboxName(name) {
if (!name || typeof name !== "string") return false;
return /^[a-zA-Z][a-zA-Z0-9_-]{0,62}$/.test(name);
}

function step(n, total, msg) {
console.log("");
console.log(` [${n}/${total}] ${msg}`);
Expand Down Expand Up @@ -130,6 +140,12 @@ async function createSandbox(gpu) {
const nameAnswer = await prompt(" Sandbox name [my-assistant]: ");
const sandboxName = nameAnswer || "my-assistant";

if (!validateSandboxName(sandboxName)) {
console.error(` Invalid sandbox name: '${sandboxName}'`);
console.error(" Names must start with a letter and contain only letters, digits, hyphens, or underscores.");
process.exit(1);
}

// Check if sandbox already exists in registry
const existing = registry.getSandbox(sandboxName);
if (existing) {
Expand All @@ -139,7 +155,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 "${sandboxName}" 2>/dev/null || true`, { ignoreError: true });
registry.removeSandbox(sandboxName);
}

Expand All @@ -158,7 +174,7 @@ async function createSandbox(gpu) {
const basePolicyPath = path.join(ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml");
const createArgs = [
`--from "${buildCtx}/Dockerfile"`,
`--name ${sandboxName}`,
`--name "${sandboxName}"`,
`--policy "${basePolicyPath}"`,
];
if (gpu && gpu.nimCapable) createArgs.push("--gpu");
Expand All @@ -172,7 +188,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 "${sandboxName}"`, { ignoreError: true });

// Clean up build context
run(`rm -rf "${buildCtx}"`, { ignoreError: true });
Expand Down Expand Up @@ -478,4 +494,4 @@ async function onboard() {
printDashboard(sandboxName, model, provider);
}

module.exports = { onboard };
module.exports = { onboard, validateSandboxName };
4 changes: 2 additions & 2 deletions bin/lib/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ function applyPreset(sandboxName, presetName) {
let rawPolicy = "";
try {
rawPolicy = runCapture(
`openshell policy get --full ${sandboxName} 2>/dev/null`,
`openshell policy get --full "${sandboxName}" 2>/dev/null`,
{ ignoreError: true }
);
} catch {}
Expand Down Expand Up @@ -146,7 +146,7 @@ function applyPreset(sandboxName, presetName) {
fs.writeFileSync(tmpFile, merged, "utf-8");

try {
run(`openshell policy set --policy "${tmpFile}" --wait ${sandboxName}`);
run(`openshell policy set --policy "${tmpFile}" --wait "${sandboxName}"`);
console.log(` Applied preset: ${presetName}`);
} finally {
fs.unlinkSync(tmpFile);
Expand Down
51 changes: 51 additions & 0 deletions test/sandbox-name.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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");

const { validateSandboxName } = require("../bin/lib/onboard");

describe("validateSandboxName", () => {
it("accepts simple names", () => {
assert.ok(validateSandboxName("my-assistant"));
assert.ok(validateSandboxName("sandbox1"));
assert.ok(validateSandboxName("test_box"));
});

it("accepts hyphenated names (WSL regression)", () => {
assert.ok(validateSandboxName("my-assistant"));
assert.ok(validateSandboxName("dev-sandbox-01"));
assert.ok(validateSandboxName("a-b-c-d"));
});

it("rejects empty or non-string input", () => {
assert.equal(validateSandboxName(""), false);
assert.equal(validateSandboxName(null), false);
assert.equal(validateSandboxName(undefined), false);
});

it("rejects names starting with non-letter", () => {
assert.equal(validateSandboxName("1sandbox"), false);
assert.equal(validateSandboxName("-sandbox"), false);
assert.equal(validateSandboxName("_sandbox"), false);
});

it("rejects names with spaces or shell metacharacters", () => {
assert.equal(validateSandboxName("my sandbox"), false);
assert.equal(validateSandboxName("sandbox;rm -rf"), false);
assert.equal(validateSandboxName("test$(whoami)"), false);
assert.equal(validateSandboxName("name`cmd`"), false);
assert.equal(validateSandboxName("a b"), false);
});

it("rejects names that could cause argument injection", () => {
assert.equal(validateSandboxName("--policy"), false);
assert.equal(validateSandboxName("-wait"), false);
});

it("rejects excessively long names", () => {
assert.equal(validateSandboxName("a".repeat(64)), false);
assert.ok(validateSandboxName("a".repeat(63)));
});
});