@assemblyline-agents/node 1.0.0 → 3.0.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/LICENSE +1 -1
- package/dist/adapters.d.ts +5 -1
- package/dist/adapters.d.ts.map +1 -1
- package/dist/adapters.js +38 -17
- package/dist/adapters.js.map +1 -1
- package/dist/auth.d.ts +2 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +10 -22
- package/dist/auth.js.map +1 -1
- package/dist/direct-conversations-http.d.ts.map +1 -1
- package/dist/direct-conversations-http.js +0 -3
- package/dist/direct-conversations-http.js.map +1 -1
- package/dist/http-io.d.ts.map +1 -1
- package/dist/http-io.js +14 -3
- package/dist/http-io.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +337 -30
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +2 -2
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +2 -2
- package/dist/lifecycle.js.map +1 -1
- package/dist/memory-admin-http.d.ts +27 -0
- package/dist/memory-admin-http.d.ts.map +1 -0
- package/dist/memory-admin-http.js +115 -0
- package/dist/memory-admin-http.js.map +1 -0
- package/dist/rate-limit.d.ts +4 -4
- package/dist/rate-limit.d.ts.map +1 -1
- package/dist/rate-limit.js +9 -7
- package/dist/rate-limit.js.map +1 -1
- package/dist/route-match.d.ts +24 -0
- package/dist/route-match.d.ts.map +1 -1
- package/dist/route-match.js +45 -0
- package/dist/route-match.js.map +1 -1
- package/dist/run-replay-http.d.ts +11 -0
- package/dist/run-replay-http.d.ts.map +1 -0
- package/dist/run-replay-http.js +16 -0
- package/dist/run-replay-http.js.map +1 -0
- package/dist/secrets.d.ts +19 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +58 -0
- package/dist/secrets.js.map +1 -0
- package/dist/usage-http.js +10 -10
- package/dist/usage-http.js.map +1 -1
- package/dist/workspace-admin-http.d.ts +5 -0
- package/dist/workspace-admin-http.d.ts.map +1 -0
- package/dist/workspace-admin-http.js +128 -0
- package/dist/workspace-admin-http.js.map +1 -0
- package/package.json +11 -12
package/dist/index.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { codexAgentHarness } from "@assemblyline-agents/codex";
|
|
4
|
-
import { adapter,
|
|
5
|
-
import { AssemblyLineRuntime, RunCapacityError, loadRuntimeBundle, queryAgentRuns, retryAfterSeconds, validateEvalRunOptions } from "@assemblyline-agents/runtime";
|
|
6
|
-
import {
|
|
4
|
+
import { adapter, ConnectionEventError } from "@assemblyline-agents/core";
|
|
5
|
+
import { AssemblyLineRuntime, RunReplayError, RunCapacityError, durableAgentScopeForManifest, loadRuntimeBundle, queryAgentRuns, retryAfterSeconds, validateEvalRunOptions } from "@assemblyline-agents/runtime";
|
|
6
|
+
import { deleteMemoryAdmin, listMemoryAdmin, parseMemoryAdminRoute, readMemoryAdmin } from "./memory-admin-http.js";
|
|
7
|
+
import { createBlobAdapter, createConnectionStores, createMediaProcessors, createSandboxAdapter, createSelfImprovementStores, createStateAdapter, loadRunTimeline, loadRunTimelinePage } from "./adapters.js";
|
|
7
8
|
import { apiRunCreationEnabled, evalRunsEnabled, authorizeOrSend, logHttpRequest, numberBodyValue, numberQueryValue, rateLimitOrSend, stringBodyValue, validateRuntimeHttpSecurity } from "./auth.js";
|
|
8
9
|
import { HttpRequestError, firstHeaderValue, maxRequestBodyBytes, normalizeHeaders, readBody, send, summarizeRun } from "./http-io.js";
|
|
9
10
|
import { serveRunEventStream, serveStreamingRun } from "./run-stream-http.js";
|
|
10
|
-
import {
|
|
11
|
+
import { resolveSecretsEnv } from "./secrets.js";
|
|
12
|
+
import { startRunReplay } from "./run-replay-http.js";
|
|
13
|
+
import { matchRoute, parseAgentControlRoute, parseRunFeedbackRoute, parseRunReplayRoute, parseRunResumeRoute, parseRunRoute, parseSelfImprovementRoute } from "./route-match.js";
|
|
11
14
|
import { resolveIngressRateLimit } from "./rate-limit.js";
|
|
12
15
|
import { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
|
|
13
16
|
import { DIRECT_CHANNEL, conversationBelongsToAgent, decodeConversationCursor, decodeMessageCursor, directAgentScope, directConversationPatch, directTurnFromBody, encodeConversationCursor, encodeMessageCursor, parseConversationHttpRoute } from "./direct-conversations-http.js";
|
|
14
17
|
import { usageHttpResult, usageReconciliationHttpResult } from "./usage-http.js";
|
|
18
|
+
import { isWorkspaceAdminPath, workspaceAdminHttpResult } from "./workspace-admin-http.js";
|
|
15
19
|
export { resolveProvider } from "./provider-resolver.js";
|
|
20
|
+
export { resolveSecretsEnv } from "./secrets.js";
|
|
16
21
|
export { createMemoryRateLimiterStore, parseRateLimitSpec, resolveIngressRateLimit } from "./rate-limit.js";
|
|
17
22
|
export { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
|
|
18
23
|
export function nodeAdapter(options = {}) {
|
|
@@ -34,11 +39,17 @@ export function withNodeModelHarnesses(options) {
|
|
|
34
39
|
}
|
|
35
40
|
export async function createProductionRuntimeOptions(options) {
|
|
36
41
|
const artifactRoot = resolve(options.artifactRoot);
|
|
37
|
-
const env = options.env ?? process.env;
|
|
38
42
|
const runtimeOptions = loadRuntimeBundle(artifactRoot);
|
|
39
43
|
runtimeOptions.devMode = options.devMode ?? false;
|
|
44
|
+
const env = await resolveSecretsEnv({
|
|
45
|
+
manifest: runtimeOptions.manifest,
|
|
46
|
+
env: options.env ?? process.env,
|
|
47
|
+
artifactRoot,
|
|
48
|
+
devMode: options.devMode ?? false
|
|
49
|
+
});
|
|
40
50
|
const state = await createStateAdapter(runtimeOptions, artifactRoot, env);
|
|
41
51
|
const blob = await createBlobAdapter(runtimeOptions, artifactRoot, env);
|
|
52
|
+
const mediaProcessors = await createMediaProcessors(runtimeOptions, artifactRoot, env);
|
|
42
53
|
const connectionStores = createConnectionStores(runtimeOptions, artifactRoot, env, state, options.devMode ?? false);
|
|
43
54
|
const selfImprovementStores = createSelfImprovementStores(artifactRoot, env, state);
|
|
44
55
|
const sandbox = await createSandboxAdapter(runtimeOptions, artifactRoot, env);
|
|
@@ -46,20 +57,23 @@ export async function createProductionRuntimeOptions(options) {
|
|
|
46
57
|
...runtimeOptions,
|
|
47
58
|
state,
|
|
48
59
|
blob,
|
|
60
|
+
mediaProcessors,
|
|
49
61
|
sandbox,
|
|
50
62
|
connectionGrantStore: connectionStores.grantStore,
|
|
51
63
|
connectionAuthorizationSessionStore: connectionStores.authorizationSessionStore,
|
|
64
|
+
connectionEventStore: connectionStores.eventStore,
|
|
52
65
|
skillStore: selfImprovementStores.skillStore,
|
|
66
|
+
learningStore: selfImprovementStores.learningStore,
|
|
53
67
|
scheduleStore: selfImprovementStores.scheduleStore,
|
|
54
68
|
connectionDefinitionStore: selfImprovementStores.connectionDefinitionStore,
|
|
55
69
|
env,
|
|
56
70
|
devMode: options.devMode ?? false
|
|
57
71
|
};
|
|
58
|
-
const callbackBaseUrl = env.
|
|
72
|
+
const callbackBaseUrl = env.ASSEMBLY_LINE_CONNECTION_CALLBACK_BASE_URL ?? env.ASSEMBLY_LINE_PUBLIC_URL;
|
|
59
73
|
if (callbackBaseUrl)
|
|
60
74
|
out.connectionCallbackBaseUrl = callbackBaseUrl;
|
|
61
75
|
// Production default: bound run concurrency unless the operator chose a limit.
|
|
62
|
-
if (!(options.devMode ?? false) && env.
|
|
76
|
+
if (!(options.devMode ?? false) && env.ASSEMBLY_LINE_MAX_CONCURRENT_RUNS === undefined) {
|
|
63
77
|
out.maxConcurrentRuns = 16;
|
|
64
78
|
}
|
|
65
79
|
return withNodeModelHarnesses(out);
|
|
@@ -87,7 +101,7 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
87
101
|
if (request.method === "GET" && url.pathname === "/health") {
|
|
88
102
|
if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
|
|
89
103
|
return;
|
|
90
|
-
send(response, 200,
|
|
104
|
+
send(response, 200, revisionHealth(options.manifest));
|
|
91
105
|
return;
|
|
92
106
|
}
|
|
93
107
|
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
@@ -96,6 +110,7 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
96
110
|
send(response, 200, {
|
|
97
111
|
ok: true,
|
|
98
112
|
agentRevision: options.manifest.agentRevision,
|
|
113
|
+
buildRevision: options.manifest.buildRevision ?? options.manifest.agentRevision,
|
|
99
114
|
capabilities: evalRunsEnabled(runtime, options) ? ["eval-runs"] : []
|
|
100
115
|
});
|
|
101
116
|
return;
|
|
@@ -106,10 +121,10 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
106
121
|
if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
|
|
107
122
|
return;
|
|
108
123
|
if (drain.draining) {
|
|
109
|
-
send(response, 503, { ok: false, draining: true });
|
|
124
|
+
send(response, 503, { ...revisionHealth(options.manifest), ok: false, draining: true });
|
|
110
125
|
return;
|
|
111
126
|
}
|
|
112
|
-
send(response, 200,
|
|
127
|
+
send(response, 200, revisionHealth(options.manifest));
|
|
113
128
|
return;
|
|
114
129
|
}
|
|
115
130
|
if (request.method === "GET" && url.pathname === "/manifest") {
|
|
@@ -148,6 +163,25 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
148
163
|
send(response, 200, result);
|
|
149
164
|
return;
|
|
150
165
|
}
|
|
166
|
+
if (isWorkspaceAdminPath(url.pathname)) {
|
|
167
|
+
const method = request.method ?? "GET";
|
|
168
|
+
if (!["GET", "POST", "DELETE"].includes(method)) {
|
|
169
|
+
send(response, 405, { error: "Method not allowed." });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!await authorizeOrSend(request, response, runtime, options, method === "GET" ? "admin-read" : "agent-control", url.pathname))
|
|
173
|
+
return;
|
|
174
|
+
try {
|
|
175
|
+
const body = method === "POST"
|
|
176
|
+
? (await readBody(request, maxRequestBodyBytes(runtime, options))).body
|
|
177
|
+
: {};
|
|
178
|
+
send(response, 200, await workspaceAdminHttpResult(runtime, method, url.pathname, body, url.searchParams));
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
151
185
|
const conversationRoute = parseConversationHttpRoute(url.pathname);
|
|
152
186
|
if (conversationRoute?.kind === "collection" && request.method === "GET") {
|
|
153
187
|
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
@@ -276,14 +310,21 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
276
310
|
send(response, 400, { error: "A message or attachment is required." });
|
|
277
311
|
return;
|
|
278
312
|
}
|
|
313
|
+
const workspaceId = stringBodyValue(body.workspaceId);
|
|
314
|
+
const projectId = stringBodyValue(body.projectId);
|
|
315
|
+
const tenantId = stringBodyValue(body.tenantId);
|
|
279
316
|
const runOptions = {
|
|
280
317
|
message: direct.turn.message,
|
|
281
318
|
channel: DIRECT_CHANNEL,
|
|
282
319
|
approve: direct.approve,
|
|
283
|
-
turn: direct.turn
|
|
320
|
+
turn: direct.turn,
|
|
321
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
322
|
+
...(projectId ? { projectId } : {}),
|
|
323
|
+
...(tenantId ? { tenantId } : {})
|
|
284
324
|
};
|
|
285
|
-
if (
|
|
286
|
-
Object.assign(runOptions, {
|
|
325
|
+
if (body.sandboxCredentials && typeof body.sandboxCredentials === "object" && !Array.isArray(body.sandboxCredentials)) {
|
|
326
|
+
Object.assign(runOptions, { sandboxCredentials: body.sandboxCredentials });
|
|
327
|
+
}
|
|
287
328
|
try {
|
|
288
329
|
if (direct.stream) {
|
|
289
330
|
await serveStreamingRun(runtime, response, (onRunCreated) => runtime.run({ ...runOptions, onRunCreated }), (result) => summarizeRun(result));
|
|
@@ -313,6 +354,104 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
313
354
|
send(response, 200, await queryAgentRuns(runtime.state, options.manifest, { includeDetails: false, limit }));
|
|
314
355
|
return;
|
|
315
356
|
}
|
|
357
|
+
const memoryAdminRoute = parseMemoryAdminRoute(url.pathname);
|
|
358
|
+
if (memoryAdminRoute) {
|
|
359
|
+
const agentScope = durableAgentScopeForManifest(options.manifest);
|
|
360
|
+
if (request.method === "GET") {
|
|
361
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
362
|
+
return;
|
|
363
|
+
const result = memoryAdminRoute.kind === "list"
|
|
364
|
+
? await listMemoryAdmin(runtime.state, agentScope, url.searchParams)
|
|
365
|
+
: await readMemoryAdmin(runtime.state, agentScope, memoryAdminRoute.path);
|
|
366
|
+
send(response, result.status, result.body);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (request.method === "DELETE" && memoryAdminRoute.kind === "document") {
|
|
370
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
371
|
+
return;
|
|
372
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
373
|
+
return;
|
|
374
|
+
const result = await deleteMemoryAdmin(runtime.state, agentScope, memoryAdminRoute.path, url.searchParams);
|
|
375
|
+
send(response, result.status, result.body);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
send(response, 405, { error: "Method not allowed." });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const selfImprovementRoute = parseSelfImprovementRoute(url.pathname);
|
|
382
|
+
const selfImprovementSurface = url.searchParams.get("surface")?.trim() || "root";
|
|
383
|
+
if (selfImprovementRoute && request.method === "GET") {
|
|
384
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
385
|
+
return;
|
|
386
|
+
if (selfImprovementRoute.kind === "pending") {
|
|
387
|
+
send(response, 200, { surface: selfImprovementSurface, changes: await runtime.listPendingSkillChanges(selfImprovementSurface) });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (selfImprovementRoute.kind === "reviews") {
|
|
391
|
+
send(response, 200, { surface: selfImprovementSurface, reviews: await runtime.listBackgroundReviews(selfImprovementSurface) });
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (selfImprovementRoute.kind === "revisions") {
|
|
395
|
+
send(response, 200, { surface: selfImprovementSurface, name: selfImprovementRoute.name, revisions: await runtime.listSkillRevisions(selfImprovementRoute.name, selfImprovementSurface) });
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
send(response, 405, { error: "Method not allowed." });
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (selfImprovementRoute && request.method === "POST") {
|
|
402
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
403
|
+
return;
|
|
404
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
|
|
405
|
+
return;
|
|
406
|
+
if (selfImprovementRoute.kind === "approve") {
|
|
407
|
+
try {
|
|
408
|
+
send(response, 200, await runtime.approveSkillChange(selfImprovementRoute.id, selfImprovementSurface));
|
|
409
|
+
}
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
412
|
+
send(response, 404, { error: error.message });
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
throw error;
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (selfImprovementRoute.kind === "reject") {
|
|
420
|
+
try {
|
|
421
|
+
await runtime.rejectSkillChange(selfImprovementRoute.id, selfImprovementSurface);
|
|
422
|
+
send(response, 200, { rejected: true, id: selfImprovementRoute.id, surface: selfImprovementSurface });
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
426
|
+
send(response, 404, { error: error.message });
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
throw error;
|
|
430
|
+
}
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (selfImprovementRoute.kind === "restore") {
|
|
434
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
435
|
+
const revision = numberBodyValue(body.revision);
|
|
436
|
+
if (revision === undefined || revision < 1 || !Number.isInteger(revision)) {
|
|
437
|
+
send(response, 400, { error: "A positive integer revision is required." });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
send(response, 200, await runtime.restoreSkillRevision(selfImprovementRoute.name, revision, selfImprovementSurface));
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
if (error instanceof Error && /not found/i.test(error.message)) {
|
|
445
|
+
send(response, 404, { error: error.message });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
throw error;
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
send(response, 405, { error: "Method not allowed." });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
316
455
|
if (request.method === "GET" && url.pathname === "/usage") {
|
|
317
456
|
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
318
457
|
return;
|
|
@@ -404,6 +543,31 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
404
543
|
send(response, 200, { ok: true, matched: results.length, results });
|
|
405
544
|
return;
|
|
406
545
|
}
|
|
546
|
+
if (request.method === "POST" && url.pathname === "/assembly-line/connections/events/reconcile") {
|
|
547
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
548
|
+
return;
|
|
549
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
550
|
+
const principal = stringBodyValue(body.userId) && stringBodyValue(body.issuer)
|
|
551
|
+
? { type: "user", id: stringBodyValue(body.userId), issuer: stringBodyValue(body.issuer) }
|
|
552
|
+
: undefined;
|
|
553
|
+
const connection = stringBodyValue(body.connection);
|
|
554
|
+
const publicUrl = stringBodyValue(body.publicUrl);
|
|
555
|
+
const results = await runtime.reconcileConnectionEvents({
|
|
556
|
+
...(connection ? { connection } : {}),
|
|
557
|
+
...(publicUrl ? { publicUrl } : {}),
|
|
558
|
+
...(principal ? { principal } : {}),
|
|
559
|
+
force: body.force === true
|
|
560
|
+
});
|
|
561
|
+
send(response, 200, { ok: true, results });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (request.method === "GET" && url.pathname === "/assembly-line/connections/events") {
|
|
565
|
+
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
566
|
+
return;
|
|
567
|
+
const results = await runtime.checkConnectionEvents(url.searchParams.get("connection") ?? undefined);
|
|
568
|
+
send(response, 200, { ok: true, results });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
407
571
|
if (request.method === "GET" && url.pathname.startsWith("/runs/")) {
|
|
408
572
|
if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
|
|
409
573
|
return;
|
|
@@ -421,7 +585,30 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
421
585
|
await serveRunEventStream(runtime, response, runRoute.runId, lastEventId);
|
|
422
586
|
return;
|
|
423
587
|
}
|
|
424
|
-
const
|
|
588
|
+
const paginatedTimeline = runRoute.kind === "timeline" && url.searchParams.has("limit");
|
|
589
|
+
const before = numberQueryValue(url.searchParams.get("before"));
|
|
590
|
+
const after = numberQueryValue(url.searchParams.get("after"));
|
|
591
|
+
const limit = numberQueryValue(url.searchParams.get("limit"));
|
|
592
|
+
if (paginatedTimeline && limit === undefined) {
|
|
593
|
+
send(response, 400, { error: "Run timeline limit must be a number." });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
let timeline;
|
|
597
|
+
try {
|
|
598
|
+
timeline = paginatedTimeline
|
|
599
|
+
? await loadRunTimelinePage(runtime, runRoute.runId, {
|
|
600
|
+
...(before !== undefined ? { before } : {}),
|
|
601
|
+
...(after !== undefined ? { after } : {}),
|
|
602
|
+
limit: limit
|
|
603
|
+
})
|
|
604
|
+
: await loadRunTimeline(runtime, runRoute.runId);
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
if (!paginatedTimeline)
|
|
608
|
+
throw error;
|
|
609
|
+
send(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
425
612
|
if (!timeline) {
|
|
426
613
|
send(response, 404, { error: `Run not found: ${runRoute.runId}` });
|
|
427
614
|
return;
|
|
@@ -438,6 +625,62 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
438
625
|
return;
|
|
439
626
|
}
|
|
440
627
|
if (request.method === "POST" && url.pathname.startsWith("/runs/")) {
|
|
628
|
+
const replayRoute = parseRunReplayRoute(url.pathname);
|
|
629
|
+
if (replayRoute) {
|
|
630
|
+
if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
|
|
631
|
+
return;
|
|
632
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
|
|
633
|
+
return;
|
|
634
|
+
try {
|
|
635
|
+
const replay = await startRunReplay(runtime, replayRoute.runId);
|
|
636
|
+
void replay.completion.catch((error) => {
|
|
637
|
+
runtime.logger.error("replay run failed after durable creation", {
|
|
638
|
+
sourceRunId: replayRoute.runId,
|
|
639
|
+
runId: replay.run.id,
|
|
640
|
+
error: error instanceof Error ? error.message : String(error)
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
send(response, 202, { runId: replay.run.id, status: replay.run.status });
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (error instanceof RunReplayError) {
|
|
647
|
+
send(response, error.code === "run_not_found" ? 404 : 409, { error: error.message, code: error.code });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (error instanceof RunCapacityError) {
|
|
651
|
+
send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
|
|
652
|
+
"retry-after": String(retryAfterSeconds(error.retryAfterMs))
|
|
653
|
+
});
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
throw error;
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const feedbackRoute = parseRunFeedbackRoute(url.pathname);
|
|
661
|
+
if (feedbackRoute) {
|
|
662
|
+
if (!await authorizeOrSend(request, response, runtime, options, "run-control", url.pathname))
|
|
663
|
+
return;
|
|
664
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "run-control", url.pathname))
|
|
665
|
+
return;
|
|
666
|
+
if (!await runtime.state.getRun(feedbackRoute.runId)) {
|
|
667
|
+
send(response, 404, { error: `Run not found: ${feedbackRoute.runId}` });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
|
|
671
|
+
const rating = stringBodyValue(body.rating);
|
|
672
|
+
const comment = stringBodyValue(body.comment);
|
|
673
|
+
if (rating !== "positive" && rating !== "negative") {
|
|
674
|
+
send(response, 400, { error: "Feedback rating must be 'positive' or 'negative'." });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const result = await runtime.recordRunFeedback(feedbackRoute.runId, {
|
|
678
|
+
rating,
|
|
679
|
+
...(comment ? { comment } : {})
|
|
680
|
+
});
|
|
681
|
+
send(response, 202, result);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
441
684
|
const resumeRoute = parseRunResumeRoute(url.pathname);
|
|
442
685
|
if (resumeRoute) {
|
|
443
686
|
const isHitlResume = resumeRoute.kind === "approve" || resumeRoute.kind === "answer";
|
|
@@ -464,7 +707,18 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
464
707
|
send(response, 409, { error: `Run ${resumeRoute.runId} is not ${expected} (status: ${run.status}).` });
|
|
465
708
|
return;
|
|
466
709
|
}
|
|
467
|
-
if (resumeRoute.kind === "cancel" &&
|
|
710
|
+
if (resumeRoute.kind === "cancel" && run.status === "cancelled") {
|
|
711
|
+
// Cancellation is an idempotent operator command. A retry may be
|
|
712
|
+
// the first response the client receives even when an earlier
|
|
713
|
+
// request already committed the terminal state.
|
|
714
|
+
send(response, 200, {
|
|
715
|
+
runId: run.id,
|
|
716
|
+
status: run.status,
|
|
717
|
+
controlIntent: run.controlIntent ?? null
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (resumeRoute.kind === "cancel" && ["completed", "failed"].includes(run.status)) {
|
|
468
722
|
send(response, 409, { error: `Run ${resumeRoute.runId} is already terminal (status: ${run.status}).` });
|
|
469
723
|
return;
|
|
470
724
|
}
|
|
@@ -505,6 +759,17 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
505
759
|
}
|
|
506
760
|
// The resume idempotency claim rejects a concurrent/replayed resume.
|
|
507
761
|
const message = error instanceof Error ? error.message : String(error);
|
|
762
|
+
if (resumeRoute.kind === "cancel" && /already terminal/i.test(message)) {
|
|
763
|
+
const latest = await runtime.state.getRun(resumeRoute.runId);
|
|
764
|
+
if (latest?.status === "cancelled") {
|
|
765
|
+
send(response, 200, {
|
|
766
|
+
runId: latest.id,
|
|
767
|
+
status: latest.status,
|
|
768
|
+
controlIntent: latest.controlIntent ?? null
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
508
773
|
if (/already in progress|not waiting|not suspended|not running|already terminal/i.test(message)) {
|
|
509
774
|
send(response, 409, { error: message });
|
|
510
775
|
return;
|
|
@@ -532,11 +797,15 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
532
797
|
const session = {};
|
|
533
798
|
const sessionChannel = url.searchParams.get("channel");
|
|
534
799
|
const sessionUserId = url.searchParams.get("userId");
|
|
800
|
+
const sessionIssuer = url.searchParams.get("issuer");
|
|
535
801
|
const sessionConversationId = url.searchParams.get("conversationId");
|
|
536
802
|
if (sessionChannel)
|
|
537
803
|
session.channel = sessionChannel;
|
|
538
804
|
if (sessionUserId)
|
|
539
805
|
session.userId = sessionUserId;
|
|
806
|
+
if (sessionUserId && sessionIssuer) {
|
|
807
|
+
session.principal = { type: "user", id: sessionUserId, issuer: sessionIssuer };
|
|
808
|
+
}
|
|
540
809
|
if (sessionConversationId)
|
|
541
810
|
session.conversationId = sessionConversationId;
|
|
542
811
|
const result = await runtime.beginConnectionAuthorization(connectionName, session);
|
|
@@ -564,6 +833,33 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
564
833
|
send(response, result.status, result.body);
|
|
565
834
|
return;
|
|
566
835
|
}
|
|
836
|
+
const connectionEvent = /^\/assembly-line\/connections\/([^/]+)\/events\/([^/]+)$/u.exec(url.pathname);
|
|
837
|
+
if ((request.method === "GET" || request.method === "POST") && connectionEvent?.[1] && connectionEvent[2]) {
|
|
838
|
+
if (!await authorizeOrSend(request, response, runtime, options, "connection-event", url.pathname))
|
|
839
|
+
return;
|
|
840
|
+
if (!await rateLimitOrSend(request, response, rateLimit, "connection-event", url.pathname))
|
|
841
|
+
return;
|
|
842
|
+
const rawBody = request.method === "POST"
|
|
843
|
+
? (await readBody(request, maxRequestBodyBytes(runtime, options))).rawBody
|
|
844
|
+
: "";
|
|
845
|
+
try {
|
|
846
|
+
const result = await runtime.acceptConnectionEvent(decodeURIComponent(connectionEvent[1]), decodeURIComponent(connectionEvent[2]), {
|
|
847
|
+
method: request.method,
|
|
848
|
+
headers: normalizeHeaders(request.headers),
|
|
849
|
+
body: rawBody,
|
|
850
|
+
query: Object.fromEntries(url.searchParams.entries())
|
|
851
|
+
});
|
|
852
|
+
send(response, result.status, result.body ?? { ok: true, accepted: result.accepted, duplicates: result.duplicates, ignored: result.ignored }, result.headers);
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
if (error instanceof ConnectionEventError) {
|
|
856
|
+
send(response, error.status, { error: error.message });
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
throw error;
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
567
863
|
if (request.method === "POST" && url.pathname === "/runs") {
|
|
568
864
|
if (!apiRunCreationEnabled(runtime, options)) {
|
|
569
865
|
send(response, 403, { error: "API run creation is disabled." });
|
|
@@ -579,15 +875,26 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
579
875
|
approve: body.approve === true,
|
|
580
876
|
channel: typeof body.channel === "string" ? body.channel : "http"
|
|
581
877
|
};
|
|
582
|
-
if (typeof body.model === "string")
|
|
583
|
-
Object.assign(runOptions, { model: body.model });
|
|
584
878
|
if (typeof body.toolName === "string")
|
|
585
879
|
Object.assign(runOptions, { toolName: body.toolName });
|
|
586
880
|
if (body.input !== undefined)
|
|
587
881
|
Object.assign(runOptions, { input: body.input });
|
|
882
|
+
if (typeof body.conversationId === "string")
|
|
883
|
+
Object.assign(runOptions, { conversationId: body.conversationId });
|
|
884
|
+
if (typeof body.projectId === "string")
|
|
885
|
+
Object.assign(runOptions, { projectId: body.projectId });
|
|
886
|
+
if (typeof body.workspaceId === "string")
|
|
887
|
+
Object.assign(runOptions, { workspaceId: body.workspaceId });
|
|
888
|
+
if (typeof body.tenantId === "string")
|
|
889
|
+
Object.assign(runOptions, { tenantId: body.tenantId });
|
|
890
|
+
if (typeof body.userId === "string")
|
|
891
|
+
Object.assign(runOptions, { userId: body.userId });
|
|
892
|
+
if (body.sandboxCredentials && typeof body.sandboxCredentials === "object" && !Array.isArray(body.sandboxCredentials)) {
|
|
893
|
+
Object.assign(runOptions, { sandboxCredentials: body.sandboxCredentials });
|
|
894
|
+
}
|
|
588
895
|
if (body.eval !== undefined) {
|
|
589
896
|
if (!evalRunsEnabled(runtime, options)) {
|
|
590
|
-
send(response, 403, { error: "Eval run options are disabled. Set
|
|
897
|
+
send(response, 403, { error: "Eval run options are disabled. Set ASSEMBLY_LINE_ENABLE_EVAL_RUNS=true or allowEvalRuns." });
|
|
591
898
|
return;
|
|
592
899
|
}
|
|
593
900
|
try {
|
|
@@ -659,14 +966,7 @@ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining:
|
|
|
659
966
|
}
|
|
660
967
|
});
|
|
661
968
|
}
|
|
662
|
-
export async function listenNodeRuntime(
|
|
663
|
-
// Rename compatibility: make every branded env var readable under both its
|
|
664
|
-
// ASSEMBLY_LINE_* and OPENEVE_* spellings for all downstream readers.
|
|
665
|
-
const options = rawOptions.env
|
|
666
|
-
? { ...rawOptions, env: applyEnvAliases(rawOptions.env).env }
|
|
667
|
-
: rawOptions;
|
|
668
|
-
if (!rawOptions.env)
|
|
669
|
-
mirrorEnvAliasesInPlace(process.env);
|
|
969
|
+
export async function listenNodeRuntime(options) {
|
|
670
970
|
const runtime = new AssemblyLineRuntime(withNodeModelHarnesses(options));
|
|
671
971
|
runtime.boot();
|
|
672
972
|
validateRuntimeHttpSecurity(runtime, options);
|
|
@@ -685,7 +985,7 @@ export async function listenNodeRuntime(rawOptions) {
|
|
|
685
985
|
if (scheduler)
|
|
686
986
|
server.once("close", stopScheduler);
|
|
687
987
|
const env = options.env ?? runtime.env ?? process.env;
|
|
688
|
-
if (env.
|
|
988
|
+
if (env.ASSEMBLY_LINE_RUN_RECOVERY !== "false" && env.ASSEMBLY_LINE_RUN_RECOVERY !== "0") {
|
|
689
989
|
// Repair runs orphaned by a previous process before accepting new traffic.
|
|
690
990
|
await runtime.recoverIncompleteRuns().catch((error) => {
|
|
691
991
|
runtime.logger.warn("boot run recovery failed", {
|
|
@@ -720,14 +1020,21 @@ export async function listenNodeRuntime(rawOptions) {
|
|
|
720
1020
|
// Hosts like Railway send SIGTERM on every redeploy/restart. With no handler
|
|
721
1021
|
// installed, Node terminates immediately and the graceful drain never runs, so
|
|
722
1022
|
// in-flight runs are orphaned. Install the handlers by default (opt-out via
|
|
723
|
-
// option or
|
|
1023
|
+
// option or ASSEMBLY_LINE_SIGNAL_HANDLERS) so redeploys drain instead of hard-kill.
|
|
724
1024
|
if (options.installSignalHandlers !== false &&
|
|
725
|
-
env.
|
|
726
|
-
env.
|
|
1025
|
+
env.ASSEMBLY_LINE_SIGNAL_HANDLERS !== "false" &&
|
|
1026
|
+
env.ASSEMBLY_LINE_SIGNAL_HANDLERS !== "0") {
|
|
727
1027
|
installSignalHandlers(handle);
|
|
728
1028
|
}
|
|
729
1029
|
return handle;
|
|
730
1030
|
}
|
|
1031
|
+
function revisionHealth(manifest) {
|
|
1032
|
+
return {
|
|
1033
|
+
ok: true,
|
|
1034
|
+
agentRevision: manifest.agentRevision,
|
|
1035
|
+
buildRevision: manifest.buildRevision ?? manifest.agentRevision
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
731
1038
|
function once(fn) {
|
|
732
1039
|
let done = false;
|
|
733
1040
|
return () => {
|