@juspay/neurolink 13.8.17 → 13.8.19

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 (214) hide show
  1. package/CHANGELOG.md +5 -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/providers/openaiChatCompletionsClient.js +12 -3
  77. package/dist/proxy/accountLedger.js +6 -8
  78. package/dist/proxy/geminiFormat.d.ts +3 -11
  79. package/dist/proxy/geminiFormat.js +11 -2
  80. package/dist/proxy/inboundAuth.d.ts +89 -0
  81. package/dist/proxy/inboundAuth.js +219 -0
  82. package/dist/proxy/openaiFormat.d.ts +3 -9
  83. package/dist/proxy/openaiFormat.js +21 -15
  84. package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
  85. package/dist/proxy/proxyTranslationEngine.js +25 -14
  86. package/dist/proxy/routingPolicy.d.ts +3 -1
  87. package/dist/proxy/routingPolicy.js +7 -8
  88. package/dist/proxy/tokenRefresh.d.ts +6 -0
  89. package/dist/proxy/tokenRefresh.js +13 -1
  90. package/dist/proxy/updateState.js +4 -4
  91. package/dist/rag/document/loaders.d.ts +0 -6
  92. package/dist/rag/document/loaders.js +16 -10
  93. package/dist/rag/ragIntegration.d.ts +2 -2
  94. package/dist/rag/ragIntegration.js +35 -10
  95. package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
  96. package/dist/rag/retrieval/vectorQueryTool.js +54 -17
  97. package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
  98. package/dist/server/abstract/baseServerAdapter.js +119 -6
  99. package/dist/server/adapters/expressAdapter.d.ts +3 -0
  100. package/dist/server/adapters/expressAdapter.js +78 -6
  101. package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
  102. package/dist/server/adapters/fastifyAdapter.js +165 -70
  103. package/dist/server/adapters/honoAdapter.d.ts +9 -1
  104. package/dist/server/adapters/honoAdapter.js +99 -21
  105. package/dist/server/adapters/koaAdapter.d.ts +1 -0
  106. package/dist/server/adapters/koaAdapter.js +153 -74
  107. package/dist/server/index.d.ts +2 -0
  108. package/dist/server/index.js +4 -0
  109. package/dist/server/middleware/auth.js +6 -2
  110. package/dist/server/middleware/cache.js +6 -1
  111. package/dist/server/middleware/rateLimit.js +29 -4
  112. package/dist/server/routes/agentRoutes.js +24 -11
  113. package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
  114. package/dist/server/routes/claudeProxyRoutes.js +273 -84
  115. package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
  116. package/dist/server/routes/codexProxyRoutes.js +125 -40
  117. package/dist/server/routes/index.d.ts +4 -0
  118. package/dist/server/routes/index.js +35 -4
  119. package/dist/server/routes/mcpRoutes.d.ts +6 -2
  120. package/dist/server/routes/mcpRoutes.js +21 -7
  121. package/dist/server/routes/memoryRoutes.js +7 -6
  122. package/dist/server/routes/toolRoutes.d.ts +12 -2
  123. package/dist/server/routes/toolRoutes.js +33 -3
  124. package/dist/server/routes/transcribeRoutes.js +60 -13
  125. package/dist/server/utils/clientAddress.d.ts +23 -0
  126. package/dist/server/utils/clientAddress.js +65 -0
  127. package/dist/server/utils/clientError.d.ts +35 -0
  128. package/dist/server/utils/clientError.js +129 -0
  129. package/dist/server/utils/pathMatch.d.ts +21 -0
  130. package/dist/server/utils/pathMatch.js +41 -0
  131. package/dist/server/utils/validation.d.ts +9 -0
  132. package/dist/server/utils/validation.js +9 -0
  133. package/dist/server/websocket/WebSocketHandler.js +22 -3
  134. package/dist/server/websocket/frameQueue.d.ts +19 -0
  135. package/dist/server/websocket/frameQueue.js +41 -3
  136. package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
  137. package/dist/server/websocket/transcribeWebSocket.js +223 -34
  138. package/dist/skills/skillStores.js +5 -0
  139. package/dist/tasks/store/fileTaskStore.d.ts +2 -0
  140. package/dist/tasks/store/fileTaskStore.js +11 -4
  141. package/dist/types/attachments.d.ts +117 -0
  142. package/dist/types/attachments.js +15 -0
  143. package/dist/types/auth.d.ts +6 -0
  144. package/dist/types/cli.d.ts +4 -0
  145. package/dist/types/config.d.ts +56 -0
  146. package/dist/types/file.d.ts +8 -0
  147. package/dist/types/hitl.d.ts +8 -0
  148. package/dist/types/index.d.ts +1 -0
  149. package/dist/types/index.js +1 -0
  150. package/dist/types/mcp.d.ts +98 -0
  151. package/dist/types/multimodal.js +0 -41
  152. package/dist/types/openaiCompatible.d.ts +1 -1
  153. package/dist/types/processor.d.ts +14 -1
  154. package/dist/types/proxy.d.ts +71 -16
  155. package/dist/types/proxyClient.d.ts +2 -0
  156. package/dist/types/rag.d.ts +9 -0
  157. package/dist/types/safeFetch.d.ts +40 -0
  158. package/dist/types/server.d.ts +206 -10
  159. package/dist/types/tools.d.ts +57 -0
  160. package/dist/types/transcribe.d.ts +6 -0
  161. package/dist/utils/attachmentPolicy.d.ts +89 -0
  162. package/dist/utils/attachmentPolicy.js +327 -0
  163. package/dist/utils/dotenvBootstrap.d.ts +48 -19
  164. package/dist/utils/dotenvBootstrap.js +172 -31
  165. package/dist/utils/fileDetector.js +115 -65
  166. package/dist/utils/json/coerce.js +11 -1
  167. package/dist/utils/logSanitize.d.ts +5 -0
  168. package/dist/utils/logSanitize.js +8 -0
  169. package/dist/utils/logger.js +73 -1
  170. package/dist/utils/messageBuilder.d.ts +2 -2
  171. package/dist/utils/messageBuilder.js +118 -105
  172. package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
  173. package/dist/utils/multimodalOptionsBuilder.js +2 -0
  174. package/dist/utils/redis.d.ts +12 -0
  175. package/dist/utils/redis.js +72 -0
  176. package/dist/utils/retryAfter.d.ts +32 -0
  177. package/dist/utils/retryAfter.js +135 -11
  178. package/dist/utils/safeFetch.d.ts +22 -1
  179. package/dist/utils/safeFetch.js +132 -38
  180. package/dist/utils/safeStoreId.d.ts +19 -0
  181. package/dist/utils/safeStoreId.js +29 -0
  182. package/dist/utils/schemaConversion.d.ts +30 -3
  183. package/dist/utils/schemaConversion.js +457 -84
  184. package/dist/utils/sizeGuard.js +2 -2
  185. package/dist/utils/ssrfGuard.d.ts +3 -3
  186. package/dist/utils/ssrfGuard.js +71 -39
  187. package/dist/voice/eval/sttEvalStore.d.ts +26 -3
  188. package/dist/voice/eval/sttEvalStore.js +77 -12
  189. package/dist/voice/livekit/agentDefinition.d.ts +18 -0
  190. package/dist/voice/livekit/agentDefinition.js +22 -0
  191. package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
  192. package/dist/voice/livekit/realtimeMcpTools.js +8 -7
  193. package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
  194. package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
  195. package/dist/voice/livekit/voiceAgent.d.ts +2 -2
  196. package/dist/voice/livekit/voiceAgent.js +2 -1
  197. package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
  198. package/dist/voice/livekit/xaiSessionWatch.js +30 -0
  199. package/dist/voice/providers/GeminiLive.js +21 -6
  200. package/dist/voice/providers/GoogleSTT.js +8 -5
  201. package/dist/voice/transcribe.d.ts +2 -2
  202. package/dist/voice/transcribe.js +34 -18
  203. package/dist/voice/whistle/audio.js +11 -5
  204. package/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
  205. package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
  206. package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
  207. package/dist/workflow/workflows/consensusWorkflow.js +12 -12
  208. package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
  209. package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
  210. package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
  211. package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
  212. package/docs-site/static/search-index.json +74 -52
  213. package/package.json +173 -156
  214. package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
