Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions src/lib/oclif-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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,
Expand Down
75 changes: 75 additions & 0 deletions src/lib/shields-cli-commands.ts
Original file line number Diff line number Diff line change
@@ -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 = ["<name> shields down [--timeout 5m] [--reason <text>] [--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<void> {
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 = ["<name> shields up"];
static args = { sandboxName: sandboxNameArg };
static flags = {
help: Flags.help({ char: "h" }),
};

public async run(): Promise<void> {
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 = ["<name> shields status"];
static args = { sandboxName: sandboxNameArg };
static flags = {
help: Flags.help({ char: "h" }),
};

public async run(): Promise<void> {
const { args } = await this.parse(ShieldsStatusCommand);
shields.shieldsStatus(args.sandboxName);
}
}
54 changes: 18 additions & 36 deletions src/nemoclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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} <name> shields <down|up|status>`);
Expand Down
20 changes: 20 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<name> 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("<name> 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("<name> 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);
Expand Down
2 changes: 2 additions & 0 deletions test/uninstall.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});

Expand Down
Loading