diff --git a/src/lib/policy-channel-actions.ts b/src/lib/policy-channel-actions.ts index 15a6bed6b4..d2cacdd429 100644 --- a/src/lib/policy-channel-actions.ts +++ b/src/lib/policy-channel-actions.ts @@ -13,6 +13,7 @@ import { recoverNamedGatewayRuntime } from "./gateway-runtime-action"; const { isNonInteractive } = require("./onboard") as { isNonInteractive: () => boolean }; const onboardProviders = require("./onboard-providers"); import * as policies from "./policies"; +import { parsePolicyAddArgs } from "./policy-channel-helpers"; import * as registry from "./registry"; import { runOpenshell } from "./openshell-runtime"; import { rebuildSandbox } from "./sandbox-runtime-actions"; @@ -44,38 +45,21 @@ const YW = useColor ? "\x1b[1;33m" : ""; * rolled back). */ export async function addSandboxPolicy(sandboxName: string, args: string[] = []): Promise { - const dryRun = args.includes("--dry-run"); - const skipConfirm = - args.includes("--yes") || - args.includes("-y") || - args.includes("--force") || - process.env.NEMOCLAW_NON_INTERACTIVE === "1"; + const { dryRun, skipConfirm, source, presetArg } = parsePolicyAddArgs(args); - const fromFileIdx = args.indexOf("--from-file"); - const fromDirIdx = args.indexOf("--from-dir"); - - if (fromFileIdx >= 0 && fromDirIdx >= 0) { - console.error(" --from-file and --from-dir are mutually exclusive."); + if (source.kind === "error") { + console.error(` ${source.message}`); process.exit(1); } - if (fromFileIdx >= 0) { - const filePath = args[fromFileIdx + 1]; - if (!filePath || filePath.startsWith("--")) { - console.error(" --from-file requires a path argument."); - process.exit(1); - } - const ok = await applyExternalPreset(sandboxName, filePath, { dryRun, yes: skipConfirm }); + if (source.kind === "file") { + const ok = await applyExternalPreset(sandboxName, source.path, { dryRun, yes: skipConfirm }); if (!ok) process.exit(1); return; } - if (fromDirIdx >= 0) { - const dirPath = args[fromDirIdx + 1]; - if (!dirPath || dirPath.startsWith("--")) { - console.error(" --from-dir requires a directory path."); - process.exit(1); - } + if (source.kind === "dir") { + const dirPath = source.path; const absDir = path.resolve(dirPath); if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) { console.error(` Directory not found: ${dirPath}`); @@ -106,7 +90,6 @@ export async function addSandboxPolicy(sandboxName: string, args: string[] = []) const allPresets = policies.listPresets(); const applied = policies.getAppliedPresets(sandboxName); - const presetArg = args.find((arg) => !arg.startsWith("-")); let answer = null; if (presetArg) { const normalized = presetArg.trim().toLowerCase(); diff --git a/src/lib/policy-channel-helpers.test.ts b/src/lib/policy-channel-helpers.test.ts new file mode 100644 index 0000000000..74297d0da3 --- /dev/null +++ b/src/lib/policy-channel-helpers.test.ts @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { + parseCustomPolicySource, + parsePolicyAddArgs, + shouldSkipPolicyConfirmation, +} from "./policy-channel-helpers"; + +describe("policy channel helpers", () => { + it("parses custom policy source flags", () => { + expect(parseCustomPolicySource([])).toEqual({ kind: "none" }); + expect(parseCustomPolicySource(["--from-file", "preset.yaml"])).toEqual({ + kind: "file", + path: "preset.yaml", + }); + expect(parseCustomPolicySource(["--from-dir", "presets"])).toEqual({ + kind: "dir", + path: "presets", + }); + }); + + it("reports custom policy source errors", () => { + expect(parseCustomPolicySource(["--from-file"])).toEqual({ + kind: "error", + message: "--from-file requires a path argument.", + }); + expect(parseCustomPolicySource(["--from-file", "a.yaml", "--from-dir", "dir"])).toEqual({ + kind: "error", + message: "--from-file and --from-dir are mutually exclusive.", + }); + }); + + it("detects policy confirmation bypass flags", () => { + expect(shouldSkipPolicyConfirmation(["--yes"])).toBe(true); + expect(shouldSkipPolicyConfirmation(["-y"])).toBe(true); + expect(shouldSkipPolicyConfirmation(["--force"])).toBe(true); + expect(shouldSkipPolicyConfirmation([], { NEMOCLAW_NON_INTERACTIVE: "1" })).toBe(true); + expect(shouldSkipPolicyConfirmation([], {})).toBe(false); + }); + + it("parses policy add args", () => { + expect(parsePolicyAddArgs(["github", "--dry-run", "--yes"], {})).toEqual({ + dryRun: true, + skipConfirm: true, + source: { kind: "none" }, + presetArg: "github", + }); + }); +}); diff --git a/src/lib/policy-channel-helpers.ts b/src/lib/policy-channel-helpers.ts new file mode 100644 index 0000000000..8517d1731a --- /dev/null +++ b/src/lib/policy-channel-helpers.ts @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/* v8 ignore start -- pure helper tests exercise this module; orchestration coverage still runs through dist. */ + +export type CustomPolicySource = + | { kind: "none" } + | { kind: "file"; path: string } + | { kind: "dir"; path: string } + | { kind: "error"; message: string }; + +export type PolicyAddArgs = { + dryRun: boolean; + skipConfirm: boolean; + source: CustomPolicySource; + presetArg: string | null; +}; + +export function parseCustomPolicySource(args: readonly string[]): CustomPolicySource { + const fromFileIdx = args.indexOf("--from-file"); + const fromDirIdx = args.indexOf("--from-dir"); + + if (fromFileIdx >= 0 && fromDirIdx >= 0) { + return { kind: "error", message: "--from-file and --from-dir are mutually exclusive." }; + } + + if (fromFileIdx >= 0) { + const filePath = args[fromFileIdx + 1]; + if (!filePath || filePath.startsWith("--")) { + return { kind: "error", message: "--from-file requires a path argument." }; + } + return { kind: "file", path: filePath }; + } + + if (fromDirIdx >= 0) { + const dirPath = args[fromDirIdx + 1]; + if (!dirPath || dirPath.startsWith("--")) { + return { kind: "error", message: "--from-dir requires a directory path." }; + } + return { kind: "dir", path: dirPath }; + } + + return { kind: "none" }; +} + +export function shouldSkipPolicyConfirmation( + args: readonly string[], + env: Record = process.env, +): boolean { + return ( + args.includes("--yes") || + args.includes("-y") || + args.includes("--force") || + env.NEMOCLAW_NON_INTERACTIVE === "1" + ); +} + +export function parsePolicyAddArgs( + args: readonly string[], + env: Record = process.env, +): PolicyAddArgs { + return { + dryRun: args.includes("--dry-run"), + skipConfirm: shouldSkipPolicyConfirmation(args, env), + source: parseCustomPolicySource(args), + presetArg: args.find((arg) => !arg.startsWith("-")) ?? null, + }; +} + +/* v8 ignore stop */