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