@juspay/neurolink 13.8.16 → 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 (194) 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 +83 -8
  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/expressBody.d.ts +33 -0
  120. package/dist/server/utils/expressBody.js +353 -0
  121. package/dist/server/utils/pathMatch.d.ts +21 -0
  122. package/dist/server/utils/pathMatch.js +41 -0
  123. package/dist/server/utils/validation.d.ts +9 -0
  124. package/dist/server/utils/validation.js +9 -0
  125. package/dist/server/websocket/WebSocketHandler.js +22 -3
  126. package/dist/server/websocket/frameQueue.d.ts +19 -0
  127. package/dist/server/websocket/frameQueue.js +41 -3
  128. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  129. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  130. package/dist/skills/skillStores.js +5 -0
  131. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  132. package/dist/tasks/store/fileTaskStore.js +11 -4
  133. package/dist/types/attachments.d.ts +117 -0
  134. package/dist/types/attachments.js +15 -0
  135. package/dist/types/auth.d.ts +6 -0
  136. package/dist/types/cli.d.ts +4 -0
  137. package/dist/types/config.d.ts +56 -0
  138. package/dist/types/file.d.ts +8 -0
  139. package/dist/types/hitl.d.ts +8 -0
  140. package/dist/types/index.d.ts +1 -0
  141. package/dist/types/index.js +1 -0
  142. package/dist/types/mcp.d.ts +98 -0
  143. package/dist/types/multimodal.js +0 -41
  144. package/dist/types/processor.d.ts +14 -1
  145. package/dist/types/proxy.d.ts +62 -3
  146. package/dist/types/rag.d.ts +9 -0
  147. package/dist/types/safeFetch.d.ts +40 -0
  148. package/dist/types/server.d.ts +255 -10
  149. package/dist/types/tools.d.ts +57 -0
  150. package/dist/types/transcribe.d.ts +6 -0
  151. package/dist/utils/attachmentPolicy.d.ts +89 -0
  152. package/dist/utils/attachmentPolicy.js +327 -0
  153. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  154. package/dist/utils/dotenvBootstrap.js +172 -31
  155. package/dist/utils/fileDetector.js +115 -65
  156. package/dist/utils/json/coerce.js +11 -1
  157. package/dist/utils/logSanitize.d.ts +5 -0
  158. package/dist/utils/logSanitize.js +8 -0
  159. package/dist/utils/logger.js +73 -1
  160. package/dist/utils/messageBuilder.d.ts +2 -2
  161. package/dist/utils/messageBuilder.js +118 -105
  162. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  163. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  164. package/dist/utils/redis.d.ts +12 -0
  165. package/dist/utils/redis.js +72 -0
  166. package/dist/utils/safeFetch.d.ts +22 -1
  167. package/dist/utils/safeFetch.js +132 -38
  168. package/dist/utils/safeStoreId.d.ts +19 -0
  169. package/dist/utils/safeStoreId.js +29 -0
  170. package/dist/utils/schemaConversion.d.ts +30 -3
  171. package/dist/utils/schemaConversion.js +457 -84
  172. package/dist/utils/sizeGuard.js +2 -2
  173. package/dist/utils/ssrfGuard.d.ts +3 -3
  174. package/dist/utils/ssrfGuard.js +71 -39
  175. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  176. package/dist/voice/eval/sttEvalStore.js +77 -12
  177. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  178. package/dist/voice/livekit/agentDefinition.js +22 -0
  179. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  180. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  181. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  182. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  183. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  184. package/dist/voice/livekit/voiceAgent.js +2 -1
  185. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  186. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  187. package/dist/voice/providers/GeminiLive.js +21 -6
  188. package/dist/voice/providers/GoogleSTT.js +8 -5
  189. package/dist/voice/transcribe.d.ts +2 -2
  190. package/dist/voice/transcribe.js +34 -18
  191. package/dist/voice/whistle/audio.js +11 -5
  192. package/docs-site/static/search-index.json +75 -52
  193. package/package.json +171 -156
  194. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -16,7 +16,7 @@ import { NeuroLink } from "../../neurolink.js";
16
16
  import { ConfigurationError, ServerStartError, } from "../../server/errors.js";
17
17
  import { withTimeout } from "../../utils/errorHandling.js";
18
18
  import { logger } from "../../utils/logger.js";
19
- import { clearStateOnExit, createServerApiKeyMiddleware, formatUptime, isOtherServerRunning, isProcessRunning, readServerApiKeys, resolveServerPort, SERVER_API_KEY_ENV, StateFileManager, } from "../utils/serverUtils.js";
19
+ import { clearStateOnExit, createServerApiKeyMiddleware, DEFAULT_SERVER_HOST, envFlag, formatUptime, isLoopbackHost, isOtherServerRunning, isProcessRunning, readServerApiKeys, resolveServerPort, SERVER_ALLOW_UNAUTHENTICATED_ENV, SERVER_API_KEY_ENV, SERVER_ENABLE_BUILTIN_TOOLS_ENV, SERVER_WS_ALLOW_QUERY_TOKEN_ENV, StateFileManager, unauthenticatedBindRefusal, } from "../utils/serverUtils.js";
20
20
  // ============================================
