@juspay/neurolink 12.0.5 → 12.2.0
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 +3 -3
- package/dist/agent/agentToolRegistrar.d.ts +30 -0
- package/dist/agent/agentToolRegistrar.js +72 -18
- package/dist/agent/backgroundCommands.d.ts +110 -0
- package/dist/agent/backgroundCommands.js +914 -0
- package/dist/agent/backgroundDelegation.d.ts +87 -0
- package/dist/agent/backgroundDelegation.js +753 -0
- package/dist/agent/gitTools.d.ts +43 -0
- package/dist/agent/gitTools.js +618 -0
- package/dist/agent/taskChecklist.d.ts +58 -0
- package/dist/agent/taskChecklist.js +322 -0
- package/dist/artifacts/artifactBanking.d.ts +57 -0
- package/dist/artifacts/artifactBanking.js +123 -0
- package/dist/artifacts/artifactStore.d.ts +36 -8
- package/dist/artifacts/artifactStore.js +164 -13
- package/dist/browser/neurolink.min.js +442 -414
- package/dist/cli/commands/setup.js +2 -1
- package/dist/constants/enums.d.ts +19 -0
- package/dist/constants/enums.js +20 -0
- package/dist/factories/providerDescriptors.js +16 -1
- package/dist/models/manifestRegistry.js +2 -0
- package/dist/models/manifests/cerebras.d.ts +9 -0
- package/dist/models/manifests/cerebras.js +19 -0
- package/dist/neurolink.d.ts +294 -3
- package/dist/neurolink.js +447 -4
- package/dist/providers/openaiCompatCatalog.d.ts +1 -1
- package/dist/providers/openaiCompatCatalog.js +34 -3
- package/dist/types/artifact.d.ts +54 -0
- package/dist/types/backgroundCommand.d.ts +174 -0
- package/dist/types/backgroundCommand.js +22 -0
- package/dist/types/delegation.d.ts +178 -0
- package/dist/types/delegation.js +18 -0
- package/dist/types/gitTools.d.ts +69 -0
- package/dist/types/gitTools.js +22 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +8 -0
- package/dist/types/pathSandbox.d.ts +23 -0
- package/dist/types/pathSandbox.js +12 -0
- package/dist/types/providers.d.ts +4 -0
- package/dist/types/tasks.d.ts +85 -0
- package/dist/types/tasks.js +14 -0
- package/dist/types/tools.d.ts +11 -0
- package/dist/utils/modelChoices.js +17 -1
- package/dist/utils/pathSandbox.d.ts +49 -0
- package/dist/utils/pathSandbox.js +127 -0
- package/dist/utils/providerConfig.d.ts +4 -0
- package/dist/utils/providerConfig.js +17 -0
- package/package.json +5 -1
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @module artifacts/artifactStore
|
|
19
19
|
*/
|
|
20
20
|
import { randomUUID } from "node:crypto";
|
|
21
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
21
|
+
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
22
22
|
import { tmpdir } from "node:os";
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { logger } from "../utils/logger.js";
|
|
@@ -28,17 +28,74 @@ import { logger } from "../utils/logger.js";
|
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
/** Characters used for the quick preview embedded in surrogate results. */
|
|
30
30
|
const DEFAULT_PREVIEW_CHARS = 500;
|
|
31
|
+
/**
|
|
32
|
+
* Sidecar written beside every payload so a process that never called
|
|
33
|
+
* `store()` can still resolve the id (see the index-miss path in `retrieve`).
|
|
34
|
+
*/
|
|
35
|
+
const META_SUFFIX = ".meta.json";
|
|
36
|
+
/**
|
|
37
|
+
* Ids that may be turned into a path.
|
|
38
|
+
*
|
|
39
|
+
* Before the index-miss fallback existed, an unknown id simply missed the
|
|
40
|
+
* in-memory map and no filesystem lookup happened. Now that a miss probes
|
|
41
|
+
* `join(dir, id + ext)`, the id reaches the path layer — and ids arrive from
|
|
42
|
+
* the model through `retrieve_context`. No dots and no separators means
|
|
43
|
+
* `../../etc/passwd` can never become a probe. Real ids are UUIDs.
|
|
44
|
+
*/
|
|
45
|
+
const SAFE_ARTIFACT_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
46
|
+
/** Extensions `store()` can produce, in the order the fallback probes them. */
|
|
47
|
+
const PAYLOAD_EXTENSIONS = [".json", ".txt"];
|
|
48
|
+
/**
|
|
49
|
+
* Runtime shape check for a sidecar read off disk — it is untrusted input
|
|
50
|
+
* (another process, an older version, a truncated write), so it is validated
|
|
51
|
+
* rather than asserted into `IndexEntry`.
|
|
52
|
+
*/
|
|
53
|
+
function parseSidecar(value) {
|
|
54
|
+
if (!value || typeof value !== "object") {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
const row = { ...value };
|
|
58
|
+
const { toolName, serverId, sizeBytes, contentType, createdAt, path } = row;
|
|
59
|
+
if (typeof toolName !== "string" ||
|
|
60
|
+
typeof serverId !== "string" ||
|
|
61
|
+
typeof sizeBytes !== "number" ||
|
|
62
|
+
(contentType !== "json" && contentType !== "text") ||
|
|
63
|
+
typeof createdAt !== "number" ||
|
|
64
|
+
typeof path !== "string") {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const entry = {
|
|
68
|
+
toolName,
|
|
69
|
+
serverId,
|
|
70
|
+
sizeBytes,
|
|
71
|
+
contentType,
|
|
72
|
+
createdAt,
|
|
73
|
+
path,
|
|
74
|
+
};
|
|
75
|
+
if (typeof row.sessionId === "string") {
|
|
76
|
+
entry.sessionId = row.sessionId;
|
|
77
|
+
}
|
|
78
|
+
if (typeof row.label === "string") {
|
|
79
|
+
entry.label = row.label;
|
|
80
|
+
}
|
|
81
|
+
const kind = row.kind;
|
|
82
|
+
if (kind === "worker-report" ||
|
|
83
|
+
kind === "command-output" ||
|
|
84
|
+
kind === "stage-output" ||
|
|
85
|
+
kind === "other") {
|
|
86
|
+
entry.kind = kind;
|
|
87
|
+
}
|
|
88
|
+
return entry;
|
|
89
|
+
}
|
|
31
90
|
/**
|
|
32
91
|
* Filesystem-backed artifact store using the OS temp directory.
|
|
33
92
|
*
|
|
34
93
|
* Files are written with mode 0o600 (owner read/write only).
|
|
35
|
-
* An in-memory index tracks metadata
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* - Multi-process deployments where each process manages its own artifacts
|
|
41
|
-
* (artifacts created in one process are not visible to others)
|
|
94
|
+
* An in-memory index tracks metadata for the fast path; every payload also
|
|
95
|
+
* gets a `<id>.meta.json` sidecar, so an id this process never stored — from
|
|
96
|
+
* another process, or from before a restart — still resolves (see
|
|
97
|
+
* `rehydrate`). `cleanup()` remains index-scoped: it expires what this process
|
|
98
|
+
* knows about, and never walks the directory deleting another process's work.
|
|
42
99
|
*
|
|
43
100
|
* @example
|
|
44
101
|
* ```typescript
|
|
@@ -56,8 +113,21 @@ const DEFAULT_PREVIEW_CHARS = 500;
|
|
|
56
113
|
export class LocalTempArtifactStore {
|
|
57
114
|
dir;
|
|
58
115
|
index = new Map();
|
|
59
|
-
|
|
116
|
+
rehydrateFromDisk;
|
|
117
|
+
/**
|
|
118
|
+
* @param dir - Storage directory; defaults to `tmpdir()/neurolink-artifacts`
|
|
119
|
+
* @param options - `rehydrateFromDisk` (default true) lets `retrieve()` and
|
|
120
|
+
* `delete()` fall back to the on-disk sidecar index on an in-memory miss,
|
|
121
|
+
* which makes artifacts READABLE AND DELETABLE ACROSS PROCESSES sharing
|
|
122
|
+
* the same directory and unix user. Pass `false` — or set
|
|
123
|
+
* `NEUROLINK_ARTIFACT_REHYDRATE=false` — to restore strict per-process
|
|
124
|
+
* isolation: ids not stored by this process resolve to nothing.
|
|
125
|
+
*/
|
|
126
|
+
constructor(dir, options) {
|
|
60
127
|
this.dir = dir ?? join(tmpdir(), "neurolink-artifacts");
|
|
128
|
+
this.rehydrateFromDisk =
|
|
129
|
+
options?.rehydrateFromDisk ??
|
|
130
|
+
process.env.NEUROLINK_ARTIFACT_REHYDRATE !== "false";
|
|
61
131
|
}
|
|
62
132
|
generatePreview(payload) {
|
|
63
133
|
if (payload.length <= DEFAULT_PREVIEW_CHARS) {
|
|
@@ -77,6 +147,7 @@ export class LocalTempArtifactStore {
|
|
|
77
147
|
path: filePath,
|
|
78
148
|
};
|
|
79
149
|
this.index.set(id, fullMeta);
|
|
150
|
+
await this.writeSidecar(id, fullMeta);
|
|
80
151
|
logger.debug(`[ArtifactStore] Stored artifact ${id} for tool "${meta.toolName}" ` +
|
|
81
152
|
`(${formatBytes(meta.sizeBytes)})`);
|
|
82
153
|
return {
|
|
@@ -87,9 +158,10 @@ export class LocalTempArtifactStore {
|
|
|
87
158
|
};
|
|
88
159
|
}
|
|
89
160
|
async retrieve(id) {
|
|
90
|
-
const entry = this.index.get(id)
|
|
161
|
+
const entry = this.index.get(id) ??
|
|
162
|
+
(this.rehydrateFromDisk ? await this.rehydrate(id) : undefined);
|
|
91
163
|
if (!entry) {
|
|
92
|
-
logger.debug(`[ArtifactStore] Artifact ${id} not
|
|
164
|
+
logger.debug(`[ArtifactStore] Artifact ${id} not found on disk`);
|
|
93
165
|
return null;
|
|
94
166
|
}
|
|
95
167
|
try {
|
|
@@ -103,15 +175,17 @@ export class LocalTempArtifactStore {
|
|
|
103
175
|
}
|
|
104
176
|
}
|
|
105
177
|
async delete(id) {
|
|
106
|
-
const entry = this.index.get(id)
|
|
178
|
+
const entry = this.index.get(id) ??
|
|
179
|
+
(this.rehydrateFromDisk ? await this.rehydrate(id) : undefined);
|
|
107
180
|
if (!entry) {
|
|
108
181
|
return;
|
|
109
182
|
}
|
|
110
183
|
try {
|
|
111
184
|
await rm(entry.path, { force: true });
|
|
185
|
+
await rm(join(this.dir, `${id}${META_SUFFIX}`), { force: true });
|
|
112
186
|
}
|
|
113
187
|
catch {
|
|
114
|
-
// Suppress —
|
|
188
|
+
// Suppress — files may already be gone
|
|
115
189
|
}
|
|
116
190
|
this.index.delete(id);
|
|
117
191
|
}
|
|
@@ -119,6 +193,11 @@ export class LocalTempArtifactStore {
|
|
|
119
193
|
const cutoff = Date.now() - olderThanMs;
|
|
120
194
|
let count = 0;
|
|
121
195
|
for (const [id, entry] of this.index.entries()) {
|
|
196
|
+
// Rehydrated entries are another process's artifacts — readable from
|
|
197
|
+
// here, but never this process's to expire.
|
|
198
|
+
if (entry.rehydrated === true) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
122
201
|
if (entry.createdAt < cutoff) {
|
|
123
202
|
await this.delete(id);
|
|
124
203
|
count++;
|
|
@@ -129,6 +208,78 @@ export class LocalTempArtifactStore {
|
|
|
129
208
|
}
|
|
130
209
|
return count;
|
|
131
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Record the index row next to the payload.
|
|
213
|
+
*
|
|
214
|
+
* The index is per-process, so without this an artifact written by one
|
|
215
|
+
* process is invisible to every other one — and to the same process after a
|
|
216
|
+
* restart. A failed sidecar write is logged, never fatal: the payload is
|
|
217
|
+
* already safely on disk and this process can still read it from its index.
|
|
218
|
+
*/
|
|
219
|
+
async writeSidecar(id, entry) {
|
|
220
|
+
try {
|
|
221
|
+
await writeFile(join(this.dir, `${id}${META_SUFFIX}`), JSON.stringify(entry), { encoding: "utf-8", mode: 0o600 });
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
logger.warn(`[ArtifactStore] Failed to write index sidecar for ${id} — the ` +
|
|
225
|
+
`artifact stays readable in this process only: ${err instanceof Error ? err.message : String(err)}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Resolve an id the in-memory index does not know: another process stored
|
|
230
|
+
* it, or this process restarted. Reads the sidecar first (full metadata);
|
|
231
|
+
* falls back to probing the payload file itself, so an artifact whose
|
|
232
|
+
* sidecar was lost is still readable with metadata recovered from `stat`.
|
|
233
|
+
*
|
|
234
|
+
* Returns undefined for an unsafe id without touching the filesystem.
|
|
235
|
+
*/
|
|
236
|
+
async rehydrate(id) {
|
|
237
|
+
if (!SAFE_ARTIFACT_ID.test(id)) {
|
|
238
|
+
logger.debug(`[ArtifactStore] Rejected unsafe artifact id "${id}"`);
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const raw = await readFile(join(this.dir, `${id}${META_SUFFIX}`), "utf-8");
|
|
243
|
+
const entry = parseSidecar(JSON.parse(raw));
|
|
244
|
+
if (entry) {
|
|
245
|
+
// The payload's location is fully determined by id + contentType, and
|
|
246
|
+
// the store directory is shared OS temp — so the path is DERIVED, never
|
|
247
|
+
// honoured from a sidecar another local account could have planted
|
|
248
|
+
// before this store first ran.
|
|
249
|
+
const derived = {
|
|
250
|
+
...entry,
|
|
251
|
+
path: join(this.dir, `${id}${entry.contentType === "json" ? ".json" : ".txt"}`),
|
|
252
|
+
rehydrated: true,
|
|
253
|
+
};
|
|
254
|
+
this.index.set(id, derived);
|
|
255
|
+
return derived;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// No sidecar, or an unreadable one — fall through to the probe.
|
|
260
|
+
}
|
|
261
|
+
for (const ext of PAYLOAD_EXTENSIONS) {
|
|
262
|
+
const path = join(this.dir, `${id}${ext}`);
|
|
263
|
+
try {
|
|
264
|
+
const stats = await stat(path);
|
|
265
|
+
const entry = {
|
|
266
|
+
toolName: "unknown",
|
|
267
|
+
serverId: "unknown",
|
|
268
|
+
sizeBytes: stats.size,
|
|
269
|
+
contentType: ext === ".json" ? "json" : "text",
|
|
270
|
+
createdAt: stats.mtimeMs,
|
|
271
|
+
path,
|
|
272
|
+
rehydrated: true,
|
|
273
|
+
};
|
|
274
|
+
this.index.set(id, entry);
|
|
275
|
+
return entry;
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// Not this extension — try the next.
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
132
283
|
}
|
|
133
284
|
// ---------------------------------------------------------------------------
|
|
134
285
|
// Helpers
|