UNPKG

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

149 lines • 7.46 kB
/** * ═══════════════════════════════════════════════════════════════════════════ * Audit Trail — Droit d'effacement / droit à l'oubli (art. 27 / 35.3) * ═══════════════════════════════════════════════════════════════════════════ * * Deux modes d'effacement : * - `anonymize` (défaut) : hash les champs texte (prompt/result/text), * pose anonymized=1, et null le data_subject_id. Les métadonnées non-RP * (timestamps, statistiques) sont conservées. * - `hard_delete` : suppression définitive des enregistrements. * * Par défaut inclut les archives cold storage (archived_runs, archived_chunks) * car l'art. 27 s'applique à toutes les RP détenues, y compris les archives. */ import crypto from 'crypto'; import { z } from 'zod'; import { getPool } from 'overmind-postgres-mcp'; import { logAccess } from '../lib/audit/guard.js'; export const auditErasureSchema = z.object({ data_subject_id: z .string() .min(1) .describe('Identifiant pseudonymisé du sujet à effacer'), mode: z .enum(['hard_delete', 'anonymize']) .optional() .default('anonymize') .describe("'anonymize' (défaut) : hash les textes + null data_subject_id | 'hard_delete' : suppression définitive"), include_archives: z .boolean() .optional() .default(true) .describe('Effacer aussi les archives cold storage (archived_runs, archived_chunks)'), }); function hashText(text) { // SHA-256 + salt local pour empêcher la ré-identification const salt = process.env.OVERMIND_AUDIT_SALT || 'overmind-loi25-default-salt-v1'; return `[ANONYMIZED:${crypto.createHash('sha256').update(`${salt}:${text}`).digest('hex').slice(0, 16)}]`; } export async function auditErasure(args) { try { const pool = getPool(); const subjectId = args.data_subject_id; const mode = args.mode; const includeArchives = args.include_archives; const counts = { agent_runs: 0, knowledge_chunks: 0, archived_runs: 0, archived_chunks: 0, total: 0, }; if (mode === 'anonymize') { // ── Anonymisation : on hash les textes et on null l'identifiant ──────── // agent_runs : hash prompt + result const agentRunsRes = await pool.query(`UPDATE agent_runs SET prompt = CASE WHEN prompt IS NOT NULL THEN $2 ELSE prompt END, result = CASE WHEN result IS NOT NULL THEN $2 ELSE result END, anonymized = 1, data_subject_id = NULL, consent_ref = NULL WHERE data_subject_id = $1`, [subjectId, hashText('[anonymized_prompt]')]); counts.agent_runs = agentRunsRes.rowCount ?? 0; // knowledge_chunks : hash text const knowledgeRes = await pool.query(`UPDATE knowledge_chunks SET text = $2, anonymized = 1, data_subject_id = NULL, consent_ref = NULL WHERE data_subject_id = $1`, [subjectId, hashText('[anonymized_chunk]')]); counts.knowledge_chunks = knowledgeRes.rowCount ?? 0; if (includeArchives) { const archRunsRes = await pool.query(`UPDATE archived_runs SET prompt = CASE WHEN prompt IS NOT NULL THEN $2 ELSE prompt END, result = CASE WHEN result IS NOT NULL THEN $2 ELSE result END, anonymized = 1, data_subject_id = NULL, consent_ref = NULL WHERE data_subject_id = $1`, [subjectId, hashText('[anonymized_prompt]')]); counts.archived_runs = archRunsRes.rowCount ?? 0; const archChunksRes = await pool.query(`UPDATE archived_chunks SET text = $2, anonymized = 1, data_subject_id = NULL, consent_ref = NULL WHERE data_subject_id = $1`, [subjectId, hashText('[anonymized_chunk]')]); counts.archived_chunks = archChunksRes.rowCount ?? 0; } } else { // ── Hard delete ─────────────────────────────────────────────────────── const agentRunsRes = await pool.query(`DELETE FROM agent_runs WHERE data_subject_id = $1`, [subjectId]); counts.agent_runs = agentRunsRes.rowCount ?? 0; const knowledgeRes = await pool.query(`DELETE FROM knowledge_chunks WHERE data_subject_id = $1`, [subjectId]); counts.knowledge_chunks = knowledgeRes.rowCount ?? 0; if (includeArchives) { const archRunsRes = await pool.query(`DELETE FROM archived_runs WHERE data_subject_id = $1`, [subjectId]); counts.archived_runs = archRunsRes.rowCount ?? 0; const archChunksRes = await pool.query(`DELETE FROM archived_chunks WHERE data_subject_id = $1`, [subjectId]); counts.archived_chunks = archChunksRes.rowCount ?? 0; } } counts.total = counts.agent_runs + counts.knowledge_chunks + counts.archived_runs + counts.archived_chunks; // ── Journalisation (art. 27 — preuve de l'effacement) ──────────────────── await logAccess({ dataSubjectId: subjectId, accessedBy: 'audit_erasure', action: 'delete', resourceType: 'data_subject_records', resourceId: subjectId, purpose: `audit_erasure_${mode}${includeArchives ? '_with_archives' : ''}`, }).catch(() => { }); // non-bloquant const modeLabel = mode === 'anonymize' ? '🛡️ Anonymisation' : '🗑️ Suppression définitive'; return { content: [ { type: 'text', text: `${modeLabel} — **Audit Trail (Loi 25 art. 27/35.3)**\n\n` + `**Sujet :** \`${subjectId}\`\n` + `**Mode :** \`${mode}\`\n` + `**Archives incluses :** ${includeArchives ? 'oui' : 'non'}\n\n` + `**Enregistrements affectés :** ${counts.total}\n` + ` • agent_runs : ${counts.agent_runs}\n` + ` • knowledge_chunks : ${counts.knowledge_chunks}` + (includeArchives ? `\n • archived_runs : ${counts.archived_runs}\n` + ` • archived_chunks : ${counts.archived_chunks}` : ''), }, ], }; } catch (err) { return { content: [ { type: 'text', text: `❌ Erreur audit_erasure: ${err instanceof Error ? err.message : String(err)}`, }, ], isError: true, }; } } //# sourceMappingURL=audit_erasure.js.map