test: gatekeeper + readonly-filter + config, interfaces need-review (v0.9.0)
Fin Phase 1. 22 tests verts : barrière readonly-filter (fail-safe), ReadOnlyGuard, paliers de risque + sursis, invariant anti-escalade, config fail-closed + masquage secrets. Interfaces du cycle need-review posées pour la Phase 2 (Asset, canExecute) sans câblage runtime. Split tsconfig typecheck/build. Palier de risque : reversible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import type { RiskTier } from "../audit/log.js";
|
||||
|
||||
/**
|
||||
* Modèle d'asset & cycle "need review" — INTERFACES posées pour la Phase 2.
|
||||
*
|
||||
* Non câblé au runtime en Phase 1 (aucune écriture). On fige ici le contrat et
|
||||
* les invariants non négociables (paliers de risque, sursis, anti-escalade) +
|
||||
* leurs tests, pour que la Phase 2 les implémente sans dériver des règles.
|
||||
* Voir docs/risk-tiers.md.
|
||||
*/
|
||||
|
||||
export type AssetType = "workflow-n8n" | "stack-portainer" | "tool" | "image";
|
||||
|
||||
export type AssetStatus = "provisoire" | "approuvé" | "refusé" | "bloqué";
|
||||
|
||||
/** Durée du sursis PROVISOIRE pour un asset réversible. */
|
||||
export const PROVISIONAL_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
/** Reflète la table d'assets décrite dans CLAUDE.md. */
|
||||
export interface Asset {
|
||||
id: string;
|
||||
type: AssetType;
|
||||
version: string; // semver de l'asset
|
||||
riskTier: RiskTier;
|
||||
status: AssetStatus;
|
||||
createdAt: number; // epoch ms
|
||||
expiresAt: number | null; // null si pas de sursis (privilégié)
|
||||
execCount: number;
|
||||
commitLink: string | null;
|
||||
docLink: string | null;
|
||||
}
|
||||
|
||||
export interface CreateAssetInput {
|
||||
id: string;
|
||||
type: AssetType;
|
||||
version: string;
|
||||
riskTier: RiskTier;
|
||||
now?: number;
|
||||
commitLink?: string;
|
||||
docLink?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un asset en appliquant la RÈGLE DE SURSIS (non négociable) :
|
||||
* - `reversible` → PROVISOIRE, exécutable, expire à +7 jours ;
|
||||
* - `privileged` → BLOQUÉ immédiatement, AUCUN sursis, pas d'expiration.
|
||||
*/
|
||||
export function createAsset(input: CreateAssetInput): Asset {
|
||||
const now = input.now ?? Date.now();
|
||||
const reversible = input.riskTier === "reversible";
|
||||
return {
|
||||
id: input.id,
|
||||
type: input.type,
|
||||
version: input.version,
|
||||
riskTier: input.riskTier,
|
||||
status: reversible ? "provisoire" : "bloqué",
|
||||
createdAt: now,
|
||||
expiresAt: reversible ? now + PROVISIONAL_TTL_MS : null,
|
||||
execCount: 0,
|
||||
commitLink: input.commitLink ?? null,
|
||||
docLink: input.docLink ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export class EscalationError extends Error {}
|
||||
|
||||
/**
|
||||
* Invariant anti-escalade : un asset `privileged` ne peut JAMAIS être reclassé
|
||||
* `reversible` (le LLM ne doit pas pouvoir contourner la review en abaissant le
|
||||
* palier). Tout autre changement est permis (durcissement autorisé).
|
||||
*/
|
||||
export function assertNoEscalation(current: RiskTier, next: RiskTier): void {
|
||||
if (current === "privileged" && next === "reversible") {
|
||||
throw new EscalationError(
|
||||
"Reclassement interdit : un asset privilégié ne peut pas devenir réversible.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GATEKEEPER (Phase 2) : décide si un asset peut s'exécuter MAINTENANT.
|
||||
* Posé ici comme référence ; non appelé en Phase 1.
|
||||
*/
|
||||
export function canExecute(asset: Asset, now = Date.now()): { ok: boolean; reason?: string } {
|
||||
switch (asset.status) {
|
||||
case "approuvé":
|
||||
return { ok: true };
|
||||
case "refusé":
|
||||
return { ok: false, reason: "asset refusé" };
|
||||
case "bloqué":
|
||||
return { ok: false, reason: "asset bloqué (review requise)" };
|
||||
case "provisoire":
|
||||
if (asset.expiresAt !== null && now > asset.expiresAt) {
|
||||
return { ok: false, reason: "sursis expiré → bloqué" };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user