feat: gatekeeper service + gate de phase (v0.11.0)
Gatekeeper vérifie le statut d'asset avant chaque exécution : privilégié inconnu → BLOQUÉ (aucun sursis) refusé jusqu'à review + hook d'alerte ; approuvé → exécutable. Lecture seule autorisée sans asset. Gate de phase CHLOVA_PHASE (défaut 1, fail-safe) ; assertReadOnlyPhase phase-aware. 7 nouveaux tests. Palier de risque : reversible (logique de contrôle, n'exécute aucune mutation). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { AssetRepository } from "../src/gatekeeper/repository.js";
|
||||
import { Gatekeeper, GatekeeperGuard } from "../src/gatekeeper/gatekeeper.js";
|
||||
import { createLogger } from "../src/audit/log.js";
|
||||
import type { ToolSpec } from "../src/agent/types.js";
|
||||
|
||||
const log = createLogger("silent");
|
||||
|
||||
const readTool: ToolSpec = {
|
||||
name: "n8n.list_workflows",
|
||||
description: "",
|
||||
parameters: {},
|
||||
server: "n8n",
|
||||
readOnly: true,
|
||||
riskTier: "reversible",
|
||||
};
|
||||
const writeTool: ToolSpec = {
|
||||
name: "portainer.deploy_stack",
|
||||
description: "",
|
||||
parameters: {},
|
||||
server: "portainer",
|
||||
readOnly: false,
|
||||
riskTier: "privileged",
|
||||
};
|
||||
|
||||
let repo: AssetRepository;
|
||||
beforeEach(() => {
|
||||
repo = new AssetRepository(":memory:");
|
||||
});
|
||||
afterEach(() => {
|
||||
repo.close();
|
||||
});
|
||||
|
||||
describe("Gatekeeper (Phase 2)", () => {
|
||||
it("autorise la lecture seule sans créer d'asset", () => {
|
||||
const gk = new Gatekeeper(repo, log);
|
||||
expect(gk.authorizeTool(readTool).allowed).toBe(true);
|
||||
expect(repo.list()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("refuse un privilégié inconnu, l'enregistre BLOQUÉ et alerte", () => {
|
||||
const onBlockedAttempt = vi.fn();
|
||||
const gk = new Gatekeeper(repo, log, { onBlockedAttempt });
|
||||
const v = gk.authorizeTool(writeTool);
|
||||
expect(v.allowed).toBe(false);
|
||||
const id = Gatekeeper.assetIdForTool(writeTool);
|
||||
const asset = repo.get(id);
|
||||
expect(asset?.status).toBe("bloqué");
|
||||
expect(asset?.riskTier).toBe("privileged");
|
||||
expect(asset?.expiresAt).toBeNull(); // aucun sursis
|
||||
expect(onBlockedAttempt).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("autorise un privilégié APPROUVÉ et incrémente le compteur", () => {
|
||||
const gk = new Gatekeeper(repo, log);
|
||||
gk.authorizeTool(writeTool); // crée l'asset bloqué
|
||||
const id = Gatekeeper.assetIdForTool(writeTool);
|
||||
repo.updateStatus(id, "approuvé"); // review humaine
|
||||
|
||||
expect(gk.authorizeTool(writeTool).allowed).toBe(true);
|
||||
expect(gk.authorizeTool(writeTool).allowed).toBe(true);
|
||||
expect(repo.get(id)?.execCount).toBe(2);
|
||||
});
|
||||
|
||||
it("refuse un asset REFUSÉ", () => {
|
||||
const gk = new Gatekeeper(repo, log);
|
||||
gk.authorizeTool(writeTool);
|
||||
const id = Gatekeeper.assetIdForTool(writeTool);
|
||||
repo.updateStatus(id, "refusé");
|
||||
expect(gk.authorizeTool(writeTool).allowed).toBe(false);
|
||||
});
|
||||
|
||||
it("GatekeeperGuard délègue au gatekeeper", () => {
|
||||
const guard = new GatekeeperGuard(new Gatekeeper(repo, log));
|
||||
expect(guard.authorize(readTool).allowed).toBe(true);
|
||||
expect(guard.authorize(writeTool).allowed).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user