@juspay/neurolink
Version:
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
285 lines (284 loc) • 11.2 kB
JavaScript
/**
* Artifact Store
*
* Pluggable storage for externalized MCP tool outputs.
*
* When `mcp.outputLimits.strategy = "externalize"` the full tool payload is
* written here instead of being sent inline to the LLM. The model receives a
* compact surrogate with a preview and an artifact ID. The full payload can be
* retrieved on demand via the `retrieve_context` tool.
*
* Architecture:
* ArtifactStore (interface) — canonical types in src/lib/types/artifactTypes.ts
* LocalTempArtifactStore — single-process, filesystem-backed implementation
*
* `RedisArtifactStore` is the shared-across-replicas backend; anything else
* (S3, a database) implements ArtifactStore and is injected via
* `artifacts.store` or `setArtifactStore()`.
*
* @module artifacts/artifactStore
*/
import { randomUUID } from "node:crypto";
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { logger } from "../utils/logger.js";
import { generateArtifactPreview, isSafeArtifactId } from "./artifactReader.js";
// ---------------------------------------------------------------------------
// LocalTempArtifactStore
// ---------------------------------------------------------------------------
/**
* Sidecar written beside every payload so a process that never called
* `store()` can still resolve the id (see the index-miss path in `retrieve`).
*/
const META_SUFFIX = ".meta.json";
/** Extensions `store()` can produce, in the order the fallback probes them. */
const PAYLOAD_EXTENSIONS = [".json", ".txt"];
/**
* Runtime shape check for a sidecar read off disk — it is untrusted input
* (another process, an older version, a truncated write), so it is validated
* rather than asserted into `IndexEntry`.
*/
function parseSidecar(value) {
if (!value || typeof value !== "object") {
return undefined;
}
const row = { ...value };
const { toolName, serverId, sizeBytes, contentType, createdAt, path } = row;
if (typeof toolName !== "string" ||
typeof serverId !== "string" ||
typeof sizeBytes !== "number" ||
(contentType !== "json" && contentType !== "text") ||
typeof createdAt !== "number" ||
typeof path !== "string") {
return undefined;
}
const entry = {
toolName,
serverId,
sizeBytes,
contentType,
createdAt,
path,
};
if (typeof row.sessionId === "string") {
entry.sessionId = row.sessionId;
}
if (typeof row.label === "string") {
entry.label = row.label;
}
const kind = row.kind;
if (kind === "worker-report" ||
kind === "command-output" ||
kind === "stage-output" ||
kind === "other") {
entry.kind = kind;
}
return entry;
}
/**
* Filesystem-backed artifact store using the OS temp directory.
*
* Files are written with mode 0o600 (owner read/write only).
* An in-memory index tracks metadata for the fast path; every payload also
* gets a `<id>.meta.json` sidecar, so an id this process never stored — from
* another process, or from before a restart — still resolves (see
* `rehydrate`). `cleanup()` remains index-scoped: it expires what this process
* knows about, and never walks the directory deleting another process's work.
*
* @example
* ```typescript
* const store = new LocalTempArtifactStore();
* const ref = await store.store(largeJson, {
* toolName: "list_files",
* serverId: "filesystem-server",
* sizeBytes: Buffer.byteLength(largeJson),
* contentType: "json",
* });
* // Later, via retrieve_context:
* const full = await store.retrieve(ref.id);
* ```
*/
export class LocalTempArtifactStore {
dir;
index = new Map();
rehydrateFromDisk;
/**
* @param dir - Storage directory; defaults to `tmpdir()/neurolink-artifacts`
* @param options - `rehydrateFromDisk` (default true) lets `retrieve()` and
* `delete()` fall back to the on-disk sidecar index on an in-memory miss,
* which makes artifacts READABLE AND DELETABLE ACROSS PROCESSES sharing
* the same directory and unix user. Pass `false` — or set
* `NEUROLINK_ARTIFACT_REHYDRATE=false` — to restore strict per-process
* isolation: ids not stored by this process resolve to nothing.
*/
constructor(dir, options) {
this.dir = dir ?? join(tmpdir(), "neurolink-artifacts");
this.rehydrateFromDisk =
options?.rehydrateFromDisk ??
process.env.NEUROLINK_ARTIFACT_REHYDRATE !== "false";
}
generatePreview(payload) {
return generateArtifactPreview(payload);
}
async store(payload, meta) {
await mkdir(this.dir, { recursive: true, mode: 0o700 });
const id = randomUUID();
const ext = meta.contentType === "json" ? ".json" : ".txt";
const filePath = join(this.dir, `${id}${ext}`);
await writeFile(filePath, payload, { encoding: "utf-8", mode: 0o600 });
const fullMeta = {
...meta,
createdAt: Date.now(),
path: filePath,
};
this.index.set(id, fullMeta);
await this.writeSidecar(id, fullMeta);
logger.debug(`[ArtifactStore] Stored artifact ${id} for tool "${meta.toolName}" ` +
`(${formatBytes(meta.sizeBytes)})`);
return {
id,
preview: this.generatePreview(payload),
sizeBytes: meta.sizeBytes,
meta: { ...meta, createdAt: fullMeta.createdAt },
};
}
async retrieve(id) {
const entry = this.index.get(id) ??
(this.rehydrateFromDisk ? await this.rehydrate(id) : undefined);
if (!entry) {
logger.debug(`[ArtifactStore] Artifact ${id} not found on disk`);
return null;
}
try {
const content = await readFile(entry.path, "utf-8");
logger.debug(`[ArtifactStore] Retrieved artifact ${id} (${formatBytes(entry.sizeBytes)})`);
return content;
}
catch (err) {
logger.warn(`[ArtifactStore] Failed to read artifact ${id}: ${err instanceof Error ? err.message : String(err)}`);
return null;
}
}
async delete(id) {
const entry = this.index.get(id) ??
(this.rehydrateFromDisk ? await this.rehydrate(id) : undefined);
if (!entry) {
return;
}
try {
await rm(entry.path, { force: true });
await rm(join(this.dir, `${id}${META_SUFFIX}`), { force: true });
}
catch {
// Suppress — files may already be gone
}
this.index.delete(id);
}
async cleanup(olderThanMs) {
const cutoff = Date.now() - olderThanMs;
let count = 0;
for (const [id, entry] of this.index.entries()) {
// Rehydrated entries are another process's artifacts — readable from
// here, but never this process's to expire.
if (entry.rehydrated === true) {
continue;
}
if (entry.createdAt < cutoff) {
await this.delete(id);
count++;
}
}
if (count > 0) {
logger.debug(`[ArtifactStore] Cleaned up ${count} expired artifact(s)`);
}
return count;
}
/**
* Record the index row next to the payload.
*
* The index is per-process, so without this an artifact written by one
* process is invisible to every other one — and to the same process after a
* restart. A failed sidecar write is logged, never fatal: the payload is
* already safely on disk and this process can still read it from its index.
*/
async writeSidecar(id, entry) {
try {
await writeFile(join(this.dir, `${id}${META_SUFFIX}`), JSON.stringify(entry), { encoding: "utf-8", mode: 0o600 });
}
catch (err) {
logger.warn(`[ArtifactStore] Failed to write index sidecar for ${id} — the ` +
`artifact stays readable in this process only: ${err instanceof Error ? err.message : String(err)}`);
}
}
/**
* Resolve an id the in-memory index does not know: another process stored
* it, or this process restarted. Reads the sidecar first (full metadata);
* falls back to probing the payload file itself, so an artifact whose
* sidecar was lost is still readable with metadata recovered from `stat`.
*
* Returns undefined for an unsafe id without touching the filesystem: before
* this fallback existed an unknown id simply missed the in-memory map, and
* now that a miss probes `join(dir, id + ext)` the id — which arrives from
* the model — reaches the path layer. See `isSafeArtifactId`.
*/
async rehydrate(id) {
if (!isSafeArtifactId(id)) {
logger.debug(`[ArtifactStore] Rejected unsafe artifact id "${id}"`);
return undefined;
}
try {
const raw = await readFile(join(this.dir, `${id}${META_SUFFIX}`), "utf-8");
const entry = parseSidecar(JSON.parse(raw));
if (entry) {
// The payload's location is fully determined by id + contentType, and
// the store directory is shared OS temp — so the path is DERIVED, never
// honoured from a sidecar another local account could have planted
// before this store first ran.
const derived = {
...entry,
path: join(this.dir, `${id}${entry.contentType === "json" ? ".json" : ".txt"}`),
rehydrated: true,
};
this.index.set(id, derived);
return derived;
}
}
catch {
// No sidecar, or an unreadable one — fall through to the probe.
}
for (const ext of PAYLOAD_EXTENSIONS) {
const path = join(this.dir, `${id}${ext}`);
try {
const stats = await stat(path);
const entry = {
toolName: "unknown",
serverId: "unknown",
sizeBytes: stats.size,
contentType: ext === ".json" ? "json" : "text",
createdAt: stats.mtimeMs,
path,
rehydrated: true,
};
this.index.set(id, entry);
return entry;
}
catch {
// Not this extension — try the next.
}
}
return undefined;
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function formatBytes(bytes) {
if (bytes < 1024) {
return `${bytes} B`;
}
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}