@juspay/neurolink 13.0.1 → 13.0.2
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/dist/browser/neurolink.min.js +419 -419
- package/dist/server/openapi/schemas.js +2 -2
- package/dist/server/routes/agentRoutes.d.ts +9 -2
- package/dist/server/routes/agentRoutes.js +35 -9
- package/dist/server/routes/index.js +1 -1
- package/dist/server/utils/agentInput.d.ts +53 -0
- package/dist/server/utils/agentInput.js +304 -0
- package/dist/types/server.d.ts +45 -0
- package/docs-site/static/search-index.json +2 -1
- package/package.json +2 -1
|
@@ -117,12 +117,12 @@ export const AgentInputSchema = {
|
|
|
117
117
|
images: {
|
|
118
118
|
type: "array",
|
|
119
119
|
items: { type: "string" },
|
|
120
|
-
description: "Array of
|
|
120
|
+
description: "Array of images, each a data: URI, an https URL the server downloads (public hosts only, no redirects) or a path under a directory the server allows (allowedInputRoots; with none set every local path is refused with 403). Bare base64 is not accepted, and http:// or any other scheme is refused with 400. A request is limited in attachment count and total bytes (413).",
|
|
121
121
|
},
|
|
122
122
|
files: {
|
|
123
123
|
type: "array",
|
|
124
124
|
items: { type: "string" },
|
|
125
|
-
description: "Array of
|
|
125
|
+
description: "Array of files, each a data: URI, an https URL the server downloads (public hosts only, no redirects) or a path under a directory the server allows (allowedInputRoots; with none set every local path is refused with 403). http:// or any other scheme is refused with 400. A request is limited in attachment count and total bytes (413), images and files together.",
|
|
126
126
|
},
|
|
127
127
|
},
|
|
128
128
|
},
|
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
* Agent Routes
|
|
3
3
|
* Endpoints for agent execution and streaming
|
|
4
4
|
*/
|
|
5
|
-
import type { RouteGroup } from "../../types/index.js";
|
|
5
|
+
import type { RouteGroup, ServerAgentRouteOptions } from "../../types/index.js";
|
|
6
6
|
/**
|
|
7
7
|
* Create agent routes
|
|
8
|
+
*
|
|
9
|
+
* @param options - Limits for `input.images` / `input.files`:
|
|
10
|
+
* `allowedInputRoots` (default: none, every local-path attachment is
|
|
11
|
+
* refused), `maxInputBytes` per attachment (default 50 MiB),
|
|
12
|
+
* `maxInputAttachments` per request (default 20, images and files together)
|
|
13
|
+
* and `maxInputTotalBytes` per request (default 100 MiB, one budget shared by
|
|
14
|
+
* images and files).
|
|
8
15
|
*/
|
|
9
|
-
export declare function createAgentRoutes(basePath?: string): RouteGroup;
|
|
16
|
+
export declare function createAgentRoutes(basePath?: string, options?: ServerAgentRouteOptions): RouteGroup;
|
|
@@ -27,6 +27,7 @@ async function resolveRequestToolFilter(ctx, requestedTools) {
|
|
|
27
27
|
logger.warn("[agentRoutes] Ignoring request.tools — no entries match registered tool names (legacy fail-open)", { requested: requestedTools });
|
|
28
28
|
return undefined;
|
|
29
29
|
}
|
|
30
|
+
import { AgentInputError, resolveAgentInput } from "../utils/agentInput.js";
|
|
30
31
|
import { createStreamRedactor } from "../utils/redaction.js";
|
|
31
32
|
import { AgentExecuteRequestSchema, createErrorResponse as createError, EmbedManyRequestSchema, EmbedRequestSchema, SkillCreateRequestSchema, SkillUpdateRequestSchema, validateRequest, } from "../utils/validation.js";
|
|
32
33
|
/**
|
|
@@ -56,10 +57,35 @@ function resolveMutableSkillsManager(ctx) {
|
|
|
56
57
|
}
|
|
57
58
|
return manager;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The request's `input` with its attachments vetted (see `agentInput.ts`), or
|
|
62
|
+
* the error response for the first one the server refuses.
|
|
63
|
+
*/
|
|
64
|
+
async function resolveRequestInput(ctx, requestInput, options) {
|
|
65
|
+
if (typeof requestInput === "string") {
|
|
66
|
+
return { text: requestInput };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return await resolveAgentInput(requestInput, options);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof AgentInputError) {
|
|
73
|
+
return createError(error.code, error.message, undefined, ctx.requestId, error.status);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
59
78
|
/**
|
|
60
79
|
* Create agent routes
|
|
80
|
+
*
|
|
81
|
+
* @param options - Limits for `input.images` / `input.files`:
|
|
82
|
+
* `allowedInputRoots` (default: none, every local-path attachment is
|
|
83
|
+
* refused), `maxInputBytes` per attachment (default 50 MiB),
|
|
84
|
+
* `maxInputAttachments` per request (default 20, images and files together)
|
|
85
|
+
* and `maxInputTotalBytes` per request (default 100 MiB, one budget shared by
|
|
86
|
+
* images and files).
|
|
61
87
|
*/
|
|
62
|
-
export function createAgentRoutes(basePath = "/api") {
|
|
88
|
+
export function createAgentRoutes(basePath = "/api", options) {
|
|
63
89
|
return {
|
|
64
90
|
prefix: `${basePath}/agent`,
|
|
65
91
|
routes: [
|
|
@@ -82,10 +108,10 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
82
108
|
"ai.model": request.model || "default",
|
|
83
109
|
},
|
|
84
110
|
}, async () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
const input = await resolveRequestInput(ctx, request.input, options);
|
|
112
|
+
if ("error" in input) {
|
|
113
|
+
return input;
|
|
114
|
+
}
|
|
89
115
|
const requestToolFilter = await resolveRequestToolFilter(ctx, request.tools);
|
|
90
116
|
const result = await ctx.neurolink.generate({
|
|
91
117
|
input,
|
|
@@ -135,10 +161,10 @@ export function createAgentRoutes(basePath = "/api") {
|
|
|
135
161
|
return validation.error;
|
|
136
162
|
}
|
|
137
163
|
const request = validation.data;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
const input = await resolveRequestInput(ctx, request.input, options);
|
|
165
|
+
if ("error" in input) {
|
|
166
|
+
return input;
|
|
167
|
+
}
|
|
142
168
|
const streamToolFilter = await resolveRequestToolFilter(ctx, request.tools);
|
|
143
169
|
const result = await ctx.neurolink.stream({
|
|
144
170
|
input,
|
|
@@ -41,7 +41,7 @@ export { createTranscribeRoutes } from "./transcribeRoutes.js";
|
|
|
41
41
|
*/
|
|
42
42
|
export function createAllRoutes(basePath = "/api", options) {
|
|
43
43
|
const routes = [
|
|
44
|
-
createAgentRoutes(basePath),
|
|
44
|
+
createAgentRoutes(basePath, options?.agent),
|
|
45
45
|
createToolRoutes(basePath),
|
|
46
46
|
createMCPRoutes(basePath),
|
|
47
47
|
createMemoryRoutes(basePath),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent request attachments, vetted at the HTTP boundary.
|
|
3
|
+
*
|
|
4
|
+
* `input.images` and `input.files` reach the SDK as strings, and the SDK
|
|
5
|
+
* resolves a string itself: an `http(s)` URL is fetched with redirects followed
|
|
6
|
+
* and no address check, and anything else is read from the local disk. Both
|
|
7
|
+
* are right for a program that names its own files. Neither is right when the
|
|
8
|
+
* string came from a caller of a network server, who could name
|
|
9
|
+
* `https://169.254.169.254/...`, a redirect to it, or `/etc/passwd`.
|
|
10
|
+
*
|
|
11
|
+
* So the route resolves every attachment itself and hands the SDK bytes:
|
|
12
|
+
* - `data:` URIs pass through, they carry their own content;
|
|
13
|
+
* - `https` URLs are downloaded with `safeDownload`: redirects are refused and
|
|
14
|
+
* the size is capped. The address is checked and the connection pinned to
|
|
15
|
+
* the checked addresses only for a direct download. Behind `HTTPS_PROXY` /
|
|
16
|
+
* `ALL_PROXY` the proxy resolves the host, so only the URL text is checked
|
|
17
|
+
* (IP literals, single-label and internal host names) and what a
|
|
18
|
+
* public-looking name resolves to is the proxy's egress policy;
|
|
19
|
+
* - local paths are read only from the operator's `allowedInputRoots`, after
|
|
20
|
+
* symlinks are resolved;
|
|
21
|
+
* - everything else is refused rather than guessed at, so this classifier
|
|
22
|
+
* can never disagree with the SDK's about what a string means.
|
|
23
|
+
*
|
|
24
|
+
* One request is also bounded as a whole: at most `maxInputAttachments`
|
|
25
|
+
* entries, each at most `maxInputBytes`, all together at most
|
|
26
|
+
* `maxInputTotalBytes`. The count and every `data:` URI are judged before
|
|
27
|
+
* anything is read; the rest are resolved one at a time, each allowed only what
|
|
28
|
+
* is left of the shared budget, so a transfer that would overflow it stops
|
|
29
|
+
* mid-stream.
|
|
30
|
+
*
|
|
31
|
+
* The SDK itself is untouched: a program calling `generate()` directly keeps
|
|
32
|
+
* reading the paths and URLs it names.
|
|
33
|
+
*
|
|
34
|
+
* @module server/utils/agentInput
|
|
35
|
+
*/
|
|
36
|
+
import type { ServerAgentResolvedInput, ServerAgentRouteOptions } from "../../types/index.js";
|
|
37
|
+
/** An attachment the server refuses, with the HTTP status that says why. */
|
|
38
|
+
export declare class AgentInputError extends Error {
|
|
39
|
+
readonly status: number;
|
|
40
|
+
readonly code: string;
|
|
41
|
+
constructor(message: string, status: number, code: string);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Replace every attachment of an agent request's `input` with vetted content.
|
|
45
|
+
*
|
|
46
|
+
* @throws {AgentInputError} for the first attachment that is refused, or for a
|
|
47
|
+
* request that carries too many attachments or too many bytes.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveAgentInput(input: {
|
|
50
|
+
text: string;
|
|
51
|
+
images?: string[];
|
|
52
|
+
files?: string[];
|
|
53
|
+
}, options?: ServerAgentRouteOptions): Promise<ServerAgentResolvedInput>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent request attachments, vetted at the HTTP boundary.
|
|
3
|
+
*
|
|
4
|
+
* `input.images` and `input.files` reach the SDK as strings, and the SDK
|
|
5
|
+
* resolves a string itself: an `http(s)` URL is fetched with redirects followed
|
|
6
|
+
* and no address check, and anything else is read from the local disk. Both
|
|
7
|
+
* are right for a program that names its own files. Neither is right when the
|
|
8
|
+
* string came from a caller of a network server, who could name
|
|
9
|
+
* `https://169.254.169.254/...`, a redirect to it, or `/etc/passwd`.
|
|
10
|
+
*
|
|
11
|
+
* So the route resolves every attachment itself and hands the SDK bytes:
|
|
12
|
+
* - `data:` URIs pass through, they carry their own content;
|
|
13
|
+
* - `https` URLs are downloaded with `safeDownload`: redirects are refused and
|
|
14
|
+
* the size is capped. The address is checked and the connection pinned to
|
|
15
|
+
* the checked addresses only for a direct download. Behind `HTTPS_PROXY` /
|
|
16
|
+
* `ALL_PROXY` the proxy resolves the host, so only the URL text is checked
|
|
17
|
+
* (IP literals, single-label and internal host names) and what a
|
|
18
|
+
* public-looking name resolves to is the proxy's egress policy;
|
|
19
|
+
* - local paths are read only from the operator's `allowedInputRoots`, after
|
|
20
|
+
* symlinks are resolved;
|
|
21
|
+
* - everything else is refused rather than guessed at, so this classifier
|
|
22
|
+
* can never disagree with the SDK's about what a string means.
|
|
23
|
+
*
|
|
24
|
+
* One request is also bounded as a whole: at most `maxInputAttachments`
|
|
25
|
+
* entries, each at most `maxInputBytes`, all together at most
|
|
26
|
+
* `maxInputTotalBytes`. The count and every `data:` URI are judged before
|
|
27
|
+
* anything is read; the rest are resolved one at a time, each allowed only what
|
|
28
|
+
* is left of the shared budget, so a transfer that would overflow it stops
|
|
29
|
+
* mid-stream.
|
|
30
|
+
*
|
|
31
|
+
* The SDK itself is untouched: a program calling `generate()` directly keeps
|
|
32
|
+
* reading the paths and URLs it names.
|
|
33
|
+
*
|
|
34
|
+
* @module server/utils/agentInput
|
|
35
|
+
*/
|
|
36
|
+
import fs from "node:fs";
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
import { logger } from "../../utils/logger.js";
|
|
39
|
+
const DEFAULT_MAX_INPUT_BYTES = 50 * 1024 * 1024;
|
|
40
|
+
const DEFAULT_MAX_INPUT_TOTAL_BYTES = 100 * 1024 * 1024;
|
|
41
|
+
const DEFAULT_MAX_INPUT_ATTACHMENTS = 20;
|
|
42
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
43
|
+
/** Two or more scheme characters, so a Windows drive letter (`C:\`) is not one. */
|
|
44
|
+
const URL_SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]+:/;
|
|
45
|
+
/** An attachment the server refuses, with the HTTP status that says why. */
|
|
46
|
+
export class AgentInputError extends Error {
|
|
47
|
+
status;
|
|
48
|
+
code;
|
|
49
|
+
constructor(message, status, code) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.status = status;
|
|
52
|
+
this.code = code;
|
|
53
|
+
this.name = "AgentInputError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const isUnder = (child, root) => child === root ||
|
|
57
|
+
child.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
|
|
58
|
+
const forbidden = () => new AgentInputError("attachment path is outside the directories this server allows (or local paths are disabled)", 403, "FORBIDDEN");
|
|
59
|
+
const notFound = () => new AgentInputError("attachment path does not exist", 404, "NOT_FOUND");
|
|
60
|
+
const notReadable = () => new AgentInputError("attachment path is not readable", 403, "FORBIDDEN");
|
|
61
|
+
const notAFile = () => new AgentInputError("attachment path is not a file", 400, "VALIDATION_ERROR");
|
|
62
|
+
const tooLarge = (limit) => new AgentInputError(`attachment exceeds the ${limit}-byte limit`, 413, "PAYLOAD_TOO_LARGE");
|
|
63
|
+
const budgetExceeded = (total) => new AgentInputError(`attachments together exceed the ${total}-byte limit`, 413, "PAYLOAD_TOO_LARGE");
|
|
64
|
+
const tooMany = (max) => new AgentInputError(`a request may carry at most ${max} attachments`, 413, "PAYLOAD_TOO_LARGE");
|
|
65
|
+
/** A configured limit, or the default when it is missing or not a number >= 0. */
|
|
66
|
+
const limitOr = (value, fallback) => typeof value === "number" && value >= 0 ? value : fallback;
|
|
67
|
+
const isDataUri = (entry) => entry.startsWith("data:");
|
|
68
|
+
const fsErrorCode = (error) => typeof error === "object" &&
|
|
69
|
+
error !== null &&
|
|
70
|
+
"code" in error &&
|
|
71
|
+
typeof error.code === "string"
|
|
72
|
+
? error.code
|
|
73
|
+
: undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Turn whatever the file system threw into the refusal the caller gets. Nothing
|
|
76
|
+
* raw gets through: a Node error message carries the server's path, and an
|
|
77
|
+
* unmapped code is logged for the operator (the code only) and answered like an
|
|
78
|
+
* unreadable file instead of surfacing as a 500.
|
|
79
|
+
*/
|
|
80
|
+
function asAgentInputError(error) {
|
|
81
|
+
if (error instanceof AgentInputError) {
|
|
82
|
+
return error;
|
|
83
|
+
}
|
|
84
|
+
const code = fsErrorCode(error);
|
|
85
|
+
switch (code) {
|
|
86
|
+
case "ENOENT":
|
|
87
|
+
case "ENOTDIR":
|
|
88
|
+
return notFound();
|
|
89
|
+
case "EACCES":
|
|
90
|
+
case "EPERM":
|
|
91
|
+
case "ELOOP":
|
|
92
|
+
return notReadable();
|
|
93
|
+
case "ENXIO":
|
|
94
|
+
case "EOPNOTSUPP":
|
|
95
|
+
case "ENOTSUP":
|
|
96
|
+
case "EISDIR":
|
|
97
|
+
return notAFile();
|
|
98
|
+
case "ENAMETOOLONG":
|
|
99
|
+
case "EINVAL":
|
|
100
|
+
case "ERR_INVALID_ARG_VALUE":
|
|
101
|
+
return new AgentInputError("attachment path is not a usable path", 400, "VALIDATION_ERROR");
|
|
102
|
+
default:
|
|
103
|
+
logger.warn("[agentInput] unmapped file-system error on an attachment", {
|
|
104
|
+
code,
|
|
105
|
+
name: error instanceof Error ? error.name : typeof error,
|
|
106
|
+
});
|
|
107
|
+
return notReadable();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A file under one of the allowed roots, after symlinks are resolved.
|
|
112
|
+
*
|
|
113
|
+
* A path outside every root is refused before the file system is touched, so
|
|
114
|
+
* the answer for `/etc/passwd` and for `/etc/nothing-here` is the same 403 and
|
|
115
|
+
* the route is not an oracle for which files exist.
|
|
116
|
+
*/
|
|
117
|
+
async function readAllowedPath(requested, roots, limit) {
|
|
118
|
+
if (!roots || roots.length === 0) {
|
|
119
|
+
throw forbidden();
|
|
120
|
+
}
|
|
121
|
+
const lexical = path.resolve(requested);
|
|
122
|
+
const realRoots = (await Promise.all(roots.map(async (root) => {
|
|
123
|
+
try {
|
|
124
|
+
return [path.resolve(root), await fs.promises.realpath(root)];
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
}))).flat();
|
|
130
|
+
if (!realRoots.some((root) => isUnder(lexical, root))) {
|
|
131
|
+
throw forbidden();
|
|
132
|
+
}
|
|
133
|
+
let real;
|
|
134
|
+
try {
|
|
135
|
+
real = await fs.promises.realpath(lexical);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
throw asAgentInputError(error);
|
|
139
|
+
}
|
|
140
|
+
if (!realRoots.some((root) => isUnder(real, root))) {
|
|
141
|
+
throw forbidden();
|
|
142
|
+
}
|
|
143
|
+
// Open once and judge the descriptor, not the path: a path can be swapped for
|
|
144
|
+
// a symlink between the checks above and a later read. O_NOFOLLOW refuses a
|
|
145
|
+
// symlink in the last component; a directory swapped higher up is not caught,
|
|
146
|
+
// which matters only if something untrusted can write under an allowed root.
|
|
147
|
+
let handle;
|
|
148
|
+
try {
|
|
149
|
+
// O_NONBLOCK: opening a FIFO for reading otherwise waits for a writer, and
|
|
150
|
+
// each such request would hold a libuv worker until one appears. With it
|
|
151
|
+
// the open returns at once and the stat below refuses the FIFO as not a
|
|
152
|
+
// file. Regular files read the same either way. Windows has no constant.
|
|
153
|
+
handle = await fs.promises.open(real, fs.constants.O_RDONLY |
|
|
154
|
+
fs.constants.O_NOFOLLOW |
|
|
155
|
+
(fs.constants.O_NONBLOCK ?? 0));
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
throw asAgentInputError(error);
|
|
159
|
+
}
|
|
160
|
+
let content;
|
|
161
|
+
try {
|
|
162
|
+
const stat = await handle.stat();
|
|
163
|
+
if (!stat.isFile()) {
|
|
164
|
+
throw notAFile();
|
|
165
|
+
}
|
|
166
|
+
if (stat.size > limit) {
|
|
167
|
+
throw tooLarge(limit);
|
|
168
|
+
}
|
|
169
|
+
content = await readWithinLimit(handle, limit);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
await handle.close().catch(() => undefined);
|
|
173
|
+
throw asAgentInputError(error);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
await handle.close();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
throw asAgentInputError(error);
|
|
180
|
+
}
|
|
181
|
+
return content;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Read the whole file, refusing one that outgrew `limit` since it was measured:
|
|
185
|
+
* reads one byte past the limit, so growth is seen rather than buffered.
|
|
186
|
+
*/
|
|
187
|
+
async function readWithinLimit(handle, limit) {
|
|
188
|
+
const chunks = [];
|
|
189
|
+
let total = 0;
|
|
190
|
+
while (total <= limit) {
|
|
191
|
+
const chunk = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, limit + 1 - total));
|
|
192
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, total);
|
|
193
|
+
if (bytesRead === 0) {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
197
|
+
total += bytesRead;
|
|
198
|
+
}
|
|
199
|
+
if (total > limit) {
|
|
200
|
+
throw tooLarge(limit);
|
|
201
|
+
}
|
|
202
|
+
return Buffer.concat(chunks, total);
|
|
203
|
+
}
|
|
204
|
+
async function downloadAttachment(url, limit) {
|
|
205
|
+
// Dynamic import keeps undici off the static import graph; see
|
|
206
|
+
// test/continuous-test-suite-import-cost.ts.
|
|
207
|
+
const { safeDownload } = await import("../../utils/safeFetch.js");
|
|
208
|
+
try {
|
|
209
|
+
return await safeDownload(url, {
|
|
210
|
+
maxBytes: limit,
|
|
211
|
+
label: "agent attachment",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
// The reason can name the address a host resolved to; the caller gets none
|
|
216
|
+
// of it, the operator gets it in the log.
|
|
217
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
218
|
+
logger.warn("[agentInput] refused an attachment URL", {
|
|
219
|
+
reason: reason.replaceAll(url, "<url>"),
|
|
220
|
+
});
|
|
221
|
+
throw new AgentInputError("attachment URL could not be fetched (it must be a public https URL, answer without a redirect and fit the size limit)", 400, "VALIDATION_ERROR");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** The bytes of one attachment that is not a `data:` URI, read within `limit`. */
|
|
225
|
+
async function resolveAttachment(entry, roots, limit) {
|
|
226
|
+
if (/^https?:\/\//i.test(entry)) {
|
|
227
|
+
return downloadAttachment(entry, limit);
|
|
228
|
+
}
|
|
229
|
+
if (URL_SCHEME.test(entry)) {
|
|
230
|
+
throw new AgentInputError("attachment must be a data: URI, an https URL, or a path under an allowed directory", 400, "VALIDATION_ERROR");
|
|
231
|
+
}
|
|
232
|
+
return readAllowedPath(entry, roots, limit);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Replace every attachment of an agent request's `input` with vetted content.
|
|
236
|
+
*
|
|
237
|
+
* @throws {AgentInputError} for the first attachment that is refused, or for a
|
|
238
|
+
* request that carries too many attachments or too many bytes.
|
|
239
|
+
*/
|
|
240
|
+
export async function resolveAgentInput(input, options) {
|
|
241
|
+
const maxAttachments = limitOr(options?.maxInputAttachments, DEFAULT_MAX_INPUT_ATTACHMENTS);
|
|
242
|
+
const totalBytes = limitOr(options?.maxInputTotalBytes, DEFAULT_MAX_INPUT_TOTAL_BYTES);
|
|
243
|
+
const perAttachment = Math.min(limitOr(options?.maxInputBytes, DEFAULT_MAX_INPUT_BYTES), totalBytes);
|
|
244
|
+
const roots = options?.allowedInputRoots;
|
|
245
|
+
const entries = [...(input.images ?? []), ...(input.files ?? [])];
|
|
246
|
+
// Everything that needs no I/O is judged first, so a request that is already
|
|
247
|
+
// too big costs the server nothing: the count, then each data: URI against
|
|
248
|
+
// its own cap and against the shared budget.
|
|
249
|
+
if (entries.length > maxAttachments) {
|
|
250
|
+
throw tooMany(maxAttachments);
|
|
251
|
+
}
|
|
252
|
+
let remaining = totalBytes;
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
if (!isDataUri(entry)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (entry.length > perAttachment) {
|
|
258
|
+
throw tooLarge(perAttachment);
|
|
259
|
+
}
|
|
260
|
+
remaining -= entry.length;
|
|
261
|
+
if (remaining < 0) {
|
|
262
|
+
throw budgetExceeded(totalBytes);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// One at a time: a request naming many URLs must not open them all at once,
|
|
266
|
+
// and each entry may use only what the ones before it left of the budget.
|
|
267
|
+
const resolveList = async (list) => {
|
|
268
|
+
if (list === undefined) {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
const resolved = [];
|
|
272
|
+
for (const entry of list) {
|
|
273
|
+
// Exactly the prefix the SDK keys on, so nothing that reaches it as a
|
|
274
|
+
// string is anything but a data URI.
|
|
275
|
+
if (isDataUri(entry)) {
|
|
276
|
+
resolved.push(entry);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const limit = Math.min(perAttachment, remaining);
|
|
280
|
+
let bytes;
|
|
281
|
+
try {
|
|
282
|
+
bytes = await resolveAttachment(entry, roots, limit);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (limit < perAttachment &&
|
|
286
|
+
error instanceof AgentInputError &&
|
|
287
|
+
error.status === 413) {
|
|
288
|
+
throw budgetExceeded(totalBytes);
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
remaining -= bytes.length;
|
|
293
|
+
resolved.push(bytes);
|
|
294
|
+
}
|
|
295
|
+
return resolved;
|
|
296
|
+
};
|
|
297
|
+
const images = await resolveList(input.images);
|
|
298
|
+
const files = await resolveList(input.files);
|
|
299
|
+
return {
|
|
300
|
+
text: input.text,
|
|
301
|
+
...(images ? { images } : {}),
|
|
302
|
+
...(files ? { files } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
package/dist/types/server.d.ts
CHANGED
|
@@ -434,6 +434,45 @@ export type AgentExecuteRequest = {
|
|
|
434
434
|
/** User ID for context */
|
|
435
435
|
userId?: string;
|
|
436
436
|
};
|
|
437
|
+
/** Options for `createAgentRoutes()`. */
|
|
438
|
+
export type ServerAgentRouteOptions = {
|
|
439
|
+
/**
|
|
440
|
+
* Directories a request's `input.images` / `input.files` entries may name as
|
|
441
|
+
* local paths. Empty or omitted (the default) refuses every local path: a
|
|
442
|
+
* server must not read its own disk on a caller's say-so unless the operator
|
|
443
|
+
* names where.
|
|
444
|
+
*/
|
|
445
|
+
allowedInputRoots?: string[];
|
|
446
|
+
/**
|
|
447
|
+
* Largest single attachment accepted, in bytes. Default 50 MiB. A `data:` URI
|
|
448
|
+
* is measured by its string length. When `maxInputTotalBytes` is set lower,
|
|
449
|
+
* the smaller of the two is the per-attachment cap.
|
|
450
|
+
*/
|
|
451
|
+
maxInputBytes?: number;
|
|
452
|
+
/**
|
|
453
|
+
* Most attachments one request may carry, `input.images` and `input.files`
|
|
454
|
+
* counted together. Default 20. A longer list is refused with 413 before any
|
|
455
|
+
* attachment is read or downloaded.
|
|
456
|
+
*/
|
|
457
|
+
maxInputAttachments?: number;
|
|
458
|
+
/**
|
|
459
|
+
* Largest sum of all attachments of one request, in bytes: images and files
|
|
460
|
+
* share this one budget, and a `data:` URI is charged its string length.
|
|
461
|
+
* Default 100 MiB. A request that overflows it is refused with 413, and a
|
|
462
|
+
* file or download that would overflow it stops mid-transfer.
|
|
463
|
+
*/
|
|
464
|
+
maxInputTotalBytes?: number;
|
|
465
|
+
};
|
|
466
|
+
/**
|
|
467
|
+
* An agent request's `input` once every attachment has been vetted: URLs and
|
|
468
|
+
* allowed local paths are replaced by the bytes the server read itself, `data:`
|
|
469
|
+
* URIs pass through untouched.
|
|
470
|
+
*/
|
|
471
|
+
export type ServerAgentResolvedInput = {
|
|
472
|
+
text: string;
|
|
473
|
+
images?: Array<Buffer | string>;
|
|
474
|
+
files?: Array<Buffer | string>;
|
|
475
|
+
};
|
|
437
476
|
/**
|
|
438
477
|
* Agent execution response
|
|
439
478
|
*/
|
|
@@ -1110,6 +1149,12 @@ export type CreateRoutesOptions = {
|
|
|
1110
1149
|
* always mounted.
|
|
1111
1150
|
*/
|
|
1112
1151
|
transcribe?: ServerTranscribeRouteOptions;
|
|
1152
|
+
/**
|
|
1153
|
+
* Options for the agent routes (`POST <basePath>/agent/execute` and
|
|
1154
|
+
* `/agent/stream`): where local-path attachments may be read from, and how
|
|
1155
|
+
* large an attachment may be.
|
|
1156
|
+
*/
|
|
1157
|
+
agent?: ServerAgentRouteOptions;
|
|
1113
1158
|
};
|
|
1114
1159
|
/** Options for `createTranscribeRoutes()`. */
|
|
1115
1160
|
export type ServerTranscribeRouteOptions = {
|
|
@@ -8268,7 +8268,8 @@
|
|
|
8268
8268
|
{"objectID":"19b651b464a6f5ce48fd3705b461d846781afb42f1bd63f1b862d0af5a7b8ed4","title":"Route Groups","url":"/docs/guides/server-adapters/api-reference#route-groups","content":"Route group factories return RouteGroup objects. Register them with adapter.registerRouteGroup(group).","hierarchy":{"lvl0":"Guides","lvl1":"Server Adapters API Reference","lvl2":"Route Groups","lvl3":""}},
|
|
8269
8269
|
{"objectID":"69d3af0a2481958a11346eed327bc59d63ae750a56f75683e1d6661de27e4e73","title":"createAllRoutes(basePath?, options?)","url":"/docs/guides/server-adapters/api-reference#createallroutesbasepath-options","content":"Creates all standard route groups in one call.","hierarchy":{"lvl0":"Guides","lvl1":"Server Adapters API Reference","lvl2":"createAllRoutes(basePath?, options?)","lvl3":""}},
|
|
8270
8270
|
{"objectID":"8af1d4e6d75ca68b8c9248b8411634663fc0eee1d650235a1e5e06122fe6c439","title":"registerAllRoutes(adapter, basePath?, options?)","url":"/docs/guides/server-adapters/api-reference#registerallroutesadapter-basepath-options","content":"Registers all route groups with an adapter. If the adapter has listRoutes(), auto-binds it for OpenAPI spec generation.","hierarchy":{"lvl0":"Guides","lvl1":"Server Adapters API Reference","lvl2":"registerAllRoutes(adapter, basePath?, options?)","lvl3":""}},
|
|
8271
|
-
{"objectID":"2a0e62a8cfac86c4b2081bf7c659fb39367ec848852b3700075feaddd6e08979","title":"Individual Route Factories","url":"/docs/guides/server-adapters/api-reference#individual-route-factories","content":"| Factory | Prefix | Endpoints |\n| -------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| createAgentRoutes(basePath?)
|
|
8271
|
+
{"objectID":"2a0e62a8cfac86c4b2081bf7c659fb39367ec848852b3700075feaddd6e08979","title":"Individual Route Factories","url":"/docs/guides/server-adapters/api-reference#individual-route-factories","content":"| Factory | Prefix | Endpoints |\n| -------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| createAgentRoutes(basePath?, options?) | /agent | POST /agent/execute -- Execute agentPOST /agent/stream -- Stream agent response (SSE)GET /agent/providers -- List available providersPOST /agent/embed -- Generate single embeddingPOST /agent/embed-many -- Generate batch embeddings |\n| createToolRoutes(basePath?) | /tools | GET /tools -- List all toolsGET /tools/search -- Search tools by queryGET /tools/:name -- Get tool detailsPOST /tools/:name/execute -- Execute toolPOST /tools/execute -- Execute tool (body-based) |\n| createMCPRoutes(basePath?) | /mcp | GET /mcp/servers -- List MCP serversGET /mcp/servers/:name -- Get server statusGET /mcp/servers/:name/tools -- List server toolsPOST /mcp/servers/:name/tools/:toolName/execute -- Execute server tool |\n| createMemoryRoutes(basePath?) | /memory | GET /memory/sessions -- List sessionsGET /memory/sessions/:id -- Get session detailsGET /memory/sessions/:id/messages -- Get session messagesDELETE /memory/sessions/:id -- Delete sessionPOST /memory/sessions/:sessionId/clear -- Clear session history |\n| createHealthRoutes(basePath?)","hierarchy":{"lvl0":"Guides","lvl1":"Server Adapters API Reference","lvl2":"Individual Route Factories","lvl3":""}},
|
|
8272
|
+
{"objectID":"69bea07b5c4ac414ed9a20d0f483fd96a7b592072d73e2fa263997e5c1aa34eb","title":"Attachments on /agent/execute and /agent/stream","url":"/docs/guides/server-adapters/api-reference#attachments-on-agentexecute-and-agentstream","content":"input.images and input.files are strings that name content. The SDK resolves\nthem itself, which is right for a program naming its own files and wrong for a\nnetwork server, so the agent routes check each one before the SDK sees it. An\nhttps:// URL or an allowed local path is read by the route and handed to the\nSDK as bytes; a data: URI already carries its content and passes through\nunchanged; anything else is refused:\n\n| Entry | Result |\n| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| data: URI | Passed through unchanged. Its length counts against the size limits below, and over a limit it is 413.","hierarchy":{"lvl0":"Guides","lvl1":"Server Adapters API Reference","lvl2":"Attachments on /agent/execute and /agent/stream","lvl3":""}},
|
|
8272
8273
|
{"objectID":"d89461a69441807c337b069b75bd37d86c2d3e0565e57b529eadb04a07da8784","title":"Proxy Route Factories","url":"/docs/guides/server-adapters/api-reference#proxy-route-factories","content":"These are only included by createAllRoutes when a proxy flag is set, and they\ntake their own dependencies rather than just a basePath.\n\n| Factory | Included when | Endpoints |\n| --------------------------------------------------------------------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| createClaudeProxyRoutes(modelRouter?, basePath?, strategy?, passthrough?, primaryAccountKey?, options?) | proxy or claudeProxy | POST /v1/messages — Anthropic Messages passthrough with account poolingGET /limits — per-account quota (?snapshot=true for stored state)GET /accounts — one row per account joining status, quota and per-account token totals and cost |\n| createOpenAIProxyRoutes(modelRouter?, basePath?, loopbackPort?, runtimeConfigProvider?) | proxy or openaiProxy | POST /v1/chat/completions — OpenAI-compatible surface; Anthropic-targeted models loop back through /v1/messages |\n| createCodexProxyRoutes(basePath?)","hierarchy":{"lvl0":"Guides","lvl1":"Server Adapters API Reference","lvl2":"Proxy Route Factories","lvl3":""}},
|
|
8273
8274
|
{"objectID":"7e72ba983f827d21de4123a24256ed23c608281e7c46bfc918a5196071715853","title":"OpenAPIGenerator","url":"/docs/guides/server-adapters/api-reference#openapigenerator","content":"Class that generates OpenAPI 3.1 specifications from route definitions.","hierarchy":{"lvl0":"Guides","lvl1":"Server Adapters API Reference","lvl2":"OpenAPIGenerator","lvl3":""}},
|
|
8274
8275
|
{"objectID":"ce33eceb23cd2d0848029ad45cb1cd7c715fd120ff081bd88e7be72fe0f5a4b2","title":"Factory Functions","url":"/docs/guides/server-adapters/api-reference#factory-functions","content":"| Function | Signature | Description |\n| --------------------------- | ---------------------------------------- | ----------------------------------- |\n| createOpenAPIGenerator | (config?) => OpenAPIGenerator | Create generator with defaults |\n| generateOpenAPISpec | (routes, config?) => OpenAPISpec | One-shot spec from routes |\n| generateOpenAPIFromConfig | (serverConfig, routes?) => OpenAPISpec | Generate from ServerAdapterConfig |","hierarchy":{"lvl0":"Guides","lvl1":"Server Adapters API Reference","lvl2":"Factory Functions","lvl3":""}},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.2",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -181,6 +181,7 @@
|
|
|
181
181
|
"test:agent-mode": "pnpm exec tsx test/continuous-test-suite-agent-mode.ts",
|
|
182
182
|
"test:servers": "pnpm exec tsx test/continuous-test-suite-servers.ts",
|
|
183
183
|
"test:server-boundaries": "pnpm exec tsx test/continuous-test-suite-server-boundaries.ts",
|
|
184
|
+
"test:agent-input-guard": "pnpm exec tsx test/continuous-test-suite-agent-input-guard.ts",
|
|
184
185
|
"test:websocket-agent-handler": "pnpm exec tsx test/continuous-test-suite-websocket-agent-handler.ts",
|
|
185
186
|
"test:tool-reliability": "pnpm exec tsx test/continuous-test-suite-tool-reliability.ts",
|
|
186
187
|
"test:video-abort": "pnpm exec tsx test/continuous-test-suite-video-abort.ts",
|