agentweaver 0.1.18 → 0.1.19
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/README.md +8 -0
- package/dist/index.js +6 -2
- package/dist/interactive/controller.js +74 -0
- package/dist/interactive/state.js +9 -0
- package/dist/interactive/web/index.js +149 -2
- package/dist/interactive/web/protocol.js +7 -1
- package/dist/interactive/web/server.js +439 -3
- package/dist/interactive/web/static/app.js +873 -2
- package/dist/interactive/web/static/index.html +37 -0
- package/dist/interactive/web/static/styles.css +1 -1
- package/dist/interactive/web/static/styles.input.css +380 -0
- package/dist/runtime/artifact-catalog.js +379 -0
- package/package.json +1 -1
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, timingSafeEqual } from "node:crypto";
|
|
3
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
4
4
|
import http from "node:http";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { parseArtifactReference } from "../../artifact-manifest.js";
|
|
9
|
+
import { scopeArtifactsDir, scopeWorkspaceDir } from "../../artifacts.js";
|
|
10
|
+
import { groupArtifactCatalog, inferArtifactRenderKind, inferArtifactRole, inferArtifactTitle, } from "../../runtime/artifact-catalog.js";
|
|
11
|
+
import { createArtifactRegistry } from "../../runtime/artifact-registry.js";
|
|
8
12
|
import { parseClientAction } from "./protocol.js";
|
|
9
13
|
const STATIC_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "static");
|
|
14
|
+
const MAX_PREVIEW_SIZE = 512 * 1024;
|
|
15
|
+
const MAX_JSON_PARSE_SIZE = 2 * 1024 * 1024;
|
|
10
16
|
const CONTENT_TYPES = new Map([
|
|
11
17
|
[".html", "text/html; charset=utf-8"],
|
|
12
18
|
[".css", "text/css; charset=utf-8"],
|
|
@@ -15,6 +21,16 @@ const CONTENT_TYPES = new Map([
|
|
|
15
21
|
[".svg", "image/svg+xml; charset=utf-8"],
|
|
16
22
|
]);
|
|
17
23
|
const BASIC_AUTH_REALM = "AgentWeaver Web UI";
|
|
24
|
+
const ARTIFACT_API_PREFIX = "/__agentweaver/api/artifacts";
|
|
25
|
+
const ARTIFACT_ERROR_STATUSES = {
|
|
26
|
+
invalid_id: 400,
|
|
27
|
+
missing_scope: 400,
|
|
28
|
+
scope_mismatch: 403,
|
|
29
|
+
forbidden_path: 403,
|
|
30
|
+
not_found: 404,
|
|
31
|
+
unsupported_preview: 415,
|
|
32
|
+
read_failed: 500,
|
|
33
|
+
};
|
|
18
34
|
function hashCredential(value) {
|
|
19
35
|
return createHash("sha256").update(value, "utf8").digest();
|
|
20
36
|
}
|
|
@@ -106,6 +122,405 @@ function serveStaticAsset(request, response) {
|
|
|
106
122
|
response.end(readFileSync(assetPath));
|
|
107
123
|
return true;
|
|
108
124
|
}
|
|
125
|
+
function writeJson(response, statusCode, value) {
|
|
126
|
+
response.writeHead(statusCode, {
|
|
127
|
+
"content-type": "application/json; charset=utf-8",
|
|
128
|
+
"cache-control": "no-store",
|
|
129
|
+
});
|
|
130
|
+
response.end(JSON.stringify(value));
|
|
131
|
+
}
|
|
132
|
+
function writeArtifactApiError(response, code, message, artifact) {
|
|
133
|
+
writeJson(response, ARTIFACT_ERROR_STATUSES[code], {
|
|
134
|
+
code,
|
|
135
|
+
message,
|
|
136
|
+
...(artifact ? { artifact } : {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function safeArtifactMetadata(item) {
|
|
140
|
+
return {
|
|
141
|
+
id: item.id,
|
|
142
|
+
scopeKey: item.scopeKey,
|
|
143
|
+
runId: item.runId,
|
|
144
|
+
logicalKey: item.logicalKey,
|
|
145
|
+
title: item.title,
|
|
146
|
+
relativePath: item.relativePath,
|
|
147
|
+
kind: item.kind,
|
|
148
|
+
role: item.role,
|
|
149
|
+
phaseId: item.phaseId,
|
|
150
|
+
stepId: item.stepId,
|
|
151
|
+
schemaId: item.schemaId,
|
|
152
|
+
sizeBytes: item.sizeBytes,
|
|
153
|
+
updatedAt: item.updatedAt,
|
|
154
|
+
isLatest: item.isLatest,
|
|
155
|
+
source: item.source,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function normalizePathSeparators(value) {
|
|
159
|
+
return value.replace(/\\/g, "/");
|
|
160
|
+
}
|
|
161
|
+
function isInsideDirectory(parent, candidate) {
|
|
162
|
+
const relative = path.relative(parent, candidate);
|
|
163
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
164
|
+
}
|
|
165
|
+
function safeDecodeURIComponent(value) {
|
|
166
|
+
try {
|
|
167
|
+
return decodeURIComponent(value);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function parseArtifactApiRoute(requestUrl) {
|
|
174
|
+
const parsed = new URL(requestUrl ?? "/", "http://agentweaver.local");
|
|
175
|
+
const scopeKey = parsed.searchParams.get("scope");
|
|
176
|
+
const runIds = parsed.searchParams.getAll("runId").filter((value, index, values) => (value.length > 0 && values.indexOf(value) === index));
|
|
177
|
+
if (parsed.pathname === ARTIFACT_API_PREFIX) {
|
|
178
|
+
return { kind: "list", scopeKey, runIds };
|
|
179
|
+
}
|
|
180
|
+
if (!parsed.pathname.startsWith(`${ARTIFACT_API_PREFIX}/`)) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const suffix = parsed.pathname.slice(`${ARTIFACT_API_PREFIX}/`.length);
|
|
184
|
+
for (const action of ["preview", "raw", "download"]) {
|
|
185
|
+
const actionSuffix = `/${action}`;
|
|
186
|
+
if (!suffix.endsWith(actionSuffix)) {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const encodedId = suffix.slice(0, -actionSuffix.length);
|
|
190
|
+
const artifactId = safeDecodeURIComponent(encodedId);
|
|
191
|
+
if (!artifactId || artifactId.includes("\0")) {
|
|
192
|
+
return { kind: "content", artifactId: "", action, scopeKey, runIds };
|
|
193
|
+
}
|
|
194
|
+
return { kind: "content", artifactId, action, scopeKey, runIds };
|
|
195
|
+
}
|
|
196
|
+
return { kind: "content", artifactId: "", action: "preview", scopeKey, runIds };
|
|
197
|
+
}
|
|
198
|
+
function filterCatalog(catalog, scopeKey, runIds) {
|
|
199
|
+
const runIdSet = new Set(runIds);
|
|
200
|
+
let items = catalog.items.filter((item) => item.scopeKey === scopeKey && item.kind === "markdown");
|
|
201
|
+
if (runIdSet.size > 0) {
|
|
202
|
+
items = items.filter((item) => item.runId !== null && runIdSet.has(item.runId));
|
|
203
|
+
}
|
|
204
|
+
const sortedItems = items.slice();
|
|
205
|
+
return {
|
|
206
|
+
scopeKey,
|
|
207
|
+
items: sortedItems,
|
|
208
|
+
groups: groupArtifactCatalog(sortedItems),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function loadArtifactCatalog(options, input) {
|
|
212
|
+
if (!options.getArtifactCatalog) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
return options.getArtifactCatalog(input);
|
|
216
|
+
}
|
|
217
|
+
function scopeRelativePath(scopeKey, payloadPath) {
|
|
218
|
+
const workspaceDir = path.resolve(scopeWorkspaceDir(scopeKey));
|
|
219
|
+
const normalizedPayloadPath = path.resolve(payloadPath);
|
|
220
|
+
if (isInsideDirectory(workspaceDir, normalizedPayloadPath)) {
|
|
221
|
+
return normalizePathSeparators(path.relative(workspaceDir, normalizedPayloadPath));
|
|
222
|
+
}
|
|
223
|
+
return normalizePathSeparators(path.basename(normalizedPayloadPath));
|
|
224
|
+
}
|
|
225
|
+
function itemFromManifest(scopeKey, manifest) {
|
|
226
|
+
const relativePath = scopeRelativePath(scopeKey, manifest.payload_path);
|
|
227
|
+
const kind = inferArtifactRenderKind({
|
|
228
|
+
payloadFamily: manifest.payload_family,
|
|
229
|
+
schemaId: manifest.schema_id,
|
|
230
|
+
filePath: manifest.payload_path,
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
id: manifest.artifact_id,
|
|
234
|
+
scopeKey,
|
|
235
|
+
runId: manifest.run_id,
|
|
236
|
+
logicalKey: manifest.logical_key,
|
|
237
|
+
title: inferArtifactTitle(scopeKey, manifest.logical_key, relativePath),
|
|
238
|
+
relativePath,
|
|
239
|
+
kind,
|
|
240
|
+
role: inferArtifactRole(manifest.logical_key, relativePath),
|
|
241
|
+
phaseId: manifest.phase_id || null,
|
|
242
|
+
stepId: manifest.step_id || null,
|
|
243
|
+
schemaId: manifest.schema_id || null,
|
|
244
|
+
sizeBytes: 0,
|
|
245
|
+
updatedAt: manifest.created_at,
|
|
246
|
+
isLatest: manifest.status === "ready",
|
|
247
|
+
source: "manifest",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function realExistingRoots(scopeKey) {
|
|
251
|
+
const roots = [scopeWorkspaceDir(scopeKey), scopeArtifactsDir(scopeKey)];
|
|
252
|
+
const realRoots = [];
|
|
253
|
+
for (const root of roots) {
|
|
254
|
+
try {
|
|
255
|
+
realRoots.push(realpathSync(root));
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// A missing .artifacts directory should not block regular scope files.
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return [...new Set(realRoots)];
|
|
262
|
+
}
|
|
263
|
+
function assertContainedRegularFile(scopeKey, filePath) {
|
|
264
|
+
let realPath;
|
|
265
|
+
try {
|
|
266
|
+
realPath = realpathSync(filePath);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
throw new ArtifactApiResolutionError("not_found");
|
|
270
|
+
}
|
|
271
|
+
const allowedRoots = realExistingRoots(scopeKey);
|
|
272
|
+
if (allowedRoots.length === 0 || !allowedRoots.some((root) => isInsideDirectory(root, realPath))) {
|
|
273
|
+
throw new ArtifactApiResolutionError("forbidden_path");
|
|
274
|
+
}
|
|
275
|
+
let stats;
|
|
276
|
+
try {
|
|
277
|
+
stats = statSync(realPath);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
throw new ArtifactApiResolutionError("not_found");
|
|
281
|
+
}
|
|
282
|
+
if (!stats.isFile()) {
|
|
283
|
+
throw new ArtifactApiResolutionError("forbidden_path");
|
|
284
|
+
}
|
|
285
|
+
return { filePath, realPath, stats };
|
|
286
|
+
}
|
|
287
|
+
class ArtifactApiResolutionError extends Error {
|
|
288
|
+
code;
|
|
289
|
+
constructor(code) {
|
|
290
|
+
super(code);
|
|
291
|
+
this.code = code;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function catalogFilePath(scopeKey, item) {
|
|
295
|
+
if (path.isAbsolute(item.relativePath) || item.relativePath.includes("\0")) {
|
|
296
|
+
throw new ArtifactApiResolutionError("forbidden_path");
|
|
297
|
+
}
|
|
298
|
+
return path.join(scopeWorkspaceDir(scopeKey), item.relativePath);
|
|
299
|
+
}
|
|
300
|
+
function findCatalogItemForReference(catalog, reference) {
|
|
301
|
+
return catalog.items.find((item) => item.id === reference) ?? null;
|
|
302
|
+
}
|
|
303
|
+
function resolveArtifactRequest(catalog, artifactId) {
|
|
304
|
+
if (!artifactId.trim() || artifactId.includes("\0")) {
|
|
305
|
+
throw new ArtifactApiResolutionError("invalid_id");
|
|
306
|
+
}
|
|
307
|
+
const parsedReference = parseArtifactReference(artifactId);
|
|
308
|
+
if (parsedReference) {
|
|
309
|
+
if (parsedReference.kind === "artifact-id" && parsedReference.parsedId.scopeKey !== catalog.scopeKey) {
|
|
310
|
+
throw new ArtifactApiResolutionError("scope_mismatch");
|
|
311
|
+
}
|
|
312
|
+
let manifest;
|
|
313
|
+
try {
|
|
314
|
+
manifest = createArtifactRegistry().resolveArtifact(catalog.scopeKey, artifactId);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
throw new ArtifactApiResolutionError("not_found");
|
|
318
|
+
}
|
|
319
|
+
const item = findCatalogItemForReference(catalog, manifest.artifact_id) ?? itemFromManifest(catalog.scopeKey, manifest);
|
|
320
|
+
const contained = assertContainedRegularFile(catalog.scopeKey, manifest.payload_path);
|
|
321
|
+
return { item: { ...item, sizeBytes: contained.stats.size }, ...contained };
|
|
322
|
+
}
|
|
323
|
+
const item = findCatalogItemForReference(catalog, artifactId);
|
|
324
|
+
if (!item) {
|
|
325
|
+
throw new ArtifactApiResolutionError("invalid_id");
|
|
326
|
+
}
|
|
327
|
+
if (item.scopeKey !== catalog.scopeKey) {
|
|
328
|
+
throw new ArtifactApiResolutionError("scope_mismatch");
|
|
329
|
+
}
|
|
330
|
+
const contained = assertContainedRegularFile(catalog.scopeKey, catalogFilePath(catalog.scopeKey, item));
|
|
331
|
+
return { item: { ...item, sizeBytes: contained.stats.size }, ...contained };
|
|
332
|
+
}
|
|
333
|
+
function readLeadingBytes(filePath, byteLimit) {
|
|
334
|
+
const fd = openSync(filePath, "r");
|
|
335
|
+
try {
|
|
336
|
+
const buffer = Buffer.alloc(byteLimit);
|
|
337
|
+
const loadedBytes = readSync(fd, buffer, 0, byteLimit, 0);
|
|
338
|
+
return { buffer: buffer.subarray(0, loadedBytes), loadedBytes };
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
closeSync(fd);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function contentTypeForArtifactKind(kind) {
|
|
345
|
+
switch (kind) {
|
|
346
|
+
case "markdown":
|
|
347
|
+
return "text/markdown; charset=utf-8";
|
|
348
|
+
case "json":
|
|
349
|
+
return "application/json; charset=utf-8";
|
|
350
|
+
case "text":
|
|
351
|
+
case "diff":
|
|
352
|
+
return "text/plain; charset=utf-8";
|
|
353
|
+
case "binary":
|
|
354
|
+
case "unknown":
|
|
355
|
+
return "application/octet-stream";
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function sanitizeDownloadFilename(value) {
|
|
359
|
+
const normalizedSeparators = normalizePathSeparators(value);
|
|
360
|
+
const basename = path.posix.basename(normalizedSeparators);
|
|
361
|
+
const sanitized = basename
|
|
362
|
+
.replace(/["\r\n\\/]/g, "_")
|
|
363
|
+
.replace(/[\x00-\x1f\x7f-\x9f]/g, "_")
|
|
364
|
+
.replace(/[^\x20-\x7e]/g, "_")
|
|
365
|
+
.replace(/\s+/g, " ")
|
|
366
|
+
.trim()
|
|
367
|
+
.slice(0, 120);
|
|
368
|
+
return sanitized && sanitized !== "." && sanitized !== ".." ? sanitized : "artifact";
|
|
369
|
+
}
|
|
370
|
+
function writeArtifactBytes(response, resolved, attachment) {
|
|
371
|
+
const headers = {
|
|
372
|
+
"content-type": contentTypeForArtifactKind(resolved.item.kind),
|
|
373
|
+
"cache-control": "no-store",
|
|
374
|
+
"x-content-type-options": "nosniff",
|
|
375
|
+
};
|
|
376
|
+
if (attachment) {
|
|
377
|
+
headers["content-disposition"] = `attachment; filename="${sanitizeDownloadFilename(resolved.item.relativePath || path.basename(resolved.realPath))}"`;
|
|
378
|
+
}
|
|
379
|
+
let bytes;
|
|
380
|
+
try {
|
|
381
|
+
bytes = readFileSync(resolved.realPath);
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
writeArtifactApiError(response, "read_failed", "Artifact content could not be read.", safeArtifactMetadata(resolved.item));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
response.writeHead(200, headers);
|
|
388
|
+
response.end(bytes);
|
|
389
|
+
}
|
|
390
|
+
function previewJsonArtifact(resolved) {
|
|
391
|
+
if (resolved.stats.size <= MAX_JSON_PARSE_SIZE) {
|
|
392
|
+
const loadedBytes = Number(resolved.stats.size);
|
|
393
|
+
const content = readFileSync(resolved.realPath).toString("utf8");
|
|
394
|
+
const contentBytes = Buffer.from(content, "utf8");
|
|
395
|
+
if (contentBytes.length > MAX_PREVIEW_SIZE) {
|
|
396
|
+
return {
|
|
397
|
+
content: contentBytes.subarray(0, MAX_PREVIEW_SIZE).toString("utf8"),
|
|
398
|
+
loadedBytes,
|
|
399
|
+
truncated: true,
|
|
400
|
+
jsonParseSafe: false,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return { content, loadedBytes, truncated: false, jsonParseSafe: true };
|
|
404
|
+
}
|
|
405
|
+
const { buffer, loadedBytes } = readLeadingBytes(resolved.realPath, MAX_PREVIEW_SIZE);
|
|
406
|
+
return {
|
|
407
|
+
content: buffer.toString("utf8"),
|
|
408
|
+
loadedBytes,
|
|
409
|
+
truncated: true,
|
|
410
|
+
jsonParseSafe: false,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
function writeArtifactPreview(response, resolved) {
|
|
414
|
+
if (resolved.item.kind === "binary" || resolved.item.kind === "unknown") {
|
|
415
|
+
writeArtifactApiError(response, "unsupported_preview", "Artifact preview is not supported for this content type.", safeArtifactMetadata(resolved.item));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const preview = resolved.item.kind === "json"
|
|
420
|
+
? previewJsonArtifact(resolved)
|
|
421
|
+
: (() => {
|
|
422
|
+
const byteLimit = Math.min(Number(resolved.stats.size), MAX_PREVIEW_SIZE);
|
|
423
|
+
const { buffer, loadedBytes } = readLeadingBytes(resolved.realPath, byteLimit);
|
|
424
|
+
return {
|
|
425
|
+
content: buffer.toString("utf8"),
|
|
426
|
+
loadedBytes,
|
|
427
|
+
truncated: resolved.stats.size > loadedBytes,
|
|
428
|
+
};
|
|
429
|
+
})();
|
|
430
|
+
writeJson(response, 200, {
|
|
431
|
+
artifact: safeArtifactMetadata(resolved.item),
|
|
432
|
+
renderKind: resolved.item.kind,
|
|
433
|
+
kind: resolved.item.kind,
|
|
434
|
+
encoding: "utf-8",
|
|
435
|
+
content: preview.content,
|
|
436
|
+
truncated: preview.truncated,
|
|
437
|
+
sizeBytes: resolved.stats.size,
|
|
438
|
+
loadedBytes: preview.loadedBytes,
|
|
439
|
+
...(resolved.item.kind === "json" ? { jsonParseSafe: preview.jsonParseSafe } : {}),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
writeArtifactApiError(response, "read_failed", "Artifact preview could not be read.", safeArtifactMetadata(resolved.item));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function handleArtifactApiRequest(request, response, options, auth) {
|
|
447
|
+
if (request.method !== "GET") {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const route = parseArtifactApiRoute(request.url);
|
|
451
|
+
if (!route) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
if (!isAuthorized(request, auth)) {
|
|
455
|
+
writeAuthRequired(response);
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
if (route.kind === "list") {
|
|
459
|
+
if (!route.scopeKey) {
|
|
460
|
+
writeArtifactApiError(response, "missing_scope", "A scope query parameter is required.");
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
const primaryRunId = route.runIds[0];
|
|
464
|
+
void loadArtifactCatalog(options, {
|
|
465
|
+
scopeKey: route.scopeKey,
|
|
466
|
+
...(route.runIds.length === 1 && primaryRunId ? { runId: primaryRunId, runIds: route.runIds } : {}),
|
|
467
|
+
...(route.runIds.length > 1 ? { runIds: route.runIds } : {}),
|
|
468
|
+
})
|
|
469
|
+
.then((catalog) => {
|
|
470
|
+
if (!catalog) {
|
|
471
|
+
writeArtifactApiError(response, "not_found", "Artifact catalog provider is not configured.");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (catalog.scopeKey !== route.scopeKey) {
|
|
475
|
+
writeArtifactApiError(response, "scope_mismatch", "Requested scope does not match the active Web UI scope.");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
writeJson(response, 200, filterCatalog(catalog, route.scopeKey, route.runIds));
|
|
479
|
+
})
|
|
480
|
+
.catch(() => {
|
|
481
|
+
writeArtifactApiError(response, "read_failed", "Artifact catalog could not be loaded.");
|
|
482
|
+
});
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
if (!route.artifactId) {
|
|
486
|
+
writeArtifactApiError(response, "invalid_id", "Artifact identifier is invalid.");
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
const primaryRunId = route.runIds[0];
|
|
490
|
+
void loadArtifactCatalog(options, {
|
|
491
|
+
...(route.scopeKey ? { scopeKey: route.scopeKey } : {}),
|
|
492
|
+
...(route.runIds.length === 1 && primaryRunId ? { runId: primaryRunId, runIds: route.runIds } : {}),
|
|
493
|
+
...(route.runIds.length > 1 ? { runIds: route.runIds } : {}),
|
|
494
|
+
})
|
|
495
|
+
.then((catalog) => {
|
|
496
|
+
if (!catalog) {
|
|
497
|
+
writeArtifactApiError(response, "not_found", "Artifact catalog provider is not configured.");
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (route.scopeKey && catalog.scopeKey !== route.scopeKey) {
|
|
501
|
+
writeArtifactApiError(response, "scope_mismatch", "Requested scope does not match the active Web UI scope.");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
let resolved;
|
|
505
|
+
try {
|
|
506
|
+
resolved = resolveArtifactRequest(catalog, route.artifactId);
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
const code = error instanceof ArtifactApiResolutionError ? error.code : "read_failed";
|
|
510
|
+
writeArtifactApiError(response, code, code === "invalid_id" ? "Artifact identifier is invalid." : "Artifact could not be resolved.");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (route.action === "preview") {
|
|
514
|
+
writeArtifactPreview(response, resolved);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
writeArtifactBytes(response, resolved, route.action === "download");
|
|
518
|
+
})
|
|
519
|
+
.catch(() => {
|
|
520
|
+
writeArtifactApiError(response, "read_failed", "Artifact could not be loaded.");
|
|
521
|
+
});
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
109
524
|
function htmlShell() {
|
|
110
525
|
return `<!doctype html>
|
|
111
526
|
<html lang="en">
|
|
@@ -425,8 +840,29 @@ export async function startWebServer(options) {
|
|
|
425
840
|
let closed = false;
|
|
426
841
|
const server = http.createServer((request, response) => {
|
|
427
842
|
if (request.method === "GET" && request.url === "/__agentweaver/health") {
|
|
428
|
-
response
|
|
429
|
-
|
|
843
|
+
writeJson(response, 200, { ok: true });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (handleArtifactApiRequest(request, response, options, auth)) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (request.method === "GET" && request.url === "/__agentweaver/artifacts") {
|
|
850
|
+
if (!isAuthorized(request, auth)) {
|
|
851
|
+
writeAuthRequired(response);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (!options.getArtifactCatalog) {
|
|
855
|
+
writeJson(response, 404, { error: "Artifact catalog provider is not configured." });
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
void Promise.resolve()
|
|
859
|
+
.then(() => options.getArtifactCatalog?.())
|
|
860
|
+
.then((catalog) => {
|
|
861
|
+
writeJson(response, 200, catalog);
|
|
862
|
+
})
|
|
863
|
+
.catch((error) => {
|
|
864
|
+
writeJson(response, 500, { error: error.message });
|
|
865
|
+
});
|
|
430
866
|
return;
|
|
431
867
|
}
|
|
432
868
|
if (request.method === "GET" && staticAssetPath(request.url)) {
|