@agent-inspect/studio 6.17.5 → 6.17.7
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.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var promises = require('fs/promises');
|
|
4
|
-
var
|
|
4
|
+
var path10 = require('path');
|
|
5
5
|
var advanced = require('agent-inspect/advanced');
|
|
6
6
|
var workspace = require('agent-inspect/workspace');
|
|
7
7
|
var fs = require('fs');
|
|
@@ -16,7 +16,7 @@ var readers = require('agent-inspect/readers');
|
|
|
16
16
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
17
17
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
18
18
|
|
|
19
|
-
var
|
|
19
|
+
var path10__default = /*#__PURE__*/_interopDefault(path10);
|
|
20
20
|
|
|
21
21
|
// packages/studio/src/registry.ts
|
|
22
22
|
function isSafeRelativePath(p) {
|
|
@@ -26,14 +26,14 @@ function isSafeRelativePath(p) {
|
|
|
26
26
|
return !trimmed.split(/[/\\]+/).some((seg) => seg === "..");
|
|
27
27
|
}
|
|
28
28
|
function resolveUnderRoot(root, ...segments) {
|
|
29
|
-
const resolvedRoot =
|
|
30
|
-
const resolved =
|
|
29
|
+
const resolvedRoot = path10__default.default.resolve(root);
|
|
30
|
+
const resolved = path10__default.default.resolve(resolvedRoot, ...segments);
|
|
31
31
|
assertPathUnderRoot(resolved, resolvedRoot);
|
|
32
32
|
return resolved;
|
|
33
33
|
}
|
|
34
34
|
function assertPathUnderRoot(resolved, root) {
|
|
35
|
-
const rel =
|
|
36
|
-
if (rel.startsWith("..") ||
|
|
35
|
+
const rel = path10__default.default.relative(path10__default.default.resolve(root), path10__default.default.resolve(resolved));
|
|
36
|
+
if (rel.startsWith("..") || path10__default.default.isAbsolute(rel)) {
|
|
37
37
|
throw new Error("path escapes allowed registry root");
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -214,7 +214,23 @@ function parseStudioRegistry(input) {
|
|
|
214
214
|
ingestConfig.bundleUpload = bundleUpload;
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
if (input.ingest.fileDrop !== void 0) {
|
|
218
|
+
if (!isPlainObject(input.ingest.fileDrop)) {
|
|
219
|
+
errors.push("ingest.fileDrop must be an object");
|
|
220
|
+
} else {
|
|
221
|
+
const fileDrop = {};
|
|
222
|
+
if (input.ingest.fileDrop.maxBytes !== void 0) {
|
|
223
|
+
const maxBytes = Number(input.ingest.fileDrop.maxBytes);
|
|
224
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
|
|
225
|
+
errors.push("ingest.fileDrop.maxBytes must be a positive integer");
|
|
226
|
+
} else {
|
|
227
|
+
fileDrop.maxBytes = maxBytes;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
ingestConfig.fileDrop = fileDrop;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const knownIngestKeys = /* @__PURE__ */ new Set(["github", "http", "bundleUpload", "fileDrop"]);
|
|
218
234
|
for (const key of Object.keys(input.ingest)) {
|
|
219
235
|
if (!knownIngestKeys.has(key)) {
|
|
220
236
|
ingestWarnings.push(`ignored unknown ingest key: ${key}`);
|
|
@@ -255,7 +271,7 @@ async function readStudioRegistryFile(filePath) {
|
|
|
255
271
|
}
|
|
256
272
|
}
|
|
257
273
|
function resolveRegistryProjectPath(registryDir, projectPath) {
|
|
258
|
-
return
|
|
274
|
+
return path10__default.default.isAbsolute(projectPath) ? path10__default.default.resolve(projectPath) : path10__default.default.resolve(registryDir, projectPath);
|
|
259
275
|
}
|
|
260
276
|
var cached;
|
|
261
277
|
function loadBetterSqlite3() {
|
|
@@ -320,9 +336,9 @@ function resolveStudioDbPath(options) {
|
|
|
320
336
|
if (raw.startsWith("postgres://") || raw.startsWith("postgresql://")) {
|
|
321
337
|
return raw;
|
|
322
338
|
}
|
|
323
|
-
return
|
|
339
|
+
return path10__default.default.resolve(options.cwd ?? process.cwd(), raw);
|
|
324
340
|
}
|
|
325
|
-
return
|
|
341
|
+
return path10__default.default.resolve(
|
|
326
342
|
options.cwd ?? process.cwd(),
|
|
327
343
|
".agent-inspect",
|
|
328
344
|
DEFAULT_STUDIO_DB_FILENAME
|
|
@@ -337,7 +353,7 @@ function openStudioDb(dbPath) {
|
|
|
337
353
|
"Postgres studio databases are not supported; use a SQLite file path (preview only)."
|
|
338
354
|
);
|
|
339
355
|
}
|
|
340
|
-
const dir =
|
|
356
|
+
const dir = path10__default.default.dirname(dbPath);
|
|
341
357
|
try {
|
|
342
358
|
fs.mkdirSync(dir, { recursive: true });
|
|
343
359
|
} catch (error) {
|
|
@@ -450,14 +466,14 @@ function insertIngestFile(db, row) {
|
|
|
450
466
|
// packages/studio/src/import.ts
|
|
451
467
|
async function discoverSuiteConfigs(projectRoot, configured) {
|
|
452
468
|
if (configured && configured.length > 0) {
|
|
453
|
-
return configured.map((rel) =>
|
|
469
|
+
return configured.map((rel) => path10__default.default.resolve(projectRoot, rel));
|
|
454
470
|
}
|
|
455
471
|
const found = [];
|
|
456
472
|
try {
|
|
457
473
|
const entries = await promises.readdir(projectRoot);
|
|
458
474
|
for (const entry of entries) {
|
|
459
475
|
if (entry.endsWith(".suite.json")) {
|
|
460
|
-
found.push(
|
|
476
|
+
found.push(path10__default.default.join(projectRoot, entry));
|
|
461
477
|
}
|
|
462
478
|
}
|
|
463
479
|
} catch {
|
|
@@ -467,7 +483,7 @@ async function discoverSuiteConfigs(projectRoot, configured) {
|
|
|
467
483
|
async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
468
484
|
const runs = [];
|
|
469
485
|
for (const rel of traceDirs) {
|
|
470
|
-
const traceDir = advanced.resolveTraceDir({ dir:
|
|
486
|
+
const traceDir = advanced.resolveTraceDir({ dir: path10__default.default.join(workspaceDir, rel) });
|
|
471
487
|
const td = new advanced.TraceDirectory({ dir: traceDir });
|
|
472
488
|
const files = await td.list();
|
|
473
489
|
const metas = await advanced.loadTraceMetadataList(
|
|
@@ -481,7 +497,7 @@ async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
|
481
497
|
runId: meta.runId,
|
|
482
498
|
...meta.name !== void 0 ? { name: meta.name } : {},
|
|
483
499
|
status: meta.status,
|
|
484
|
-
file:
|
|
500
|
+
file: path10__default.default.basename(meta.filePath),
|
|
485
501
|
...meta.startedAt !== void 0 ? { startedAt: meta.startedAt } : {},
|
|
486
502
|
...meta.durationMs !== void 0 ? { durationMs: meta.durationMs } : {}
|
|
487
503
|
});
|
|
@@ -490,7 +506,7 @@ async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
|
490
506
|
return runs.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
|
|
491
507
|
}
|
|
492
508
|
async function importStudioRegistry(options) {
|
|
493
|
-
const registryDir =
|
|
509
|
+
const registryDir = path10__default.default.dirname(options.registryPath);
|
|
494
510
|
const warnings = [];
|
|
495
511
|
const projects = [];
|
|
496
512
|
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -551,21 +567,21 @@ async function importStudioProject(options) {
|
|
|
551
567
|
}
|
|
552
568
|
async function resolveStudioRegistryPath(options) {
|
|
553
569
|
if (options.workspacePath && options.workspacePath.trim() !== "") {
|
|
554
|
-
return
|
|
570
|
+
return path10__default.default.resolve(options.cwd ?? process.cwd(), options.workspacePath);
|
|
555
571
|
}
|
|
556
|
-
const cwd =
|
|
572
|
+
const cwd = path10__default.default.resolve(options.cwd ?? process.cwd());
|
|
557
573
|
for (const rel of STUDIO_REGISTRY_FILENAMES) {
|
|
558
|
-
const candidate =
|
|
574
|
+
const candidate = path10__default.default.join(cwd, rel);
|
|
559
575
|
try {
|
|
560
576
|
await promises.access(candidate);
|
|
561
577
|
return candidate;
|
|
562
578
|
} catch {
|
|
563
579
|
}
|
|
564
580
|
}
|
|
565
|
-
return
|
|
581
|
+
return path10__default.default.join(cwd, STUDIO_REGISTRY_FILENAMES[0]);
|
|
566
582
|
}
|
|
567
583
|
function resolveImportDirs(registryPath, registry) {
|
|
568
|
-
const registryDir =
|
|
584
|
+
const registryDir = path10__default.default.dirname(registryPath);
|
|
569
585
|
const importConfig = registry.import ?? {};
|
|
570
586
|
const fileDropDir = importConfig.fileDropDir ?? "imports/drop";
|
|
571
587
|
const ciArtifactsDir = importConfig.ciArtifactsDir ?? "imports/ci";
|
|
@@ -578,10 +594,10 @@ function resolveImportDirs(registryPath, registry) {
|
|
|
578
594
|
};
|
|
579
595
|
}
|
|
580
596
|
function uniqueDestPath(destDir, fileName, contentHash) {
|
|
581
|
-
const ext =
|
|
582
|
-
const base =
|
|
597
|
+
const ext = path10__default.default.extname(fileName);
|
|
598
|
+
const base = path10__default.default.basename(fileName, ext);
|
|
583
599
|
const shortHash = contentHash.slice(0, 8);
|
|
584
|
-
return
|
|
600
|
+
return path10__default.default.join(destDir, `${base}-${shortHash}${ext}`);
|
|
585
601
|
}
|
|
586
602
|
function sanitizeSafeErrorMessage(message, secret) {
|
|
587
603
|
if (!secret || secret.length < 4) return message;
|
|
@@ -598,6 +614,136 @@ function parseGitHubRepo(repo) {
|
|
|
598
614
|
function buildGitHubArtifactSourceKey(options) {
|
|
599
615
|
return `github:${options.owner}/${options.repo}/runs/${options.runId}/${options.artifactName}`;
|
|
600
616
|
}
|
|
617
|
+
var DEFAULT_MAX_INGEST_BYTES = 52428800;
|
|
618
|
+
var IngestLimitError = class extends Error {
|
|
619
|
+
code;
|
|
620
|
+
constructor(code, message) {
|
|
621
|
+
super(message);
|
|
622
|
+
this.name = "IngestLimitError";
|
|
623
|
+
this.code = code;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
function resolveIngestMaxBytes(configured) {
|
|
627
|
+
if (configured === void 0) return DEFAULT_MAX_INGEST_BYTES;
|
|
628
|
+
if (!Number.isInteger(configured) || configured <= 0) {
|
|
629
|
+
throw new IngestLimitError(
|
|
630
|
+
"INGEST_SIZE_LIMIT",
|
|
631
|
+
"maxBytes must be a positive integer"
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
return configured;
|
|
635
|
+
}
|
|
636
|
+
async function lstatRegularFile(filePath) {
|
|
637
|
+
const info = await promises.lstat(filePath);
|
|
638
|
+
if (info.isSymbolicLink()) {
|
|
639
|
+
throw new IngestLimitError(
|
|
640
|
+
"INGEST_SYMLINK_REJECTED",
|
|
641
|
+
"symbolic links are not allowed for ingest"
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
if (!info.isFile()) {
|
|
645
|
+
throw new IngestLimitError("INGEST_NOT_A_FILE", "ingest path must be a regular file");
|
|
646
|
+
}
|
|
647
|
+
return info;
|
|
648
|
+
}
|
|
649
|
+
async function lstatRegularDirectory(dirPath) {
|
|
650
|
+
const info = await promises.lstat(dirPath);
|
|
651
|
+
if (info.isSymbolicLink()) {
|
|
652
|
+
throw new IngestLimitError(
|
|
653
|
+
"INGEST_SYMLINK_REJECTED",
|
|
654
|
+
"symbolic links are not allowed for ingest"
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (!info.isDirectory()) {
|
|
658
|
+
throw new IngestLimitError(
|
|
659
|
+
"INGEST_NOT_A_DIRECTORY",
|
|
660
|
+
"ingest path must be a directory"
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
return info;
|
|
664
|
+
}
|
|
665
|
+
async function assertFileWithinByteLimit(filePath, maxBytes) {
|
|
666
|
+
const info = await lstatRegularFile(filePath);
|
|
667
|
+
if (info.size > maxBytes) {
|
|
668
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "file exceeds size limit");
|
|
669
|
+
}
|
|
670
|
+
return info;
|
|
671
|
+
}
|
|
672
|
+
async function measureDirectoryBytes(dirPath, maxBytes) {
|
|
673
|
+
await lstatRegularDirectory(dirPath);
|
|
674
|
+
let total = 0;
|
|
675
|
+
const walk = async (current) => {
|
|
676
|
+
const entries = await promises.readdir(current, { withFileTypes: true });
|
|
677
|
+
for (const entry of entries) {
|
|
678
|
+
const abs = path10__default.default.join(current, entry.name);
|
|
679
|
+
const info = await promises.lstat(abs);
|
|
680
|
+
if (info.isSymbolicLink()) {
|
|
681
|
+
throw new IngestLimitError(
|
|
682
|
+
"INGEST_SYMLINK_REJECTED",
|
|
683
|
+
"symbolic links are not allowed for ingest"
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (info.isDirectory()) {
|
|
687
|
+
await walk(abs);
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (!info.isFile()) continue;
|
|
691
|
+
total += info.size;
|
|
692
|
+
if (total > maxBytes) {
|
|
693
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "directory exceeds size limit");
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
await walk(dirPath);
|
|
698
|
+
return total;
|
|
699
|
+
}
|
|
700
|
+
async function readBoundedResponseBody(response, maxBytes) {
|
|
701
|
+
const lengthHeader = response.headers.get("content-length");
|
|
702
|
+
if (lengthHeader) {
|
|
703
|
+
const length = Number(lengthHeader);
|
|
704
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
705
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "response exceeds size limit");
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (!response.body) {
|
|
709
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
710
|
+
if (arrayBuffer.byteLength > maxBytes) {
|
|
711
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "response exceeds size limit");
|
|
712
|
+
}
|
|
713
|
+
return Buffer.from(arrayBuffer);
|
|
714
|
+
}
|
|
715
|
+
const reader = response.body.getReader();
|
|
716
|
+
const chunks = [];
|
|
717
|
+
let total = 0;
|
|
718
|
+
for (; ; ) {
|
|
719
|
+
const { done, value } = await reader.read();
|
|
720
|
+
if (done) break;
|
|
721
|
+
if (!value || value.byteLength === 0) continue;
|
|
722
|
+
total += value.byteLength;
|
|
723
|
+
if (total > maxBytes) {
|
|
724
|
+
try {
|
|
725
|
+
await reader.cancel();
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
throw new IngestLimitError("INGEST_SIZE_LIMIT", "response exceeds size limit");
|
|
729
|
+
}
|
|
730
|
+
chunks.push(Buffer.from(value));
|
|
731
|
+
}
|
|
732
|
+
return Buffer.concat(chunks);
|
|
733
|
+
}
|
|
734
|
+
async function withAtomicStagingDir(parentDir, work) {
|
|
735
|
+
await promises.mkdir(parentDir, { recursive: true });
|
|
736
|
+
const stagingDir = await promises.mkdtemp(path10__default.default.join(parentDir, ".ingest-staging-"));
|
|
737
|
+
try {
|
|
738
|
+
return await work(stagingDir);
|
|
739
|
+
} finally {
|
|
740
|
+
await promises.rm(stagingDir, { recursive: true, force: true });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async function promoteStagingPath(stagingPath, finalPath) {
|
|
744
|
+
await promises.mkdir(path10__default.default.dirname(finalPath), { recursive: true });
|
|
745
|
+
await promises.rename(stagingPath, finalPath);
|
|
746
|
+
}
|
|
601
747
|
|
|
602
748
|
// packages/studio/src/ingest/file-drop.ts
|
|
603
749
|
var FILE_DROP_ARCHIVE_DIR = ".imported";
|
|
@@ -610,26 +756,38 @@ function classifyFile(fileName) {
|
|
|
610
756
|
return void 0;
|
|
611
757
|
}
|
|
612
758
|
async function hashFile(filePath) {
|
|
613
|
-
const
|
|
614
|
-
|
|
759
|
+
const hash = crypto.createHash("sha256");
|
|
760
|
+
const stream = fs.createReadStream(filePath);
|
|
761
|
+
for await (const chunk of stream) {
|
|
762
|
+
hash.update(chunk);
|
|
763
|
+
}
|
|
764
|
+
return hash.digest("hex");
|
|
615
765
|
}
|
|
616
766
|
async function importOneFile(options) {
|
|
617
767
|
const destPath = uniqueDestPath(options.destDir, options.fileName, options.contentHash);
|
|
618
|
-
assertPathUnderRoot(destPath,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
768
|
+
assertPathUnderRoot(destPath, path10__default.default.dirname(options.destDir));
|
|
769
|
+
try {
|
|
770
|
+
await withAtomicStagingDir(options.destDir, async (stagingDir) => {
|
|
771
|
+
const stagedPath = path10__default.default.join(stagingDir, options.fileName);
|
|
772
|
+
await promises.copyFile(options.sourcePath, stagedPath);
|
|
773
|
+
await promoteStagingPath(stagedPath, destPath);
|
|
774
|
+
});
|
|
775
|
+
insertIngestFile(options.db, {
|
|
776
|
+
sourceKey: options.sourceKey,
|
|
777
|
+
sourceName: options.fileName,
|
|
778
|
+
destPath,
|
|
779
|
+
kind: options.kind,
|
|
780
|
+
contentHash: options.contentHash,
|
|
781
|
+
importedAt: options.importedAt
|
|
782
|
+
});
|
|
783
|
+
} catch (error) {
|
|
784
|
+
await promises.rm(destPath, { force: true }).catch(() => void 0);
|
|
785
|
+
throw error;
|
|
786
|
+
}
|
|
629
787
|
let archived = false;
|
|
630
788
|
if (options.archiveAfterImport) {
|
|
631
789
|
await promises.mkdir(options.archiveDir, { recursive: true });
|
|
632
|
-
const archiveTarget =
|
|
790
|
+
const archiveTarget = path10__default.default.join(options.archiveDir, options.fileName);
|
|
633
791
|
await promises.rename(options.sourcePath, archiveTarget);
|
|
634
792
|
archived = true;
|
|
635
793
|
}
|
|
@@ -658,10 +816,27 @@ async function importFileDrop(options) {
|
|
|
658
816
|
files
|
|
659
817
|
};
|
|
660
818
|
}
|
|
819
|
+
let maxBytes;
|
|
820
|
+
try {
|
|
821
|
+
maxBytes = resolveIngestMaxBytes(
|
|
822
|
+
options.maxBytes ?? options.registry.ingest?.fileDrop?.maxBytes
|
|
823
|
+
);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
826
|
+
return {
|
|
827
|
+
skipped: false,
|
|
828
|
+
scanned: 0,
|
|
829
|
+
imported: 0,
|
|
830
|
+
skippedFiles: 0,
|
|
831
|
+
errors: [message],
|
|
832
|
+
warnings,
|
|
833
|
+
files
|
|
834
|
+
};
|
|
835
|
+
}
|
|
661
836
|
const dirs = resolveImportDirs(options.registryPath, options.registry);
|
|
662
837
|
let dropDir;
|
|
663
838
|
try {
|
|
664
|
-
dropDir = options.dropDir ?
|
|
839
|
+
dropDir = options.dropDir ? path10__default.default.isAbsolute(options.dropDir) ? (assertPathUnderRoot(options.dropDir, dirs.registryDir), options.dropDir) : resolveUnderRoot(dirs.registryDir, options.dropDir) : dirs.fileDropDir;
|
|
665
840
|
assertPathUnderRoot(dropDir, dirs.registryDir);
|
|
666
841
|
} catch (error) {
|
|
667
842
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -691,24 +866,35 @@ async function importFileDrop(options) {
|
|
|
691
866
|
};
|
|
692
867
|
}
|
|
693
868
|
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
694
|
-
const archiveDir =
|
|
869
|
+
const archiveDir = path10__default.default.join(dropDir, FILE_DROP_ARCHIVE_DIR);
|
|
695
870
|
let scanned = 0;
|
|
696
871
|
let imported = 0;
|
|
697
872
|
let skippedFiles = 0;
|
|
698
873
|
for (const entry of entries.sort()) {
|
|
699
874
|
if (entry === FILE_DROP_ARCHIVE_DIR || entry.startsWith(".")) continue;
|
|
700
|
-
const sourcePath =
|
|
701
|
-
|
|
875
|
+
const sourcePath = path10__default.default.join(dropDir, entry);
|
|
876
|
+
const kind = classifyFile(entry);
|
|
877
|
+
if (!kind) continue;
|
|
878
|
+
scanned += 1;
|
|
702
879
|
try {
|
|
703
|
-
|
|
704
|
-
} catch {
|
|
880
|
+
await assertFileWithinByteLimit(sourcePath, maxBytes);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (error instanceof IngestLimitError && error.code === "INGEST_NOT_A_FILE") {
|
|
883
|
+
scanned -= 1;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (error instanceof IngestLimitError && error.code === "INGEST_SYMLINK_REJECTED") {
|
|
887
|
+
errors.push(`failed to import ${entry}: ${error.message}`);
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (error instanceof IngestLimitError && error.code === "INGEST_SIZE_LIMIT") {
|
|
891
|
+
errors.push(`failed to import ${entry}: ${error.message}`);
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
705
894
|
warnings.push(`skipped unreadable entry: ${entry}`);
|
|
895
|
+
scanned -= 1;
|
|
706
896
|
continue;
|
|
707
897
|
}
|
|
708
|
-
if (!fileStat.isFile()) continue;
|
|
709
|
-
const kind = classifyFile(entry);
|
|
710
|
-
if (!kind) continue;
|
|
711
|
-
scanned += 1;
|
|
712
898
|
const sourceKey = entry;
|
|
713
899
|
let contentHash;
|
|
714
900
|
try {
|
|
@@ -761,7 +947,8 @@ async function importFileDropFromRegistry(options) {
|
|
|
761
947
|
registry: options.registry,
|
|
762
948
|
enabled: options.enabled,
|
|
763
949
|
...options.dropDir !== void 0 ? { dropDir: options.dropDir } : {},
|
|
764
|
-
...options.archiveAfterImport !== void 0 ? { archiveAfterImport: options.archiveAfterImport } : {}
|
|
950
|
+
...options.archiveAfterImport !== void 0 ? { archiveAfterImport: options.archiveAfterImport } : {},
|
|
951
|
+
...options.maxBytes !== void 0 ? { maxBytes: options.maxBytes } : {}
|
|
765
952
|
});
|
|
766
953
|
}
|
|
767
954
|
async function runStudioFileDropImport(options) {
|
|
@@ -878,7 +1065,6 @@ function isIngestTokenValid(provided, expected) {
|
|
|
878
1065
|
var DEFAULT_HTTP_INGEST_BASE_PATH = "/api/ingest";
|
|
879
1066
|
var HTTP_INGEST_BUNDLE_PATH = "/api/ingest/bundle";
|
|
880
1067
|
var HTTP_INGEST_ARTIFACT_PATH = "/api/ingest/artifact";
|
|
881
|
-
var DEFAULT_MAX_INGEST_BYTES = 52428800;
|
|
882
1068
|
function resolveHttpIngestConfig(options, registryHttp) {
|
|
883
1069
|
const http = registryHttp ?? options.context?.registry.ingest?.http;
|
|
884
1070
|
const enabled = options.ingestHttp === true || http?.enabled === true;
|
|
@@ -887,7 +1073,7 @@ function resolveHttpIngestConfig(options, registryHttp) {
|
|
|
887
1073
|
...options.ingestTokenEnv !== void 0 ? { tokenEnv: options.ingestTokenEnv } : {},
|
|
888
1074
|
...http?.tokenEnv !== void 0 ? { registryTokenEnv: http.tokenEnv } : {}
|
|
889
1075
|
});
|
|
890
|
-
const maxBytes = http?.maxBytes
|
|
1076
|
+
const maxBytes = resolveIngestMaxBytes(http?.maxBytes);
|
|
891
1077
|
return { enabled, basePath, tokenEnv, maxBytes };
|
|
892
1078
|
}
|
|
893
1079
|
function sendJson(res, status, body) {
|
|
@@ -957,22 +1143,30 @@ async function handleHttpIngestRequest(req, res, ctx, options, pathname) {
|
|
|
957
1143
|
const destDir = isBundle ? dirs.bundlesDir : dirs.ciArtifactsDir;
|
|
958
1144
|
const destPath = uniqueDestPath(destDir, fileName, contentHash);
|
|
959
1145
|
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1146
|
+
try {
|
|
1147
|
+
await withAtomicStagingDir(destDir, async (stagingDir) => {
|
|
1148
|
+
const stagedPath = path10__default.default.join(stagingDir, fileName);
|
|
1149
|
+
await promises.writeFile(stagedPath, body);
|
|
1150
|
+
await promoteStagingPath(stagedPath, destPath);
|
|
1151
|
+
});
|
|
1152
|
+
const sourceKey = isBundle ? `http:bundle:${contentHash}` : buildGitHubArtifactSourceKey({
|
|
1153
|
+
owner: "http",
|
|
1154
|
+
repo: "ingest",
|
|
1155
|
+
runId: importedAt,
|
|
1156
|
+
artifactName: fileName
|
|
1157
|
+
});
|
|
1158
|
+
insertIngestFile(ctx.db, {
|
|
1159
|
+
sourceKey,
|
|
1160
|
+
sourceName: fileName,
|
|
1161
|
+
destPath,
|
|
1162
|
+
kind: isBundle ? "bundle" : "ci",
|
|
1163
|
+
contentHash,
|
|
1164
|
+
importedAt
|
|
1165
|
+
});
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
await promises.rm(destPath, { force: true }).catch(() => void 0);
|
|
1168
|
+
throw error;
|
|
1169
|
+
}
|
|
976
1170
|
const registryImport = await importStudioRegistry({
|
|
977
1171
|
db: ctx.db,
|
|
978
1172
|
registry: ctx.registry,
|
|
@@ -1157,7 +1351,7 @@ function getImportedProject(db, projects, projectId) {
|
|
|
1157
1351
|
async function loadTraceDirMetas(workspaceDir, traceDirs) {
|
|
1158
1352
|
const metas = [];
|
|
1159
1353
|
for (const rel of traceDirs) {
|
|
1160
|
-
const traceDir = advanced.resolveTraceDir({ dir:
|
|
1354
|
+
const traceDir = advanced.resolveTraceDir({ dir: path10__default.default.join(workspaceDir, rel) });
|
|
1161
1355
|
const td = new advanced.TraceDirectory({ dir: traceDir });
|
|
1162
1356
|
const files = await td.list();
|
|
1163
1357
|
const listed = await advanced.loadTraceMetadataList(
|
|
@@ -1186,7 +1380,7 @@ async function loadProjectSuitesView(ctx) {
|
|
|
1186
1380
|
try {
|
|
1187
1381
|
const result = await advanced.runSuite({
|
|
1188
1382
|
configPath,
|
|
1189
|
-
cwd:
|
|
1383
|
+
cwd: path10__default.default.dirname(configPath)
|
|
1190
1384
|
});
|
|
1191
1385
|
suites.push({
|
|
1192
1386
|
suiteName: result.suiteName,
|
|
@@ -1204,7 +1398,7 @@ async function loadProjectSuitesView(ctx) {
|
|
|
1204
1398
|
} catch (error) {
|
|
1205
1399
|
const message = error instanceof Error ? error.message : String(error);
|
|
1206
1400
|
suites.push({
|
|
1207
|
-
suiteName:
|
|
1401
|
+
suiteName: path10__default.default.basename(configPath),
|
|
1208
1402
|
configPath,
|
|
1209
1403
|
ok: false,
|
|
1210
1404
|
status: "error",
|
|
@@ -1253,7 +1447,7 @@ async function loadProjectSearchView(ctx, db, params) {
|
|
|
1253
1447
|
}
|
|
1254
1448
|
const metas = await loadTraceDirMetas(ctx.project.workspaceDir, ["runs"]);
|
|
1255
1449
|
const traceDir = advanced.resolveTraceDir({
|
|
1256
|
-
dir:
|
|
1450
|
+
dir: path10__default.default.join(ctx.project.workspaceDir, "runs")
|
|
1257
1451
|
});
|
|
1258
1452
|
const results = await advanced.searchTraces(metas, {
|
|
1259
1453
|
traceDir,
|
|
@@ -1297,12 +1491,12 @@ async function loadProjectDiffView(ctx, params) {
|
|
|
1297
1491
|
};
|
|
1298
1492
|
}
|
|
1299
1493
|
async function loadProjectReportsView(ctx) {
|
|
1300
|
-
const reportsDir =
|
|
1494
|
+
const reportsDir = path10__default.default.join(ctx.project.workspaceDir, "reports");
|
|
1301
1495
|
const reports = [];
|
|
1302
1496
|
try {
|
|
1303
1497
|
const files = await promises.readdir(reportsDir);
|
|
1304
1498
|
for (const file of files) {
|
|
1305
|
-
const filePath =
|
|
1499
|
+
const filePath = path10__default.default.join(reportsDir, file);
|
|
1306
1500
|
const info = await promises.stat(filePath);
|
|
1307
1501
|
if (!info.isFile()) continue;
|
|
1308
1502
|
reports.push({ name: file, path: filePath, sizeBytes: info.size });
|
|
@@ -1357,7 +1551,7 @@ async function loadBundleExportView(ctx, params) {
|
|
|
1357
1551
|
runId,
|
|
1358
1552
|
readOnly: true,
|
|
1359
1553
|
redactionProfile: ctx.project.redactionProfile ?? "share",
|
|
1360
|
-
cliHint: `npx agent-inspect bundle ${runId} --profile ${ctx.project.redactionProfile ?? "share"} --dir ${
|
|
1554
|
+
cliHint: `npx agent-inspect bundle ${runId} --profile ${ctx.project.redactionProfile ?? "share"} --dir ${path10__default.default.join(ctx.project.workspaceDir, "runs")}`,
|
|
1361
1555
|
note: "Studio does not mutate traces or upload bundles. Run the CLI locally to assemble a share-safe bundle."
|
|
1362
1556
|
};
|
|
1363
1557
|
}
|
|
@@ -1699,7 +1893,6 @@ async function startStudioServer(options = {}) {
|
|
|
1699
1893
|
}
|
|
1700
1894
|
var DEFAULT_GITHUB_TOKEN_ENV = "GITHUB_TOKEN";
|
|
1701
1895
|
var GITHUB_API_BASE = "https://api.github.com";
|
|
1702
|
-
var MAX_ARTIFACT_BYTES = 52428800;
|
|
1703
1896
|
function resolveTokenEnv(registry, override) {
|
|
1704
1897
|
const fromRegistry = registry.ingest?.github?.tokenEnv?.trim();
|
|
1705
1898
|
const envName = override?.trim() || fromRegistry || DEFAULT_GITHUB_TOKEN_ENV;
|
|
@@ -1724,20 +1917,17 @@ function githubHeaders(token) {
|
|
|
1724
1917
|
};
|
|
1725
1918
|
}
|
|
1726
1919
|
async function readResponseBody(response, maxBytes) {
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
if (
|
|
1920
|
+
try {
|
|
1921
|
+
return await readBoundedResponseBody(response, maxBytes);
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
if (error instanceof IngestLimitError && error.code === "INGEST_SIZE_LIMIT") {
|
|
1731
1924
|
throw new Error("artifact exceeds size limit");
|
|
1732
1925
|
}
|
|
1926
|
+
throw error;
|
|
1733
1927
|
}
|
|
1734
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
1735
|
-
if (arrayBuffer.byteLength > maxBytes) {
|
|
1736
|
-
throw new Error("artifact exceeds size limit");
|
|
1737
|
-
}
|
|
1738
|
-
return Buffer.from(arrayBuffer);
|
|
1739
1928
|
}
|
|
1740
1929
|
async function downloadGitHubArtifactArchive(options) {
|
|
1930
|
+
const maxBytes = resolveIngestMaxBytes(options.maxBytes);
|
|
1741
1931
|
const fetchFn = options.fetchImpl ?? fetch;
|
|
1742
1932
|
const { owner, name } = parseGitHubRepo(options.repo);
|
|
1743
1933
|
const runId = options.runId.trim();
|
|
@@ -1763,7 +1953,7 @@ async function downloadGitHubArtifactArchive(options) {
|
|
|
1763
1953
|
if (artifact.expired === true) {
|
|
1764
1954
|
throw new Error(`artifact expired for run ${runId}: ${artifactName}`);
|
|
1765
1955
|
}
|
|
1766
|
-
if (typeof artifact.size_in_bytes === "number" && artifact.size_in_bytes >
|
|
1956
|
+
if (typeof artifact.size_in_bytes === "number" && artifact.size_in_bytes > maxBytes) {
|
|
1767
1957
|
throw new Error("artifact exceeds size limit");
|
|
1768
1958
|
}
|
|
1769
1959
|
const downloadResponse = await fetchFn(artifact.archive_download_url, {
|
|
@@ -1773,7 +1963,7 @@ async function downloadGitHubArtifactArchive(options) {
|
|
|
1773
1963
|
if (!downloadResponse.ok) {
|
|
1774
1964
|
throw new Error(`GitHub artifact download failed (${downloadResponse.status})`);
|
|
1775
1965
|
}
|
|
1776
|
-
return readResponseBody(downloadResponse,
|
|
1966
|
+
return readResponseBody(downloadResponse, maxBytes);
|
|
1777
1967
|
}
|
|
1778
1968
|
async function importGitHubArtifact(options) {
|
|
1779
1969
|
const registryImportWarnings = [];
|
|
@@ -1823,17 +2013,25 @@ async function importGitHubArtifact(options) {
|
|
|
1823
2013
|
const fileName = `${options.artifactName}.zip`;
|
|
1824
2014
|
const destPath = uniqueDestPath(dirs.bundlesDir, fileName, contentHash);
|
|
1825
2015
|
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
2016
|
+
try {
|
|
2017
|
+
await withAtomicStagingDir(dirs.bundlesDir, async (stagingDir) => {
|
|
2018
|
+
const stagedPath = path10__default.default.join(stagingDir, fileName);
|
|
2019
|
+
await promises.writeFile(stagedPath, archive);
|
|
2020
|
+
await promoteStagingPath(stagedPath, destPath);
|
|
2021
|
+
});
|
|
2022
|
+
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2023
|
+
insertIngestFile(options.db, {
|
|
2024
|
+
sourceKey,
|
|
2025
|
+
sourceName: fileName,
|
|
2026
|
+
destPath,
|
|
2027
|
+
kind: "bundle",
|
|
2028
|
+
contentHash,
|
|
2029
|
+
importedAt
|
|
2030
|
+
});
|
|
2031
|
+
} catch (error) {
|
|
2032
|
+
await promises.rm(destPath, { force: true }).catch(() => void 0);
|
|
2033
|
+
throw error;
|
|
2034
|
+
}
|
|
1837
2035
|
const registryImport = await importStudioRegistry({
|
|
1838
2036
|
db: options.db,
|
|
1839
2037
|
registry: options.registry,
|
|
@@ -1897,7 +2095,7 @@ async function pathExists(filePath) {
|
|
|
1897
2095
|
}
|
|
1898
2096
|
async function validateBundleDirectory(bundleDir) {
|
|
1899
2097
|
const errors = [];
|
|
1900
|
-
const metadataPath =
|
|
2098
|
+
const metadataPath = path10__default.default.join(bundleDir, "metadata.json");
|
|
1901
2099
|
if (!await pathExists(metadataPath)) {
|
|
1902
2100
|
errors.push("bundle missing metadata.json");
|
|
1903
2101
|
return errors;
|
|
@@ -1923,11 +2121,18 @@ async function hashBundleContents(bundleDir) {
|
|
|
1923
2121
|
(a, b) => a.name.localeCompare(b.name)
|
|
1924
2122
|
);
|
|
1925
2123
|
for (const entry of entries) {
|
|
1926
|
-
const abs =
|
|
2124
|
+
const abs = path10__default.default.join(dir, entry.name);
|
|
1927
2125
|
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
1928
|
-
|
|
2126
|
+
const info = await promises.lstat(abs);
|
|
2127
|
+
if (info.isSymbolicLink()) {
|
|
2128
|
+
throw new IngestLimitError(
|
|
2129
|
+
"INGEST_SYMLINK_REJECTED",
|
|
2130
|
+
"symbolic links are not allowed for ingest"
|
|
2131
|
+
);
|
|
2132
|
+
}
|
|
2133
|
+
if (info.isDirectory()) {
|
|
1929
2134
|
await walk(abs, rel);
|
|
1930
|
-
} else if (
|
|
2135
|
+
} else if (info.isFile()) {
|
|
1931
2136
|
hash.update(rel);
|
|
1932
2137
|
hash.update("\0");
|
|
1933
2138
|
hash.update(await promises.readFile(abs));
|
|
@@ -1942,11 +2147,18 @@ async function copyDirectoryRecursive(sourceDir, destDir) {
|
|
|
1942
2147
|
await promises.mkdir(destDir, { recursive: true });
|
|
1943
2148
|
const entries = await promises.readdir(sourceDir, { withFileTypes: true });
|
|
1944
2149
|
for (const entry of entries) {
|
|
1945
|
-
const from =
|
|
1946
|
-
const to =
|
|
1947
|
-
|
|
2150
|
+
const from = path10__default.default.join(sourceDir, entry.name);
|
|
2151
|
+
const to = path10__default.default.join(destDir, entry.name);
|
|
2152
|
+
const info = await promises.lstat(from);
|
|
2153
|
+
if (info.isSymbolicLink()) {
|
|
2154
|
+
throw new IngestLimitError(
|
|
2155
|
+
"INGEST_SYMLINK_REJECTED",
|
|
2156
|
+
"symbolic links are not allowed for ingest"
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
if (info.isDirectory()) {
|
|
1948
2160
|
await copyDirectoryRecursive(from, to);
|
|
1949
|
-
} else if (
|
|
2161
|
+
} else if (info.isFile()) {
|
|
1950
2162
|
await promises.copyFile(from, to);
|
|
1951
2163
|
}
|
|
1952
2164
|
}
|
|
@@ -1963,11 +2175,18 @@ async function importBundleUpload(options) {
|
|
|
1963
2175
|
registryImportWarnings
|
|
1964
2176
|
};
|
|
1965
2177
|
}
|
|
1966
|
-
const bundlePath =
|
|
1967
|
-
let bundleStat;
|
|
2178
|
+
const bundlePath = path10__default.default.resolve(options.bundlePath);
|
|
1968
2179
|
try {
|
|
1969
|
-
|
|
1970
|
-
} catch {
|
|
2180
|
+
await lstatRegularDirectory(bundlePath);
|
|
2181
|
+
} catch (error) {
|
|
2182
|
+
if (error instanceof IngestLimitError) {
|
|
2183
|
+
return {
|
|
2184
|
+
skipped: false,
|
|
2185
|
+
imported: false,
|
|
2186
|
+
errors: [error.message],
|
|
2187
|
+
registryImportWarnings
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
1971
2190
|
return {
|
|
1972
2191
|
skipped: false,
|
|
1973
2192
|
imported: false,
|
|
@@ -1975,24 +2194,42 @@ async function importBundleUpload(options) {
|
|
|
1975
2194
|
registryImportWarnings
|
|
1976
2195
|
};
|
|
1977
2196
|
}
|
|
1978
|
-
|
|
2197
|
+
const validationErrors = await validateBundleDirectory(bundlePath);
|
|
2198
|
+
if (validationErrors.length > 0) {
|
|
1979
2199
|
return {
|
|
1980
2200
|
skipped: false,
|
|
1981
2201
|
imported: false,
|
|
1982
|
-
errors:
|
|
2202
|
+
errors: validationErrors,
|
|
1983
2203
|
registryImportWarnings
|
|
1984
2204
|
};
|
|
1985
2205
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
2206
|
+
let maxBytes;
|
|
2207
|
+
try {
|
|
2208
|
+
maxBytes = resolveIngestMaxBytes(
|
|
2209
|
+
options.maxBytes ?? options.registry.ingest?.bundleUpload?.maxBytes
|
|
2210
|
+
);
|
|
2211
|
+
await measureDirectoryBytes(bundlePath, maxBytes);
|
|
2212
|
+
} catch (error) {
|
|
2213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1988
2214
|
return {
|
|
1989
2215
|
skipped: false,
|
|
1990
2216
|
imported: false,
|
|
1991
|
-
errors:
|
|
2217
|
+
errors: [message],
|
|
2218
|
+
registryImportWarnings
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
let contentHash;
|
|
2222
|
+
try {
|
|
2223
|
+
contentHash = await hashBundleContents(bundlePath);
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2226
|
+
return {
|
|
2227
|
+
skipped: false,
|
|
2228
|
+
imported: false,
|
|
2229
|
+
errors: [message],
|
|
1992
2230
|
registryImportWarnings
|
|
1993
2231
|
};
|
|
1994
2232
|
}
|
|
1995
|
-
const contentHash = await hashBundleContents(bundlePath);
|
|
1996
2233
|
const sourceKey = `bundle:${bundlePath}`;
|
|
1997
2234
|
const existing = findIngestFileBySourceKey(options.db, sourceKey);
|
|
1998
2235
|
if (existing && existing.contentHash === contentHash) {
|
|
@@ -2006,11 +2243,15 @@ async function importBundleUpload(options) {
|
|
|
2006
2243
|
};
|
|
2007
2244
|
}
|
|
2008
2245
|
const dirs = resolveImportDirs(options.registryPath, options.registry);
|
|
2009
|
-
const folderName =
|
|
2246
|
+
const folderName = path10__default.default.basename(bundlePath);
|
|
2010
2247
|
const destPath = uniqueDestPath(dirs.bundlesDir, folderName, contentHash);
|
|
2011
2248
|
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
2012
2249
|
try {
|
|
2013
|
-
await
|
|
2250
|
+
await withAtomicStagingDir(dirs.bundlesDir, async (stagingDir) => {
|
|
2251
|
+
const stagedDest = path10__default.default.join(stagingDir, folderName);
|
|
2252
|
+
await copyDirectoryRecursive(bundlePath, stagedDest);
|
|
2253
|
+
await promoteStagingPath(stagedDest, destPath);
|
|
2254
|
+
});
|
|
2014
2255
|
insertIngestFile(options.db, {
|
|
2015
2256
|
sourceKey,
|
|
2016
2257
|
sourceName: folderName,
|
|
@@ -2034,6 +2275,7 @@ async function importBundleUpload(options) {
|
|
|
2034
2275
|
registryImportWarnings
|
|
2035
2276
|
};
|
|
2036
2277
|
} catch (error) {
|
|
2278
|
+
await promises.rm(destPath, { recursive: true, force: true }).catch(() => void 0);
|
|
2037
2279
|
const message = error instanceof Error ? error.message : String(error);
|
|
2038
2280
|
return {
|
|
2039
2281
|
skipped: false,
|