diff --git a/nemoclaw/docs/workspace/backup-restore.md b/nemoclaw/docs/workspace/backup-restore.md new file mode 100644 index 00000000000..8940fe848e2 --- /dev/null +++ b/nemoclaw/docs/workspace/backup-restore.md @@ -0,0 +1,144 @@ +--- +title: + page: "Back Up and Restore Workspace Files" + nav: "Back Up & Restore" +description: "How to back up and restore OpenClaw workspace files before destructive operations." +keywords: ["nemoclaw backup", "nemoclaw restore", "workspace backup", "openshell sandbox download upload"] +topics: ["generative_ai", "ai_agents"] +tags: ["openclaw", "openshell", "sandboxing", "workspace", "backup", "nemoclaw"] +content: + type: how_to + difficulty: technical_beginner + audience: ["developer", "engineer"] +status: published +--- + + + +# Back Up and Restore Workspace Files + +Workspace files define your agent's personality, memory, and user context. +They persist across sandbox restarts but are **permanently deleted** when you run `nemoclaw destroy`. + +This guide covers manual backup with CLI commands and an automated script. + +## Prerequisites + +- A running NemoClaw sandbox (for backup) or a freshly created sandbox (for restore). +- The OpenShell CLI on your `PATH`. +- The sandbox name (shown by `nemoclaw list`). + +## When to Back Up + +- Before running `nemoclaw destroy`. +- Before major NemoClaw version upgrades. +- Periodically, if you have invested time customizing your agent. + +## Manual Backup + +Use `openshell sandbox download` to copy files from the sandbox to your host. + +```console +$ SANDBOX=my-assistant +$ BACKUP_DIR=~/.nemoclaw/backups/$(date +%Y%m%d-%H%M%S) +$ mkdir -p "$BACKUP_DIR" + +$ openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/SOUL.md "$BACKUP_DIR/" +$ openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/USER.md "$BACKUP_DIR/" +$ openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/IDENTITY.md "$BACKUP_DIR/" +$ openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/AGENTS.md "$BACKUP_DIR/" +$ openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/MEMORY.md "$BACKUP_DIR/" +$ openshell sandbox download "$SANDBOX" /sandbox/.openclaw/workspace/memory/ "$BACKUP_DIR/memory/" +``` + +## Manual Restore + +Use `openshell sandbox upload` to push files back into a sandbox. + +```console +$ SANDBOX=my-assistant +$ BACKUP_DIR=~/.nemoclaw/backups/20260320-120000 # pick a timestamp + +$ openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/SOUL.md" /sandbox/.openclaw/workspace/ +$ openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/USER.md" /sandbox/.openclaw/workspace/ +$ openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/IDENTITY.md" /sandbox/.openclaw/workspace/ +$ openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/AGENTS.md" /sandbox/.openclaw/workspace/ +$ openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/MEMORY.md" /sandbox/.openclaw/workspace/ +$ openshell sandbox upload "$SANDBOX" "$BACKUP_DIR/memory/" /sandbox/.openclaw/workspace/memory/ +``` + +## Using the Backup Script + +The repository includes a convenience script at `scripts/backup-workspace.sh`. + +### Backup + +```console +$ ./scripts/backup-workspace.sh backup my-assistant +Backing up workspace from sandbox 'my-assistant'... +Backup saved to /home/user/.nemoclaw/backups/20260320-120000/ (6 items) +``` + +### Restore + +Restore from the most recent backup: + +```console +$ ./scripts/backup-workspace.sh restore my-assistant +``` + +Restore from a specific timestamp: + +```console +$ ./scripts/backup-workspace.sh restore my-assistant 20260320-120000 +``` + +## Upgrade workflow (image rebuild / onboard) + +Before you change the sandbox image (for example after editing `Dockerfile`) or run `nemoclaw onboard`, back up. Optional files such as `MEMORY.md` or `memory/` may not exist yet; the backup script skips them with a warning. + +Use the upgrade helper to run workspace backup, optionally snapshot all of `/sandbox/.openclaw-data/`, and optionally run `nemoclaw onboard`: + +```console +$ ./scripts/upgrade-sandbox.sh my-assistant +$ ./scripts/upgrade-sandbox.sh --full-data my-assistant +$ ./scripts/upgrade-sandbox.sh --run-onboard --yes my-assistant +``` + +- `--full-data` downloads the full OpenClaw state tree (larger; includes sessions and agents beyond the Markdown workspace). +- `--run-onboard` runs `nemoclaw onboard` from the repo root (or `NEMOCLAW_REPO_ROOT`). After a successful onboard, the script **automatically** runs `backup-workspace.sh restore` for the backup timestamp created at the start (so you do not need to run restore by hand). Use `--no-restore` to skip that step if you want a fresh workspace after the rebuild. +- Without `--run-onboard`, the script only backs up and prints suggested next commands. + +See `scripts/upgrade-sandbox.sh --help` for options and environment variables. + +## Verifying a Backup + +List backed-up files to confirm completeness: + +```console +$ ls ~/.nemoclaw/backups/20260320-120000/ +AGENTS.md +IDENTITY.md +MEMORY.md +SOUL.md +USER.md +memory/ +``` + +## Inspecting Files Inside the Sandbox + +Connect to the sandbox to list or view workspace files directly: + +```console +$ openshell sandbox connect my-assistant +$ ls -la /sandbox/.openclaw/workspace/ +``` + +## Next Steps + +- [Workspace Files overview](workspace-files.md) — learn what each file does +- [Commands reference](../reference/commands.md) +- [Monitor Sandbox Activity](../monitoring/monitor-sandbox-activity.md) diff --git a/nemoclaw/scripts/backup-workspace.sh b/nemoclaw/scripts/backup-workspace.sh new file mode 100755 index 00000000000..0c5e1ecdd77 --- /dev/null +++ b/nemoclaw/scripts/backup-workspace.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +WORKSPACE_PATH="/sandbox/.openclaw/workspace" +BACKUP_BASE="${HOME}/.nemoclaw/backups" + +# Core persona files — expected once the agent has initialized workspace. +FILES_CORE=(SOUL.md USER.md IDENTITY.md AGENTS.md) +# Long-term memory — created later; missing paths are normal (no noisy tar errors). +FILES_OPTIONAL=(MEMORY.md) +DIRS_OPTIONAL=(memory) + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +DIM='\033[0;90m' +NC='\033[0m' + +info() { echo -e "${GREEN}[backup]${NC} $1"; } +warn() { echo -e "${YELLOW}[backup]${NC} $1"; } +fail() { + echo -e "${RED}[backup]${NC} $1" >&2 + exit 1 +} + +usage() { + cat < + $(basename "$0") restore [timestamp] + +Commands: + backup Download workspace files from a sandbox to a timestamped local backup. + restore Upload workspace files from a local backup into a sandbox. + If no timestamp is given, the most recent backup is used. + +Backup location: ${BACKUP_BASE}// +EOF + exit 1 +} + +# Exit codes: 0 = success, 2 = not found (quiet mode only), 1 = fatal error. +sandbox_download() { + local sandbox="$1" remote="$2" dest="$3" + local quiet="${4:-0}" + local rc=0 + if [[ "$quiet" == "1" ]]; then + openshell sandbox download "$sandbox" "$remote" "$dest" 2>/dev/null || rc=$? + else + openshell sandbox download "$sandbox" "$remote" "$dest" || rc=$? + fi + if [[ "$rc" -ne 0 && "$quiet" == "1" ]]; then + return 2 # treat as not-found when quiet + fi + return "$rc" +} + +do_backup() { + local sandbox="$1" + local ts + ts="$(date +%Y%m%d-%H%M%S)" + local dest="${BACKUP_BASE}/${ts}" + + mkdir -p "$BACKUP_BASE" + chmod 0700 "${HOME}/.nemoclaw" "$BACKUP_BASE" \ + || fail "Failed to set secure permissions on ${HOME}/.nemoclaw — check directory ownership." + mkdir -p "$dest" + chmod 0700 "$dest" + # Write manifest so restores can verify sandbox origin. + echo "$sandbox" >"${dest}/.sandbox-name" + + info "Backing up workspace from sandbox '${sandbox}'..." + + local count=0 + local f d + + for f in "${FILES_CORE[@]}"; do + local dl_rc=0 + sandbox_download "$sandbox" "${WORKSPACE_PATH}/${f}" "${dest}/" 0 || dl_rc=$? + if [[ "$dl_rc" -eq 0 ]]; then + count=$((count + 1)) + else + fail "Failed to download core file ${f} (exit code ${dl_rc}) — aborting backup." + fi + done + + for f in "${FILES_OPTIONAL[@]}"; do + local dl_rc=0 + sandbox_download "$sandbox" "${WORKSPACE_PATH}/${f}" "${dest}/" 1 || dl_rc=$? + if [[ "$dl_rc" -eq 0 ]]; then + count=$((count + 1)) + elif [[ "$dl_rc" -eq 2 ]]; then + echo -e "${DIM}[backup]${NC} Optional ${f} not in sandbox — skipped (normal until created)." + else + fail "Fatal error downloading optional file ${f} (exit code ${dl_rc})." + fi + done + + for d in "${DIRS_OPTIONAL[@]}"; do + local dl_rc=0 + sandbox_download "$sandbox" "${WORKSPACE_PATH}/${d}/" "${dest}/${d}/" 1 || dl_rc=$? + if [[ "$dl_rc" -eq 0 ]]; then + count=$((count + 1)) + elif [[ "$dl_rc" -eq 2 ]]; then + echo -e "${DIM}[backup]${NC} Optional ${d}/ not in sandbox — skipped (normal until created)." + else + fail "Fatal error downloading optional directory ${d}/ (exit code ${dl_rc})." + fi + done + + if [ "$count" -eq 0 ]; then + fail "No files were backed up. Check that the sandbox '${sandbox}' exists and has workspace files." + fi + + # Machine-readable output for callers (e.g., upgrade-sandbox.sh). + echo "BACKUP_TS=${ts}" + info "Backup saved to ${dest}/ (${count} items)" +} + +# Latest backup directory name (mtime); portable (no find -printf). +latest_backup_timestamp() { + if [ ! -d "$BACKUP_BASE" ]; then + return 1 + fi + # shellcheck disable=SC2012 # backup dirs are YYYYMMDD-HHMMSS — no special chars + ls -1t "$BACKUP_BASE" 2>/dev/null | head -n1 +} + +do_restore() { + local sandbox="$1" + local ts="${2:-}" + + if [ -z "$ts" ]; then + ts="$(latest_backup_timestamp || true)" + [ -n "$ts" ] || fail "No backups found in ${BACKUP_BASE}/" + info "Using most recent backup: ${ts}" + fi + + local src="${BACKUP_BASE}/${ts}" + [ -d "$src" ] || fail "Backup directory not found: ${src}" + # Verify backup was created for this sandbox (or allow override). + if [ -f "${src}/.sandbox-name" ]; then + local origin + origin="$(cat "${src}/.sandbox-name")" + if [ "$origin" != "$sandbox" ]; then + fail "Backup in ${src}/ was created for sandbox '${origin}', not '${sandbox}'. Use a different timestamp or back up the correct sandbox." + fi + fi + + info "Restoring workspace to sandbox '${sandbox}' from ${src}..." + + local count=0 + local f d + + for f in "${FILES_CORE[@]}"; do + if [ -f "${src}/${f}" ]; then + if openshell sandbox upload "$sandbox" "${src}/${f}" "${WORKSPACE_PATH}/"; then + count=$((count + 1)) + else + fail "Failed to restore core file ${f} — aborting to prevent incomplete workspace." + fi + fi + done + + for f in "${FILES_OPTIONAL[@]}"; do + if [ -f "${src}/${f}" ]; then + if openshell sandbox upload "$sandbox" "${src}/${f}" "${WORKSPACE_PATH}/"; then + count=$((count + 1)) + else + warn "Failed to restore ${f}" + fi + fi + done + + for d in "${DIRS_OPTIONAL[@]}"; do + if [ -d "${src}/${d}" ]; then + if openshell sandbox upload "$sandbox" "${src}/${d}/" "${WORKSPACE_PATH}/${d}/"; then + count=$((count + 1)) + else + warn "Failed to restore ${d}/" + fi + fi + done + + if [ "$count" -eq 0 ]; then + fail "No files were restored. Check that the sandbox '${sandbox}' is running." + fi + + info "Restored ${count} items to sandbox '${sandbox}'." +} + +# --- Main --- + +[ $# -ge 2 ] || usage +command -v openshell >/dev/null 2>&1 || fail "'openshell' is required but not found in PATH." + +action="$1" +sandbox="$2" +shift 2 + +case "$action" in + backup) do_backup "$sandbox" ;; + restore) do_restore "$sandbox" "$@" ;; + *) usage ;; +esac diff --git a/nemoclaw/scripts/upgrade-sandbox.sh b/nemoclaw/scripts/upgrade-sandbox.sh new file mode 100755 index 00000000000..b4a33996c63 --- /dev/null +++ b/nemoclaw/scripts/upgrade-sandbox.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Safe upgrade helper: back up workspace (and optionally all OpenClaw state), then +# optionally run `nemoclaw onboard` to rebuild the sandbox image from the repo. +# +# Does not destroy the sandbox by itself — `nemoclaw onboard` behavior depends on +# your OpenShell/NemoClaw version (may recreate the sandbox). Always have backups. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +BACKUP_BASE="${HOME}/.nemoclaw/backups" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${GREEN}[upgrade]${NC} $1"; } +warn() { echo -e "${YELLOW}[upgrade]${NC} $1"; } +fail() { + echo -e "${RED}[upgrade]${NC} $1" >&2 + exit 1 +} + +usage() { + cat < + +Backs up workspace files via scripts/backup-workspace.sh, then optionally snapshots +/sandbox/.openclaw-data/ and/or runs nemoclaw onboard from the NemoClaw repo. + +Options: + --full-data Also download /sandbox/.openclaw-data/ (agents, sessions, plugins; + can be large). Stored under the same timestamp as workspace backup. + --run-onboard After backups, run: nemoclaw onboard (from NEMOCLAW_REPO_ROOT or ${REPO_ROOT}) + --yes With --run-onboard, skip the confirmation prompt + --no-restore With --run-onboard, do not run backup-workspace.sh restore after onboard + -h, --help Show this help + +Environment: + NEMOCLAW_REPO_ROOT Directory containing install.sh and Dockerfile (default: parent of scripts/) + NEMOCLAW_NON_INTERACTIVE Defaults to 1 for --run-onboard (no prompts). Set to 0 for interactive onboard. + NEMOCLAW_RECREATE_SANDBOX Defaults to 1 with --run-onboard so the named sandbox is rebuilt. Set to 0 to fail if it already exists. + +Example: + $(basename "$0") --full-data my-assistant + $(basename "$0") --run-onboard --yes my-assistant + $(basename "$0") --run-onboard --yes --no-restore my-assistant # keep fresh workspace after rebuild +EOF + exit "${1:-0}" +} + +command -v openshell >/dev/null 2>&1 || fail "'openshell' is required but not found in PATH." + +FULL_DATA=0 +RUN_ONBOARD=0 +YES=0 +NO_RESTORE=0 +SANDBOX="" + +while [ $# -gt 0 ]; do + case "$1" in + --full-data) FULL_DATA=1 ;; + --run-onboard) RUN_ONBOARD=1 ;; + --yes) YES=1 ;; + --no-restore) NO_RESTORE=1 ;; + -h | --help) usage 0 ;; + -*) + fail "Unknown option: $1 (try --help)" + ;; + *) + [ -z "$SANDBOX" ] || fail "Unexpected extra argument: $1" + SANDBOX="$1" + ;; + esac + shift +done + +[ -n "$SANDBOX" ] || usage 1 + +BACKUP_WORKSPACE="${REPO_ROOT}/scripts/backup-workspace.sh" +[ -x "$BACKUP_WORKSPACE" ] || [ -f "$BACKUP_WORKSPACE" ] || fail "Missing ${BACKUP_WORKSPACE}" + +info "Sandbox: ${SANDBOX}" +info "Step 1/3: Workspace backup (SOUL, USER, identity, etc.)..." +BACKUP_OUTPUT="$(bash "$BACKUP_WORKSPACE" backup "$SANDBOX")" +echo "$BACKUP_OUTPUT" # preserve original output for the user + +TS="$(echo "$BACKUP_OUTPUT" | grep '^BACKUP_TS=' | tail -1 | cut -d= -f2)" +[ -n "$TS" ] || fail "Could not determine backup timestamp from backup output." +DEST="${BACKUP_BASE}/${TS}" +info "Backup directory: ${DEST}/" + +if [ "$FULL_DATA" -eq 1 ]; then + info "Step 2/3: Full OpenClaw snapshot (/sandbox/.openclaw-data/)..." + if openshell sandbox download "$SANDBOX" /sandbox/.openclaw-data/ "${DEST}/openclaw-data/"; then + info "Saved openclaw-data under ${DEST}/openclaw-data/" + else + fail "--full-data was requested but openclaw-data download failed. Workspace backup in ${DEST}/ is still valid." + fi +else + info "Step 2/3: Skipped full openclaw-data (--full-data not set)." +fi + +info "Step 3/3: Onboard / image rebuild" +if [ "$RUN_ONBOARD" -eq 1 ]; then + ROOT="${NEMOCLAW_REPO_ROOT:-$REPO_ROOT}" + [ -f "${ROOT}/install.sh" ] || fail "NEMOCLAW_REPO_ROOT must contain install.sh (got ${ROOT})" + command -v nemoclaw >/dev/null 2>&1 || fail "'nemoclaw' not found in PATH; install the CLI or omit --run-onboard" + if [ "$YES" -ne 1 ]; then + echo -e "${CYAN}This will run ${ROOT}/install.sh flow via \`nemoclaw onboard\`, which may recreate the sandbox.${NC}" + read -r -p "Continue? [y/N] " reply + case "$reply" in + y | Y | yes | YES) ;; + *) + info "Aborted. Restore later with: ./scripts/backup-workspace.sh restore ${SANDBOX} ${TS}" + exit 0 + ;; + esac + fi + # Non-interactive onboard: same sandbox name as CLI, no inference prompts (uses registry / env / defaults). + export NEMOCLAW_NON_INTERACTIVE="${NEMOCLAW_NON_INTERACTIVE:-1}" + export NEMOCLAW_SANDBOX_NAME="$SANDBOX" + export NEMOCLAW_RECREATE_SANDBOX="${NEMOCLAW_RECREATE_SANDBOX:-1}" + ONBOARD_RC=0 + (cd "$ROOT" && nemoclaw onboard) || ONBOARD_RC=$? + if [ "$ONBOARD_RC" -ne 0 ]; then + warn "nemoclaw onboard exited with code ${ONBOARD_RC}." + info "Your workspace backup is safe at: ${DEST}/" + info "To restore manually: ./scripts/backup-workspace.sh restore ${SANDBOX} ${TS}" + exit "$ONBOARD_RC" + fi + if [ "$NO_RESTORE" -eq 1 ]; then + info "Skipping automatic workspace restore (--no-restore)." + info "To restore manually: ./scripts/backup-workspace.sh restore ${SANDBOX} ${TS}" + else + info "Restoring workspace from backup ${TS}..." + bash "$BACKUP_WORKSPACE" restore "$SANDBOX" "$TS" + fi +else + echo "" + info "Backups complete. To rebuild the image and re-run setup (pick up Dockerfile changes):" + echo -e " ${CYAN}cd ${REPO_ROOT}${NC}" + echo -e " ${CYAN}nemoclaw onboard${NC}" + echo "" + warn "If onboard recreates the sandbox, restore workspace with:" + echo -e " ${CYAN}./scripts/backup-workspace.sh restore ${SANDBOX} ${TS}${NC}" + if [ "$FULL_DATA" -eq 1 ] && [ -d "${DEST}/openclaw-data" ]; then + warn "For a full state restore, re-upload ${DEST}/openclaw-data/ with openshell sandbox upload (see docs)." + fi +fi + +info "Done." diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 8b7f66a91f2..9c5c8e1fdcf 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -1743,6 +1743,63 @@ function getNonInteractiveModel(providerKey) { return model; } +/** Map openshell provider name (stored in registry) to NEMOCLAW_PROVIDER / REMOTE_PROVIDER_CONFIG key. */ +function inferRemoteProviderKeyFromStoredName(storedName) { + if (!storedName) return null; + const map = { + "nvidia-prod": "build", + "nvidia-nim": "build", + "openai-api": "openai", + "anthropic-prod": "anthropic", + "compatible-endpoint": "custom", + "compatible-anthropic-endpoint": "anthropicCompatible", + "gemini-api": "gemini", + "ollama-local": "ollama", + "vllm-local": "vllm", + }; + return map[storedName] || null; +} + +/** + * When NEMOCLAW_SANDBOX_NAME is set and provider/model env vars are unset, fill them from + * ~/.nemoclaw/sandboxes.json so upgrade / CI can skip inference prompts. + * Falls back to the gateway key "nemoclaw" for legacy registry entries. + */ +function hydrateNonInteractiveInferenceFromRegistry() { + if (!isNonInteractive()) return; + const name = (process.env.NEMOCLAW_SANDBOX_NAME || "").trim(); + if (!name) return; + let entry = registry.getSandbox(name); + if (!entry || !entry.model) { + entry = registry.getSandbox(GATEWAY_NAME); + } + if (!entry || !entry.model) return; + + const providerKey = (process.env.NEMOCLAW_PROVIDER || "").trim(); + const modelEnv = (process.env.NEMOCLAW_MODEL || "").trim(); + if (!providerKey && entry.provider) { + let inferred = inferRemoteProviderKeyFromStoredName(entry.provider); + if (entry.provider === "vllm-local" && entry.nimContainer) { + inferred = "nim-local"; + } else if (entry.provider === "vllm-local" && !entry.nimContainer) { + inferred = "vllm"; + } + if (inferred) { + process.env.NEMOCLAW_PROVIDER = inferred; + note(` [non-interactive] NEMOCLAW_PROVIDER (from registry) -> ${inferred}`); + } + } + if (!modelEnv && entry.model) { + process.env.NEMOCLAW_MODEL = entry.model; + note(` [non-interactive] NEMOCLAW_MODEL (from registry) -> ${entry.model}`); + } + // CodeRabbit fix: hydrate endpoint URL for compatible endpoints + if (!process.env.NEMOCLAW_ENDPOINT_URL && entry.endpointUrl) { + process.env.NEMOCLAW_ENDPOINT_URL = entry.endpointUrl; + note(` [non-interactive] NEMOCLAW_ENDPOINT_URL (from registry) -> ${entry.endpointUrl}`); + } +} + // ── Step 1: Preflight ──────────────────────────────────────────── // eslint-disable-next-line complexity @@ -2737,6 +2794,7 @@ async function createSandbox( // eslint-disable-next-line complexity async function setupNim(gpu) { step(3, 8, "Configuring inference (NIM)"); + hydrateNonInteractiveInferenceFromRegistry(); let model = null; let provider = REMOTE_PROVIDER_CONFIG.build.providerName; @@ -2879,13 +2937,18 @@ async function setupNim(gpu) { if (selected.key === "build") { if (isNonInteractive()) { - if (!process.env.NVIDIA_API_KEY) { + const key = getCredential("NVIDIA_API_KEY") || process.env.NVIDIA_API_KEY; + if (!key) { console.error( " NVIDIA_API_KEY is required for NVIDIA Endpoints in non-interactive mode.", ); + console.error( + " Check ~/.nemoclaw/credentials.json or export NVIDIA_API_KEY.", + ); process.exit(1); } - const keyError = validateNvidiaApiKeyValue(process.env.NVIDIA_API_KEY); + process.env.NVIDIA_API_KEY = key; + const keyError = validateNvidiaApiKeyValue(key); if (keyError) { console.error(keyError); console.error(` Get a key from ${REMOTE_PROVIDER_CONFIG.build.helpUrl}`); @@ -2905,12 +2968,17 @@ async function setupNim(gpu) { } } else { if (isNonInteractive()) { - if (!process.env[credentialEnv]) { + const key = getCredential(credentialEnv); + if (!key) { console.error( ` ${credentialEnv} is required for ${remoteConfig.label} in non-interactive mode.`, ); + console.error( + " Check ~/.nemoclaw/credentials.json or export the variable.", + ); process.exit(1); } + process.env[credentialEnv] = key; } else { await ensureNamedCredential( credentialEnv, @@ -3483,7 +3551,6 @@ async function setupInference( } verifyInferenceRoute(provider, model); - registry.updateSandbox(sandboxName, { model, provider }); console.log(` ✓ Inference route set: ${provider} / ${model}`); return { ok: true }; } @@ -4561,8 +4628,19 @@ async function onboard(opts = {}) { isInferenceRouteReady(provider, model); if (resumeInference) { skippedStepMessage("inference", `${provider} / ${model}`); - if (nimContainer) { - registry.updateSandbox(sandboxName, { nimContainer }); + const resumeUpdate = {}; + if (nimContainer) resumeUpdate.nimContainer = nimContainer; + if (model) resumeUpdate.model = model; + if (provider) resumeUpdate.provider = provider; + if ( + endpointUrl && + (provider === "compatible-endpoint" || provider === "compatible-anthropic-endpoint") + ) { + resumeUpdate.endpointUrl = endpointUrl; + } + if (Object.keys(resumeUpdate).length > 0) { + const updateKey = sandboxName || GATEWAY_NAME; + registry.updateSandbox(updateKey, resumeUpdate); } onboardSession.markStepComplete("inference", { sandboxName, @@ -4586,9 +4664,17 @@ async function onboard(opts = {}) { forceProviderSelection = true; continue; } + const sandboxUpdate = { model, provider }; if (nimContainer) { - registry.updateSandbox(sandboxName, { nimContainer }); + sandboxUpdate.nimContainer = nimContainer; + } + if ( + endpointUrl && + (provider === "compatible-endpoint" || provider === "compatible-anthropic-endpoint") + ) { + sandboxUpdate.endpointUrl = endpointUrl; } + registry.updateSandbox(sandboxName, sandboxUpdate); onboardSession.markStepComplete("inference", { sandboxName, provider, model, nimContainer }); break; } diff --git a/src/lib/registry.ts b/src/lib/registry.ts index 4969a969795..2dd45c4a7a3 100644 --- a/src/lib/registry.ts +++ b/src/lib/registry.ts @@ -12,6 +12,7 @@ export interface SandboxEntry { model?: string | null; nimContainer?: string | null; provider?: string | null; + endpointUrl?: string | null; gpuEnabled?: boolean; policies?: string[]; agent?: string | null;