Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 @@ -248,6 +248,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
}
89 changes: 89 additions & 0 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ 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 { loadResourceProfiles, appendResourceFlags } = require("./resources-cmd");

import type { AgentDefinition } from "./agent/defs";
import type { CurlProbeResult } from "./http-probe";
Expand Down Expand Up @@ -4783,6 +4784,7 @@ async function createSandbox(
agent: AgentDefinition | null = null,
controlUiPort: number | null = null,
gpuPassthrough: boolean = false,
resourceProfile: { cpu_request: string; cpu_limit: string; memory_request: string; memory_limit: string } | null = null,
) {
step(6, 8, "Creating sandbox");

Expand Down Expand Up @@ -5392,6 +5394,14 @@ async function createSandbox(
createArgs.push("--gpu");
}

// 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 @@ -10204,6 +10214,84 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
console.error(" Inference selection is incomplete; cannot create sandbox.");
process.exit(1);
}

// ── Resource profile selection ─────────────────────────────
const availableProfiles = loadResourceProfiles();
const profileNames = Object.keys(availableProfiles);
let selectedProfile: { cpu_request: string; cpu_limit: string; memory_request: string; memory_limit: string } | null = null;

const hasResourceEnvOverrides = !!(
process.env.NEMOCLAW_CPU_REQUEST || process.env.NEMOCLAW_CPU_LIMIT ||
process.env.NEMOCLAW_RAM_REQUEST || process.env.NEMOCLAW_RAM_LIMIT
);
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(", ")}`);
process.exit(1);
}
} else if (profileNames.length > 0 && !isNonInteractive() && !hasResourceEnvOverrides) {
const { getHardwareResources, resolveResourceValue } = require("./resources-cmd");
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 idx = parseInt(choice.trim(), 10) - 1;
if (idx >= 0 && idx < profileNames.length) {
selectedProfile = availableProfiles[profileNames[idx]];
console.log(` Using profile: ${profileNames[idx]}`);
} else if (idx === profileNames.length) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
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 {
const cpuTotal = hw.cpu.cores;
const memTotal = hw.memory.totalMB;
const resolvedCpu = resolveResourceValue(cpuLim, cpuTotal, "cpu");
const resolvedRam = resolveResourceValue(ramLim, memTotal, "memory");
console.log(` Resolved: CPU limit=${resolvedCpu} cores, RAM limit=${resolvedRam}`);
} catch (e: unknown) {
console.error(` ${(e as Error).message}`);
process.exit(1);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
}
}

if (process.env.NEMOCLAW_CPU_REQUEST || process.env.NEMOCLAW_CPU_LIMIT ||
process.env.NEMOCLAW_RAM_REQUEST || process.env.NEMOCLAW_RAM_LIMIT) {
selectedProfile = selectedProfile || { cpu_request: "", cpu_limit: "", memory_request: "", memory_limit: "" };
if (process.env.NEMOCLAW_CPU_REQUEST) selectedProfile.cpu_request = process.env.NEMOCLAW_CPU_REQUEST;
if (process.env.NEMOCLAW_CPU_LIMIT) selectedProfile.cpu_limit = process.env.NEMOCLAW_CPU_LIMIT;
if (process.env.NEMOCLAW_RAM_REQUEST) selectedProfile.memory_request = process.env.NEMOCLAW_RAM_REQUEST;
if (process.env.NEMOCLAW_RAM_LIMIT) selectedProfile.memory_limit = process.env.NEMOCLAW_RAM_LIMIT;
note(` Resource overrides (env): cpu=${selectedProfile.cpu_request}/${selectedProfile.cpu_limit}, ram=${selectedProfile.memory_request}/${selectedProfile.memory_limit}`);
}

sandboxName = await createSandbox(
gpu,
model,
Expand All @@ -10216,6 +10304,7 @@ async function onboard(opts: OnboardOptions = {}): Promise<void> {
agent,
opts.controlUiPort || null,
gpuPassthrough,
selectedProfile,
);
webSearchConfig = nextWebSearchConfig;
// Persist model and provider after the sandbox entry exists in the registry.
Expand Down
Loading
Loading