diff --git a/bin/nemoclaw.js b/bin/nemoclaw.js index 02caf73ae4c..6fcb16ac951 100755 --- a/bin/nemoclaw.js +++ b/bin/nemoclaw.js @@ -1077,7 +1077,8 @@ function sandboxLogs(sandboxName, follow) { exitWithSpawnResult(result); } -async function sandboxPolicyAdd(sandboxName) { +async function sandboxPolicyAdd(sandboxName, args = []) { + const dryRun = args.includes("--dry-run"); const allPresets = policies.listPresets(); const applied = policies.getAppliedPresets(sandboxName); @@ -1085,6 +1086,19 @@ async function sandboxPolicyAdd(sandboxName) { const answer = await policies.selectFromList(allPresets, { applied }); if (!answer) return; + const presetContent = policies.loadPreset(answer); + if (!presetContent) return; + + const endpoints = policies.getPresetEndpoints(presetContent); + if (endpoints.length > 0) { + console.log(` Endpoints that would be opened: ${endpoints.join(", ")}`); + } + + if (dryRun) { + console.log(" --dry-run: no changes applied."); + return; + } + const confirm = await askPrompt(` Apply '${answer}' to sandbox '${sandboxName}'? [Y/n]: `); if (confirm.toLowerCase() === "n") return; @@ -1179,7 +1193,7 @@ function help() { nemoclaw destroy Stop NIM + delete sandbox ${D}(--yes to skip prompt)${R} ${G}Policy Presets:${R} - nemoclaw policy-add Add a network or filesystem policy preset + nemoclaw policy-add Add a network or filesystem policy preset ${D}(--dry-run to preview)${R} nemoclaw policy-list List presets ${D}(● = applied)${R} ${G}Compatibility Commands:${R} @@ -1285,7 +1299,7 @@ const [cmd, ...args] = process.argv.slice(2); sandboxLogs(cmd, actionArgs.includes("--follow")); break; case "policy-add": - await sandboxPolicyAdd(cmd); + await sandboxPolicyAdd(cmd, actionArgs); break; case "policy-list": sandboxPolicyList(cmd); diff --git a/test/policies.test.js b/test/policies.test.js index 27c9b6d57ea..eb1ce5e6524 100644 --- a/test/policies.test.js +++ b/test/policies.test.js @@ -19,7 +19,7 @@ const SELECT_FROM_LIST_ITEMS = [ { name: "pypi", description: "Python Package Index (PyPI) access" }, ]; -function runPolicyAdd(confirmAnswer) { +function runPolicyAdd(confirmAnswer, extraArgs = []) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-policy-add-")); const scriptPath = path.join(tmpDir, "policy-add-check.js"); const script = String.raw` @@ -28,6 +28,8 @@ const policies = require(${POLICIES_PATH}); const credentials = require(${CREDENTIALS_PATH}); const calls = []; policies.selectFromList = async () => "pypi"; +policies.loadPreset = () => "network_policies:\n pypi:\n host: pypi.org\n"; +policies.getPresetEndpoints = () => ["pypi.org"]; credentials.prompt = async (message) => { calls.push({ type: "prompt", message }); return ${JSON.stringify(confirmAnswer)}; @@ -42,10 +44,10 @@ policies.getAppliedPresets = () => []; policies.applyPreset = (sandboxName, presetName) => { calls.push({ type: "apply", sandboxName, presetName }); }; -process.argv = ["node", "nemoclaw.js", "test-sandbox", "policy-add"]; +process.argv = ["node", "nemoclaw.js", "test-sandbox", "policy-add", ...${JSON.stringify(extraArgs)}]; require(${CLI_PATH}); setImmediate(() => { - process.stdout.write(JSON.stringify(calls)); + process.stdout.write("\n__CALLS__" + JSON.stringify(calls)); }); `; @@ -658,7 +660,7 @@ describe("policies", () => { const result = runPolicyAdd("y"); expect(result.status).toBe(0); - const calls = JSON.parse(result.stdout.trim()); + const calls = JSON.parse(result.stdout.split("__CALLS__")[1].trim()); expect(calls).toContainEqual({ type: "prompt", message: " Apply 'pypi' to sandbox 'test-sandbox'? [Y/n]: ", @@ -674,12 +676,23 @@ describe("policies", () => { const result = runPolicyAdd("n"); expect(result.status).toBe(0); - const calls = JSON.parse(result.stdout.trim()); + const calls = JSON.parse(result.stdout.split("__CALLS__")[1].trim()); expect(calls).toContainEqual({ type: "prompt", message: " Apply 'pypi' to sandbox 'test-sandbox'? [Y/n]: ", }); expect(calls.some((call) => call.type === "apply")).toBeFalsy(); }); + + it("does not prompt or apply when --dry-run is passed", () => { + const result = runPolicyAdd("y", ["--dry-run"]); + + expect(result.status).toBe(0); + const calls = JSON.parse(result.stdout.split("__CALLS__")[1].trim()); + expect(calls.some((call) => call.type === "prompt")).toBeFalsy(); + expect(calls.some((call) => call.type === "apply")).toBeFalsy(); + expect(result.stdout).toMatch(/Endpoints that would be opened: pypi\.org/); + expect(result.stdout).toMatch(/--dry-run: no changes applied\./); + }); }); });