@@ -6,14 +6,60 @@ import { timingSafeEqual } from "crypto";
6
6
  import fs from "fs";
7
7
  import os from "os";
8
8
  import path from "path";
9
+ import { DEFAULT_SERVER_HOST } from "../../server/abstract/baseServerAdapter.js";
9
10
  import { ConfigurationError } from "../../server/errors.js";
10
11
  import { createAuthMiddleware } from "../../server/middleware/auth.js";
12
+ import { isLoopbackAddress } from "../../server/utils/clientAddress.js";
13
+ import { stripQuery } from "../../server/utils/pathMatch.js";
11
14
  // ============================================
12
15
  // Listen Address & Access Control
13
16
  // ============================================
14
17
  export const DEFAULT_SERVER_PORT = 3000;
15
- /** Comma-separated API keys; when set, every route except health needs one. */
18
+ /** The interface `neurolink serve` binds unless told otherwise: this host only. */
19
+ export { DEFAULT_SERVER_HOST };
20
+ /** Comma-separated API keys; when set, every route except the health probes needs one. */
16
21
  export const SERVER_API_KEY_ENV = "NEUROLINK_SERVER_API_KEY";
22
+ /**
23
+ * `=1` lets `neurolink serve` listen on a non-loopback host with no API key,
24
+ * and serves the tool and MCP execution routes to anyone who can reach it.
25
+ */
26
+ export const SERVER_ALLOW_UNAUTHENTICATED_ENV = "NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED";
27
+ /** `=1` keeps the built-in tools (filesystem among them) in the server's registry. */
28
+ export const SERVER_ENABLE_BUILTIN_TOOLS_ENV = "NEUROLINK_SERVER_ENABLE_BUILTIN_TOOLS";
29
+ /** `=1` lets the transcription WebSocket take the API key as `?token=`. */
30
+ export const SERVER_WS_ALLOW_QUERY_TOKEN_ENV = "NEUROLINK_SERVER_WS_ALLOW_QUERY_TOKEN";
31
+ /** Whether `name` is set to `1` (trimmed) in the environment. */
32
+ export function envFlag(name) {
33
+ return process.env[name]?.trim() === "1";
34
+ }
35
+ /** `true` when `host` names only this machine (`127.0.0.1`, `::1`, `localhost`). */
36
+ export function isLoopbackHost(host) {
37
+ return isLoopbackAddress(host);
38
+ }
39
+ /**
40
+ * Why `neurolink serve` must not start as asked, or `undefined` when it may.
41
+ * A listener the network can reach with no API key hands every route, tool
42
+ * execution included, to anyone who finds the port; the operator has to say so.
43
+ */
44
+ export function unauthenticatedBindRefusal(options) {
45
+ if (options.apiKeyCount > 0 ||
46
+ options.allowUnauthenticated ||
47
+ isLoopbackHost(options.host)) {
48
+ return undefined;
49
+ }
50
+ return (`Refusing to listen on ${options.host} without authentication: every route, including tool execution, would be open to anyone who can reach this port. ` +
51
+ `Set ${SERVER_API_KEY_ENV}, bind a loopback address (--host ${DEFAULT_SERVER_HOST}), or pass --allow-unauthenticated (${SERVER_ALLOW_UNAUTHENTICATED_ENV}=1) to run open on purpose.`);
52
+ }
53
+ /**
54
+ * The paths a key never guards: the liveness, readiness and startup probes an
55
+ * orchestrator calls with no credentials, and the basic health check. An
56
+ * exact list: `/health/detailed` reports the runtime and the MCP inventory,
57
+ * and `/version` the Node version, so both need the key like any other route.
58
+ */
59
+ export function healthExemptPaths(basePath) {
60
+ const health = `${basePath.replace(/\/+$/, "")}/health`;
61
+ return [health, `${health}/live`, `${health}/ready`, `${health}/startup`];
62
+ }
17
63
  /**
18
64
  * Resolve the listen port: --port, then the config file, then the PORT
19
65
  * environment variable (what container platforms set), then 3000.
@@ -39,8 +85,11 @@ export function readServerApiKeys() {
39
85
  const matchesKey = (key, token) => key.length === token.length && timingSafeEqual(key, token);
40
86
  /**
41
87
  * Require one of `apiKeys` as `Authorization: Bearer <key>` or `X-API-Key`.
42
- * Health probes stay open so orchestrators can reach them, and the dev
43
- * playground header cannot bypass a key the operator configured.
88
+ * The health probes (see `healthExemptPaths`) stay open so orchestrators can
89
+ * reach them, and the dev playground header cannot bypass a key the operator
90
+ * configured. The exemption is an exact match on the path, made here rather
91
+ * than through the adapters' `excludePaths`, whose entries cover a subtree:
92
+ * `/health` used to exempt `/health/detailed` and even `/health/api/version`.
44
93
  *
45
94
  * Every holder of a key gets the same `{ id: "api-key" }`, marked `shared` so
46
95
  * the agent routes know it names no person and do not warn when a request
@@ -48,10 +97,11 @@ const matchesKey = (key, token) => key.length === token.length && timingSafeEqua
48
97
  */
