@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.
- package/CHANGELOG.md +5 -2
- package/README.md +1 -1
- package/dist/action/actionExecutor.d.ts +33 -3
- package/dist/action/actionExecutor.js +71 -15
- package/dist/action/actionInputs.js +4 -1
- package/dist/action/cliVersion.d.ts +25 -0
- package/dist/action/cliVersion.js +26 -0
- package/dist/action/index.d.ts +2 -1
- package/dist/action/index.js +2 -1
- package/dist/adapters/video/directorPipeline.js +46 -36
- package/dist/adapters/video/ffmpegAdapter.d.ts +17 -0
- package/dist/adapters/video/ffmpegAdapter.js +32 -5
- package/dist/adapters/video/frameExtractor.js +1 -1
- package/dist/adapters/video/videoMerger.js +5 -2
- package/dist/agent/arithmeticEvaluator.d.ts +32 -0
- package/dist/agent/arithmeticEvaluator.js +349 -0
- package/dist/agent/directTools.d.ts +6 -18
- package/dist/agent/directTools.js +17 -56
- package/dist/artifacts/artifactStore.d.ts +22 -1
- package/dist/artifacts/artifactStore.js +66 -6
- package/dist/auth/anthropicOAuth.d.ts +26 -1
- package/dist/auth/anthropicOAuth.js +81 -5
- package/dist/auth/secureFiles.d.ts +37 -0
- package/dist/auth/secureFiles.js +95 -0
- package/dist/auth/tokenStore.d.ts +43 -6
- package/dist/auth/tokenStore.js +260 -79
- package/dist/avatar/providers/DIDAvatar.js +49 -45
- package/dist/avatar/providers/ReplicateAvatar.js +33 -40
- package/dist/browser/neurolink.min.js +485 -558
- package/dist/cli/commands/auth.js +112 -70
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +128 -13
- package/dist/cli/commands/serve.d.ts +10 -1
- package/dist/cli/commands/serve.js +98 -17
- package/dist/cli/commands/server.js +17 -5
- package/dist/cli/index.js +146 -5
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/utils/envManager.js +17 -3
- package/dist/cli/utils/serverUtils.d.ts +39 -3
- package/dist/cli/utils/serverUtils.js +63 -6
- package/dist/core/baseProvider.js +7 -1
- package/dist/core/modules/MessageBuilder.js +5 -0
- package/dist/core/modules/ToolsManager.d.ts +46 -1
- package/dist/core/modules/ToolsManager.js +190 -43
- package/dist/files/fileReferenceRegistry.d.ts +2 -23
- package/dist/files/fileReferenceRegistry.js +55 -5
- package/dist/hitl/hitlManager.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -1
- package/dist/mcp/auth/tokenStorage.d.ts +7 -0
- package/dist/mcp/auth/tokenStorage.js +23 -4
- package/dist/mcp/externalServerManager.d.ts +13 -3
- package/dist/mcp/externalServerManager.js +154 -136
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +3 -0
- package/dist/mcp/mcpClientFactory.d.ts +14 -0
- package/dist/mcp/mcpClientFactory.js +70 -7
- package/dist/mcp/projectConfigTrust.d.ts +72 -0
- package/dist/mcp/projectConfigTrust.js +237 -0
- package/dist/mcp/transportUrlPolicy.d.ts +39 -0
- package/dist/mcp/transportUrlPolicy.js +172 -0
- package/dist/music/providers/BeatovenMusic.js +35 -46
- package/dist/music/providers/ReplicateMusic.js +33 -40
- package/dist/neurolink.d.ts +16 -1
- package/dist/neurolink.js +64 -11
- package/dist/processors/archive/ArchiveProcessor.js +15 -1
- package/dist/processors/archive/zipEntryIndex.d.ts +22 -0
- package/dist/processors/archive/zipEntryIndex.js +33 -0
- package/dist/processors/base/BaseFileProcessor.d.ts +9 -21
- package/dist/processors/base/BaseFileProcessor.js +40 -111
- package/dist/processors/data/YamlProcessor.d.ts +13 -1
- package/dist/processors/data/YamlProcessor.js +93 -2
- package/dist/processors/document/OpenDocumentProcessor.js +7 -2
- package/dist/processors/document/PptxProcessor.js +12 -7
- package/dist/processors/media/VideoProcessor.js +93 -15
- package/dist/providers/openaiChatCompletionsClient.js +12 -3
- package/dist/proxy/accountLedger.js +6 -8
- package/dist/proxy/geminiFormat.d.ts +3 -11
- package/dist/proxy/geminiFormat.js +11 -2
- package/dist/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- package/dist/proxy/openaiFormat.d.ts +3 -9
- package/dist/proxy/openaiFormat.js +21 -15
- package/dist/proxy/proxyTranslationEngine.d.ts +2 -6
- package/dist/proxy/proxyTranslationEngine.js +25 -14
- package/dist/proxy/routingPolicy.d.ts +3 -1
- package/dist/proxy/routingPolicy.js +7 -8
- package/dist/proxy/tokenRefresh.d.ts +6 -0
- package/dist/proxy/tokenRefresh.js +13 -1
- package/dist/proxy/updateState.js +4 -4
- package/dist/rag/document/loaders.d.ts +0 -6
- package/dist/rag/document/loaders.js +16 -10
- package/dist/rag/ragIntegration.d.ts +2 -2
- package/dist/rag/ragIntegration.js +35 -10
- package/dist/rag/retrieval/vectorQueryTool.d.ts +7 -0
- package/dist/rag/retrieval/vectorQueryTool.js +54 -17
- package/dist/server/abstract/baseServerAdapter.d.ts +43 -0
- package/dist/server/abstract/baseServerAdapter.js +119 -6
- package/dist/server/adapters/expressAdapter.d.ts +3 -0
- package/dist/server/adapters/expressAdapter.js +78 -6
- package/dist/server/adapters/fastifyAdapter.d.ts +3 -0
- package/dist/server/adapters/fastifyAdapter.js +165 -70
- package/dist/server/adapters/honoAdapter.d.ts +9 -1
- package/dist/server/adapters/honoAdapter.js +99 -21
- package/dist/server/adapters/koaAdapter.d.ts +1 -0
- package/dist/server/adapters/koaAdapter.js +153 -74
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +4 -0
- package/dist/server/middleware/auth.js +6 -2
- package/dist/server/middleware/cache.js +6 -1
- package/dist/server/middleware/rateLimit.js +29 -4
- package/dist/server/routes/agentRoutes.js +24 -11
- package/dist/server/routes/claudeProxyRoutes.d.ts +1 -0
- package/dist/server/routes/claudeProxyRoutes.js +273 -84
- package/dist/server/routes/codexProxyRoutes.d.ts +2 -2
- package/dist/server/routes/codexProxyRoutes.js +125 -40
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +35 -4
- package/dist/server/routes/mcpRoutes.d.ts +6 -2
- package/dist/server/routes/mcpRoutes.js +21 -7
- package/dist/server/routes/memoryRoutes.js +7 -6
- package/dist/server/routes/toolRoutes.d.ts +12 -2
- package/dist/server/routes/toolRoutes.js +33 -3
- package/dist/server/routes/transcribeRoutes.js +60 -13
- package/dist/server/utils/clientAddress.d.ts +23 -0
- package/dist/server/utils/clientAddress.js +65 -0
- package/dist/server/utils/clientError.d.ts +35 -0
- package/dist/server/utils/clientError.js +129 -0
- package/dist/server/utils/pathMatch.d.ts +21 -0
- package/dist/server/utils/pathMatch.js +41 -0
- package/dist/server/utils/validation.d.ts +9 -0
- package/dist/server/utils/validation.js +9 -0
- package/dist/server/websocket/WebSocketHandler.js +22 -3
- package/dist/server/websocket/frameQueue.d.ts +19 -0
- package/dist/server/websocket/frameQueue.js +41 -3
- package/dist/server/websocket/transcribeWebSocket.d.ts +8 -1
- package/dist/server/websocket/transcribeWebSocket.js +223 -34
- package/dist/skills/skillStores.js +5 -0
- package/dist/tasks/store/fileTaskStore.d.ts +2 -0
- package/dist/tasks/store/fileTaskStore.js +11 -4
- package/dist/types/attachments.d.ts +117 -0
- package/dist/types/attachments.js +15 -0
- package/dist/types/auth.d.ts +6 -0
- package/dist/types/cli.d.ts +4 -0
- package/dist/types/config.d.ts +56 -0
- package/dist/types/file.d.ts +8 -0
- package/dist/types/hitl.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/mcp.d.ts +98 -0
- package/dist/types/multimodal.js +0 -41
- package/dist/types/openaiCompatible.d.ts +1 -1
- package/dist/types/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +71 -16
- package/dist/types/proxyClient.d.ts +2 -0
- package/dist/types/rag.d.ts +9 -0
- package/dist/types/safeFetch.d.ts +40 -0
- package/dist/types/server.d.ts +206 -10
- package/dist/types/tools.d.ts +57 -0
- package/dist/types/transcribe.d.ts +6 -0
- package/dist/utils/attachmentPolicy.d.ts +89 -0
- package/dist/utils/attachmentPolicy.js +327 -0
- package/dist/utils/dotenvBootstrap.d.ts +48 -19
- package/dist/utils/dotenvBootstrap.js +172 -31
- package/dist/utils/fileDetector.js +115 -65
- package/dist/utils/json/coerce.js +11 -1
- package/dist/utils/logSanitize.d.ts +5 -0
- package/dist/utils/logSanitize.js +8 -0
- package/dist/utils/logger.js +73 -1
- package/dist/utils/messageBuilder.d.ts +2 -2
- package/dist/utils/messageBuilder.js +118 -105
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +2 -0
- package/dist/utils/redis.d.ts +12 -0
- package/dist/utils/redis.js +72 -0
- package/dist/utils/retryAfter.d.ts +32 -0
- package/dist/utils/retryAfter.js +135 -11
- package/dist/utils/safeFetch.d.ts +22 -1
- package/dist/utils/safeFetch.js +132 -38
- package/dist/utils/safeStoreId.d.ts +19 -0
- package/dist/utils/safeStoreId.js +29 -0
- package/dist/utils/schemaConversion.d.ts +30 -3
- package/dist/utils/schemaConversion.js +457 -84
- package/dist/utils/sizeGuard.js +2 -2
- package/dist/utils/ssrfGuard.d.ts +3 -3
- package/dist/utils/ssrfGuard.js +71 -39
- package/dist/voice/eval/sttEvalStore.d.ts +26 -3
- package/dist/voice/eval/sttEvalStore.js +77 -12
- package/dist/voice/livekit/agentDefinition.d.ts +18 -0
- package/dist/voice/livekit/agentDefinition.js +22 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +1 -1
- package/dist/voice/livekit/realtimeMcpTools.js +8 -7
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +2 -4
- package/dist/voice/livekit/realtimeVoiceAgent.js +18 -7
- package/dist/voice/livekit/voiceAgent.d.ts +2 -2
- package/dist/voice/livekit/voiceAgent.js +2 -1
- package/dist/voice/livekit/xaiSessionWatch.d.ts +0 -5
- package/dist/voice/livekit/xaiSessionWatch.js +30 -0
- package/dist/voice/providers/GeminiLive.js +21 -6
- package/dist/voice/providers/GoogleSTT.js +8 -5
- package/dist/voice/transcribe.d.ts +2 -2
- package/dist/voice/transcribe.js +34 -18
- package/dist/voice/whistle/audio.js +11 -5
- package/dist/workflow/workflows/adaptiveWorkflow.d.ts +2 -2
- package/dist/workflow/workflows/adaptiveWorkflow.js +10 -10
- package/dist/workflow/workflows/consensusWorkflow.d.ts +4 -4
- package/dist/workflow/workflows/consensusWorkflow.js +12 -12
- package/dist/workflow/workflows/fallbackWorkflow.d.ts +3 -3
- package/dist/workflow/workflows/fallbackWorkflow.js +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.d.ts +7 -7
- package/dist/workflow/workflows/multiJudgeWorkflow.js +31 -31
- package/docs-site/static/search-index.json +74 -52
- package/package.json +173 -156
- 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
|
-
/**
|
|
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
|
-
*
|
|
43
|
-
* playground header cannot bypass a key the operator
|
|
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
|
-
|
|
100
|
+
const exempt = new Set(healthExemptPaths(basePath));
|
|
101
|
+
const inner = createAuthMiddleware({
|
|
52
102
|
type: "custom",
|
|
53
103
|
extractToken: (ctx) => {
|
|
54
|
-
const bearer = /^Bearer\s+(
|
|
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 ?? {},
|
|
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
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
1004
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
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(),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/hitl/hitlManager.js
CHANGED
|
@@ -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
|