@juspay/neurolink 13.8.17 → 13.8.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +1 -1
  3. package/dist/action/actionExecutor.d.ts +33 -3
  4. package/dist/action/actionExecutor.js +71 -15
  5. package/dist/action/actionInputs.js +4 -1
  6. package/dist/action/cliVersion.d.ts +25 -0
  7. package/dist/action/cliVersion.js +26 -0
  8. package/dist/action/index.d.ts +2 -1
  9. package/dist/action/index.js +2 -1
  10. package/dist/adapters/video/directorPipeline.js +46 -36
  11. package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
  12. package/dist/adapters/video/ffmpegAdapter.js +32 -5
  13. package/dist/adapters/video/frameExtractor.js +1 -1
  14. package/dist/adapters/video/videoMerger.js +5 -2
  15. package/dist/agent/arithmeticEvaluator.d.ts +32 -0
  16. package/dist/agent/arithmeticEvaluator.js +349 -0
  17. package/dist/agent/directTools.d.ts +6 -18
  18. package/dist/agent/directTools.js +17 -56
  19. package/dist/artifacts/artifactStore.d.ts +22 -1
  20. package/dist/artifacts/artifactStore.js +66 -6
  21. package/dist/auth/anthropicOAuth.d.ts +26 -1
  22. package/dist/auth/anthropicOAuth.js +81 -5
  23. package/dist/auth/secureFiles.d.ts +37 -0
  24. package/dist/auth/secureFiles.js +95 -0
  25. package/dist/auth/tokenStore.d.ts +43 -6
  26. package/dist/auth/tokenStore.js +260 -79
  27. package/dist/avatar/providers/DIDAvatar.js +49 -45
  28. package/dist/avatar/providers/ReplicateAvatar.js +33 -40
  29. package/dist/browser/neurolink.min.js +485 -558
  30. package/dist/cli/commands/auth.js +112 -70
  31. package/dist/cli/commands/proxy.d.ts +4 -0
  32. package/dist/cli/commands/proxy.js +128 -13
  33. package/dist/cli/commands/serve.d.ts +10 -1
  34. package/dist/cli/commands/serve.js +98 -17
  35. package/dist/cli/commands/server.js +17 -5
  36. package/dist/cli/index.js +146 -5
  37. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  38. package/dist/cli/utils/envManager.js +17 -3
  39. package/dist/cli/utils/serverUtils.d.ts +39 -3
  40. package/dist/cli/utils/serverUtils.js +63 -6
  41. package/dist/core/baseProvider.js +7 -1
  42. package/dist/core/modules/MessageBuilder.js +5 -0
  43. package/dist/core/modules/ToolsManager.d.ts +46 -1
  44. package/dist/core/modules/ToolsManager.js +190 -43
  45. package/dist/files/fileReferenceRegistry.d.ts +2 -23
  46. package/dist/files/fileReferenceRegistry.js +55 -5
  47. package/dist/hitl/hitlManager.js +1 -0
  48. package/dist/index.d.ts +2 -1
  49. package/dist/index.js +6 -1
  50. package/dist/mcp/auth/tokenStorage.d.ts +7 -0
  51. package/dist/mcp/auth/tokenStorage.js +23 -4
  52. package/dist/mcp/externalServerManager.d.ts +13 -3
  53. package/dist/mcp/externalServerManager.js +154 -136
  54. package/dist/mcp/index.d.ts +2 -0
  55. package/dist/mcp/index.js +3 -0
  56. package/dist/mcp/mcpClientFactory.d.ts +14 -0
  57. package/dist/mcp/mcpClientFactory.js +70 -7
  58. package/dist/mcp/projectConfigTrust.d.ts +72 -0
  59. package/dist/mcp/projectConfigTrust.js +237 -0
  60. package/dist/mcp/transportUrlPolicy.d.ts +39 -0
  61. package/dist/mcp/transportUrlPolicy.js +172 -0
  62. package/dist/music/providers/BeatovenMusic.js +35 -46
  63. package/dist/music/providers/ReplicateMusic.js +33 -40
  64. package/dist/neurolink.d.ts +16 -1
  65. package/dist/neurolink.js +64 -11
  66. package/dist/processors/archive/ArchiveProcessor.js +15 -1
  67. package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
  68. package/dist/processors/archive/zipEntryIndex.js +33 -0
  69. package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
  70. package/dist/processors/base/BaseFileProcessor.js +40 -111
  71. package/dist/processors/data/YamlProcessor.d.ts +13 -1
  72. package/dist/processors/data/YamlProcessor.js +93 -2
  73. package/dist/processors/document/OpenDocumentProcessor.js +7 -2
  74. package/dist/processors/document/PptxProcessor.js +12 -7
  75. package/dist/processors/media/VideoProcessor.js +93 -15
  76. package/dist/proxy/inboundAuth.d.ts +89 -0
  77. package/dist/proxy/inboundAuth.js +219 -0
  78. package/dist/proxy/tokenRefresh.d.ts +6 -0
  79. package/dist/proxy/tokenRefresh.js +13 -1
  80. package/dist/proxy/updateState.js +4 -4
  81. package/dist/rag/document/loaders.d.ts +0 -6
  82. package/dist/rag/document/loaders.js +16 -10
  83. package/dist/rag/ragIntegration.d.ts +2 -2
  84. package/dist/rag/ragIntegration.js +35 -10
  85. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  86. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  87. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  88. package/dist/server/abstract/baseServerAdapter.js +119 -6
  89. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  90. package/dist/server/adapters/expressAdapter.js +78 -6
  91. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  92. package/dist/server/adapters/fastifyAdapter.js +165 -70
  93. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  94. package/dist/server/adapters/honoAdapter.js +99 -21
  95. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  96. package/dist/server/adapters/koaAdapter.js +153 -74
  97. package/dist/server/index.d.ts +2 -0
  98. package/dist/server/index.js +4 -0
  99. package/dist/server/middleware/auth.js +6 -2
  100. package/dist/server/middleware/cache.js +6 -1
  101. package/dist/server/middleware/rateLimit.js +29 -4
  102. package/dist/server/routes/agentRoutes.js +24 -11
  103. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  104. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  105. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  106. package/dist/server/routes/codexProxyRoutes.js +125 -40
  107. package/dist/server/routes/index.d.ts +4 -0
  108. package/dist/server/routes/index.js +35 -4
  109. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  110. package/dist/server/routes/mcpRoutes.js +21 -7
  111. package/dist/server/routes/memoryRoutes.js +7 -6
  112. package/dist/server/routes/toolRoutes.d.ts +12 -2
  113. package/dist/server/routes/toolRoutes.js +33 -3
  114. package/dist/server/routes/transcribeRoutes.js +60 -13
  115. package/dist/server/utils/clientAddress.d.ts +23 -0
  116. package/dist/server/utils/clientAddress.js +65 -0
  117. package/dist/server/utils/clientError.d.ts +35 -0
  118. package/dist/server/utils/clientError.js +129 -0
  119. package/dist/server/utils/pathMatch.d.ts +21 -0
  120. package/dist/server/utils/pathMatch.js +41 -0
  121. package/dist/server/utils/validation.d.ts +9 -0
  122. package/dist/server/utils/validation.js +9 -0
  123. package/dist/server/websocket/WebSocketHandler.js +22 -3
  124. package/dist/server/websocket/frameQueue.d.ts +19 -0
  125. package/dist/server/websocket/frameQueue.js +41 -3
  126. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  127. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  128. package/dist/skills/skillStores.js +5 -0
  129. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  130. package/dist/tasks/store/fileTaskStore.js +11 -4
  131. package/dist/types/attachments.d.ts +117 -0
  132. package/dist/types/attachments.js +15 -0
  133. package/dist/types/auth.d.ts +6 -0
  134. package/dist/types/cli.d.ts +4 -0
  135. package/dist/types/config.d.ts +56 -0
  136. package/dist/types/file.d.ts +8 -0
  137. package/dist/types/hitl.d.ts +8 -0
  138. package/dist/types/index.d.ts +1 -0
  139. package/dist/types/index.js +1 -0
  140. package/dist/types/mcp.d.ts +98 -0
  141. package/dist/types/multimodal.js +0 -41
  142. package/dist/types/processor.d.ts +14 -1
  143. package/dist/types/proxy.d.ts +62 -3
  144. package/dist/types/rag.d.ts +9 -0
  145. package/dist/types/safeFetch.d.ts +40 -0
  146. package/dist/types/server.d.ts +206 -10
  147. package/dist/types/tools.d.ts +57 -0
  148. package/dist/types/transcribe.d.ts +6 -0
  149. package/dist/utils/attachmentPolicy.d.ts +89 -0
  150. package/dist/utils/attachmentPolicy.js +327 -0
  151. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  152. package/dist/utils/dotenvBootstrap.js +172 -31
  153. package/dist/utils/fileDetector.js +115 -65
  154. package/dist/utils/json/coerce.js +11 -1
  155. package/dist/utils/logSanitize.d.ts +5 -0
  156. package/dist/utils/logSanitize.js +8 -0
  157. package/dist/utils/logger.js +73 -1
  158. package/dist/utils/messageBuilder.d.ts +2 -2
  159. package/dist/utils/messageBuilder.js +118 -105
  160. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  161. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  162. package/dist/utils/redis.d.ts +12 -0
  163. package/dist/utils/redis.js +72 -0
  164. package/dist/utils/safeFetch.d.ts +22 -1
  165. package/dist/utils/safeFetch.js +132 -38
  166. package/dist/utils/safeStoreId.d.ts +19 -0
  167. package/dist/utils/safeStoreId.js +29 -0
  168. package/dist/utils/schemaConversion.d.ts +30 -3
  169. package/dist/utils/schemaConversion.js +457 -84
  170. package/dist/utils/sizeGuard.js +2 -2
  171. package/dist/utils/ssrfGuard.d.ts +3 -3
  172. package/dist/utils/ssrfGuard.js +71 -39
  173. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  174. package/dist/voice/eval/sttEvalStore.js +77 -12
  175. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  176. package/dist/voice/livekit/agentDefinition.js +22 -0
  177. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  178. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  179. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  180. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  181. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  182. package/dist/voice/livekit/voiceAgent.js +2 -1
  183. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  184. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  185. package/dist/voice/providers/GeminiLive.js +21 -6
  186. package/dist/voice/providers/GoogleSTT.js +8 -5
  187. package/dist/voice/transcribe.d.ts +2 -2
  188. package/dist/voice/transcribe.js +34 -18
  189. package/dist/voice/whistle/audio.js +11 -5
  190. package/docs-site/static/search-index.json +71 -49
  191. package/package.json +171 -156
  192. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -10,8 +10,8 @@
