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>
531 lines
18 KiB
JavaScript
Executable file
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);
|
|
});
|