49
98
  export function createServerApiKeyMiddleware(apiKeys, basePath) {
50
99
  const keys = apiKeys.map((key) => Buffer.from(key, "utf8"));
51
- return createAuthMiddleware({
100
+ const exempt = new Set(healthExemptPaths(basePath));
101
+ const inner = createAuthMiddleware({
52
102
  type: "custom",
53
103
  extractToken: (ctx) => {
54
- const bearer = /^Bearer\s+(.+)$/i.exec(ctx.headers["authorization"] ?? "");
104
+ const bearer = /^Bearer\s+(\S.*)$/i.exec(ctx.headers["authorization"] ?? "");
55
105
  return bearer?.[1] ?? ctx.headers["x-api-key"] ?? null;
56
106
  },
57
107
  validate: async (token) => {
@@ -60,9 +110,16 @@ export function createServerApiKeyMiddleware(apiKeys, basePath) {
60
110
  ? { id: "api-key", shared: true }
61
111
  : null;
62
112
  },
63
- skipPaths: [`${basePath.replace(/\/+$/, "")}/health`],
64
113
  skipDevPlayground: false,
65
114
  });
115
+ return {
116
+ ...inner,
117
+ name: "api-key",
118
+ excludePaths: undefined,
119
+ handler: (ctx, next) => exempt.has(stripQuery(ctx.path).replace(/\/+$/, "") || "/")
120
+ ? next()
121
+ : inner.handler(ctx, next),
122
+ };
66
123
  }
67
124
  // ============================================
68
125
  // State Directory Management
@@ -3039,7 +3039,13 @@ export class BaseProvider {
3039
3039
  const { executeDirectorPipeline, DIRECTOR_PIPELINE_TIMEOUT_MS } = await import("../adapters/video/directorPipeline.js");
3040
3040
  // Use caller's timeout if provided, otherwise use default Director timeout
3041
3041
  const directorTimeout = options.timeout ?? DIRECTOR_PIPELINE_TIMEOUT_MS;
3042
- const videoResult = await this.executeWithTimeout(() => executeDirectorPipeline(segments, generateOptions.output?.video ?? {}, generateOptions.output?.director ?? {}, options.region), { timeout: directorTimeout, operationType: "generate" });
3042
+ const videoResult = await this.executeWithTimeout(() => executeDirectorPipeline(segments, generateOptions.output?.video ?? {}, {
3043
+ ...(generateOptions.output?.director ?? {}),
3044
+ // Director Mode fetches/reads reference images itself; hand it
3045
+ // the same attachment policy the rest of the request runs under.
3046
+ attachmentPolicy: generateOptions.output?.director?.attachmentPolicy ??
3047
+ options.attachmentPolicy,
3048
+ }, options.region), { timeout: directorTimeout, operationType: "generate" });
3043
3049
  // Build content summary with metadata
3044
3050
  const joinedPrompts = generateOptions.input.segments
3045
3051
  .map((s) => s.prompt)
@@ -1,6 +1,7 @@
1
1
  import { tracers, ATTR, withSpan } from "../../telemetry/index.js";
2
2
  import { logger } from "../../utils/logger.js";
3
3
  import { buildMessagesArray, buildMultimodalModelMessages, } from "../../utils/messageBuilder.js";
4
+ import { getAttachmentPolicy } from "../../utils/attachmentPolicy.js";
4
5
  /**
5
6
  * Compute total content length across all messages for span attributes.
6
7
  */
@@ -131,6 +132,9 @@ export class MessageBuilder {
131
132
  schema: options.schema,
132
133
  output: options.output,
133
134
  fileRegistry: options.fileRegistry,
135
+ // The instance's attachment policy has to ride along like the
136
+ // registry: this literal is the only road into the loaders.
137
+ attachmentPolicy: getAttachmentPolicy(options),
134
138
  };
135
139
  messages = await buildMultimodalModelMessages(multimodalOptions, this.providerName, this.modelName);
136
140
  // Propagate any systemPrompt augmentation (e.g. inline-file
@@ -280,6 +284,7 @@ export class MessageBuilder {
280
284
  schema: options.schema,
281
285
  output: options.output,
282
286
  fileRegistry: options.fileRegistry,
287
+ attachmentPolicy: getAttachmentPolicy(options),
283
288
  };
284
289
  messages = await buildMultimodalModelMessages(multimodalOptions, this.providerName, this.modelName);
285
290
  // Propagate any systemPrompt augmentation (e.g. inline-file
@@ -40,6 +40,36 @@ export declare class ToolsManager {
40
40
  * every tool uncancellable (deadline overshoot / ghost executions).
41
41
  */
42
42
  private wrapExecuteWithTruncation;
43
+ /**
44
+ * The HITL manager of the owning NeuroLink instance, when one is configured.
45
+ * It lives on the tool registry (`setHITLManager`), which is also where the
46
+ * registered-tool execution path reads it, so the two paths cannot disagree.
47
+ */
48
+ private resolveHITLManager;
49
+ /**
50
+ * Whether a built-in tool call must be confirmed by a human first.
51
+ *
52
+ * Same rule as `MCPToolRegistry.executeTool` applies to registered tools —
53
+ * `dangerousActions` keywords and `customRules` — plus the built-in tools
54
+ * that change the machine (`HITL_ALWAYS_CONFIRM_DIRECT_TOOLS`), which are
55
+ * confirmed on every call unless `hitl.confirmBuiltinTools` is `false`.
56
+ */
57
+ private directToolNeedsConfirmation;
58
+ /**
59
+ * Gate a built-in (direct) tool's execute behind the instance's HITL
60
+ * manager, with the semantics of the registry path: a denial throws
61
+ * `HITLUserRejectedError` (the loop reports it to the model as a failed
62
+ * call), a timeout propagates the manager's `HITLTimeoutError` (or the
63
+ * auto-approval it is configured for), and user-modified arguments replace
64
+ * the model's. Without HITL the execute runs untouched.
65
+ *
66
+ * Direct tools reach the model through `getAllTools()` and are executed by
67
+ * the provider loops directly, never through `MCPToolRegistry.executeTool`
68
+ * — so before this wrapper the registry's HITL check never saw them, and
69
+ * `writeFile` / `executeBashCommand` ran unconfirmed while the bash tool's
70
+ * own description promised otherwise.
71
+ */
72
+ private wrapExecuteWithHITL;
43
73
  /**
44
74
  * Byte budget for BZ-666 truncation. Reads the instance-level
45
75
  * `tools.outputTruncationMaxBytes` config (see `ToolConfig` /
@@ -182,7 +212,22 @@ export declare class ToolsManager {
182
212
  */
183
213
  private createCustomToolFromDefinition;
184
214
  /**
185
- * Create an external MCP tool
215
+ * Prepare an external tool's declaration and validator from its inputSchema,
216
+ * once per schema object (see mcpSchemaCache).
217
+ *
218
+ * The schema is a remote server's data. Every `pattern` and every
219
+ * `patternProperties` key in it is compiled with `new RegExp` by the
220
+ * validator on each check, so one nested-quantifier pattern from a server
221
+ * could stall the event loop for seconds per tool call.
222
+ * Patterns `vetRegexPattern` refuses are removed before the validator is
223
+ * built and reported once, at warn level.
224
+ */
225
+ private prepareExternalToolSchema;
226
+ /**
227
+ * Create an external MCP tool. `registeredName` is the key the tool is
228
+ * exposed under — normally `tool.name`, or `<serverId>__<tool.name>` after a
229
+ * collision — and is what events and spans report; execution always goes to
230
+ * the owning server by the tool's own name.
186
231
  */
187
232
  private createExternalMCPTool;
188
233
  }
@@ -7,7 +7,8 @@ import { TOOL_EVENTS_WRAPPED } from "../../types/index.js";
7
7
  import { getKeyCount } from "../../utils/transformationUtils.js";
8
8
  import { extractIsErrorText } from "../../utils/toolResultStatus.js";
9
9
  import { createToolExecutionId } from "../../utils/toolExecutionId.js";
10
- import { convertJsonSchemaToZod } from "../../utils/schemaConversion.js";
10
+ import { convertJsonSchemaToZod, stripUnsafeRegexPatterns, } from "../../utils/schemaConversion.js";
11
+ import { HITLUserRejectedError } from "../../hitl/hitlErrors.js";
11
12
  import { generateToolOutputPreview, DEFAULT_MAX_PREVIEW_BYTES, RETRIEVE_CONTEXT_TOOL_NAME, } from "../../context/toolOutputLimits.js";
12
13
  /**
13
14
  * Own property stamped on a tool object whose `execute` already emits
@@ -32,17 +33,55 @@ function makeToolAbortError() {
32
33
  return e;
33
34
  }
34
35
  /**
35
- * Compiled-validator cache, keyed by the ORIGINAL MCP tool inputSchema
36
- * object. createExternalMCPTool runs once per external tool on EVERY
37
- * getAllTools() (i.e. every generation call), and the schema object identity
38
- * is stable for the lifetime of a server connection — without the cache the
39
- * same JSON Schema is recompiled on every generate() (hot-path CPU for
40
- * 50+-tool deployments). Rediscovery produces new schema objects, so stale
41
- * entries fall out via WeakMap semantics. `undefined` values (schemas that
42
- * failed to compile) are cached too — hence has()/get() rather than a
43
- * get()-only check.
36
+ * Built-in tools that change the machine. While HITL is enabled they require
37
+ * confirmation on every call, whatever `dangerousActions` lists, unless the
38
+ * host sets `hitl.confirmBuiltinTools: false`. The other built-ins (clock,
39
+ * arithmetic, reads, directory listings, CSV analysis, web search) are gated
40
+ * by `dangerousActions` keywords and `customRules` like every other tool.
44
41
  */
45
- const mcpValidatorCache = new WeakMap();
42
+ const HITL_ALWAYS_CONFIRM_DIRECT_TOOLS = new Set([
43
+ "writeFile",
44
+ "executeBashCommand",
45
+ ]);
46
+ /** serverId the registry uses for the built-in tools; reused in HITL requests. */
47
+ const DIRECT_TOOL_SERVER_ID = "direct";
48
+ /** Tool names a provider accepts: 1-64 characters from this set. */
49
+ const TOOL_NAME_SAFE_CHARS = /[^A-Za-z0-9_-]/g;
50
+ const MAX_TOOL_NAME_LENGTH = 64;
51
+ /**
52
+ * `<serverId>__<tool>`, reduced to the characters every provider accepts and
53
+ * to 64 characters, keeping the tool name's tail (the distinctive part) when
54
+ * the two together are too long.
55
+ */
56
+ function qualifyToolName(serverId, toolName) {
57
+ const safeServer = serverId.replace(TOOL_NAME_SAFE_CHARS, "_") || "server";
58
+ const safeTool = toolName.replace(TOOL_NAME_SAFE_CHARS, "_") || "tool";
59
+ const joined = `${safeServer}__${safeTool}`;
60
+ if (joined.length <= MAX_TOOL_NAME_LENGTH) {
61
+ return joined;
62
+ }
63
+ const room = MAX_TOOL_NAME_LENGTH - safeTool.length - 2;
64
+ return room > 0
65
+ ? `${safeServer.slice(0, room)}__${safeTool}`
66
+ : joined.slice(-MAX_TOOL_NAME_LENGTH);
67
+ }
68
+ /**
69
+ * Per-schema cache of the prepared declaration and compiled validator, keyed
70
+ * by the ORIGINAL MCP tool inputSchema object. createExternalMCPTool runs once
71
+ * per external tool on EVERY getAllTools() (i.e. every generation call), and
72
+ * the schema object identity is stable for the lifetime of a server connection
73
+ * — without the cache the same JSON Schema is cloned, vetted and recompiled on
74
+ * every generate() (hot-path CPU for 50+-tool deployments). Rediscovery
75
+ * produces new schema objects, so stale entries fall out via WeakMap
76
+ * semantics. An `undefined` validator (a schema that failed to compile) is
77
+ * cached too — hence has()/get() rather than a get()-only check.
78
+ *
79
+ * `schema` is the declaration the model sees: strict-mode fixed and with every
80
+ * regular expression `vetRegexPattern` refused removed. The removal
81
+ * happens once per schema object, so its warning fires once per schema, not
82
+ * once per request.
83
+ */
84
+ const mcpSchemaCache = new WeakMap();
46
85
  /**
47
86
  * Build an argument validator for an external MCP tool's JSON Schema, in the
48
87
  * shape the AI SDK's `jsonSchema()` wrapper expects. Returns undefined when
@@ -235,6 +274,72 @@ export class ToolsManager {
235
274
  return this.truncateToolResult(toolName, result);
236
275
  };
237
276
  }
277
+ /**
278
+ * The HITL manager of the owning NeuroLink instance, when one is configured.
279
+ * It lives on the tool registry (`setHITLManager`), which is also where the
280
+ * registered-tool execution path reads it, so the two paths cannot disagree.
281
+ */
282
+ resolveHITLManager() {
283
+ const registry = this.neurolink?.getToolRegistry?.();
284
+ const manager = registry && typeof registry.getHITLManager === "function"
285
+ ? registry.getHITLManager()
286
+ : undefined;
287
+ return manager && manager.isEnabled() ? manager : undefined;
288
+ }
289
+ /**
290
+ * Whether a built-in tool call must be confirmed by a human first.
291
+ *
292
+ * Same rule as `MCPToolRegistry.executeTool` applies to registered tools —
293
+ * `dangerousActions` keywords and `customRules` — plus the built-in tools
294
+ * that change the machine (`HITL_ALWAYS_CONFIRM_DIRECT_TOOLS`), which are
295
+ * confirmed on every call unless `hitl.confirmBuiltinTools` is `false`.
296
+ */
297
+ directToolNeedsConfirmation(manager, toolName, params) {
298
+ if (manager.requiresConfirmation(toolName, params)) {
299
+ return true;
300
+ }
301
+ if (!HITL_ALWAYS_CONFIRM_DIRECT_TOOLS.has(toolName)) {
302
+ return false;
303
+ }
304
+ const confirmBuiltins = manager.getConfig().confirmBuiltinTools;
305
+ return confirmBuiltins !== false;
306
+ }
307
+ /**
308
+ * Gate a built-in (direct) tool's execute behind the instance's HITL
309
+ * manager, with the semantics of the registry path: a denial throws
310
+ * `HITLUserRejectedError` (the loop reports it to the model as a failed
311
+ * call), a timeout propagates the manager's `HITLTimeoutError` (or the
312
+ * auto-approval it is configured for), and user-modified arguments replace
313
+ * the model's. Without HITL the execute runs untouched.
314
+ *
315
+ * Direct tools reach the model through `getAllTools()` and are executed by
316
+ * the provider loops directly, never through `MCPToolRegistry.executeTool`
317
+ * — so before this wrapper the registry's HITL check never saw them, and
318
+ * `writeFile` / `executeBashCommand` ran unconfirmed while the bash tool's
319
+ * own description promised otherwise.
320
+ */
321
+ wrapExecuteWithHITL(toolName, execute) {
322
+ return async (params, execOptions) => {
323
+ const manager = this.resolveHITLManager();
324
+ if (!manager ||
325
+ !this.directToolNeedsConfirmation(manager, toolName, params)) {
326
+ return execute(params, execOptions);
327
+ }
328
+ logger.info(`Built-in tool '${toolName}' requires HITL confirmation`);
329
+ const confirmation = await manager.requestConfirmation(toolName, params, {
330
+ serverId: DIRECT_TOOL_SERVER_ID,
331
+ sessionId: this.sessionId,
332
+ userId: this.userId,
333
+ });
334
+ if (!confirmation.approved) {
335
+ throw new HITLUserRejectedError(`Tool execution rejected by user: ${confirmation.reason || "No reason provided"}`, toolName, confirmation.reason);
336
+ }
337
+ const finalParams = confirmation.modifiedArguments !== undefined
338
+ ? confirmation.modifiedArguments
339
+ : params;
340
+ return execute(finalParams, execOptions);
341
+ };
342
+ }
238
343
  /**
239
344
  * Byte budget for BZ-666 truncation. Reads the instance-level
240
345
  * `tools.outputTruncationMaxBytes` config (see `ToolConfig` /
@@ -740,8 +845,10 @@ export class ToolsManager {
740
845
  typeof directTool === "object" &&
741
846
  "execute" in directTool) {
742
847
  const originalExecute = directTool.execute;
743
- // Create a new tool with wrapped execute function (BZ-666/BZ-664 guards applied)
744
- const guardedExecute = this.wrapExecuteWithTruncation(toolName, originalExecute);
848
+ // Create a new tool with wrapped execute function (BZ-666/BZ-664 guards applied).
849
+ // HITL sits INSIDE the truncation wrapper so a pending confirmation is
850
+ // raced against the loop's abort signal like any other tool wait.
851
+ const guardedExecute = this.wrapExecuteWithTruncation(toolName, this.wrapExecuteWithHITL(toolName, originalExecute));
745
852
  tools[toolName] = markEventWrapped({
746
853
  ...directTool,
747
854
  execute: this.wrapExecuteWithEvents(toolName, guardedExecute),
@@ -810,11 +917,32 @@ export class ToolsManager {
810
917
  }
811
918
  try {
812
919
  const externalTools = await this.neurolink.getExternalMCPTools();
920
+ // Which server owns each external name added in this pass, so a
921
+ // collision names both sides. Names present before this phase belong to
922
+ // a built-in or a registered tool.
923
+ const externalOwners = new Map();
813
924
  let addedCount = 0;
814
925
  for (const tool of externalTools) {
815
- const mcpTool = await this.createExternalMCPTool(tool);
816
- if (mcpTool && !tools[tool.name]) {
817
- tools[tool.name] = mcpTool;
926
+ const serverId = tool.serverId || "unknown";
927
+ let registeredName = tool.name;
928
+ if (tools[tool.name]) {
929
+ // A silent first-wins drop here left the losing server's tool
930
+ // unreachable, with the outcome depending on connection order
931
+ //. Keep both: the later one is registered
932
+ // under `<serverId>__<tool>` so the model can still call it.
933
+ const owner = externalOwners.get(tool.name);
934
+ const qualified = qualifyToolName(serverId, tool.name);
935
+ if (tools[qualified]) {
936
+ logger.warn(`[ToolsManager] Tool name collision: '${tool.name}' from server '${serverId}' is already provided by ${owner ? `server '${owner}'` : "a built-in or registered tool"}, and the qualified name '${qualified}' is taken too; the '${serverId}' copy is not exposed to the model`);
937
+ continue;
938
+ }
939
+ logger.warn(`[ToolsManager] Tool name collision: '${tool.name}' from server '${serverId}' is already provided by ${owner ? `server '${owner}'` : "a built-in or registered tool"}; the '${serverId}' copy is exposed as '${qualified}'`);
940
+ registeredName = qualified;
941
+ }
942
+ const mcpTool = await this.createExternalMCPTool(tool, registeredName);
943
+ if (mcpTool) {
944
+ tools[registeredName] = mcpTool;
945
+ externalOwners.set(registeredName, serverId);
818
946
  addedCount++;
819
947
  }
820
948
  }
@@ -993,36 +1121,55 @@ export class ToolsManager {
993
1121
  }
994
1122
  }
995
1123
  /**
996
- * Create an external MCP tool
1124
+ * Prepare an external tool's declaration and validator from its inputSchema,
1125
+ * once per schema object (see mcpSchemaCache).
1126
+ *
1127
+ * The schema is a remote server's data. Every `pattern` and every
1128
+ * `patternProperties` key in it is compiled with `new RegExp` by the
1129
+ * validator on each check, so one nested-quantifier pattern from a server
1130
+ * could stall the event loop for seconds per tool call.
1131
+ * Patterns `vetRegexPattern` refuses are removed before the validator is
1132
+ * built and reported once, at warn level.
997
1133
  */
998
- async createExternalMCPTool(tool) {
1134
+ async prepareExternalToolSchema(toolName, serverId, originalSchema) {
1135
+ const cached = mcpSchemaCache.get(originalSchema);
1136
+ if (cached) {
1137
+ return cached;
1138
+ }
1139
+ // Clone and fix the schema for OpenAI strict mode compatibility
1140
+ const fixedSchema = this.utilities?.fixSchemaForOpenAIStrictMode
1141
+ ? this.utilities.fixSchemaForOpenAIStrictMode(originalSchema)
1142
+ : originalSchema;
1143
+ const { schema, drops } = stripUnsafeRegexPatterns(fixedSchema);
1144
+ if (drops.length > 0) {
1145
+ logger.warn(`[ToolsManager] Removed ${drops.length} regular expression pattern(s) from the schema of tool '${toolName}' (server '${serverId}') because they could stall the process; the affected fields are validated without them`, { drops: drops.map(({ path, reason }) => ({ path, reason })) });
1146
+ }
1147
+ // A jsonSchema() wrapper WITHOUT a validate function is declarative
1148
+ // only — the AI SDK passes any parsed arguments straight through
1149
+ // (safeValidateTypes short-circuits on `validate == null`). That let
1150
+ // malformed calls (missing required params, "123" for a number)
1151
+ // reach execution, where the MCP-layer validator rejected them at
1152
+ // the cost of a full model round-trip. Attach a real validator so
1153
+ // invalid calls fail at parse time, where experimental_repairToolCall
1154
+ // can still fix them silently.
1155
+ const validate = await buildMCPSchemaValidator(toolName, schema);
1156
+ const prepared = { schema, validate };
1157
+ mcpSchemaCache.set(originalSchema, prepared);
1158
+ return prepared;
1159
+ }
1160
+ /**
1161
+ * Create an external MCP tool. `registeredName` is the key the tool is
1162
+ * exposed under — normally `tool.name`, or `<serverId>__<tool.name>` after a
1163
+ * collision — and is what events and spans report; execution always goes to
1164
+ * the owning server by the tool's own name.
1165
+ */
1166
+ async createExternalMCPTool(tool, registeredName = tool.name) {
999
1167
  try {
1000
1168
  // Use original JSON Schema from MCP tool if available, otherwise use permissive schema
1001
1169
  let finalSchema;
1002
1170
  if (tool.inputSchema && typeof tool.inputSchema === "object") {
1003
- // Clone and fix the schema for OpenAI strict mode compatibility
1004
- const originalSchema = tool.inputSchema;
1005
- const fixedSchema = this.utilities?.fixSchemaForOpenAIStrictMode
1006
- ? this.utilities.fixSchemaForOpenAIStrictMode(originalSchema)
1007
- : originalSchema;
1008
- // A jsonSchema() wrapper WITHOUT a validate function is declarative
1009
- // only — the AI SDK passes any parsed arguments straight through
1010
- // (safeValidateTypes short-circuits on `validate == null`). That let
1011
- // malformed calls (missing required params, "123" for a number)
1012
- // reach execution, where the MCP-layer validator rejected them at
1013
- // the cost of a full model round-trip. Attach a real validator so
1014
- // invalid calls fail at parse time, where experimental_repairToolCall
1015
- // can still fix them silently. Compiled once per schema object —
1016
- // see mcpValidatorCache.
1017
- let validate;
1018
- if (mcpValidatorCache.has(originalSchema)) {
1019
- validate = mcpValidatorCache.get(originalSchema);
1020
- }
1021
- else {
1022
- validate = await buildMCPSchemaValidator(tool.name, fixedSchema);
1023
- mcpValidatorCache.set(originalSchema, validate);
1024
- }
1025
- finalSchema = jsonSchema(fixedSchema, validate ? { validate } : {});
1171
+ const { schema, validate } = await this.prepareExternalToolSchema(tool.name, tool.serverId || "unknown", tool.inputSchema);
1172
+ finalSchema = jsonSchema(schema, validate ? { validate } : {});
1026
1173
  }
1027
1174
  else {
1028
1175
  finalSchema = this.utilities?.createPermissiveZodSchema
@@ -1037,7 +1184,7 @@ export class ToolsManager {
1037
1184
  }
1038
1185
  throw new Error(`Cannot execute external MCP tool: NeuroLink executeExternalMCPTool not available`);
1039
1186
  };
1040
- const guardedExecute = this.wrapExecuteWithTruncation(tool.name, rawExecute);
1187
+ const guardedExecute = this.wrapExecuteWithTruncation(registeredName, rawExecute);
1041
1188
  const loggedExecute = async (params, execOptions) => {
1042
1189
  try {
1043
1190
  return await guardedExecute(params, execOptions);
@@ -1053,7 +1200,7 @@ export class ToolsManager {
1053
1200
  return markEventWrapped(createAISDKTool({
1054
1201
  description: tool.description || `External MCP tool ${tool.name}`,
1055
1202
  inputSchema: finalSchema, // AI SDK v6 uses inputSchema (not parameters)
1056
- execute: this.wrapExecuteWithEvents(tool.name, loggedExecute),
1203
+ execute: this.wrapExecuteWithEvents(registeredName, loggedExecute),
1057
1204
  }));
1058
1205
  }
1059
1206
  catch (toolCreationError) {
@@ -12,32 +12,11 @@
12
12
  * @module files/fileReferenceRegistry
13
13
  */
14
14
  import type { FileSource, FileExtractionParams, FileExtractionResult, FileReadResult, FileReference, FileRegistrationOptions, FileRegistryOptions, FileSearchResult, SizeTier } from "../types/index.js";
15
- /**
16
- * Registry for managing file references with on-demand processing.
17
- *
18
- * Design decisions:
19
- * - One instance per NeuroLink SDK instance (not global singleton)
20
- * - File buffers persisted to temp dir for later streaming access
21
- * - LRU eviction when maxFiles exceeded
22
- * - Thread-safe via sequential async operations (Node.js single-threaded)
23
- *
24
- * @example
25
- * ```typescript
26
- * const registry = new FileReferenceRegistry();
27
- * const ref = await registry.register(buffer, {
28
- * filename: 'report.xlsx',
29
- * });
30
- * console.log(ref.sizeTier); // 'medium'
31
- * console.log(ref.preview); // First 2000 chars of processed content
32
- * console.log(ref.estimatedTokens); // Type-aware estimate
33
- *
34
- * // Later, LLM requests specific section
35
- * const section = await registry.readSection(ref.id, 1, 50, 5000);
36
- * ```
37
- */
38
15
  export declare class FileReferenceRegistry {
39
16
  private files;
40
17
  private tempDir;
18
+ /** `tempDir` was supplied by the host rather than minted here. */
19
+ private readonly tempDirSupplied;
41
20
  private maxFiles;
42
21
  private maxTempBytes;
43
22
  private defaultPreviewChars;
@@ -12,7 +12,8 @@
12
12
  * @module files/fileReferenceRegistry
13
13
  */
14
14
  import { randomUUID } from "node:crypto";
15
- import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
15
+ import { rmSync } from "node:fs";
16
+ import { mkdir, mkdtemp, readFile, rm, stat, unlink, writeFile, } from "node:fs/promises";
16
17
  import { tmpdir } from "node:os";
17
18
  import { basename, extname, join } from "node:path";
18
19
  import { estimatePostProcessingTokens } from "../context/fileTokenBudget.js";
@@ -51,17 +52,49 @@ const MAX_ACCEPTED_SIZE = 2 * 1024 * 1024 * 1024;
51
52
  * const section = await registry.readSection(ref.id, 1, 50, 5000);
52
53
  * ```
53
54
  */
55
+ /**
56
+ * Temp directories this process created and has not yet removed. Swept on
57
+ * `exit` so an attachment persisted for lazy reading does not outlive the
58
+ * process that read it (a `clear()` that never ran, a crash, a SIGTERM that
59
+ * reached `exit`). Created directories are 0o700 and files 0o600, so until
60
+ * then only this user can read them.
61
+ */
62
+ const ACTIVE_TEMP_DIRS = new Set();
63
+ let exitSweepRegistered = false;
64
+ function registerExitSweep() {
65
+ if (exitSweepRegistered) {
66
+ return;
67
+ }
68
+ exitSweepRegistered = true;
69
+ process.on("exit", () => {
70
+ for (const dir of ACTIVE_TEMP_DIRS) {
71
+ try {
72
+ rmSync(dir, { recursive: true, force: true });
73
+ }
74
+ catch {
75
+ // Best effort: the exit handler must not throw.
76
+ }
77
+ }
78
+ ACTIVE_TEMP_DIRS.clear();
79
+ });
80
+ }
54
81
  export class FileReferenceRegistry {
55
82
  files = new Map();
56
83
  tempDir;
84
+ /** `tempDir` was supplied by the host rather than minted here. */
85
+ tempDirSupplied;
57
86
  maxFiles;
58
87
  maxTempBytes;
59
88
  defaultPreviewChars;
60
89
  currentTempBytes = 0;
61
90
  tempDirCreated = false;
62
91
  constructor(options = {}) {
92
+ // A host-supplied directory is used as given; otherwise the directory is
93
+ // minted with `mkdtemp` (0o700) on first use, so its name is unguessable
94
+ // and nothing else on the host can read what lands in it.
95
+ this.tempDirSupplied = Boolean(options.tempDir);
63
96
  this.tempDir =
64
- options.tempDir || join(tmpdir(), "neurolink-files", randomUUID());
97
+ options.tempDir || join(tmpdir(), `neurolink-files-${randomUUID()}`);
65
98
  this.maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
66
99
  this.maxTempBytes = options.maxTempBytes ?? DEFAULT_MAX_TEMP_BYTES;
67
100
  this.defaultPreviewChars =
@@ -686,6 +719,13 @@ export class FileReferenceRegistry {
686
719
  }
687
720
  this.files.clear();
688
721
  this.currentTempBytes = 0;
722
+ // Remove the directory itself, not only the files in it, and forget it so
723
+ // a later registration mints a fresh one.
724
+ if (this.tempDirCreated) {
725
+ await rm(this.tempDir, { recursive: true, force: true }).catch(() => undefined);
726
+ ACTIVE_TEMP_DIRS.delete(this.tempDir);
727
+ this.tempDirCreated = false;
728
+ }
689
729
  }
690
730
  /**
691
731
  * Get the number of registered files.
@@ -1505,13 +1545,23 @@ export class FileReferenceRegistry {
1505
1545
  throw new Error(`Temp directory budget exceeded (${this.formatSize(this.maxTempBytes)})`);
1506
1546
  }
1507
1547
  }
1508
- // Ensure temp directory exists
1548
+ // Ensure temp directory exists. Owner-only: the files persisted here are
1549
+ // the caller's attachments, and the OS temp directory is shared.
1509
1550
  if (!this.tempDirCreated) {
1510
- await mkdir(this.tempDir, { recursive: true });
1551
+ if (this.tempDirSupplied) {
1552
+ await mkdir(this.tempDir, { recursive: true, mode: 0o700 });
1553
+ }
1554
+ else {
1555
+ this.tempDir = await mkdtemp(join(tmpdir(), "neurolink-files-"));
1556
+ }
1557
+ registerExitSweep();
1558
+ ACTIVE_TEMP_DIRS.add(this.tempDir);
1511
1559
  this.tempDirCreated = true;
1512
1560
  }
1513
1561
  const tempPath = join(this.tempDir, `${id}${ext ? `.${ext}` : ""}`);
1514
- await writeFile(tempPath, buffer);
1562
+ // `wx`: create, never overwrite — an id collision or a file planted at the
1563
+ // path fails loudly instead of being written over. 0o600: owner-only.
1564
+ await writeFile(tempPath, buffer, { mode: 0o600, flag: "wx" });
1515
1565
  this.currentTempBytes += buffer.length;
1516
1566
  return tempPath;
1517
1567
  }
@@ -57,6 +57,7 @@ export class HITLManager extends EventEmitter {
57
57
  : (config.autoApproveExclusions ?? []),
58
58
  auditLogging: config.auditLogging ?? false, // Default: false
59
59
  customRules: config.customRules ?? [], // Default: empty array
60
+ confirmBuiltinTools: config.confirmBuiltinTools ?? true, // Default: true (built-in write/bash tools prompt)
60
61
  };
61
62
  if (!configWithDefaults.enabled) {
62
63
  return configWithDefaults; // If disabled, don't validate other fields