@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.
Files changed (50) hide show
  1. package/LICENSE +1 -1
  2. package/dist/adapters.d.ts +5 -1
  3. package/dist/adapters.d.ts.map +1 -1
  4. package/dist/adapters.js +38 -17
  5. package/dist/adapters.js.map +1 -1
  6. package/dist/auth.d.ts +2 -2
  7. package/dist/auth.d.ts.map +1 -1
  8. package/dist/auth.js +10 -22
  9. package/dist/auth.js.map +1 -1
  10. package/dist/direct-conversations-http.d.ts.map +1 -1
  11. package/dist/direct-conversations-http.js +0 -3
  12. package/dist/direct-conversations-http.js.map +1 -1
  13. package/dist/http-io.d.ts.map +1 -1
  14. package/dist/http-io.js +14 -3
  15. package/dist/http-io.js.map +1 -1
  16. package/dist/index.d.ts +5 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +337 -30
  19. package/dist/index.js.map +1 -1
  20. package/dist/lifecycle.d.ts +2 -2
  21. package/dist/lifecycle.d.ts.map +1 -1
  22. package/dist/lifecycle.js +2 -2
  23. package/dist/lifecycle.js.map +1 -1
  24. package/dist/memory-admin-http.d.ts +27 -0
  25. package/dist/memory-admin-http.d.ts.map +1 -0
  26. package/dist/memory-admin-http.js +115 -0
  27. package/dist/memory-admin-http.js.map +1 -0
  28. package/dist/rate-limit.d.ts +4 -4
  29. package/dist/rate-limit.d.ts.map +1 -1
  30. package/dist/rate-limit.js +9 -7
  31. package/dist/rate-limit.js.map +1 -1
  32. package/dist/route-match.d.ts +24 -0
  33. package/dist/route-match.d.ts.map +1 -1
  34. package/dist/route-match.js +45 -0
  35. package/dist/route-match.js.map +1 -1
  36. package/dist/run-replay-http.d.ts +11 -0
  37. package/dist/run-replay-http.d.ts.map +1 -0
  38. package/dist/run-replay-http.js +16 -0
  39. package/dist/run-replay-http.js.map +1 -0
  40. package/dist/secrets.d.ts +19 -0
  41. package/dist/secrets.d.ts.map +1 -0
  42. package/dist/secrets.js +58 -0
  43. package/dist/secrets.js.map +1 -0
  44. package/dist/usage-http.js +10 -10
  45. package/dist/usage-http.js.map +1 -1
  46. package/dist/workspace-admin-http.d.ts +5 -0
  47. package/dist/workspace-admin-http.d.ts.map +1 -0
  48. package/dist/workspace-admin-http.js +128 -0
  49. package/dist/workspace-admin-http.js.map +1 -0
  50. 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, applyEnvAliases, mirrorEnvAliasesInPlace } from "@assemblyline-agents/core";
5
- import { AssemblyLineRuntime, RunCapacityError, loadRuntimeBundle, queryAgentRuns, retryAfterSeconds, validateEvalRunOptions } from "@assemblyline-agents/runtime";
6
- import { createBlobAdapter, createConnectionStores, createSandboxAdapter, createSelfImprovementStores, createStateAdapter, loadRunTimeline } from "./adapters.js";
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 { matchRoute, parseAgentControlRoute, parseRunResumeRoute, parseRunRoute } from "./route-match.js";
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.OPENEVE_CONNECTION_CALLBACK_BASE_URL ?? env.OPENEVE_PUBLIC_URL;
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.OPENEVE_MAX_CONCURRENT_RUNS === undefined) {
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, { ok: true, agentRevision: options.manifest.agentRevision });
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, { ok: true, agentRevision: options.manifest.agentRevision });
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 (direct.turn.model)
286
- Object.assign(runOptions, { model: direct.turn.model });
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 timeline = await loadRunTimeline(runtime, runRoute.runId);
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" && ["completed", "failed", "cancelled"].includes(run.status)) {
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 OPENEVE_ENABLE_EVAL_RUNS=true or allowEvalRuns." });
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(rawOptions) {
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.OPENEVE_RUN_RECOVERY !== "false" && env.OPENEVE_RUN_RECOVERY !== "0") {
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 OPENEVE_SIGNAL_HANDLERS) so redeploys drain instead of hard-kill.
1023
+ // option or ASSEMBLY_LINE_SIGNAL_HANDLERS) so redeploys drain instead of hard-kill.
724
1024
  if (options.installSignalHandlers !== false &&
725
- env.OPENEVE_SIGNAL_HANDLERS !== "false" &&
726
- env.OPENEVE_SIGNAL_HANDLERS !== "0") {
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 () => {