10
10
  import { ErrorCategory, ErrorSeverity } from "../../constants/enums.js";
11
11
  import { logger } from "../../utils/logger.js";
12
12
  import { MUSIC_ERROR_CODES, MusicError } from "../../utils/musicProcessor.js";
13
- import { MAX_AUDIO_BYTES, readBoundedBuffer } from "../../utils/sizeGuard.js";
14
- import { assertSafeUrl } from "../../utils/ssrfGuard.js";
13
+ import { MAX_AUDIO_BYTES } from "../../utils/sizeGuard.js";
14
+ import { safeDownload } from "../../utils/safeFetch.js";
15
15
  const DEFAULT_BASE_URL = "https://public-api.beatoven.ai";
16
16
  const REQUEST_TIMEOUT_MS = 30_000;
17
17
  const POLL_INTERVAL_MS = 3_000;
@@ -263,26 +263,20 @@ export class BeatovenMusic {
263
263
  return (await response.json());
264
264
  }
265
265
  async downloadTrack(url) {
266
+ // `safeDownload` validates the provider-returned URL (HTTPS, public host),
267
+ // pins the connection to the vetted addresses, refuses redirects and
268
+ // bounds the body while it streams — where this was a one-off URL check
269
+ // followed by a plain `fetch`, which left redirects and a rebinding
270
+ // resolver uncovered.
266
271
  try {
267
- await assertSafeUrl(url);
268
- }
269
- catch (err) {
270
- throw new MusicError({
271
- code: MUSIC_ERROR_CODES.GENERATION_FAILED,
272
- message: `Beatoven track URL rejected: ${err instanceof Error ? err.message : String(err)}`,
273
- category: ErrorCategory.VALIDATION,
274
- severity: ErrorSeverity.HIGH,
275
- retriable: false,
276
- context: { url },
272
+ return await safeDownload(url, {
273
+ maxBytes: MAX_AUDIO_BYTES,
274
+ label: "Beatoven track",
275
+ timeoutMs: REQUEST_TIMEOUT_MS,
277
276
  });
278
277
  }
279
- const controller = new AbortController();
280
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
281
- let response;
282
- try {
283
- response = await fetch(url, { signal: controller.signal });
284
- }
285
278
  catch (err) {
279
+ const message = err instanceof Error ? err.message : String(err);
286
280
  if (err instanceof Error && err.name === "AbortError") {
287
281
  throw new MusicError({
288
282
  code: MUSIC_ERROR_CODES.GENERATION_FAILED,
@@ -293,41 +287,36 @@ export class BeatovenMusic {
293
287
  originalError: err,
294
288
  });
295
289
  }
290
+ if (/rejected|permitted|blocked range|redirect/i.test(message)) {
291
+ throw new MusicError({
292
+ code: MUSIC_ERROR_CODES.GENERATION_FAILED,
293
+ message: `Beatoven track URL rejected: ${message}`,
294
+ category: ErrorCategory.VALIDATION,
295
+ severity: ErrorSeverity.HIGH,
296
+ retriable: false,
297
+ context: { url },
298
+ });
299
+ }
300
+ if (/too large|size cap/i.test(message)) {
301
+ throw new MusicError({
302
+ code: MUSIC_ERROR_CODES.GENERATION_FAILED,
303
+ message: `Beatoven track download exceeded size limit: ${message}`,
304
+ category: ErrorCategory.EXECUTION,
305
+ severity: ErrorSeverity.HIGH,
306
+ retriable: false,
307
+ context: { url },
308
+ });
309
+ }
310
+ const status = /HTTP (\d{3})/.exec(message)?.[1];
296
311
  throw new MusicError({
297
312
  code: MUSIC_ERROR_CODES.GENERATION_FAILED,
298
- message: `Beatoven track download network error: ${err instanceof Error ? err.message : String(err)}`,
313
+ message: `Beatoven track download failed: ${message}`,
299
314
  category: ErrorCategory.NETWORK,
300
315
  severity: ErrorSeverity.MEDIUM,
301
- retriable: true,
316
+ retriable: status === undefined || Number(status) >= 500,
302
317
  originalError: err instanceof Error ? err : undefined,
303
318
  context: { url },
304
319
  });
305
320
  }
306
- finally {
307
- clearTimeout(timeoutId);
308
- }
309
- if (!response.ok) {
310
- throw new MusicError({
311
- code: MUSIC_ERROR_CODES.GENERATION_FAILED,
312
- message: `Beatoven track download failed: ${response.status}`,
313
- category: ErrorCategory.NETWORK,
314
- severity: ErrorSeverity.MEDIUM,
315
- retriable: response.status >= 500,
316
- context: { status: response.status, url },
317
- });
318
- }
319
- try {
320
- return await readBoundedBuffer(response, MAX_AUDIO_BYTES, "Beatoven track");
321
- }
322
- catch (err) {
323
- throw new MusicError({
324
- code: MUSIC_ERROR_CODES.GENERATION_FAILED,
325
- message: `Beatoven track download exceeded size limit: ${err instanceof Error ? err.message : String(err)}`,
326
- category: ErrorCategory.EXECUTION,
327
- severity: ErrorSeverity.HIGH,
328
- retriable: false,
329
- context: { url },
330
- });
331
- }
332
321
  }
333
322
  }
@@ -13,8 +13,8 @@ import { MUSIC_ERROR_CODES, MusicError } from "../../utils/musicProcessor.js";
13
13
  import { logger } from "../../utils/logger.js";
14
14
  import { getReplicateAuth } from "../../adapters/replicate/auth.js";
15
15
  import { downloadPredictionOutput, predict, } from "../../adapters/replicate/predictionLifecycle.js";
16
- import { MAX_AUDIO_BYTES, readBoundedBuffer } from "../../utils/sizeGuard.js";
17
- import { assertSafeUrl } from "../../utils/ssrfGuard.js";
16
+ import { MAX_AUDIO_BYTES } from "../../utils/sizeGuard.js";
17
+ import { safeDownload } from "../../utils/safeFetch.js";
18
18
  const DEFAULT_MODEL = "meta/musicgen:7be0f12c54a8d033a0fbd14418c9af98962da9a86f5ff7811f9b3423a1f0b7d7";
19
19
  export class ReplicateMusic {
20
20
  maxDurationSeconds = 30;
@@ -154,27 +154,18 @@ export class ReplicateMusic {
154
154
  retriable: false,
155
155
  });
156
156
  }
157
- try {
158
- await assertSafeUrl(input);
159
- }
160
- catch (err) {
161
- throw new MusicError({
162
- code: MUSIC_ERROR_CODES.INVALID_INPUT,
163
- message: `Unsafe URL rejected: ${err instanceof Error ? err.message : String(err)}`,
164
- category: ErrorCategory.VALIDATION,
165
- severity: ErrorSeverity.HIGH,
166
- retriable: false,
167
- context: { url: input },
168
- });
169
- }
157
+ // `safeDownload`: URL vetted (HTTPS, public host), connection pinned to
158
+ // the vetted addresses, redirects refused, body bounded while streaming.
170
159
  const FETCH_TIMEOUT_MS = 60_000;
171
- const controller = new AbortController();
172
- const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
173
- let r;
174
160
  try {
175
- r = await fetch(input, { signal: controller.signal });
161
+ return await safeDownload(input, {
162
+ maxBytes: MAX_AUDIO_BYTES,
163
+ label: "Replicate reference audio",
164
+ timeoutMs: FETCH_TIMEOUT_MS,
165
+ });
176
166
  }
177
167
  catch (err) {
168
+ const message = err instanceof Error ? err.message : String(err);
178
169
  if (err instanceof Error && err.name === "AbortError") {
179
170
  throw new MusicError({
180
171
  code: MUSIC_ERROR_CODES.GENERATION_FAILED,
@@ -184,31 +175,33 @@ export class ReplicateMusic {
184
175
  retriable: true,
185
176
  });
186
177
  }
187
- throw err;
188
- }
189
- finally {
190
- clearTimeout(timeoutId);
191
- }
192
- if (!r.ok) {
178
+ if (/rejected|permitted|blocked range|redirect/i.test(message)) {
179
+ throw new MusicError({
180
+ code: MUSIC_ERROR_CODES.INVALID_INPUT,
181
+ message: `Unsafe URL rejected: ${message}`,
182
+ category: ErrorCategory.VALIDATION,
183
+ severity: ErrorSeverity.HIGH,
184
+ retriable: false,
185
+ context: { url: input },
186
+ });
187
+ }
188
+ if (/too large|size cap/i.test(message)) {
189
+ throw new MusicError({
190
+ code: MUSIC_ERROR_CODES.INVALID_INPUT,
191
+ message: `Replicate reference audio too large: ${message}`,
192
+ category: ErrorCategory.VALIDATION,
193
+ severity: ErrorSeverity.HIGH,
194
+ retriable: false,
195
+ context: { url: input },
196
+ });
197
+ }
198
+ const status = /HTTP (\d{3})/.exec(message)?.[1];
193
199
  throw new MusicError({
194
200
  code: MUSIC_ERROR_CODES.GENERATION_FAILED,
195
- message: `Failed to fetch reference audio: ${input}: ${r.status}`,
201
+ message: `Failed to fetch reference audio: ${input}: ${message}`,
196
202
  category: ErrorCategory.NETWORK,
197
203
  severity: ErrorSeverity.MEDIUM,
198
- retriable: r.status >= 500,
199
- });
200
- }
201
- try {
202
- return await readBoundedBuffer(r, MAX_AUDIO_BYTES, "Replicate reference audio");
203
- }
204
- catch (err) {
205
- throw new MusicError({
206
- code: MUSIC_ERROR_CODES.INVALID_INPUT,
207
- message: `Replicate reference audio too large: ${err instanceof Error ? err.message : String(err)}`,
208
- category: ErrorCategory.VALIDATION,
209
- severity: ErrorSeverity.HIGH,
210
- retriable: false,
211
- context: { url: input },
204
+ retriable: status === undefined || Number(status) >= 500,
212
205
  });
213
206
  }
214
207
  }
@@ -194,6 +194,13 @@ export declare class NeuroLink {
194
194
  private hitlManager?;
195
195
  private _sessionCostUsd;
196
196
  private fileRegistry;
197
+ /**
198
+ * What this instance may read or fetch when a call names a file path or a
199
+ * URL: resolved once from `config.attachments` and the
200
+ * `NEUROLINK_ATTACHMENT_*` variables, stamped onto every call's options so
201
+ * the loaders see the same answer. See `utils/attachmentPolicy.ts`.
202
+ */
203
+ private readonly attachmentPolicy;
197
204
  private cachedFileTools;
198
205
  private memoryInstance?;
199
206
  private memorySDKConfig?;
@@ -591,7 +598,15 @@ export declare class NeuroLink {
591
598
  */
592
599
  private registerDirectToolsServerInternal;
593
600
  /**
594
- * Load MCP configuration from .mcp-config.json with parallel loading for improved performance
601
+ * Load the project MCP configuration, if this instance is allowed one.
602
+ *
603
+ * The working directory is not searched on its own. A file is loaded only
604
+ * when `mcp.configPath` names it, `mcp.trustProjectConfig` opts into
605
+ * `<cwd>/.mcp-config.json`, the CLI has registered its hash-pinned trust
606
+ * decision, or a host set `ProviderRegistry.setOptions({ enableManualMCP:
607
+ * true })` after construction (the constructor resets it to `false`, the
608
+ * SDK default). Otherwise this is a no-op and only servers added through
609
+ * `addExternalMCPServer` / `addInMemoryMCPServer` exist.
595
610
  */
596
611
  private loadMCPConfigurationInternal;
597
612
  /**
package/dist/neurolink.js CHANGED
@@ -6,12 +6,18 @@ import { beginOllamaDiagnostic, captureOllamaDiagnosticContext, completeOllamaDi
6
6
  * Enhanced AI provider system with natural MCP tool access.
7
7
  * Uses real MCP infrastructure for tool discovery and execution.
8
8
  */
9
- // Load environment variables from .env file (critical for SDK usage).
10
- // Shared with the CLI so the two implicit loads cannot drift apart again.
11
- // Neither honoured DOTENV_CONFIG_PATH, and they had already drifted in how
12
- // each suppressed dotenv's banner — one conditional, one unconditional.
13
- import { loadProcessDotenv } from "./utils/dotenvBootstrap.js";
14
- await loadProcessDotenv();
9
+ // The SDK no longer reads `<cwd>/.env` as a side effect of being imported:
10
+ // a library loaded inside an untrusted checkout would pick up that
11
+ // checkout's `*_BASE_URL` overrides and send the host's real keys wherever
12
+ // they pointed. The CLI still loads `.env` at startup (it is interactive and
13
+ // the directory is the user's); an SDK host that wants the same calls
14
+ // `loadDotenv()` from the package root, or starts the process with
15
+ // `NEUROLINK_DOTENV=1` in the real environment, which is honoured here.
16
+ import { isSdkDotenvOptIn, loadProcessDotenv, } from "./utils/dotenvBootstrap.js";
17
+ import { resolveProjectMcpConfigPath } from "./mcp/projectConfigTrust.js";
18
+ if (isSdkDotenvOptIn()) {
19
+ await loadProcessDotenv({ caller: "sdk" });
20
+ }
15
21
  import { SpanKind, SpanStatusCode, context, trace, } from "@opentelemetry/api";
16
22
  import { getPropagatedAttributesFromContext } from "@langfuse/core";
17
23
  import { LangfuseOtelSpanAttributes } from "@langfuse/tracing";
@@ -43,6 +49,7 @@ import { createToolEventPayload } from "./core/toolEvents.js";
43
49
  import { ProviderFactory } from "./factories/providerFactory.js";
44
50
  import { ProviderRegistry } from "./factories/providerRegistry.js";
45
51
  import { FileReferenceRegistry } from "./files/fileReferenceRegistry.js";
52
+ import { resolveAttachmentPolicy } from "./utils/attachmentPolicy.js";
46
53
  import { createFileTools } from "./files/fileTools.js";
47
54
  import { HITLManager, DEFAULT_ALLOW_MODIFICATION } from "./hitl/hitlManager.js";
48
55
  import { ToolCallBatcher } from "./mcp/batching/index.js";
@@ -796,6 +803,13 @@ export class NeuroLink {
796
803
  _sessionCostUsd = 0;
797
804
  // File Reference Registry for lazy on-demand file processing
798
805
  fileRegistry;
806
+ /**
807
+ * What this instance may read or fetch when a call names a file path or a
808
+ * URL: resolved once from `config.attachments` and the
809
+ * `NEUROLINK_ATTACHMENT_*` variables, stamped onto every call's options so
810
+ * the loaders see the same answer. See `utils/attachmentPolicy.ts`.
811
+ */
812
+ attachmentPolicy;
799
813
  // Cached file tools to avoid redundant createFileTools() calls per generate/stream
800
814
  cachedFileTools = null;
801
815
  // Memory instance and config
@@ -1294,6 +1308,7 @@ export class NeuroLink {
1294
1308
  this.toolRegistry.setFileToolRootResolver(() => this.resolveFileToolRoots(undefined));
1295
1309
  }
1296
1310
  this.fileRegistry = new FileReferenceRegistry();
1311
+ this.attachmentPolicy = resolveAttachmentPolicy(config?.attachments);
1297
1312
  this.observabilityConfig = config?.observability;
1298
1313
  if (this.observabilityConfig) {
1299
1314
  ensureTelemetry(this.observabilityConfig);
@@ -2743,12 +2758,32 @@ Current user's request: ${currentInput}`;
2743
2758
  }
2744
2759
  }
2745
2760
  /**
2746
- * Load MCP configuration from .mcp-config.json with parallel loading for improved performance
2761
+ * Load the project MCP configuration, if this instance is allowed one.
2762
+ *
2763
+ * The working directory is not searched on its own. A file is loaded only
2764
+ * when `mcp.configPath` names it, `mcp.trustProjectConfig` opts into
2765
+ * `<cwd>/.mcp-config.json`, the CLI has registered its hash-pinned trust
2766
+ * decision, or a host set `ProviderRegistry.setOptions({ enableManualMCP:
2767
+ * true })` after construction (the constructor resets it to `false`, the
2768
+ * SDK default). Otherwise this is a no-op and only servers added through
2769
+ * `addExternalMCPServer` / `addInMemoryMCPServer` exist.
2747
2770
  */
2748
2771
  async loadMCPConfigurationInternal() {
2749
2772
  try {
2750
- const configResult = await this.externalServerManager.loadMCPConfiguration(undefined, // Use default config path
2751
- { parallel: true });
2773
+ const resolution = await resolveProjectMcpConfigPath({
2774
+ configPath: this.mcpEnhancementsConfig?.configPath,
2775
+ trustProjectConfig: this.mcpEnhancementsConfig?.trustProjectConfig,
2776
+ enableManualMCP: ProviderRegistry.getOptions().enableManualMCP,
2777
+ });
2778
+ if (!resolution) {
2779
+ mcpLogger.debug("[NeuroLink] No project MCP configuration to load: the working directory is not searched unless mcp.configPath, mcp.trustProjectConfig or a CLI trust decision names a file");
2780
+ return;
2781
+ }
2782
+ mcpLogger.debug("[NeuroLink] Loading project MCP configuration", {
2783
+ path: resolution.path,
2784
+ source: resolution.source,
2785
+ });
2786
+ const configResult = await this.externalServerManager.loadMCPConfiguration(resolution.path, { parallel: true });
2752
2787
  mcpLogger.debug("[NeuroLink] MCP configuration loaded successfully", {
2753
2788
  serversLoaded: configResult.serversLoaded,
2754
2789
  errors: configResult.errors.length,
@@ -3339,6 +3374,14 @@ Current user's request: ${currentInput}`;
3339
3374
  catch (error) {
3340
3375
  logger.warn("[NeuroLink] Memory condenser shutdown failed:", error);
3341
3376
  }
3377
+ // Persisted attachments live under a private tmpdir; remove them with
3378
+ // the instance instead of leaving uploads behind for the exit sweep.
3379
+ try {
3380
+ await this.fileRegistry.clear();
3381
+ }
3382
+ catch (error) {
3383
+ logger.warn("[NeuroLink] File registry cleanup failed:", error);
3384
+ }
3342
3385
  try {
3343
3386
  await this.releaseTelemetryHold();
3344
3387
  logger.debug("[NeuroLink] OpenTelemetry shutdown completed");
@@ -4709,7 +4752,7 @@ Current user's request: ${currentInput}`;
4709
4752
  if (options.rag?.files?.length) {
4710
4753
  try {
4711
4754
  const { prepareRAGTool } = await import("./rag/ragIntegration.js");
4712
- const ragResult = await prepareRAGTool(options.rag, options.provider);
4755
+ const ragResult = await prepareRAGTool(options.rag, options.provider, this.attachmentPolicy);
4713
4756
  if (!options.tools) {
4714
4757
  options.tools = {};
4715
4758
  }
@@ -4822,6 +4865,7 @@ Current user's request: ${currentInput}`;
4822
4865
  tts: options.tts,
4823
4866
  stt: options.stt,
4824
4867
  fileRegistry: this.fileRegistry,
4868
+ attachmentPolicy: this.attachmentPolicy,
4825
4869
  timeout: options.timeout,
4826
4870
  turnTimeoutMs: options.turnTimeoutMs,
4827
4871
  stallTimeoutMs: options.stallTimeoutMs,
@@ -7779,6 +7823,7 @@ Current user's request: ${currentInput}`;
7779
7823
  }
7780
7824
  const originalPrompt = options.input?.text ?? "";
7781
7825
  options.fileRegistry = this.fileRegistry;
7826
+ options.attachmentPolicy = this.attachmentPolicy;
7782
7827
  await this.validateStreamRequestOptions(options, startTime);
7783
7828
  const workflowResult = await this.maybeHandleWorkflowStreamRequest({
7784
7829
  options,
@@ -9053,7 +9098,7 @@ Current user's request: ${currentInput}`;
9053
9098
  if (options.rag?.files?.length) {
9054
9099
  try {
9055
9100
  const { prepareRAGTool } = await import("./rag/ragIntegration.js");
9056
- const ragResult = await prepareRAGTool(options.rag, options.provider);
9101
+ const ragResult = await prepareRAGTool(options.rag, options.provider, this.attachmentPolicy);
9057
9102
  // Inject the RAG tool into the tools record
9058
9103
  if (!options.tools) {
9059
9104
  options.tools = {};
@@ -14661,6 +14706,7 @@ Current user's request: ${currentInput}`;
14661
14706
  transcribe: (audio, provider, options) => STTProcessor.transcribe(audio, provider, options, credentials),
14662
14707
  resolveProvider,
14663
14708
  getHandler,
14709
+ attachmentPolicy: this.attachmentPolicy,
14664
14710
  };
14665
14711
  }
14666
14712
  /**
@@ -15888,6 +15934,13 @@ Current user's request: ${currentInput}`;
15888
15934
  catch (error) {
15889
15935
  cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
15890
15936
  }
15937
+ // 0c. Persisted attachments go with the instance (private tmpdir).
15938
+ try {
15939
+ await this.fileRegistry.clear();
15940
+ }
15941
+ catch (error) {
15942
+ logger.warn("[NeuroLink] File registry cleanup failed:", error);
15943
+ }
15891
15944
  // 1. Flush and shutdown OpenTelemetry
15892
15945
  try {
15893
15946
  logger.debug("[NeuroLink] Flushing and shutting down OpenTelemetry...");
@@ -38,6 +38,7 @@
38
38
  import * as path from "path";
39
39
  import { BaseFileProcessor } from "../base/BaseFileProcessor.js";
40
40
  import { isDecompressionBoundExceeded, readZipEntryWithinLimit, } from "./zipEntryReader.js";
41
+ import { indexZipEntriesByName } from "./zipEntryIndex.js";
41
42
  import { SIZE_LIMITS_MB } from "../config/index.js";
42
43
  import { FileErrorCode } from "../errors/index.js";
43
44
  // =============================================================================
@@ -641,8 +642,17 @@ export class ArchiveProcessor extends BaseFileProcessor {
641
642
  };
642
643
  }
643
644
  let cumulativeUncompressedSize = 0;
645
+ // A name that appears twice is one listing with two possible contents:
646
+ // the first occurrence is what every reader here opens, and the
647
+ // repeat is reported rather than listed as if it were a second file.
648
+ const seenNames = new Set();
644
649
  for (const entry of zipEntries) {
645
650
  const entryName = entry.entryName;
651
+ if (seenNames.has(entryName)) {
652
+ securityWarnings.push(`Duplicate entry name "${entryName}" - only the first occurrence is read; the repeat is skipped`);
653
+ continue;
654
+ }
655
+ seenNames.add(entryName);
646
656
  // Security: path traversal check
647
657
  if (this.hasPathTraversal(entryName)) {
648
658
  securityWarnings.push(`Path traversal detected in entry: "${entryName}" - entry skipped`);
@@ -1337,6 +1347,10 @@ export class ArchiveProcessor extends BaseFileProcessor {
1337
1347
  try {
1338
1348
  const AdmZip = (await import("adm-zip")).default;
1339
1349
  const zip = new AdmZip(buffer);
1350
+ // Read from the iterated entries, first occurrence per name — the same
1351
+ // entry the listing described. `zip.getEntry(name)` resolves the LAST
1352
+ // duplicate and would open a different member than the one listed.
1353
+ const { byName } = indexZipEntriesByName(zip.getEntries());
1340
1354
  // Filter to extractable text-based entries within size limits
1341
1355
  const candidates = entries
1342
1356
  .filter((e) => {
@@ -1364,7 +1378,7 @@ export class ArchiveProcessor extends BaseFileProcessor {
1364
1378
  break;
1365
1379
  }
1366
1380
  try {
1367
- const zipEntry = zip.getEntry(entry.name);
1381
+ const zipEntry = byName.get(entry.name);
1368
1382
  if (!zipEntry) {
1369
1383
  continue;
1370
1384
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * First-wins lookup over a ZIP's central directory.
3
+ *
4
+ * A ZIP may carry two entries with the same name. adm-zip's `getEntry(name)`
5
+ * resolves the LAST one, while `getEntries()` lists them in directory order,
6
+ * so a processor that lists with one call and reads with the other can show
7
+ * the user a listing of one file and the contents of another. Every reader
8
+ * here indexes the iterated entries once and reads from that index, so the
9
+ * entry it describes is the entry it opens; a repeated name is reported to
10
+ * the caller rather than silently resolved either way.
11
+ *
12
+ * @module processors/archive/zipEntryIndex
13
+ */
14
+ import type { NamedZipEntry } from "../../types/index.js";
15
+ /**
16
+ * Index `entries` by name, keeping the FIRST occurrence of each. Returns the
17
+ * index and the names that occurred more than once, in first-seen order.
18
+ */
19
+ export declare function indexZipEntriesByName<T extends NamedZipEntry>(entries: readonly T[]): {
20
+ byName: Map<string, T>;
21
+ duplicates: string[];
22
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * First-wins lookup over a ZIP's central directory.
3
+ *
4
+ * A ZIP may carry two entries with the same name. adm-zip's `getEntry(name)`
5
+ * resolves the LAST one, while `getEntries()` lists them in directory order,
6
+ * so a processor that lists with one call and reads with the other can show
7
+ * the user a listing of one file and the contents of another. Every reader
8
+ * here indexes the iterated entries once and reads from that index, so the
9
+ * entry it describes is the entry it opens; a repeated name is reported to
10
+ * the caller rather than silently resolved either way.
11
+ *
12
+ * @module processors/archive/zipEntryIndex
13
+ */
14
+ /**
15
+ * Index `entries` by name, keeping the FIRST occurrence of each. Returns the
16
+ * index and the names that occurred more than once, in first-seen order.
17
+ */
18
+ export function indexZipEntriesByName(entries) {
19
+ const byName = new Map();
20
+ const duplicates = [];
21
+ const flagged = new Set();
22
+ for (const entry of entries) {
23
+ if (byName.has(entry.entryName)) {
24
+ if (!flagged.has(entry.entryName)) {
25
+ flagged.add(entry.entryName);
26
+ duplicates.push(entry.entryName);
27
+ }
28
+ continue;
29
+ }
30
+ byName.set(entry.entryName, entry);
31
+ }
32
+ return { byName, duplicates };
33
+ }
@@ -45,7 +45,7 @@
45
45
  */
46
46
  import "../../services/server/ai/observability/instrumentation.js";
47
47
  import { FileErrorCode } from "../errors/index.js";
48
- import type { BatchProcessingSummary, FileInfo, FileProcessingError, ProcessorFileProcessingResult, FileProcessorConfig, ProcessorOperationResult, ProcessedFileBase, ProcessOptions } from "../../types/index.js";
48
+ import type { AttachmentPolicy, BatchProcessingSummary, FileInfo, FileProcessingError, ProcessorFileProcessingResult, FileProcessorConfig, ProcessorOperationResult, ProcessedFileBase, ProcessOptions } from "../../types/index.js";
49
49
  /**
50
50
  * Marker on the error raised when a processor refuses content for exceeding
51
51
  * the size limit *after* the download — a ZIP entry that expands past the
@@ -192,32 +192,20 @@ export declare abstract class BaseFileProcessor<T extends ProcessedFileBase> {
192
192
  /**
193
193
  * Download file from URL with authentication.
194
194
  *
195
+ * Goes through `downloadAttachment`, which applies the attachment policy
196
+ * (HTTPS and public hosts unless the instance says otherwise), pins the
197
+ * connection to the vetted addresses, re-validates every redirect hop and
198
+ * bounds the body while it streams. `authHeaders` ride on the first request
199
+ * only — they are never replayed to a redirect target.
200
+ *
195
201
  * @param url - URL to download from
196
202
  * @param authHeaders - Optional authentication headers
197
203
  * @param timeout - Optional timeout override
204
+ * @param attachmentPolicy - Policy for the fetch; the environment default when omitted
198
205
  * @returns Downloaded file content as Buffer
199
206
  * @throws Error if download fails
200
207
  */
201
- protected downloadFile(url: string, authHeaders?: Record<string, string>, timeout?: number): Promise<Buffer>;
202
- /**
203
- * Read a response body, refusing it the moment it passes `maxBytes`.
204
- *
205
- * `response.arrayBuffer()` buffers whatever the server sends before anyone
206
- * can object, so the size check further up only ever ran against a body that
207
- * had already been paid for in full. Metering the stream makes the ceiling
208
- * the configured limit instead of the server's choice, and cancelling the
209
- * reader stops the transfer rather than merely stopping our interest in it.
210
- *
211
- * Content-Length is deliberately not trusted as the bound — it is a hint that
212
- * can be absent or a lie — but it is worth honouring when present, because it
213
- * refuses the honest oversized case without reading a byte.
214
- *
215
- * @param response - Fetch response whose body should be read
216
- * @param maxBytes - Ceiling for the body, in bytes
217
- * @returns The body as a Buffer
218
- * @throws When the body exceeds `maxBytes`
219
- */
220
- private readBodyWithinLimit;
208
+ protected downloadFile(url: string, authHeaders?: Record<string, string>, timeout?: number, attachmentPolicy?: AttachmentPolicy): Promise<Buffer>;
221
209
  /**
222
210
  * Download file with retry logic for transient failures.
223
211
  *