Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
/**
* Human-in-the-Loop (HITL) System
* Approval workflow for autonomous operations
*/
export type RiskLevel = 'safe' | 'medium' | 'high' | 'critical';
export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'expired';
export interface TaskApproval {
id: string;
taskId: string;
taskType: string;
description: string;
riskLevel: RiskLevel;
requestedBy: string;
requestedAt: Date;
status: ApprovalStatus;
approvedBy?: string;
approvedAt?: Date;
rejectionReason?: string;
expiresAt: Date;
metadata: Record<string, any>;
}
export interface ApprovalRule {
taskType: string;
riskLevel: RiskLevel;
requiresApproval: boolean;
approvers: string[];
timeout: number; // milliseconds
}
export class HumanInTheLoopSystem {
private approvalQueue: Map<string, TaskApproval> = new Map();
private approvalRules: Map<string, ApprovalRule> = new Map();
private auditTrail: TaskApproval[] = [];
private killSwitchActive: boolean = false;
private notificationCallbacks: Set<(approval: TaskApproval) => void> = new Set();
constructor() {
this.initializeDefaultRules();
this.startExpirationChecker();
}
/**
* Initialize default approval rules
*/
private initializeDefaultRules(): void {
const defaultRules: ApprovalRule[] = [
{
taskType: 'data_deletion',
riskLevel: 'critical',
requiresApproval: true,
approvers: ['admin'],
timeout: 24 * 60 * 60 * 1000, // 24 hours
},
{
taskType: 'external_api_call',
riskLevel: 'high',
requiresApproval: true,
approvers: ['admin', 'supervisor'],
timeout: 4 * 60 * 60 * 1000, // 4 hours
},
{
taskType: 'data_modification',
riskLevel: 'medium',
requiresApproval: true,
approvers: ['admin', 'supervisor', 'user'],
timeout: 2 * 60 * 60 * 1000, // 2 hours
},
{
taskType: 'read_operation',
riskLevel: 'safe',
requiresApproval: false,
approvers: [],
timeout: 0,
},
];
defaultRules.forEach(rule => {
this.approvalRules.set(rule.taskType, rule);
});
}
/**
* Classify task risk level
*/
classifyRisk(taskType: string, taskData: any): RiskLevel {
// Check if task type has predefined risk
const rule = this.approvalRules.get(taskType);
if (rule) {
return rule.riskLevel;
}
// Heuristic-based classification
const riskFactors = {
hasExternalCall: taskData.externalCall ? 2 : 0,
hasDataModification: taskData.modifiesData ? 2 : 0,
hasDataDeletion: taskData.deletesData ? 3 : 0,
affectsMultipleUsers: taskData.userCount > 10 ? 1 : 0,
hasCost: taskData.estimatedCost > 100 ? 1 : 0,
};
const totalRisk = Object.values(riskFactors).reduce((sum, val) => sum + val, 0);
if (totalRisk >= 6) return 'critical';
if (totalRisk >= 4) return 'high';
if (totalRisk >= 2) return 'medium';
return 'safe';
}
/**
* Request approval for a task
*/
async requestApproval(
taskId: string,
taskType: string,
description: string,
requestedBy: string,
metadata: Record<string, any> = {}
): Promise<TaskApproval> {
// Check kill switch
if (this.killSwitchActive) {
throw new Error('Kill switch is active - all autonomous operations are disabled');
}
// Classify risk
const riskLevel = this.classifyRisk(taskType, metadata);
// Check if approval is required
const rule = this.approvalRules.get(taskType);
if (!rule?.requiresApproval && riskLevel === 'safe') {
// Auto-approve safe tasks
return this.createAutoApprovedTask(taskId, taskType, description, requestedBy, metadata);
}
// Create approval request
const approval: TaskApproval = {
id: this.generateId(),
taskId,
taskType,
description,
riskLevel,
requestedBy,
requestedAt: new Date(),
status: 'pending',
expiresAt: new Date(Date.now() + (rule?.timeout || 60 * 60 * 1000)),
metadata,
};
this.approvalQueue.set(approval.id, approval);
this.auditTrail.push(approval);
// Notify approvers
this.notifyApprovers(approval);
console.log(`📋 Approval requested: ${approval.id} (${riskLevel})`);
return approval;
}
/**
* Approve a task
*/
async approve(approvalId: string, approvedBy: string, reason?: string): Promise<TaskApproval> {
const approval = this.approvalQueue.get(approvalId);
if (!approval) {
throw new Error(`Approval ${approvalId} not found`);
}
if (approval.status !== 'pending') {
throw new Error(`Approval ${approvalId} is already ${approval.status}`);
}
if (new Date() > approval.expiresAt) {
approval.status = 'expired';
throw new Error(`Approval ${approvalId} has expired`);
}
// Check if approver is authorized
const rule = this.approvalRules.get(approval.taskType);
if (rule && !rule.approvers.includes(approvedBy)) {
throw new Error(`${approvedBy} is not authorized to approve ${approval.taskType}`);
}
approval.status = 'approved';
approval.approvedBy = approvedBy;
approval.approvedAt = new Date();
this.approvalQueue.delete(approvalId);
console.log(`✅ Approval granted: ${approvalId} by ${approvedBy}`);
return approval;
}
/**
* Reject a task
*/
async reject(approvalId: string, rejectedBy: string, reason: string): Promise<TaskApproval> {
const approval = this.approvalQueue.get(approvalId);
if (!approval) {
throw new Error(`Approval ${approvalId} not found`);
}
if (approval.status !== 'pending') {
throw new Error(`Approval ${approvalId} is already ${approval.status}`);
}
approval.status = 'rejected';
approval.approvedBy = rejectedBy;
approval.approvedAt = new Date();
approval.rejectionReason = reason;
this.approvalQueue.delete(approvalId);
console.log(`❌ Approval rejected: ${approvalId} by ${rejectedBy}`);
return approval;
}
/**
* Get pending approvals
*/
getPendingApprovals(approver?: string): TaskApproval[] {
const pending = Array.from(this.approvalQueue.values());
if (!approver) return pending;
return pending.filter(approval => {
const rule = this.approvalRules.get(approval.taskType);
return rule?.approvers.includes(approver);
});
}
/**
* Get approval by ID
*/
getApproval(approvalId: string): TaskApproval | undefined {
return this.approvalQueue.get(approvalId) ||
this.auditTrail.find(a => a.id === approvalId);
}
/**
* Get audit trail
*/
getAuditTrail(filters?: {
taskType?: string;
riskLevel?: RiskLevel;
status?: ApprovalStatus;
since?: Date;
}): TaskApproval[] {
let trail = this.auditTrail;
if (filters) {
if (filters.taskType) {
trail = trail.filter(a => a.taskType === filters.taskType);
}
if (filters.riskLevel) {
trail = trail.filter(a => a.riskLevel === filters.riskLevel);
}
if (filters.status) {
trail = trail.filter(a => a.status === filters.status);
}
if (filters.since) {
trail = trail.filter(a => a.requestedAt >= filters.since!);
}
}
return trail;
}
/**
* Activate kill switch
*/
activateKillSwitch(activatedBy: string, reason: string): void {
this.killSwitchActive = true;
console.log(`🚨 KILL SWITCH ACTIVATED by ${activatedBy}: ${reason}`);
// Reject all pending approvals
Array.from(this.approvalQueue.values()).forEach(approval => {
this.reject(approval.id, 'system', `Kill switch activated: ${reason}`);
});
}
/**
* Deactivate kill switch
*/
deactivateKillSwitch(deactivatedBy: string): void {
this.killSwitchActive = false;
console.log(`✅ Kill switch deactivated by ${deactivatedBy}`);
}
/**
* Check if kill switch is active
*/
isKillSwitchActive(): boolean {
return this.killSwitchActive;
}
/**
* Subscribe to approval notifications
*/
onApprovalRequest(callback: (approval: TaskApproval) => void): () => void {
this.notificationCallbacks.add(callback);
return () => this.notificationCallbacks.delete(callback);
}
/**
* Notify approvers
*/
private notifyApprovers(approval: TaskApproval): void {
this.notificationCallbacks.forEach(callback => {
try {
callback(approval);
} catch (error) {
console.error('Notification callback error:', error);
}
});
}
/**
* Create auto-approved task
*/
private createAutoApprovedTask(
taskId: string,
taskType: string,
description: string,
requestedBy: string,
metadata: Record<string, any>
): TaskApproval {
const approval: TaskApproval = {
id: this.generateId(),
taskId,
taskType,
description,
riskLevel: 'safe',
requestedBy,
requestedAt: new Date(),
status: 'approved',
approvedBy: 'system',
approvedAt: new Date(),
expiresAt: new Date(Date.now() + 1000),
metadata,
};
this.auditTrail.push(approval);
return approval;
}
/**
* Start expiration checker
*/
private startExpirationChecker(): void {
setInterval(() => {
const now = new Date();
Array.from(this.approvalQueue.values()).forEach(approval => {
if (now > approval.expiresAt && approval.status === 'pending') {
approval.status = 'expired';
this.approvalQueue.delete(approval.id);
console.log(`⏰ Approval expired: ${approval.id}`);
}
});
}, 60 * 1000); // Check every minute
}
/**
* Get statistics
*/
getStatistics(): {
totalRequests: number;
pending: number;
approved: number;
rejected: number;
expired: number;
byRiskLevel: Record<RiskLevel, number>;
} {
const stats = {
totalRequests: this.auditTrail.length,
pending: 0,
approved: 0,
rejected: 0,
expired: 0,
byRiskLevel: {
safe: 0,
medium: 0,
high: 0,
critical: 0,
} as Record<RiskLevel, number>,
};
this.auditTrail.forEach(approval => {
stats[approval.status]++;
stats.byRiskLevel[approval.riskLevel]++;
});
stats.pending = this.approvalQueue.size;
return stats;
}
private generateId(): string {
return `approval_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
export const hitlSystem = new HumanInTheLoopSystem();