@agent-inspect/studio 6.17.5 → 6.17.6
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/dist/index.cjs +359 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.mjs +360 -118
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readFile, access, readdir,
|
|
2
|
-
import
|
|
1
|
+
import { readFile, access, readdir, writeFile, rm, copyFile, mkdir, rename, mkdtemp, lstat, stat } from 'fs/promises';
|
|
2
|
+
import path10 from 'path';
|
|
3
3
|
import { loadSessionRunRecords, buildSessionIndex, runSuite, buildRunTimeline, extractOutcomesFromTraceEvents, resolveTraceDir, searchTraces, TraceDirectory, loadTraceMetadataList } from 'agent-inspect/advanced';
|
|
4
4
|
import { resolveWorkspaceLocation, readWorkspaceManifestFile } from 'agent-inspect/workspace';
|
|
5
|
-
import { mkdirSync } from 'fs';
|
|
5
|
+
import { mkdirSync, createReadStream } from 'fs';
|
|
6
6
|
import { createRequire } from 'module';
|
|
7
7
|
import { timingSafeEqual, createHash } from 'crypto';
|
|
8
8
|
import { createServer } from 'http';
|
|
@@ -19,14 +19,14 @@ function isSafeRelativePath(p) {
|
|
|
19
19
|
return !trimmed.split(/[/\\]+/).some((seg) => seg === "..");
|
|
20
20
|
}
|
|
21
21
|
function resolveUnderRoot(root, ...segments) {
|
|
22
|
-
const resolvedRoot =
|
|
23
|
-
const resolved =
|
|
22
|
+
const resolvedRoot = path10.resolve(root);
|
|
23
|
+
const resolved = path10.resolve(resolvedRoot, ...segments);
|
|
24
24
|
assertPathUnderRoot(resolved, resolvedRoot);
|
|
25
25
|
return resolved;
|
|
26
26
|
}
|
|
27
27
|
function assertPathUnderRoot(resolved, root) {
|
|
28
|
-
const rel =
|
|
29
|
-
if (rel.startsWith("..") ||
|
|
28
|
+
const rel = path10.relative(path10.resolve(root), path10.resolve(resolved));
|
|
29
|
+
if (rel.startsWith("..") || path10.isAbsolute(rel)) {
|
|
30
30
|
throw new Error("path escapes allowed registry root");
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -207,7 +207,23 @@ function parseStudioRegistry(input) {
|
|
|
207
207
|
ingestConfig.bundleUpload = bundleUpload;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
|
|
210
|
+
if (input.ingest.fileDrop !== void 0) {
|
|
211
|
+
if (!isPlainObject(input.ingest.fileDrop)) {
|
|
212
|
+
errors.push("ingest.fileDrop must be an object");
|
|
213
|
+
} else {
|
|
214
|
+
const fileDrop = {};
|
|
215
|
+
if (input.ingest.fileDrop.maxBytes !== void 0) {
|
|
216
|
+
const maxBytes = Number(input.ingest.fileDrop.maxBytes);
|
|
217
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
|
|
218
|
+
errors.push("ingest.fileDrop.maxBytes must be a positive integer");
|
|
219
|
+
} else {
|
|
220
|
+
fileDrop.maxBytes = maxBytes;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
ingestConfig.fileDrop = fileDrop;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const knownIngestKeys = /* @__PURE__ */ new Set(["github", "http", "bundleUpload", "fileDrop"]);
|
|
211
227
|
for (const key of Object.keys(input.ingest)) {
|
|
212
228
|
if (!knownIngestKeys.has(key)) {
|
|
213
229
|
ingestWarnings.push(`ignored unknown ingest key: ${key}`);
|
|
@@ -248,7 +264,7 @@ async function readStudioRegistryFile(filePath) {
|
|
|
248
264
|
}
|
|
249
265
|
}
|
|
250
266
|
function resolveRegistryProjectPath(registryDir, projectPath) {
|
|
251
|
-
return
|
|
267
|
+
return path10.isAbsolute(projectPath) ? path10.resolve(projectPath) : path10.resolve(registryDir, projectPath);
|
|
252
268
|
}
|
|
253
269
|
var cached;
|
|
254
270
|
function loadBetterSqlite3() {
|
|
@@ -313,9 +329,9 @@ function resolveStudioDbPath(options) {
|
|
|
313
329
|
if (raw.startsWith("postgres://") || raw.startsWith("postgresql://")) {
|
|
314
330
|
return raw;
|
|
315
331
|
}
|
|
316
|
-
return
|
|
332
|
+
return path10.resolve(options.cwd ?? process.cwd(), raw);
|
|
317
333
|
}
|
|
318
|
-
return
|
|
334
|
+
return path10.resolve(
|
|
319
335
|
options.cwd ?? process.cwd(),
|
|
320
336
|
".agent-inspect",
|
|
321
337
|
DEFAULT_STUDIO_DB_FILENAME
|
|
@@ -330,7 +346,7 @@ function openStudioDb(dbPath) {
|
|
|
330
346
|
"Postgres studio databases are not supported; use a SQLite file path (preview only)."
|
|
331
347
|
);
|
|
332
348
|
}
|
|
333
|
-
const dir =
|
|
349
|
+
const dir = path10.dirname(dbPath);
|
|
334
350
|
try {
|
|
335
351
|
mkdirSync(dir, { recursive: true });
|
|
336
352
|
} catch (error) {
|
|
@@ -443,14 +459,14 @@ function insertIngestFile(db, row) {
|
|
|
443
459
|
// packages/studio/src/import.ts
|
|
444
460
|
async function discoverSuiteConfigs(projectRoot, configured) {
|
|
445
461
|
if (configured && configured.length > 0) {
|
|
446
|
-
return configured.map((rel) =>
|
|
462
|
+
return configured.map((rel) => path10.resolve(projectRoot, rel));
|
|
447
463
|
}
|
|
448
464
|
const found = [];
|
|
449
465
|
try {
|
|
450
466
|
const entries = await readdir(projectRoot);
|
|
451
467
|
for (const entry of entries) {
|
|
452
468
|
if (entry.endsWith(".suite.json")) {
|
|
453
|
-
found.push(
|
|
469
|
+
found.push(path10.join(projectRoot, entry));
|
|
454
470
|
}
|
|
455
471
|
}
|
|
456
472
|
} catch {
|
|
@@ -460,7 +476,7 @@ async function discoverSuiteConfigs(projectRoot, configured) {
|
|
|
460
476
|
async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
461
477
|
const runs = [];
|
|
462
478
|
for (const rel of traceDirs) {
|
|
463
|
-
const traceDir = resolveTraceDir({ dir:
|
|
479
|
+
const traceDir = resolveTraceDir({ dir: path10.join(workspaceDir, rel) });
|
|
464
480
|
const td = new TraceDirectory({ dir: traceDir });
|
|
465
481
|
const files = await td.list();
|
|
466
482
|
const metas = await loadTraceMetadataList(
|
|
@@ -474,7 +490,7 @@ async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
|
474
490
|
runId: meta.runId,
|
|
475
491
|
...meta.name !== void 0 ? { name: meta.name } : {},
|
|
476
492
|
status: meta.status,
|
|
477
|
-
file:
|
|
493
|
+
file: path10.basename(meta.filePath),
|
|
478
494
|
...meta.startedAt !== void 0 ? { startedAt: meta.startedAt } : {},
|
|
479
495
|
...meta.durationMs !== void 0 ? { durationMs: meta.durationMs } : {}
|
|
480
496
|
});
|
|
@@ -483,7 +499,7 @@ async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
|
483
499
|
return runs.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
|
|
484
500
|
}
|
|
485
501
|
async function importStudioRegistry(options) {
|
|
486
|
-
const registryDir =
|
|
502
|
+
const registryDir = path10.dirname(options.registryPath);
|
|
487
503
|
const warnings = [];
|
|
488
504
|
const projects = [];
|
|
489
505
|
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -544,21 +560,21 @@ async function importStudioProject(options) {
|
|
|
544
560
|
}
|
|
545
561
|
async function resolveStudioRegistryPath(options) {
|
|
546
562
|
if (options.workspacePath && options.workspacePath.trim() !== "") {
|
|
547
|
-
return
|
|
563
|
+
return path10.resolve(options.cwd ?? process.cwd(), options.workspacePath);
|
|
548
564
|
}
|
|
549
|
-
const cwd =
|
|
565
|
+
const cwd = path10.resolve(options.cwd ?? process.cwd());
|
|
550
566
|
for (const rel of STUDIO_REGISTRY_FILENAMES) {
|
|
551
|
-
const candidate =
|
|
567
|
+
const candidate = path10.join(cwd, rel);
|
|
552
568
|
try {
|
|
553
569
|
await access(candidate);
|
|
554
570
|
return candidate;
|
|
555
571
|
} catch {
|
|
556
572
|
}
|
|
557
573
|
}
|
|
558
|
-
return
|
|
574
|
+
return path10.join(cwd, STUDIO_REGISTRY_FILENAMES[0]);
|
|
559
575
|
}
|
|
560
576
|
function resolveImportDirs(registryPath, registry) {
|
|
561
|
-
const registryDir =
|
|
577
|
+
const registryDir = path10.dirname(registryPath);
|
|
562
578
|
const importConfig = registry.import ?? {};
|
|
563
579
|
const fileDropDir = importConfig.fileDropDir ?? "imports/drop";
|
|
564
580
|
const ciArtifactsDir = importConfig.ciArtifactsDir ?? "imports/ci";
|
|
@@ -571,10 +587,10 @@ function resolveImportDirs(registryPath, registry) {
|
|
|
571
587
|
};
|
|
572
588
|
}
|
|
573
589
|
function uniqueDestPath(destDir, fileName, contentHash) {
|
|
574
|
-
const ext =
|
|
575
|
-
const base =
|
|
590
|
+
const ext = path10.extname(fileName);
|
|
591
|
+
const base = path10.basename(fileName, ext);
|
|
576
592
|
const shortHash = contentHash.slice(0, 8);
|
|
577
|
-
return
|
|
593
|
+
return path10.join(destDir, `${base}-${shortHash}${ext}`);
|
|
578
594
|
}
|
|
579
595
|
function sanitizeSafeErrorMessage(message, secret) {
|
|
580
596
|
if (!secret || secret.length < 4) return message;
|
|
@@ -591,6 +607,136 @@ function parseGitHubRepo(repo) {
|
|
|
591
607
|
function buildGitHubArtifactSourceKey(options) {
|
|
592
608
|
return `github:${options.owner}/${options.repo}/runs/${options.runId}/${options.artifactName}`;
|
|
593
609
|
}
|
|
610
|
+
var DEFAULT_MAX_INGEST_BYTES = 52428800;
|
|
611
|
+
var IngestLimitError = class extends Error {
|
|
612
|
+
code;
|
|
613
|
+
constructor(code, message) {
|
|
614
|
+
super(message);
|
|
615
|
+
this.name = "IngestLimitError";
|
|
616
|
+
this.code = code;
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
function resolveIngestMaxBytes(configured) {
|
|
620
|
+
if (configured === void 0) return DEFAULT_MAX_INGEST_BYTES;
|
|
621
|
+
if (!Number.isInteger(configured) || configured <= 0) {
|
|
622
|
+
throw new IngestLimitError(
|
|
623
|
+
"INGEST_SIZE_LIMIT",
|
|
624
|
+
"maxBytes must be a positive integer"
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
return configured;
|
|
628
|
+
}
|
|
629
|
+
async function lstatRegularFile(filePath) {
|
|
630
|
+
const info = await lstat(filePath);
|
|
631
|
+
if (info.isSymbolicLink()) {
|
|
632
|
+
throw new IngestLimitError(
|
|
633
|
+
"INGEST_SYMLINK_REJECTED",
|
|
634
|
+
"symbolic links are not allowed for ingest"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
if (!info.isFile()) {
|
|
638
|
+
throw new IngestLimitError("INGEST_NOT_A_FILE", "ingest path must be a regular file");
|
|
639
|
+
}
|
|
640
|
+
return info;
|
|
641
|
+
}
|
|
642
|
+
async function lstatRegularDirectory(dirPath) {
|
|
643
|
+
const info = await lstat(dirPath);
|
|
644
|
+
if (info.isSymbolicLink()) {
|
|
645
|
+
throw new IngestLimitError(
|
|
646
|
+
"INGEST_SYMLINK_REJECTED",
|
|
647
|
+
"symbolic links are not allowed for ingest"
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (!info.isDirectory()) {
|
|
651
|
+
throw new IngestLimitError(
|
|
652
|
+
"INGEST_NOT_A_DIRECTORY",
|
|
653
|
+
"ingest path must be a directory"
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
return info;
|
|
657
|
+
}
|
|
658
|
+
async function assertFileWithinByteLimit(filePath, maxBytes) {
|
|
659
|
+
const info = await lstatRegularFile(filePath);
|
|
660
|
+
if (info.size > maxBytes) {
|
|
661
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "file exceeds size limit");
|
|
662
|
+
}
|
|
663
|
+
return info;
|
|
664
|
+
}
|
|
665
|
+
async function measureDirectoryBytes(dirPath, maxBytes) {
|
|
666
|
+
await lstatRegularDirectory(dirPath);
|
|
667
|
+
let total = 0;
|
|
668
|
+
const walk = async (current) => {
|
|
669
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
670
|
+
for (const entry of entries) {
|
|
671
|
+
const abs = path10.join(current, entry.name);
|
|
672
|
+
const info = await lstat(abs);
|
|
673
|
+
if (info.isSymbolicLink()) {
|
|
674
|
+
throw new IngestLimitError(
|
|
675
|
+
"INGEST_SYMLINK_REJECTED",
|
|
676
|
+
"symbolic links are not allowed for ingest"
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
if (info.isDirectory()) {
|
|
680
|
+
await walk(abs);
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (!info.isFile()) continue;
|
|
684
|
+
total += info.size;
|
|
685
|
+
if (total > maxBytes) {
|
|
686
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "directory exceeds size limit");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
await walk(dirPath);
|
|
691
|
+
return total;
|
|
692
|
+
}
|
|
693
|
+
async function readBoundedResponseBody(response, maxBytes) {
|
|
694
|
+
const lengthHeader = response.headers.get("content-length");
|
|
695
|
+
if (lengthHeader) {
|
|
696
|
+
const length = Number(lengthHeader);
|
|
697
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
698
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "response exceeds size limit");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (!response.body) {
|
|
702
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
703
|
+
if (arrayBuffer.byteLength > maxBytes) {
|
|
704
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "response exceeds size limit");
|
|
705
|
+
}
|
|
706
|
+
return Buffer.from(arrayBuffer);
|
|
707
|
+
}
|
|
708
|
+
const reader = response.body.getReader();
|
|
709
|
+
const chunks = [];
|
|
710
|
+
let total = 0;
|
|
711
|
+
for (; ; ) {
|
|
712
|
+
const { done, value } = await reader.read();
|
|
713
|
+
if (done) break;
|
|
714
|
+
if (!value || value.byteLength === 0) continue;
|
|
715
|
+
total += value.byteLength;
|
|
716
|
+
if (total > maxBytes) {
|
|
717
|
+
try {
|
|
718
|
+
await reader.cancel();
|
|
719
|
+
} catch {
|
|
720
|
+
}
|
|
721
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "response exceeds size limit");
|
|
722
|
+
}
|
|
723
|
+
chunks.push(Buffer.from(value));
|
|
724
|
+
}
|
|
725
|
+
return Buffer.concat(chunks);
|
|
726
|
+
}
|
|
727
|
+
async function withAtomicStagingDir(parentDir, work) {
|
|
728
|
+
await mkdir(parentDir, { recursive: true });
|
|
729
|
+
const stagingDir = await mkdtemp(path10.join(parentDir, ".ingest-staging-"));
|
|
730
|
+
try {
|
|
731
|
+
return await work(stagingDir);
|
|
732
|
+
} finally {
|
|
733
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async function promoteStagingPath(stagingPath, finalPath) {
|
|
737
|
+
await mkdir(path10.dirname(finalPath), { recursive: true });
|
|
738
|
+
await rename(stagingPath, finalPath);
|
|
739
|
+
}
|
|
594
740
|
|
|
595
741
|
// packages/studio/src/ingest/file-drop.ts
|
|
596
742
|
var FILE_DROP_ARCHIVE_DIR = ".imported";
|
|
@@ -603,26 +749,38 @@ function classifyFile(fileName) {
|
|
|
603
749
|
return void 0;
|
|
604
750
|
}
|
|
605
751
|
async function hashFile(filePath) {
|
|
606
|
-
const
|
|
607
|
-
|
|
752
|
+
const hash = createHash("sha256");
|
|
753
|
+
const stream = createReadStream(filePath);
|
|
754
|
+
for await (const chunk of stream) {
|
|
755
|
+
hash.update(chunk);
|
|
756
|
+
}
|
|
757
|
+
return hash.digest("hex");
|
|
608
758
|
}
|
|
609
759
|
async function importOneFile(options) {
|
|
610
760
|
const destPath = uniqueDestPath(options.destDir, options.fileName, options.contentHash);
|
|
611
|
-
assertPathUnderRoot(destPath,
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
761
|
+
assertPathUnderRoot(destPath, path10.dirname(options.destDir));
|
|
762
|
+
try {
|
|
763
|
+
await withAtomicStagingDir(options.destDir, async (stagingDir) => {
|
|
764
|
+
const stagedPath = path10.join(stagingDir, options.fileName);
|
|
765
|
+
await copyFile(options.sourcePath, stagedPath);
|
|
766
|
+
await promoteStagingPath(stagedPath, destPath);
|
|
767
|
+
});
|
|
768
|
+
insertIngestFile(options.db, {
|
|
769
|
+
sourceKey: options.sourceKey,
|
|
770
|
+
sourceName: options.fileName,
|
|
771
|
+
destPath,
|
|
772
|
+
kind: options.kind,
|
|
773
|
+
contentHash: options.contentHash,
|
|
774
|
+
importedAt: options.importedAt
|
|
775
|
+
});
|
|
776
|
+
} catch (error) {
|
|
777
|
+
await rm(destPath, { force: true }).catch(() => void 0);
|
|
778
|
+
throw error;
|
|
779
|
+
}
|
|
622
780
|
let archived = false;
|
|
623
781
|
if (options.archiveAfterImport) {
|
|
624
782
|
await mkdir(options.archiveDir, { recursive: true });
|
|
625
|
-
const archiveTarget =
|
|
783
|
+
const archiveTarget = path10.join(options.archiveDir, options.fileName);
|
|
626
784
|
await rename(options.sourcePath, archiveTarget);
|
|
627
785
|
archived = true;
|
|
628
786
|
}
|
|
@@ -651,10 +809,27 @@ async function importFileDrop(options) {
|
|
|
651
809
|
files
|
|
652
810
|
};
|
|
653
811
|
}
|
|
812
|
+
let maxBytes;
|
|
813
|
+
try {
|
|
814
|
+
maxBytes = resolveIngestMaxBytes(
|
|
815
|
+
options.maxBytes ?? options.registry.ingest?.fileDrop?.maxBytes
|
|
816
|
+
);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
819
|
+
return {
|
|
820
|
+
skipped: false,
|
|
821
|
+
scanned: 0,
|
|
822
|
+
imported: 0,
|
|
823
|
+
skippedFiles: 0,
|
|
824
|
+
errors: [message],
|
|
825
|
+
warnings,
|
|
826
|
+
files
|
|
827
|
+
};
|
|
828
|
+
}
|
|
654
829
|
const dirs = resolveImportDirs(options.registryPath, options.registry);
|
|
655
830
|
let dropDir;
|
|
656
831
|
try {
|
|
657
|
-
dropDir = options.dropDir ?
|
|
832
|
+
dropDir = options.dropDir ? path10.isAbsolute(options.dropDir) ? (assertPathUnderRoot(options.dropDir, dirs.registryDir), options.dropDir) : resolveUnderRoot(dirs.registryDir, options.dropDir) : dirs.fileDropDir;
|
|
658
833
|
assertPathUnderRoot(dropDir, dirs.registryDir);
|
|
659
834
|
} catch (error) {
|
|
660
835
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -684,24 +859,35 @@ async function importFileDrop(options) {
|
|
|
684
859
|
};
|
|
685
860
|
}
|
|
686
861
|
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
687
|
-
const archiveDir =
|
|
862
|
+
const archiveDir = path10.join(dropDir, FILE_DROP_ARCHIVE_DIR);
|
|
688
863
|
let scanned = 0;
|
|
689
864
|
let imported = 0;
|
|
690
865
|
let skippedFiles = 0;
|
|
691
866
|
for (const entry of entries.sort()) {
|
|
692
867
|
if (entry === FILE_DROP_ARCHIVE_DIR || entry.startsWith(".")) continue;
|
|
693
|
-
const sourcePath =
|
|
694
|
-
|
|
868
|
+
const sourcePath = path10.join(dropDir, entry);
|
|
869
|
+
const kind = classifyFile(entry);
|
|
870
|
+
if (!kind) continue;
|
|
871
|
+
scanned += 1;
|
|
695
872
|
try {
|
|
696
|
-
|
|
697
|
-
} catch {
|
|
873
|
+
await assertFileWithinByteLimit(sourcePath, maxBytes);
|
|
874
|
+
} catch (error) {
|
|
875
|
+
if (error instanceof IngestLimitError && error.code === "INGEST_NOT_A_FILE") {
|
|
876
|
+
scanned -= 1;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (error instanceof IngestLimitError && error.code === "INGEST_SYMLINK_REJECTED") {
|
|
880
|
+
errors.push(`failed to import ${entry}: ${error.message}`);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
if (error instanceof IngestLimitError && error.code === "INGEST_SIZE_LIMIT") {
|
|
884
|
+
errors.push(`failed to import ${entry}: ${error.message}`);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
698
887
|
warnings.push(`skipped unreadable entry: ${entry}`);
|
|
888
|
+
scanned -= 1;
|
|
699
889
|
continue;
|
|
700
890
|
}
|
|
701
|
-
if (!fileStat.isFile()) continue;
|
|
702
|
-
const kind = classifyFile(entry);
|
|
703
|
-
if (!kind) continue;
|
|
704
|
-
scanned += 1;
|
|
705
891
|
const sourceKey = entry;
|
|
706
892
|
let contentHash;
|
|
707
893
|
try {
|
|
@@ -754,7 +940,8 @@ async function importFileDropFromRegistry(options) {
|
|
|
754
940
|
registry: options.registry,
|
|
755
941
|
enabled: options.enabled,
|
|
756
942
|
...options.dropDir !== void 0 ? { dropDir: options.dropDir } : {},
|
|
757
|
-
...options.archiveAfterImport !== void 0 ? { archiveAfterImport: options.archiveAfterImport } : {}
|
|
943
|
+
...options.archiveAfterImport !== void 0 ? { archiveAfterImport: options.archiveAfterImport } : {},
|
|
944
|
+
...options.maxBytes !== void 0 ? { maxBytes: options.maxBytes } : {}
|
|
758
945
|
});
|
|
759
946
|
}
|
|
760
947
|
async function runStudioFileDropImport(options) {
|
|
@@ -871,7 +1058,6 @@ function isIngestTokenValid(provided, expected) {
|
|
|
871
1058
|
var DEFAULT_HTTP_INGEST_BASE_PATH = "/api/ingest";
|
|
872
1059
|
var HTTP_INGEST_BUNDLE_PATH = "/api/ingest/bundle";
|
|
873
1060
|
var HTTP_INGEST_ARTIFACT_PATH = "/api/ingest/artifact";
|
|
874
|
-
var DEFAULT_MAX_INGEST_BYTES = 52428800;
|
|
875
1061
|
function resolveHttpIngestConfig(options, registryHttp) {
|
|
876
1062
|
const http = registryHttp ?? options.context?.registry.ingest?.http;
|
|
877
1063
|
const enabled = options.ingestHttp === true || http?.enabled === true;
|
|
@@ -880,7 +1066,7 @@ function resolveHttpIngestConfig(options, registryHttp) {
|
|
|
880
1066
|
...options.ingestTokenEnv !== void 0 ? { tokenEnv: options.ingestTokenEnv } : {},
|
|
881
1067
|
...http?.tokenEnv !== void 0 ? { registryTokenEnv: http.tokenEnv } : {}
|
|
882
1068
|
});
|
|
883
|
-
const maxBytes = http?.maxBytes
|
|
1069
|
+
const maxBytes = resolveIngestMaxBytes(http?.maxBytes);
|
|
884
1070
|
return { enabled, basePath, tokenEnv, maxBytes };
|
|
885
1071
|
}
|
|
886
1072
|
function sendJson(res, status, body) {
|
|
@@ -950,22 +1136,30 @@ async function handleHttpIngestRequest(req, res, ctx, options, pathname) {
|
|
|
950
1136
|
const destDir = isBundle ? dirs.bundlesDir : dirs.ciArtifactsDir;
|
|
951
1137
|
const destPath = uniqueDestPath(destDir, fileName, contentHash);
|
|
952
1138
|
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1139
|
+
try {
|
|
1140
|
+
await withAtomicStagingDir(destDir, async (stagingDir) => {
|
|
1141
|
+
const stagedPath = path10.join(stagingDir, fileName);
|
|
1142
|
+
await writeFile(stagedPath, body);
|
|
1143
|
+
await promoteStagingPath(stagedPath, destPath);
|
|
1144
|
+
});
|
|
1145
|
+
const sourceKey = isBundle ? `http:bundle:${contentHash}` : buildGitHubArtifactSourceKey({
|
|
1146
|
+
owner: "http",
|
|
1147
|
+
repo: "ingest",
|
|
1148
|
+
runId: importedAt,
|
|
1149
|
+
artifactName: fileName
|
|
1150
|
+
});
|
|
1151
|
+
insertIngestFile(ctx.db, {
|
|
1152
|
+
sourceKey,
|
|
1153
|
+
sourceName: fileName,
|
|
1154
|
+
destPath,
|
|
1155
|
+
kind: isBundle ? "bundle" : "ci",
|
|
1156
|
+
contentHash,
|
|
1157
|
+
importedAt
|
|
1158
|
+
});
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
await rm(destPath, { force: true }).catch(() => void 0);
|
|
1161
|
+
throw error;
|
|
1162
|
+
}
|
|
969
1163
|
const registryImport = await importStudioRegistry({
|
|
970
1164
|
db: ctx.db,
|
|
971
1165
|
registry: ctx.registry,
|
|
@@ -1150,7 +1344,7 @@ function getImportedProject(db, projects, projectId) {
|
|
|
1150
1344
|
async function loadTraceDirMetas(workspaceDir, traceDirs) {
|
|
1151
1345
|
const metas = [];
|
|
1152
1346
|
for (const rel of traceDirs) {
|
|
1153
|
-
const traceDir = resolveTraceDir({ dir:
|
|
1347
|
+
const traceDir = resolveTraceDir({ dir: path10.join(workspaceDir, rel) });
|
|
1154
1348
|
const td = new TraceDirectory({ dir: traceDir });
|
|
1155
1349
|
const files = await td.list();
|
|
1156
1350
|
const listed = await loadTraceMetadataList(
|
|
@@ -1179,7 +1373,7 @@ async function loadProjectSuitesView(ctx) {
|
|
|
1179
1373
|
try {
|
|
1180
1374
|
const result = await runSuite({
|
|
1181
1375
|
configPath,
|
|
1182
|
-
cwd:
|
|
1376
|
+
cwd: path10.dirname(configPath)
|
|
1183
1377
|
});
|
|
1184
1378
|
suites.push({
|
|
1185
1379
|
suiteName: result.suiteName,
|
|
@@ -1197,7 +1391,7 @@ async function loadProjectSuitesView(ctx) {
|
|
|
1197
1391
|
} catch (error) {
|
|
1198
1392
|
const message = error instanceof Error ? error.message : String(error);
|
|
1199
1393
|
suites.push({
|
|
1200
|
-
suiteName:
|
|
1394
|
+
suiteName: path10.basename(configPath),
|
|
1201
1395
|
configPath,
|
|
1202
1396
|
ok: false,
|
|
1203
1397
|
status: "error",
|
|
@@ -1246,7 +1440,7 @@ async function loadProjectSearchView(ctx, db, params) {
|
|
|
1246
1440
|
}
|
|
1247
1441
|
const metas = await loadTraceDirMetas(ctx.project.workspaceDir, ["runs"]);
|
|
1248
1442
|
const traceDir = resolveTraceDir({
|
|
1249
|
-
dir:
|
|
1443
|
+
dir: path10.join(ctx.project.workspaceDir, "runs")
|
|
1250
1444
|
});
|
|
1251
1445
|
const results = await searchTraces(metas, {
|
|
1252
1446
|
traceDir,
|
|
@@ -1290,12 +1484,12 @@ async function loadProjectDiffView(ctx, params) {
|
|
|
1290
1484
|
};
|
|
1291
1485
|
}
|
|
1292
1486
|
async function loadProjectReportsView(ctx) {
|
|
1293
|
-
const reportsDir =
|
|
1487
|
+
const reportsDir = path10.join(ctx.project.workspaceDir, "reports");
|
|
1294
1488
|
const reports = [];
|
|
1295
1489
|
try {
|
|
1296
1490
|
const files = await readdir(reportsDir);
|
|
1297
1491
|
for (const file of files) {
|
|
1298
|
-
const filePath =
|
|
1492
|
+
const filePath = path10.join(reportsDir, file);
|
|
1299
1493
|
const info = await stat(filePath);
|
|
1300
1494
|
if (!info.isFile()) continue;
|
|
1301
1495
|
reports.push({ name: file, path: filePath, sizeBytes: info.size });
|
|
@@ -1350,7 +1544,7 @@ async function loadBundleExportView(ctx, params) {
|
|
|
1350
1544
|
runId,
|
|
1351
1545
|
readOnly: true,
|
|
1352
1546
|
redactionProfile: ctx.project.redactionProfile ?? "share",
|
|
1353
|
-
cliHint: `npx agent-inspect bundle ${runId} --profile ${ctx.project.redactionProfile ?? "share"} --dir ${
|
|
1547
|
+
cliHint: `npx agent-inspect bundle ${runId} --profile ${ctx.project.redactionProfile ?? "share"} --dir ${path10.join(ctx.project.workspaceDir, "runs")}`,
|
|
1354
1548
|
note: "Studio does not mutate traces or upload bundles. Run the CLI locally to assemble a share-safe bundle."
|
|
1355
1549
|
};
|
|
1356
1550
|
}
|
|
@@ -1692,7 +1886,6 @@ async function startStudioServer(options = {}) {
|
|
|
1692
1886
|
}
|
|
1693
1887
|
var DEFAULT_GITHUB_TOKEN_ENV = "GITHUB_TOKEN";
|
|
1694
1888
|
var GITHUB_API_BASE = "https://api.github.com";
|
|
1695
|
-
var MAX_ARTIFACT_BYTES = 52428800;
|
|
1696
1889
|
function resolveTokenEnv(registry, override) {
|
|
1697
1890
|
const fromRegistry = registry.ingest?.github?.tokenEnv?.trim();
|
|
1698
1891
|
const envName = override?.trim() || fromRegistry || DEFAULT_GITHUB_TOKEN_ENV;
|
|
@@ -1717,20 +1910,17 @@ function githubHeaders(token) {
|
|
|
1717
1910
|
};
|
|
1718
1911
|
}
|
|
1719
1912
|
async function readResponseBody(response, maxBytes) {
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
if (
|
|
1913
|
+
try {
|
|
1914
|
+
return await readBoundedResponseBody(response, maxBytes);
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
if (error instanceof IngestLimitError && error.code === "INGEST_SIZE_LIMIT") {
|
|
1724
1917
|
throw new Error("artifact exceeds size limit");
|
|
1725
1918
|
}
|
|
1919
|
+
throw error;
|
|
1726
1920
|
}
|
|
1727
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
1728
|
-
if (arrayBuffer.byteLength > maxBytes) {
|
|
1729
|
-
throw new Error("artifact exceeds size limit");
|
|
1730
|
-
}
|
|
1731
|
-
return Buffer.from(arrayBuffer);
|
|
1732
1921
|
}
|
|
1733
1922
|
async function downloadGitHubArtifactArchive(options) {
|
|
1923
|
+
const maxBytes = resolveIngestMaxBytes(options.maxBytes);
|
|
1734
1924
|
const fetchFn = options.fetchImpl ?? fetch;
|
|
1735
1925
|
const { owner, name } = parseGitHubRepo(options.repo);
|
|
1736
1926
|
const runId = options.runId.trim();
|
|
@@ -1756,7 +1946,7 @@ async function downloadGitHubArtifactArchive(options) {
|
|
|
1756
1946
|
if (artifact.expired === true) {
|
|
1757
1947
|
throw new Error(`artifact expired for run ${runId}: ${artifactName}`);
|
|
1758
1948
|
}
|
|
1759
|
-
if (typeof artifact.size_in_bytes === "number" && artifact.size_in_bytes >
|
|
1949
|
+
if (typeof artifact.size_in_bytes === "number" && artifact.size_in_bytes > maxBytes) {
|
|
1760
1950
|
throw new Error("artifact exceeds size limit");
|
|
1761
1951
|
}
|
|
1762
1952
|
const downloadResponse = await fetchFn(artifact.archive_download_url, {
|
|
@@ -1766,7 +1956,7 @@ async function downloadGitHubArtifactArchive(options) {
|
|
|
1766
1956
|
if (!downloadResponse.ok) {
|
|
1767
1957
|
throw new Error(`GitHub artifact download failed (${downloadResponse.status})`);
|
|
1768
1958
|
}
|
|
1769
|
-
return readResponseBody(downloadResponse,
|
|
1959
|
+
return readResponseBody(downloadResponse, maxBytes);
|
|
1770
1960
|
}
|
|
1771
1961
|
async function importGitHubArtifact(options) {
|
|
1772
1962
|
const registryImportWarnings = [];
|
|
@@ -1816,17 +2006,25 @@ async function importGitHubArtifact(options) {
|
|
|
1816
2006
|
const fileName = `${options.artifactName}.zip`;
|
|
1817
2007
|
const destPath = uniqueDestPath(dirs.bundlesDir, fileName, contentHash);
|
|
1818
2008
|
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2009
|
+
try {
|
|
2010
|
+
await withAtomicStagingDir(dirs.bundlesDir, async (stagingDir) => {
|
|
2011
|
+
const stagedPath = path10.join(stagingDir, fileName);
|
|
2012
|
+
await writeFile(stagedPath, archive);
|
|
2013
|
+
await promoteStagingPath(stagedPath, destPath);
|
|
2014
|
+
});
|
|
2015
|
+
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2016
|
+
insertIngestFile(options.db, {
|
|
2017
|
+
sourceKey,
|
|
2018
|
+
sourceName: fileName,
|
|
2019
|
+
destPath,
|
|
2020
|
+
kind: "bundle",
|
|
2021
|
+
contentHash,
|
|
2022
|
+
importedAt
|
|
2023
|
+
});
|
|
2024
|
+
} catch (error) {
|
|
2025
|
+
await rm(destPath, { force: true }).catch(() => void 0);
|
|
2026
|
+
throw error;
|
|
2027
|
+
}
|
|
1830
2028
|
const registryImport = await importStudioRegistry({
|
|
1831
2029
|
db: options.db,
|
|
1832
2030
|
registry: options.registry,
|
|
@@ -1890,7 +2088,7 @@ async function pathExists(filePath) {
|
|
|
1890
2088
|
}
|
|
1891
2089
|
async function validateBundleDirectory(bundleDir) {
|
|
1892
2090
|
const errors = [];
|
|
1893
|
-
const metadataPath =
|
|
2091
|
+
const metadataPath = path10.join(bundleDir, "metadata.json");
|
|
1894
2092
|
if (!await pathExists(metadataPath)) {
|
|
1895
2093
|
errors.push("bundle missing metadata.json");
|
|
1896
2094
|
return errors;
|
|
@@ -1916,11 +2114,18 @@ async function hashBundleContents(bundleDir) {
|
|
|
1916
2114
|
(a, b) => a.name.localeCompare(b.name)
|
|
1917
2115
|
);
|
|
1918
2116
|
for (const entry of entries) {
|
|
1919
|
-
const abs =
|
|
2117
|
+
const abs = path10.join(dir, entry.name);
|
|
1920
2118
|
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
1921
|
-
|
|
2119
|
+
const info = await lstat(abs);
|
|
2120
|
+
if (info.isSymbolicLink()) {
|
|
2121
|
+
throw new IngestLimitError(
|
|
2122
|
+
"INGEST_SYMLINK_REJECTED",
|
|
2123
|
+
"symbolic links are not allowed for ingest"
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
if (info.isDirectory()) {
|
|
1922
2127
|
await walk(abs, rel);
|
|
1923
|
-
} else if (
|
|
2128
|
+
} else if (info.isFile()) {
|
|
1924
2129
|
hash.update(rel);
|
|
1925
2130
|
hash.update("\0");
|
|
1926
2131
|
hash.update(await readFile(abs));
|
|
@@ -1935,11 +2140,18 @@ async function copyDirectoryRecursive(sourceDir, destDir) {
|
|
|
1935
2140
|
await mkdir(destDir, { recursive: true });
|
|
1936
2141
|
const entries = await readdir(sourceDir, { withFileTypes: true });
|
|
1937
2142
|
for (const entry of entries) {
|
|
1938
|
-
const from =
|
|
1939
|
-
const to =
|
|
1940
|
-
|
|
2143
|
+
const from = path10.join(sourceDir, entry.name);
|
|
2144
|
+
const to = path10.join(destDir, entry.name);
|
|
2145
|
+
const info = await lstat(from);
|
|
2146
|
+
if (info.isSymbolicLink()) {
|
|
2147
|
+
throw new IngestLimitError(
|
|
2148
|
+
"INGEST_SYMLINK_REJECTED",
|
|
2149
|
+
"symbolic links are not allowed for ingest"
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
if (info.isDirectory()) {
|
|
1941
2153
|
await copyDirectoryRecursive(from, to);
|
|
1942
|
-
} else if (
|
|
2154
|
+
} else if (info.isFile()) {
|
|
1943
2155
|
await copyFile(from, to);
|
|
1944
2156
|
}
|
|
1945
2157
|
}
|
|
@@ -1956,11 +2168,18 @@ async function importBundleUpload(options) {
|
|
|
1956
2168
|
registryImportWarnings
|
|
1957
2169
|
};
|
|
1958
2170
|
}
|
|
1959
|
-
const bundlePath =
|
|
1960
|
-
let bundleStat;
|
|
2171
|
+
const bundlePath = path10.resolve(options.bundlePath);
|
|
1961
2172
|
try {
|
|
1962
|
-
|
|
1963
|
-
} catch {
|
|
2173
|
+
await lstatRegularDirectory(bundlePath);
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
if (error instanceof IngestLimitError) {
|
|
2176
|
+
return {
|
|
2177
|
+
skipped: false,
|
|
2178
|
+
imported: false,
|
|
2179
|
+
errors: [error.message],
|
|
2180
|
+
registryImportWarnings
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
1964
2183
|
return {
|
|
1965
2184
|
skipped: false,
|
|
1966
2185
|
imported: false,
|
|
@@ -1968,24 +2187,42 @@ async function importBundleUpload(options) {
|
|
|
1968
2187
|
registryImportWarnings
|
|
1969
2188
|
};
|
|
1970
2189
|
}
|
|
1971
|
-
|
|
2190
|
+
const validationErrors = await validateBundleDirectory(bundlePath);
|
|
2191
|
+
if (validationErrors.length > 0) {
|
|
1972
2192
|
return {
|
|
1973
2193
|
skipped: false,
|
|
1974
2194
|
imported: false,
|
|
1975
|
-
errors:
|
|
2195
|
+
errors: validationErrors,
|
|
1976
2196
|
registryImportWarnings
|
|
1977
2197
|
};
|
|
1978
2198
|
}
|
|
1979
|
-
|
|
1980
|
-
|
|
2199
|
+
let maxBytes;
|
|
2200
|
+
try {
|
|
2201
|
+
maxBytes = resolveIngestMaxBytes(
|
|
2202
|
+
options.maxBytes ?? options.registry.ingest?.bundleUpload?.maxBytes
|
|
2203
|
+
);
|
|
2204
|
+
await measureDirectoryBytes(bundlePath, maxBytes);
|
|
2205
|
+
} catch (error) {
|
|
2206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1981
2207
|
return {
|
|
1982
2208
|
skipped: false,
|
|
1983
2209
|
imported: false,
|
|
1984
|
-
errors:
|
|
2210
|
+
errors: [message],
|
|
2211
|
+
registryImportWarnings
|
|
2212
|
+
};
|
|
2213
|
+
}
|
|
2214
|
+
let contentHash;
|
|
2215
|
+
try {
|
|
2216
|
+
contentHash = await hashBundleContents(bundlePath);
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2219
|
+
return {
|
|
2220
|
+
skipped: false,
|
|
2221
|
+
imported: false,
|
|
2222
|
+
errors: [message],
|
|
1985
2223
|
registryImportWarnings
|
|
1986
2224
|
};
|
|
1987
2225
|
}
|
|
1988
|
-
const contentHash = await hashBundleContents(bundlePath);
|
|
1989
2226
|
const sourceKey = `bundle:${bundlePath}`;
|
|
1990
2227
|
const existing = findIngestFileBySourceKey(options.db, sourceKey);
|
|
1991
2228
|
if (existing && existing.contentHash === contentHash) {
|
|
@@ -1999,11 +2236,15 @@ async function importBundleUpload(options) {
|
|
|
1999
2236
|
};
|
|
2000
2237
|
}
|
|
2001
2238
|
const dirs = resolveImportDirs(options.registryPath, options.registry);
|
|
2002
|
-
const folderName =
|
|
2239
|
+
const folderName = path10.basename(bundlePath);
|
|
2003
2240
|
const destPath = uniqueDestPath(dirs.bundlesDir, folderName, contentHash);
|
|
2004
2241
|
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
2005
2242
|
try {
|
|
2006
|
-
await
|
|
2243
|
+
await withAtomicStagingDir(dirs.bundlesDir, async (stagingDir) => {
|
|
2244
|
+
const stagedDest = path10.join(stagingDir, folderName);
|
|
2245
|
+
await copyDirectoryRecursive(bundlePath, stagedDest);
|
|
2246
|
+
await promoteStagingPath(stagedDest, destPath);
|
|
2247
|
+
});
|
|
2007
2248
|
insertIngestFile(options.db, {
|
|
2008
2249
|
sourceKey,
|
|
2009
2250
|
sourceName: folderName,
|
|
@@ -2027,6 +2268,7 @@ async function importBundleUpload(options) {
|
|
|
2027
2268
|
registryImportWarnings
|
|
2028
2269
|
};
|
|
2029
2270
|
} catch (error) {
|
|
2271
|
+
await rm(destPath, { recursive: true, force: true }).catch(() => void 0);
|
|
2030
2272
|
const message = error instanceof Error ? error.message : String(error);
|
|
2031
2273
|
return {
|
|
2032
2274
|
skipped: false,
|