overmind-mcp
Version:
Orchestrateur universel agents IA multi-modeles via MCP. Inclut le protocole 'Custom-Nickname' pour identifier vos agents avec des surnoms originaux (The Chaos Prophet, Shadow Sniper, etc.), l'isolation mémoire (Private Memory Context) et le support pour
213 lines • 8.87 kB
JavaScript
/**
* ═══════════════════════════════════════════════════════════════════════════
* Audit Trail — Privacy Guard (middleware)
* ═══════════════════════════════════════════════════════════════════════════
*
* Intercepte chaque flux de RP pour appliquer les contrôles d'audit.
* Appelé par les runners, le bridge et les outils de stockage.
*
* Comportement :
* 1. Si OVERMIND_AUDIT_ENABLED=false → pass-through (zero overhead, v3.8)
* 2. Détection de la base légale (interne vs tiers)
* 3. Validation du consentement si tiers (art. 8)
* 4. Détection de RP dans le texte (email, NAS, téléphone, etc.)
* 5. Pseudonymisation / anonymisation si requis (art. 23.1)
* 6. Log du transfert si runner LLM externe (art. 21)
* 7. Calcul de la rétention (art. 35.2)
*/
import { getPool } from 'overmind-postgres-mcp';
import { rootLogger } from '../logger.js';
import { isAuditEnabled, getDefaultLegalBasis, isInternalSubject, } from './types.js';
import { detectPii, pseudonymize, hashShort } from './anonymize.js';
import { calculateRetentionExpiry } from './retention.js';
import { getProviderInfo, requiresExplicitConsent } from './transfer_map.js';
const logger = rootLogger.child({ module: 'AuditGuard' });
// ── Validation de consentement ───────────────────────────────────────────────
/**
* Valide qu'un consentement est actif pour un sujet + finalité.
*/
async function validateConsent(dataSubjectId, purpose) {
const pool = getPool();
try {
const res = await pool.query(`SELECT id, granted_at, expires_at, withdrawn_at
FROM consent_records
WHERE data_subject_id = $1 AND purpose = $2
ORDER BY granted_at DESC LIMIT 1`, [dataSubjectId, purpose]);
if (res.rows.length === 0) {
return {
valid: false,
consentRef: null,
reason: `Aucun consentement trouvé pour le sujet ${dataSubjectId} (finalité: ${purpose})`,
};
}
const consent = res.rows[0];
const now = Date.now();
if (consent.withdrawn_at && parseInt(consent.withdrawn_at, 10) <= now) {
return {
valid: false,
consentRef: null,
reason: `Consentement révoqué pour le sujet ${dataSubjectId}`,
};
}
if (consent.expires_at && parseInt(consent.expires_at, 10) <= now) {
return {
valid: false,
consentRef: null,
reason: `Consentement expiré pour le sujet ${dataSubjectId}`,
};
}
return { valid: true, consentRef: consent.id };
}
catch (err) {
logger.error({ error: err instanceof Error ? err.message : String(err) }, 'AuditGuard: Failed to validate consent — allowing (fail-open to avoid blocking ops)');
// Fail-open : si la DB est down, on laisse passer pour éviter de bloquer l'Overmind
return { valid: true, consentRef: null };
}
}
// ── Log d'accès ──────────────────────────────────────────────────────────────
/**
* Enregistre un accès dans access_log.
*/
export async function logAccess(params) {
if (!isAuditEnabled())
return;
const pool = getPool();
try {
await pool.query(`INSERT INTO access_log (id, data_subject_id, accessed_by, action, resource_type, resource_id, purpose, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [
hashShort(`${Date.now()}-${Math.random()}`),
params.dataSubjectId || null,
params.accessedBy,
params.action,
params.resourceType,
params.resourceId,
params.purpose || null,
Date.now(),
]);
}
catch (err) {
logger.warn({ error: err instanceof Error ? err.message : String(err) }, 'AuditGuard: Failed to log access (non-blocking)');
}
}
// ── Log de transfert ─────────────────────────────────────────────────────────
/**
* Enregistre un transfert de RP hors Québec dans transfer_log (art. 21).
*/
export async function logTransfer(params) {
if (!isAuditEnabled())
return;
const pool = getPool();
try {
await pool.query(`INSERT INTO transfer_log (id, data_subject_id, destination, destination_region, legal_mechanism, data_type, timestamp)
VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
hashShort(`transfer-${Date.now()}-${Math.random()}`),
params.dataSubjectId || null,
params.destination,
params.destinationRegion,
params.legalMechanism,
params.dataType,
Date.now(),
]);
}
catch (err) {
logger.warn({ error: err instanceof Error ? err.message : String(err) }, 'AuditGuard: Failed to log transfer (non-blocking)');
}
}
/**
* Applique les contrôles d'audit sur une requête.
*
* @returns GuardResult — allowed=false si la requête doit être bloquée
*/
export async function auditGuard(opts) {
const now = Date.now();
const purpose = opts.purpose || 'agent_execution';
// ── 1. Feature flag off → pass-through ──
if (!isAuditEnabled()) {
return {
allowed: true,
context: {
legalBasis: 'legitimate_interest',
purpose,
},
piiDetected: false,
piiTypes: [],
retentionExpiresAt: calculateRetentionExpiry(now),
};
}
// ── 2. Détection des RP ──
const piiResult = detectPii(opts.prompt, opts.sanitize === true);
// ── 3. Résolution de la base légale ──
let legalBasis;
let consentRef;
const dataSubjectId = opts.dataSubjectId;
if (opts.legalBasisOverride) {
legalBasis = opts.legalBasisOverride;
}
else if (dataSubjectId && isInternalSubject(dataSubjectId)) {
legalBasis = 'legitimate_interest';
}
else if (dataSubjectId) {
// Sujet identifié mais pas interne → tiers → consentement requis
legalBasis = 'consent';
}
else {
legalBasis = getDefaultLegalBasis();
}
// ── 4. Validation du consentement (tiers uniquement) ──
if (legalBasis === 'consent' && dataSubjectId) {
const consentCheck = await validateConsent(dataSubjectId, purpose);
if (!consentCheck.valid) {
await logAccess({
dataSubjectId,
accessedBy: 'audit_guard',
action: 'read',
resourceType: 'consent_check',
resourceId: purpose,
purpose: 'blocked_no_consent',
});
return {
allowed: false,
reason: consentCheck.reason || 'Consentement requis et manquant',
context: { dataSubjectId, legalBasis, purpose },
piiDetected: piiResult.found,
piiTypes: piiResult.types,
retentionExpiresAt: calculateRetentionExpiry(now),
};
}
consentRef = consentCheck.consentRef || undefined;
}
// ── 5. Log du transfert si provider externe ──
if (opts.provider) {
const providerInfo = getProviderInfo(opts.provider);
if (requiresExplicitConsent(opts.provider)) {
logTransfer({
dataSubjectId: dataSubjectId || null,
destination: providerInfo.name,
destinationRegion: providerInfo.region,
legalMechanism: providerInfo.mechanism,
dataType: 'prompt',
}).catch(() => { }); // fire-and-forget
}
}
// ── 6. Construction du résultat ──
return {
allowed: true,
context: {
dataSubjectId,
legalBasis,
consentRef,
purpose,
},
sanitizedPrompt: piiResult.sanitized,
piiDetected: piiResult.found,
piiTypes: piiResult.types,
retentionExpiresAt: calculateRetentionExpiry(now),
};
}
/**
* Helper : pseudonymise un identifiant pour stockage en DB.
*/
export function pseudonymizeSubject(rawId) {
return pseudonymize(rawId);
}
//# sourceMappingURL=guard.js.map