@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
461 lines (460 loc) • 17 kB
JavaScript
/**
* HITL (Human-in-the-Loop) Manager
*
* Central orchestrator for all HITL confirmation workflows.
* Manages user confirmation requests, timeouts, and argument modifications
* for enterprise-grade AI safety.
*/
import { EventEmitter } from "events";
import { randomUUID } from "crypto";
import { HITLTimeoutError, HITLConfigurationError } from "./hitlErrors.js";
import { logger } from "../utils/logger.js";
// Default configuration constants
const DEFAULT_TIMEOUT = 30000; // 30 seconds
const DEFAULT_ALLOW_MODIFICATION = false;
/**
* HITLManager - Central orchestrator for Human-in-the-Loop safety mechanisms
*
* Features:
* - Real-time user confirmation via events
* - Configurable dangerous action detection
* - Custom rule engine for complex scenarios
* - Argument modification support
* - Comprehensive audit logging
* - Timeout handling with cleanup
*/
export class HITLManager extends EventEmitter {
config;
pendingConfirmations = new Map();
statistics = {
totalRequests: 0,
pendingRequests: 0,
averageResponseTime: 0,
approvedRequests: 0,
rejectedRequests: 0,
timedOutRequests: 0,
};
constructor(config) {
super();
this.config = this.validateConfig(config);
this.setupEventHandlers();
}
/**
* Validate HITL configuration and apply defaults
*/
validateConfig(config) {
// Apply defaults for optional fields
const configWithDefaults = {
enabled: config.enabled,
dangerousActions: config.dangerousActions,
timeout: config.timeout ?? DEFAULT_TIMEOUT, // Default: 30 seconds
confirmationMethod: config.confirmationMethod ?? "event", // Default: "event"
allowArgumentModification: config.allowArgumentModification ?? DEFAULT_ALLOW_MODIFICATION, // Default: true
autoApproveOnTimeout: config.autoApproveOnTimeout ?? false, // Default: false (safe)
auditLogging: config.auditLogging ?? false, // Default: false
customRules: config.customRules ?? [], // Default: empty array
};
if (!configWithDefaults.enabled) {
return configWithDefaults; // If disabled, don't validate other fields
}
if (!Array.isArray(configWithDefaults.dangerousActions)) {
throw new HITLConfigurationError("dangerousActions must be an array of strings");
}
if (typeof configWithDefaults.timeout !== "number" ||
configWithDefaults.timeout <= 0) {
throw new HITLConfigurationError("timeout must be a positive number (milliseconds)");
}
if (configWithDefaults.confirmationMethod !== "event") {
throw new HITLConfigurationError("confirmationMethod must be 'event' (only supported method)");
}
if (typeof configWithDefaults.allowArgumentModification !== "boolean") {
throw new HITLConfigurationError("allowArgumentModification must be a boolean");
}
return configWithDefaults;
}
/**
* Check if a tool requires confirmation based on configuration
*/
requiresConfirmation(toolName, args) {
if (!this.config.enabled) {
return false;
}
// Check dangerous actions keywords
const lowerToolName = toolName.toLowerCase();
for (const action of this.config.dangerousActions) {
if (lowerToolName.includes(action.toLowerCase())) {
return true;
}
}
// Check custom rules
if (this.config.customRules) {
for (const rule of this.config.customRules) {
if (rule.requiresConfirmation) {
try {
if (rule.condition(toolName, args)) {
return true;
}
}
catch (error) {
// Log rule evaluation error but don't fail
this.logAuditEvent("rule-evaluation-error", {
ruleName: rule.name,
toolName,
error: error instanceof Error ? error.message : String(error),
});
}
}
}
}
return false;
}
/**
* Request confirmation for a tool execution
*/
async requestConfirmation(toolName, arguments_, context) {
const confirmationId = this.generateConfirmationId();
const startTime = Date.now();
// Update statistics
this.statistics.totalRequests++;
this.statistics.pendingRequests++;
return new Promise((resolve, reject) => {
// Set up timeout
const timeoutHandle = setTimeout(() => {
this.handleTimeout(confirmationId);
}, this.config.timeout);
// Store pending confirmation
const request = {
confirmationId,
toolName,
arguments: arguments_,
timestamp: startTime,
timeoutHandle,
resolve,
reject,
};
this.pendingConfirmations.set(confirmationId, request);
// Create confirmation request event
const requestEvent = {
type: "hitl:confirmation-request",
payload: {
confirmationId,
toolName,
serverId: context?.serverId,
actionType: this.generateActionDescription(toolName, arguments_),
arguments: arguments_,
metadata: {
timestamp: new Date(startTime).toISOString(),
sessionId: context?.sessionId,
userId: context?.userId,
dangerousKeywords: this.getTriggeredKeywords(toolName, arguments_),
},
timeoutMs: this.config.timeout ?? DEFAULT_TIMEOUT,
allowModification: this.config.allowArgumentModification ?? DEFAULT_ALLOW_MODIFICATION,
},
};
// Emit confirmation request event
this.emit("hitl:confirmation-request", requestEvent);
// Log audit trail if enabled
if (this.config.auditLogging) {
this.logAuditEvent("confirmation-requested", {
confirmationId,
toolName,
userId: context?.userId,
sessionId: context?.sessionId,
timestamp: startTime,
arguments: arguments_,
});
}
});
}
/**
* Process user response to confirmation request
*/
processUserResponse(confirmationId, response) {
const request = this.pendingConfirmations.get(confirmationId);
if (!request) {
logger.warn(`No pending confirmation found for ID: ${confirmationId}`);
return;
}
// Clear timeout
clearTimeout(request.timeoutHandle);
// Remove from pending
this.pendingConfirmations.delete(confirmationId);
this.statistics.pendingRequests--;
// Calculate response time
const responseTime = response.responseTime || Date.now() - request.timestamp;
// Update statistics
if (response.approved) {
this.statistics.approvedRequests++;
}
else {
this.statistics.rejectedRequests++;
}
// Update average response time
const totalResponses = this.statistics.approvedRequests + this.statistics.rejectedRequests;
this.statistics.averageResponseTime =
(this.statistics.averageResponseTime * (totalResponses - 1) +
responseTime) /
totalResponses;
// Create result
const result = {
approved: response.approved,
reason: response.reason,
modifiedArguments: response.modifiedArguments,
responseTime,
};
// Log audit trail if enabled
if (this.config.auditLogging) {
this.logAuditEvent(response.approved ? "confirmation-approved" : "confirmation-rejected", {
confirmationId,
toolName: request.toolName,
approved: response.approved,
reason: response.reason,
userId: response.userId,
responseTime,
arguments: request.arguments,
});
}
// Resolve the promise
request.resolve(result);
}
/**
* Handle confirmation timeout
*/
handleTimeout(confirmationId) {
const request = this.pendingConfirmations.get(confirmationId);
if (!request) {
return;
}
// Remove from pending
this.pendingConfirmations.delete(confirmationId);
this.statistics.pendingRequests--;
this.statistics.timedOutRequests++;
// Calculate response time (timeout duration)
const responseTime = Date.now() - request.timestamp;
// Check if auto-approve on timeout is enabled
const shouldAutoApprove = this.config.autoApproveOnTimeout === true;
// Log audit trail if enabled
if (this.config.auditLogging) {
this.logAuditEvent("confirmation-timeout", {
confirmationId,
toolName: request.toolName,
timeout: this.config.timeout ?? DEFAULT_TIMEOUT,
arguments: request.arguments,
autoApproved: shouldAutoApprove,
});
}
// Create timeout event
const timeoutEvent = {
type: "hitl:timeout",
payload: {
confirmationId,
toolName: request.toolName,
timeout: this.config.timeout ?? DEFAULT_TIMEOUT,
},
};
// Emit timeout event
this.emit("hitl:timeout", timeoutEvent);
if (shouldAutoApprove) {
// Auto-approve the request
this.statistics.approvedRequests++;
// Update average response time
const totalResponses = this.statistics.approvedRequests + this.statistics.rejectedRequests;
this.statistics.averageResponseTime =
(this.statistics.averageResponseTime * (totalResponses - 1) +
responseTime) /
totalResponses;
// Log auto-approval if enabled
if (this.config.auditLogging) {
this.logAuditEvent("confirmation-auto-approved", {
confirmationId,
toolName: request.toolName,
reason: "Auto-approved due to timeout",
responseTime,
arguments: request.arguments,
});
}
// Resolve with auto-approval
const result = {
approved: true,
reason: "Auto-approved due to timeout",
responseTime,
};
request.resolve(result);
}
else {
// Reject with timeout error (original behavior)
request.reject(new HITLTimeoutError(`Confirmation timeout for tool: ${request.toolName}`, confirmationId, this.config.timeout ?? DEFAULT_TIMEOUT));
}
}
/**
* Set up event handlers for processing responses
*/
setupEventHandlers() {
this.on("hitl:confirmation-response", (event) => {
if (event.payload?.confirmationId) {
this.processUserResponse(event.payload.confirmationId, {
approved: event.payload.approved,
reason: event.payload.reason,
modifiedArguments: event.payload.modifiedArguments,
responseTime: event.payload.metadata?.responseTime,
userId: event.payload.metadata?.userId,
});
}
});
}
/**
* Generate unique confirmation ID
*/
generateConfirmationId() {
return `hitl-${Date.now()}-${randomUUID()}`;
}
/**
* Generate human-readable action description
*/
generateActionDescription(toolName, args) {
const lowerToolName = toolName.toLowerCase();
// Check for specific action types
if (lowerToolName.includes("delete")) {
return "Delete Operation";
}
if (lowerToolName.includes("remove")) {
return "Remove Operation";
}
if (lowerToolName.includes("update")) {
return "Update Operation";
}
if (lowerToolName.includes("create")) {
return "Create Operation";
}
if (lowerToolName.includes("drop")) {
return "Drop Operation";
}
if (lowerToolName.includes("truncate")) {
return "Truncate Operation";
}
if (lowerToolName.includes("restart")) {
return "Restart Operation";
}
if (lowerToolName.includes("stop")) {
return "Stop Operation";
}
if (lowerToolName.includes("kill")) {
return "Kill Operation";
}
// Check custom rules for custom messages
if (this.config.customRules) {
for (const rule of this.config.customRules) {
try {
if (rule.condition(toolName, args) && rule.customMessage) {
return rule.customMessage;
}
}
catch {
// Ignore rule evaluation errors
}
}
}
return `Execute ${toolName}`;
}
/**
* Get keywords that triggered HITL
*/
getTriggeredKeywords(toolName, args) {
const triggered = [];
const lowerToolName = toolName.toLowerCase();
// Check dangerous actions
for (const action of this.config.dangerousActions) {
if (lowerToolName.includes(action.toLowerCase())) {
triggered.push(action);
}
}
// Check custom rules
if (this.config.customRules) {
for (const rule of this.config.customRules) {
try {
if (rule.requiresConfirmation && rule.condition(toolName, args)) {
triggered.push(rule.name);
}
}
catch {
// Ignore rule evaluation errors
}
}
}
return triggered;
}
/**
* Log audit events for compliance and debugging
*/
logAuditEvent(eventType, data) {
const auditLog = {
timestamp: new Date().toISOString(),
eventType: eventType,
toolName: data.toolName,
userId: data.userId,
sessionId: data.sessionId,
arguments: data.arguments,
reason: data.reason,
responseTime: data.responseTime,
...data,
};
logger.info(`[HITL Audit] ${eventType}:`, auditLog);
// Emit audit event for external logging systems
this.emit("hitl:audit", auditLog);
}
/**
* Get current HITL usage statistics
*/
getStatistics() {
return { ...this.statistics };
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Update configuration (for dynamic reconfiguration)
*/
updateConfig(newConfig) {
const updatedConfig = { ...this.config, ...newConfig };
this.config = this.validateConfig(updatedConfig);
if (this.config.auditLogging) {
this.logAuditEvent("configuration-updated", {
oldConfig: this.config,
newConfig: updatedConfig,
});
}
}
/**
* Clean up resources and reject pending confirmations
*/
cleanup() {
// Clear all pending confirmations
for (const [confirmationId, request] of this.pendingConfirmations) {
clearTimeout(request.timeoutHandle);
request.reject(new Error(`HITL cleanup: confirmation ${confirmationId} cancelled`));
}
this.pendingConfirmations.clear();
this.statistics.pendingRequests = 0;
if (this.config.auditLogging) {
this.logAuditEvent("manager-cleanup", {
clearedConfirmations: this.pendingConfirmations.size,
});
}
}
/**
* Check if manager is currently enabled
*/
isEnabled() {
return this.config.enabled;
}
/**
* Get count of pending confirmations
*/
getPendingCount() {
return this.pendingConfirmations.size;
}
}