Skip to content
Open
24 changes: 17 additions & 7 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -2258,32 +2258,42 @@ async function recoverGatewayRuntime() {
// ── Step 3: Sandbox ──────────────────────────────────────────────

async function promptValidatedSandboxName() {
while (true) {
const MAX_ATTEMPTS = 3;
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
const nameAnswer = await promptOrDefault(
" Sandbox name (lowercase, numbers, hyphens) [my-assistant]: ",
" Sandbox name (lowercase, starts with letter, hyphens ok) [my-assistant]: ",
"NEMOCLAW_SANDBOX_NAME",
"my-assistant",
);
const sandboxName = (nameAnswer || "my-assistant").trim().toLowerCase();

// Validate: RFC 1123 subdomain — lowercase alphanumeric and hyphens,
// must start and end with alphanumeric (required by Kubernetes/OpenShell)
if (/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sandboxName)) {
// must start with a letter (not a digit) to satisfy Kubernetes naming.
if (/^[a-z]([a-z0-9-]*[a-z0-9])?$/.test(sandboxName)) {
return sandboxName;
}

console.error(` Invalid sandbox name: '${sandboxName}'`);
console.error(" Names must be lowercase, contain only letters, numbers, and hyphens,");
console.error(" and must start and end with a letter or number.");
if (/^[0-9]/.test(sandboxName)) {
console.error(" Names must start with a letter, not a digit.");
} else {
console.error(" Names must be lowercase, contain only letters, numbers, and hyphens,");
console.error(" and must start and end with a letter or number.");
}

// Non-interactive runs cannot re-prompt — abort so the caller can fix the
// NEMOCLAW_SANDBOX_NAME env var and retry.
if (isNonInteractive()) {
process.exit(1);
}

console.error(" Please try again.\n");
if (attempt < MAX_ATTEMPTS - 1) {
console.error(" Please try again.\n");
}
}

console.error(" Too many invalid attempts.");
process.exit(1);
}

// ── Step 5: Sandbox ──────────────────────────────────────────────
Expand Down
18 changes: 18 additions & 0 deletions test/onboard.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,24 @@ describe("onboard helpers", () => {
}
});

it("rejects sandbox names starting with a digit", () => {
// The validation regex must require names to start with a letter,
// not a digit — Kubernetes rejects digit-prefixed names downstream.
const SANDBOX_NAME_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;

expect(SANDBOX_NAME_REGEX.test("my-assistant")).toBe(true);
expect(SANDBOX_NAME_REGEX.test("a")).toBe(true);
expect(SANDBOX_NAME_REGEX.test("agent-1")).toBe(true);
expect(SANDBOX_NAME_REGEX.test("test-sandbox-v2")).toBe(true);

expect(SANDBOX_NAME_REGEX.test("7racii")).toBe(false);
expect(SANDBOX_NAME_REGEX.test("1sandbox")).toBe(false);
expect(SANDBOX_NAME_REGEX.test("123")).toBe(false);
expect(SANDBOX_NAME_REGEX.test("-start-hyphen")).toBe(false);
expect(SANDBOX_NAME_REGEX.test("end-hyphen-")).toBe(false);
expect(SANDBOX_NAME_REGEX.test("")).toBe(false);
});

it("passes credential names to openshell without embedding secret values in argv", () => {
const repoRoot = path.join(import.meta.dirname, "..");
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-inference-"));
Expand Down