diff --git a/.claude/hooks/framework-audit-hook.js b/.claude/hooks/framework-audit-hook.js index d1567ef9..76f5f816 100755 --- a/.claude/hooks/framework-audit-hook.js +++ b/.claude/hooks/framework-audit-hook.js @@ -72,6 +72,40 @@ function outputResponse(decision, reason, systemMessage = null) { console.log(JSON.stringify(response)); } +/** + * Log denial to audit database + */ +async function logDenial(toolName, toolInput, sessionId, violations, reason, metadata = {}) { + try { + const AuditLog = require('../../src/models/AuditLog.model'); + + const auditEntry = new AuditLog({ + sessionId: sessionId || 'unknown', + action: 'hook_denial', + allowed: false, + rulesChecked: violations.map(v => v.ruleId).filter(Boolean), + violations: violations, + metadata: { + ...metadata, + tool: toolName, + file_path: toolInput.file_path || toolInput.path, + command: toolInput.command, + denial_reason: reason, + hook: 'framework-audit-hook' + }, + domain: 'SYSTEM', + service: 'PreToolUseHook', + environment: 'development', + timestamp: new Date() + }); + + await auditEntry.save(); + } catch (err) { + // Non-fatal: Continue with denial even if logging fails + console.error('[Framework Audit] Failed to log denial:', err.message); + } +} + /** * Main hook logic */ @@ -269,6 +303,18 @@ async function handleFileModification(toolInput, sessionId) { // If boundary enforcer blocks, deny the action if (!boundaryResult.allowed) { + const violations = boundaryResult.violations || [{ + ruleId: boundaryResult.ruleId || 'boundary_violation', + ruleText: boundaryResult.message || 'Boundary violation detected', + severity: 'HIGH', + details: boundaryResult.message + }]; + + await logDenial('Edit/Write', { file_path: filePath }, sessionId, violations, boundaryResult.message, { + boundary: boundaryResult.boundary, + domain: boundaryResult.domain + }); + return { decision: 'deny', reason: boundaryResult.message || 'Boundary violation detected' @@ -299,6 +345,19 @@ async function handleFileModification(toolInput, sessionId) { // If critical conflicts found, block the action if (!schemaValidation.allowed && schemaValidation.criticalConflicts > 0) { + const violations = schemaValidation.violations || [{ + ruleId: 'schema_conflict', + ruleText: schemaValidation.recommendation, + severity: 'CRITICAL', + details: `Critical schema conflicts: ${schemaValidation.criticalConflicts}` + }]; + + await logDenial('Edit/Write', { file_path: filePath }, sessionId, violations, schemaValidation.recommendation, { + schema_change: true, + sensitive_collection: schemaDetection.isSensitiveCollection, + critical_conflicts: schemaValidation.criticalConflicts + }); + return { decision: 'deny', reason: `BLOCKED: ${schemaValidation.recommendation}`, @@ -312,6 +371,17 @@ async function handleFileModification(toolInput, sessionId) { // 2. HARD BLOCK: instruction-history.json modifications (inst_027) if (filePath.includes('instruction-history.json')) { + const violations = [{ + ruleId: 'inst_027', + ruleText: 'NEVER modify instruction-history.json without explicit human approval', + severity: 'CRITICAL', + details: 'Manual edits risk corrupting the governance system. Use scripts/add-instruction.js instead.' + }]; + + await logDenial('Edit/Write', { file_path: filePath }, sessionId, violations, 'inst_027 violation', { + governance_file: true + }); + return { decision: 'deny', reason: 'BLOCKED by inst_027: NEVER modify instruction-history.json without explicit human approval. Use scripts/add-instruction.js or similar tools instead. Manual edits risk corrupting the governance system.' @@ -495,6 +565,19 @@ async function handleBashCommand(toolInput, sessionId) { } if (!result.allowed) { + const violations = result.violations || [{ + ruleId: result.ruleId || 'bash_blocked', + ruleText: result.message || 'Bash command blocked by BoundaryEnforcer', + severity: 'HIGH', + details: result.message + }]; + + await logDenial('Bash', { command }, sessionId, violations, result.message, { + cross_project: isCrossProject, + boundary: result.boundary, + domain: result.domain + }); + return { decision: 'deny', reason: result.message || 'Bash command blocked by BoundaryEnforcer', diff --git a/public/js/admin/audit-analytics.js b/public/js/admin/audit-analytics.js index b725f0e6..b3ebd95b 100644 --- a/public/js/admin/audit-analytics.js +++ b/public/js/admin/audit-analytics.js @@ -121,7 +121,8 @@ function updateSummaryCards() { const totalDecisions = auditData.length; const allowedCount = auditData.filter(d => d.allowed).length; const blockedCount = auditData.filter(d => !d.allowed).length; - const violationsCount = auditData.filter(d => d.violations && d.violations.length > 0).length; + // Count total violations across all decisions (not just decisions with violations) + const violationsCount = auditData.reduce((sum, d) => sum + (d.violations ? d.violations.length : 0), 0); const servicesSet = new Set(auditData.map(d => d.service).filter(s => s && s !== 'unknown')); document.getElementById('total-decisions').textContent = totalDecisions; @@ -304,8 +305,19 @@ async function renderBusinessIntelligence() { progressBar.style.width = maturityScore + '%'; // PHASE 3.4: Framework Participation Rate + // All decisions from framework services represent framework participation + const frameworkServices = [ + 'FileEditHook', + 'BoundaryEnforcer', + 'ContextPressureMonitor', + 'MetacognitiveVerifier', + 'CrossReferenceValidator', + 'InstructionPersistenceClassifier', + 'PluralisticDeliberationOrchestrator' + ]; + const frameworkBackedDecisions = auditData.filter(d => - d.metadata && d.metadata.framework_backed_decision === true + frameworkServices.includes(d.service) ); const participationRate = auditData.length > 0 ? ((frameworkBackedDecisions.length / auditData.length) * 100).toFixed(1) @@ -356,11 +368,13 @@ async function renderBusinessIntelligence() { } // Team Comparison (AI vs Human) + // Use same framework services list defined above const aiDecisions = auditData.filter(d => - d.service === 'FileEditHook' || d.service === 'BoundaryEnforcer' || - d.service === 'ContextPressureMonitor' || d.service === 'MetacognitiveVerifier' + frameworkServices.includes(d.service) + ); + const humanDecisions = auditData.filter(d => + !frameworkServices.includes(d.service) && d.service && d.service !== 'unknown' ); - const humanDecisions = auditData.filter(d => !aiDecisions.includes(d)); const comparisonEl = document.getElementById('team-comparison'); comparisonEl.innerHTML = ''; diff --git a/src/controllers/audit.controller.js b/src/controllers/audit.controller.js index 1ba1c0f4..99269c59 100644 --- a/src/controllers/audit.controller.js +++ b/src/controllers/audit.controller.js @@ -105,7 +105,7 @@ async function getAuditAnalytics(req, res) { total: decisions.length, allowed: allowedDecisions.length, blocked: blockedDecisions.length, - violations: decisions.filter(d => d.violations && d.violations.length > 0).length, + violations: decisions.reduce((sum, d) => sum + (d.violations ? d.violations.length : 0), 0), // Block metrics blockRate: decisions.length > 0 ? ((blockedDecisions.length / decisions.length) * 100).toFixed(1) : 0, @@ -345,17 +345,23 @@ async function getAuditAnalytics(req, res) { // === AI vs HUMAN PERFORMANCE === - // Detect AI vs Human based on service patterns - // AI = FileEditHook, Framework services - // Human = Manual overrides, direct database operations + // All framework services are AI-assisted decisions + // Framework services: ALL decisions in audit logs are framework-backed + const frameworkServices = [ + 'FileEditHook', + 'BoundaryEnforcer', + 'ContextPressureMonitor', + 'MetacognitiveVerifier', + 'CrossReferenceValidator', + 'InstructionPersistenceClassifier', + 'PluralisticDeliberationOrchestrator' + ]; + const aiDecisions = decisions.filter(d => - d.service === 'FileEditHook' || - d.service === 'BoundaryEnforcer' || - d.service === 'ContextPressureMonitor' || - d.service === 'MetacognitiveVerifier' + frameworkServices.includes(d.service) ); const humanDecisions = decisions.filter(d => - !aiDecisions.includes(d) + !frameworkServices.includes(d.service) && d.service && d.service !== 'unknown' ); const aiBlocked = aiDecisions.filter(d => !d.allowed).length; @@ -376,6 +382,11 @@ async function getAuditAnalytics(req, res) { } }; + // Calculate overall block rate (needed for maturity score) + const totalDecisions = decisions.length; + const totalBlocks = blockedDecisions.length; + const avgBlockRate = totalDecisions > 0 ? (totalBlocks / totalDecisions) : 0; + // Framework Maturity Score (0-100) // Based on: block rate trend, severity distribution, learning curve const recentDays = 7; @@ -411,9 +422,6 @@ async function getAuditAnalytics(req, res) { }; // ROI Projections - const totalDecisions = decisions.length; - const totalBlocks = blockedDecisions.length; - const avgBlockRate = totalDecisions > 0 ? (totalBlocks / totalDecisions) : 0; analytics.roiProjections = { // Current period metrics