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
12 changes: 12 additions & 0 deletions src/lib/policy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,12 @@ function applyPresetContent(
}

const currentPolicy = parseCurrentPolicy(rawPolicy);
if (rawPolicy.trim() && !currentPolicy) {
console.error(
` Could not read the current policy for sandbox '${sandboxName}'; refusing to apply '${presetName}' to avoid overwriting it.`,
);
return false;
}
const merged = mergePresetIntoPolicy(currentPolicy, presetEntries);

const endpoints = getPresetEndpoints(presetContent);
Expand Down Expand Up @@ -897,6 +903,12 @@ function applyPresets(sandboxName: string, presetNames: string[]): boolean {
}

let merged = parseCurrentPolicy(rawPolicy);
if (rawPolicy.trim() && !merged) {
console.error(
` Could not read the current policy for sandbox '${sandboxName}'; refusing to apply presets to avoid overwriting it.`,
);
return false;
}
const endpointLogs: string[][] = [];

for (const presetName of uniquePresetNames) {
Expand Down
101 changes: 101 additions & 0 deletions test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,107 @@ exit 1
});
});

describe("issue 4586: preset apply must not overwrite a live policy that could not be read", () => {
const registryModule = requireForTest(
path.join(REPO_ROOT, "dist", "lib", "state", "registry.js"),
) as Record<string, any>;
const CUSTOM = "network_policies:\n example:\n host: example.com\n";
const DEGRADED =
'#!/bin/sh\nif [ "$1" = "policy" ] && [ "$2" = "get" ]; then echo "error: gateway is restarting"; fi\nexit 0\n';
const EMPTY_OK = "#!/bin/sh\nexit 0\n";

let tmpHome: string;
let fakeOpenshell: string;
let origHome: string | undefined;
let resolveSpy: ReturnType<typeof vi.spyOn>;
let savedGetSandbox: any;
let savedAddCustomPolicy: any;

beforeEach(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-issue4586-"));
const localBin = path.join(tmpHome, ".local", "bin");
fs.mkdirSync(localBin, { recursive: true });
fakeOpenshell = path.join(localBin, "openshell");
origHome = process.env.HOME;
process.env.HOME = tmpHome;
resolveSpy = vi
.spyOn(resolveOpenshellModule, "resolveOpenshell")
.mockReturnValue(fakeOpenshell);
savedGetSandbox = registryModule.getSandbox;
savedAddCustomPolicy = registryModule.addCustomPolicy;
registryModule.getSandbox = (name: string) => ({ name });
registryModule.addCustomPolicy = () => true;
});

afterEach(() => {
if (origHome === undefined) delete process.env.HOME;
else process.env.HOME = origHome;
resolveSpy.mockRestore();
registryModule.getSandbox = savedGetSandbox;
registryModule.addCustomPolicy = savedAddCustomPolicy;
fs.rmSync(tmpHome, { recursive: true, force: true });
});

it("aborts applyPresetContent (returns false) when policy get exits 0 with degraded output", () => {
fs.writeFileSync(fakeOpenshell, DEGRADED, { mode: 0o755 });
const errs: string[] = [];
const errSpy = vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => {
errs.push(a.map((x) => String(x)).join(" "));
});
const logs: string[] = [];
const logSpy = vi.spyOn(console, "log").mockImplementation((...a: unknown[]) => {
logs.push(a.map((x) => String(x)).join(" "));
});
try {
const result = policies.applyPresetContent("alpha", "my-custom", CUSTOM, {
custom: { sourcePath: "/tmp/x.yaml" },
});
expect(result).toBe(false);
expect(errs.join("\n")).toMatch(/[Cc]ould not read the current policy/);
expect(logs.join("\n")).not.toContain("Applied preset:");
} finally {
errSpy.mockRestore();
logSpy.mockRestore();
}
});

it("still applies applyPresetContent when policy get returns an empty policy (fresh sandbox)", () => {
fs.writeFileSync(fakeOpenshell, EMPTY_OK, { mode: 0o755 });
const logs: string[] = [];
const logSpy = vi.spyOn(console, "log").mockImplementation((...a: unknown[]) => {
logs.push(a.map((x) => String(x)).join(" "));
});
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
try {
const result = policies.applyPresetContent("alpha", "my-custom", CUSTOM, {
custom: { sourcePath: "/tmp/x.yaml" },
});
expect(result).toBe(true);
expect(logs.join("\n")).toContain("Applied preset:");
} finally {
logSpy.mockRestore();
errSpy.mockRestore();
}
});

it("aborts applyPresets (returns false) when policy get exits 0 with degraded output", () => {
fs.writeFileSync(fakeOpenshell, DEGRADED, { mode: 0o755 });
const errs: string[] = [];
const errSpy = vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => {
errs.push(a.map((x) => String(x)).join(" "));
});
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
try {
const result = policies.applyPresets("alpha", ["npm"]);
expect(result).toBe(false);
expect(errs.join("\n")).toMatch(/[Cc]ould not read the current policy/);
} finally {
errSpy.mockRestore();
logSpy.mockRestore();
}
});
});

describe("issue 4510: policy-add --from-file false success when the sandbox is absent from the registry", () => {
const registryModule = requireForTest(
path.join(REPO_ROOT, "dist", "lib", "state", "registry.js"),
Expand Down
Loading