Skip to content
Open
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
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 @@ -1849,20 +1850,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 @@ -2025,24 +2033,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 @@ -807,139 +802,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 @@ -1374,7 +1263,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 @@ -1387,8 +1275,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 @@ -1435,7 +1325,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
Loading