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