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
212 lines • 9.81 kB
JavaScript
/**
* ═══════════════════════════════════════════════════════════════════════════
* Audit Trail — Gestion du consentement (art. 8.1-8.2)
* ═══════════════════════════════════════════════════════════════════════════
*
* Trois actions :
* - `grant` : enregistre un nouveau consentement dans consent_records
* - `revoke` : marque withdrawn_at = now() sur le consentement actif
* - `check` : vérifie la validité d'un consentement pour une finalité
*
* Le consentement doit être libre, éclairé, spécifique à une finalité (art. 8.1)
* et peut être révoqué à tout moment (art. 8.2). Une preuve (evidence) peut être
* attachée pour audit (hash du message Discord, log, etc.).
*/
import { randomUUID } from 'crypto';
import { z } from 'zod';
import { getPool } from 'overmind-postgres-mcp';
import { logAccess } from '../lib/audit/guard.js';
export const auditConsentSchema = z.object({
action: z
.enum(['grant', 'revoke', 'check'])
.describe("'grant' : accorder | 'revoke' : révoquer | 'check' : vérifier"),
data_subject_id: z.string().min(1).describe('Identifiant pseudonymisé du sujet'),
purpose: z
.string()
.optional()
.default('agent_execution')
.describe("Finalité du traitement (ex: 'agent_execution', 'memory_storage', 'embedding')"),
legal_basis: z
.enum(['consent', 'contract', 'legitimate_interest', 'legal_obligation'])
.optional()
.default('consent')
.describe('Base légale invoquée'),
expires_in_days: z
.number()
.int()
.min(1)
.optional()
.describe('Durée de validité en jours (optionnel, sinon perpétuel tant que non révoqué)'),
evidence: z
.string()
.optional()
.describe('Preuve du consentement (hash du message, ID log, etc.) pour audit'),
});
const MS_PER_DAY = 24 * 60 * 60 * 1000;
export async function auditConsent(args) {
try {
const pool = getPool();
const subjectId = args.data_subject_id;
const purpose = args.purpose;
const now = Date.now();
// ── Action : GRANT ──────────────────────────────────────────────────────
if (args.action === 'grant') {
const consentId = randomUUID();
const expiresAt = args.expires_in_days
? now + args.expires_in_days * MS_PER_DAY
: null;
// S'assurer que le sujet existe dans data_subjects (upsert minimal)
await pool.query(`INSERT INTO data_subjects (id, display_name, source, created_at, metadata)
VALUES ($1, NULL, 'api', $2, NULL)
ON CONFLICT (id) DO NOTHING`, [subjectId, now]);
await pool.query(`INSERT INTO consent_records
(id, data_subject_id, purpose, legal_basis, granted_at, expires_at, withdrawn_at, evidence)
VALUES ($1, $2, $3, $4, $5, $6, NULL, $7)`, [
consentId,
subjectId,
purpose,
args.legal_basis,
now,
expiresAt,
args.evidence ?? null,
]);
await logAccess({
dataSubjectId: subjectId,
accessedBy: 'audit_consent',
action: 'write',
resourceType: 'consent_record',
resourceId: consentId,
purpose: `audit_consent_grant:${purpose}`,
}).catch(() => { });
return {
content: [
{
type: 'text',
text: `✅ **Consentement accordé** — Audit Trail (Loi 25 art. 8.1)\n\n` +
`**Sujet :** \`${subjectId}\`\n` +
`**Consent ID :** \`${consentId}\`\n` +
`**Finalité :** ${purpose}\n` +
`**Base légale :** ${args.legal_basis}\n` +
`**Expire :** ${expiresAt ? new Date(expiresAt).toISOString() : 'jamais (jusqu\'à révocation)'}\n` +
(args.evidence ? `**Preuve :** \`${args.evidence}\`\n` : ''),
},
],
};
}
// ── Action : REVOKE ─────────────────────────────────────────────────────
if (args.action === 'revoke') {
// Révoque le consentement le plus récent pour cette finalité
const res = await pool.query(`UPDATE consent_records
SET withdrawn_at = $3
WHERE data_subject_id = $1
AND purpose = $2
AND withdrawn_at IS NULL
RETURNING id, granted_at, expires_at`, [subjectId, purpose, now]);
const revokedCount = res.rowCount ?? 0;
await logAccess({
dataSubjectId: subjectId,
accessedBy: 'audit_consent',
action: 'write',
resourceType: 'consent_record',
resourceId: revokedCount > 0 ? res.rows[0].id : 'none',
purpose: `audit_consent_revoke:${purpose}`,
}).catch(() => { });
if (revokedCount === 0) {
return {
content: [
{
type: 'text',
text: `⚠️ **Aucun consentement actif à révoquer**\n\n` +
`Sujet : \`${subjectId}\` | Finalité : \`${purpose}\`\n` +
`_Aucun enregistrement trouvé ou déjà révoqué._`,
},
],
};
}
return {
content: [
{
type: 'text',
text: `🚫 **Consentement révoqué** — Audit Trail (Loi 25 art. 8.2)\n\n` +
`**Sujet :** \`${subjectId}\`\n` +
`**Finalité :** ${purpose}\n` +
`**Révoqué le :** ${new Date(now).toISOString()}\n` +
`**Enregistrements affectés :** ${revokedCount}`,
},
],
};
}
// ── Action : CHECK ──────────────────────────────────────────────────────
const res = await pool.query(`SELECT id, purpose, legal_basis, granted_at, expires_at, withdrawn_at, evidence
FROM consent_records
WHERE data_subject_id = $1 AND purpose = $2
ORDER BY granted_at DESC
LIMIT 1`, [subjectId, purpose]);
await logAccess({
dataSubjectId: subjectId,
accessedBy: 'audit_consent',
action: 'read',
resourceType: 'consent_record',
resourceId: purpose,
purpose: 'audit_consent_check',
}).catch(() => { });
if (res.rows.length === 0) {
return {
content: [
{
type: 'text',
text: `❓ **Aucun consentement trouvé**\n\n` +
`Sujet : \`${subjectId}\` | Finalité : \`${purpose}\`\n` +
`_Aucun consentement enregistré pour cette finalité._`,
},
],
};
}
const consent = res.rows[0];
const grantedAt = parseInt(consent.granted_at, 10);
const expiresAt = consent.expires_at ? parseInt(consent.expires_at, 10) : null;
const withdrawnAt = consent.withdrawn_at ? parseInt(consent.withdrawn_at, 10) : null;
let status;
let statusEmoji;
if (withdrawnAt && withdrawnAt <= now) {
status = 'révoqué';
statusEmoji = '🚫';
}
else if (expiresAt && expiresAt <= now) {
status = 'expiré';
statusEmoji = '⏰';
}
else {
status = 'valide';
statusEmoji = '✅';
}
return {
content: [
{
type: 'text',
text: `${statusEmoji} **Consentement ${status}**\n\n` +
`**Sujet :** \`${subjectId}\`\n` +
`**Consent ID :** \`${consent.id}\`\n` +
`**Finalité :** ${consent.purpose}\n` +
`**Base légale :** ${consent.legal_basis}\n` +
`**Accordé le :** ${new Date(grantedAt).toISOString()}\n` +
(expiresAt ? `**Expire le :** ${new Date(expiresAt).toISOString()}\n` : '') +
(withdrawnAt ? `**Révoqué le :** ${new Date(withdrawnAt).toISOString()}\n` : '') +
(consent.evidence ? `**Preuve :** \`${consent.evidence}\`\n` : ''),
},
],
};
}
catch (err) {
return {
content: [
{
type: 'text',
text: `❌ Erreur audit_consent: ${err instanceof Error ? err.message : String(err)}`,
},
],
isError: true,
};
}
}
//# sourceMappingURL=audit_consent.js.map