This commit is contained in:
175
packages/core/src/compliance/status-checker.ts
Normal file
175
packages/core/src/compliance/status-checker.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* ComplianceStatusChecker — evaluates AI Act compliance per workspace.
|
||||
*
|
||||
* Checks:
|
||||
* - Art. 12: Automatic event logging (ai_interactions count)
|
||||
* - Art. 14: Human oversight (approval/denial actions recorded)
|
||||
* - Art. 19: Log retention (oldest log >= 6 months ago)
|
||||
* - Art. 26: Deployer monitoring (active monitors)
|
||||
* - Art. 50: Model transparency (models disclosed)
|
||||
*/
|
||||
|
||||
import type { InteractionStore } from './interaction-store.js';
|
||||
import type { ComplianceStatus, ArticleStatus } from './types.js';
|
||||
|
||||
const SIX_MONTHS_MS = 180 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export class ComplianceStatusChecker {
|
||||
private store: InteractionStore;
|
||||
|
||||
constructor(store: InteractionStore) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
/** Evaluate full compliance status for a workspace (or all workspaces if null). */
|
||||
check(workspaceId?: string): ComplianceStatus {
|
||||
const art12 = this.checkArt12(workspaceId);
|
||||
const art14 = this.checkArt14(workspaceId);
|
||||
const art19 = this.checkArt19();
|
||||
const art26 = this.checkArt26();
|
||||
const art50 = this.checkArt50(workspaceId);
|
||||
|
||||
const statuses = [art12.status, art14.status, art19.status, art26.status, art50.status];
|
||||
const overall = statuses.includes('non-compliant') ? 'non-compliant'
|
||||
: statuses.includes('warning') ? 'warning'
|
||||
: 'compliant';
|
||||
|
||||
return {
|
||||
overall,
|
||||
art12Logging: art12,
|
||||
art14Oversight: art14,
|
||||
art19Retention: art19,
|
||||
art26Monitoring: art26,
|
||||
art50Transparency: art50,
|
||||
};
|
||||
}
|
||||
|
||||
/** Art. 12: Automatic recording of events. */
|
||||
private checkArt12(workspaceId?: string): ArticleStatus & { totalInteractions: number } {
|
||||
const total = this.store.count(workspaceId);
|
||||
|
||||
if (total === 0) {
|
||||
// M11: if the DB has been active for >24h with zero interactions, escalate to non-compliant
|
||||
const firstRun = this.store.getFirstRunAt();
|
||||
const activeOver24h = firstRun
|
||||
? (Date.now() - new Date(firstRun).getTime()) > 24 * 60 * 60 * 1000
|
||||
: false;
|
||||
|
||||
return {
|
||||
status: activeOver24h ? 'non-compliant' : 'warning',
|
||||
detail: activeOver24h
|
||||
? 'No interactions logged despite system being active for over 24 hours. Verify logging pipeline.'
|
||||
: 'No interactions logged yet. Logging activates automatically on first AI interaction.',
|
||||
totalInteractions: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'compliant',
|
||||
detail: `${total} interactions logged with full model, token, cost, and tool tracking.`,
|
||||
totalInteractions: total,
|
||||
};
|
||||
}
|
||||
|
||||
/** Art. 14: Human oversight capability. */
|
||||
private checkArt14(workspaceId?: string): ArticleStatus & { humanActions: number; approvalRate: number } {
|
||||
const counts = this.store.getOversightCounts(workspaceId);
|
||||
|
||||
if (counts.total === 0) {
|
||||
return {
|
||||
status: 'compliant',
|
||||
detail: 'Human oversight capabilities available (approval gates, tool deny lists). No oversight actions recorded yet.',
|
||||
humanActions: 0,
|
||||
approvalRate: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const approvalRate = counts.total > 0
|
||||
? Math.round((counts.approved / counts.total) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
status: 'compliant',
|
||||
detail: `${counts.total} human oversight actions: ${counts.approved} approved, ${counts.denied} denied, ${counts.modified} modified.`,
|
||||
humanActions: counts.total,
|
||||
approvalRate,
|
||||
};
|
||||
}
|
||||
|
||||
/** Art. 19: Log retention (minimum 6 months). */
|
||||
private checkArt19(): ArticleStatus & { oldestLogDate: string | null; retentionDays: number } {
|
||||
const oldest = this.store.getOldestTimestamp();
|
||||
|
||||
if (!oldest) {
|
||||
return {
|
||||
status: 'compliant',
|
||||
detail: 'No logs to retain yet. Retention policy is permanent by default.',
|
||||
oldestLogDate: null,
|
||||
retentionDays: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const oldestDate = new Date(oldest);
|
||||
const now = new Date();
|
||||
const retentionMs = now.getTime() - oldestDate.getTime();
|
||||
const retentionDays = Math.floor(retentionMs / (24 * 60 * 60 * 1000));
|
||||
|
||||
// Review Critical #2: the previous expression was a tautology
|
||||
// (`retentionMs >= SIX_MONTHS_MS || retentionDays < 180`) that covered every
|
||||
// non-negative value of retentionDays. A deployment that pruned logs after 30
|
||||
// days still reported compliant.
|
||||
//
|
||||
// Proper fix requires distinguishing 'system is young' from 'logs were pruned'.
|
||||
// We track system age via MindDB's `meta.first_run_at` entry (set on schema init,
|
||||
// backfilled for pre-existing DBs). If the system has been running for 180+ days
|
||||
// but the oldest log is younger than that, something pruned the logs and we
|
||||
// correctly report warning.
|
||||
const firstRun = this.store.getFirstRunAt();
|
||||
const systemAgeMs = firstRun ? now.getTime() - new Date(firstRun).getTime() : retentionMs;
|
||||
const hasBeenRunning6Months = systemAgeMs >= SIX_MONTHS_MS;
|
||||
const logsOlderThan6Months = retentionMs >= SIX_MONTHS_MS;
|
||||
const meetsMinimum = !hasBeenRunning6Months || logsOlderThan6Months;
|
||||
|
||||
return {
|
||||
status: meetsMinimum ? 'compliant' : 'warning',
|
||||
detail: meetsMinimum
|
||||
? hasBeenRunning6Months
|
||||
? `Logs retained since ${oldest.split('T')[0]} (${retentionDays} days). Art. 19 minimum (180 days) met.`
|
||||
: `Logs retained since ${oldest.split('T')[0]} (${retentionDays} days). System is still within its first 180 days — retention compliance will be enforceable after 2026-${(new Date(firstRun ?? now).getMonth() + 7).toString().padStart(2, '0')}.`
|
||||
: `Oldest log: ${oldest.split('T')[0]} (${retentionDays} days) but system is ${Math.floor(systemAgeMs / (24 * 60 * 60 * 1000))} days old. Logs appear to have been pruned — EU AI Act Art. 19 requires 180-day minimum retention.`,
|
||||
oldestLogDate: oldest,
|
||||
retentionDays,
|
||||
};
|
||||
}
|
||||
|
||||
/** Art. 26: Deployer monitoring obligations. */
|
||||
private checkArt26(): ArticleStatus & { activeMonitors: string[] } {
|
||||
// Waggle always has these monitors active
|
||||
const monitors = [
|
||||
'cost_tracking', // CostTracker in packages/agent
|
||||
'tool_logging', // Tool calls logged per interaction
|
||||
'model_identification', // Model recorded per interaction
|
||||
'persona_tracking', // Persona recorded per interaction
|
||||
];
|
||||
|
||||
return {
|
||||
status: 'compliant',
|
||||
detail: `${monitors.length} active monitors: cost, tools, model ID, persona.`,
|
||||
activeMonitors: monitors,
|
||||
};
|
||||
}
|
||||
|
||||
/** Art. 50: Transparency — models disclosed. */
|
||||
private checkArt50(workspaceId?: string): ArticleStatus & { modelsDisclosed: boolean } {
|
||||
const inventory = this.store.getModelInventory(undefined, undefined, workspaceId);
|
||||
const modelsDisclosed = inventory.length > 0;
|
||||
|
||||
return {
|
||||
status: 'compliant',
|
||||
detail: modelsDisclosed
|
||||
? `${inventory.length} model(s) in use, all identified in StatusBar and interaction logs.`
|
||||
: 'Model identification active. Models will be disclosed on first interaction.',
|
||||
modelsDisclosed,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user