21
21
  // State Management
22
22
  // ============================================
@@ -159,7 +159,7 @@ function corsOriginsFromArgv(values) {
159
159
  .filter((origin) => origin.length > 0);
160
160
  return origins.length > 0 ? origins : undefined;
161
161
  }
162
- async function attachTranscriptionWebSocket(server, neurolink, serverConfig) {
162
+ async function attachTranscriptionWebSocket(server, neurolink, serverConfig, hardening) {
163
163
  const native = server.getNativeServer?.();
164
164
  if (!isNodeHttpServer(native)) {
165
165
  return;
@@ -175,9 +175,28 @@ async function attachTranscriptionWebSocket(server, neurolink, serverConfig) {
175
175
  await attachTranscribeWebSocket(native, neurolink, {
176
176
  ...(apiKeys.length > 0 ? { authToken: apiKeys } : {}),
177
177
  ...(allowedOrigins ? { allowedOrigins } : {}),
178
+ // The key rides in the Authorization header or the subprotocol; a URL is
179
+ // logged, so `?token=` is an explicit choice.
180
+ allowQueryToken: envFlag(SERVER_WS_ALLOW_QUERY_TOKEN_ENV),
181
+ // The same opt-out as the HTTP routes: whoever runs open on purpose also
182
+ // gets file mode without a key.
183
+ allowUnauthenticatedFileMode: hardening.allowUnauthenticated === true,
178
184
  });
179
185
  logger.debug("[serve] transcription WebSocket attached at /v1/audio/transcriptions/stream");
180
186
  }
187
+ /**
188
+ * The hardening flags, from the command line or the environment:
189
+ * `--allow-unauthenticated` / NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1 and
190
+ * `--enable-builtin-tools` / NEUROLINK_SERVER_ENABLE_BUILTIN_TOOLS=1.
191
+ */
192
+ function resolveHardening(argv) {
193
+ return {
194
+ allowUnauthenticated: argv.allowUnauthenticated === true ||
195
+ envFlag(SERVER_ALLOW_UNAUTHENTICATED_ENV),
196
+ enableBuiltinTools: argv.enableBuiltinTools === true ||
197
+ envFlag(SERVER_ENABLE_BUILTIN_TOOLS_ENV),
198
+ };
199
+ }
181
200
  export class ServeCommandFactory {
182
201
  /**
183
202
  * Create the main serve command
@@ -208,7 +227,19 @@ export class ServeCommandFactory {
208
227
  .option("host", {
209
228
  type: "string",
210
229
  alias: "H",
211
- description: "Host to bind to (default: 0.0.0.0)",
230
+ description: `Host to bind to (default: ${DEFAULT_SERVER_HOST}, this machine only; 0.0.0.0 listens on every interface and needs ${SERVER_API_KEY_ENV} or --allow-unauthenticated)`,
231
+ })
232
+ .option("allow-unauthenticated", {
233
+ type: "boolean",
234
+ alias: "allowUnauthenticated",
235
+ default: false,
236
+ description: `Listen on a non-loopback host with no ${SERVER_API_KEY_ENV}, and serve tool and MCP execution to anyone who can reach the port (${SERVER_ALLOW_UNAUTHENTICATED_ENV}=1)`,
237
+ })
238
+ .option("enable-builtin-tools", {
239
+ type: "boolean",
240
+ alias: "enableBuiltinTools",
241
+ default: false,
242
+ description: `Keep NeuroLink's built-in tools, file reads and writes under the working directory among them, in the server's registry (${SERVER_ENABLE_BUILTIN_TOOLS_ENV}=1)`,
212
243
  })
213
244
  .option("framework", {
214
245
  type: "string",
@@ -275,7 +306,7 @@ export class ServeCommandFactory {
275
306
  .example("neurolink serve --cors --rate-limit 50", "Start server with CORS and rate limiting (50 req/15min)")
276
307
  .example("neurolink serve --swagger", "Start server with OpenAPI documentation enabled")
277
308
  .example("neurolink serve --watch", "Start server in watch mode (restart on changes)")
278
- .example("NEUROLINK_SERVER_API_KEY=secret neurolink serve", "Require 'Authorization: Bearer secret' (or X-API-Key) on every route except health")
309
+ .example("NEUROLINK_SERVER_API_KEY=secret neurolink serve --host 0.0.0.0", "Listen on every interface; require 'Authorization: Bearer secret' (or X-API-Key) on every route except the health probes")
279
310
  .example("neurolink serve status", "Show server status")
280
311
  .help();
281
312
  },
@@ -319,9 +350,24 @@ export class ServeCommandFactory {
319
350
  const fileConfig = ServeCommandFactory.loadFileConfig(argv, spinner);
320
351
  // Merge CLI args with file config (CLI takes precedence)
321
352
  const port = resolveServerPort(argv.port, fileConfig.port);
322
- const host = argv.host ?? fileConfig.host ?? "0.0.0.0";
353
+ const host = argv.host ?? fileConfig.host ?? DEFAULT_SERVER_HOST;
323
354
  const framework = argv.framework ?? fileConfig.framework ?? "hono";
324
355
  const basePath = argv.basePath ?? fileConfig.basePath ?? "/api";
356
+ const hardening = resolveHardening(argv);
357
+ // A listener the network can reach with no key is refused unless the
358
+ // operator asked for exactly that.
359
+ const refusal = unauthenticatedBindRefusal({
360
+ host,
361
+ apiKeyCount: readServerApiKeys().length,
362
+ allowUnauthenticated: hardening.allowUnauthenticated === true,
363
+ });
364
+ if (refusal) {
365
+ if (spinner) {
366
+ spinner.fail(chalk.red("Refusing to start"));
367
+ }
368
+ logger.error(chalk.red(refusal));
369
+ process.exit(1);
370
+ }
325
371
  // Build server adapter config from merged values
326
372
  const serverConfig = ServeCommandFactory.buildServerConfig(argv, fileConfig, {
327
373
  port,
@@ -332,9 +378,9 @@ export class ServeCommandFactory {
332
378
  spinner.text = `Creating ${framework} server...`;
333
379
  }
334
380
  // Create, register routes, initialize and start server
335
- const serverRef = await ServeCommandFactory.createAndStartServer({ framework, serverConfig, basePath, port, host }, spinner);
381
+ const serverRef = await ServeCommandFactory.createAndStartServer({ framework, serverConfig, basePath, port, host, hardening }, spinner);
336
382
  // Save state and print startup banner
337
- ServeCommandFactory.saveAndPrintStartupInfo({ argv, port, host, framework, basePath, serverConfig }, spinner);
383
+ ServeCommandFactory.saveAndPrintStartupInfo({ argv, port, host, framework, basePath, serverConfig, hardening }, spinner);
338
384
  // Set up watch mode if enabled
339
385
  const stopWatcher = ServeCommandFactory.setupWatchMode(argv, {
340
386
  serverRef,
@@ -343,6 +389,7 @@ export class ServeCommandFactory {
343
389
  basePath,
344
390
  port,
345
391
  host,
392
+ hardening,
346
393
  });
347
394
  // Register signal handlers for graceful shutdown
348
395
  ServeCommandFactory.registerSignalHandlers(serverRef, stopWatcher);
@@ -450,7 +497,7 @@ export class ServeCommandFactory {
450
497
  */
451
498
  static async createAndStartServer(opts, spinner) {
452
499
  const { createServer, registerAllRoutes } = await import("../../server/index.js");
453
- const neurolink = new NeuroLink();
500
+ const neurolink = ServeCommandFactory.createServerNeuroLink(opts.hardening);
454
501
  const serverRef = {
455
502
  current: await createServer(neurolink, {
456
503
  framework: opts.framework,
@@ -458,17 +505,33 @@ export class ServeCommandFactory {
458
505
  }),
459
506
  };
460
507
  ServeCommandFactory.applyApiKeyAuth(serverRef.current, opts.basePath);
461
- registerAllRoutes(serverRef.current, opts.basePath);
508
+ registerAllRoutes(serverRef.current, opts.basePath, {
509
+ allowUnauthenticated: opts.hardening.allowUnauthenticated,
510
+ });
462
511
  if (spinner) {
463
512
  spinner.text = "Initializing server...";
464
513
  }
465
514
  await withTimeout(serverRef.current.initialize(), 30000, new ServerStartError("Server initialization timed out after 30 seconds", undefined, opts.port, opts.host));
466
515
  await withTimeout(serverRef.current.start(), 30000, new ServerStartError("Server startup timed out after 30 seconds", undefined, opts.port, opts.host));
467
- await attachTranscriptionWebSocket(serverRef.current, neurolink, opts.serverConfig);
516
+ await attachTranscriptionWebSocket(serverRef.current, neurolink, opts.serverConfig, opts.hardening);
468
517
  return serverRef;
469
518
  }
470
519
  /**
471
- * Put every route except health behind the API key(s) in
520
+ * The NeuroLink instance the server runs. Its registry holds no built-in
521
+ * tool unless `--enable-builtin-tools`: those tools read and write files
522
+ * under the server's working directory, and `/tools/execute` and the agent
523
+ * routes would hand them to every caller. The switch is the SDK's own
524
+ * (NEUROLINK_DISABLE_BUILTIN_TOOLS), set before the instance and its
525
+ * providers read it, so generate() in this process does not see them either.
526
+ */
527
+ static createServerNeuroLink(hardening) {
528
+ if (!hardening.enableBuiltinTools) {
529
+ process.env.NEUROLINK_DISABLE_BUILTIN_TOOLS = "true";
530
+ }
531
+ return new NeuroLink();
532
+ }
533
+ /**
534
+ * Put every route except the health probes behind the API key(s) in
472
535
  * NEUROLINK_SERVER_API_KEY. Must run before the routes are registered.
473
536
  */
474
537
  static applyApiKeyAuth(server, basePath) {
@@ -495,7 +558,7 @@ export class ServeCommandFactory {
495
558
  if (spinner) {
496
559
  spinner.succeed(chalk.green("NeuroLink server started successfully"));
497
560
  }
498
- const url = `http://${opts.host === "0.0.0.0" ? "localhost" : opts.host}:${opts.port}`;
561
+ const url = `http://${opts.host === "0.0.0.0" || opts.host === "::" ? "localhost" : opts.host}:${opts.port}`;
499
562
  const corsEnabled = opts.argv.cors ?? opts.serverConfig.cors?.enabled ?? true;
500
563
  // The origin list and the API key together decide whether any browser
501
564
  // page may call this server; the banner says which case this is.
@@ -507,6 +570,7 @@ export class ServeCommandFactory {
507
570
  const swaggerEnabled = opts.argv.swagger ?? opts.serverConfig.enableSwagger ?? false;
508
571
  ServeCommandFactory.printStartupBanner({
509
572
  url,
573
+ host: opts.host,
510
574
  framework: opts.framework,
511
575
  basePath: opts.basePath,
512
576
  pid: state.pid,
@@ -519,6 +583,8 @@ export class ServeCommandFactory {
519
583
  swaggerEnabled,
520
584
  watchEnabled: opts.argv.watch ?? false,
521
585
  authEnabled: readServerApiKeys().length > 0,
586
+ allowUnauthenticated: opts.hardening.allowUnauthenticated === true,
587
+ builtinToolsEnabled: opts.hardening.enableBuiltinTools === true,
522
588
  });
523
589
  return state;
524
590
  }
@@ -531,6 +597,11 @@ export class ServeCommandFactory {
531
597
  logger.always(chalk.gray("=".repeat(50)));
532
598
  logger.always("");
533
599
  logger.always(` ${chalk.bold("URL:")} ${chalk.cyan(info.url)}`);
600
+ logger.always(` ${chalk.bold("Listening:")} ${isLoopbackHost(info.host)
601
+ ? chalk.green(`${info.host} (this machine only)`)
602
+ : info.authEnabled
603
+ ? chalk.green(`${info.host} (network; API key required)`)
604
+ : chalk.yellow(`${info.host} (network, NO authentication — every route is open to anyone who can reach this port)`)}`);
534
605
  logger.always(` ${chalk.bold("Framework:")} ${chalk.cyan(info.framework)}`);
535
606
  logger.always(` ${chalk.bold("Base Path:")} ${chalk.cyan(info.basePath)}`);
536
607
  logger.always(` ${chalk.bold("PID:")} ${chalk.cyan(info.pid)}`);
@@ -554,7 +625,15 @@ export class ServeCommandFactory {
554
625
  if (info.watchEnabled) {
555
626
  logger.always(` Watch Mode: ${chalk.green("enabled")}`);
556
627
  }
557
- logger.always(` API Key: ${info.authEnabled ? chalk.green("required (health routes exempt)") : chalk.yellow(`disabled — set ${SERVER_API_KEY_ENV} before exposing this server`)}`);
628
+ logger.always(` API Key: ${info.authEnabled ? chalk.green("required (health probes exempt)") : chalk.yellow(`disabled — set ${SERVER_API_KEY_ENV} before exposing this server`)}`);
629
+ if (!info.authEnabled) {
630
+ logger.always(` Tool/MCP: ${info.allowUnauthenticated
631
+ ? chalk.yellow("execution routes OPEN (--allow-unauthenticated)")
632
+ : chalk.green("execution routes answer 403 until a key is set")}`);
633
+ }
634
+ logger.always(` Built-in: ${info.builtinToolsEnabled
635
+ ? chalk.yellow("tools enabled (file reads/writes under the working directory)")
636
+ : chalk.green("tools disabled (--enable-builtin-tools to allow)")}`);
558
637
  logger.always("");
559
638
  logger.always(chalk.bold("Available Endpoints:"));
560
639
  logger.always(chalk.gray(" Health & Monitoring:"));
@@ -600,20 +679,22 @@ export class ServeCommandFactory {
600
679
  const timestamp = Date.now();
601
680
  const { createServer: createNewServer, registerAllRoutes: registerNewRoutes, } = await import(`../../server/index.js?t=${timestamp}`);
602
681
  // Create new server
603
- const newNeurolink = new NeuroLink();
682
+ const newNeurolink = ServeCommandFactory.createServerNeuroLink(ctx.hardening);
604
683
  const newServer = await createNewServer(newNeurolink, {
605
684
  framework: ctx.framework,
606
685
  config: ctx.serverConfig,
607
686
  });
608
687
  ServeCommandFactory.applyApiKeyAuth(newServer, ctx.basePath);
609
- registerNewRoutes(newServer, ctx.basePath);
688
+ registerNewRoutes(newServer, ctx.basePath, {
689
+ allowUnauthenticated: ctx.hardening.allowUnauthenticated,
690
+ });
610
691
  // Initialize and start with timeouts
611
692
  await withTimeout(newServer.initialize(), 30000, new ServerStartError("Server initialization timed out during restart", undefined, ctx.port, ctx.host));
612
693
  await withTimeout(newServer.start(), 30000, new ServerStartError("Server startup timed out during restart", undefined, ctx.port, ctx.host));
613
694
  // The upgrade handler lives on the old adapter's http.Server; the
614
695
  // restarted one needs its own, or the stream route is gone after the
615
696
  // first file change.
616
- await attachTranscriptionWebSocket(newServer, newNeurolink, ctx.serverConfig);
697
+ await attachTranscriptionWebSocket(newServer, newNeurolink, ctx.serverConfig, ctx.hardening);
617
698
  // Update the reference so signal handlers use the new server instance
618
699
  ctx.serverRef.current = newServer;
619
700
  logger.always(chalk.green("Server restarted successfully"));
@@ -709,7 +790,7 @@ export class ServeCommandFactory {
709
790
  status.startTime = state.startTime;
710
791
  status.uptime = Date.now() - new Date(state.startTime).getTime();
711
792
  status.configFile = state.configFile ?? null;
712
- status.url = `http://${state.host === "0.0.0.0" ? "localhost" : state.host}:${state.port}`;
793
+ status.url = `http://${state.host === "0.0.0.0" || state.host === "::" ? "localhost" : state.host}:${state.port}`;
713
794
  }
714
795
  if (argv.format === "json") {
715
796
  logger.always(JSON.stringify(status, null, 2));
@@ -9,7 +9,7 @@ import path from "path";
9
9
  import { logger } from "../../utils/logger.js";
10
10
  import { NeuroLink } from "../../neurolink.js";
11
11
  import { withTimeout } from "../../utils/errorHandling.js";
12
- import { isProcessRunning, formatUptime, ensureStateDir, getNeuroLinkDir, clearStateOnExit, createServerApiKeyMiddleware, isOtherServerRunning, readServerApiKeys, resolveServerPort, SERVER_API_KEY_ENV, } from "../utils/serverUtils.js";
12
+ import { DEFAULT_SERVER_HOST, SERVER_ALLOW_UNAUTHENTICATED_ENV, SERVER_API_KEY_ENV, clearStateOnExit, createServerApiKeyMiddleware, ensureStateDir, formatUptime, getNeuroLinkDir, isOtherServerRunning, isProcessRunning, readServerApiKeys, resolveServerPort, unauthenticatedBindRefusal, } from "../utils/serverUtils.js";
13
13
  // ============================================
14
14
  // State Management
15
15
  // ============================================
@@ -47,7 +47,7 @@ const CONFIG_FILE = path.join(getNeuroLinkDir(), "server-config.json");
47
47
  function loadServerConfig() {
48
48
  const defaults = {
49
49
  defaultPort: 3000,
50
- defaultHost: "0.0.0.0",
50
+ defaultHost: DEFAULT_SERVER_HOST,
51
51
  defaultFramework: "hono",
52
52
  defaultBasePath: "/api",
53
53
  cors: { enabled: true },
@@ -165,7 +165,7 @@ export class ServerCommandFactory {
165
165
  .option("host", {
166
166
  type: "string",
167
167
  alias: "H",
168
- default: "0.0.0.0",
168
+ default: DEFAULT_SERVER_HOST,
169
169
  description: "Host to bind to",
170
170
  })
171
171
  .option("framework", {
@@ -356,12 +356,24 @@ export class ServerCommandFactory {
356
356
  const { createServer, registerAllRoutes } = await import("../../server/index.js");
357
357
  const port = resolveServerPort(argv.port);
358
358
  const framework = (argv.framework ?? "hono");
359
+ // A listener the network can reach with no key is refused unless the
360
+ // operator asked for exactly that (NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1).
361
+ const bindHost = argv.host ?? DEFAULT_SERVER_HOST;
362
+ const refusal = unauthenticatedBindRefusal({
363
+ host: bindHost,
364
+ apiKeyCount: readServerApiKeys().length,
365
+ allowUnauthenticated: process.env[SERVER_ALLOW_UNAUTHENTICATED_ENV] === "1",
366
+ });
367
+ if (refusal) {
368
+ logger.error(chalk.red(refusal));
369
+ process.exit(1);
370
+ }
359
371
  // Create server
360
372
  const server = await createServer(neurolink, {
361
373
  framework,
362
374
  config: {
363
375
  port,
364
- host: argv.host ?? "0.0.0.0",
376
+ host: bindHost,
365
377
  basePath: argv.basePath ?? "/api",
366
378
  cors: {
367
379
  enabled: argv.cors ?? true,
@@ -386,7 +398,7 @@ export class ServerCommandFactory {
386
398
  const state = {
387
399
  pid: process.pid,
388
400
  port,
389
- host: argv.host ?? "0.0.0.0",
401
+ host: argv.host ?? DEFAULT_SERVER_HOST,
390
402
  framework,
391
403
  startTime: new Date().toISOString(),
392
404
  basePath: argv.basePath ?? "/api",
package/dist/cli/index.js CHANGED
@@ -7,10 +7,14 @@ process.env.DOTENV_CONFIG_QUIET = "true";
7
7
  * Professional CLI experience with minimal maintenance overhead.
8
8
  * Features: Spinners, colors, batch processing, provider testing, rich help
9
9
  */
10
+ import { existsSync } from "node:fs";
11
+ import * as path from "node:path";
10
12
  import { initializeCliParser } from "./parser.js";
11
13
  import chalk from "chalk";
12
14
  import { initializeCliLifecycle, cleanupCliLifecycle } from "./lifecycle.js";
13
15
  import { loadProcessDotenv } from "../utils/dotenvBootstrap.js";
16
+ import { ensureStdinRef } from "./utils/stdinRef.js";
17
+ import { PROJECT_MCP_CONFIG_FILENAME, TRUST_MCP_CONFIG_ENV, TRUST_MCP_CONFIG_FLAG, formatProjectMcpConfigSummary, getProjectMcpConfigTrustStatus, getTrustedMcpConfigStorePath, recordTrustedProjectMcpConfig, setProjectMcpConfigTrustHook, summarizeProjectMcpConfig, } from "../mcp/projectConfigTrust.js";
14
18
  // Clean up pnpm-specific environment variables that cause npm warnings
15
19
  // These variables are set by pnpm but cause "Unknown env config" warnings in npm
16
20
  if (process.env.npm_config_verify_deps_before_run) {
@@ -19,11 +23,148 @@ if (process.env.npm_config_verify_deps_before_run) {
19
23
  if (process.env.npm_config__jsr_registry) {
20
24
  delete process.env.npm_config__jsr_registry;
21
25
  }
22
- // Load environment variables from .env file. Shared with the SDK entry
23
- // point; see dotenvBootstrap for why DOTENV_CONFIG_PATH has to be honoured
24
- // here too — a suite that drives the built CLI inherits the stripped
25
- // environment, and this is the process that would otherwise undo it.
26
- await loadProcessDotenv();
26
+ // `--trust-mcp-config` is a global flag consumed here, before yargs sees the
27
+ // argument vector: the parser runs with `strictOptions()`, so an option it
28
+ // does not know about would otherwise be rejected on every command.
29
+ const trustMcpConfigFlag = process.argv.includes(TRUST_MCP_CONFIG_FLAG);
30
+ if (trustMcpConfigFlag) {
31
+ process.argv = process.argv.filter((argument) => argument !== TRUST_MCP_CONFIG_FLAG);
32
+ }
33
+ // Load environment variables from `.env`. The CLI is the one entry point
34
+ // that still does this implicitly — a terminal session in a project directory
35
+ // is what the file is for. Endpoint-override variables are refused unless the
36
+ // file is owned by this user and not group/world-writable; see
37
+ // dotenvBootstrap.ts. `NEUROLINK_DOTENV=0` disables the load entirely.
38
+ await loadProcessDotenv({ caller: "cli" });
39
+ function warn(message) {
40
+ process.stderr.write(chalk.yellow(`${message}\n`));
41
+ }
42
+ function isHelpOrVersionInvocation(args) {
43
+ if (args.length === 0) {
44
+ return true;
45
+ }
46
+ return args.some((argument) => argument === "--help" ||
47
+ argument === "-h" ||
48
+ argument === "help" ||
49
+ argument === "--version" ||
50
+ argument === "-V" ||
51
+ argument === "version");
52
+ }
53
+ function describeUntrusted(summary, status) {
54
+ return [
55
+ status === "changed"
56
+ ? `This project's ${PROJECT_MCP_CONFIG_FILENAME} has changed since you last trusted it:`
57
+ : `This project has a ${PROJECT_MCP_CONFIG_FILENAME} you have not trusted yet:`,
58
+ ...formatProjectMcpConfigSummary(summary),
59
+ "Loading it starts the commands and connections above with your environment.",
60
+ ];
61
+ }
62
+ function howToDecide() {
63
+ return [
64
+ "To continue, either:",
65
+ ` • review the file, then re-run with ${TRUST_MCP_CONFIG_FLAG} (or ${TRUST_MCP_CONFIG_ENV}=1) to trust exactly this content — the hash is remembered in ${getTrustedMcpConfigStorePath()}`,
66
+ ` • or set ${TRUST_MCP_CONFIG_ENV}=0 to run without it`,
67
+ " • or pass --disable-tools so no MCP server is needed",
68
+ ];
69
+ }
70
+ async function promptForTrust(summary, status) {
71
+ ensureStdinRef();
72
+ const readline = await import("node:readline/promises");
73
+ const rl = readline.createInterface({
74
+ input: process.stdin,
75
+ output: process.stderr,
76
+ });
77
+ try {
78
+ process.stderr.write(chalk.yellow(`${describeUntrusted(summary, status).join("\n")}\n`));
79
+ const answer = await rl.question("Trust this file and start these servers? [y/N] ");
80
+ return /^y(es)?$/i.test(answer.trim());
81
+ }
82
+ finally {
83
+ rl.close();
84
+ }
85
+ }
86
+ /**
87
+ * Decide, once per invocation, what to do about the working directory's
88
+ * `.mcp-config.json`, and hand that decision to the SDK through its trust
89
+ * hook. The hook only runs when a command actually initialises MCP, so a
90
+ * command that never needs a tool is unaffected, and the decision that does
91
+ * refuse happens before any server is started.
92
+ *
93
+ * A file already recorded in `~/.neurolink/trusted-mcp-configs.json` with the
94
+ * same hash loads silently. Otherwise `--trust-mcp-config` /
95
+ * `NEUROLINK_TRUST_MCP_CONFIG=1` accepts and records it, `=0` ignores it, an
96
+ * interactive terminal is asked, and a non-interactive run that was told
97
+ * nothing refuses with exit status 1 at the moment the file would load.
98
+ */
99
+ async function installProjectMcpConfigGate() {
100
+ if (isHelpOrVersionInvocation(process.argv.slice(2))) {
101
+ return;
102
+ }
103
+ const candidate = path.join(process.cwd(), PROJECT_MCP_CONFIG_FILENAME);
104
+ if (!existsSync(candidate)) {
105
+ return;
106
+ }
107
+ const envValue = process.env[TRUST_MCP_CONFIG_ENV];
108
+ if (envValue === "0" || envValue === "false") {
109
+ setProjectMcpConfigTrustHook(async () => {
110
+ warn(`[neurolink] Ignoring ${candidate} (${TRUST_MCP_CONFIG_ENV}=${envValue}).`);
111
+ return null;
112
+ });
113
+ return;
114
+ }
115
+ let summary;
116
+ try {
117
+ summary = summarizeProjectMcpConfig(candidate);
118
+ }
119
+ catch (error) {
120
+ setProjectMcpConfigTrustHook(async () => {
121
+ warn(`[neurolink] Not loading ${candidate}: it could not be read (${error instanceof Error ? error.message : String(error)}).`);
122
+ return null;
123
+ });
124
+ return;
125
+ }
126
+ // Nothing to run, nothing to trust: let the loader report the parse error
127
+ // or the empty server list itself.
128
+ if (summary.parseError !== undefined || summary.servers.length === 0) {
129
+ setProjectMcpConfigTrustHook(async () => summary.path);
130
+ return;
131
+ }
132
+ const status = getProjectMcpConfigTrustStatus(summary.path, summary.sha256);
133
+ if (status === "trusted") {
134
+ setProjectMcpConfigTrustHook(async () => summary.path);
135
+ return;
136
+ }
137
+ const accept = () => {
138
+ try {
139
+ recordTrustedProjectMcpConfig(summary.path, summary.sha256);
140
+ }
141
+ catch (error) {
142
+ warn(`[neurolink] Trusted ${summary.path} for this run, but could not record it in ${getTrustedMcpConfigStorePath()} (${error instanceof Error ? error.message : String(error)}).`);
143
+ }
144
+ setProjectMcpConfigTrustHook(async () => summary.path);
145
+ };
146
+ if (trustMcpConfigFlag || envValue === "1" || envValue === "true") {
147
+ accept();
148
+ return;
149
+ }
150
+ const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
151
+ if (interactive) {
152
+ if (await promptForTrust(summary, status)) {
153
+ accept();
154
+ }
155
+ else {
156
+ warn(`[neurolink] Not loading ${summary.path}. Re-run with ${TRUST_MCP_CONFIG_FLAG} once you have reviewed it.`);
157
+ setProjectMcpConfigTrustHook(async () => null);
158
+ }
159
+ return;
160
+ }
161
+ setProjectMcpConfigTrustHook(async () => {
162
+ process.stderr.write(chalk.red(`neurolink: refusing to load an untrusted project MCP configuration.\n`));
163
+ process.stderr.write(`${[...describeUntrusted(summary, status), ...howToDecide()].join("\n")}\n`);
164
+ process.exit(1);
165
+ });
166
+ }
167
+ await installProjectMcpConfigGate();
27
168
  // Enhanced CLI with Professional UX
28
169
  const cli = initializeCliParser();
29
170
  initializeCliLifecycle(async () => {
@@ -4,4 +4,4 @@ import type { OptionSchema, TextGenerationOptions } from "../../types/index.js";
4
4
  * This object provides metadata for validation and help text in the CLI loop.
5
5
  * It is derived from the main TextGenerationOptions interface to ensure consistency.
6
6
  */
7
- export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "imageOptions" | "audioOptions" | "videoOptions" | "officeOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation" | "toolExecutionCapture" | "toolExecutionRecorder" | "toolRoots">, OptionSchema>;
7
+ export declare const textGenerationOptionsSchema: Record<keyof Omit<TextGenerationOptions, "prompt" | "input" | "schema" | "tools" | "context" | "conversationHistory" | "conversationMessages" | "conversationMemoryConfig" | "originalPrompt" | "middleware" | "expectedOutcome" | "evaluationCriteria" | "region" | "csvOptions" | "pdfOptions" | "imageOptions" | "audioOptions" | "videoOptions" | "officeOptions" | "tts" | "stt" | "thinkingConfig" | "requestId" | "fileRegistry" | "attachmentPolicy" | "abortSignal" | "toolFilter" | "excludeTools" | "toolChoice" | "prepareStep" | "credentials" | "onFinish" | "onError" | "processors" | "piiDetection" | "responseValidation" | "inputValidation" | "toolExecutionCapture" | "toolExecutionRecorder" | "toolRoots">, OptionSchema>;
@@ -22,7 +22,13 @@ export function backupEnvFile(envPath = ".env") {
22
22
  .split(".")[0]; // Remove milliseconds
23
23
  const backupPath = `${envPath}.backup.${timestamp}`;
24
24
  try {
25
- fs.copyFileSync(envPath, backupPath);
25
+ // The backup holds the same provider keys as `.env`: owner-only, and
26
+ // never over an existing file (a second backup in the same second gets
27
+ // a fresh error rather than a silent overwrite).
28
+ fs.writeFileSync(backupPath, fs.readFileSync(envPath), {
29
+ mode: 0o600,
30
+ flag: "wx",
31
+ });
26
32
  result.backupPath = backupPath;
27
33
  }
28
34
  catch (error) {
@@ -153,9 +159,17 @@ export function updateEnvFile(newVars, envPath = ".env", createBackup = true, ke
153
159
  }
154
160
  // Generate new content
155
161
  const newContent = generateEnvContent(newVars, existingContent, keysToDelete);
156
- // Write updated file
162
+ // Write updated file, owner-only. `mode` applies only when the file is
163
+ // created, so an existing `.env` is tightened explicitly as well; a
164
+ // platform without POSIX modes (Windows) ignores the chmod.
157
165
  try {
158
- fs.writeFileSync(envPath, newContent, "utf8");
166
+ fs.writeFileSync(envPath, newContent, { encoding: "utf8", mode: 0o600 });
167
+ try {
168
+ fs.chmodSync(envPath, 0o600);
169
+ }
170
+ catch {
171
+ // Not a POSIX file system; the write itself succeeded.
172
+ }
159
173
  }
160
174
  catch (error) {
161
175
  throw new Error(`Failed to write .env file: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
@@ -2,10 +2,43 @@
2
2
  * Server Utilities for NeuroLink CLI
3
3
  * Shared utility functions for server management commands (serve.ts and server.ts)
4
4
  */
5
+ import { DEFAULT_SERVER_HOST } from "../../server/abstract/baseServerAdapter.js";
5
6
  import type { MiddlewareDefinition } from "../../types/index.js";
6
7
  export declare const DEFAULT_SERVER_PORT = 3000;
7
- /** Comma-separated API keys; when set, every route except health needs one. */
8
+ /** The interface `neurolink serve` binds unless told otherwise: this host only. */
9
+ export { DEFAULT_SERVER_HOST };
10
+ /** Comma-separated API keys; when set, every route except the health probes needs one. */
8
11
  export declare const SERVER_API_KEY_ENV = "NEUROLINK_SERVER_API_KEY";
12
+ /**
13
+ * `=1` lets `neurolink serve` listen on a non-loopback host with no API key,
14
+ * and serves the tool and MCP execution routes to anyone who can reach it.
15
+ */
16
+ export declare const SERVER_ALLOW_UNAUTHENTICATED_ENV = "NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED";
17
+ /** `=1` keeps the built-in tools (filesystem among them) in the server's registry. */
18
+ export declare const SERVER_ENABLE_BUILTIN_TOOLS_ENV = "NEUROLINK_SERVER_ENABLE_BUILTIN_TOOLS";
19
+ /** `=1` lets the transcription WebSocket take the API key as `?token=`. */
20
+ export declare const SERVER_WS_ALLOW_QUERY_TOKEN_ENV = "NEUROLINK_SERVER_WS_ALLOW_QUERY_TOKEN";
21
+ /** Whether `name` is set to `1` (trimmed) in the environment. */
22
+ export declare function envFlag(name: string): boolean;
23
+ /** `true` when `host` names only this machine (`127.0.0.1`, `::1`, `localhost`). */
24
+ export declare function isLoopbackHost(host: string): boolean;
25
+ /**
26
+ * Why `neurolink serve` must not start as asked, or `undefined` when it may.
27
+ * A listener the network can reach with no API key hands every route, tool
28
+ * execution included, to anyone who finds the port; the operator has to say so.
29
+ */
30
+ export declare function unauthenticatedBindRefusal(options: {
31
+ host: string;
32
+ apiKeyCount: number;
33
+ allowUnauthenticated: boolean;
34
+ }): string | undefined;
35
+ /**
36
+ * The paths a key never guards: the liveness, readiness and startup probes an
37
+ * orchestrator calls with no credentials, and the basic health check. An
38
+ * exact list: `/health/detailed` reports the runtime and the MCP inventory,
39
+ * and `/version` the Node version, so both need the key like any other route.
40
+ */
41
+ export declare function healthExemptPaths(basePath: string): string[];
9
42
  /**
10
43
  * Resolve the listen port: --port, then the config file, then the PORT
11
44
  * environment variable (what container platforms set), then 3000.
@@ -14,8 +47,11 @@ export declare function resolveServerPort(cliPort?: number, filePort?: number):
14
47
  export declare function readServerApiKeys(): string[];
15
48
  /**
16
49
  * Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`.
17
- * Health probes stay open so orchestrators can reach them, and the dev
18
- * playground header cannot bypass a key the operator configured.
50
+ * The health probes (see `healthExemptPaths`) stay open so orchestrators can
51
+ * reach them, and the dev playground header cannot bypass a key the operator
52
+ * configured. The exemption is an exact match on the path, made here rather
53
+ * than through the adapters' `excludePaths`, whose entries cover a subtree:
54
+ * `/health` used to exempt `/health/detailed` and even `/health/api/version`.
19
55
  *
20
56
  * Every holder of a key gets the same `{ id: "api-key" }`, marked `shared` so
21
57
  * the agent routes know it names no person and do not warn when a request