Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
172 changes: 172 additions & 0 deletions bin/lib/backup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Sandbox backup and restore functionality

const fs = require("fs");
const path = require("path");
const os = require("os");
const { execFileSync } = require("child_process");
const registry = require("./registry");

const BACKUP_DIR = path.join(process.env.HOME || "/tmp", ".nemoclaw", "backups");

function ensureBackupDir() {
fs.mkdirSync(BACKUP_DIR, { recursive: true, mode: 0o700 });
}

function listBackups() {
ensureBackupDir();
const files = fs.readdirSync(BACKUP_DIR).filter(f => f.endsWith(".json"));
const backups = [];

for (const file of files) {
try {
const content = JSON.parse(fs.readFileSync(path.join(BACKUP_DIR, file), "utf-8"));
backups.push({
name: content.metadata.name,
createdAt: content.metadata.createdAt,
path: path.join(BACKUP_DIR, file),
size: fs.statSync(path.join(BACKUP_DIR, file)).size,
});
} catch {}
}

return backups.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}

function exportSandbox(sandboxName, outputPath) {
const sandbox = registry.getSandbox(sandboxName);
if (!sandbox) {
console.error(` Sandbox not found: ${sandboxName}`);
return null;
}

ensureBackupDir();

const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const defaultName = `${sandboxName}-${timestamp}.json`;
const backupPath = outputPath || path.join(BACKUP_DIR, defaultName);

let policyContent = "";
try {
policyContent = execFileSync("openshell", ["policy", "get", sandboxName], {
encoding: "utf-8",
timeout: 10000,
stdio: ["pipe", "pipe", "pipe"]
});
} catch {
policyContent = "";
}

const backup = {
version: "1.0",
metadata: {
name: sandboxName,
createdAt: new Date().toISOString(),
nemoclawVersion: require("../../package.json").version,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
sandbox: {
name: sandbox.name,
model: sandbox.model,
provider: sandbox.provider,
gpuEnabled: sandbox.gpuEnabled,
policies: sandbox.policies || [],
},
policy: policyContent,
};

const dir = path.dirname(backupPath);
if (dir !== BACKUP_DIR) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}

fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2), { mode: 0o600 });
console.log(` Exported sandbox '${sandboxName}' to: ${backupPath}`);

return backupPath;
}

function importSandbox(backupPath, newName) {
if (!fs.existsSync(backupPath)) {
console.error(` Backup file not found: ${backupPath}`);
return false;
}

let backup;
try {
backup = JSON.parse(fs.readFileSync(backupPath, "utf-8"));
} catch {
console.error(` Invalid backup file: ${backupPath}`);
return false;
}

if (!backup.version || !backup.sandbox) {
console.error(" Invalid backup format");
return false;
}

const sandboxName = newName || backup.sandbox.name;

console.log(` Creating sandbox '${sandboxName}' from backup...`);

try {
execFileSync("openshell", ["sandbox", "exists", sandboxName], {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"]
});
console.error(` Sandbox '${sandboxName}' already exists. Use a different name or delete it first.`);
return false;
} catch {}

console.log(` Note: This only imports the registry config.`);
console.log(` You need to manually recreate the sandbox and apply policies.`);

registry.registerSandbox({
name: sandboxName,
model: backup.sandbox.model,
provider: backup.sandbox.provider,
gpuEnabled: backup.sandbox.gpuEnabled,
policies: backup.sandbox.policies || [],
});
Comment on lines +128 to +150

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate imported sandbox names before persisting them.

Line 128 accepts newName or backup.sandbox.name verbatim, but normal CLI flows already gate sandbox names through validateName(). A malformed backup can therefore register an entry that later nemoclaw <name> ... paths reject. Validate the chosen name before the openshell existence check and before registry.registerSandbox().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/backup.js` around lines 128 - 150, The code uses newName ||
backup.sandbox.name directly for sandboxName and registers it via
registry.registerSandbox without validating; call the existing
validateName(name) function on sandboxName (reject or normalize when invalid)
before running the openshell existence check and before calling
registry.registerSandbox, and handle validation failures by printing an error
and returning false so malformed backup names are not persisted.


if (backup.policy) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-restore-"));
const policyPath = path.join(tmpDir, "policy.yaml");
fs.writeFileSync(policyPath, backup.policy, { mode: 0o600 });
try {
execFileSync("openshell", ["policy", "set", "--policy", policyPath, "--wait", sandboxName], {
encoding: "utf-8",
timeout: 30000,
stdio: ["pipe", "pipe", "pipe"]
});
console.log(` Restored policy for '${sandboxName}'`);
} catch (err) {
console.warn(` Warning: Could not restore policy: ${err.message}`);
} finally {
fs.unlinkSync(policyPath);
fs.rmdirSync(tmpDir);
}
}

console.log(` Imported sandbox '${sandboxName}' from backup`);
return true;
}

function deleteBackup(backupPath) {
if (!fs.existsSync(backupPath)) {
console.error(` Backup not found: ${backupPath}`);
return false;
}
fs.unlinkSync(backupPath);
console.log(` Deleted backup: ${backupPath}`);
return true;
}

module.exports = {
BACKUP_DIR,
listBackups,
exportSandbox,
importSandbox,
deleteBackup,
};
Loading