Skip to content
Open
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: 7 additions & 1 deletion bin/lib/policies.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function getPresetEndpoints(content) {
const regex = /host:\s*([^\s,}]+)/g;
let match;
while ((match = regex.exec(content)) !== null) {
hosts.push(match[1]);
hosts.push(match[1].replace(/^["']|["']$/g, ""));
}
return hosts;
}
Expand Down Expand Up @@ -175,6 +175,12 @@ function applyPreset(sandboxName, presetName) {
merged = "version: 1\n\nnetwork_policies:\n" + presetEntries;
}

// Disclose the egress endpoints being added so the operator can audit
const endpoints = getPresetEndpoints(presetContent);
if (endpoints.length > 0) {
console.log(` Widening sandbox egress — adding: ${endpoints.join(", ")}`);
}

// Write temp file and apply
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-policy-"));
const tmpFile = path.join(tmpDir, "policy.yaml");
Expand Down
41 changes: 40 additions & 1 deletion test/policies.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import assert from "node:assert/strict";
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import policies from "../bin/lib/policies";

describe("policies", () => {
Expand Down Expand Up @@ -79,6 +79,45 @@ describe("policies", () => {
expect(hosts.length > 0).toBeTruthy();
}
});

it("strips surrounding quotes from hostnames", () => {
const yaml = 'host: "example.com"\n host: \'other.com\'';
const hosts = policies.getPresetEndpoints(yaml);
expect(hosts).toEqual(["example.com", "other.com"]);
});
});

describe("applyPreset disclosure logging", () => {
it("logs egress endpoints before applying", () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); });

try {
policies.applyPreset("test-sandbox", "npm");
} catch {}

const messages = logSpy.mock.calls.map((c) => c[0]);
expect(messages.some((m) => typeof m === "string" && m.includes("Widening sandbox egress"))).toBe(true);

logSpy.mockRestore();
errSpy.mockRestore();
exitSpy.mockRestore();
});

it("does not log when preset has no endpoints", () => {
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
const errSpy = vi.spyOn(console, "error").mockImplementation(() => {});

// loadPreset returns null for nonexistent presets → early return
policies.applyPreset("test-sandbox", "nonexistent");

const messages = logSpy.mock.calls.map((c) => c[0]);
expect(messages.some((m) => typeof m === "string" && m.includes("Widening sandbox egress"))).toBe(false);

logSpy.mockRestore();
errSpy.mockRestore();
});
});

describe("buildPolicySetCommand", () => {
Expand Down