Skip to content
Merged
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc
|----------|--------------------|-------|
| Linux | Docker | Primary supported path. |
| macOS (Apple Silicon) | Colima, Docker Desktop | Install Xcode Command Line Tools (`xcode-select --install`) and start the runtime before running the installer. |
| macOS (Intel) | Podman | Not supported yet. Depends on OpenShell support for Podman on macOS. |
| macOS (Intel) | Docker Desktop | Start the runtime before running the installer. |
| Windows WSL | Docker Desktop (WSL backend) | Supported target path. |
| DGX Spark | Docker | Refer to the [DGX Spark setup guide](https://github.com/NVIDIA/NemoClaw/blob/main/spark-install.md) for cgroup v2 and Docker configuration. |
| DGX Spark | Docker | Use the standard installer and `nemoclaw onboard`. |

### Install NemoClaw and Onboard OpenClaw Agent

Expand Down
67 changes: 39 additions & 28 deletions bin/lib/onboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,7 @@ const {
getProviderSelectionConfig,
parseGatewayInference,
} = require("./inference-config");
const {
inferContainerRuntime,
isUnsupportedMacosRuntime,
isWsl,
shouldPatchCoredns,
} = require("./platform");
const { inferContainerRuntime, isWsl, shouldPatchCoredns } = require("./platform");
const { resolveOpenshell } = require("./resolve-openshell");
const {
prompt,
Expand All @@ -54,7 +49,13 @@ const nim = require("./nim");
const onboardSession = require("./onboard-session");
const policies = require("./policies");
const { ensureUsageNoticeConsent } = require("./usage-notice");
const { checkPortAvailable, ensureSwap, getMemoryInfo } = require("./preflight");
const {
assessHost,
checkPortAvailable,
ensureSwap,
getMemoryInfo,
planHostRemediation,
} = require("./preflight");

// Typed modules (compiled from src/lib/*.ts → dist/lib/*.js)
const gatewayState = require("../../dist/lib/gateway-state");
Expand Down Expand Up @@ -1554,20 +1555,27 @@ function getResumeConfigConflicts(session, opts = {}) {
return conflicts;
}

function isDockerRunning() {
try {
runCapture("docker info", { ignoreError: false });
return true;
} catch {
return false;
}
}

function getContainerRuntime() {
const info = runCapture("docker info 2>/dev/null", { ignoreError: true });
return inferContainerRuntime(info);
}

function printRemediationActions(actions) {
if (!Array.isArray(actions) || actions.length === 0) {
return;
}

console.error("");
console.error(" Suggested fix:");
console.error("");
for (const action of actions) {
console.error(` - ${action.title}: ${action.reason}`);
for (const command of action.commands || []) {
console.error(` ${command}`);
}
}
}

function isOpenshellInstalled() {
return resolveOpenshell() !== null;
}
Expand Down Expand Up @@ -1724,24 +1732,27 @@ function getNonInteractiveModel(providerKey) {
async function preflight() {
step(1, 7, "Preflight checks");

// Docker
if (!isDockerRunning()) {
console.error(" Docker is not running. Please start Docker and try again.");
const host = assessHost();

// Docker / runtime
if (!host.dockerReachable) {
console.error(" Docker is not reachable. Please fix Docker and try again.");
printRemediationActions(planHostRemediation(host));
process.exit(1);
}
console.log(" ✓ Docker is running");

const runtime = getContainerRuntime();
if (isUnsupportedMacosRuntime(runtime)) {
console.error(" Podman on macOS is not supported by NemoClaw at this time.");
console.error(
" OpenShell currently depends on Docker host-gateway behavior that Podman on macOS does not provide.",
if (host.runtime !== "unknown") {
console.log(` ✓ Container runtime: ${host.runtime}`);
}
if (host.isUnsupportedRuntime) {
console.warn(
" ! Podman is not a supported OpenShell runtime. NemoClaw will continue, but your experience may vary.",
);
console.error(" Use Colima or Docker Desktop on macOS instead.");
process.exit(1);
printRemediationActions(planHostRemediation(host));
}
if (runtime !== "unknown") {
console.log(` ✓ Container runtime: ${runtime}`);
if (host.notes.includes("Running under WSL")) {
console.log(" ⓘ Running under WSL");
}

// OpenShell CLI
Expand Down
172 changes: 31 additions & 141 deletions bin/nemoclaw.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const YW = _useColor ? "\x1b[1;33m" : "";

const {
ROOT,
SCRIPTS,
run,
runCapture: _runCapture,
runInteractive,
Expand All @@ -32,12 +31,7 @@ const {
} = require("./lib/runner");
const { resolveOpenshell } = require("./lib/resolve-openshell");
const { startGatewayForRecovery } = require("./lib/onboard");
const {
ensureApiKey,
ensureGithubToken,
getCredential,
isRepoPrivate,
} = require("./lib/credentials");
const { getCredential } = require("./lib/credentials");
const registry = require("./lib/registry");
const nim = require("./lib/nim");
const policies = require("./lib/policies");
Expand All @@ -46,6 +40,7 @@ const { getVersion } = require("./lib/version");
const onboardSession = require("./lib/onboard-session");
const { parseLiveSandboxNames } = require("./lib/runtime-recovery");
const { NOTICE_ACCEPT_ENV, NOTICE_ACCEPT_FLAG } = require("./lib/usage-notice");
const { executeDeploy } = require("../dist/lib/deploy");

// ── Global commands ──────────────────────────────────────────────

Expand Down Expand Up @@ -646,139 +641,33 @@ async function setup(args = []) {
await onboard(args);
}

async function setupSpark() {
// setup-spark.sh configures Docker cgroups — it does not use NVIDIA_API_KEY.
run(`sudo bash "${SCRIPTS}/setup-spark.sh"`);
}

// eslint-disable-next-line complexity
async function deploy(instanceName) {
if (!instanceName) {
console.error(" Usage: nemoclaw deploy <instance-name>");
console.error("");
console.error(" Examples:");
console.error(" nemoclaw deploy my-gpu-box");
console.error(" nemoclaw deploy nemoclaw-prod");
console.error(" nemoclaw deploy nemoclaw-test");
process.exit(1);
}
await ensureApiKey();
if (isRepoPrivate("NVIDIA/OpenShell")) {
await ensureGithubToken();
}
validateName(instanceName, "instance name");
const name = instanceName;
const qname = shellQuote(name);
const gpu = process.env.NEMOCLAW_GPU || "a2-highgpu-1g:nvidia-tesla-a100:1";

async function setupSpark(args = []) {
console.log("");
console.log(` Deploying NemoClaw to Brev instance: ${name}`);
console.log(" ⚠ `nemoclaw setup-spark` is deprecated.");
console.log(" Current OpenShell releases handle the old DGX Spark cgroup issue themselves.");
console.log(" Use `nemoclaw onboard` instead.");
console.log("");
await onboard(args);
}

try {
execFileSync("which", ["brev"], { stdio: "ignore" });
} catch {
console.error("brev CLI not found. Install: https://brev.nvidia.com");
process.exit(1);
}

let exists = false;
try {
const out = execFileSync("brev", ["ls"], { encoding: "utf-8" });
exists = out.includes(name);
} catch (err) {
if (err.stdout && err.stdout.includes(name)) exists = true;
if (err.stderr && err.stderr.includes(name)) exists = true;
}

if (!exists) {
console.log(` Creating Brev instance '${name}' (${gpu})...`);
run(`brev create ${qname} --gpu ${shellQuote(gpu)}`);
} else {
console.log(` Brev instance '${name}' already exists.`);
}

run(`brev refresh`, { ignoreError: true });

process.stdout.write(` Waiting for SSH `);
for (let i = 0; i < 60; i++) {
try {
execFileSync(
"ssh",
["-o", "ConnectTimeout=5", "-o", "StrictHostKeyChecking=no", name, "echo", "ok"],
{ encoding: "utf-8", stdio: "ignore" },
);
process.stdout.write(` ${G}✓${R}\n`);
break;
} catch {
if (i === 59) {
process.stdout.write("\n");
console.error(` Timed out waiting for SSH to ${name}`);
process.exit(1);
}
process.stdout.write(".");
spawnSync("sleep", ["3"]);
}
}

console.log(" Syncing NemoClaw to VM...");
run(
`ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR ${qname} 'mkdir -p /home/ubuntu/nemoclaw'`,
);
run(
`rsync -az --delete --exclude node_modules --exclude .git --exclude src -e "ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR" "${ROOT}/scripts" "${ROOT}/Dockerfile" "${ROOT}/nemoclaw" "${ROOT}/nemoclaw-blueprint" "${ROOT}/bin" "${ROOT}/package.json" ${qname}:/home/ubuntu/nemoclaw/`,
);

const envLines = [`NVIDIA_API_KEY=${shellQuote(process.env.NVIDIA_API_KEY || "")}`];
const ghToken = process.env.GITHUB_TOKEN;
if (ghToken) envLines.push(`GITHUB_TOKEN=${shellQuote(ghToken)}`);
const tgToken = getCredential("TELEGRAM_BOT_TOKEN");
if (tgToken) envLines.push(`TELEGRAM_BOT_TOKEN=${shellQuote(tgToken)}`);
const discordToken = getCredential("DISCORD_BOT_TOKEN");
if (discordToken) envLines.push(`DISCORD_BOT_TOKEN=${shellQuote(discordToken)}`);
const slackToken = getCredential("SLACK_BOT_TOKEN");
if (slackToken) envLines.push(`SLACK_BOT_TOKEN=${shellQuote(slackToken)}`);
const envDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-env-"));
const envTmp = path.join(envDir, "env");
fs.writeFileSync(envTmp, envLines.join("\n") + "\n", { mode: 0o600 });
try {
run(
`scp -q -o StrictHostKeyChecking=no -o LogLevel=ERROR ${shellQuote(envTmp)} ${qname}:/home/ubuntu/nemoclaw/.env`,
);
run(
`ssh -q -o StrictHostKeyChecking=no -o LogLevel=ERROR ${qname} 'chmod 600 /home/ubuntu/nemoclaw/.env'`,
);
} finally {
try {
fs.unlinkSync(envTmp);
} catch {
/* ignored */
}
try {
fs.rmdirSync(envDir);
} catch {
/* ignored */
}
}

console.log(" Running setup...");
runInteractive(
`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${qname} 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && bash scripts/brev-setup.sh'`,
);

if (tgToken) {
console.log(" Starting services...");
run(
`ssh -o StrictHostKeyChecking=no -o LogLevel=ERROR ${qname} 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && bash scripts/start-services.sh'`,
);
}

console.log("");
console.log(" Connecting to sandbox...");
console.log("");
runInteractive(
`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${qname} 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && openshell sandbox connect nemoclaw'`,
);
async function deploy(instanceName) {
await executeDeploy({
instanceName,
env: process.env,
rootDir: ROOT,
getCredential,
validateName,
shellQuote,
run,
runInteractive,
execFileSync: (file, args, opts = {}) =>
String(execFileSync(file, args, { encoding: "utf-8", ...opts })),
spawnSync,
log: console.log,
error: console.error,
stdoutWrite: (message) => process.stdout.write(message),
exit: (code) => process.exit(code),
});
}

async function start() {
Expand Down Expand Up @@ -1198,7 +1087,6 @@ function help() {
${G}Getting Started:${R}
${B}nemoclaw onboard${R} Configure inference endpoint and credentials
${D}(non-interactive: ${NOTICE_ACCEPT_FLAG} or ${NOTICE_ACCEPT_ENV}=1)${R}
nemoclaw setup-spark Set up on DGX Spark ${D}(fixes cgroup v2 + Docker)${R}

${G}Sandbox Management:${R}
${B}nemoclaw list${R} List all sandboxes
Expand All @@ -1211,8 +1099,10 @@ function help() {
nemoclaw <name> policy-add Add a network or filesystem policy preset
nemoclaw <name> policy-list List presets ${D}(● = applied)${R}

${G}Deploy:${R}
nemoclaw deploy <instance> Deploy to a Brev VM and start services
${G}Compatibility Commands:${R}
nemoclaw setup Deprecated alias for ${B}nemoclaw onboard${R}
nemoclaw setup-spark Deprecated alias for ${B}nemoclaw onboard${R}
nemoclaw deploy <instance> Deprecated Brev-specific bootstrap path

${G}Services:${R}
nemoclaw start Start auxiliary services ${D}(Telegram, tunnel)${R}
Expand Down Expand Up @@ -1259,7 +1149,7 @@ const [cmd, ...args] = process.argv.slice(2);
await setup(args);
break;
case "setup-spark":
await setupSpark();
await setupSpark(args);
break;
case "deploy":
await deploy(args[0]);
Expand Down
Loading