Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 path from "node:path";
import policies from "../bin/lib/policies";

Expand Down Expand Up @@ -67,6 +67,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