UNPKG

@astreus-ai/astreus

Version:

Open-source AI agent framework for building autonomous systems that solve real-world tasks effectively.

1,546 lines (1,536 loc) 611 kB
var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); // src/agent/defaults.ts var DEFAULT_AGENT_CONFIG = { model: "gpt-4o-mini", temperature: 0.7, maxTokens: 2e3, useTools: true, memory: false, knowledge: false, vision: false, autoContextCompression: false, debug: false }; // src/plugin/defaults.ts var DEFAULT_PLUGIN_CONFIG = { defaultVersion: "unknown", defaultDescription: "none", missingName: "[missing]", defaultTimeout: 3e4 // 30 seconds }; // src/errors/index.ts var _AstreusError = class _AstreusError extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = "AstreusError"; Object.setPrototypeOf(this, new.target.prototype); } /** * Get the full error chain as a string for logging */ getFullStack() { let fullStack = this.stack || this.message; if (this.cause instanceof Error) { fullStack += ` Caused by: ${this.cause.stack || this.cause.message}`; } return fullStack; } }; __name(_AstreusError, "AstreusError"); var AstreusError = _AstreusError; var _LLMApiError = class _LLMApiError extends AstreusError { constructor(message, provider, cause) { super(message, cause); this.provider = provider; this.name = "LLMApiError"; } }; __name(_LLMApiError, "LLMApiError"); var LLMApiError = _LLMApiError; var _SubAgentError = class _SubAgentError extends AstreusError { constructor(message, agentId, cause) { super(message, cause); this.agentId = agentId; this.name = "SubAgentError"; } }; __name(_SubAgentError, "SubAgentError"); var SubAgentError = _SubAgentError; var _VisionError = class _VisionError extends AstreusError { constructor(message, provider, cause) { super(message, cause); this.provider = provider; this.name = "VisionError"; } }; __name(_VisionError, "VisionError"); var VisionError = _VisionError; var _GraphNodeError = class _GraphNodeError extends AstreusError { constructor(message, nodeId, nodeName, step, graphId, parentNodeId, cause) { super(message, cause); this.nodeId = nodeId; this.nodeName = nodeName; this.step = step; this.graphId = graphId; this.parentNodeId = parentNodeId; this.name = "GraphNodeError"; } /** * Get error chain information for debugging */ getErrorChain() { return { nodeId: this.nodeId, nodeName: this.nodeName, step: this.step, graphId: this.graphId, parentNodeId: this.parentNodeId, message: this.message, cause: this.cause instanceof Error ? this.cause.message : void 0 }; } }; __name(_GraphNodeError, "GraphNodeError"); var GraphNodeError = _GraphNodeError; var _ToolError = class _ToolError extends AstreusError { constructor(message, toolName, toolType, errorType, recoverable = true, cause) { super(message, cause); this.toolName = toolName; this.toolType = toolType; this.errorType = errorType; this.recoverable = recoverable; this.name = "ToolError"; } /** * Get normalized error response for LLM */ toToolResult() { return { success: false, error: this.message, recoverable: this.recoverable, toolType: this.toolType }; } }; __name(_ToolError, "ToolError"); var ToolError = _ToolError; // src/plugin/index.ts var _Plugin = class _Plugin { // Lock for preventing race conditions constructor(agent) { this.agent = agent; this.name = "plugin"; this.plugins = /* @__PURE__ */ new Map(); this.configs = /* @__PURE__ */ new Map(); this.tools = /* @__PURE__ */ new Map(); this.registrationLock = /* @__PURE__ */ new Set(); this.logger = agent.logger; this.logger.info("Plugin manager initialized"); this.logger.debug("Plugin manager initialized", { agentId: agent.id, agentName: agent.name }); } async initialize() { this.logger.info("Plugin manager ready"); this.logger.debug("Plugin manager initialization completed"); } async registerPlugin(plugin, config) { this.logger.info(`Registering plugin: ${plugin.name}`); this.logger.debug("Registering plugin", { name: plugin.name, version: plugin.version, description: plugin.description, toolCount: plugin.tools.length, toolNames: plugin.tools.map((t) => t.name), hasConfig: !!config, hasInitialize: !!plugin.initialize, hasCleanup: !!plugin.cleanup }); if (this.plugins.has(plugin.name)) { this.logger.error(`Plugin already registered: ${plugin.name}`); this.logger.debug("Plugin registration failed - already exists", { pluginName: plugin.name, existingPlugins: Array.from(this.plugins.keys()) }); throw new Error(`Plugin '${plugin.name}' is already registered`); } this.logger.debug("Validating plugin structure", { pluginName: plugin.name }); this.validatePlugin(plugin); const pluginConfig = config || { name: plugin.name, enabled: true }; this.logger.debug("Plugin config prepared", { pluginName: plugin.name, enabled: pluginConfig.enabled, hasCustomConfig: !!pluginConfig.config }); if (plugin.initialize) { this.logger.debug("Initializing plugin", { pluginName: plugin.name }); await plugin.initialize(pluginConfig.config); this.logger.debug("Plugin initialized successfully", { pluginName: plugin.name }); } this.plugins.set(plugin.name, plugin); this.configs.set(plugin.name, pluginConfig); if (pluginConfig.enabled) { this.logger.debug("Registering plugin tools", { pluginName: plugin.name, toolCount: plugin.tools.length }); for (const tool of plugin.tools) { const maxWaitTime = 5e3; const startWait = Date.now(); while (this.registrationLock.has(tool.name)) { if (Date.now() - startWait > maxWaitTime) { this.logger.error(`Tool registration lock timeout: ${tool.name}`); throw new Error(`Tool registration timeout for '${tool.name}': lock held for too long`); } await new Promise((r) => setTimeout(r, 10)); } this.registrationLock.add(tool.name); try { if (this.tools.has(tool.name)) { this.logger.error(`Tool name conflict: ${tool.name}`); this.logger.debug("Tool registration failed - name conflict", { toolName: tool.name, pluginName: plugin.name, existingTools: Array.from(this.tools.keys()) }); throw new Error(`Tool '${tool.name}' is already registered by another plugin`); } this.tools.set(tool.name, tool); this.logger.debug("Tool registered", { toolName: tool.name, pluginName: plugin.name, description: tool.description }); } finally { this.registrationLock.delete(tool.name); } } } else { this.logger.debug("Plugin disabled, tools not registered", { pluginName: plugin.name, toolCount: plugin.tools.length }); } this.logger.info(`Plugin registered: ${plugin.name} (${plugin.tools.length} tools)`); this.logger.debug("Plugin registered successfully", { pluginName: plugin.name, version: plugin.version, toolsRegistered: pluginConfig.enabled ? plugin.tools.length : 0, totalPlugins: this.plugins.size, totalTools: this.tools.size }); } async unregisterPlugin(name) { this.logger.info(`Unregistering plugin: ${name}`); this.logger.debug("Unregistering plugin", { pluginName: name, isRegistered: this.plugins.has(name) }); const plugin = this.plugins.get(name); if (!plugin) { this.logger.error(`Plugin not found: ${name}`); this.logger.debug("Plugin unregistration failed - not found", { pluginName: name, availablePlugins: Array.from(this.plugins.keys()) }); throw new Error(`Plugin '${name}' is not registered`); } const removedTools = []; for (const tool of plugin.tools) { if (this.tools.has(tool.name)) { this.tools.delete(tool.name); removedTools.push(tool.name); this.logger.debug("Tool unregistered", { toolName: tool.name, pluginName: name }); } } this.logger.debug("Plugin tools removed", { pluginName: name, removedTools, removedCount: removedTools.length }); if (plugin.cleanup) { this.logger.debug("Running plugin cleanup", { pluginName: name }); await plugin.cleanup(); this.logger.debug("Plugin cleanup completed", { pluginName: name }); } this.plugins.delete(name); this.configs.delete(name); this.logger.info(`Plugin unregistered: ${name}`); this.logger.debug("Plugin unregistered successfully", { pluginName: name, removedToolCount: removedTools.length, remainingPlugins: this.plugins.size, remainingTools: this.tools.size }); } getPlugin(name) { const plugin = this.plugins.get(name); this.logger.debug("Plugin lookup", { pluginName: name, found: !!plugin, version: plugin?.version ?? DEFAULT_PLUGIN_CONFIG.defaultVersion }); return plugin; } getTools() { const tools = Array.from(this.tools.values()); this.logger.debug("Retrieved all tools", { toolCount: tools.length, toolNames: tools.map((t) => t.name) }); return tools; } getTool(name) { const tool = this.tools.get(name); this.logger.debug("Tool lookup", { toolName: name, found: !!tool, description: tool?.description ?? DEFAULT_PLUGIN_CONFIG.defaultDescription }); return tool; } async executeTool(toolCall, context) { const startTime = Date.now(); this.logger.info(`Executing tool: ${toolCall.name}`); this.logger.debug("Executing tool", { toolName: toolCall.name, callId: toolCall.id, parameters: Object.keys(toolCall.parameters), parameterCount: Object.keys(toolCall.parameters).length, hasContext: !!context }); try { const tool = this.getTool(toolCall.name); if (!tool) { this.logger.error(`Tool not found: ${toolCall.name}`); this.logger.debug("Tool execution failed - tool not found", { toolName: toolCall.name, callId: toolCall.id, availableTools: Array.from(this.tools.keys()) }); return { id: toolCall.id, name: toolCall.name, result: { success: false, error: `Tool '${toolCall.name}' not found` }, executionTime: Date.now() - startTime }; } this.logger.debug("Validating tool parameters", { toolName: toolCall.name, callId: toolCall.id }); const validationError = this.validateToolParameters(tool, toolCall.parameters); if (validationError) { this.logger.error(`Tool parameter validation failed: ${toolCall.name}`); this.logger.debug("Tool parameter validation error", { toolName: toolCall.name, callId: toolCall.id, validationError, parameters: toolCall.parameters }); return { id: toolCall.id, name: toolCall.name, result: { success: false, error: validationError }, executionTime: Date.now() - startTime }; } if (!this.agent || typeof this.agent.id !== "string") { this.logger.error("Agent not available for tool execution"); return { id: toolCall.id, name: toolCall.name, result: { success: false, error: "Agent not available for tool execution" }, executionTime: Date.now() - startTime }; } const isolatedContext = { // Copy existing context properties if provided ...context || {}, // Add execution isolation metadata agentId: context?.agentId || this.agent.id, agent: context?.agent || this.agent, // Add unique execution ID for this specific call executionId: `${toolCall.id}-${Date.now()}`, // Isolate any shared state by creating fresh copies toolName: toolCall.name, callTimestamp: /* @__PURE__ */ new Date() }; this.logger.debug("Calling tool handler with isolated context", { toolName: toolCall.name, callId: toolCall.id, timeout: DEFAULT_PLUGIN_CONFIG.defaultTimeout, executionId: isolatedContext.executionId ?? null }); let timeoutId; const timeoutPromise = new Promise((_, reject) => { timeoutId = setTimeout(() => { reject( new Error( `Tool '${toolCall.name}' execution timed out after ${DEFAULT_PLUGIN_CONFIG.defaultTimeout}ms` ) ); }, DEFAULT_PLUGIN_CONFIG.defaultTimeout); }); let result; try { result = await Promise.race([ tool.handler(toolCall.parameters, isolatedContext), timeoutPromise ]); clearTimeout(timeoutId); } catch (raceError) { clearTimeout(timeoutId); throw raceError; } const executionTime = Date.now() - startTime; this.logger.info(`Tool completed: ${toolCall.name} (${executionTime}ms)`); this.logger.debug("Tool execution successful", { toolName: toolCall.name, callId: toolCall.id, executionTime, success: result.success, hasData: !!result.data, hasError: !!result.error, executionId: isolatedContext.executionId ?? null }); return { id: toolCall.id, name: toolCall.name, result, executionTime }; } catch (error) { const executionTime = Date.now() - startTime; const originalError = error instanceof Error ? error : new Error("Unknown error occurred"); let errorType = "execution"; if (originalError.message.includes("timed out") || originalError.message.includes("timeout")) { errorType = "timeout"; } else if (originalError.message.includes("not found") || originalError.message.includes("not available")) { errorType = "not_found"; } else if (originalError.message.includes("Invalid") || originalError.message.includes("validation")) { errorType = "validation"; } const recoverable = errorType !== "not_found"; const toolError = new ToolError( `Plugin tool '${toolCall.name}' failed: ${originalError.message}`, toolCall.name, "plugin", errorType, recoverable, originalError ); this.logger.error(`Tool execution failed: ${toolCall.name}`, toolError); this.logger.debug("Tool execution error", { toolName: toolCall.name, callId: toolCall.id, executionTime, errorType, recoverable, error: originalError.message, hasStack: !!originalError.stack }); const errorResult = toolError.toToolResult(); return { id: toolCall.id, name: toolCall.name, result: { success: false, error: errorResult.error }, executionTime }; } } listPlugins() { const plugins = Array.from(this.plugins.values()); this.logger.debug("Listed plugins", { pluginCount: plugins.length, pluginNames: plugins.map((p) => p.name) }); return plugins; } // Get tools formatted for LLM function calling getToolsForLLM() { const tools = this.getTools(); const llmTools = tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: this.convertParametersToJsonSchema(tool.parameters) } })); this.logger.debug("Generated LLM tool schemas", { toolCount: llmTools.length, toolNames: llmTools.map((t) => t.function.name) }); return llmTools; } validatePlugin(plugin) { this.logger.debug("Validating plugin structure", { pluginName: plugin.name, hasName: !!plugin.name, hasVersion: !!plugin.version, hasTools: Array.isArray(plugin.tools), toolCount: Array.isArray(plugin.tools) ? plugin.tools.length : 0 }); if (!plugin.name || typeof plugin.name !== "string" || plugin.name.trim().length === 0) { this.logger.debug("Plugin validation failed - invalid name", { pluginName: plugin.name ?? DEFAULT_PLUGIN_CONFIG.missingName, nameType: typeof plugin.name }); throw new Error("Plugin must have a valid non-empty name"); } if (!plugin.version || typeof plugin.version !== "string" || plugin.version.trim().length === 0) { this.logger.debug("Plugin validation failed - invalid version", { pluginName: plugin.name, version: plugin.version ?? DEFAULT_PLUGIN_CONFIG.defaultVersion, versionType: typeof plugin.version }); throw new Error(`Plugin '${plugin.name}' must have a valid non-empty version`); } const namePattern = /^[a-zA-Z][a-zA-Z0-9_-]*$/; if (!namePattern.test(plugin.name)) { this.logger.debug("Plugin validation failed - invalid name format", { pluginName: plugin.name }); throw new Error( `Plugin name '${plugin.name}' must start with a letter and contain only alphanumeric characters, hyphens, or underscores` ); } if (!Array.isArray(plugin.tools)) { this.logger.debug("Plugin validation failed - invalid tools array", { pluginName: plugin.name, toolsType: typeof plugin.tools, isArray: Array.isArray(plugin.tools) }); throw new Error(`Plugin '${plugin.name}' must have tools array`); } const toolNames = /* @__PURE__ */ new Set(); for (const tool of plugin.tools) { if (toolNames.has(tool.name)) { this.logger.debug("Plugin validation failed - duplicate tool name", { pluginName: plugin.name, duplicateToolName: tool.name }); throw new Error(`Plugin '${plugin.name}' has duplicate tool name '${tool.name}'`); } toolNames.add(tool.name); } for (const tool of plugin.tools) { this.logger.debug("Validating tool definition", { pluginName: plugin.name, toolName: tool.name, hasName: !!tool.name, hasDescription: !!tool.description, hasHandler: !!tool.handler }); if (!tool.name || typeof tool.name !== "string" || tool.name.trim().length === 0) { this.logger.debug("Tool validation failed - invalid name", { pluginName: plugin.name, toolName: tool.name ?? DEFAULT_PLUGIN_CONFIG.missingName }); throw new Error(`Invalid tool name in plugin '${plugin.name}'`); } if (!tool.description || typeof tool.description !== "string") { this.logger.debug("Tool validation failed - invalid description", { pluginName: plugin.name, toolName: tool.name }); throw new Error( `Tool '${tool.name}' in plugin '${plugin.name}' must have a valid description` ); } if (!tool.handler || typeof tool.handler !== "function") { this.logger.debug("Tool validation failed - invalid handler", { pluginName: plugin.name, toolName: tool.name, handlerType: typeof tool.handler }); throw new Error( `Tool '${tool.name}' in plugin '${plugin.name}' must have a valid handler function` ); } if (tool.parameters && typeof tool.parameters === "object") { this.validateToolParameterDefinitions(plugin.name, tool.name, tool.parameters); } } this.logger.debug("Plugin validation successful", { pluginName: plugin.name, version: plugin.version, toolCount: plugin.tools.length }); } validateToolParameterDefinitions(pluginName, toolName, parameters, depth = 0) { const MAX_DEPTH = 50; if (depth > MAX_DEPTH) { throw new Error(`Parameter nesting too deep in tool '${toolName}' (max depth: ${MAX_DEPTH})`); } const validTypes = ["string", "number", "boolean", "object", "array"]; for (const [paramName, paramDef] of Object.entries(parameters)) { if (!paramDef.type || !validTypes.includes(paramDef.type)) { this.logger.debug("Parameter validation failed - invalid type", { pluginName, toolName, paramName, paramType: paramDef.type, validTypes }); throw new Error( `Parameter '${paramName}' in tool '${toolName}' has invalid type '${paramDef.type}'` ); } if (!paramDef.description || typeof paramDef.description !== "string") { this.logger.debug("Parameter validation failed - missing description", { pluginName, toolName, paramName }); throw new Error(`Parameter '${paramName}' in tool '${toolName}' must have a description`); } if (paramDef.type === "object" && paramDef.properties) { this.validateToolParameterDefinitions(pluginName, toolName, paramDef.properties, depth + 1); } if (paramDef.type === "array" && paramDef.items) { if (!validTypes.includes(paramDef.items.type)) { this.logger.debug("Parameter validation failed - invalid array items type", { pluginName, toolName, paramName, itemsType: paramDef.items.type }); throw new Error( `Array parameter '${paramName}' in tool '${toolName}' has invalid items type` ); } } } } validateToolParameters(tool, parameters) { this.logger.debug("Validating tool parameters", { toolName: tool.name, expectedParams: Object.keys(tool.parameters), providedParams: Object.keys(parameters), parameterCount: Object.keys(parameters).length }); for (const [paramName, paramDef] of Object.entries(tool.parameters)) { const value = parameters[paramName]; this.logger.debug("Validating parameter", { toolName: tool.name, paramName, paramType: paramDef.type, required: !!paramDef.required, hasValue: value !== void 0 && value !== null, valueType: typeof value }); if (paramDef.required && (value === void 0 || value === null)) { this.logger.debug("Required parameter missing", { toolName: tool.name, paramName, paramType: paramDef.type }); return `Required parameter '${paramName}' is missing`; } if (value !== void 0 && value !== null) { if (!this.isToolParameterValue(value) || !this.validateParameterType(value, paramDef)) { this.logger.debug("Parameter type validation failed", { toolName: tool.name, paramName, expectedType: paramDef.type, actualType: typeof value, value: String(value).slice(0, 100) // Truncate long values }); return `Parameter '${paramName}' has invalid type. Expected ${paramDef.type}`; } } } this.logger.debug("Tool parameter validation successful", { toolName: tool.name, validatedParams: Object.keys(tool.parameters).length }); return null; } isToolParameterValue(value, depth = 0) { const MAX_DEPTH = 50; if (depth > MAX_DEPTH) { this.logger.warn("Maximum nesting depth exceeded for parameter validation", { depth }); return false; } if (value === null) return true; if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { return true; } if (Array.isArray(value)) { return value.every((item) => this.isToolParameterValue(item, depth + 1)); } if (typeof value === "object") { return Object.values(value).every( (v) => this.isToolParameterValue(v, depth + 1) ); } return false; } validateParameterType(value, paramDef) { switch (paramDef.type) { case "string": return typeof value === "string"; case "number": return typeof value === "number"; case "boolean": return typeof value === "boolean"; case "object": return typeof value === "object" && value !== null && !Array.isArray(value); case "array": return Array.isArray(value); default: return false; } } convertParametersToJsonSchema(parameters, depth = 0) { const MAX_DEPTH = 50; if (depth > MAX_DEPTH) { this.logger.warn("Maximum nesting depth exceeded for JSON schema conversion", { depth }); return { type: "object", properties: {}, required: [] }; } const properties = {}; for (const [name, param] of Object.entries(parameters)) { properties[name] = { type: param.type, description: param.description }; if (param.enum) { properties[name].enum = param.enum; } if (param.properties) { properties[name].properties = this.convertParametersToJsonSchema( param.properties, depth + 1 ).properties; } if (param.items) { properties[name].items = { type: param.items.type }; } } return { type: "object", properties, required: Object.entries(parameters).filter(([, param]) => param.required).map(([name]) => name) }; } }; __name(_Plugin, "Plugin"); var Plugin = _Plugin; var pluginInstances = /* @__PURE__ */ new Map(); function getPlugin(agent) { if (!agent) { throw new Error("Agent required for plugin initialization"); } const agentId = agent.id; if (!pluginInstances.has(agentId)) { pluginInstances.set(agentId, new Plugin(agent)); } return pluginInstances.get(agentId); } __name(getPlugin, "getPlugin"); async function cleanupPluginForAgent(agentId) { const plugin = pluginInstances.get(agentId); if (plugin) { const plugins = plugin.listPlugins(); for (const p of plugins) { await plugin.unregisterPlugin(p.name); } pluginInstances.delete(agentId); } } __name(cleanupPluginForAgent, "cleanupPluginForAgent"); async function cleanupPlugin(agentId) { if (agentId) { await cleanupPluginForAgent(agentId); } else { const agentIds = Array.from(pluginInstances.keys()); for (const id of agentIds) { await cleanupPluginForAgent(id); } } } __name(cleanupPlugin, "cleanupPlugin"); function convertToolParametersToJsonSchema(parameters, depth = 0) { const MAX_DEPTH = 50; if (depth > MAX_DEPTH) { return { type: "object", properties: {}, required: [] }; } const properties = {}; for (const [name, param] of Object.entries(parameters)) { properties[name] = { type: param.type, description: param.description }; if (param.enum) { properties[name].enum = param.enum; } if (param.properties) { properties[name].properties = convertToolParametersToJsonSchema( param.properties, depth + 1 ).properties; } if (param.items) { properties[name].items = { type: param.items.type }; } } return { type: "object", properties, required: Object.entries(parameters).filter(([, param]) => param.required).map(([name]) => name) }; } __name(convertToolParametersToJsonSchema, "convertToolParametersToJsonSchema"); // src/task/index.ts import crypto3 from "crypto"; // src/database/index.ts import knex from "knex"; import crypto2 from "crypto"; // src/database/defaults.ts var DEFAULT_DATABASE_CONFIG = { defaultTemperature: 0.7, defaultMaxTokens: 2e3, defaultModel: "gpt-4o-mini", defaultBooleanValue: false }; // src/logger/index.ts import pino from "pino"; var _Logger = class _Logger { constructor(config = {}) { this.transportWorker = null; const envLevel = process.env.LOG_LEVEL; const validLevels = ["debug", "info", "warn", "error", "silent", "success"]; const isValidLogLevel = /* @__PURE__ */ __name((level) => level !== void 0 && validLevels.includes(level), "isValidLogLevel"); this.config = { level: isValidLogLevel(envLevel) ? envLevel : "info", debug: config.debug ?? false, enableConsole: true, enableFile: false, ...config }; if (envLevel === "silent") { this.pino = pino({ level: "silent" }); return; } const isProduction = process.env.NODE_ENV === "production"; const pinoConfig = { level: this.config.level === "success" ? "info" : this.config.level, formatters: { level: /* @__PURE__ */ __name((label) => { return { level: label }; }, "level") }, base: { framework: "Astreus" }, timestamp: this.config.debug ? pino.stdTimeFunctions.isoTime : false }; if (!isProduction && this.config.enableConsole) { this.pino = pino({ ...pinoConfig, transport: { target: "pino-pretty", options: { colorize: true, translateTime: false, ignore: this.config.debug ? "pid,hostname,framework,level,time,module,agent,err" : "pid,hostname,framework,level,time,module,agent,data,err", messageFormat: "\x1B[36mAstreus [\x1B[34m{agent}\x1B[36m] {module}\x1B[0m \u2192 \x1B[32m{msg}\x1B[0m", customColors: "info:cyan,warn:yellow,error:red", hideObject: !this.config.debug, singleLine: false, messageKey: "msg" } } }); this.transportWorker = this.pino; } else { this.pino = pino(pinoConfig); } } formatLogObject(message, module = "Core", data, error, agentName) { const logObject = { msg: message, module, framework: "Astreus", agent: agentName ?? this.config.agentName ?? "System" }; if (data) { logObject.data = data; } if (error) { logObject.err = { message: error.message, stack: error.stack ?? null, name: error.name }; } return logObject; } debug(message, data, agentName) { const logObj = this.formatLogObject(message, "Core", data, void 0, agentName); this.pino.debug(logObj); } info(message, data, agentName) { const logObj = this.formatLogObject(message, "Core", data, void 0, agentName); this.pino.info(logObj); } warn(message, data, agentName) { const logObj = this.formatLogObject(message, "Core", data, void 0, agentName); this.pino.warn(logObj); } error(message, error, data, agentName) { const logObj = this.formatLogObject(message, "Core", data, error, agentName); this.pino.error(logObj); } success(message, data, agentName) { const logObj = this.formatLogObject(message, "Core", data, void 0, agentName); logObj.level = "success"; this.pino.info(logObj); } // Add a public log method for custom module names log(level, message, module = "Core", data, error, agentName) { const logObj = this.formatLogObject(message, module, data, error, agentName); switch (level) { case "debug": this.pino.debug(logObj); break; case "info": case "success": this.pino.info(logObj); break; case "warn": this.pino.warn(logObj); break; case "error": this.pino.error(logObj); break; case "silent": break; default: { const _exhaustiveCheck = level; throw new Error(`Unknown log level: ${_exhaustiveCheck}`); } } } setLevel(level) { this.config.level = level; this.pino.level = level === "success" ? "info" : level; } setDebug(debug) { if (this.config.debug === debug) return; this.cleanupTransportWorker(); this.config.debug = debug; const isProduction = process.env.NODE_ENV === "production"; const pinoConfig = { level: this.config.level === "success" ? "info" : this.config.level, formatters: { level: /* @__PURE__ */ __name((label) => { return { level: label }; }, "level") }, base: { framework: "Astreus" }, timestamp: debug ? pino.stdTimeFunctions.isoTime : false }; if (!isProduction && this.config.enableConsole) { this.pino = pino({ ...pinoConfig, transport: { target: "pino-pretty", options: { colorize: true, translateTime: false, ignore: debug ? "pid,hostname,framework,level,time,module,agent,err" : "pid,hostname,framework,level,time,module,agent,data,err", messageFormat: "\x1B[36mAstreus [\x1B[34m{agent}\x1B[36m] {module}\x1B[0m \u2192 \x1B[32m{msg}\x1B[0m", customColors: "info:cyan,warn:yellow,error:red", hideObject: !debug, singleLine: false, messageKey: "msg" } } }); this.transportWorker = this.pino; } else { this.pino = pino(pinoConfig); this.transportWorker = null; } } /** * Cleanup the transport worker to prevent memory leaks */ cleanupTransportWorker() { if (this.transportWorker) { if (this.transportWorker.flush) { this.transportWorker.flush(); } const pinoStreamSymbol = /* @__PURE__ */ Symbol.for("pino.stream"); const transportInstance = this.transportWorker; const destination = transportInstance[pinoStreamSymbol]; if (destination?.end) { destination.end(); } this.transportWorker = null; } } /** * Flush any pending log entries and cleanup resources. * Call this before application shutdown. */ async flush() { return new Promise((resolve5, reject) => { if (this.pino.flush) { this.pino.flush((err) => { if (err) { reject(err); } else { resolve5(); } }); } else { resolve5(); } }); } /** * Dispose of the logger instance and cleanup resources. */ dispose() { this.cleanupTransportWorker(); if (this.pino.flush) { this.pino.flush(); } } }; __name(_Logger, "Logger"); var Logger = _Logger; var globalLogger = null; var _AsyncMutex = class _AsyncMutex { constructor() { this.locked = false; this.queue = []; } async acquire() { return new Promise((resolve5) => { if (!this.locked) { this.locked = true; resolve5(); } else { this.queue.push(resolve5); } }); } release() { if (this.queue.length > 0) { const next = this.queue.shift(); if (next) next(); } else { this.locked = false; } } }; __name(_AsyncMutex, "AsyncMutex"); var AsyncMutex = _AsyncMutex; var loggerMutex = new AsyncMutex(); function getLogger(config) { if (globalLogger) return globalLogger; if (!globalLogger) { globalLogger = new Logger({ agentName: "System", ...config }); } return globalLogger; } __name(getLogger, "getLogger"); async function initializeLogger(config) { await loggerMutex.acquire(); try { if (globalLogger) { globalLogger.dispose(); } globalLogger = new Logger(config); return globalLogger; } finally { loggerMutex.release(); } } __name(initializeLogger, "initializeLogger"); async function shutdownLogger() { const logger = globalLogger; globalLogger = null; if (logger) { await logger.flush(); logger.dispose(); } } __name(shutdownLogger, "shutdownLogger"); function resetLogger() { if (globalLogger) { globalLogger.dispose(); globalLogger = null; } } __name(resetLogger, "resetLogger"); // src/database/knex.ts function detectDatabaseType(config) { if (config.type) { return config.type; } if (config.connectionString) { if (config.connectionString.startsWith("sqlite://")) { return "sqlite"; } if (config.connectionString.startsWith("postgresql://") || config.connectionString.startsWith("postgres://")) { return "postgres"; } } return "sqlite"; } __name(detectDatabaseType, "detectDatabaseType"); var _ConnectionPoolManager = class _ConnectionPoolManager { constructor(maxPoolSize, leakThresholdMs = 3e4) { this.activeConnections = /* @__PURE__ */ new Map(); this.totalAcquired = 0; this.totalReleased = 0; this.leakCheckInterval = null; this.logger = getLogger(); this.maxPoolSize = maxPoolSize; this.leakThresholdMs = leakThresholdMs; this.startLeakDetection(); } static getInstance(maxPoolSize = 10, leakThresholdMs = 3e4) { if (!_ConnectionPoolManager.instance) { _ConnectionPoolManager.instance = new _ConnectionPoolManager(maxPoolSize, leakThresholdMs); } return _ConnectionPoolManager.instance; } /** * Track when a connection is acquired from the pool */ onAcquire(connectionId) { const id = connectionId || `conn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const stack = new Error().stack; this.activeConnections.set(id, { acquiredAt: Date.now(), stack }); this.totalAcquired++; if (this.activeConnections.size >= this.maxPoolSize * 0.8) { this.logger.warn("Connection pool utilization high", { active: this.activeConnections.size, max: this.maxPoolSize, utilization: `${(this.activeConnections.size / this.maxPoolSize * 100).toFixed(1)}%` }); } this.logger.debug("Database connection acquired", { connectionId: id, activeConnections: this.activeConnections.size, totalAcquired: this.totalAcquired }); } /** * Track when a connection is released back to the pool */ onRelease(connectionId) { if (!connectionId && this.activeConnections.size > 0) { const keysIterator = this.activeConnections.keys().next(); if (!keysIterator.done && keysIterator.value !== void 0) { connectionId = keysIterator.value; } } if (connectionId && this.activeConnections.has(connectionId)) { this.activeConnections.delete(connectionId); this.totalReleased++; this.logger.debug("Database connection released", { connectionId, activeConnections: this.activeConnections.size, totalReleased: this.totalReleased }); } } /** * Start periodic leak detection */ startLeakDetection() { this.leakCheckInterval = setInterval(() => { this.detectLeaks(); }, 1e4); this.leakCheckInterval.unref(); } /** * Detect and log potential connection leaks */ detectLeaks() { const now = Date.now(); const leaks = []; for (const [id, info] of this.activeConnections.entries()) { const heldForMs = now - info.acquiredAt; if (heldForMs > this.leakThresholdMs) { leaks.push({ id, heldForMs, stack: info.stack }); } } if (leaks.length > 0) { const leakDetails = leaks.map((l) => ({ connectionId: l.id, heldForSeconds: (l.heldForMs / 1e3).toFixed(1), stackPreview: l.stack?.split("\n").slice(2, 5).join(" -> ") ?? null })); this.logger.warn("Potential database connection leaks detected", { leakCount: leaks.length, leakSummary: JSON.stringify(leakDetails) }); } } /** * Check if pool can accept new connections */ canAcquire() { return this.activeConnections.size < this.maxPoolSize; } /** * Get current pool statistics */ getStats() { return { activeConnections: this.activeConnections.size, maxPoolSize: this.maxPoolSize, totalAcquired: this.totalAcquired, totalReleased: this.totalReleased, utilization: this.activeConnections.size / this.maxPoolSize }; } /** * Cleanup resources */ destroy() { if (this.leakCheckInterval) { clearInterval(this.leakCheckInterval); this.leakCheckInterval = null; } this.activeConnections.clear(); _ConnectionPoolManager.instance = null; } }; __name(_ConnectionPoolManager, "ConnectionPoolManager"); _ConnectionPoolManager.instance = null; var ConnectionPoolManager = _ConnectionPoolManager; var poolManager = null; function getPoolManager(maxPoolSize = 10) { if (!poolManager) { poolManager = ConnectionPoolManager.getInstance(maxPoolSize); } return poolManager; } __name(getPoolManager, "getPoolManager"); function createKnexConfig(config) { const dbType = detectDatabaseType(config); switch (dbType) { case "sqlite": { let filename = config.filename || ":memory:"; if (config.connectionString && config.connectionString.startsWith("sqlite://")) { filename = config.connectionString.replace("sqlite://", ""); } return { client: "sqlite3", connection: { filename }, useNullAsDefault: true, migrations: { directory: "./migrations" } }; } case "postgres": { const maxPoolSize = config.maxPoolSize ?? 10; const minPoolSize = config.minPoolSize ?? 2; const manager = getPoolManager(maxPoolSize); if (!config.connectionString) { throw new Error("PostgreSQL requires DB_URL connection string to be set"); } return { client: "pg", connection: config.connectionString, pool: { min: minPoolSize, max: maxPoolSize, acquireTimeoutMillis: 3e4, idleTimeoutMillis: 3e4, // Track connection creation for pool monitoring // Note: Knex/tarn pool only supports afterCreate hook natively // Release tracking is handled via periodic leak detection in ConnectionPoolManager // The leak detection timer (every 10 seconds) will identify connections held too long afterCreate: /* @__PURE__ */ __name((conn, done) => { manager.onAcquire(); done(null, conn); }, "afterCreate") }, migrations: { directory: "./migrations" } }; } default: throw new Error(`Unsupported database type: ${dbType}`); } } __name(createKnexConfig, "createKnexConfig"); // src/database/encryption.ts import { createCipheriv, createDecipheriv, randomBytes, scrypt, pbkdf2 } from "crypto"; import { promisify } from "util"; var scryptAsync = promisify(scrypt); var pbkdf2Async = promisify(pbkdf2); var _EncryptionService = class _EncryptionService { // OWASP recommended minimum constructor(config) { this.keyCache = /* @__PURE__ */ new Map(); // Prevent unbounded memory growth // Current encryption version for future key rotation support this.CURRENT_VERSION = 1; this.ALGORITHM = "aes-256-gcm"; this.IV_LENGTH = 12; // 12 bytes for GCM this.TAG_LENGTH = 16; // 16 bytes for GCM auth tag this.SALT_LENGTH = 32; // 32 bytes for salt this.PBKDF2_ITERATIONS = 1e5; this.config = config; if (config.enabled && !config.masterKey) { throw new Error("ENCRYPTION_MASTER_KEY is required when encryption is enabled"); } if (config.enabled && config.masterKey.length < 32) { throw new Error("ENCRYPTION_MASTER_KEY must be at least 32 characters long"); } } /** * Check if encryption is enabled */ isEnabled() { return this.config.enabled; } /** * Derive a field-specific encryption key using secure salt generation * Uses LRU (Least Recently Used) cache eviction strategy */ async deriveKey(fieldName) { const cacheKey = `${fieldName}_v${this.CURRENT_VERSION}`; if (this.keyCache.has(cacheKey)) { const cachedKey = this.keyCache.get(cacheKey); this.keyCache.delete(cacheKey); this.keyCache.set(cacheKey, cachedKey); return cachedKey; } const fieldBuffer = Buffer.from(fieldName, "utf8"); const versionBuffer = Buffer.from(this.CURRENT_VERSION.toString(), "utf8"); const contextBuffer = Buffer.concat([fieldBuffer, versionBuffer]); const salt = await pbkdf2Async( this.config.masterKey, contextBuffer, this.PBKDF2_ITERATIONS, this.SALT_LENGTH, "sha256" ); const derivedKey = await scryptAsync(this.config.masterKey, salt, 32); if (this.keyCache.size >= _EncryptionService.MAX_CACHE_SIZE) { const keysIterator = this.keyCache.keys().next(); if (!keysIterator.done && keysIterator.value !== void 0) { this.keyCache.delete(keysIterator.value); } } this.keyCache.set(cacheKey, derivedKey); return derivedKey; } /** * Encrypt a string value */ async encrypt(value, fieldName) { if (!this.config.enabled) { return value; } if (value === null || value === void 0 || value === "") { return value; } if (this.isEncrypted(value)) { return value; } try { const key = await this.deriveKey(fieldName); const iv = randomBytes(this.IV_LENGTH); const cipher = createCipheriv(this.ALGORITHM, key, iv); let encrypted = cipher.update(value, "utf8", "base64"); encrypted += cipher.final("base64"); const tag = cipher.getAuthTag(); const encryptedData = { iv: iv.toString("base64"), encrypted, tag: tag.toString("base64"), version: this.CURRENT_VERSION }; return `enc:${encryptedData.version}:${encryptedData.iv}:${encryptedData.encrypted}:${encryptedData.tag}`; } catch (error) { throw new Error( `Encryption failed for field ${fieldName}: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Decrypt a string value */ async decrypt(value, fieldName) { if (!this.config.enabled) { return value; } if (value === null || value === void 0 || value === "") { return value; } if (!this.isEncrypted(value)) { return value; } try { const encryptedData = this.parseEncryptedData(value); const key = await this.deriveKey(fieldName); const decipher = createDecipheriv( this.ALGORITHM, key, Buffer.from(encryptedData.iv, "base64") ); decipher.setAuthTag(Buffer.from(encryptedData.tag, "base64")); let decrypted = decipher.update(encryptedData.encrypted, "base64", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } catch (error) { throw new Error( `Decryption failed for field ${fieldName}: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Check if a value appears to be encrypted */ isEncrypted(value) { if (typeof value !== "string") return false; return value.startsWith("enc:") && value.split(":").length === 5; } /** * Parse encrypted data string into components */ parseEncryptedData(encryptedString) { const parts = encryptedString.split(":"); if (parts.length !== 5 || parts[0] !== "enc") { throw new Error("Invalid encrypted data format"); } return { version: parseInt(parts[1], 10), iv: parts[2], encrypted: parts[3], tag: parts[4] }; } /** * Encrypt JSON metadata */ async encryptJSON(value, fieldName) { if (!value) return null; const jsonString = typeof value === "string" ? value : JSON.stringify(value); return this.encrypt(jsonString, fieldName); } /** * Decrypt JSON metadata */ async decryptJSON(value, fieldName) { if (!value) return null; const decrypted = await this.decrypt(value, fieldName); if (!decrypted) return null; try { const parsed = JSON.parse(decrypted); if (typeof parsed !== "object" || parsed === null) { if (typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean") { return decrypted; } throw new Error("Invalid decrypted data format: expected object or valid primitive"); } return parsed; } catch (error) { if (error instanceof SyntaxError) { return decrypted; } throw error; } } /** * Clear the key cache (useful for testing or key rotation) */ clearCache() { this.keyCache.clear(); } }; __name(_EncryptionService, "EncryptionService"); _EncryptionService.MAX_CACHE_SIZE = 100; var EncryptionService = _EncryptionService; var _EncryptionServiceSingleton = class _EncryptionServiceSingleton { /** * Get current encryption config from environment */ static getCurrentConfig() { return { enabled: process.env.ENCRYPTION_ENABLED === "true", masterKey: process.env.ENCRYPTION_MASTER_KEY || "", algorithm: process.env.ENCRYPTION_ALGORITHM || "aes-256-gcm" }; } /** * Check if config has changed */ static hasConfigChanged(newConfig) { if (!this.cachedConfig) return true; return this.cachedConfig.enabled !== newConfig.enabled || this.cachedConfig.masterKey !== newConfig.masterKey || this.cachedConfig.algorithm !== newConfig.algorithm; } /** * Get or create the encryption service instance * Thread-safe singleton with proper mutex pattern for JavaScript async */ static getInstance() { const currentConfig2 = this.getCurrentConfig(); if (this.instance && !this.hasConfigChanged(currentConfig2)) { return this.instance; } if (this.initializationLock) { if (this.instance) { return this.instance; } const fallbackInstance = new EncryptionService(currentConfig2); return fallbackInstance; } this.initializationLock = true; try { if (this.instance