@tuned-tensor/local 0.2.7 → 0.2.9

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +147 -2
  3. package/dist/artifacts.d.ts +92 -0
  4. package/dist/artifacts.js +591 -3
  5. package/dist/artifacts.js.map +1 -1
  6. package/dist/contracts.d.ts +5 -2
  7. package/dist/contracts.js +15 -10
  8. package/dist/contracts.js.map +1 -1
  9. package/dist/dataset.d.ts +3 -0
  10. package/dist/dataset.js +87 -5
  11. package/dist/dataset.js.map +1 -1
  12. package/dist/doctor.d.ts +13 -2
  13. package/dist/doctor.js +372 -49
  14. package/dist/doctor.js.map +1 -1
  15. package/dist/evaluation.d.ts +15 -0
  16. package/dist/evaluation.js +120 -16
  17. package/dist/evaluation.js.map +1 -1
  18. package/dist/huggingface-cache.d.ts +26 -0
  19. package/dist/huggingface-cache.js +68 -0
  20. package/dist/huggingface-cache.js.map +1 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +742 -76
  23. package/dist/index.js.map +1 -1
  24. package/dist/local-project.d.ts +11 -0
  25. package/dist/local-project.js +96 -3
  26. package/dist/local-project.js.map +1 -1
  27. package/dist/model-registry.d.ts +21 -0
  28. package/dist/model-registry.js +198 -0
  29. package/dist/model-registry.js.map +1 -1
  30. package/dist/model-server.d.ts +32 -0
  31. package/dist/model-server.js +158 -0
  32. package/dist/model-server.js.map +1 -0
  33. package/dist/orchestrator.d.ts +3 -0
  34. package/dist/orchestrator.js +1001 -142
  35. package/dist/orchestrator.js.map +1 -1
  36. package/dist/prefetch.d.ts +13 -0
  37. package/dist/prefetch.js +107 -19
  38. package/dist/prefetch.js.map +1 -1
  39. package/dist/process-runner.d.ts +7 -0
  40. package/dist/process-runner.js +171 -16
  41. package/dist/process-runner.js.map +1 -1
  42. package/dist/process-training.d.ts +5 -1
  43. package/dist/process-training.js +32 -16
  44. package/dist/process-training.js.map +1 -1
  45. package/dist/run-reporter.d.ts +2 -0
  46. package/dist/run-reporter.js +10 -0
  47. package/dist/run-reporter.js.map +1 -1
  48. package/dist/store.d.ts +16 -2
  49. package/dist/store.js +189 -24
  50. package/dist/store.js.map +1 -1
  51. package/docs/architecture.md +32 -0
  52. package/docs/local-workflow-remediation-2026-07-13.md +130 -0
  53. package/docs/local-workflow-ux-review-2026-07-13.md +458 -0
  54. package/package.json +4 -2
  55. package/training/local-runner/src/evaluate.py +80 -20
  56. package/training/local-runner/src/prefetch.py +107 -8
  57. package/training/local-runner/src/serve.py +329 -0
  58. package/training/local-runner/src/train.py +47 -22
  59. package/training/local-runner/src/train_dpo.py +41 -25
  60. package/training/local-runner/uv.lock +2401 -0
