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
14 changes: 14 additions & 0 deletions src/lib/policy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -833,6 +833,20 @@ function applyPresetContent(
}
registry.updateSandbox(sandboxName, { policies: pols });
}
} else if (options.custom) {
// The preset reached the gateway, but sandbox `sandboxName` has no local
// registry entry, so it cannot be recorded under `customPolicies`. Custom
// presets are surfaced only from the registry (both `listCustomPresets`
// and `getGatewayPresets` read `registry.getCustomPolicies`), so an
// unrecorded custom preset never appears in `policy-list` or `status`.
// Report the gap instead of exiting 0 as if the preset were fully applied. (#4510)
console.error(
` Warning: '${presetName}' was applied to the gateway but could not be ` +
`recorded locally because sandbox '${sandboxName}' is not in the ` +
`registry, so it will not appear in policy-list or status. Recover or ` +
`re-onboard the sandbox, then re-apply.`,
);
return false;
}

return true;
Expand Down
99 changes: 99 additions & 0 deletions test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,105 @@ exit 1
});
});

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"),
) as Record<string, any>;
const CUSTOM_CONTENT = "network_policies:\n slack-files-upload:\n host: files.slack.com\n";
const SOURCE_PATH = "/tmp/slack-files-upload-case.yaml";

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-issue4510-"));
const localBin = path.join(tmpHome, ".local", "bin");
fs.mkdirSync(localBin, { recursive: true });
fakeOpenshell = path.join(localBin, "openshell");
fs.writeFileSync(fakeOpenshell, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
origHome = process.env.HOME;
process.env.HOME = tmpHome;
resolveSpy = vi
.spyOn(resolveOpenshellModule, "resolveOpenshell")
.mockReturnValue(fakeOpenshell);
savedGetSandbox = registryModule.getSandbox;
savedAddCustomPolicy = registryModule.addCustomPolicy;
});

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("returns false and warns when a custom preset cannot be recorded locally", () => {
// Sandbox is Ready on the gateway but missing from the local registry
// (e.g. after stale-registry pruning), so addCustomPolicy cannot persist.
registryModule.getSandbox = () => null;
const addSpy = vi.fn(() => false);
registryModule.addCustomPolicy = addSpy;
const errors: string[] = [];
const errSpy = vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => {
errors.push(a.map((x) => String(x)).join(" "));
});
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
try {
const result = policies.applyPresetContent(
"my-assistant",
"slack-files-upload",
CUSTOM_CONTENT,
{ custom: { sourcePath: SOURCE_PATH } },
);
// Pre-fix this returned true (silent exit 0) while policy-list/status
// never showed the preset. The command must not claim success.
expect(result).toBe(false);
expect(addSpy).not.toHaveBeenCalled();
const combined = errors.join("\n");
expect(combined).toContain("my-assistant");
expect(combined).toMatch(/could not be\s+recorded locally/);
expect(combined).toMatch(/policy-list or status/);
} finally {
errSpy.mockRestore();
logSpy.mockRestore();
}
});

it("records the custom preset and returns true when the sandbox is registered", () => {
registryModule.getSandbox = (name: string) => ({ name });
const addSpy = vi.fn(() => true);
registryModule.addCustomPolicy = addSpy;
const logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined);
const errSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
try {
const result = policies.applyPresetContent(
"my-assistant",
"slack-files-upload",
CUSTOM_CONTENT,
{ custom: { sourcePath: SOURCE_PATH } },
);
expect(result).toBe(true);
expect(addSpy).toHaveBeenCalledWith(
"my-assistant",
expect.objectContaining({
name: "slack-files-upload",
content: CUSTOM_CONTENT,
sourcePath: SOURCE_PATH,
}),
);
} finally {
logSpy.mockRestore();
errSpy.mockRestore();
}
});
});

describe("extractPresetEntries", () => {
it("returns null for null input", () => {
expect(policies.extractPresetEntries(null)).toBe(null);
Expand Down
Loading