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
279 lines • 12.2 kB
JavaScript
/**
* ═══════════════════════════════════════════════════════════════════════════
* Audit Trail — Outil MCP : Registre des traitements (art. 3-3.1 / 35.18)
* ═══════════════════════════════════════════════════════════════════════════
*
* Consulte, crée ou modifie le registre des traitements de renseignements
* personnels (table `processing_registry` dans `overmind_core`).
*
* Liste également les providers LLM documentés depuis PROVIDER_REGISTRY
* (cartographie des transferts hors QC — art. 21-22).
*/
import { z } from 'zod';
import { randomUUID } from 'crypto';
import { getPool } from 'overmind-postgres-mcp';
import { listDocumentedProviders } from '../lib/audit/transfer_map.js';
// ── Schéma Zod ────────────────────────────────────────────────────────────────
export const auditProcessingRegistrySchema = z.object({
action: z
.enum(['list', 'get', 'create', 'update'])
.optional()
.default('list')
.describe('list: tous les traitements + providers documentés | ' +
'get: un traitement par nom | ' +
'create: nouveau traitement | ' +
'update: modifie un traitement existant'),
name: z
.string()
.optional()
.describe("Nom unique du traitement (ex: 'llm_inference'). Requis pour get/create/update."),
purpose: z
.string()
.optional()
.describe('Finalité du traitement (art. 4). Requis pour create.'),
legal_basis: z
.string()
.optional()
.describe("Base légale: 'consent' | 'contract' | 'legitimate_interest' | 'legal_obligation'."),
data_categories: z
.string()
.optional()
.describe('Catégories de RP traitées (CSV: "prompts,embeddings,user_ids").'),
recipients: z
.string()
.optional()
.describe('Destinataires des RP (CSV: "anthropic,PostgreSQL local").'),
retention_days: z
.number()
.int()
.positive()
.optional()
.describe('Durée de rétention active en jours (défaut: .env OVERMIND_AUDIT_RETENTION_DAYS).'),
});
// ── Helpers ──────────────────────────────────────────────────────────────────
function tsNow() {
return Date.now();
}
function formatEntry(e) {
return [
`• **${e.name}** \`${e.id.slice(0, 8)}\``,
` - Finalité: ${e.purpose}`,
` - Base légale: \`${e.legal_basis}\``,
` - Catégories: ${e.data_categories ?? '—'}`,
` - Destinataires: ${e.recipients ?? '—'}`,
` - Rétention: ${e.retention_days} jours`,
].join('\n');
}
function formatProvider(p) {
const doc = p.documented ? '✅' : '⚠️';
return `- ${doc} **${p.name}** — région \`${p.region}\`, mécanisme \`${p.mechanism}\`${p.notes ? ` — ${p.notes}` : ''}`;
}
// ── Tool ─────────────────────────────────────────────────────────────────────
export async function auditProcessingRegistry(args) {
try {
const pool = getPool();
const action = args.action ?? 'list';
// ── LIST ─────────────────────────────────────────────────────────────────
if (action === 'list') {
const res = await pool.query(`SELECT * FROM processing_registry ORDER BY name ASC`);
const providers = listDocumentedProviders().map((p) => ({
name: p.name,
region: p.region,
mechanism: p.mechanism,
documented: p.documented,
notes: p.notes,
}));
const entriesBlock = res.rows.length > 0
? res.rows.map(formatEntry).join('\n\n')
: '_Aucun traitement enregistré._';
const providersBlock = providers.length > 0
? providers.map(formatProvider).join('\n')
: '_Aucun provider documenté._';
return {
content: [
{
type: 'text',
text: [
`📚 **Registre des traitements** (${res.rows.length} entrée(s))`,
'',
entriesBlock,
'',
`🌐 **Cartographie des transferts** (${providers.length} provider(s))`,
'',
providersBlock,
].join('\n'),
},
],
};
}
// ── GET ──────────────────────────────────────────────────────────────────
if (action === 'get') {
if (!args.name) {
return {
content: [{ type: 'text', text: '❌ Paramètre `name` requis pour get.' }],
isError: true,
};
}
const res = await pool.query(`SELECT * FROM processing_registry WHERE name = $1`, [args.name]);
if (res.rows.length === 0) {
return {
content: [
{
type: 'text',
text: `❌ Aucun traitement nommé \`${args.name}\`.`,
},
],
isError: true,
};
}
return {
content: [
{ type: 'text', text: formatEntry(res.rows[0]) },
],
};
}
// ── CREATE ───────────────────────────────────────────────────────────────
if (action === 'create') {
if (!args.name || !args.purpose) {
return {
content: [
{
type: 'text',
text: '❌ `name` et `purpose` sont requis pour create.',
},
],
isError: true,
};
}
const id = randomUUID();
const now = tsNow();
const retentionDays = args.retention_days ??
parseInt(process.env.OVERMIND_AUDIT_RETENTION_DAYS || '30', 10);
try {
await pool.query(`INSERT INTO processing_registry
(id, name, purpose, legal_basis, data_categories, recipients, retention_days, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [
id,
args.name,
args.purpose,
args.legal_basis ?? 'legitimate_interest',
args.data_categories ?? null,
args.recipients ?? null,
retentionDays,
now,
now,
]);
}
catch (err) {
// Conflit d'unicité sur name
if (err instanceof Error && /duplicate|unique/i.test(err.message)) {
return {
content: [
{
type: 'text',
text: `❌ Un traitement nommé \`${args.name}\` existe déjà. Utilisez action="update".`,
},
],
isError: true,
};
}
throw err;
}
return {
content: [
{
type: 'text',
text: `✅ **Traitement créé**: \`${args.name}\`\nID: \`${id}\`\nBase légale: \`${args.legal_basis ?? 'legitimate_interest'}\` | Rétention: ${retentionDays} jours`,
},
],
};
}
// ── UPDATE ───────────────────────────────────────────────────────────────
if (action === 'update') {
if (!args.name) {
return {
content: [
{ type: 'text', text: '❌ `name` requis pour update.' },
],
isError: true,
};
}
const sets = [];
const values = [];
let i = 1;
if (args.purpose !== undefined) {
sets.push(`purpose = $${i++}`);
values.push(args.purpose);
}
if (args.legal_basis !== undefined) {
sets.push(`legal_basis = $${i++}`);
values.push(args.legal_basis);
}
if (args.data_categories !== undefined) {
sets.push(`data_categories = $${i++}`);
values.push(args.data_categories);
}
if (args.recipients !== undefined) {
sets.push(`recipients = $${i++}`);
values.push(args.recipients);
}
if (args.retention_days !== undefined) {
sets.push(`retention_days = $${i++}`);
values.push(args.retention_days);
}
if (sets.length === 0) {
return {
content: [
{
type: 'text',
text: '❌ Aucun champ à mettre à jour (fournir purpose, legal_basis, data_categories, recipients ou retention_days).',
},
],
isError: true,
};
}
sets.push(`updated_at = $${i++}`);
values.push(tsNow());
values.push(args.name); // WHERE name = $N
const res = await pool.query(`UPDATE processing_registry SET ${sets.join(', ')} WHERE name = $${i}`, values);
if (res.rowCount === 0) {
return {
content: [
{
type: 'text',
text: `❌ Aucun traitement nommé \`${args.name}\`.`,
},
],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: `✅ **Traitement mis à jour**: \`${args.name}\` (${sets.length - 1} champ(s))`,
},
],
};
}
// unreachable — action est un enum exhaustif
return {
content: [
{ type: 'text', text: `❌ Action inconnue: ${action}` },
],
isError: true,
};
}
catch (err) {
return {
content: [
{
type: 'text',
text: `❌ Erreur audit_processing_registry: ${err instanceof Error ? err.message : String(err)}`,
},
],
isError: true,
};
}
}
//# sourceMappingURL=audit_processing_registry.js.map