package/dist/artifacts.js CHANGED
@@ -1,5 +1,10 @@
1
- import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, isAbsolute, join, resolve } from "node:path";
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { copyFile, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { createGunzip } from "node:zlib";
6
+ const RUN_OWNER_FILE = ".tt-local-run-owner.json";
7
+ export const ARTIFACT_WORKFLOW_LOCK_FILE = ".tt-local-workflow.lock";
3
8
  export function fileUri(path) {
4
9
  return `file://${resolve(path)}`;
5
10
  }
@@ -9,7 +14,11 @@ export function defaultArtifactPrefix(input) {
9
14
  export function resolveRunArtifacts(args) {
10
15
  const root = resolve(args.artifactRoot);
11
16
  const safePrefix = args.prefix.replace(/^[/\\]+/, "");
12
- const runDir = isAbsolute(safePrefix) ? safePrefix : join(root, safePrefix);
17
+ const runDir = resolve(root, safePrefix);
18
+ const containment = relative(root, runDir);
19
+ if (containment === ".." || containment.startsWith(`..${sep}`) || isAbsolute(containment)) {
20
+ throw new Error(`Artifact prefix escapes artifactRoot: ${args.prefix}`);
21
+ }
13
22
  const trainingDir = join(runDir, "training");
14
23
  return {
15
24
  root,
@@ -21,6 +30,7 @@ export function resolveRunArtifacts(args) {
21
30
  baselineEvalJson: join(runDir, "baseline-eval.json"),
22
31
  candidateEvalJson: join(runDir, "candidate-eval.json"),
23
32
  runReportJson: join(runDir, "run-report.json"),
33
+ artifactManifestJson: join(runDir, "artifact-manifest.json"),
24
34
  progressJsonl: join(runDir, "progress.jsonl"),
25
35
  trainingDir,
26
36
  trainingInputDir: join(trainingDir, "input", "data", "training"),
@@ -30,7 +40,93 @@ export function resolveRunArtifacts(args) {
30
40
  trainingLog: join(trainingDir, "training.log"),
31
41
  };
32
42
  }
43
+ /**
44
+ * Creates the run directory one component at a time and rejects symlinked
45
+ * descendants. Lexical `resolve()` containment alone is insufficient because
46
+ * an existing child symlink could redirect later writes or recursive cleanup
47
+ * outside `artifactRoot`.
48
+ */
49
+ export async function ensureSafeRunDirectory(artifacts) {
50
+ await mkdir(artifacts.root, { recursive: true });
51
+ const canonicalRoot = await realpath(artifacts.root);
52
+ const pathFromRoot = relative(artifacts.root, artifacts.runDir);
53
+ const segments = pathFromRoot ? pathFromRoot.split(sep).filter(Boolean) : [];
54
+ let current = artifacts.root;
55
+ for (const segment of segments) {
56
+ current = join(current, segment);
57
+ let metadata = await lstat(current).catch((error) => {
58
+ if (error.code === "ENOENT")
59
+ return null;
60
+ throw error;
61
+ });
62
+ if (!metadata) {
63
+ await mkdir(current).catch((error) => {
64
+ if (error.code !== "EEXIST")
65
+ throw error;
66
+ });
67
+ metadata = await lstat(current);
68
+ }
69
+ if (metadata.isSymbolicLink()) {
70
+ throw new Error(`Artifact path must not contain symbolic links: ${current}`);
71
+ }
72
+ if (!metadata.isDirectory()) {
73
+ throw new Error(`Artifact path component is not a directory: ${current}`);
74
+ }
75
+ }
76
+ const canonicalRunDir = await realpath(artifacts.runDir);
77
+ const containment = relative(canonicalRoot, canonicalRunDir);
78
+ if (containment === ".." || containment.startsWith(`..${sep}`) || isAbsolute(containment)) {
79
+ throw new Error(`Artifact run directory escapes artifactRoot: ${artifacts.runDir}`);
80
+ }
81
+ }
82
+ /**
83
+ * Durably binds an artifact directory to one run identity. The owner survives
84
+ * workflow-lock release, preventing a later run with a colliding custom
85
+ * prefix from deleting or silently reusing another run's artifacts.
86
+ */
87
+ export async function claimRunArtifactDirectory(args) {
88
+ await ensureSafeRunDirectory(args.artifacts);
89
+ const ownerPath = join(args.artifacts.runDir, RUN_OWNER_FILE);
90
+ const proposed = {
91
+ schema_version: 1,
92
+ run_id: args.runId,
93
+ user_id: args.userId,
94
+ behavior_spec_id: args.behaviorSpecId,
95
+ created_at: new Date().toISOString(),
96
+ };
97
+ try {
98
+ const handle = await open(ownerPath, "wx", 0o600);
99
+ try {
100
+ await handle.writeFile(`${JSON.stringify(proposed, null, 2)}\n`);
101
+ await handle.sync();
102
+ }
103
+ finally {
104
+ await handle.close();
105
+ }
106
+ return proposed;
107
+ }
108
+ catch (error) {
109
+ if (error.code !== "EEXIST")
110
+ throw error;
111
+ }
112
+ let existing;
113
+ try {
114
+ existing = JSON.parse(await readFile(ownerPath, "utf8"));
115
+ }
116
+ catch (error) {
117
+ throw new Error(`Artifact run owner claim is unreadable: ${ownerPath}`, { cause: error });
118
+ }
119
+ if (existing.schema_version !== 1
120
+ || existing.run_id !== args.runId
121
+ || existing.user_id !== args.userId
122
+ || existing.behavior_spec_id !== args.behaviorSpecId) {
123
+ throw new Error(`Artifact directory ${args.artifacts.runDir} is already owned by run ${existing.run_id ?? "unknown"}; `
124
+ + `run ${args.runId} must use a different artifacts.prefix.`);
125
+ }
126
+ return existing;
127
+ }
33
128
  export async function prepareRunDirectories(artifacts) {
129
+ await ensureSafeRunDirectory(artifacts);
34
130
  await Promise.all([
35
131
  mkdir(artifacts.runDir, { recursive: true }),
36
132
  mkdir(artifacts.trainingInputDir, { recursive: true }),
@@ -43,6 +139,15 @@ export async function writeJson(path, value) {
43
139
  await mkdir(dirname(path), { recursive: true });
44
140
  await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
45
141
  }
142
+ export async function writeJsonAtomic(path, value) {
143
+ await writeFileAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
144
+ }
145
+ export async function writeFileAtomic(path, value) {
146
+ await mkdir(dirname(path), { recursive: true });
147
+ const temporary = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
148
+ await writeFile(temporary, value);
149
+ await rename(temporary, path);
150
+ }
46
151
  export async function readJson(path) {
47
152
  return JSON.parse(await readFile(path, "utf8"));
48
153
  }
@@ -55,4 +160,487 @@ export async function copyDatasetToTrainingChannel(artifacts) {
55
160
  await copyFile(artifacts.trainingJsonl, destination);
56
161
  return destination;
57
162
  }
163
+ const MANIFEST_IGNORED_PATHS = new Set([
164
+ "artifact-manifest.json",
165
+ // These are append-only/control-plane files rather than immutable stage
166
+ // outputs. Including them would invalidate a manifest whenever the store
167
+ // records the next stage event or refreshes the canonical request copy.
168
+ "progress.jsonl",
169
+ "request.json",
170
+ "detached.log",
171
+ RUN_OWNER_FILE,
172
+ ARTIFACT_WORKFLOW_LOCK_FILE,
173
+ ]);
174
+ function portableRelative(root, path) {
175
+ return relative(root, path).split(sep).join("/");
176
+ }
177
+ function shouldManifest(path) {
178
+ const atomicTemporary = /\.\d+\.\d+\.[0-9a-f-]{36}\.tmp$/i.test(path);
179
+ return !MANIFEST_IGNORED_PATHS.has(path) && !atomicTemporary;
180
+ }
181
+ async function listArtifactFiles(root, current = root) {
182
+ const files = [];
183
+ for (const entry of await readdir(current, { withFileTypes: true }).catch(() => [])) {
184
+ const absolute = join(current, entry.name);
185
+ if (entry.isSymbolicLink()) {
186
+ throw new Error(`Artifact tree must not contain symbolic links: ${absolute}`);
187
+ }
188
+ if (entry.isDirectory()) {
189
+ files.push(...await listArtifactFiles(root, absolute));
190
+ continue;
191
+ }
192
+ if (!entry.isFile())
193
+ continue;
194
+ const relativePath = portableRelative(root, absolute);
195
+ if (!shouldManifest(relativePath))
196
+ continue;
197
+ const metadata = await stat(absolute).catch(() => null);
198
+ if (metadata?.isFile())
199
+ files.push(relativePath);
200
+ }
201
+ return files.sort();
202
+ }
203
+ async function sha256File(path) {
204
+ const hash = createHash("sha256");
205
+ for await (const chunk of createReadStream(path))
206
+ hash.update(chunk);
207
+ return hash.digest("hex");
208
+ }
209
+ async function describeArtifactFile(root, path) {
210
+ const absolute = join(root, path);
211
+ const metadata = await stat(absolute);
212
+ return {
213
+ path,
214
+ size_bytes: metadata.size,
215
+ sha256: await sha256File(absolute),
216
+ };
217
+ }
218
+ function assertManifestFiles(value, label) {
219
+ if (!Array.isArray(value))
220
+ throw new Error(`Artifact manifest ${label} must be an array.`);
221
+ const paths = new Set();
222
+ for (const file of value) {
223
+ if (!file
224
+ || typeof file.path !== "string"
225
+ || !Number.isSafeInteger(file.size_bytes)
226
+ || file.size_bytes < 0
227
+ || typeof file.sha256 !== "string"
228
+ || !/^[a-f0-9]{64}$/.test(file.sha256)) {
229
+ throw new Error(`Artifact manifest contains an invalid ${label} entry.`);
230
+ }
231
+ const manifestPath = file.path;
232
+ const segments = manifestPath.split("/");
233
+ if (!manifestPath
234
+ || manifestPath.includes("\\")
235
+ || manifestPath.includes("\0")
236
+ || manifestPath.startsWith("/")
237
+ || /^[a-z]:/i.test(manifestPath)
238
+ || segments.some((segment) => !segment || segment === "." || segment === "..")) {
239
+ throw new Error(`Artifact manifest contains an unsafe ${label} path: ${manifestPath}`);
240
+ }
241
+ if (paths.has(manifestPath))
242
+ throw new Error(`Artifact manifest contains a duplicate ${label} path: ${manifestPath}`);
243
+ paths.add(manifestPath);
244
+ }
245
+ }
246
+ function parseArtifactManifest(value) {
247
+ if (!value || typeof value !== "object")
248
+ throw new Error("Artifact manifest must be an object.");
249
+ const candidate = value;
250
+ if (candidate.schema_version !== 1 || !Array.isArray(candidate.files)) {
251
+ throw new Error("Unsupported or invalid artifact manifest.");
252
+ }
253
+ assertManifestFiles(candidate.files, "file");
254
+ if (candidate.model) {
255
+ const model = candidate.model;
256
+ const hasBaseArtifact = model.base_model_artifact_uri !== undefined;
257
+ const hasBaseFingerprint = model.base_model_fingerprint !== undefined;
258
+ if ((model.artifact_kind !== "file" && model.artifact_kind !== "directory")
259
+ || typeof model.format !== "string"
260
+ || typeof model.framework !== "string"
261
+ || typeof model.base_model !== "string"
262
+ || (model.base_model_revision !== undefined && typeof model.base_model_revision !== "string")
263
+ || (model.base_model_artifact_uri !== undefined && typeof model.base_model_artifact_uri !== "string")
264
+ || (model.base_model_fingerprint !== undefined && typeof model.base_model_fingerprint !== "string")
265
+ || hasBaseArtifact !== hasBaseFingerprint
266
+ || typeof model.artifact_uri !== "string"
267
+ || typeof model.artifact_root !== "string"
268
+ || typeof model.servable !== "boolean"
269
+ || (model.parent_model_artifact !== undefined && typeof model.parent_model_artifact !== "string")
270
+ || !Array.isArray(model.files)
271
+ || model.files.length === 0) {
272
+ throw new Error("Artifact manifest contains invalid model contract metadata.");
273
+ }
274
+ assertManifestFiles(model.files, "model file");
275
+ }
276
+ return candidate;
277
+ }
278
+ /** Atomically snapshots every immutable file currently present in a run directory. */
279
+ export async function writeArtifactManifest(artifacts, options = {}) {
280
+ const paths = await listArtifactFiles(artifacts.runDir);
281
+ let model;
282
+ if (options.model) {
283
+ if ((options.model.base_model_artifact_uri === undefined)
284
+ !== (options.model.base_model_fingerprint === undefined)) {
285
+ throw new Error("Model contracts must record a local base-model URI and fingerprint together.");
286
+ }
287
+ const modelRoot = resolve(options.model.artifact_root);
288
+ const modelPaths = options.model.artifact_kind === "file"
289
+ ? [portableRelative(dirname(modelRoot), modelRoot)]
290
+ : await listArtifactFiles(modelRoot);
291
+ const checksumRoot = options.model.artifact_kind === "file" ? dirname(modelRoot) : modelRoot;
292
+ if (options.model.artifact_kind === "file"
293
+ && (options.model.format === "tar.gz" || modelRoot.endsWith(".tar.gz"))) {
294
+ const archive = await verifyTarGzipArchive(modelRoot);
295
+ if ((options.model.framework === "transformers-peft" || options.model.servable)
296
+ && archive.adapter_weight_entries === 0) {
297
+ throw new Error(`PEFT model archive contains no adapter_model.safetensors or adapter_model.bin: ${modelRoot}`);
298
+ }
299
+ if ((options.model.framework === "transformers-peft" || options.model.servable)
300
+ && archive.adapter_config_entries === 0) {
301
+ throw new Error(`PEFT model archive contains no adapter_config.json: ${modelRoot}`);
302
+ }
303
+ if (options.model.framework === "transformers-full" && archive.full_model_weight_entries === 0) {
304
+ throw new Error(`Full-model archive contains no Transformers model weights: ${modelRoot}`);
305
+ }
306
+ }
307
+ model = {
308
+ ...options.model,
309
+ artifact_root: modelRoot,
310
+ files: await Promise.all(modelPaths.map((path) => describeArtifactFile(checksumRoot, path))),
311
+ };
312
+ }
313
+ const manifest = {
314
+ schema_version: 1,
315
+ generated_at: new Date().toISOString(),
316
+ files: await Promise.all(paths.map((path) => describeArtifactFile(artifacts.runDir, path))),
317
+ ...(model ? { model } : {}),
318
+ };
319
+ await writeJsonAtomic(artifacts.artifactManifestJson, manifest);
320
+ return manifest;
321
+ }
322
+ /** Re-hashes a persisted run and reports missing, changed, and untracked files. */
323
+ export async function verifyArtifactManifest(manifestPath, options = {}) {
324
+ const root = dirname(resolve(manifestPath));
325
+ const manifest = parseArtifactManifest(JSON.parse(await readFile(manifestPath, "utf8")));
326
+ const expected = new Map(manifest.files.map((file) => [file.path, file]));
327
+ const actualPaths = await listArtifactFiles(root);
328
+ const actual = new Set(actualPaths);
329
+ const missing = [];
330
+ const changed = [];
331
+ const normalizedRequired = (options.requiredPaths ?? [])
332
+ .map((path) => path.split(sep).join("/").replace(/^\.\//, ""));
333
+ const checkedEntries = options.scopeToRequired
334
+ ? normalizedRequired.flatMap((path) => {
335
+ const entry = expected.get(path);
336
+ return entry ? [[path, entry]] : [];
337
+ })
338
+ : [...expected.entries()];
339
+ for (const [path, expectedFile] of checkedEntries) {
340
+ if (!actual.has(path)) {
341
+ missing.push(path);
342
+ continue;
343
+ }
344
+ const actualFile = await describeArtifactFile(root, path);
345
+ if (actualFile.size_bytes !== expectedFile.size_bytes || actualFile.sha256 !== expectedFile.sha256) {
346
+ changed.push(path);
347
+ }
348
+ }
349
+ for (const normalized of normalizedRequired) {
350
+ if (!expected.has(normalized) && !missing.includes(normalized))
351
+ missing.push(normalized);
352
+ }
353
+ const unexpected = options.allowUnexpected || options.scopeToRequired
354
+ ? []
355
+ : actualPaths.filter((path) => !expected.has(path));
356
+ let modelChecked = 0;
357
+ if (manifest.model && (options.verifyModel ?? !options.scopeToRequired)) {
358
+ const modelRoot = resolve(manifest.model.artifact_root);
359
+ const checksumRoot = manifest.model.artifact_kind === "file" ? dirname(modelRoot) : modelRoot;
360
+ const modelActualPaths = manifest.model.artifact_kind === "file"
361
+ ? [portableRelative(checksumRoot, modelRoot)]
362
+ : await listArtifactFiles(modelRoot);
363
+ const modelActual = new Set(modelActualPaths);
364
+ for (const expectedFile of manifest.model.files) {
365
+ const label = `model:${expectedFile.path}`;
366
+ if (!modelActual.has(expectedFile.path)) {
367
+ missing.push(label);
368
+ continue;
369
+ }
370
+ const actualFile = await describeArtifactFile(checksumRoot, expectedFile.path);
371
+ modelChecked += 1;
372
+ if (actualFile.size_bytes !== expectedFile.size_bytes || actualFile.sha256 !== expectedFile.sha256) {
373
+ changed.push(label);
374
+ }
375
+ }
376
+ if (!options.allowUnexpected) {
377
+ const expectedModelPaths = new Set(manifest.model.files.map((file) => file.path));
378
+ unexpected.push(...modelActualPaths.filter((path) => !expectedModelPaths.has(path)).map((path) => `model:${path}`));
379
+ }
380
+ if (manifest.model.artifact_kind === "file"
381
+ && (manifest.model.format === "tar.gz" || modelRoot.endsWith(".tar.gz"))) {
382
+ try {
383
+ const archive = await verifyTarGzipArchive(modelRoot);
384
+ if ((manifest.model.framework === "transformers-peft" || manifest.model.servable)
385
+ && archive.adapter_weight_entries === 0) {
386
+ changed.push(`model:${portableRelative(checksumRoot, modelRoot)}:missing_adapter_weights`);
387
+ }
388
+ if ((manifest.model.framework === "transformers-peft" || manifest.model.servable)
389
+ && archive.adapter_config_entries === 0) {
390
+ changed.push(`model:${portableRelative(checksumRoot, modelRoot)}:missing_adapter_config`);
391
+ }
392
+ if (manifest.model.framework === "transformers-full" && archive.full_model_weight_entries === 0) {
393
+ changed.push(`model:${portableRelative(checksumRoot, modelRoot)}:missing_full_model_weights`);
394
+ }
395
+ }
396
+ catch {
397
+ changed.push(`model:${portableRelative(checksumRoot, modelRoot)}:invalid_archive`);
398
+ }
399
+ }
400
+ }
401
+ return {
402
+ valid: missing.length === 0 && changed.length === 0 && unexpected.length === 0,
403
+ checked: checkedEntries.length + modelChecked,
404
+ missing: [...new Set(missing)].sort(),
405
+ changed: changed.sort(),
406
+ unexpected: unexpected.sort(),
407
+ };
408
+ }
409
+ /** Validates gzip CRC/truncation, tar headers, paths, and bounded PAX metadata. */
410
+ export async function verifyTarGzipArchive(path) {
411
+ const maxEntries = 100_000;
412
+ const maxExpandedBytes = 20 * 1024 * 1024 * 1024;
413
+ const maxPaxBytes = 1024 * 1024;
414
+ const safeMemberPath = (value) => {
415
+ const normalized = value.replaceAll("\\", "/");
416
+ const segments = normalized.split("/").filter(Boolean);
417
+ if (!normalized
418
+ || normalized.startsWith("/")
419
+ || /^[a-z]:\//i.test(normalized)
420
+ || segments.includes("..")) {
421
+ throw new Error(`Unsafe tar member path ${JSON.stringify(normalized)} in ${path}.`);
422
+ }
423
+ return normalized;
424
+ };
425
+ const parsePax = (payload, global) => {
426
+ let offset = 0;
427
+ let memberPath;
428
+ const allowedMetadata = new Set(["mtime", "atime", "ctime", "uid", "gid", "uname", "gname"]);
429
+ while (offset < payload.length) {
430
+ const separator = payload.indexOf(0x20, offset);
431
+ if (separator < 0)
432
+ throw new Error(`Malformed PAX record in ${path}.`);
433
+ const lengthText = payload.subarray(offset, separator).toString("ascii");
434
+ if (!/^[1-9][0-9]*$/.test(lengthText))
435
+ throw new Error(`Malformed PAX record length in ${path}.`);
436
+ const length = Number.parseInt(lengthText, 10);
437
+ const end = offset + length;
438
+ if (!Number.isSafeInteger(length) || end > payload.length || payload[end - 1] !== 0x0a) {
439
+ throw new Error(`Truncated PAX record in ${path}.`);
440
+ }
441
+ const record = payload.subarray(separator + 1, end - 1).toString("utf8");
442
+ const equals = record.indexOf("=");
443
+ if (equals <= 0)
444
+ throw new Error(`Malformed PAX key/value in ${path}.`);
445
+ const key = record.slice(0, equals);
446
+ const value = record.slice(equals + 1);
447
+ if (key === "path") {
448
+ if (global)
449
+ throw new Error(`Global PAX path overrides are unsupported in ${path}.`);
450
+ memberPath = safeMemberPath(value);
451
+ }
452
+ else if (!allowedMetadata.has(key)) {
453
+ // In particular, reject linkpath and size overrides: the validator
454
+ // must never interpret a member differently from a downstream reader.
455
+ throw new Error(`Unsupported PAX key ${JSON.stringify(key)} in ${path}.`);
456
+ }
457
+ offset = end;
458
+ }
459
+ return memberPath;
460
+ };
461
+ const gunzip = createReadStream(path).pipe(createGunzip());
462
+ let buffered = Buffer.alloc(0);
463
+ let payloadBytes = 0;
464
+ let paddingBytes = 0;
465
+ let paxType = null;
466
+ let paxChunks = [];
467
+ let pendingPaxPath;
468
+ let entries = 0;
469
+ let payloadEntries = 0;
470
+ let recognizedPayloadEntries = 0;
471
+ let recognizedPayloadBytes = 0;
472
+ let adapterWeightEntries = 0;
473
+ let adapterWeightBytes = 0;
474
+ let fullModelWeightEntries = 0;
475
+ let fullModelWeightBytes = 0;
476
+ let adapterConfigEntries = 0;
477
+ let expandedBytes = 0;
478
+ let zeroBlocks = 0;
479
+ let ended = false;
480
+ for await (const chunk of gunzip) {
481
+ buffered = Buffer.concat([buffered, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
482
+ while (true) {
483
+ if (payloadBytes > 0) {
484
+ if (buffered.length === 0)
485
+ break;
486
+ const consumed = Math.min(payloadBytes, buffered.length);
487
+ if (paxType)
488
+ paxChunks.push(buffered.subarray(0, consumed));
489
+ buffered = buffered.subarray(consumed);
490
+ payloadBytes -= consumed;
491
+ continue;
492
+ }
493
+ if (paddingBytes > 0) {
494
+ if (buffered.length === 0)
495
+ break;
496
+ const consumed = Math.min(paddingBytes, buffered.length);
497
+ buffered = buffered.subarray(consumed);
498
+ paddingBytes -= consumed;
499
+ continue;
500
+ }
501
+ if (paxType) {
502
+ const paxPath = parsePax(Buffer.concat(paxChunks), paxType === "g");
503
+ if (paxType === "x")
504
+ pendingPaxPath = paxPath;
505
+ paxType = null;
506
+ paxChunks = [];
507
+ continue;
508
+ }
509
+ if (buffered.length < 512)
510
+ break;
511
+ const header = buffered.subarray(0, 512);
512
+ buffered = buffered.subarray(512);
513
+ if (header.every((byte) => byte === 0)) {
514
+ zeroBlocks += 1;
515
+ if (zeroBlocks >= 2)
516
+ ended = true;
517
+ continue;
518
+ }
519
+ if (ended)
520
+ throw new Error(`Tar archive contains data after its end marker: ${path}.`);
521
+ zeroBlocks = 0;
522
+ const checksumText = header.subarray(148, 156).toString("ascii").replace(/\0/g, "").trim();
523
+ if (!/^[0-7]+$/.test(checksumText))
524
+ throw new Error(`Invalid tar checksum field in ${path}.`);
525
+ const expectedChecksum = Number.parseInt(checksumText, 8);
526
+ let actualChecksum = 0;
527
+ for (let index = 0; index < header.length; index += 1) {
528
+ actualChecksum += index >= 148 && index < 156 ? 32 : header[index];
529
+ }
530
+ if (actualChecksum !== expectedChecksum)
531
+ throw new Error(`Tar member checksum mismatch in ${path}.`);
532
+ const sizeText = header.subarray(124, 136).toString("ascii").replace(/\0/g, "").trim();
533
+ if (sizeText && !/^[0-7]+$/.test(sizeText))
534
+ throw new Error(`Invalid tar member size in ${path}.`);
535
+ const size = sizeText ? Number.parseInt(sizeText, 8) : 0;
536
+ if (!Number.isSafeInteger(size) || size < 0)
537
+ throw new Error(`Invalid tar member size in ${path}.`);
538
+ const type = String.fromCharCode(header[156] ?? 0);
539
+ if (type !== "\0" && type !== "0" && type !== "5" && type !== "x" && type !== "g") {
540
+ throw new Error(`Unsafe or unsupported tar member type ${JSON.stringify(type)} in ${path}.`);
541
+ }
542
+ const shortName = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
543
+ const prefix = header.subarray(345, 500).toString("utf8").replace(/\0.*$/, "");
544
+ const headerPath = safeMemberPath(prefix ? `${prefix}/${shortName}` : shortName);
545
+ const memberPath = type === "x" || type === "g"
546
+ ? headerPath
547
+ : safeMemberPath(pendingPaxPath ?? headerPath);
548
+ if (type !== "x" && type !== "g")
549
+ pendingPaxPath = undefined;
550
+ const segments = memberPath.split("/").filter(Boolean);
551
+ const name = segments.at(-1)?.toLowerCase();
552
+ const metadataOnly = name === "training-metrics.json"
553
+ || name === "trainer_state.json"
554
+ || name === "training_args.bin"
555
+ || name === "config.json"
556
+ || name === "generation_config.json"
557
+ || name === "adapter_config.json"
558
+ || name === "readme.md"
559
+ || name?.startsWith("tokenizer")
560
+ || name?.startsWith("special_tokens")
561
+ || name?.startsWith("added_tokens")
562
+ || name === "vocab.json"
563
+ || name === "merges.txt";
564
+ const recognizedPayload = Boolean(name)
565
+ && name !== "training_args.bin"
566
+ && (name.endsWith(".safetensors")
567
+ || name.endsWith(".bin")
568
+ || name.endsWith(".pt")
569
+ || name.endsWith(".pth")
570
+ || name.endsWith(".gguf")
571
+ || name.endsWith(".onnx")
572
+ || name.endsWith(".ckpt")
573
+ || name.endsWith(".npz"));
574
+ const adapterWeight = name === "adapter_model.safetensors" || name === "adapter_model.bin";
575
+ const fullModelWeight = Boolean(name)
576
+ && ((name.endsWith(".safetensors") && !name.startsWith("adapter_"))
577
+ || /^pytorch_model.*\.bin$/.test(name));
578
+ if (size > 0 && name === "adapter_config.json" && (type === "\0" || type === "0")) {
579
+ adapterConfigEntries += 1;
580
+ }
581
+ if ((type === "x" || type === "g") && size > maxPaxBytes) {
582
+ throw new Error(`PAX metadata exceeds 1 MiB in ${path}.`);
583
+ }
584
+ if (size > 0 && !metadataOnly && (type === "\0" || type === "0"))
585
+ payloadEntries += 1;
586
+ if (size > 0 && recognizedPayload && (type === "\0" || type === "0")) {
587
+ recognizedPayloadEntries += 1;
588
+ recognizedPayloadBytes += size;
589
+ }
590
+ if (size > 0 && adapterWeight && (type === "\0" || type === "0")) {
591
+ adapterWeightEntries += 1;
592
+ adapterWeightBytes += size;
593
+ }
594
+ if (size > 0 && fullModelWeight && (type === "\0" || type === "0")) {
595
+ fullModelWeightEntries += 1;
596
+ fullModelWeightBytes += size;
597
+ }
598
+ expandedBytes += size;
599
+ if (expandedBytes > maxExpandedBytes)
600
+ throw new Error(`Tar archive exceeds 20 GiB expanded size: ${path}.`);
601
+ payloadBytes = size;
602
+ paddingBytes = (512 - (size % 512)) % 512;
603
+ paxType = type === "x" || type === "g" ? type : null;
604
+ paxChunks = [];
605
+ entries += 1;
606
+ if (entries > maxEntries)
607
+ throw new Error(`Tar archive exceeds ${maxEntries} members: ${path}.`);
608
+ }
609
+ }
610
+ if (payloadBytes !== 0 || paddingBytes !== 0 || paxType || buffered.some((byte) => byte !== 0)) {
611
+ throw new Error(`Truncated tar archive: ${path}.`);
612
+ }
613
+ if (pendingPaxPath)
614
+ throw new Error(`PAX path override has no following member in ${path}.`);
615
+ if (!ended)
616
+ throw new Error(`Tar archive is missing its end marker: ${path}.`);
617
+ if (entries === 0)
618
+ throw new Error(`Empty tar archive: ${path}.`);
619
+ if (payloadEntries === 0)
620
+ throw new Error(`Tar archive contains no model payload files: ${path}.`);
621
+ return {
622
+ entries,
623
+ payload_entries: payloadEntries,
624
+ recognized_payload_entries: recognizedPayloadEntries,
625
+ recognized_payload_bytes: recognizedPayloadBytes,
626
+ adapter_weight_entries: adapterWeightEntries,
627
+ adapter_weight_bytes: adapterWeightBytes,
628
+ full_model_weight_entries: fullModelWeightEntries,
629
+ full_model_weight_bytes: fullModelWeightBytes,
630
+ adapter_config_entries: adapterConfigEntries,
631
+ expanded_bytes: expandedBytes,
632
+ };
633
+ }
634
+ export async function assertArtifactManifest(manifestPath, options = {}) {
635
+ const result = await verifyArtifactManifest(manifestPath, options);
636
+ if (!result.valid) {
637
+ const details = [
638
+ result.missing.length ? `missing=${result.missing.join(",")}` : "",
639
+ result.changed.length ? `changed=${result.changed.join(",")}` : "",
640
+ result.unexpected.length ? `unexpected=${result.unexpected.join(",")}` : "",
641
+ ].filter(Boolean).join(" ");
642
+ throw new Error(`Artifact integrity verification failed${details ? `: ${details}` : "."}`);
643
+ }
644
+ return result;
645
+ }
58
646
  //# sourceMappingURL=artifacts.js.map