tractatus/.claude/hooks/framework-audit-hook.js
TheFlow 8ee2f73928 feat(framework): implement Phase 3 bidirectional communication architecture
Phase 3.5: Cross-validation between prompt analysis and action analysis
- Added prompt-analyzer-hook.js to store prompt expectations in session state
- Modified framework-audit-hook.js to retrieve and compare prompt vs action
- Implemented cross-validation logic tracking agreements, disagreements, missed flags
- Added validation feedback to systemMessage for real-time guidance

Services enhanced with guidance generation:
- BoundaryEnforcer: _buildGuidance() provides systemMessage for enforcement decisions
- CrossReferenceValidator: Generates guidance for cross-reference conflicts
- MetacognitiveVerifier: Provides guidance on metacognitive verification
- PluralisticDeliberationOrchestrator: Offers guidance on values conflicts

Framework now communicates bidirectionally:
- TO Claude: systemMessage injection with proactive guidance
- FROM Claude: Audit logs with framework_backed_decision metadata

Integration testing: 92% success (23/25 tests passed)
Recent performance: 100% guidance generation for new decisions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 19:45:24 +13:00

531 lines
18 KiB
JavaScript
Executable file

#!/usr/bin/env node
/**
* Tractatus Framework Audit Hook (PreToolUse)
*
* Automatically invokes framework services during Claude Code tool execution
* and logs all decisions to the audit database for dashboard visibility.
*
* Hook Input (JSON via stdin):
* {
* "session_id": "abc123",
* "hook_event_name": "PreToolUse",
* "tool_name": "Write",
* "tool_input": { "file_path": "/path", "content": "..." }
* }
*
* Hook Output (JSON to stdout):
* {
* "hookSpecificOutput": {
* "hookEventName": "PreToolUse",
* "permissionDecision": "allow|deny|ask",
* "permissionDecisionReason": "explanation"
* },
* "continue": true,
* "suppressOutput": false
* }
*
* Exit Codes:
* - 0: Success (allow tool execution)
* - 2: Block tool execution
*/
const path = require('path');
const fs = require('fs');
/**
* Read JSON input from stdin
*/
function readStdin() {
return new Promise((resolve, reject) => {
let data = '';
process.stdin.on('data', chunk => { data += chunk; });
process.stdin.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(new Error('Invalid JSON input'));
}
});
process.stdin.on('error', reject);
});
}
/**
* Output hook response
*/
function outputResponse(decision, reason, systemMessage = null) {
const response = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: decision,
permissionDecisionReason: reason
},
continue: true,
suppressOutput: decision === 'allow'
};
if (systemMessage) {
response.systemMessage = systemMessage;
}
console.log(JSON.stringify(response));
}
/**
* Main hook logic
*/
async function main() {
let input;
try {
input = await readStdin();
} catch (err) {
// Invalid input, allow execution
outputResponse('allow', 'Invalid hook input');
process.exit(0);
}
const { session_id, tool_name, tool_input } = input;
// Skip framework for non-invasive tools
if (['Read', 'Glob', 'Grep'].includes(tool_name)) {
outputResponse('allow', 'Read-only tool, no framework check needed');
process.exit(0);
}
// Connect to MongoDB and invoke framework
const mongoose = require('mongoose');
try {
await mongoose.connect('mongodb://localhost:27017/tractatus_dev', {
serverSelectionTimeoutMS: 2000
});
} catch (err) {
// MongoDB not available, allow execution but log warning
outputResponse('allow', 'Framework unavailable (MongoDB not connected)');
process.exit(0);
}
// Import all 6 framework services
const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service');
const CrossReferenceValidator = require('../../src/services/CrossReferenceValidator.service');
const MetacognitiveVerifier = require('../../src/services/MetacognitiveVerifier.service');
const ContextPressureMonitor = require('../../src/services/ContextPressureMonitor.service');
const InstructionPersistenceClassifier = require('../../src/services/InstructionPersistenceClassifier.service');
const PluralisticDeliberationOrchestrator = require('../../src/services/PluralisticDeliberationOrchestrator.service');
// CRITICAL: Initialize all services so audit logging works
await BoundaryEnforcer.initialize();
await CrossReferenceValidator.initialize();
await MetacognitiveVerifier.initialize();
await ContextPressureMonitor.initialize(session_id);
await InstructionPersistenceClassifier.initialize();
await PluralisticDeliberationOrchestrator.initialize();
const sessionId = session_id || 'claude-code-session';
try {
// Route to framework service based on tool
let decision = 'allow';
let reason = 'Framework check passed';
let systemMessage = null; // PHASE 3: Collect systemMessage
if (tool_name === 'Edit' || tool_name === 'Write') {
const result = await handleFileModification(tool_input, sessionId);
decision = result.decision;
reason = result.reason;
systemMessage = result.systemMessage; // PHASE 3: Capture systemMessage
} else if (tool_name === 'Bash') {
const result = await handleBashCommand(tool_input, sessionId);
decision = result.decision;
reason = result.reason;
systemMessage = result.systemMessage; // PHASE 3: Capture systemMessage
}
// Wait for async audit logging to complete before disconnecting
await new Promise(resolve => setTimeout(resolve, 500));
await mongoose.disconnect();
outputResponse(decision, reason, systemMessage); // PHASE 3: Pass systemMessage
process.exit(decision === 'deny' ? 2 : 0);
} catch (err) {
await mongoose.disconnect();
// Framework error - allow execution but log
outputResponse('allow', `Framework error: ${err.message}`);
process.exit(0);
}
}
/**
* Handle file modifications (Edit, Write tools)
*/
async function handleFileModification(toolInput, sessionId) {
const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service');
const CrossReferenceValidator = require('../../src/services/CrossReferenceValidator.service');
const MetacognitiveVerifier = require('../../src/services/MetacognitiveVerifier.service');
const filePath = toolInput.file_path || toolInput.path || 'unknown';
const content = toolInput.new_string || toolInput.content || '';
// PHASE 3.5: Cross-validate prompt analysis vs action analysis
let promptAnalysis = null;
let crossValidation = {
hasPromptAnalysis: false,
agreements: [],
disagreements: [],
missedFlags: [],
validationScore: 0
};
try {
const sessionStatePath = path.join(__dirname, '..', 'session-state.json');
if (fs.existsSync(sessionStatePath)) {
const sessionState = JSON.parse(fs.readFileSync(sessionStatePath, 'utf-8'));
promptAnalysis = sessionState.promptAnalysis?.latest || null;
if (promptAnalysis) {
crossValidation.hasPromptAnalysis = true;
// Detect action characteristics
const lowerPath = filePath.toLowerCase();
const lowerContent = content.toLowerCase();
const actionFlags = {
isSchemaChange: lowerPath.includes('model') || lowerPath.includes('schema') ||
lowerContent.includes('schema') || lowerContent.includes('mongoose'),
isSecurityChange: lowerPath.includes('auth') || lowerPath.includes('jwt') ||
lowerPath.includes('security') || lowerContent.includes('password'),
isServiceFile: lowerPath.includes('service.js'),
isHookFile: lowerPath.includes('hook'),
isConfigFile: lowerPath.includes('config')
};
// Compare prompt expectations vs actual action
// 1. Schema change validation
if (promptAnalysis.flags.schemaChange === true && actionFlags.isSchemaChange === true) {
crossValidation.agreements.push('schema_change_detected');
} else if (promptAnalysis.flags.schemaChange === true && actionFlags.isSchemaChange === false) {
crossValidation.disagreements.push('prompt_expected_schema_but_action_is_not');
} else if (promptAnalysis.flags.schemaChange === false && actionFlags.isSchemaChange === true) {
crossValidation.missedFlags.push('action_is_schema_but_prompt_didnt_detect');
}
// 2. Security change validation
if (promptAnalysis.flags.securityChange === true && actionFlags.isSecurityChange === true) {
crossValidation.agreements.push('security_change_detected');
} else if (promptAnalysis.flags.securityChange === true && actionFlags.isSecurityChange === false) {
crossValidation.disagreements.push('prompt_expected_security_but_action_is_not');
} else if (promptAnalysis.flags.securityChange === false && actionFlags.isSecurityChange === true) {
crossValidation.missedFlags.push('action_is_security_but_prompt_didnt_detect');
}
// Calculate validation score (0-100)
const totalChecks = crossValidation.agreements.length +
crossValidation.disagreements.length +
crossValidation.missedFlags.length;
if (totalChecks > 0) {
crossValidation.validationScore = Math.round(
(crossValidation.agreements.length / totalChecks) * 100
);
} else {
// No flags to validate - perfect alignment
crossValidation.validationScore = 100;
}
}
}
} catch (err) {
// Non-fatal: Continue without cross-validation
console.error('[Framework Audit] Cross-validation error:', err.message);
}
// PHASE 3: Collect guidance from all framework services
const guidanceMessages = [];
// 1. Boundary enforcement
const action = {
type: 'file_modification',
description: `Modify ${path.basename(filePath)}`,
target: filePath,
content_length: content.length
};
const context = {
sessionId,
tool: 'Edit/Write',
file: filePath
};
const boundaryResult = BoundaryEnforcer.enforce(action, context);
// PHASE 3: Collect guidance from BoundaryEnforcer
if (boundaryResult.guidance && boundaryResult.guidance.systemMessage) {
guidanceMessages.push(boundaryResult.guidance.systemMessage);
}
// If boundary enforcer blocks, deny the action
if (!boundaryResult.allowed) {
return {
decision: 'deny',
reason: boundaryResult.message || 'Boundary violation detected'
};
}
// 1a. PHASE 2: Semantic detection - schema changes
const schemaDetection = BoundaryEnforcer.detectSchemaChange(filePath, content);
if (schemaDetection.isSchemaChange) {
// Schema change detected - invoke CrossReferenceValidator
const schemaValidation = await CrossReferenceValidator.validateSchemaChange({
type: 'schema_modification',
description: `${schemaDetection.isSensitiveCollection ? 'SENSITIVE ' : ''}Schema change in ${path.basename(filePath)}`,
file: filePath,
severity: schemaDetection.severity,
patterns: schemaDetection.detectedPatterns
}, {
...context,
schema_change: true,
sensitive_collection: schemaDetection.isSensitiveCollection
});
// PHASE 3: Collect guidance from schema validation
if (schemaValidation.guidance && schemaValidation.guidance.systemMessage) {
guidanceMessages.push(schemaValidation.guidance.systemMessage);
}
// If critical conflicts found, block the action
if (!schemaValidation.allowed && schemaValidation.criticalConflicts > 0) {
return {
decision: 'deny',
reason: `BLOCKED: ${schemaValidation.recommendation}`,
systemMessage: guidanceMessages.join('\n') // Include collected guidance
};
}
}
// 1b. PHASE 2: Semantic detection - security gradient
const securityGradient = BoundaryEnforcer.detectSecurityGradient(filePath, content);
// 2. HARD BLOCK: instruction-history.json modifications (inst_027)
if (filePath.includes('instruction-history.json')) {
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.'
};
}
// 3. Validate against instructions for governance files
const governanceFiles = [
'CLAUDE.md',
'auth.middleware.js',
'auth.controller.js',
'session-state.json'
];
const isGovernanceFile = governanceFiles.some(f => filePath.includes(f));
if (isGovernanceFile) {
const validateAction = {
type: 'modify_governance_file',
description: `Modifying ${path.basename(filePath)}`,
file: filePath
};
CrossReferenceValidator.validate(validateAction, { ...context, governance: true });
}
// 4. PHASE 2: Graduated security verification based on content analysis
if (securityGradient !== 'ROUTINE') {
const verifyAction = {
type: 'modify_security_file',
description: `[${securityGradient}] Modifying ${path.basename(filePath)}`,
file: filePath,
security_gradient: securityGradient
};
// Determine automated_approval based on security gradient
const canAutoApprove = securityGradient === 'DOCUMENTATION';
const reasoning = canAutoApprove
? `${securityGradient} level change to ${filePath}. Documentation/comment update - automated approval.`
: `${securityGradient} level security modification detected in ${filePath}. Requires elevated scrutiny.`;
const verificationResult = MetacognitiveVerifier.verify(verifyAction, reasoning, {
...context,
security_impact: securityGradient === 'CRITICAL' || securityGradient === 'HIGH',
security_gradient: securityGradient,
automated_approval: canAutoApprove,
requires_human_review: securityGradient === 'CRITICAL'
});
// PHASE 3: Collect guidance from MetacognitiveVerifier
if (verificationResult && verificationResult.guidance && verificationResult.guidance.systemMessage) {
guidanceMessages.push(verificationResult.guidance.systemMessage);
}
}
// 5. Context pressure monitoring (on every tool use)
const ContextPressureMonitor = require('../../src/services/ContextPressureMonitor.service');
ContextPressureMonitor.analyzePressure({
sessionId,
tool: 'Edit/Write',
action: 'file_modification',
file: filePath
});
// 6. Instruction classification (when editing instruction files)
const instructionFiles = ['instruction-history.json', 'CLAUDE.md', 'settings.json'];
const isInstructionFile = instructionFiles.some(f => filePath.includes(f));
if (isInstructionFile) {
const InstructionPersistenceClassifier = require('../../src/services/InstructionPersistenceClassifier.service');
InstructionPersistenceClassifier.classify({
text: `File modification: ${path.basename(filePath)}`,
context: {
...context,
file: filePath,
contentLength: content.length
},
timestamp: new Date(),
source: 'tool_use'
});
}
// 7. Pluralistic deliberation (when value conflicts might occur)
const valueConflictFiles = ['auth', 'security', 'privacy', 'accessibility', 'performance'];
const hasValueConflict = valueConflictFiles.some(keyword => filePath.toLowerCase().includes(keyword));
if (hasValueConflict) {
const PluralisticDeliberationOrchestrator = require('../../src/services/PluralisticDeliberationOrchestrator.service');
const deliberationResult = PluralisticDeliberationOrchestrator.analyzeConflict({
type: 'file_modification',
description: `Modifying file with potential value conflicts: ${path.basename(filePath)}`,
file: filePath
}, {
...context,
value_domains: valueConflictFiles.filter(k => filePath.toLowerCase().includes(k))
});
// PHASE 3: Collect guidance from PluralisticDeliberationOrchestrator
if (deliberationResult && deliberationResult.guidance && deliberationResult.guidance.systemMessage) {
guidanceMessages.push(deliberationResult.guidance.systemMessage);
}
}
// PHASE 3.5: Add cross-validation feedback to systemMessage
if (crossValidation.hasPromptAnalysis) {
if (crossValidation.missedFlags.length > 0) {
guidanceMessages.push(
`\n⚠️ CROSS-VALIDATION: Prompt analysis missed ${crossValidation.missedFlags.length} flag(s):\n` +
crossValidation.missedFlags.map(f => `${f}`).join('\n')
);
}
if (crossValidation.disagreements.length > 0) {
guidanceMessages.push(
`\n⚠️ CROSS-VALIDATION: ${crossValidation.disagreements.length} disagreement(s) between prompt and action:\n` +
crossValidation.disagreements.map(d => `${d}`).join('\n')
);
}
if (crossValidation.validationScore < 80 && crossValidation.validationScore > 0) {
guidanceMessages.push(
`\n📊 VALIDATION SCORE: ${crossValidation.validationScore}% (prompt-action alignment)`
);
}
}
// PHASE 3: Aggregate guidance and return with systemMessage
const result = {
decision: 'allow',
reason: `Framework audit complete: ${path.basename(filePath)}`,
crossValidation // PHASE 3.5: Include validation data
};
// Add systemMessage if we have any guidance
if (guidanceMessages.length > 0) {
result.systemMessage = guidanceMessages.join('\n');
}
return result;
}
/**
* Handle Bash command execution
*/
async function handleBashCommand(toolInput, sessionId) {
const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service');
const command = toolInput.command || '';
// PHASE 3: Collect guidance
const guidanceMessages = [];
// Check for cross-project commands
const crossProjectPatterns = [
'/family-history/',
'/sydigital/',
'cd ../family-history',
'cd ../sydigital'
];
const isCrossProject = crossProjectPatterns.some(pattern => command.includes(pattern));
const action = {
type: 'bash_command',
description: isCrossProject ? 'Cross-project bash command' : 'Bash command execution',
target: command.substring(0, 100),
cross_project: isCrossProject
};
const context = {
sessionId,
tool: 'Bash',
command: command.substring(0, 200)
};
const result = BoundaryEnforcer.enforce(action, context);
// PHASE 3: Collect guidance from BoundaryEnforcer
if (result.guidance && result.guidance.systemMessage) {
guidanceMessages.push(result.guidance.systemMessage);
}
if (!result.allowed) {
return {
decision: 'deny',
reason: result.message || 'Bash command blocked by BoundaryEnforcer',
systemMessage: guidanceMessages.join('\n') // Include collected guidance
};
}
// Context pressure monitoring for Bash commands
const ContextPressureMonitor = require('../../src/services/ContextPressureMonitor.service');
ContextPressureMonitor.analyzePressure({
sessionId,
tool: 'Bash',
action: 'bash_command',
command: command.substring(0, 100)
});
// PHASE 3: Return with aggregated guidance
const response = {
decision: 'allow',
reason: 'Bash command allowed'
};
if (guidanceMessages.length > 0) {
response.systemMessage = guidanceMessages.join('\n');
}
return response;
}
// Run hook
main().catch(err => {
outputResponse('allow', `Fatal error: ${err.message}`);
process.exit(0);
});