tractatus/.claude/hooks/framework-audit-hook.js
TheFlow 8210876421 feat(blog): integrate Tractatus framework governance into blog publishing
Implements architectural enforcement of governance rules (inst_016/017/018/079)
for all external communications. Publication blocked at API level if violations
detected.

New Features:
- Framework content checker script with pattern matching for prohibited terms
- Admin UI displays framework violations with severity indicators
- Manual "Check Framework" button for pre-publication validation
- API endpoint /api/blog/check-framework for real-time content analysis

Governance Rules Added:
- inst_078: "ff" trigger for manual framework invocation in conversations
- inst_079: Dark patterns prohibition (sovereignty principle)
- inst_080: Open source commitment enforcement (community principle)
- inst_081: Pluralism principle with indigenous framework recognition

Session Management:
- Fix session-init.js infinite loop (removed early return after tests)
- Add session-closedown.js for comprehensive session handoff
- Refactor check-csp-violations.js to prevent parent process exit

Framework Services:
- Enhanced PluralisticDeliberationOrchestrator with audit logging
- Updated all 6 services with consistent initialization patterns
- Added framework invocation scripts for blog content validation

Files: blog.controller.js:1211-1305, blog.routes.js:77-82,
blog-curation.html:61-72, blog-curation.js:320-446

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-25 08:47:31 +13:00

344 lines
10 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';
if (tool_name === 'Edit' || tool_name === 'Write') {
const result = await handleFileModification(tool_input, sessionId);
decision = result.decision;
reason = result.reason;
} else if (tool_name === 'Bash') {
const result = await handleBashCommand(tool_input, sessionId);
decision = result.decision;
reason = result.reason;
}
// Wait for async audit logging to complete before disconnecting
await new Promise(resolve => setTimeout(resolve, 500));
await mongoose.disconnect();
outputResponse(decision, reason);
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 || '';
// 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);
// If boundary enforcer blocks, deny the action
if (!boundaryResult.allowed) {
return {
decision: 'deny',
reason: boundaryResult.message || 'Boundary violation detected'
};
}
// 2. Validate against instructions for governance files
const governanceFiles = [
'CLAUDE.md',
'instruction-history.json',
'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 });
}
// 3. Metacognitive verification for security-critical files
const securityFiles = ['auth', 'security', 'credential', 'jwt', 'password', 'secret'];
const isSecurityFile = securityFiles.some(keyword => filePath.toLowerCase().includes(keyword));
if (isSecurityFile) {
const verifyAction = {
type: 'modify_security_file',
description: `Modifying security-critical file: ${filePath}`,
file: filePath
};
const reasoning = `Modifying security-critical file: ${filePath}. Automated approval for documentation/comments.`;
MetacognitiveVerifier.verify(verifyAction, reasoning, {
...context,
security_impact: true,
automated_approval: true
});
}
// 4. 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
});
// 5. 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'
});
}
// 6. 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');
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))
});
}
return {
decision: 'allow',
reason: `Framework audit complete: ${path.basename(filePath)}`
};
}
/**
* Handle Bash command execution
*/
async function handleBashCommand(toolInput, sessionId) {
const BoundaryEnforcer = require('../../src/services/BoundaryEnforcer.service');
const command = toolInput.command || '';
// 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);
if (!result.allowed) {
return {
decision: 'deny',
reason: result.message || 'Bash command blocked by BoundaryEnforcer'
};
}
// 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)
});
return {
decision: 'allow',
reason: 'Bash command allowed'
};
}
// Run hook
main().catch(err => {
outputResponse('allow', `Fatal error: ${err.message}`);
process.exit(0);
});