@juspay/neurolink 13.8.17 → 13.8.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -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/proxy/inboundAuth.d.ts +89 -0
- package/dist/proxy/inboundAuth.js +219 -0
- 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/processor.d.ts +14 -1
- package/dist/types/proxy.d.ts +62 -3
- 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/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/docs-site/static/search-index.json +71 -49
- package/package.json +171 -156
- package/scripts/observability/import-openobserve-dashboard.mjs +11 -3
|
@@ -39,6 +39,8 @@ import { logRequest, logRequestAttempt, logBodyCapture, isProxyBodyCaptureEnable
|
|
|
39
39
|
import { createRawStreamCapture } from "../../proxy/rawStreamCapture.js";
|
|
40
40
|
import { resolveProxyLogTraceContext, getProxyRequestTraceContext, } from "../../proxy/proxyTraceContext.js";
|
|
41
41
|
import { isBorrowedRequest } from "../../proxy/shareContext.js";
|
|
42
|
+
import { authorizeInboundRequest, inboundAuthRefusalResponse, resolveInboundToken, resolveMaxAccountsPerRequest, warnIfProxyRoutesUnauthenticated, } from "../../proxy/inboundAuth.js";
|
|
43
|
+
import { MAX_COOLDOWN_MS_BY_REASON } from "../../proxy/routingEvidence.js";
|
|
42
44
|
import { ProxyTracer, recordFallbackAttempt } from "../../proxy/proxyTracer.js";
|
|
43
45
|
import { parseRetryAfterMs } from "../../proxy/routingPolicy.js";
|
|
44
46
|
import { recordAttempt, recordAttemptError, recordFinalError, recordFinalSuccess, } from "../../proxy/usageStats.js";
|
|
@@ -153,20 +155,52 @@ async function refreshCodexTokenOnceWithDependencies(key, refreshToken, store, r
|
|
|
153
155
|
async function refreshCodexTokenOnce(key, refreshToken) {
|
|
154
156
|
return refreshCodexTokenOnceWithDependencies(key, refreshToken, tokenStore, refreshCodexToken);
|
|
155
157
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
168
|
-
"
|
|
158
|
+
/**
|
|
159
|
+
* Protocol headers a client may influence upstream: the documented Codex
|
|
160
|
+
* client fingerprint and content negotiation. Everything else — proxy-internal
|
|
161
|
+
* `x-neurolink-*` (a borrower's share token among them), trace context, CDN and
|
|
162
|
+
* forwarding identity, cookies, hop-by-hop — is dropped. The client's own
|
|
163
|
+
* credentials are replaced with the pooled account's, never forwarded.
|
|
164
|
+
*/
|
|
165
|
+
const FORWARDABLE_UPSTREAM_HEADERS = new Set([
|
|
166
|
+
"accept",
|
|
167
|
+
"content-type",
|
|
168
|
+
"user-agent",
|
|
169
|
+
"originator",
|
|
170
|
+
"session_id",
|
|
171
|
+
"conversation_id",
|
|
172
|
+
"openai-beta",
|
|
169
173
|
]);
|
|
174
|
+
const FORWARDABLE_UPSTREAM_HEADER_PREFIXES = ["x-codex-"];
|
|
175
|
+
function isForwardableCodexHeader(lowerName) {
|
|
176
|
+
return (FORWARDABLE_UPSTREAM_HEADERS.has(lowerName) ||
|
|
177
|
+
FORWARDABLE_UPSTREAM_HEADER_PREFIXES.some((prefix) => lowerName.startsWith(prefix)));
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Body fields that reference or create account-side state at OpenAI. A
|
|
181
|
+
* borrower may not read another caller's stored response, pin a cache key or
|
|
182
|
+
* stamp `metadata`/`user` onto the lender's account; their own `instructions`
|
|
183
|
+
* are their prompt and stay.
|
|
184
|
+
*/
|
|
185
|
+
const BORROWER_STRIPPED_BODY_FIELDS = [
|
|
186
|
+
"previous_response_id",
|
|
187
|
+
"prompt_cache_key",
|
|
188
|
+
"metadata",
|
|
189
|
+
"user",
|
|
190
|
+
];
|
|
191
|
+
function stripBorrowerAccountState(body) {
|
|
192
|
+
if (!isBorrowedRequest()) {
|
|
193
|
+
return body;
|
|
194
|
+
}
|
|
195
|
+
const stripped = { ...body };
|
|
196
|
+
for (const field of BORROWER_STRIPPED_BODY_FIELDS) {
|
|
197
|
+
delete stripped[field];
|
|
198
|
+
}
|
|
199
|
+
if ("store" in stripped) {
|
|
200
|
+
stripped.store = false;
|
|
201
|
+
}
|
|
202
|
+
return stripped;
|
|
203
|
+
}
|
|
170
204
|
/** Build a Codex error body as a Response with the intended status. */
|
|
171
205
|
function buildCodexErrorResponse(status, message, code) {
|
|
172
206
|
return new Response(JSON.stringify({
|
|
@@ -290,7 +324,7 @@ function buildCodexUpstreamHeaders(clientHeaders, account) {
|
|
|
290
324
|
const headers = {};
|
|
291
325
|
for (const [name, value] of Object.entries(clientHeaders)) {
|
|
292
326
|
const lower = name.toLowerCase();
|
|
293
|
-
if (
|
|
327
|
+
if (!isForwardableCodexHeader(lower) || typeof value !== "string") {
|
|
294
328
|
continue;
|
|
295
329
|
}
|
|
296
330
|
headers[lower] = value;
|
|
@@ -361,16 +395,20 @@ function planCodexCooldown(quota, retryAfterMs, now, errorText = "") {
|
|
|
361
395
|
// skew. Taken literally the account is eligible again immediately and the
|
|
362
396
|
// pool re-sends to something the provider just rejected.
|
|
363
397
|
const floor = now + DEFAULT_TRANSIENT_COOLDOWN_MS;
|
|
398
|
+
// A reset is upstream's claim about the future. Bound it by what the window
|
|
399
|
+
// can mean, as the Claude engine does: a bogus far-future reset must not park
|
|
400
|
+
// an account until someone edits the cooldown file by hand.
|
|
401
|
+
const clamp = (untilMs, reason) => Math.min(Math.max(untilMs, floor), now + (MAX_COOLDOWN_MS_BY_REASON[reason] ?? MAX_TRANSIENT_COOLDOWN_MS));
|
|
364
402
|
if (quota) {
|
|
365
403
|
if (quota.weeklyStatus === "rejected" && quota.weeklyResetAt > 0) {
|
|
366
404
|
return {
|
|
367
|
-
coolingUntil:
|
|
405
|
+
coolingUntil: clamp(quota.weeklyResetAt * 1000, "weekly"),
|
|
368
406
|
reason: "weekly",
|
|
369
407
|
};
|
|
370
408
|
}
|
|
371
409
|
if (quota.sessionStatus === "rejected" && quota.sessionResetAt > 0) {
|
|
372
410
|
return {
|
|
373
|
-
coolingUntil:
|
|
411
|
+
coolingUntil: clamp(quota.sessionResetAt * 1000, "session"),
|
|
374
412
|
reason: "session",
|
|
375
413
|
};
|
|
376
414
|
}
|
|
@@ -379,7 +417,7 @@ function planCodexCooldown(quota, retryAfterMs, now, errorText = "") {
|
|
|
379
417
|
if (bodyQuota) {
|
|
380
418
|
if (bodyQuota.scope !== "unknown" && bodyQuota.resetAt > 0) {
|
|
381
419
|
return {
|
|
382
|
-
coolingUntil:
|
|
420
|
+
coolingUntil: clamp(bodyQuota.resetAt, bodyQuota.scope),
|
|
383
421
|
reason: bodyQuota.scope,
|
|
384
422
|
};
|
|
385
423
|
}
|
|
@@ -400,8 +438,13 @@ function publishCodexHeaders(ctx, account, attempt, quota) {
|
|
|
400
438
|
if (!ctx.responseHeaders) {
|
|
401
439
|
ctx.responseHeaders = {};
|
|
402
440
|
}
|
|
403
|
-
|
|
404
|
-
|
|
441
|
+
// The label is the lender's identity (an email for a ChatGPT account). A
|
|
442
|
+
// borrower gets the quota it routes on, not whose account served it — the
|
|
443
|
+
// same redaction the Claude engine applies for borrowed traffic.
|
|
444
|
+
if (!isBorrowedRequest()) {
|
|
445
|
+
ctx.responseHeaders["x-neurolink-account"] = account.label;
|
|
446
|
+
ctx.responseHeaders["x-neurolink-account-type"] = "codex-oauth";
|
|
447
|
+
}
|
|
405
448
|
ctx.responseHeaders["x-neurolink-served-by"] = "codex";
|
|
406
449
|
ctx.responseHeaders["x-neurolink-attempt"] = String(attempt);
|
|
407
450
|
ctx.responseHeaders["x-neurolink-quota-source"] = quota ? "live" : "none";
|
|
@@ -1536,6 +1579,29 @@ async function handleCodexUpstreamFailure(rt, args) {
|
|
|
1536
1579
|
lastErrorMessage: "Codex account rate-limited",
|
|
1537
1580
|
};
|
|
1538
1581
|
}
|
|
1582
|
+
// Any other 4xx is upstream's verdict on the request itself — a malformed
|
|
1583
|
+
// body, an unknown model, an over-long input. Every other account would
|
|
1584
|
+
// answer the same way, so trying them only multiplies the cost: end the
|
|
1585
|
+
// request here, with the upstream status.
|
|
1586
|
+
if (upstream.status >= 400 &&
|
|
1587
|
+
upstream.status < 500 &&
|
|
1588
|
+
upstream.status !== 401 &&
|
|
1589
|
+
upstream.status !== 403) {
|
|
1590
|
+
const errorMessage = summarizeCodexUpstreamError(errText, "Codex error");
|
|
1591
|
+
recordAttemptError(account.label, CODEX_ACCOUNT_TYPE, upstream.status);
|
|
1592
|
+
writeAttempt(account, attempt, attemptStartedAt, upstream.status, {
|
|
1593
|
+
errorType: "invalid_request_error",
|
|
1594
|
+
errorMessage,
|
|
1595
|
+
retryable: false,
|
|
1596
|
+
});
|
|
1597
|
+
return {
|
|
1598
|
+
lastFailure: { errorType: "invalid_request_error", retryable: false },
|
|
1599
|
+
lastErrorStatus: upstream.status,
|
|
1600
|
+
lastErrorCode: getProxyUpstreamFailure({ responseBody: errText })?.code,
|
|
1601
|
+
lastErrorMessage: errorMessage,
|
|
1602
|
+
refusedRequest: true,
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1539
1605
|
// Other non-ok → record and rotate.
|
|
1540
1606
|
if (upstream.status === 401 || upstream.status === 403) {
|
|
1541
1607
|
// Reached when the account has no refresh token to retry with, or when
|
|
@@ -1748,7 +1814,7 @@ loopbackPort, internalDispatch) {
|
|
|
1748
1814
|
body: state.body,
|
|
1749
1815
|
});
|
|
1750
1816
|
state.contextPreflight = preparedContext.evidence;
|
|
1751
|
-
state.body = preparedContext.body;
|
|
1817
|
+
state.body = stripBorrowerAccountState(preparedContext.body);
|
|
1752
1818
|
state.bodyStr = JSON.stringify(state.body);
|
|
1753
1819
|
}
|
|
1754
1820
|
catch (error) {
|
|
@@ -1774,10 +1840,12 @@ loopbackPort, internalDispatch) {
|
|
|
1774
1840
|
}
|
|
1775
1841
|
const now = Date.now();
|
|
1776
1842
|
const ordered = orderCodexAccounts(accounts, now);
|
|
1777
|
-
const
|
|
1778
|
-
if (
|
|
1843
|
+
const notCooling = ordered.filter((a) => !(a.coolingUntil !== undefined && a.coolingUntil > now));
|
|
1844
|
+
if (notCooling.length === 0) {
|
|
1779
1845
|
return respondCodexPoolCooling(rt, ordered, now);
|
|
1780
1846
|
}
|
|
1847
|
+
// One inbound request may be tried against at most this many accounts.
|
|
1848
|
+
const eligible = notCooling.slice(0, resolveMaxAccountsPerRequest());
|
|
1781
1849
|
let attempt = 0;
|
|
1782
1850
|
let lastErrorMessage = "All Codex accounts failed";
|
|
1783
1851
|
let lastErrorStatus = 502;
|
|
@@ -2022,20 +2090,26 @@ async function handleCodexModelsRequest(ctx) {
|
|
|
2022
2090
|
}
|
|
2023
2091
|
const now = Date.now();
|
|
2024
2092
|
const ordered = orderCodexAccounts(accounts, now);
|
|
2025
|
-
//
|
|
2026
|
-
//
|
|
2027
|
-
//
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
//
|
|
2037
|
-
//
|
|
2038
|
-
|
|
2093
|
+
// Discovery used to reach every account, cooling ones included, and relay
|
|
2094
|
+
// the client's whole query. Only healthy accounts answer it now, capped by
|
|
2095
|
+
// the per-request account limit. Every call still reaches upstream: the
|
|
2096
|
+
// limits it reports (context window, output ceiling) are recorded per model
|
|
2097
|
+
// and must refresh whenever upstream changes them.
|
|
2098
|
+
const candidates = ordered
|
|
2099
|
+
.filter((a) => !(a.coolingUntil !== undefined && a.coolingUntil > now))
|
|
2100
|
+
.slice(0, resolveMaxAccountsPerRequest());
|
|
2101
|
+
if (candidates.length === 0) {
|
|
2102
|
+
return buildCodexErrorResponse(503, "Every Codex account is cooling; model discovery is unavailable until one recovers.");
|
|
2103
|
+
}
|
|
2104
|
+
// Forward only the CLI's `client_version` — upstream *requires* it: without
|
|
2105
|
+
// it ChatGPT answers 400 with a pydantic "Field required" on
|
|
2106
|
+
// ('query','client_version'). Rebuild from ctx.query, not ctx.path: path
|
|
2107
|
+
// carries no query string. Nothing else a client puts in the query travels.
|
|
2108
|
+
const params = new URLSearchParams();
|
|
2109
|
+
const clientVersion = ctx.query?.client_version;
|
|
2110
|
+
if (typeof clientVersion === "string" && clientVersion !== "") {
|
|
2111
|
+
params.set("client_version", clientVersion);
|
|
2112
|
+
}
|
|
2039
2113
|
const query = params.toString();
|
|
2040
2114
|
const url = query ? `${CODEX_MODELS_URL}?${query}` : CODEX_MODELS_URL;
|
|
2041
2115
|
let lastErrorStatus = 502;
|
|
@@ -2099,10 +2173,10 @@ async function handleCodexModelsRequest(ctx) {
|
|
|
2099
2173
|
// query was not forwarded correctly, and hiding it behind a retry would
|
|
2100
2174
|
// bury the exact regression this route was added to fix.
|
|
2101
2175
|
const body = await upstream.text();
|
|
2176
|
+
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
2102
2177
|
if (upstream.ok) {
|
|
2103
2178
|
registerCodexDiscoveredLimits(body);
|
|
2104
2179
|
}
|
|
2105
|
-
const contentType = upstream.headers.get("content-type") ?? "application/json";
|
|
2106
2180
|
return new Response(body, {
|
|
2107
2181
|
status: upstream.status,
|
|
2108
2182
|
headers: { "content-type": contentType },
|
|
@@ -2175,8 +2249,19 @@ function warnIfAnthropicOutboundFallbackUnreachable(runtimeConfigProvider, loopb
|
|
|
2175
2249
|
* `loopbackPort` when omitted.
|
|
2176
2250
|
* @returns RouteGroup with the Codex backend Responses endpoint.
|
|
2177
2251
|
*/
|
|
2178
|
-
export function createCodexProxyRoutes(basePath = "", runtimeConfigProvider, loopbackPort, internalDispatch) {
|
|
2252
|
+
export function createCodexProxyRoutes(basePath = "", runtimeConfigProvider, loopbackPort, internalDispatch, inboundAuth) {
|
|
2179
2253
|
warnIfAnthropicOutboundFallbackUnreachable(runtimeConfigProvider, loopbackPort, internalDispatch);
|
|
2254
|
+
const inboundToken = resolveInboundToken(inboundAuth?.token);
|
|
2255
|
+
warnIfProxyRoutesUnauthenticated("Codex", inboundAuth, inboundToken);
|
|
2256
|
+
const guard = (handler) => (ctx) => {
|
|
2257
|
+
if (inboundToken) {
|
|
2258
|
+
const verdict = authorizeInboundRequest(ctx.headers, inboundToken);
|
|
2259
|
+
if (!verdict.ok) {
|
|
2260
|
+
return Promise.resolve(inboundAuthRefusalResponse());
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return handler(ctx);
|
|
2264
|
+
};
|
|
2180
2265
|
return {
|
|
2181
2266
|
prefix: `${basePath}/backend-api/codex`,
|
|
2182
2267
|
routes: [
|
|
@@ -2184,13 +2269,13 @@ export function createCodexProxyRoutes(basePath = "", runtimeConfigProvider, loo
|
|
|
2184
2269
|
method: "POST",
|
|
2185
2270
|
path: `${basePath}/backend-api/codex/responses`,
|
|
2186
2271
|
description: "Codex ChatGPT-backend Responses API (account pool)",
|
|
2187
|
-
handler: (ctx) => handleCodexResponsesRequest(ctx, runtimeConfigProvider, loopbackPort, internalDispatch),
|
|
2272
|
+
handler: guard((ctx) => handleCodexResponsesRequest(ctx, runtimeConfigProvider, loopbackPort, internalDispatch)),
|
|
2188
2273
|
},
|
|
2189
2274
|
{
|
|
2190
2275
|
method: "GET",
|
|
2191
2276
|
path: `${basePath}/backend-api/codex/models`,
|
|
2192
2277
|
description: "Codex model discovery, relayed upstream (account pool)",
|
|
2193
|
-
handler: (ctx) => handleCodexModelsRequest(ctx),
|
|
2278
|
+
handler: guard((ctx) => handleCodexModelsRequest(ctx)),
|
|
2194
2279
|
},
|
|
2195
2280
|
],
|
|
2196
2281
|
};
|
|
@@ -42,4 +42,8 @@ export declare function registerAllRoutes(adapter: {
|
|
|
42
42
|
registerRouteGroup: (group: RouteGroup) => void;
|
|
43
43
|
listRoutes?: () => RouteDefinition[];
|
|
44
44
|
enableMiddlewareStateCarry?: () => void;
|
|
45
|
+
hasAuthenticationMiddleware?: () => boolean;
|
|
46
|
+
noteSensitiveRoutes?: (options?: {
|
|
47
|
+
allowUnauthenticated?: boolean;
|
|
48
|
+
}) => void;
|
|
45
49
|
}, basePath?: string, options?: CreateRoutesOptions): void;
|
|
@@ -40,13 +40,30 @@ export { createTranscribeRoutes } from "./transcribeRoutes.js";
|
|
|
40
40
|
* and docs/features/codex-proxy-support.md.
|
|
41
41
|
*/
|
|
42
42
|
export function createAllRoutes(basePath = "/api", options) {
|
|
43
|
+
// The tool and MCP execution routes act on the host. They refuse callers
|
|
44
|
+
// the server has not authenticated, when they can tell: registerAllRoutes()
|
|
45
|
+
// tells them through `hasAuthentication`; an explicit opt-out, by option or
|
|
46
|
+
// by NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1, opens them regardless.
|
|
47
|
+
const sensitive = {
|
|
48
|
+
allowUnauthenticated: options?.allowUnauthenticated === true || allowUnauthenticatedFromEnv(),
|
|
49
|
+
hasAuthentication: options?.hasAuthentication,
|
|
50
|
+
};
|
|
51
|
+
// The proxy doors spend pooled subscriptions; their inbound token defaults to
|
|
52
|
+
// NEUROLINK_PROXY_INBOUND_TOKEN and the factories warn loudly without one.
|
|
53
|
+
const proxyAuth = {
|
|
54
|
+
...options?.proxyAuth,
|
|
55
|
+
allowUnauthenticated: options?.proxyAuth?.allowUnauthenticated ??
|
|
56
|
+
options?.allowUnauthenticated === true,
|
|
57
|
+
};
|
|
43
58
|
const routes = [
|
|
44
59
|
createAgentRoutes(basePath, {
|
|
45
60
|
...options?.agent,
|
|
46
61
|
perUserAuth: options?.perUserAuth,
|
|
47
62
|
}),
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
...(options?.tools === false
|
|
64
|
+
? []
|
|
65
|
+
: [createToolRoutes(basePath, sensitive)]),
|
|
66
|
+
createMCPRoutes(basePath, sensitive),
|
|
50
67
|
createMemoryRoutes(basePath),
|
|
51
68
|
createHealthRoutes(basePath),
|
|
52
69
|
createTranscribeRoutes(basePath, options?.transcribe),
|
|
@@ -64,13 +81,13 @@ export function createAllRoutes(basePath = "/api", options) {
|
|
|
64
81
|
const enableCodexProxy = options?.proxy || options?.codexProxy;
|
|
65
82
|
const enableGeminiProxy = options?.proxy || options?.geminiProxy;
|
|
66
83
|
if (enableClaudeProxy) {
|
|
67
|
-
routes.push(createClaudeProxyRoutes(undefined, basePath));
|
|
84
|
+
routes.push(createClaudeProxyRoutes(undefined, basePath, "fill-first", false, undefined, { inboundAuth: proxyAuth }));
|
|
68
85
|
}
|
|
69
86
|
if (enableOpenAIProxy) {
|
|
70
87
|
routes.push(createOpenAIProxyRoutes(undefined, basePath));
|
|
71
88
|
}
|
|
72
89
|
if (enableCodexProxy) {
|
|
73
|
-
routes.push(createCodexProxyRoutes(basePath, options?.runtimeConfigProvider));
|
|
90
|
+
routes.push(createCodexProxyRoutes(basePath, options?.runtimeConfigProvider, undefined, undefined, proxyAuth));
|
|
74
91
|
}
|
|
75
92
|
if (enableGeminiProxy) {
|
|
76
93
|
routes.push(createGeminiProxyRoutes(undefined, basePath));
|
|
@@ -92,13 +109,27 @@ export function registerAllRoutes(adapter, basePath = "/api", options) {
|
|
|
92
109
|
if (options?.perUserAuth) {
|
|
93
110
|
adapter.enableMiddlewareStateCarry?.();
|
|
94
111
|
}
|
|
112
|
+
const allowUnauthenticated = options?.allowUnauthenticated === true || allowUnauthenticatedFromEnv();
|
|
113
|
+
// The adapter knows whether authentication middleware is registered, and is
|
|
114
|
+
// asked on every request, so authentication registered after the routes
|
|
115
|
+
// counts. It also warns at start() when the routes that act on the host are
|
|
116
|
+
// mounted with no such middleware (see BaseServerAdapter).
|
|
117
|
+
adapter.noteSensitiveRoutes?.({ allowUnauthenticated });
|
|
95
118
|
// If adapter has listRoutes and getRoutes not provided, use adapter's listRoutes
|
|
96
119
|
const routeOptions = {
|
|
97
120
|
...options,
|
|
98
121
|
getRoutes: options?.getRoutes ?? adapter.listRoutes?.bind(adapter),
|
|
122
|
+
hasAuthentication: options?.hasAuthentication ??
|
|
123
|
+
(adapter.hasAuthenticationMiddleware
|
|
124
|
+
? () => adapter.hasAuthenticationMiddleware?.() === true
|
|
125
|
+
: undefined),
|
|
99
126
|
};
|
|
100
127
|
const routeGroups = createAllRoutes(basePath, routeOptions);
|
|
101
128
|
for (const group of routeGroups) {
|
|
102
129
|
adapter.registerRouteGroup(group);
|
|
103
130
|
}
|
|
104
131
|
}
|
|
132
|
+
/** `NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1`: the operator's opt-out, read when routes are built. */
|
|
133
|
+
function allowUnauthenticatedFromEnv() {
|
|
134
|
+
return process.env.NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED?.trim() === "1";
|
|
135
|
+
}
|
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
* MCP Routes
|
|
3
3
|
* Endpoints for MCP server management
|
|
4
4
|
*/
|
|
5
|
-
import type { RouteGroup } from "../../types/index.js";
|
|
5
|
+
import type { RouteGroup, ServerSensitiveRouteOptions } from "../../types/index.js";
|
|
6
6
|
/**
|
|
7
7
|
* Create MCP server management routes
|
|
8
|
+
*
|
|
9
|
+
* @param options - `hasAuthentication` / `allowUnauthenticated`: whether the
|
|
10
|
+
* routes that change or use servers (reconnect, remove, execute a tool)
|
|
11
|
+
* answer an unauthenticated caller (see `ServerSensitiveRouteOptions`).
|
|
8
12
|
*/
|
|
9
|
-
export declare function createMCPRoutes(basePath?: string): RouteGroup;
|
|
13
|
+
export declare function createMCPRoutes(basePath?: string, options?: ServerSensitiveRouteOptions): RouteGroup;
|
|
@@ -6,7 +6,14 @@ import { SpanStatusCode } from "@opentelemetry/api";
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
8
8
|
import { tracers } from "../../telemetry/tracers.js";
|
|
9
|
+
import { shapeRouteError } from "../utils/clientError.js";
|
|
10
|
+
import { refuseUnauthenticatedExecution } from "./toolRoutes.js";
|
|
9
11
|
import { createErrorResponse, ServerNameParamSchema, ToolArgumentsSchema, validateParams, validateRequest, } from "../utils/validation.js";
|
|
12
|
+
/** The route's error response for an exception: a fixed message, the detail logged. */
|
|
13
|
+
function mcpFailure(code, ctx, error, details) {
|
|
14
|
+
const shaped = shapeRouteError("mcpRoutes", ctx.requestId, error);
|
|
15
|
+
return createErrorResponse(code, shaped.message, details, ctx.requestId, shaped.status);
|
|
16
|
+
}
|
|
10
17
|
/**
|
|
11
18
|
* Wrap a route handler with an OTel span for HTTP observability.
|
|
12
19
|
*/
|
|
@@ -166,7 +173,7 @@ async function handleReconnectServer(ctx) {
|
|
|
166
173
|
};
|
|
167
174
|
}
|
|
168
175
|
catch (error) {
|
|
169
|
-
return
|
|
176
|
+
return mcpFailure("RECONNECT_FAILED", ctx, error);
|
|
170
177
|
}
|
|
171
178
|
}
|
|
172
179
|
/**
|
|
@@ -198,7 +205,7 @@ async function handleRemoveServer(ctx) {
|
|
|
198
205
|
};
|
|
199
206
|
}
|
|
200
207
|
catch (error) {
|
|
201
|
-
return
|
|
208
|
+
return mcpFailure("REMOVE_FAILED", ctx, error);
|
|
202
209
|
}
|
|
203
210
|
}
|
|
204
211
|
/**
|
|
@@ -278,7 +285,9 @@ async function handleExecuteTool(ctx) {
|
|
|
278
285
|
};
|
|
279
286
|
}
|
|
280
287
|
catch (error) {
|
|
281
|
-
return
|
|
288
|
+
return mcpFailure("EXECUTION_FAILED", ctx, error, {
|
|
289
|
+
duration: Date.now() - startTime,
|
|
290
|
+
});
|
|
282
291
|
}
|
|
283
292
|
}
|
|
284
293
|
/**
|
|
@@ -316,8 +325,13 @@ async function handleMCPHealth(ctx) {
|
|
|
316
325
|
}
|
|
317
326
|
/**
|
|
318
327
|
* Create MCP server management routes
|
|
328
|
+
*
|
|
329
|
+
* @param options - `hasAuthentication` / `allowUnauthenticated`: whether the
|
|
330
|
+
* routes that change or use servers (reconnect, remove, execute a tool)
|
|
331
|
+
* answer an unauthenticated caller (see `ServerSensitiveRouteOptions`).
|
|
319
332
|
*/
|
|
320
|
-
export function createMCPRoutes(basePath = "/api") {
|
|
333
|
+
export function createMCPRoutes(basePath = "/api", options) {
|
|
334
|
+
const guarded = (handler) => async (ctx) => refuseUnauthenticatedExecution(ctx, options) ?? (await handler(ctx));
|
|
321
335
|
return {
|
|
322
336
|
prefix: `${basePath}/mcp`,
|
|
323
337
|
routes: [
|
|
@@ -338,14 +352,14 @@ export function createMCPRoutes(basePath = "/api") {
|
|
|
338
352
|
{
|
|
339
353
|
method: "POST",
|
|
340
354
|
path: `${basePath}/mcp/servers/:name/reconnect`,
|
|
341
|
-
handler: tracedMcpHandler("http.mcp.reconnectServer", `${basePath}/mcp/servers/:name/reconnect`, handleReconnectServer),
|
|
355
|
+
handler: tracedMcpHandler("http.mcp.reconnectServer", `${basePath}/mcp/servers/:name/reconnect`, guarded(handleReconnectServer)),
|
|
342
356
|
description: "Reconnect to an MCP server",
|
|
343
357
|
tags: ["mcp"],
|
|
344
358
|
},
|
|
345
359
|
{
|
|
346
360
|
method: "DELETE",
|
|
347
361
|
path: `${basePath}/mcp/servers/:name`,
|
|
348
|
-
handler: tracedMcpHandler("http.mcp.removeServer", `${basePath}/mcp/servers/:name`, handleRemoveServer),
|
|
362
|
+
handler: tracedMcpHandler("http.mcp.removeServer", `${basePath}/mcp/servers/:name`, guarded(handleRemoveServer)),
|
|
349
363
|
description: "Remove an MCP server",
|
|
350
364
|
tags: ["mcp"],
|
|
351
365
|
},
|
|
@@ -359,7 +373,7 @@ export function createMCPRoutes(basePath = "/api") {
|
|
|
359
373
|
{
|
|
360
374
|
method: "POST",
|
|
361
375
|
path: `${basePath}/mcp/servers/:name/tools/:toolName/execute`,
|
|
362
|
-
handler: tracedMcpHandler("http.mcp.executeTool", `${basePath}/mcp/servers/:name/tools/:toolName/execute`, handleExecuteTool),
|
|
376
|
+
handler: tracedMcpHandler("http.mcp.executeTool", `${basePath}/mcp/servers/:name/tools/:toolName/execute`, guarded(handleExecuteTool)),
|
|
363
377
|
description: "Execute a tool from a specific MCP server",
|
|
364
378
|
tags: ["mcp", "tools"],
|
|
365
379
|
},
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
6
6
|
import { tracers } from "../../telemetry/tracers.js";
|
|
7
|
+
import { shapeRouteError } from "../utils/clientError.js";
|
|
7
8
|
import { createErrorResponse, IdParamSchema, SessionIdParamSchema, validateParams, } from "../utils/validation.js";
|
|
8
9
|
/**
|
|
9
10
|
* Wrap a route handler with an OTel span for HTTP observability.
|
|
@@ -81,7 +82,7 @@ async function handleGetSessionMessages(ctx) {
|
|
|
81
82
|
};
|
|
82
83
|
}
|
|
83
84
|
catch (error) {
|
|
84
|
-
return createErrorResponse("MEMORY_ERROR",
|
|
85
|
+
return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
/**
|
|
@@ -132,7 +133,7 @@ async function handleGetSession(ctx) {
|
|
|
132
133
|
return createErrorResponse("MEMORY_ERROR", "Session retrieval not supported for this memory type", undefined, ctx.requestId);
|
|
133
134
|
}
|
|
134
135
|
catch (error) {
|
|
135
|
-
return createErrorResponse("MEMORY_ERROR",
|
|
136
|
+
return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
|
|
136
137
|
}
|
|
137
138
|
}
|
|
138
139
|
/**
|
|
@@ -202,7 +203,7 @@ async function handleListSessions(ctx) {
|
|
|
202
203
|
};
|
|
203
204
|
}
|
|
204
205
|
catch (error) {
|
|
205
|
-
return createErrorResponse("MEMORY_ERROR",
|
|
206
|
+
return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
/**
|
|
@@ -226,7 +227,7 @@ async function handleGetStats(ctx) {
|
|
|
226
227
|
};
|
|
227
228
|
}
|
|
228
229
|
catch (error) {
|
|
229
|
-
return createErrorResponse("MEMORY_ERROR",
|
|
230
|
+
return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
233
|
/**
|
|
@@ -260,7 +261,7 @@ async function handleClearSession(ctx) {
|
|
|
260
261
|
};
|
|
261
262
|
}
|
|
262
263
|
catch (error) {
|
|
263
|
-
return createErrorResponse("MEMORY_ERROR",
|
|
264
|
+
return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
|
|
264
265
|
}
|
|
265
266
|
}
|
|
266
267
|
/**
|
|
@@ -284,7 +285,7 @@ async function handleClearAllSessions(ctx) {
|
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
287
|
catch (error) {
|
|
287
|
-
return createErrorResponse("MEMORY_ERROR",
|
|
288
|
+
return createErrorResponse("MEMORY_ERROR", shapeRouteError("memoryRoutes", ctx.requestId, error).message, undefined, ctx.requestId);
|
|
288
289
|
}
|
|
289
290
|
}
|
|
290
291
|
/**
|
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
* Tool Routes
|
|
3
3
|
* Endpoints for tool listing, discovery, and execution
|
|
4
4
|
*/
|
|
5
|
-
import type { RouteGroup } from "../../types/index.js";
|
|
5
|
+
import type { ErrorResponse, RouteGroup, ServerContext, ServerSensitiveRouteOptions } from "../../types/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* The 403 for an execution route on a server that authenticates nobody, or
|
|
8
|
+
* `undefined` when the request may proceed: the server authenticates, the
|
|
9
|
+
* operator opted out, or the routes were built without a way to tell.
|
|
10
|
+
*/
|
|
11
|
+
export declare function refuseUnauthenticatedExecution(ctx: ServerContext, options: ServerSensitiveRouteOptions | undefined): ErrorResponse | undefined;
|
|
6
12
|
/**
|
|
7
13
|
* Create tool routes
|
|
14
|
+
*
|
|
15
|
+
* @param options - `hasAuthentication` / `allowUnauthenticated`: whether the
|
|
16
|
+
* execution routes answer an unauthenticated caller (see
|
|
17
|
+
* `ServerSensitiveRouteOptions`). `createAllRoutes` supplies them.
|
|
8
18
|
*/
|
|
9
|
-
export declare function createToolRoutes(basePath?: string): RouteGroup;
|
|
19
|
+
export declare function createToolRoutes(basePath?: string, options?: ServerSensitiveRouteOptions): RouteGroup;
|
|
@@ -5,11 +5,29 @@
|
|
|
5
5
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
6
6
|
import { withSpan } from "../../telemetry/withSpan.js";
|
|
7
7
|
import { tracers } from "../../telemetry/tracers.js";
|
|
8
|
+
import { shapeRouteError } from "../utils/clientError.js";
|
|
8
9
|
import { createErrorResponse, ToolArgumentsSchema, ToolExecuteRequestSchema, ToolNameParamSchema, validateParams, validateRequest, } from "../utils/validation.js";
|
|
10
|
+
/**
|
|
11
|
+
* The 403 for an execution route on a server that authenticates nobody, or
|
|
12
|
+
* `undefined` when the request may proceed: the server authenticates, the
|
|
13
|
+
* operator opted out, or the routes were built without a way to tell.
|
|
14
|
+
*/
|
|
15
|
+
export function refuseUnauthenticatedExecution(ctx, options) {
|
|
16
|
+
if (!options?.hasAuthentication ||
|
|
17
|
+
options.allowUnauthenticated === true ||
|
|
18
|
+
options.hasAuthentication()) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return createErrorResponse("FORBIDDEN", "This route executes tools on the server and is not served to unauthenticated callers. Register authentication middleware, or opt out with allowUnauthenticated / NEUROLINK_SERVER_ALLOW_UNAUTHENTICATED=1.", undefined, ctx.requestId, 403);
|
|
22
|
+
}
|
|
9
23
|
/**
|
|
10
24
|
* Create tool routes
|
|
25
|
+
*
|
|
26
|
+
* @param options - `hasAuthentication` / `allowUnauthenticated`: whether the
|
|
27
|
+
* execution routes answer an unauthenticated caller (see
|
|
28
|
+
* `ServerSensitiveRouteOptions`). `createAllRoutes` supplies them.
|
|
11
29
|
*/
|
|
12
|
-
export function createToolRoutes(basePath = "/api") {
|
|
30
|
+
export function createToolRoutes(basePath = "/api", options) {
|
|
13
31
|
return {
|
|
14
32
|
prefix: `${basePath}/tools`,
|
|
15
33
|
// IMPORTANT: Route ordering matters for proper matching!
|
|
@@ -89,6 +107,10 @@ export function createToolRoutes(basePath = "/api") {
|
|
|
89
107
|
method: "POST",
|
|
90
108
|
path: `${basePath}/tools/execute`,
|
|
91
109
|
handler: async (ctx) => {
|
|
110
|
+
const refused = refuseUnauthenticatedExecution(ctx, options);
|
|
111
|
+
if (refused) {
|
|
112
|
+
return refused;
|
|
113
|
+
}
|
|
92
114
|
// Validate request body
|
|
93
115
|
const validation = validateRequest(ToolExecuteRequestSchema, ctx.body, ctx.requestId);
|
|
94
116
|
if (!validation.success) {
|
|
@@ -141,9 +163,12 @@ export function createToolRoutes(basePath = "/api") {
|
|
|
141
163
|
message: errorMessage,
|
|
142
164
|
});
|
|
143
165
|
span.recordException(error instanceof Error ? error : new Error(errorMessage));
|
|
166
|
+
// The tool's own exception text (paths, upstream bodies) stays
|
|
167
|
+
// in the log under the request id.
|
|
144
168
|
return {
|
|
145
169
|
success: false,
|
|
146
|
-
error:
|
|
170
|
+
error: shapeRouteError("toolRoutes", ctx.requestId, error)
|
|
171
|
+
.message,
|
|
147
172
|
duration: Date.now() - startTime,
|
|
148
173
|
};
|
|
149
174
|
}
|
|
@@ -157,6 +182,10 @@ export function createToolRoutes(basePath = "/api") {
|
|
|
157
182
|
method: "POST",
|
|
158
183
|
path: `${basePath}/tools/:name/execute`,
|
|
159
184
|
handler: async (ctx) => {
|
|
185
|
+
const refused = refuseUnauthenticatedExecution(ctx, options);
|
|
186
|
+
if (refused) {
|
|
187
|
+
return refused;
|
|
188
|
+
}
|
|
160
189
|
// Validate params
|
|
161
190
|
const paramValidation = validateParams(ToolNameParamSchema, ctx.params, ctx.requestId);
|
|
162
191
|
if (!paramValidation.success) {
|
|
@@ -216,7 +245,8 @@ export function createToolRoutes(basePath = "/api") {
|
|
|
216
245
|
span.recordException(error instanceof Error ? error : new Error(errorMessage));
|
|
217
246
|
return {
|
|
218
247
|
success: false,
|
|
219
|
-
error:
|
|
248
|
+
error: shapeRouteError("toolRoutes", ctx.requestId, error)
|
|
249
|
+
.message,
|
|
220
250
|
duration: Date.now() - startTime,
|
|
221
251
|
};
|
|
222
252
|
}
|