feat(FN-3077): enforce plugin AI security scan gate across install flows

- Add core plugin AI security scan module and schema support for scan toggle/state metadata
- Enforce scan checks during CLI and dashboard plugin install flows, with preserved API error status on scan failures
- Expose plugin scan toggle and rescan actions in dashboard/plugin manager with route and UI coverage
- Update plugin authoring and CLI/dashboard docs, plus add changeset for published CLI package

Fusion-Task-Id: FN-3077
This commit is contained in:
Fusion
2026-05-07 02:14:36 -07:00
committed by gsxdsm
parent d68d598a4f
commit 5299745fc4
29 changed files with 800 additions and 43 deletions

View File

@@ -76,7 +76,7 @@ vi.mock("node:fs/promises", () => ({
),
}));
import { runPluginAvailable, runPluginInstall, runPluginSettings } from "../plugin.js";
import { runPluginAvailable, runPluginInstall, runPluginSettings, runPluginRescan } from "../plugin.js";
import { resolveProject } from "../../project-context.js";
describe("plugin commands", () => {
@@ -107,6 +107,25 @@ describe("plugin commands", () => {
expect(console.log).toHaveBeenCalledWith(expect.stringContaining("fusion-plugin-agent-browser"));
});
it("exits non-zero when rescan verdict is blocked", async () => {
const storeInstance = {
init: vi.fn().mockResolvedValue(undefined),
registerPlugin: vi.fn(),
listPlugins: vi.fn(),
getPlugin: vi
.fn()
.mockResolvedValueOnce({ id: "paperclip-runtime", name: "Paperclip Runtime", enabled: true, state: "started" })
.mockResolvedValueOnce({ id: "paperclip-runtime", name: "Paperclip Runtime", enabled: true, state: "error", lastSecurityScan: { verdict: "blocked", summary: "blocked", findings: [], scannedAt: "now", scannedFiles: [] } }),
updatePluginSettings: vi.fn().mockResolvedValue(undefined),
};
mocks.PluginStore.mockImplementationOnce(() => storeInstance as never);
mocks.PluginLoader.mockImplementationOnce(() => ({ loadPlugin: vi.fn(), reloadPlugin: vi.fn().mockResolvedValue(undefined) }) as never);
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { throw new Error(`exit:${code}`); }) as never);
await expect(runPluginRescan("paperclip-runtime", { projectName: "demo" })).rejects.toThrow("exit:1");
expect(exitSpy).toHaveBeenCalledWith(1);
});
it("reads and updates plugin settings", async () => {
const storeInstance = {
init: vi.fn().mockResolvedValue(undefined),