diff --git a/scripts/security-check.ts b/scripts/security-check.ts index e9f3bc973..7f147cc3a 100755 --- a/scripts/security-check.ts +++ b/scripts/security-check.ts @@ -42,8 +42,8 @@ const IGNORED_VULNERABLE_PACKAGES = [ "jsondiffpatch", // XSS in ai dependency - tracked separately "undici", // DoS in transitive dependency - requires upstream fix "ai", // File upload bypass - planned upgrade - "lodash", // Code injection in @semantic-release dev dep - no patch available - "lodash-es", // Code injection in @semantic-release dev dep - no patch available + "lodash", // TODO: track in #xxx — no patch available upstream for @semantic-release dev dep + "lodash-es", // TODO: track in #xxx — no patch available upstream for @semantic-release dev dep ]; interface SecurityIssue { diff --git a/src/cli/commands/mcp.ts b/src/cli/commands/mcp.ts index f315b2452..c42819120 100644 --- a/src/cli/commands/mcp.ts +++ b/src/cli/commands/mcp.ts @@ -3034,6 +3034,19 @@ ${tools.length > 0 ? tools.map((t) => `- **${t}**: TODO: Add description`).join( const serverId = argv.server as string | undefined; const foundTool = this.findToolForAnnotation(servers, toolName, serverId); + if (foundTool === "ambiguous") { + logger.error( + chalk.red( + `Tool '${toolName}' exists on multiple servers. Use --server to specify which server to annotate.`, + ), + ); + logger.always( + chalk.yellow( + "Use 'neurolink mcp annotate --list' to see available tools and their server IDs.", + ), + ); + process.exit(1); + } if (!foundTool) { logger.error( chalk.red( @@ -3171,7 +3184,9 @@ ${tools.length > 0 ? tools.map((t) => `- **${t}**: TODO: Add description`).join( servers: MCPServerInfo[], toolName: string, serverId?: string, - ): AnnotatedToolTarget | null { + ): AnnotatedToolTarget | null | "ambiguous" { + const matches: AnnotatedToolTarget[] = []; + for (const server of servers) { if (serverId && server.id !== serverId) { continue; @@ -3179,17 +3194,25 @@ ${tools.length > 0 ? tools.map((t) => `- **${t}**: TODO: Add description`).join( for (const tool of server.tools || []) { if (tool.name === toolName) { - return { + matches.push({ name: tool.name, description: tool.description, serverId: server.id, serverName: server.name, - }; + }); } } } - return null; + if (matches.length === 0) { + return null; + } + + if (matches.length > 1) { + return "ambiguous"; + } + + return matches[0] ?? null; } private static buildAnnotationsFromArgs( diff --git a/src/lib/core/factory.ts b/src/lib/core/factory.ts index 1691a255e..af024a74f 100644 --- a/src/lib/core/factory.ts +++ b/src/lib/core/factory.ts @@ -7,7 +7,7 @@ import { ProviderRegistry } from "../factories/providerRegistry.js"; import { getBestProvider } from "../utils/providerUtils.js"; import { logger } from "../utils/logger.js"; import { dynamicModelProvider } from "./dynamicModels.js"; -import { withTimeout } from "../utils/errorHandling.js"; +import { withTimeout, ErrorFactory } from "../utils/errorHandling.js"; import type { AIProvider, SupportedModelName } from "../types/index.js"; import { AIProviderName } from "../constants/enums.js"; import type { UnknownRecord } from "../types/common.js"; @@ -47,7 +47,7 @@ export class AIProviderFactory { await withTimeout( dynamicModelProvider.initialize(), INIT_TIMEOUT, - new Error("Dynamic provider initialization timeout"), + ErrorFactory.toolTimeout("dynamic-provider-init", INIT_TIMEOUT), ); logger.debug( @@ -251,7 +251,7 @@ export class AIProviderFactory { await withTimeout( ProviderRegistry.registerAllProviders(), 30_000, - new Error("Provider registration timed out"), + ErrorFactory.toolTimeout("provider-registration", 30_000), ); const normalizedName = this.normalizeProviderName(providerName); @@ -275,7 +275,7 @@ export class AIProviderFactory { region, ), 30_000, - new Error(`Provider creation timed out for ${normalizedName}`), + ErrorFactory.toolTimeout(`provider-creation:${normalizedName}`, 30_000), ); return { normalizedName, finalModelName, provider }; diff --git a/src/lib/evaluation/scorers/scorerRegistry.ts b/src/lib/evaluation/scorers/scorerRegistry.ts index a6d0cadda..bf1f11fc4 100644 --- a/src/lib/evaluation/scorers/scorerRegistry.ts +++ b/src/lib/evaluation/scorers/scorerRegistry.ts @@ -473,8 +473,9 @@ export class ScorerRegistry { logger.debug( `Registered ${ScorerRegistry.scorers.size} built-in scorers (including aliases)`, ); - } finally { - // Keep initPromise for future callers to await + } catch (err) { + ScorerRegistry.initPromise = null; // allow retry on next call + throw err; } })(); diff --git a/src/lib/proxy/requestLogger.ts b/src/lib/proxy/requestLogger.ts index cecd0173a..943002fa6 100644 --- a/src/lib/proxy/requestLogger.ts +++ b/src/lib/proxy/requestLogger.ts @@ -641,7 +641,7 @@ export async function logBodyCapture( const redactedHeaders = redactHeaders(entry.headers); const preparedBody = prepareRedactedBody(entry.body); - let stored: StoredBodyArtifact = {}; + let stored: StoredBodyArtifact; try { stored = await writeBodyArtifact( entry, @@ -649,8 +649,16 @@ export async function logBodyCapture( preparedBody.value, preparedBody.truncated, ); - } catch { - // Best-effort artifact persistence; continue with in-memory metadata only. + } catch (writeError) { + logger.warn( + "[RequestLogger] writeBodyArtifact failed, falling back to in-memory body for OTLP", + { error: writeError }, + ); + stored = { + redactedBody: preparedBody.value, + redactedBodyBytes: preparedBody.bytes, + bodyTruncated: preparedBody.truncated, + }; } const dateStr = new Date(entry.timestamp).toISOString().split("T")[0]; diff --git a/src/lib/tasks/store/redisTaskStore.ts b/src/lib/tasks/store/redisTaskStore.ts index 72fdb5d22..a9fa280c4 100644 --- a/src/lib/tasks/store/redisTaskStore.ts +++ b/src/lib/tasks/store/redisTaskStore.ts @@ -245,27 +245,41 @@ export class RedisTaskStore implements TaskStore { const ttlSeconds = Math.ceil(ttlMs / 1000); // Set TTL on associated keys best-effort. A successful task write should not // be surfaced as a failure just because the retention metadata could not be updated. - client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => { - logger.warn( - "[TaskStore:Redis] Failed to set TTL on task runs key — task data may outlive retention window", - { - taskId: task.id, - key: taskRunsKey(task.id), - ttlSeconds, - error: String(err), - }, - ); - }); - client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => { - logger.warn( - "[TaskStore:Redis] Failed to set TTL on task history key — task data may outlive retention window", - { - taskId: task.id, - key: taskHistoryKey(task.id), - ttlSeconds, - error: String(err), - }, - ); - }); + void (async () => { + const runsKey = taskRunsKey(task.id); + for (let attempt = 1; attempt <= 3; attempt++) { + try { + await client.expire(runsKey, ttlSeconds); + break; + } catch (err) { + if (attempt === 3) { + logger.warn( + "[TaskStore:Redis] expire failed after 3 attempts on task runs key — task data may outlive retention window", + { taskId: task.id, key: runsKey, ttlSeconds, err: String(err) }, + ); + } else { + await new Promise((r) => setTimeout(r, 100 * attempt)); + } + } + } + })(); + void (async () => { + const histKey = taskHistoryKey(task.id); + for (let attempt = 1; attempt <= 3; attempt++) { + try { + await client.expire(histKey, ttlSeconds); + break; + } catch (err) { + if (attempt === 3) { + logger.warn( + "[TaskStore:Redis] expire failed after 3 attempts on task history key — task data may outlive retention window", + { taskId: task.id, key: histKey, ttlSeconds, err: String(err) }, + ); + } else { + await new Promise((r) => setTimeout(r, 100 * attempt)); + } + } + } + })(); } }