@agent-inspect/studio 6.0.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +989 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +183 -2
- package/dist/index.d.ts +183 -2
- package/dist/index.mjs +969 -63
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var promises = require('fs/promises');
|
|
4
|
-
var
|
|
4
|
+
var path8 = require('path');
|
|
5
5
|
var advanced = require('agent-inspect/advanced');
|
|
6
6
|
var workspace = require('agent-inspect/workspace');
|
|
7
7
|
var Database = require('better-sqlite3');
|
|
8
|
+
var crypto = require('crypto');
|
|
8
9
|
var http = require('http');
|
|
9
10
|
var checks = require('agent-inspect/checks');
|
|
10
11
|
var diff = require('agent-inspect/diff');
|
|
@@ -13,9 +14,29 @@ var readers = require('agent-inspect/readers');
|
|
|
13
14
|
|
|
14
15
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
15
16
|
|
|
16
|
-
var
|
|
17
|
+
var path8__default = /*#__PURE__*/_interopDefault(path8);
|
|
17
18
|
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
18
19
|
|
|
20
|
+
// packages/studio/src/registry.ts
|
|
21
|
+
function isSafeRelativePath(p) {
|
|
22
|
+
const trimmed = p.trim();
|
|
23
|
+
if (trimmed === "" || trimmed.startsWith("/") || trimmed.startsWith("\\")) return false;
|
|
24
|
+
if (/^[a-zA-Z]:/.test(trimmed)) return false;
|
|
25
|
+
return !trimmed.split(/[/\\]+/).some((seg) => seg === "..");
|
|
26
|
+
}
|
|
27
|
+
function resolveUnderRoot(root, ...segments) {
|
|
28
|
+
const resolvedRoot = path8__default.default.resolve(root);
|
|
29
|
+
const resolved = path8__default.default.resolve(resolvedRoot, ...segments);
|
|
30
|
+
assertPathUnderRoot(resolved, resolvedRoot);
|
|
31
|
+
return resolved;
|
|
32
|
+
}
|
|
33
|
+
function assertPathUnderRoot(resolved, root) {
|
|
34
|
+
const rel = path8__default.default.relative(path8__default.default.resolve(root), path8__default.default.resolve(resolved));
|
|
35
|
+
if (rel.startsWith("..") || path8__default.default.isAbsolute(rel)) {
|
|
36
|
+
throw new Error("path escapes allowed registry root");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
// packages/studio/src/registry.ts
|
|
20
41
|
var STUDIO_REGISTRY_SCHEMA_VERSION = "1.0";
|
|
21
42
|
var STUDIO_REGISTRY_FILENAMES = [
|
|
@@ -26,16 +47,10 @@ var MAX_REGISTRY_BYTES = 256 * 1024;
|
|
|
26
47
|
function isPlainObject(value) {
|
|
27
48
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
49
|
}
|
|
29
|
-
function isSafeRelativePath(p) {
|
|
30
|
-
const trimmed = p.trim();
|
|
31
|
-
if (trimmed === "" || trimmed.startsWith("/") || trimmed.startsWith("\\")) return false;
|
|
32
|
-
if (/^[a-zA-Z]:/.test(trimmed)) return false;
|
|
33
|
-
return !trimmed.split(/[/\\]+/).some((seg) => seg === "..");
|
|
34
|
-
}
|
|
35
50
|
function parseStudioRegistry(input) {
|
|
36
51
|
const errors = [];
|
|
37
52
|
if (!isPlainObject(input)) {
|
|
38
|
-
return { ok: false, errors: ["registry must be a JSON object"] };
|
|
53
|
+
return { ok: false, errors: ["registry must be a JSON object"], warnings: [] };
|
|
39
54
|
}
|
|
40
55
|
if (input.schemaVersion !== STUDIO_REGISTRY_SCHEMA_VERSION) {
|
|
41
56
|
errors.push(`schemaVersion must be "${STUDIO_REGISTRY_SCHEMA_VERSION}"`);
|
|
@@ -89,35 +104,157 @@ function parseStudioRegistry(input) {
|
|
|
89
104
|
importConfig.bundlesDir = dir;
|
|
90
105
|
}
|
|
91
106
|
}
|
|
107
|
+
if (input.import.fileDropDir !== void 0) {
|
|
108
|
+
const dir = String(input.import.fileDropDir).trim();
|
|
109
|
+
if (!isSafeRelativePath(dir)) {
|
|
110
|
+
errors.push("import.fileDropDir must be a safe relative path");
|
|
111
|
+
} else {
|
|
112
|
+
importConfig.fileDropDir = dir;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (input.import.enabled !== void 0) {
|
|
116
|
+
if (typeof input.import.enabled !== "boolean") {
|
|
117
|
+
errors.push("import.enabled must be a boolean");
|
|
118
|
+
} else {
|
|
119
|
+
importConfig.enabled = input.import.enabled;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
92
122
|
}
|
|
93
123
|
}
|
|
94
|
-
|
|
124
|
+
let ingestConfig;
|
|
125
|
+
const ingestWarnings = [];
|
|
126
|
+
if (input.ingest !== void 0) {
|
|
127
|
+
if (!isPlainObject(input.ingest)) {
|
|
128
|
+
errors.push("ingest must be an object");
|
|
129
|
+
} else {
|
|
130
|
+
ingestConfig = {};
|
|
131
|
+
if (input.ingest.github !== void 0) {
|
|
132
|
+
if (!isPlainObject(input.ingest.github)) {
|
|
133
|
+
errors.push("ingest.github must be an object");
|
|
134
|
+
} else {
|
|
135
|
+
const github = {};
|
|
136
|
+
if (input.ingest.github.enabled !== void 0) {
|
|
137
|
+
if (typeof input.ingest.github.enabled !== "boolean") {
|
|
138
|
+
errors.push("ingest.github.enabled must be a boolean");
|
|
139
|
+
} else {
|
|
140
|
+
github.enabled = input.ingest.github.enabled;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (input.ingest.github.tokenEnv !== void 0) {
|
|
144
|
+
const tokenEnv = String(input.ingest.github.tokenEnv).trim();
|
|
145
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(tokenEnv)) {
|
|
146
|
+
errors.push("ingest.github.tokenEnv must be an uppercase env var name");
|
|
147
|
+
} else {
|
|
148
|
+
github.tokenEnv = tokenEnv;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
ingestConfig.github = github;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (input.ingest.http !== void 0) {
|
|
155
|
+
if (!isPlainObject(input.ingest.http)) {
|
|
156
|
+
errors.push("ingest.http must be an object");
|
|
157
|
+
} else {
|
|
158
|
+
const http = {};
|
|
159
|
+
if (input.ingest.http.enabled !== void 0) {
|
|
160
|
+
if (typeof input.ingest.http.enabled !== "boolean") {
|
|
161
|
+
errors.push("ingest.http.enabled must be a boolean");
|
|
162
|
+
} else {
|
|
163
|
+
http.enabled = input.ingest.http.enabled;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (input.ingest.http.path !== void 0) {
|
|
167
|
+
const ingestPath = String(input.ingest.http.path).trim();
|
|
168
|
+
if (!ingestPath.startsWith("/") || ingestPath.includes("..")) {
|
|
169
|
+
errors.push("ingest.http.path must be an absolute safe path");
|
|
170
|
+
} else {
|
|
171
|
+
http.path = ingestPath;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (input.ingest.http.tokenEnv !== void 0) {
|
|
175
|
+
const tokenEnv = String(input.ingest.http.tokenEnv).trim();
|
|
176
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(tokenEnv)) {
|
|
177
|
+
errors.push("ingest.http.tokenEnv must be an uppercase env var name");
|
|
178
|
+
} else {
|
|
179
|
+
http.tokenEnv = tokenEnv;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (input.ingest.http.maxBytes !== void 0) {
|
|
183
|
+
const maxBytes = Number(input.ingest.http.maxBytes);
|
|
184
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
|
|
185
|
+
errors.push("ingest.http.maxBytes must be a positive integer");
|
|
186
|
+
} else {
|
|
187
|
+
http.maxBytes = maxBytes;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
ingestConfig.http = http;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (input.ingest.bundleUpload !== void 0) {
|
|
194
|
+
if (!isPlainObject(input.ingest.bundleUpload)) {
|
|
195
|
+
errors.push("ingest.bundleUpload must be an object");
|
|
196
|
+
} else {
|
|
197
|
+
const bundleUpload = {};
|
|
198
|
+
if (input.ingest.bundleUpload.enabled !== void 0) {
|
|
199
|
+
if (typeof input.ingest.bundleUpload.enabled !== "boolean") {
|
|
200
|
+
errors.push("ingest.bundleUpload.enabled must be a boolean");
|
|
201
|
+
} else {
|
|
202
|
+
bundleUpload.enabled = input.ingest.bundleUpload.enabled;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (input.ingest.bundleUpload.maxBytes !== void 0) {
|
|
206
|
+
const maxBytes = Number(input.ingest.bundleUpload.maxBytes);
|
|
207
|
+
if (!Number.isInteger(maxBytes) || maxBytes <= 0) {
|
|
208
|
+
errors.push("ingest.bundleUpload.maxBytes must be a positive integer");
|
|
209
|
+
} else {
|
|
210
|
+
bundleUpload.maxBytes = maxBytes;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
ingestConfig.bundleUpload = bundleUpload;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const knownIngestKeys = /* @__PURE__ */ new Set(["github", "http", "bundleUpload"]);
|
|
217
|
+
for (const key of Object.keys(input.ingest)) {
|
|
218
|
+
if (!knownIngestKeys.has(key)) {
|
|
219
|
+
ingestWarnings.push(`ignored unknown ingest key: ${key}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (errors.length > 0) return { ok: false, errors, warnings: ingestWarnings };
|
|
95
225
|
return {
|
|
96
226
|
ok: true,
|
|
97
227
|
registry: {
|
|
98
228
|
schemaVersion: STUDIO_REGISTRY_SCHEMA_VERSION,
|
|
99
229
|
name: String(input.name).trim(),
|
|
100
230
|
projects,
|
|
101
|
-
...importConfig ? { import: importConfig } : {}
|
|
231
|
+
...importConfig ? { import: importConfig } : {},
|
|
232
|
+
...ingestConfig ? { ingest: ingestConfig } : {}
|
|
102
233
|
},
|
|
103
|
-
errors: []
|
|
234
|
+
errors: [],
|
|
235
|
+
warnings: ingestWarnings
|
|
104
236
|
};
|
|
105
237
|
}
|
|
106
238
|
async function readStudioRegistryFile(filePath) {
|
|
107
239
|
try {
|
|
108
240
|
const raw = await promises.readFile(filePath, "utf8");
|
|
109
241
|
if (raw.length > MAX_REGISTRY_BYTES) {
|
|
110
|
-
return {
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
path: filePath,
|
|
245
|
+
errors: ["registry file exceeds size limit"],
|
|
246
|
+
warnings: []
|
|
247
|
+
};
|
|
111
248
|
}
|
|
112
249
|
const parsed = parseStudioRegistry(JSON.parse(raw));
|
|
113
250
|
return { ...parsed, path: filePath };
|
|
114
251
|
} catch (error) {
|
|
115
252
|
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
-
return { ok: false, path: filePath, errors: [message] };
|
|
253
|
+
return { ok: false, path: filePath, errors: [message], warnings: [] };
|
|
117
254
|
}
|
|
118
255
|
}
|
|
119
256
|
function resolveRegistryProjectPath(registryDir, projectPath) {
|
|
120
|
-
return
|
|
257
|
+
return path8__default.default.isAbsolute(projectPath) ? path8__default.default.resolve(projectPath) : path8__default.default.resolve(registryDir, projectPath);
|
|
121
258
|
}
|
|
122
259
|
var STUDIO_DB_SCHEMA_VERSION = "1.0";
|
|
123
260
|
var DEFAULT_STUDIO_DB_FILENAME = "studio.db";
|
|
@@ -150,6 +287,15 @@ CREATE TABLE IF NOT EXISTS runs (
|
|
|
150
287
|
);
|
|
151
288
|
CREATE INDEX IF NOT EXISTS idx_runs_project ON runs(project_id);
|
|
152
289
|
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
290
|
+
CREATE TABLE IF NOT EXISTS ingest_files (
|
|
291
|
+
source_key TEXT PRIMARY KEY,
|
|
292
|
+
source_name TEXT NOT NULL,
|
|
293
|
+
dest_path TEXT NOT NULL,
|
|
294
|
+
kind TEXT NOT NULL CHECK(kind IN ('ci', 'bundle')),
|
|
295
|
+
content_hash TEXT NOT NULL,
|
|
296
|
+
imported_at TEXT NOT NULL
|
|
297
|
+
);
|
|
298
|
+
CREATE INDEX IF NOT EXISTS idx_ingest_files_kind ON ingest_files(kind);
|
|
153
299
|
`;
|
|
154
300
|
function resolveStudioDbPath(options) {
|
|
155
301
|
if (options.dbPath && options.dbPath.trim() !== "") {
|
|
@@ -157,9 +303,9 @@ function resolveStudioDbPath(options) {
|
|
|
157
303
|
if (raw.startsWith("postgres://") || raw.startsWith("postgresql://")) {
|
|
158
304
|
return raw;
|
|
159
305
|
}
|
|
160
|
-
return
|
|
306
|
+
return path8__default.default.resolve(options.cwd ?? process.cwd(), raw);
|
|
161
307
|
}
|
|
162
|
-
return
|
|
308
|
+
return path8__default.default.resolve(
|
|
163
309
|
options.cwd ?? process.cwd(),
|
|
164
310
|
".agent-inspect",
|
|
165
311
|
DEFAULT_STUDIO_DB_FILENAME
|
|
@@ -174,7 +320,7 @@ function openStudioDb(dbPath) {
|
|
|
174
320
|
"Postgres studio databases are not implemented in v6.0.0; use a SQLite file path."
|
|
175
321
|
);
|
|
176
322
|
}
|
|
177
|
-
const dir =
|
|
323
|
+
const dir = path8__default.default.dirname(dbPath);
|
|
178
324
|
void promises.mkdir(dir, { recursive: true });
|
|
179
325
|
const db = new Database__default.default(dbPath);
|
|
180
326
|
db.pragma("journal_mode = WAL");
|
|
@@ -258,18 +404,37 @@ function searchProjectRuns(db, projectId, query, limit = 50) {
|
|
|
258
404
|
LIMIT ?`
|
|
259
405
|
).all(projectId, pattern, pattern, pattern, limit);
|
|
260
406
|
}
|
|
407
|
+
function findIngestFileBySourceKey(db, sourceKey) {
|
|
408
|
+
return db.prepare(
|
|
409
|
+
`SELECT source_key AS sourceKey, source_name AS sourceName, dest_path AS destPath,
|
|
410
|
+
kind, content_hash AS contentHash, imported_at AS importedAt
|
|
411
|
+
FROM ingest_files WHERE source_key = ?`
|
|
412
|
+
).get(sourceKey);
|
|
413
|
+
}
|
|
414
|
+
function insertIngestFile(db, row) {
|
|
415
|
+
db.prepare(
|
|
416
|
+
`INSERT INTO ingest_files(source_key, source_name, dest_path, kind, content_hash, imported_at)
|
|
417
|
+
VALUES (@sourceKey, @sourceName, @destPath, @kind, @contentHash, @importedAt)
|
|
418
|
+
ON CONFLICT(source_key) DO UPDATE SET
|
|
419
|
+
source_name = excluded.source_name,
|
|
420
|
+
dest_path = excluded.dest_path,
|
|
421
|
+
kind = excluded.kind,
|
|
422
|
+
content_hash = excluded.content_hash,
|
|
423
|
+
imported_at = excluded.imported_at`
|
|
424
|
+
).run(row);
|
|
425
|
+
}
|
|
261
426
|
|
|
262
427
|
// packages/studio/src/import.ts
|
|
263
428
|
async function discoverSuiteConfigs(projectRoot, configured) {
|
|
264
429
|
if (configured && configured.length > 0) {
|
|
265
|
-
return configured.map((rel) =>
|
|
430
|
+
return configured.map((rel) => path8__default.default.resolve(projectRoot, rel));
|
|
266
431
|
}
|
|
267
432
|
const found = [];
|
|
268
433
|
try {
|
|
269
434
|
const entries = await promises.readdir(projectRoot);
|
|
270
435
|
for (const entry of entries) {
|
|
271
436
|
if (entry.endsWith(".suite.json")) {
|
|
272
|
-
found.push(
|
|
437
|
+
found.push(path8__default.default.join(projectRoot, entry));
|
|
273
438
|
}
|
|
274
439
|
}
|
|
275
440
|
} catch {
|
|
@@ -279,7 +444,7 @@ async function discoverSuiteConfigs(projectRoot, configured) {
|
|
|
279
444
|
async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
280
445
|
const runs = [];
|
|
281
446
|
for (const rel of traceDirs) {
|
|
282
|
-
const traceDir = advanced.resolveTraceDir({ dir:
|
|
447
|
+
const traceDir = advanced.resolveTraceDir({ dir: path8__default.default.join(workspaceDir, rel) });
|
|
283
448
|
const td = new advanced.TraceDirectory({ dir: traceDir });
|
|
284
449
|
const files = await td.list();
|
|
285
450
|
const metas = await advanced.loadTraceMetadataList(
|
|
@@ -293,7 +458,7 @@ async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
|
293
458
|
runId: meta.runId,
|
|
294
459
|
...meta.name !== void 0 ? { name: meta.name } : {},
|
|
295
460
|
status: meta.status,
|
|
296
|
-
file:
|
|
461
|
+
file: path8__default.default.basename(meta.filePath),
|
|
297
462
|
...meta.startedAt !== void 0 ? { startedAt: meta.startedAt } : {},
|
|
298
463
|
...meta.durationMs !== void 0 ? { durationMs: meta.durationMs } : {}
|
|
299
464
|
});
|
|
@@ -302,7 +467,7 @@ async function loadProjectRuns(workspaceDir, traceDirs) {
|
|
|
302
467
|
return runs.sort((a, b) => (b.startedAt ?? 0) - (a.startedAt ?? 0));
|
|
303
468
|
}
|
|
304
469
|
async function importStudioRegistry(options) {
|
|
305
|
-
const registryDir =
|
|
470
|
+
const registryDir = path8__default.default.dirname(options.registryPath);
|
|
306
471
|
const warnings = [];
|
|
307
472
|
const projects = [];
|
|
308
473
|
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -363,18 +528,242 @@ async function importStudioProject(options) {
|
|
|
363
528
|
}
|
|
364
529
|
async function resolveStudioRegistryPath(options) {
|
|
365
530
|
if (options.workspacePath && options.workspacePath.trim() !== "") {
|
|
366
|
-
return
|
|
531
|
+
return path8__default.default.resolve(options.cwd ?? process.cwd(), options.workspacePath);
|
|
367
532
|
}
|
|
368
|
-
const cwd =
|
|
533
|
+
const cwd = path8__default.default.resolve(options.cwd ?? process.cwd());
|
|
369
534
|
for (const rel of STUDIO_REGISTRY_FILENAMES) {
|
|
370
|
-
const candidate =
|
|
535
|
+
const candidate = path8__default.default.join(cwd, rel);
|
|
371
536
|
try {
|
|
372
537
|
await promises.access(candidate);
|
|
373
538
|
return candidate;
|
|
374
539
|
} catch {
|
|
375
540
|
}
|
|
376
541
|
}
|
|
377
|
-
return
|
|
542
|
+
return path8__default.default.join(cwd, STUDIO_REGISTRY_FILENAMES[0]);
|
|
543
|
+
}
|
|
544
|
+
function resolveImportDirs(registryPath, registry) {
|
|
545
|
+
const registryDir = path8__default.default.dirname(registryPath);
|
|
546
|
+
const importConfig = registry.import ?? {};
|
|
547
|
+
const fileDropDir = importConfig.fileDropDir ?? "imports/drop";
|
|
548
|
+
const ciArtifactsDir = importConfig.ciArtifactsDir ?? "imports/ci";
|
|
549
|
+
const bundlesDir = importConfig.bundlesDir ?? "imports/bundles";
|
|
550
|
+
return {
|
|
551
|
+
registryDir,
|
|
552
|
+
fileDropDir: resolveUnderRoot(registryDir, fileDropDir),
|
|
553
|
+
ciArtifactsDir: resolveUnderRoot(registryDir, ciArtifactsDir),
|
|
554
|
+
bundlesDir: resolveUnderRoot(registryDir, bundlesDir)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function uniqueDestPath(destDir, fileName, contentHash) {
|
|
558
|
+
const ext = path8__default.default.extname(fileName);
|
|
559
|
+
const base = path8__default.default.basename(fileName, ext);
|
|
560
|
+
const shortHash = contentHash.slice(0, 8);
|
|
561
|
+
return path8__default.default.join(destDir, `${base}-${shortHash}${ext}`);
|
|
562
|
+
}
|
|
563
|
+
function sanitizeSafeErrorMessage(message, secret) {
|
|
564
|
+
if (!secret || secret.length < 4) return message;
|
|
565
|
+
return message.split(secret).join("[redacted]");
|
|
566
|
+
}
|
|
567
|
+
function parseGitHubRepo(repo) {
|
|
568
|
+
const trimmed = repo.trim();
|
|
569
|
+
const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(trimmed);
|
|
570
|
+
if (!match) {
|
|
571
|
+
throw new Error("repo must be in owner/name format");
|
|
572
|
+
}
|
|
573
|
+
return { owner: match[1], name: match[2] };
|
|
574
|
+
}
|
|
575
|
+
function buildGitHubArtifactSourceKey(options) {
|
|
576
|
+
return `github:${options.owner}/${options.repo}/runs/${options.runId}/${options.artifactName}`;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// packages/studio/src/ingest/file-drop.ts
|
|
580
|
+
var FILE_DROP_ARCHIVE_DIR = ".imported";
|
|
581
|
+
var CI_EXTENSIONS = [".jsonl", ".suite.json"];
|
|
582
|
+
var BUNDLE_EXTENSIONS = [".tgz", ".zip"];
|
|
583
|
+
function classifyFile(fileName) {
|
|
584
|
+
const lower = fileName.toLowerCase();
|
|
585
|
+
if (CI_EXTENSIONS.some((ext) => lower.endsWith(ext))) return "ci";
|
|
586
|
+
if (BUNDLE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return "bundle";
|
|
587
|
+
return void 0;
|
|
588
|
+
}
|
|
589
|
+
async function hashFile(filePath) {
|
|
590
|
+
const data = await promises.readFile(filePath);
|
|
591
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
592
|
+
}
|
|
593
|
+
async function importOneFile(options) {
|
|
594
|
+
const destPath = uniqueDestPath(options.destDir, options.fileName, options.contentHash);
|
|
595
|
+
assertPathUnderRoot(destPath, path8__default.default.dirname(options.destDir));
|
|
596
|
+
await promises.mkdir(options.destDir, { recursive: true });
|
|
597
|
+
await promises.copyFile(options.sourcePath, destPath);
|
|
598
|
+
insertIngestFile(options.db, {
|
|
599
|
+
sourceKey: options.sourceKey,
|
|
600
|
+
sourceName: options.fileName,
|
|
601
|
+
destPath,
|
|
602
|
+
kind: options.kind,
|
|
603
|
+
contentHash: options.contentHash,
|
|
604
|
+
importedAt: options.importedAt
|
|
605
|
+
});
|
|
606
|
+
let archived = false;
|
|
607
|
+
if (options.archiveAfterImport) {
|
|
608
|
+
await promises.mkdir(options.archiveDir, { recursive: true });
|
|
609
|
+
const archiveTarget = path8__default.default.join(options.archiveDir, options.fileName);
|
|
610
|
+
await promises.rename(options.sourcePath, archiveTarget);
|
|
611
|
+
archived = true;
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
sourceKey: options.sourceKey,
|
|
615
|
+
sourceName: options.fileName,
|
|
616
|
+
destPath,
|
|
617
|
+
kind: options.kind,
|
|
618
|
+
contentHash: options.contentHash,
|
|
619
|
+
archived
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
async function importFileDrop(options) {
|
|
623
|
+
const warnings = [];
|
|
624
|
+
const errors = [];
|
|
625
|
+
const files = [];
|
|
626
|
+
if (!options.enabled) {
|
|
627
|
+
return {
|
|
628
|
+
skipped: true,
|
|
629
|
+
reason: "file-drop ingest is disabled; pass --ingest file-drop or use studio import drop",
|
|
630
|
+
scanned: 0,
|
|
631
|
+
imported: 0,
|
|
632
|
+
skippedFiles: 0,
|
|
633
|
+
errors,
|
|
634
|
+
warnings,
|
|
635
|
+
files
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
const dirs = resolveImportDirs(options.registryPath, options.registry);
|
|
639
|
+
let dropDir;
|
|
640
|
+
try {
|
|
641
|
+
dropDir = options.dropDir ? path8__default.default.isAbsolute(options.dropDir) ? (assertPathUnderRoot(options.dropDir, dirs.registryDir), options.dropDir) : resolveUnderRoot(dirs.registryDir, options.dropDir) : dirs.fileDropDir;
|
|
642
|
+
assertPathUnderRoot(dropDir, dirs.registryDir);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
645
|
+
return {
|
|
646
|
+
skipped: false,
|
|
647
|
+
scanned: 0,
|
|
648
|
+
imported: 0,
|
|
649
|
+
skippedFiles: 0,
|
|
650
|
+
errors: [message],
|
|
651
|
+
warnings,
|
|
652
|
+
files
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
let entries;
|
|
656
|
+
try {
|
|
657
|
+
entries = await promises.readdir(dropDir);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
660
|
+
return {
|
|
661
|
+
skipped: false,
|
|
662
|
+
scanned: 0,
|
|
663
|
+
imported: 0,
|
|
664
|
+
skippedFiles: 0,
|
|
665
|
+
errors: [`unable to read file-drop directory: ${message}`],
|
|
666
|
+
warnings,
|
|
667
|
+
files
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
671
|
+
const archiveDir = path8__default.default.join(dropDir, FILE_DROP_ARCHIVE_DIR);
|
|
672
|
+
let scanned = 0;
|
|
673
|
+
let imported = 0;
|
|
674
|
+
let skippedFiles = 0;
|
|
675
|
+
for (const entry of entries.sort()) {
|
|
676
|
+
if (entry === FILE_DROP_ARCHIVE_DIR || entry.startsWith(".")) continue;
|
|
677
|
+
const sourcePath = path8__default.default.join(dropDir, entry);
|
|
678
|
+
let fileStat;
|
|
679
|
+
try {
|
|
680
|
+
fileStat = await promises.stat(sourcePath);
|
|
681
|
+
} catch {
|
|
682
|
+
warnings.push(`skipped unreadable entry: ${entry}`);
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if (!fileStat.isFile()) continue;
|
|
686
|
+
const kind = classifyFile(entry);
|
|
687
|
+
if (!kind) continue;
|
|
688
|
+
scanned += 1;
|
|
689
|
+
const sourceKey = entry;
|
|
690
|
+
let contentHash;
|
|
691
|
+
try {
|
|
692
|
+
contentHash = await hashFile(sourcePath);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
695
|
+
errors.push(`failed to read ${entry}: ${message}`);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const existing = findIngestFileBySourceKey(options.db, sourceKey);
|
|
699
|
+
if (existing && existing.contentHash === contentHash) {
|
|
700
|
+
skippedFiles += 1;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const destDir = kind === "ci" ? dirs.ciArtifactsDir : dirs.bundlesDir;
|
|
704
|
+
try {
|
|
705
|
+
const importedFile = await importOneFile({
|
|
706
|
+
db: options.db,
|
|
707
|
+
sourcePath,
|
|
708
|
+
sourceKey,
|
|
709
|
+
fileName: entry,
|
|
710
|
+
kind,
|
|
711
|
+
destDir,
|
|
712
|
+
archiveAfterImport: options.archiveAfterImport === true,
|
|
713
|
+
archiveDir,
|
|
714
|
+
importedAt,
|
|
715
|
+
contentHash
|
|
716
|
+
});
|
|
717
|
+
files.push(importedFile);
|
|
718
|
+
imported += 1;
|
|
719
|
+
} catch (error) {
|
|
720
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
721
|
+
errors.push(`failed to import ${entry}: ${message}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return {
|
|
725
|
+
skipped: false,
|
|
726
|
+
scanned,
|
|
727
|
+
imported,
|
|
728
|
+
skippedFiles,
|
|
729
|
+
errors,
|
|
730
|
+
warnings,
|
|
731
|
+
files
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
async function importFileDropFromRegistry(options) {
|
|
735
|
+
return importFileDrop({
|
|
736
|
+
db: options.db,
|
|
737
|
+
registryPath: options.registryPath,
|
|
738
|
+
registry: options.registry,
|
|
739
|
+
enabled: options.enabled,
|
|
740
|
+
...options.dropDir !== void 0 ? { dropDir: options.dropDir } : {},
|
|
741
|
+
...options.archiveAfterImport !== void 0 ? { archiveAfterImport: options.archiveAfterImport } : {}
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
async function runStudioFileDropImport(options) {
|
|
745
|
+
const cwd = options.cwd ?? process.cwd();
|
|
746
|
+
const registryPath = await resolveStudioRegistryPath({
|
|
747
|
+
...options.workspacePath !== void 0 ? { workspacePath: options.workspacePath } : {},
|
|
748
|
+
cwd
|
|
749
|
+
});
|
|
750
|
+
const registryRead = await readStudioRegistryFile(registryPath);
|
|
751
|
+
if (!registryRead.ok || registryRead.registry === void 0) {
|
|
752
|
+
throw new Error(registryRead.errors.join("; ") || "invalid studio registry");
|
|
753
|
+
}
|
|
754
|
+
const dbPath = resolveStudioDbPath({
|
|
755
|
+
...options.dbPath !== void 0 ? { dbPath: options.dbPath } : {},
|
|
756
|
+
cwd
|
|
757
|
+
});
|
|
758
|
+
const db = openStudioDb(dbPath);
|
|
759
|
+
return importFileDropFromRegistry({
|
|
760
|
+
db,
|
|
761
|
+
registryPath,
|
|
762
|
+
registry: registryRead.registry,
|
|
763
|
+
enabled: true,
|
|
764
|
+
...options.dropDir !== void 0 ? { dropDir: options.dropDir } : {},
|
|
765
|
+
...options.archiveAfterImport !== void 0 ? { archiveAfterImport: options.archiveAfterImport } : {}
|
|
766
|
+
});
|
|
378
767
|
}
|
|
379
768
|
|
|
380
769
|
// packages/studio/src/context.ts
|
|
@@ -398,12 +787,23 @@ async function createStudioContext(options = {}) {
|
|
|
398
787
|
registry: registryRead.registry,
|
|
399
788
|
registryPath
|
|
400
789
|
});
|
|
790
|
+
let fileDropResult;
|
|
791
|
+
if (options.ingestFileDrop === true) {
|
|
792
|
+
fileDropResult = await importFileDropFromRegistry({
|
|
793
|
+
db,
|
|
794
|
+
registryPath,
|
|
795
|
+
registry: registryRead.registry,
|
|
796
|
+
enabled: true,
|
|
797
|
+
...options.archiveFileDrop === true ? { archiveAfterImport: true } : {}
|
|
798
|
+
});
|
|
799
|
+
}
|
|
401
800
|
return {
|
|
402
801
|
db,
|
|
403
802
|
dbPath,
|
|
404
803
|
registryPath,
|
|
405
804
|
registry: registryRead.registry,
|
|
406
805
|
importResult,
|
|
806
|
+
...fileDropResult !== void 0 ? { fileDropResult } : {},
|
|
407
807
|
projects: importResult.projects
|
|
408
808
|
};
|
|
409
809
|
}
|
|
@@ -417,6 +817,158 @@ function summarizeProjects(projects) {
|
|
|
417
817
|
importedAt: project.importedAt
|
|
418
818
|
}));
|
|
419
819
|
}
|
|
820
|
+
var DEFAULT_INGEST_TOKEN_ENV = "STUDIO_INGEST_TOKEN";
|
|
821
|
+
function resolveIngestTokenEnv(options) {
|
|
822
|
+
const envName = options.tokenEnv?.trim() || options.registryTokenEnv?.trim() || DEFAULT_INGEST_TOKEN_ENV;
|
|
823
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(envName)) {
|
|
824
|
+
throw new Error("ingest token env name must be an uppercase identifier");
|
|
825
|
+
}
|
|
826
|
+
return envName;
|
|
827
|
+
}
|
|
828
|
+
function resolveIngestToken(envName) {
|
|
829
|
+
const token = process.env[envName]?.trim();
|
|
830
|
+
return token && token.length > 0 ? token : void 0;
|
|
831
|
+
}
|
|
832
|
+
function extractIngestTokenFromRequest(headers) {
|
|
833
|
+
const headerToken = firstHeader(headers["x-agentinspect-token"]) ?? firstHeader(headers["x-agent-inspect-token"]);
|
|
834
|
+
if (headerToken) return headerToken;
|
|
835
|
+
const auth = firstHeader(headers.authorization);
|
|
836
|
+
if (auth?.startsWith("Bearer ")) {
|
|
837
|
+
const token = auth.slice("Bearer ".length).trim();
|
|
838
|
+
return token.length > 0 ? token : void 0;
|
|
839
|
+
}
|
|
840
|
+
return void 0;
|
|
841
|
+
}
|
|
842
|
+
function firstHeader(value) {
|
|
843
|
+
if (Array.isArray(value)) return value[0]?.trim();
|
|
844
|
+
return value?.trim();
|
|
845
|
+
}
|
|
846
|
+
function isIngestTokenValid(provided, expected) {
|
|
847
|
+
if (!provided || !expected) return false;
|
|
848
|
+
const providedBuf = Buffer.from(provided);
|
|
849
|
+
const expectedBuf = Buffer.from(expected);
|
|
850
|
+
if (providedBuf.length !== expectedBuf.length) return false;
|
|
851
|
+
return crypto.timingSafeEqual(providedBuf, expectedBuf);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// packages/studio/src/ingest/http.ts
|
|
855
|
+
var DEFAULT_HTTP_INGEST_BASE_PATH = "/api/ingest";
|
|
856
|
+
var HTTP_INGEST_BUNDLE_PATH = "/api/ingest/bundle";
|
|
857
|
+
var HTTP_INGEST_ARTIFACT_PATH = "/api/ingest/artifact";
|
|
858
|
+
var DEFAULT_MAX_INGEST_BYTES = 52428800;
|
|
859
|
+
function resolveHttpIngestConfig(options, registryEnabled) {
|
|
860
|
+
const http = options.context?.registry.ingest?.http;
|
|
861
|
+
const enabled = options.ingestHttp === true || registryEnabled === true || http?.enabled === true;
|
|
862
|
+
const basePath = (http?.path ?? DEFAULT_HTTP_INGEST_BASE_PATH).trim() || DEFAULT_HTTP_INGEST_BASE_PATH;
|
|
863
|
+
const tokenEnv = resolveIngestTokenEnv({
|
|
864
|
+
...options.ingestTokenEnv !== void 0 ? { tokenEnv: options.ingestTokenEnv } : {},
|
|
865
|
+
...http?.tokenEnv !== void 0 ? { registryTokenEnv: http.tokenEnv } : {}
|
|
866
|
+
});
|
|
867
|
+
const maxBytes = http?.maxBytes ?? DEFAULT_MAX_INGEST_BYTES;
|
|
868
|
+
return { enabled, basePath, tokenEnv, maxBytes };
|
|
869
|
+
}
|
|
870
|
+
function sendJson(res, status, body) {
|
|
871
|
+
res.writeHead(status, {
|
|
872
|
+
"content-type": "application/json; charset=utf-8",
|
|
873
|
+
"cache-control": "no-store"
|
|
874
|
+
});
|
|
875
|
+
res.end(JSON.stringify(body));
|
|
876
|
+
}
|
|
877
|
+
async function readBoundedRequestBody(req, maxBytes) {
|
|
878
|
+
const chunks = [];
|
|
879
|
+
let total = 0;
|
|
880
|
+
for await (const chunk of req) {
|
|
881
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
882
|
+
total += buf.length;
|
|
883
|
+
if (total > maxBytes) {
|
|
884
|
+
throw new Error("request body exceeds size limit");
|
|
885
|
+
}
|
|
886
|
+
chunks.push(buf);
|
|
887
|
+
}
|
|
888
|
+
return Buffer.concat(chunks);
|
|
889
|
+
}
|
|
890
|
+
function isHttpIngestRoute(pathname, config) {
|
|
891
|
+
return pathname === HTTP_INGEST_BUNDLE_PATH || pathname === HTTP_INGEST_ARTIFACT_PATH || pathname === `${config.basePath}/bundle` || pathname === `${config.basePath}/artifact`;
|
|
892
|
+
}
|
|
893
|
+
async function handleHttpIngestRequest(req, res, ctx, options, pathname) {
|
|
894
|
+
const config = resolveHttpIngestConfig(options, ctx.registry.ingest?.http?.enabled);
|
|
895
|
+
if (!isHttpIngestRoute(pathname, config)) return false;
|
|
896
|
+
if (!config.enabled) {
|
|
897
|
+
sendJson(res, 404, { error: "HTTP ingest is disabled" });
|
|
898
|
+
return true;
|
|
899
|
+
}
|
|
900
|
+
if (req.method !== "POST") {
|
|
901
|
+
sendJson(res, 405, { error: "Method not allowed" });
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
const expectedToken = resolveIngestToken(config.tokenEnv);
|
|
905
|
+
const providedToken = extractIngestTokenFromRequest(req.headers);
|
|
906
|
+
if (!isIngestTokenValid(providedToken, expectedToken)) {
|
|
907
|
+
sendJson(res, 403, { error: "Invalid or missing ingest token" });
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
let body;
|
|
911
|
+
try {
|
|
912
|
+
body = await readBoundedRequestBody(req, config.maxBytes);
|
|
913
|
+
} catch (error) {
|
|
914
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
915
|
+
const status = message.includes("size limit") ? 413 : 400;
|
|
916
|
+
sendJson(res, status, { error: sanitizeSafeErrorMessage(message, expectedToken) });
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
if (body.length === 0) {
|
|
920
|
+
sendJson(res, 400, { error: "Empty request body" });
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
try {
|
|
924
|
+
const isBundle = pathname === HTTP_INGEST_BUNDLE_PATH || pathname === `${config.basePath}/bundle`;
|
|
925
|
+
const isArtifact = pathname === HTTP_INGEST_ARTIFACT_PATH || pathname === `${config.basePath}/artifact`;
|
|
926
|
+
if (!isBundle && !isArtifact) {
|
|
927
|
+
sendJson(res, 404, { error: "Unknown ingest route" });
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
const dirs = resolveImportDirs(ctx.registryPath, ctx.registry);
|
|
931
|
+
const contentHash = crypto.createHash("sha256").update(body).digest("hex");
|
|
932
|
+
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
933
|
+
const fileName = isBundle ? `http-bundle-${contentHash.slice(0, 8)}.bin` : `http-artifact-${contentHash.slice(0, 8)}.zip`;
|
|
934
|
+
const destDir = isBundle ? dirs.bundlesDir : dirs.ciArtifactsDir;
|
|
935
|
+
const destPath = uniqueDestPath(destDir, fileName, contentHash);
|
|
936
|
+
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
937
|
+
await promises.mkdir(destDir, { recursive: true });
|
|
938
|
+
await promises.writeFile(destPath, body);
|
|
939
|
+
const sourceKey = isBundle ? `http:bundle:${contentHash}` : buildGitHubArtifactSourceKey({
|
|
940
|
+
owner: "http",
|
|
941
|
+
repo: "ingest",
|
|
942
|
+
runId: importedAt,
|
|
943
|
+
artifactName: fileName
|
|
944
|
+
});
|
|
945
|
+
insertIngestFile(ctx.db, {
|
|
946
|
+
sourceKey,
|
|
947
|
+
sourceName: fileName,
|
|
948
|
+
destPath,
|
|
949
|
+
kind: isBundle ? "bundle" : "ci",
|
|
950
|
+
contentHash,
|
|
951
|
+
importedAt
|
|
952
|
+
});
|
|
953
|
+
const registryImport = await importStudioRegistry({
|
|
954
|
+
db: ctx.db,
|
|
955
|
+
registry: ctx.registry,
|
|
956
|
+
registryPath: ctx.registryPath
|
|
957
|
+
});
|
|
958
|
+
sendJson(res, 200, {
|
|
959
|
+
ok: true,
|
|
960
|
+
imported: true,
|
|
961
|
+
kind: isBundle ? "bundle" : "artifact",
|
|
962
|
+
destPath,
|
|
963
|
+
warnings: registryImport.warnings
|
|
964
|
+
});
|
|
965
|
+
return true;
|
|
966
|
+
} catch (error) {
|
|
967
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
968
|
+
sendJson(res, 500, { error: sanitizeSafeErrorMessage(message, expectedToken) });
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
420
972
|
|
|
421
973
|
// packages/studio/src/html.ts
|
|
422
974
|
var studioIndexHtml = `<!DOCTYPE html>
|
|
@@ -462,7 +1014,7 @@ function getImportedProject(db, projects, projectId) {
|
|
|
462
1014
|
async function loadTraceDirMetas(workspaceDir, traceDirs) {
|
|
463
1015
|
const metas = [];
|
|
464
1016
|
for (const rel of traceDirs) {
|
|
465
|
-
const traceDir = advanced.resolveTraceDir({ dir:
|
|
1017
|
+
const traceDir = advanced.resolveTraceDir({ dir: path8__default.default.join(workspaceDir, rel) });
|
|
466
1018
|
const td = new advanced.TraceDirectory({ dir: traceDir });
|
|
467
1019
|
const files = await td.list();
|
|
468
1020
|
const listed = await advanced.loadTraceMetadataList(
|
|
@@ -491,7 +1043,7 @@ async function loadProjectSuitesView(ctx) {
|
|
|
491
1043
|
try {
|
|
492
1044
|
const result = await advanced.runSuite({
|
|
493
1045
|
configPath,
|
|
494
|
-
cwd:
|
|
1046
|
+
cwd: path8__default.default.dirname(configPath)
|
|
495
1047
|
});
|
|
496
1048
|
suites.push({
|
|
497
1049
|
suiteName: result.suiteName,
|
|
@@ -509,7 +1061,7 @@ async function loadProjectSuitesView(ctx) {
|
|
|
509
1061
|
} catch (error) {
|
|
510
1062
|
const message = error instanceof Error ? error.message : String(error);
|
|
511
1063
|
suites.push({
|
|
512
|
-
suiteName:
|
|
1064
|
+
suiteName: path8__default.default.basename(configPath),
|
|
513
1065
|
configPath,
|
|
514
1066
|
ok: false,
|
|
515
1067
|
status: "error",
|
|
@@ -558,7 +1110,7 @@ async function loadProjectSearchView(ctx, db, params) {
|
|
|
558
1110
|
}
|
|
559
1111
|
const metas = await loadTraceDirMetas(ctx.project.workspaceDir, ["runs"]);
|
|
560
1112
|
const traceDir = advanced.resolveTraceDir({
|
|
561
|
-
dir:
|
|
1113
|
+
dir: path8__default.default.join(ctx.project.workspaceDir, "runs")
|
|
562
1114
|
});
|
|
563
1115
|
const results = await advanced.searchTraces(metas, {
|
|
564
1116
|
traceDir,
|
|
@@ -602,12 +1154,12 @@ async function loadProjectDiffView(ctx, params) {
|
|
|
602
1154
|
};
|
|
603
1155
|
}
|
|
604
1156
|
async function loadProjectReportsView(ctx) {
|
|
605
|
-
const reportsDir =
|
|
1157
|
+
const reportsDir = path8__default.default.join(ctx.project.workspaceDir, "reports");
|
|
606
1158
|
const reports = [];
|
|
607
1159
|
try {
|
|
608
1160
|
const files = await promises.readdir(reportsDir);
|
|
609
1161
|
for (const file of files) {
|
|
610
|
-
const filePath =
|
|
1162
|
+
const filePath = path8__default.default.join(reportsDir, file);
|
|
611
1163
|
const info = await promises.stat(filePath);
|
|
612
1164
|
if (!info.isFile()) continue;
|
|
613
1165
|
reports.push({ name: file, path: filePath, sizeBytes: info.size });
|
|
@@ -662,7 +1214,7 @@ async function loadBundleExportView(ctx, params) {
|
|
|
662
1214
|
runId,
|
|
663
1215
|
readOnly: true,
|
|
664
1216
|
redactionProfile: ctx.project.redactionProfile ?? "share",
|
|
665
|
-
cliHint: `npx agent-inspect bundle ${runId} --profile ${ctx.project.redactionProfile ?? "share"} --dir ${
|
|
1217
|
+
cliHint: `npx agent-inspect bundle ${runId} --profile ${ctx.project.redactionProfile ?? "share"} --dir ${path8__default.default.join(ctx.project.workspaceDir, "runs")}`,
|
|
666
1218
|
note: "Studio does not mutate traces or upload bundles. Run the CLI locally to assemble a share-safe bundle."
|
|
667
1219
|
};
|
|
668
1220
|
}
|
|
@@ -706,7 +1258,7 @@ function studioAuthRequiredResponse() {
|
|
|
706
1258
|
}
|
|
707
1259
|
|
|
708
1260
|
// packages/studio/src/routes.ts
|
|
709
|
-
function
|
|
1261
|
+
function sendJson2(res, status, body, headers = {}) {
|
|
710
1262
|
const payload = JSON.stringify(body);
|
|
711
1263
|
res.writeHead(status, {
|
|
712
1264
|
"content-type": "application/json; charset=utf-8",
|
|
@@ -716,7 +1268,7 @@ function sendJson(res, status, body, headers = {}) {
|
|
|
716
1268
|
res.end(payload);
|
|
717
1269
|
}
|
|
718
1270
|
function notFound(res, message) {
|
|
719
|
-
|
|
1271
|
+
sendJson2(res, 404, { error: message });
|
|
720
1272
|
}
|
|
721
1273
|
function decodeSegment(segment) {
|
|
722
1274
|
if (!segment) return "";
|
|
@@ -729,11 +1281,11 @@ function decodeSegment(segment) {
|
|
|
729
1281
|
async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
730
1282
|
if (!isStudioRequestAuthorized(req, options)) {
|
|
731
1283
|
const auth = studioAuthRequiredResponse();
|
|
732
|
-
|
|
1284
|
+
sendJson2(res, auth.status, auth.body, auth.headers);
|
|
733
1285
|
return true;
|
|
734
1286
|
}
|
|
735
1287
|
if (pathname === "/api/health") {
|
|
736
|
-
|
|
1288
|
+
sendJson2(res, 200, {
|
|
737
1289
|
ok: true,
|
|
738
1290
|
readOnly: true,
|
|
739
1291
|
mode: "studio",
|
|
@@ -746,7 +1298,7 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
746
1298
|
return true;
|
|
747
1299
|
}
|
|
748
1300
|
if (pathname === "/api/projects") {
|
|
749
|
-
|
|
1301
|
+
sendJson2(res, 200, {
|
|
750
1302
|
registryName: ctx.registry.name,
|
|
751
1303
|
projects: summarizeProjects(ctx.projects),
|
|
752
1304
|
warnings: ctx.importResult.warnings
|
|
@@ -763,56 +1315,56 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
763
1315
|
return true;
|
|
764
1316
|
}
|
|
765
1317
|
if (subpath === "runs" || subpath === "") {
|
|
766
|
-
|
|
1318
|
+
sendJson2(res, 200, {
|
|
767
1319
|
projectId,
|
|
768
1320
|
runs: await loadProjectRunsView(projectCtx, ctx.db)
|
|
769
1321
|
});
|
|
770
1322
|
return true;
|
|
771
1323
|
}
|
|
772
1324
|
if (subpath === "sessions") {
|
|
773
|
-
|
|
1325
|
+
sendJson2(res, 200, {
|
|
774
1326
|
projectId,
|
|
775
1327
|
...await loadProjectSessionsView(projectCtx)
|
|
776
1328
|
});
|
|
777
1329
|
return true;
|
|
778
1330
|
}
|
|
779
1331
|
if (subpath === "suites") {
|
|
780
|
-
|
|
1332
|
+
sendJson2(res, 200, {
|
|
781
1333
|
projectId,
|
|
782
1334
|
...await loadProjectSuitesView(projectCtx)
|
|
783
1335
|
});
|
|
784
1336
|
return true;
|
|
785
1337
|
}
|
|
786
1338
|
if (subpath === "checks") {
|
|
787
|
-
|
|
1339
|
+
sendJson2(res, 200, {
|
|
788
1340
|
projectId,
|
|
789
1341
|
...await loadProjectChecksView(projectCtx)
|
|
790
1342
|
});
|
|
791
1343
|
return true;
|
|
792
1344
|
}
|
|
793
1345
|
if (subpath === "observations") {
|
|
794
|
-
|
|
1346
|
+
sendJson2(res, 200, {
|
|
795
1347
|
projectId,
|
|
796
1348
|
...await loadProjectObservationsView(projectCtx)
|
|
797
1349
|
});
|
|
798
1350
|
return true;
|
|
799
1351
|
}
|
|
800
1352
|
if (subpath === "guardrails") {
|
|
801
|
-
|
|
1353
|
+
sendJson2(res, 200, {
|
|
802
1354
|
projectId,
|
|
803
1355
|
...await loadProjectGuardrailsView(projectCtx)
|
|
804
1356
|
});
|
|
805
1357
|
return true;
|
|
806
1358
|
}
|
|
807
1359
|
if (subpath === "redaction") {
|
|
808
|
-
|
|
1360
|
+
sendJson2(res, 200, {
|
|
809
1361
|
projectId,
|
|
810
1362
|
...await loadProjectRedactionView(projectCtx)
|
|
811
1363
|
});
|
|
812
1364
|
return true;
|
|
813
1365
|
}
|
|
814
1366
|
if (subpath === "reports") {
|
|
815
|
-
|
|
1367
|
+
sendJson2(res, 200, {
|
|
816
1368
|
projectId,
|
|
817
1369
|
...await loadProjectReportsView(projectCtx)
|
|
818
1370
|
});
|
|
@@ -824,7 +1376,7 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
824
1376
|
if (pathname === "/api/search") {
|
|
825
1377
|
const projectId = url.searchParams.get("projectId");
|
|
826
1378
|
if (!projectId) {
|
|
827
|
-
|
|
1379
|
+
sendJson2(res, 400, { error: "projectId query parameter is required." });
|
|
828
1380
|
return true;
|
|
829
1381
|
}
|
|
830
1382
|
const projectCtx = getImportedProject(ctx.db, ctx.projects, projectId);
|
|
@@ -832,7 +1384,7 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
832
1384
|
notFound(res, `Project not found: ${projectId}`);
|
|
833
1385
|
return true;
|
|
834
1386
|
}
|
|
835
|
-
|
|
1387
|
+
sendJson2(res, 200, {
|
|
836
1388
|
projectId,
|
|
837
1389
|
...await loadProjectSearchView(projectCtx, ctx.db, url.searchParams)
|
|
838
1390
|
});
|
|
@@ -841,7 +1393,7 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
841
1393
|
if (pathname === "/api/diff") {
|
|
842
1394
|
const projectId = url.searchParams.get("projectId");
|
|
843
1395
|
if (!projectId) {
|
|
844
|
-
|
|
1396
|
+
sendJson2(res, 400, { error: "projectId query parameter is required." });
|
|
845
1397
|
return true;
|
|
846
1398
|
}
|
|
847
1399
|
const projectCtx = getImportedProject(ctx.db, ctx.projects, projectId);
|
|
@@ -850,20 +1402,20 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
850
1402
|
return true;
|
|
851
1403
|
}
|
|
852
1404
|
try {
|
|
853
|
-
|
|
1405
|
+
sendJson2(res, 200, {
|
|
854
1406
|
projectId,
|
|
855
1407
|
...await loadProjectDiffView(projectCtx, url.searchParams)
|
|
856
1408
|
});
|
|
857
1409
|
} catch (error) {
|
|
858
1410
|
const message = error instanceof Error ? error.message : String(error);
|
|
859
|
-
|
|
1411
|
+
sendJson2(res, 400, { error: message });
|
|
860
1412
|
}
|
|
861
1413
|
return true;
|
|
862
1414
|
}
|
|
863
1415
|
if (pathname === "/api/reports") {
|
|
864
1416
|
const projectId = url.searchParams.get("projectId");
|
|
865
1417
|
if (!projectId) {
|
|
866
|
-
|
|
1418
|
+
sendJson2(res, 400, { error: "projectId query parameter is required." });
|
|
867
1419
|
return true;
|
|
868
1420
|
}
|
|
869
1421
|
const projectCtx = getImportedProject(ctx.db, ctx.projects, projectId);
|
|
@@ -871,7 +1423,7 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
871
1423
|
notFound(res, `Project not found: ${projectId}`);
|
|
872
1424
|
return true;
|
|
873
1425
|
}
|
|
874
|
-
|
|
1426
|
+
sendJson2(res, 200, {
|
|
875
1427
|
projectId,
|
|
876
1428
|
...await loadProjectReportsView(projectCtx)
|
|
877
1429
|
});
|
|
@@ -880,7 +1432,7 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
880
1432
|
if (pathname === "/api/bundles/export") {
|
|
881
1433
|
const projectId = url.searchParams.get("projectId");
|
|
882
1434
|
if (!projectId) {
|
|
883
|
-
|
|
1435
|
+
sendJson2(res, 400, { error: "projectId query parameter is required." });
|
|
884
1436
|
return true;
|
|
885
1437
|
}
|
|
886
1438
|
const projectCtx = getImportedProject(ctx.db, ctx.projects, projectId);
|
|
@@ -889,10 +1441,10 @@ async function handleStudioRoute(req, res, ctx, options, pathname, url) {
|
|
|
889
1441
|
return true;
|
|
890
1442
|
}
|
|
891
1443
|
try {
|
|
892
|
-
|
|
1444
|
+
sendJson2(res, 200, await loadBundleExportView(projectCtx, url.searchParams));
|
|
893
1445
|
} catch (error) {
|
|
894
1446
|
const message = error instanceof Error ? error.message : String(error);
|
|
895
|
-
|
|
1447
|
+
sendJson2(res, 400, { error: message });
|
|
896
1448
|
}
|
|
897
1449
|
return true;
|
|
898
1450
|
}
|
|
@@ -931,15 +1483,21 @@ function createStudioServer(options = {}) {
|
|
|
931
1483
|
}
|
|
932
1484
|
const server = http.createServer(async (req, res) => {
|
|
933
1485
|
try {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1486
|
+
const method = req.method ?? "GET";
|
|
1487
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
1488
|
+
const pathname = url.pathname;
|
|
937
1489
|
if (!contextPromise) {
|
|
938
1490
|
contextPromise = createStudioContext(options);
|
|
939
1491
|
}
|
|
940
1492
|
const ctx = await contextPromise;
|
|
941
|
-
const
|
|
942
|
-
|
|
1493
|
+
const httpConfig = resolveHttpIngestConfig(options, ctx.registry.ingest?.http?.enabled);
|
|
1494
|
+
if (isHttpIngestRoute(pathname, httpConfig)) {
|
|
1495
|
+
const handled2 = await handleHttpIngestRequest(req, res, ctx, options, pathname);
|
|
1496
|
+
if (handled2) return;
|
|
1497
|
+
}
|
|
1498
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
1499
|
+
return badRequest(res, "Only GET is supported.");
|
|
1500
|
+
}
|
|
943
1501
|
if (pathname === "/" || pathname === "/index.html") {
|
|
944
1502
|
if (req.method === "HEAD") {
|
|
945
1503
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
@@ -992,13 +1550,382 @@ async function startStudioServer(options = {}) {
|
|
|
992
1550
|
});
|
|
993
1551
|
});
|
|
994
1552
|
}
|
|
1553
|
+
var DEFAULT_GITHUB_TOKEN_ENV = "GITHUB_TOKEN";
|
|
1554
|
+
var GITHUB_API_BASE = "https://api.github.com";
|
|
1555
|
+
var MAX_ARTIFACT_BYTES = 52428800;
|
|
1556
|
+
function resolveTokenEnv(registry, override) {
|
|
1557
|
+
const fromRegistry = registry.ingest?.github?.tokenEnv?.trim();
|
|
1558
|
+
const envName = override?.trim() || fromRegistry || DEFAULT_GITHUB_TOKEN_ENV;
|
|
1559
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(envName)) {
|
|
1560
|
+
throw new Error("token env name must be an uppercase identifier");
|
|
1561
|
+
}
|
|
1562
|
+
return envName;
|
|
1563
|
+
}
|
|
1564
|
+
function resolveToken(envName) {
|
|
1565
|
+
const token = process.env[envName]?.trim();
|
|
1566
|
+
if (!token) {
|
|
1567
|
+
throw new Error(`missing GitHub token in environment variable ${envName}`);
|
|
1568
|
+
}
|
|
1569
|
+
return token;
|
|
1570
|
+
}
|
|
1571
|
+
function githubHeaders(token) {
|
|
1572
|
+
return {
|
|
1573
|
+
Authorization: `Bearer ${token}`,
|
|
1574
|
+
Accept: "application/vnd.github+json",
|
|
1575
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
1576
|
+
"User-Agent": "agent-inspect-studio-ingest"
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
async function readResponseBody(response, maxBytes) {
|
|
1580
|
+
const lengthHeader = response.headers.get("content-length");
|
|
1581
|
+
if (lengthHeader) {
|
|
1582
|
+
const length = Number(lengthHeader);
|
|
1583
|
+
if (Number.isFinite(length) && length > maxBytes) {
|
|
1584
|
+
throw new Error("artifact exceeds size limit");
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1588
|
+
if (arrayBuffer.byteLength > maxBytes) {
|
|
1589
|
+
throw new Error("artifact exceeds size limit");
|
|
1590
|
+
}
|
|
1591
|
+
return Buffer.from(arrayBuffer);
|
|
1592
|
+
}
|
|
1593
|
+
async function downloadGitHubArtifactArchive(options) {
|
|
1594
|
+
const fetchFn = options.fetchImpl ?? fetch;
|
|
1595
|
+
const { owner, name } = parseGitHubRepo(options.repo);
|
|
1596
|
+
const runId = options.runId.trim();
|
|
1597
|
+
const artifactName = options.artifactName.trim();
|
|
1598
|
+
if (!/^\d+$/.test(runId)) {
|
|
1599
|
+
throw new Error("run-id must be a numeric workflow run id");
|
|
1600
|
+
}
|
|
1601
|
+
if (artifactName === "") {
|
|
1602
|
+
throw new Error("artifact name must be non-empty");
|
|
1603
|
+
}
|
|
1604
|
+
const listUrl = `${GITHUB_API_BASE}/repos/${owner}/${name}/actions/runs/${runId}/artifacts`;
|
|
1605
|
+
const listResponse = await fetchFn(listUrl, {
|
|
1606
|
+
headers: githubHeaders(options.token)
|
|
1607
|
+
});
|
|
1608
|
+
if (!listResponse.ok) {
|
|
1609
|
+
throw new Error(`GitHub artifact lookup failed (${listResponse.status})`);
|
|
1610
|
+
}
|
|
1611
|
+
const payload = await listResponse.json();
|
|
1612
|
+
const artifact = payload.artifacts?.find((item) => item.name === artifactName);
|
|
1613
|
+
if (!artifact?.archive_download_url) {
|
|
1614
|
+
throw new Error(`artifact not found for run ${runId}: ${artifactName}`);
|
|
1615
|
+
}
|
|
1616
|
+
if (artifact.expired === true) {
|
|
1617
|
+
throw new Error(`artifact expired for run ${runId}: ${artifactName}`);
|
|
1618
|
+
}
|
|
1619
|
+
if (typeof artifact.size_in_bytes === "number" && artifact.size_in_bytes > MAX_ARTIFACT_BYTES) {
|
|
1620
|
+
throw new Error("artifact exceeds size limit");
|
|
1621
|
+
}
|
|
1622
|
+
const downloadResponse = await fetchFn(artifact.archive_download_url, {
|
|
1623
|
+
headers: githubHeaders(options.token),
|
|
1624
|
+
redirect: "follow"
|
|
1625
|
+
});
|
|
1626
|
+
if (!downloadResponse.ok) {
|
|
1627
|
+
throw new Error(`GitHub artifact download failed (${downloadResponse.status})`);
|
|
1628
|
+
}
|
|
1629
|
+
return readResponseBody(downloadResponse, MAX_ARTIFACT_BYTES);
|
|
1630
|
+
}
|
|
1631
|
+
async function importGitHubArtifact(options) {
|
|
1632
|
+
const registryImportWarnings = [];
|
|
1633
|
+
const errors = [];
|
|
1634
|
+
if (!options.enabled) {
|
|
1635
|
+
return {
|
|
1636
|
+
skipped: true,
|
|
1637
|
+
reason: "GitHub artifact ingest is disabled; use studio import github explicitly",
|
|
1638
|
+
imported: false,
|
|
1639
|
+
registryImportWarnings,
|
|
1640
|
+
errors
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
let token;
|
|
1644
|
+
let tokenEnv;
|
|
1645
|
+
try {
|
|
1646
|
+
const { owner, name } = parseGitHubRepo(options.repo);
|
|
1647
|
+
tokenEnv = resolveTokenEnv(options.registry, options.tokenEnv);
|
|
1648
|
+
token = resolveToken(tokenEnv);
|
|
1649
|
+
const sourceKey = buildGitHubArtifactSourceKey({
|
|
1650
|
+
owner,
|
|
1651
|
+
repo: name,
|
|
1652
|
+
runId: options.runId,
|
|
1653
|
+
artifactName: options.artifactName
|
|
1654
|
+
});
|
|
1655
|
+
const archive = await downloadGitHubArtifactArchive({
|
|
1656
|
+
repo: options.repo,
|
|
1657
|
+
runId: options.runId,
|
|
1658
|
+
artifactName: options.artifactName,
|
|
1659
|
+
token,
|
|
1660
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1661
|
+
});
|
|
1662
|
+
const contentHash = crypto.createHash("sha256").update(archive).digest("hex");
|
|
1663
|
+
const existing = findIngestFileBySourceKey(options.db, sourceKey);
|
|
1664
|
+
if (existing && existing.contentHash === contentHash) {
|
|
1665
|
+
return {
|
|
1666
|
+
skipped: false,
|
|
1667
|
+
imported: false,
|
|
1668
|
+
sourceKey,
|
|
1669
|
+
contentHash,
|
|
1670
|
+
destPath: existing.destPath,
|
|
1671
|
+
registryImportWarnings,
|
|
1672
|
+
errors
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
const dirs = resolveImportDirs(options.registryPath, options.registry);
|
|
1676
|
+
const fileName = `${options.artifactName}.zip`;
|
|
1677
|
+
const destPath = uniqueDestPath(dirs.bundlesDir, fileName, contentHash);
|
|
1678
|
+
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
1679
|
+
await promises.mkdir(dirs.bundlesDir, { recursive: true });
|
|
1680
|
+
await promises.writeFile(destPath, archive);
|
|
1681
|
+
const importedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1682
|
+
insertIngestFile(options.db, {
|
|
1683
|
+
sourceKey,
|
|
1684
|
+
sourceName: fileName,
|
|
1685
|
+
destPath,
|
|
1686
|
+
kind: "bundle",
|
|
1687
|
+
contentHash,
|
|
1688
|
+
importedAt
|
|
1689
|
+
});
|
|
1690
|
+
const registryImport = await importStudioRegistry({
|
|
1691
|
+
db: options.db,
|
|
1692
|
+
registry: options.registry,
|
|
1693
|
+
registryPath: options.registryPath
|
|
1694
|
+
});
|
|
1695
|
+
registryImportWarnings.push(...registryImport.warnings);
|
|
1696
|
+
return {
|
|
1697
|
+
skipped: false,
|
|
1698
|
+
imported: true,
|
|
1699
|
+
sourceKey,
|
|
1700
|
+
contentHash,
|
|
1701
|
+
destPath,
|
|
1702
|
+
registryImportWarnings,
|
|
1703
|
+
errors
|
|
1704
|
+
};
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1707
|
+
errors.push(sanitizeSafeErrorMessage(message, token));
|
|
1708
|
+
return {
|
|
1709
|
+
skipped: false,
|
|
1710
|
+
imported: false,
|
|
1711
|
+
registryImportWarnings,
|
|
1712
|
+
errors
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
async function runStudioGitHubArtifactImport(options) {
|
|
1717
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1718
|
+
const registryPath = await resolveStudioRegistryPath({
|
|
1719
|
+
...options.workspacePath !== void 0 ? { workspacePath: options.workspacePath } : {},
|
|
1720
|
+
cwd
|
|
1721
|
+
});
|
|
1722
|
+
const registryRead = await readStudioRegistryFile(registryPath);
|
|
1723
|
+
if (!registryRead.ok || registryRead.registry === void 0) {
|
|
1724
|
+
throw new Error(registryRead.errors.join("; ") || "invalid studio registry");
|
|
1725
|
+
}
|
|
1726
|
+
const dbPath = resolveStudioDbPath({
|
|
1727
|
+
...options.dbPath !== void 0 ? { dbPath: options.dbPath } : {},
|
|
1728
|
+
cwd
|
|
1729
|
+
});
|
|
1730
|
+
const db = openStudioDb(dbPath);
|
|
1731
|
+
return importGitHubArtifact({
|
|
1732
|
+
db,
|
|
1733
|
+
registryPath,
|
|
1734
|
+
registry: registryRead.registry,
|
|
1735
|
+
repo: options.repo,
|
|
1736
|
+
runId: options.runId,
|
|
1737
|
+
artifactName: options.artifact,
|
|
1738
|
+
enabled: true,
|
|
1739
|
+
...options.tokenEnv !== void 0 ? { tokenEnv: options.tokenEnv } : {},
|
|
1740
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
async function pathExists(filePath) {
|
|
1744
|
+
try {
|
|
1745
|
+
await promises.access(filePath);
|
|
1746
|
+
return true;
|
|
1747
|
+
} catch {
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
async function validateBundleDirectory(bundleDir) {
|
|
1752
|
+
const errors = [];
|
|
1753
|
+
const metadataPath = path8__default.default.join(bundleDir, "metadata.json");
|
|
1754
|
+
if (!await pathExists(metadataPath)) {
|
|
1755
|
+
errors.push("bundle missing metadata.json");
|
|
1756
|
+
return errors;
|
|
1757
|
+
}
|
|
1758
|
+
try {
|
|
1759
|
+
const raw = await promises.readFile(metadataPath, "utf8");
|
|
1760
|
+
const parsed = JSON.parse(raw);
|
|
1761
|
+
if (typeof parsed.agentInspectVersion !== "string") {
|
|
1762
|
+
errors.push("bundle metadata.json missing agentInspectVersion");
|
|
1763
|
+
}
|
|
1764
|
+
if (!Array.isArray(parsed.runIds) || parsed.runIds.length === 0) {
|
|
1765
|
+
errors.push("bundle metadata.json missing runIds");
|
|
1766
|
+
}
|
|
1767
|
+
} catch {
|
|
1768
|
+
errors.push("bundle metadata.json is invalid JSON");
|
|
1769
|
+
}
|
|
1770
|
+
return errors;
|
|
1771
|
+
}
|
|
1772
|
+
async function copyDirectoryRecursive(sourceDir, destDir) {
|
|
1773
|
+
await promises.mkdir(destDir, { recursive: true });
|
|
1774
|
+
const entries = await promises.readdir(sourceDir, { withFileTypes: true });
|
|
1775
|
+
for (const entry of entries) {
|
|
1776
|
+
const from = path8__default.default.join(sourceDir, entry.name);
|
|
1777
|
+
const to = path8__default.default.join(destDir, entry.name);
|
|
1778
|
+
if (entry.isDirectory()) {
|
|
1779
|
+
await copyDirectoryRecursive(from, to);
|
|
1780
|
+
} else if (entry.isFile()) {
|
|
1781
|
+
await promises.copyFile(from, to);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
async function importBundleUpload(options) {
|
|
1786
|
+
const registryImportWarnings = [];
|
|
1787
|
+
const errors = [];
|
|
1788
|
+
if (!options.enabled) {
|
|
1789
|
+
return {
|
|
1790
|
+
skipped: true,
|
|
1791
|
+
reason: "bundle upload ingest is disabled; use studio import bundle explicitly",
|
|
1792
|
+
imported: false,
|
|
1793
|
+
errors,
|
|
1794
|
+
registryImportWarnings
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
const bundlePath = path8__default.default.resolve(options.bundlePath);
|
|
1798
|
+
let bundleStat;
|
|
1799
|
+
try {
|
|
1800
|
+
bundleStat = await promises.stat(bundlePath);
|
|
1801
|
+
} catch {
|
|
1802
|
+
return {
|
|
1803
|
+
skipped: false,
|
|
1804
|
+
imported: false,
|
|
1805
|
+
errors: ["bundle path does not exist"],
|
|
1806
|
+
registryImportWarnings
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
if (!bundleStat.isDirectory()) {
|
|
1810
|
+
return {
|
|
1811
|
+
skipped: false,
|
|
1812
|
+
imported: false,
|
|
1813
|
+
errors: ["bundle path must be a directory produced by agent-inspect bundle"],
|
|
1814
|
+
registryImportWarnings
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
const validationErrors = await validateBundleDirectory(bundlePath);
|
|
1818
|
+
if (validationErrors.length > 0) {
|
|
1819
|
+
return {
|
|
1820
|
+
skipped: false,
|
|
1821
|
+
imported: false,
|
|
1822
|
+
errors: validationErrors,
|
|
1823
|
+
registryImportWarnings
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
const metadataRaw = await promises.readFile(path8__default.default.join(bundlePath, "metadata.json"), "utf8");
|
|
1827
|
+
const contentHash = crypto.createHash("sha256").update(metadataRaw).digest("hex");
|
|
1828
|
+
const sourceKey = `bundle:${bundlePath}`;
|
|
1829
|
+
const existing = findIngestFileBySourceKey(options.db, sourceKey);
|
|
1830
|
+
if (existing && existing.contentHash === contentHash) {
|
|
1831
|
+
return {
|
|
1832
|
+
skipped: false,
|
|
1833
|
+
imported: false,
|
|
1834
|
+
destPath: existing.destPath,
|
|
1835
|
+
sourceKey,
|
|
1836
|
+
errors,
|
|
1837
|
+
registryImportWarnings
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
const dirs = resolveImportDirs(options.registryPath, options.registry);
|
|
1841
|
+
const folderName = path8__default.default.basename(bundlePath);
|
|
1842
|
+
const destPath = uniqueDestPath(dirs.bundlesDir, folderName, contentHash);
|
|
1843
|
+
assertPathUnderRoot(destPath, dirs.registryDir);
|
|
1844
|
+
try {
|
|
1845
|
+
await copyDirectoryRecursive(bundlePath, destPath);
|
|
1846
|
+
insertIngestFile(options.db, {
|
|
1847
|
+
sourceKey,
|
|
1848
|
+
sourceName: folderName,
|
|
1849
|
+
destPath,
|
|
1850
|
+
kind: "bundle",
|
|
1851
|
+
contentHash,
|
|
1852
|
+
importedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1853
|
+
});
|
|
1854
|
+
const registryImport = await importStudioRegistry({
|
|
1855
|
+
db: options.db,
|
|
1856
|
+
registry: options.registry,
|
|
1857
|
+
registryPath: options.registryPath
|
|
1858
|
+
});
|
|
1859
|
+
registryImportWarnings.push(...registryImport.warnings);
|
|
1860
|
+
return {
|
|
1861
|
+
skipped: false,
|
|
1862
|
+
imported: true,
|
|
1863
|
+
destPath,
|
|
1864
|
+
sourceKey,
|
|
1865
|
+
errors,
|
|
1866
|
+
registryImportWarnings
|
|
1867
|
+
};
|
|
1868
|
+
} catch (error) {
|
|
1869
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1870
|
+
return {
|
|
1871
|
+
skipped: false,
|
|
1872
|
+
imported: false,
|
|
1873
|
+
errors: [message],
|
|
1874
|
+
registryImportWarnings
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
async function runStudioBundleUploadImport(options) {
|
|
1879
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1880
|
+
const registryPath = await resolveStudioRegistryPath({
|
|
1881
|
+
...options.workspacePath !== void 0 ? { workspacePath: options.workspacePath } : {},
|
|
1882
|
+
cwd
|
|
1883
|
+
});
|
|
1884
|
+
const registryRead = await readStudioRegistryFile(registryPath);
|
|
1885
|
+
if (!registryRead.ok || registryRead.registry === void 0) {
|
|
1886
|
+
throw new Error(registryRead.errors.join("; ") || "invalid studio registry");
|
|
1887
|
+
}
|
|
1888
|
+
const dbPath = resolveStudioDbPath({
|
|
1889
|
+
...options.dbPath !== void 0 ? { dbPath: options.dbPath } : {},
|
|
1890
|
+
cwd
|
|
1891
|
+
});
|
|
1892
|
+
const db = openStudioDb(dbPath);
|
|
1893
|
+
return importBundleUpload({
|
|
1894
|
+
db,
|
|
1895
|
+
registryPath,
|
|
1896
|
+
registry: registryRead.registry,
|
|
1897
|
+
bundlePath: options.bundlePath,
|
|
1898
|
+
enabled: true
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
995
1901
|
|
|
1902
|
+
exports.HTTP_INGEST_ARTIFACT_PATH = HTTP_INGEST_ARTIFACT_PATH;
|
|
1903
|
+
exports.HTTP_INGEST_BUNDLE_PATH = HTTP_INGEST_BUNDLE_PATH;
|
|
996
1904
|
exports.createStudioContext = createStudioContext;
|
|
997
1905
|
exports.createStudioServer = createStudioServer;
|
|
1906
|
+
exports.downloadGitHubArtifactArchive = downloadGitHubArtifactArchive;
|
|
1907
|
+
exports.extractIngestTokenFromRequest = extractIngestTokenFromRequest;
|
|
1908
|
+
exports.handleHttpIngestRequest = handleHttpIngestRequest;
|
|
1909
|
+
exports.importBundleUpload = importBundleUpload;
|
|
1910
|
+
exports.importFileDrop = importFileDrop;
|
|
1911
|
+
exports.importFileDropFromRegistry = importFileDropFromRegistry;
|
|
1912
|
+
exports.importGitHubArtifact = importGitHubArtifact;
|
|
1913
|
+
exports.isHttpIngestRoute = isHttpIngestRoute;
|
|
1914
|
+
exports.isIngestTokenValid = isIngestTokenValid;
|
|
1915
|
+
exports.openStudioDb = openStudioDb;
|
|
998
1916
|
exports.parseStudioRegistry = parseStudioRegistry;
|
|
999
1917
|
exports.readStudioRegistryFile = readStudioRegistryFile;
|
|
1918
|
+
exports.resolveHttpIngestConfig = resolveHttpIngestConfig;
|
|
1919
|
+
exports.resolveIngestToken = resolveIngestToken;
|
|
1920
|
+
exports.resolveIngestTokenEnv = resolveIngestTokenEnv;
|
|
1921
|
+
exports.resolveStudioDbPath = resolveStudioDbPath;
|
|
1922
|
+
exports.resolveStudioRegistryPath = resolveStudioRegistryPath;
|
|
1923
|
+
exports.runStudioBundleUploadImport = runStudioBundleUploadImport;
|
|
1924
|
+
exports.runStudioFileDropImport = runStudioFileDropImport;
|
|
1925
|
+
exports.runStudioGitHubArtifactImport = runStudioGitHubArtifactImport;
|
|
1000
1926
|
exports.startStudioServer = startStudioServer;
|
|
1001
1927
|
exports.studioIndexHtml = studioIndexHtml;
|
|
1002
1928
|
exports.summarizeProjects = summarizeProjects;
|
|
1929
|
+
exports.validateBundleDirectory = validateBundleDirectory;
|
|
1003
1930
|
//# sourceMappingURL=index.cjs.map
|
|
1004
1931
|
//# sourceMappingURL=index.cjs.map
|