Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
17 changes: 17 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ Print the installed NemoClaw CLI version.
$ nemoclaw --version
```

### `nemoclaw resources`

Display host hardware inventory and configured sandbox resource profiles.
Use `--json` for machine-readable CPU, memory, GPU, Kubernetes allocatable-capacity, and profile data.

```console
$ nemoclaw resources [--json]
```

`nemoclaw resources` reads `NEMOCLAW_GATEWAY_CONTAINER` only as an advanced override for the OpenShell gateway container name used when querying Kubernetes allocatable capacity.
If the gateway is not running, Kubernetes allocatable fields are omitted and host CPU/RAM totals are still shown.

### `nemoclaw onboard`

Run the interactive setup wizard (recommended for new installs).
Expand Down Expand Up @@ -1142,6 +1154,11 @@ These flags toggle optional behaviors during onboarding; set them before running
| `NEMOCLAW_OVERLAY_SNAPSHOTTER` | snapshotter name | Selects the containerd overlay snapshotter for sandbox builds. Empty (default) preserves containerd's choice. |
| `NEMOCLAW_SKIP_TELEGRAM_REACHABILITY` | `1` to enable | Skips the Telegram bot reachability probe during onboard (useful in restricted networks). |
| `NEMOCLAW_CONFIG_ACCEPT_NEW_PATH` | `1` to enable | Accepts a new sandbox config path without an interactive prompt when the stored path differs from the discovered one. |
| `NEMOCLAW_RESOURCE_PROFILE` | profile name | Selects a sandbox CPU/RAM resource profile from the blueprint during onboarding. Unknown names fail fast. |
| `NEMOCLAW_CPU_REQUEST` | percentage or Kubernetes CPU quantity | Overrides the selected profile's CPU request. Percentages resolve against detected capacity. |
| `NEMOCLAW_CPU_LIMIT` | percentage or Kubernetes CPU quantity | Overrides the selected profile's CPU limit. Percentages resolve against detected capacity. |
| `NEMOCLAW_RAM_REQUEST` | percentage or Kubernetes memory quantity | Overrides the selected profile's memory request. Percentages resolve against detected capacity. |
| `NEMOCLAW_RAM_LIMIT` | percentage or Kubernetes memory quantity | Overrides the selected profile's memory limit. Percentages resolve against detected capacity. |
| `NEMOCLAW_SANDBOX_GPU` | `auto`, `1`, or `0` | Controls sandbox GPU passthrough during onboarding. `auto` enables GPU passthrough when an NVIDIA GPU is detected, `1` requires GPU passthrough, and `0` forces CPU-only sandbox creation. |
| `NEMOCLAW_SANDBOX_GPU_DEVICE` | OpenShell GPU device selector | Selects the GPU device passed with `openshell sandbox create --gpu-device`. Setting this value enables sandbox GPU passthrough unless `NEMOCLAW_SANDBOX_GPU=0` is also set, which is rejected. |
| `NEMOCLAW_DOCKER_GPU_PATCH` | `0` to disable, anything else to keep the default | Controls the Linux Docker-driver GPU sandbox compatibility patch. Set to `0` only as an escape hatch when the patch fails and you need onboarding to continue without patching the GPU sandbox container. |
Expand Down
21 changes: 21 additions & 0 deletions nemoclaw-blueprint/blueprint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ components:
name: "openclaw"
forward_ports:
- 18789
resource_profiles:
creator:
cpu_request: "25%"
cpu_limit: "50%"
memory_request: "25%"
memory_limit: "50%"
gamer:
cpu_request: "12%"
cpu_limit: "25%"
memory_request: "12%"
memory_limit: "25%"
game-developer:
cpu_request: "25%"
cpu_limit: "50%"
memory_request: "25%"
memory_limit: "50%"
developer:
cpu_request: "37%"
cpu_limit: "75%"
memory_request: "37%"
memory_limit: "75%"

inference:
profiles:
Expand Down
15 changes: 15 additions & 0 deletions schemas/blueprint.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@
"forward_ports": {
"type": "array",
"items": { "type": "integer", "minimum": 1, "maximum": 65535 }
},
"resource_profiles": {
"type": "object",
"description": "Named resource profiles for sandbox CPU/memory allocation. Values can be absolute Kubernetes quantities (e.g. '4', '8Gi') or percentages of detected hardware (e.g. '25%').",
"additionalProperties": {
"type": "object",
"required": ["cpu_request", "cpu_limit", "memory_request", "memory_limit"],
"properties": {
"cpu_request": { "type": "string", "minLength": 1, "pattern": "^([1-9]\\d?%|100%|\\d+(\\.\\d+)?[mKMGTPE]?i?)$" },
"cpu_limit": { "type": "string", "minLength": 1, "pattern": "^([1-9]\\d?%|100%|\\d+(\\.\\d+)?[mKMGTPE]?i?)$" },
"memory_request": { "type": "string", "minLength": 1, "pattern": "^([1-9]\\d?%|100%|\\d+(\\.\\d+)?[mKMGTPE]?i?)$" },
"memory_limit": { "type": "string", "minLength": 1, "pattern": "^([1-9]\\d?%|100%|\\d+(\\.\\d+)?[mKMGTPE]?i?)$" }
},
"additionalProperties": false
}
}
}
},
Expand Down
1 change: 1 addition & 0 deletions src/lib/cli/command-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type CommandGroup =
| "Credentials"
| "Backup"
| "Upgrade"
| "Resources"
| "Cleanup";

export interface CommandDisplayEntry {
Expand Down
1 change: 1 addition & 0 deletions src/lib/cli/command-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ describe("command-registry", () => {
"Credentials",
"Backup",
"Upgrade",
"Resources",
"Cleanup",
]);
});
Expand Down
1 change: 1 addition & 0 deletions src/lib/cli/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const GROUP_ORDER: readonly CommandGroup[] = [
"Credentials",
"Backup",
"Upgrade",
"Resources",
"Cleanup",
] as const;

Expand Down
1 change: 1 addition & 0 deletions src/lib/cli/oclif-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const GLOBAL_ROUTES: Readonly<Record<string, string>> = {
"backup-all": "backup-all",
"upgrade-sandboxes": "upgrade-sandboxes",
gc: "gc",
resources: "resources",
};

export function resolveGlobalOclifDispatch(cmd: string, args: string[]): DispatchResult {
Expand Down
44 changes: 44 additions & 0 deletions src/lib/commands/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import type { CommandDisplayEntry } from "../cli/command-display";

import { printHardwareResources } from "../resources-cmd";
import { NemoClawCommand } from "../cli/nemoclaw-oclif-command";

export default class ResourcesCommand extends NemoClawCommand {
static id = "resources";
static strict = true;
static enableJsonFlag = true;
static summary = "Show hardware inventory (CPU cores, RAM, GPU VRAM)";
static description =
"Display available hardware resources including CPU core count and model, " +
"total system RAM and swap, Kubernetes node allocatable capacity (when a " +
"gateway is running), and NVIDIA GPU name and VRAM. Supports --json for " +
"machine-readable output.";
static usage = ["resources [--json]"];
static examples = [
"<%= config.bin %> resources",
"<%= config.bin %> resources --json",
];
static flags = {};

static display: readonly CommandDisplayEntry[] = [
{
usage: "nemoclaw resources",
description: "Show hardware inventory (CPU cores, RAM, GPU VRAM)",
flags: "(--json)",
group: "Resources",
scope: "global",
order: 900,
},
];

public async run(): Promise<unknown> {
await this.parse(ResourcesCommand);
const result = printHardwareResources(this.jsonEnabled());
if (this.jsonEnabled()) {
return result;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
124 changes: 124 additions & 0 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ const providerModels: typeof import("./inference/provider-models") = require("./
const sandboxCreateStream: typeof import("./sandbox/create-stream") = require("./sandbox/create-stream");
const validationRecovery: typeof import("./validation-recovery") = require("./validation-recovery");
const webSearch: typeof import("./inference/web-search") = require("./inference/web-search");
const resourcesCmd: typeof import("./resources-cmd") = require("./resources-cmd");
const { appendResourceFlags, getHardwareResources, loadResourceProfiles, resolveResourceValue } = resourcesCmd;
const openshellInstallFlow: typeof import("./onboard/openshell-install") =
require("./onboard/openshell-install");
const openshellPinFlow: typeof import("./onboard/openshell-pin") =
Expand All @@ -321,6 +323,7 @@ import type { AgentDefinition } from "./agent/defs";
import type { CurlProbeResult } from "./adapters/http/probe";
import type { GatewayReuseState } from "./state/gateway";
import type { GatewayInference } from "./inference/config";
import type { ResourceProfile } from "./resources-cmd";
import type { GpuInfo, ValidationResult } from "./inference/local";
import {
hydrateMessagingChannelConfig,
Expand Down Expand Up @@ -5008,6 +5011,116 @@ function formatOnboardConfigSummary({
].join("\n");
}

function hasResourceEnvOverrides(): boolean {
return !!(
process.env.NEMOCLAW_CPU_REQUEST ||
process.env.NEMOCLAW_CPU_LIMIT ||
process.env.NEMOCLAW_RAM_REQUEST ||
process.env.NEMOCLAW_RAM_LIMIT
);
}

function applyResourceEnvOverrides(selectedProfile: ResourceProfile | null): ResourceProfile | null {
if (!hasResourceEnvOverrides()) return selectedProfile;
const nextProfile = selectedProfile || {
cpu_request: "",
cpu_limit: "",
memory_request: "",
memory_limit: "",
};
if (process.env.NEMOCLAW_CPU_REQUEST) nextProfile.cpu_request = process.env.NEMOCLAW_CPU_REQUEST;
if (process.env.NEMOCLAW_CPU_LIMIT) nextProfile.cpu_limit = process.env.NEMOCLAW_CPU_LIMIT;
if (process.env.NEMOCLAW_RAM_REQUEST) nextProfile.memory_request = process.env.NEMOCLAW_RAM_REQUEST;
if (process.env.NEMOCLAW_RAM_LIMIT) nextProfile.memory_limit = process.env.NEMOCLAW_RAM_LIMIT;
note(
` Resource overrides (env): cpu=${nextProfile.cpu_request}/${nextProfile.cpu_limit}, ram=${nextProfile.memory_request}/${nextProfile.memory_limit}`,
);
return nextProfile;
}

function exitWithResourceProfileError(message: string): never {
console.error(` ${message}`);
process.exit(1);
}

function printResolvedResourceProfile(profile: ResourceProfile, cpuTotal: number, memTotal: number): void {
const resolvedCpuRequest = resolveResourceValue(profile.cpu_request, cpuTotal, "cpu");
const resolvedCpuLimit = resolveResourceValue(profile.cpu_limit, cpuTotal, "cpu");
const resolvedMemoryRequest = resolveResourceValue(profile.memory_request, memTotal, "memory");
const resolvedMemoryLimit = resolveResourceValue(profile.memory_limit, memTotal, "memory");
console.log(
` Resolved: CPU request=${resolvedCpuRequest} cores, CPU limit=${resolvedCpuLimit} cores, RAM request=${resolvedMemoryRequest}, RAM limit=${resolvedMemoryLimit}`,
);
}

async function selectResourceProfileForSandbox(): Promise<ResourceProfile | null> {
const availableProfiles = loadResourceProfiles();
const profileNames = Object.keys(availableProfiles);
let selectedProfile: ResourceProfile | null = null;

if (process.env.NEMOCLAW_RESOURCE_PROFILE) {
const envProfile = process.env.NEMOCLAW_RESOURCE_PROFILE;
if (profileNames.length > 0 && availableProfiles[envProfile]) {
selectedProfile = { ...availableProfiles[envProfile] };
note(` Resource profile (env): ${envProfile}`);
} else {
console.error(` Unknown resource profile: '${envProfile}'`);
console.error(` Valid profiles: ${profileNames.join(", ")}`);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
process.exit(1);
}
} else if (profileNames.length > 0 && !isNonInteractive() && !hasResourceEnvOverrides()) {
const hw = getHardwareResources();
console.log("");
console.log(" Resource profiles:");
profileNames.forEach((name: string, i: number) => {
const p = availableProfiles[name];
console.log(
` ${i + 1}) ${name} (cpu=${p.cpu_request}/${p.cpu_limit}, ram=${p.memory_request}/${p.memory_limit})`,
);
});
console.log(` ${profileNames.length + 1}) custom (enter values manually)`);
console.log(` ${profileNames.length + 2}) No profile (default resources)`);
const choice = await promptOrDefault(
` Choose [${profileNames.length + 2}]: `,
null,
String(profileNames.length + 2),
);
const trimmedChoice = choice.trim();
const idx = Number.parseInt(trimmedChoice, 10) - 1;
if (!/^\d+$/.test(trimmedChoice) || idx < 0 || idx > profileNames.length + 1) {
exitWithResourceProfileError(
`Invalid resource profile selection '${choice}'. Choose a number from 1 to ${profileNames.length + 2}.`,
);
}
if (idx >= 0 && idx < profileNames.length) {
selectedProfile = availableProfiles[profileNames[idx]];
console.log(` Using profile: ${profileNames[idx]}`);
} else if (idx === profileNames.length) {
console.log("");
console.log(` Available: ${hw.cpu.cores} CPU cores, ${hw.memory.totalMB} MB RAM`);
console.log(" Enter values as percentages (e.g. 25%) or absolutes (e.g. 4, 8Gi)");
console.log("");
const cpuReq = (await prompt(` CPU min (request) [25%]: `)).trim() || "25%";
const cpuLim = (await prompt(` CPU max (limit) [50%]: `)).trim() || "50%";
const ramReq = (await prompt(` RAM min (request) [25%]: `)).trim() || "25%";
const ramLim = (await prompt(` RAM max (limit) [50%]: `)).trim() || "50%";
selectedProfile = {
cpu_request: cpuReq,
cpu_limit: cpuLim,
memory_request: ramReq,
memory_limit: ramLim,
};
try {
printResolvedResourceProfile(selectedProfile, hw.cpu.cores, hw.memory.totalMB);
} catch (e: unknown) {
exitWithResourceProfileError((e as Error).message);
}
}
}

return applyResourceEnvOverrides(selectedProfile);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

async function createSandbox(
gpu: ReturnType<typeof nim.detectGpu>,
model: string,
Expand All @@ -5020,6 +5133,7 @@ async function createSandbox(
agent: AgentDefinition | null = null,
controlUiPort: number | null = null,
sandboxGpuConfig: SandboxGpuConfig | null = null,
resourceProfile: ResourceProfile | null = null,
) {
step(6, 8, "Creating sandbox");

Expand Down Expand Up @@ -5662,6 +5776,14 @@ async function createSandbox(
}),
];

// Append CPU/memory resource flags from selected profile (graceful degradation).
if (resourceProfile) {
const applied = appendResourceFlags(createArgs, resourceProfile, getOpenshellBinary());
if (!applied) {
note(" OpenShell does not support resource flags — sandbox will use default limits.");
}
}

// Create OpenShell providers for messaging credentials so they flow through
// the provider/placeholder system instead of raw env vars. The L7 proxy
// rewrites Authorization headers (Bearer/Bot) and URL-path segments
Expand Down Expand Up @@ -10728,6 +10850,7 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
console.error(" Inference selection is incomplete; cannot create sandbox.");
process.exit(1);
}
const selectedProfile = await selectResourceProfileForSandbox();
if (fresh) {
stopStaleDashboardListenersForSandbox(registry.listSandboxes().sandboxes, sandboxName);
}
Expand All @@ -10743,6 +10866,7 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
agent,
opts.controlUiPort || null,
sandboxGpuConfig,
selectedProfile,
);
webSearchConfig = nextWebSearchConfig;
registry.updateSandbox(sandboxName, {
Expand Down
Loading
Loading