diff --git a/src/lib/oclif-commands.ts b/src/lib/oclif-commands.ts index 74853931093..bcc8768fa51 100644 --- a/src/lib/oclif-commands.ts +++ b/src/lib/oclif-commands.ts @@ -21,6 +21,11 @@ import { SandboxStatusCommand, } from "./sandbox-inspection-cli-command"; import SandboxLogsCommand from "./sandbox-logs-cli-command"; +import { + ShieldsDownCommand, + ShieldsStatusCommand, + ShieldsUpCommand, +} from "./shields-cli-commands"; import ShareCommand from "./share-command"; import SkillInstallCliCommand from "./skill-install-cli-command"; import { SnapshotCreateCommand, SnapshotListCommand } from "./snapshot-cli-commands"; @@ -44,6 +49,9 @@ export default { "sandbox:config:get": SandboxConfigGetCommand, "sandbox:logs": SandboxLogsCommand, "sandbox:policy-list": SandboxPolicyListCommand, + "sandbox:shields:down": ShieldsDownCommand, + "sandbox:shields:status": ShieldsStatusCommand, + "sandbox:shields:up": ShieldsUpCommand, "sandbox:skill:install": SkillInstallCliCommand, "sandbox:snapshot:create": SnapshotCreateCommand, "sandbox:snapshot:list": SnapshotListCommand, diff --git a/src/lib/shields-cli-commands.ts b/src/lib/shields-cli-commands.ts new file mode 100644 index 00000000000..7811f69f66a --- /dev/null +++ b/src/lib/shields-cli-commands.ts @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* v8 ignore start -- thin oclif adapters covered through CLI integration tests. */ + +import { Args, Command, Flags } from "@oclif/core"; + +import * as shields from "./shields"; + +const sandboxNameArg = Args.string({ + name: "sandbox", + description: "Sandbox name", + required: true, +}); + +export class ShieldsDownCommand extends Command { + static id = "sandbox:shields:down"; + static hidden = true; + static strict = true; + static summary = "Lower sandbox security shields"; + static description = "Temporarily lower sandbox shields."; + static usage = [" shields down [--timeout 5m] [--reason ] [--policy permissive]"]; + static args = { sandboxName: sandboxNameArg }; + static flags = { + help: Flags.help({ char: "h" }), + timeout: Flags.string({ description: "Duration before shields are restored" }), + reason: Flags.string({ description: "Reason for lowering shields" }), + policy: Flags.string({ description: "Policy to apply while shields are down" }), + }; + + public async run(): Promise { + const { args, flags } = await this.parse(ShieldsDownCommand); + shields.shieldsDown(args.sandboxName, { + timeout: flags.timeout ?? null, + reason: flags.reason ?? null, + policy: flags.policy ?? "permissive", + }); + } +} + +export class ShieldsUpCommand extends Command { + static id = "sandbox:shields:up"; + static hidden = true; + static strict = true; + static summary = "Raise sandbox security shields"; + static description = "Restore sandbox shields from the saved snapshot."; + static usage = [" shields up"]; + static args = { sandboxName: sandboxNameArg }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(ShieldsUpCommand); + shields.shieldsUp(args.sandboxName); + } +} + +export class ShieldsStatusCommand extends Command { + static id = "sandbox:shields:status"; + static hidden = true; + static strict = true; + static summary = "Show current shields state"; + static description = "Show current sandbox shields state."; + static usage = [" shields status"]; + static args = { sandboxName: sandboxNameArg }; + static flags = { + help: Flags.help({ char: "h" }), + }; + + public async run(): Promise { + const { args } = await this.parse(ShieldsStatusCommand); + shields.shieldsStatus(args.sandboxName); + } +} diff --git a/src/nemoclaw.ts b/src/nemoclaw.ts index 82689a47906..ae4bf381454 100644 --- a/src/nemoclaw.ts +++ b/src/nemoclaw.ts @@ -4845,48 +4845,30 @@ const [cmd, ...args] = process.argv.slice(2); break; case "shields": { const shieldsSub = actionArgs[0]; - const shieldsFlags = actionArgs.slice(1); + const shieldsArgs = actionArgs.slice(1); switch (shieldsSub) { - case "down": { - const opts: { timeout: string | null; reason: string | null; policy: string } = { - timeout: null, - reason: null, - policy: "permissive", - }; - for (let i = 0; i < shieldsFlags.length; i++) { - if (shieldsFlags[i] === "--timeout") { - if (i + 1 >= shieldsFlags.length || shieldsFlags[i + 1].startsWith("--")) { - console.error(" --timeout requires a value (e.g. 5m, 30m, 300)"); - process.exit(1); - } - opts.timeout = shieldsFlags[++i]; - } else if (shieldsFlags[i] === "--reason") { - if (i + 1 >= shieldsFlags.length || shieldsFlags[i + 1].startsWith("--")) { - console.error(" --reason requires a value"); - process.exit(1); - } - opts.reason = shieldsFlags[++i]; - } else if (shieldsFlags[i] === "--policy") { - if (i + 1 >= shieldsFlags.length || shieldsFlags[i + 1].startsWith("--")) { - console.error( - " --policy requires a value (e.g. permissive, /path/to/policy.yaml)", - ); - process.exit(1); - } - opts.policy = shieldsFlags[++i]; - } else { - console.error(` Unknown flag: ${shieldsFlags[i]}`); - process.exit(1); - } + case "down": + if (hasHelpFlag(shieldsArgs)) { + printSandboxActionUsage( + "shields down [--timeout 5m] [--reason 'text'] [--policy permissive]", + ); + break; } - shields.shieldsDown(cmd, opts); + await runOclif("sandbox:shields:down", [cmd, ...shieldsArgs]); break; - } case "up": - shields.shieldsUp(cmd); + if (hasHelpFlag(shieldsArgs)) { + printSandboxActionUsage("shields up"); + break; + } + await runOclif("sandbox:shields:up", [cmd, ...shieldsArgs]); break; case "status": - shields.shieldsStatus(cmd); + if (hasHelpFlag(shieldsArgs)) { + printSandboxActionUsage("shields status"); + break; + } + await runOclif("sandbox:shields:status", [cmd, ...shieldsArgs]); break; default: console.error(` Usage: ${CLI_NAME} shields `); diff --git a/test/cli.test.ts b/test/cli.test.ts index 448d5ed936c..79488e32ee0 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -1042,6 +1042,26 @@ describe("CLI dispatch", () => { expect(config.out).not.toContain("sandbox:config:get"); }); + it("shields help keeps public sandbox-scoped usage", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-shields-help-")); + writeSandboxRegistry(home); + + const down = runWithEnv("alpha shields down --help", { HOME: home }); + expect(down.code).toBe(0); + expect(down.out).toContain(" shields down"); + expect(down.out).not.toContain("sandbox:shields:down"); + + const up = runWithEnv("alpha shields up --help", { HOME: home }); + expect(up.code).toBe(0); + expect(up.out).toContain(" shields up"); + expect(up.out).not.toContain("sandbox:shields:up"); + + const status = runWithEnv("alpha shields status --help", { HOME: home }); + expect(status.code).toBe(0); + expect(status.out).toContain(" shields status"); + expect(status.out).not.toContain("sandbox:shields:status"); + }); + it("snapshot list/create help keeps public sandbox-scoped usage", () => { const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-snapshot-help-")); writeSandboxRegistry(home); diff --git a/test/uninstall.test.ts b/test/uninstall.test.ts index 601c800a0fa..f7b54027ffc 100644 --- a/test/uninstall.test.ts +++ b/test/uninstall.test.ts @@ -56,6 +56,8 @@ describe("uninstall CLI flags", () => { HOME: tmp, PATH: `${fakeBin}:/usr/bin:/bin`, SCRIPT_DIR: path.join(import.meta.dirname, ".."), + // Keep helper-service glob cleanup isolated from concurrently running tests. + TMPDIR: tmp, }, });