Skip to content
Merged
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
6 changes: 2 additions & 4 deletions bin/lib/local-inference.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

const { shellQuote } = require("./runner");

const HOST_GATEWAY_URL = "http://host.openshell.internal";
const CONTAINER_REACHABILITY_IMAGE = "curlimages/curl:8.10.1";
const DEFAULT_OLLAMA_MODEL = "nemotron-3-nano:30b";
Expand Down Expand Up @@ -114,10 +116,6 @@ function getDefaultOllamaModel(runCapture) {
return models.includes(DEFAULT_OLLAMA_MODEL) ? DEFAULT_OLLAMA_MODEL : models[0];
}

function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}

function getOllamaWarmupCommand(model, keepAlive = "15m") {
const payload = JSON.stringify({
model,
Expand Down
21 changes: 12 additions & 9 deletions bin/lib/nim.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//
// NIM container management — pull, start, stop, health-check NIM images.

const { run, runCapture } = require("./runner");
const { run, runCapture, shellQuote } = require("./runner");
const nimImages = require("./nim-images.json");

function containerName(sandboxName) {
Expand Down Expand Up @@ -121,7 +121,7 @@ function pullNimImage(model) {
process.exit(1);
}
console.log(` Pulling NIM image: ${image}`);
run(`docker pull ${image}`);
run(`docker pull ${shellQuote(image)}`);
return image;
}

Expand All @@ -134,23 +134,25 @@ function startNimContainer(sandboxName, model, port = 8000) {
}

// Stop any existing container with same name
run(`docker rm -f ${name} 2>/dev/null || true`, { ignoreError: true });
const qn = shellQuote(name);
run(`docker rm -f ${qn} 2>/dev/null || true`, { ignoreError: true });

console.log(` Starting NIM container: ${name}`);
run(
`docker run -d --gpus all -p ${port}:8000 --name ${name} --shm-size 16g ${image}`
`docker run -d --gpus all -p ${Number(port)}:8000 --name ${qn} --shm-size 16g ${shellQuote(image)}`
);
return name;
}

function waitForNimHealth(port = 8000, timeout = 300) {
const start = Date.now();
const interval = 5000;
console.log(` Waiting for NIM health on port ${port} (timeout: ${timeout}s)...`);
const safePort = Number(port);
console.log(` Waiting for NIM health on port ${safePort} (timeout: ${timeout}s)...`);

while ((Date.now() - start) / 1000 < timeout) {
try {
const result = runCapture(`curl -sf http://localhost:${port}/v1/models`, {
const result = runCapture(`curl -sf http://localhost:${safePort}/v1/models`, {
ignoreError: true,
});
if (result) {
Expand All @@ -167,16 +169,17 @@ function waitForNimHealth(port = 8000, timeout = 300) {

function stopNimContainer(sandboxName) {
const name = containerName(sandboxName);
const qn = shellQuote(name);
console.log(` Stopping NIM container: ${name}`);
run(`docker stop ${name} 2>/dev/null || true`, { ignoreError: true });
run(`docker rm ${name} 2>/dev/null || true`, { ignoreError: true });
run(`docker stop ${qn} 2>/dev/null || true`, { ignoreError: true });
run(`docker rm ${qn} 2>/dev/null || true`, { ignoreError: true });
}

function nimStatus(sandboxName) {
const name = containerName(sandboxName);
try {
const state = runCapture(
`docker inspect --format '{{.State.Status}}' ${name} 2>/dev/null`,
`docker inspect --format '{{.State.Status}}' ${shellQuote(name)} 2>/dev/null`,
{ ignoreError: true }
);
if (!state) return { running: false, container: name };
Expand Down
18 changes: 7 additions & 11 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
const fs = require("fs");
const os = require("os");
const path = require("path");
const { ROOT, SCRIPTS, run, runCapture } = require("./runner");
const { ROOT, SCRIPTS, run, runCapture, shellQuote } = require("./runner");
const {
getDefaultOllamaModel,
getLocalProviderBaseUrl,
Expand Down Expand Up @@ -84,10 +84,6 @@ function step(n, total, msg) {
console.log(` ${"─".repeat(50)}`);
}

function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}

function getInstalledOpenshellVersion(versionOutput = null) {
const output = String(versionOutput ?? runCapture("openshell -V", { ignoreError: true })).trim();
const match = output.match(/openshell\s+([0-9]+\.[0-9]+\.[0-9]+)/i);
Expand Down Expand Up @@ -447,9 +443,9 @@ async function createSandbox(gpu) {

console.log(` Creating sandbox '${sandboxName}' (this takes a few minutes on first run)...`);
const chatUiUrl = process.env.CHAT_UI_URL || 'http://127.0.0.1:18789';
const envArgs = [`CHAT_UI_URL=${chatUiUrl}`];
const envArgs = [`CHAT_UI_URL=${shellQuote(chatUiUrl)}`];
if (process.env.NVIDIA_API_KEY) {
envArgs.push(`NVIDIA_API_KEY=${process.env.NVIDIA_API_KEY}`);
envArgs.push(`NVIDIA_API_KEY=${shellQuote(process.env.NVIDIA_API_KEY)}`);
}

// Run without piping through awk — the pipe masked non-zero exit codes
Expand Down Expand Up @@ -711,12 +707,12 @@ async function setupInference(sandboxName, model, provider) {
// Create nvidia-nim provider
run(
`openshell provider create --name nvidia-nim --type openai ` +
`--credential "NVIDIA_API_KEY=${process.env.NVIDIA_API_KEY}" ` +
`--credential ${shellQuote("NVIDIA_API_KEY=" + process.env.NVIDIA_API_KEY)} ` +
`--config "OPENAI_BASE_URL=https://integrate.api.nvidia.com/v1" 2>&1 || true`,
{ ignoreError: true }
);
run(
`openshell inference set --no-verify --provider nvidia-nim --model ${model} 2>/dev/null || true`,
`openshell inference set --no-verify --provider nvidia-nim --model ${shellQuote(model)} 2>/dev/null || true`,
{ ignoreError: true }
);
} else if (provider === "vllm-local") {
Expand All @@ -735,7 +731,7 @@ async function setupInference(sandboxName, model, provider) {
{ ignoreError: true }
);
run(
`openshell inference set --no-verify --provider vllm-local --model ${model} 2>/dev/null || true`,
`openshell inference set --no-verify --provider vllm-local --model ${shellQuote(model)} 2>/dev/null || true`,
{ ignoreError: true }
);
} else if (provider === "ollama-local") {
Expand All @@ -755,7 +751,7 @@ async function setupInference(sandboxName, model, provider) {
{ ignoreError: true }
);
run(
`openshell inference set --no-verify --provider ollama-local --model ${model} 2>/dev/null || true`,
`openshell inference set --no-verify --provider ollama-local --model ${shellQuote(model)} 2>/dev/null || true`,
{ ignoreError: true }
);
console.log(` Priming Ollama model: ${model}`);
Expand Down
20 changes: 13 additions & 7 deletions bin/lib/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
const fs = require("fs");
const path = require("path");
const os = require("os");
const { ROOT, run, runCapture } = require("./runner");
const { ROOT, run, runCapture, shellQuote } = require("./runner");
const registry = require("./registry");

const PRESETS_DIR = path.join(ROOT, "nemoclaw-blueprint", "policies", "presets");
Expand All @@ -29,7 +29,11 @@ function listPresets() {
}

function loadPreset(name) {
const file = path.join(PRESETS_DIR, `${name}.yaml`);
const file = path.resolve(PRESETS_DIR, `${name}.yaml`);
if (!file.startsWith(PRESETS_DIR + path.sep) && file !== PRESETS_DIR) {
console.error(` Invalid preset name: ${name}`);
return null;
}
if (!fs.existsSync(file)) {
console.error(` Preset not found: ${name}`);
return null;
Expand Down Expand Up @@ -73,14 +77,14 @@ function parseCurrentPolicy(raw) {
* Build the openshell policy set command with properly quoted arguments.
*/
function buildPolicySetCommand(policyFile, sandboxName) {
return `openshell policy set --policy "${policyFile}" --wait "${sandboxName}"`;
return `openshell policy set --policy ${shellQuote(policyFile)} --wait ${shellQuote(sandboxName)}`;
}

/**
* Build the openshell policy get command with properly quoted arguments.
*/
function buildPolicyGetCommand(sandboxName) {
return `openshell policy get --full "${sandboxName}" 2>/dev/null`;
return `openshell policy get --full ${shellQuote(sandboxName)} 2>/dev/null`;
}

function applyPreset(sandboxName, presetName) {
Expand Down Expand Up @@ -166,15 +170,17 @@ function applyPreset(sandboxName, presetName) {
}

// Write temp file and apply
const tmpFile = path.join(os.tmpdir(), `nemoclaw-policy-${Date.now()}.yaml`);
fs.writeFileSync(tmpFile, merged, "utf-8");
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-policy-"));
const tmpFile = path.join(tmpDir, "policy.yaml");
fs.writeFileSync(tmpFile, merged, { encoding: "utf-8", mode: 0o600 });

try {
run(buildPolicySetCommand(tmpFile, sandboxName));

console.log(` Applied preset: ${presetName}`);
} finally {
fs.unlinkSync(tmpFile);
try { fs.unlinkSync(tmpFile); } catch {}
try { fs.rmdirSync(tmpDir); } catch {}
}

// Update registry
Expand Down
29 changes: 28 additions & 1 deletion bin/lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,31 @@ function runCapture(cmd, opts = {}) {
}
}

module.exports = { ROOT, SCRIPTS, run, runCapture, runInteractive };
/**
* Shell-quote a value for safe interpolation into bash -c strings.
* Wraps in single quotes and escapes embedded single quotes.
*/
function shellQuote(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}

/**
* Validate a name (sandbox, instance, container) against RFC 1123 label rules.
* Rejects shell metacharacters, path traversal, and empty/overlength names.
*/
function validateName(name, label = "name") {
if (!name || typeof name !== "string") {
throw new Error(`${label} is required`);
}
if (name.length > 63) {
throw new Error(`${label} too long (max 63 chars): '${name.slice(0, 20)}...'`);
}
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) {
throw new Error(
`Invalid ${label}: '${name}'. Must be lowercase alphanumeric with optional internal hyphens.`
);
}
return name;
}

module.exports = { ROOT, SCRIPTS, run, runCapture, runInteractive, shellQuote, validateName };
Loading
Loading