@assemblyline-agents/node 0.1.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 (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/dist/adapters.d.ts +17 -0
  4. package/dist/adapters.d.ts.map +1 -0
  5. package/dist/adapters.js +122 -0
  6. package/dist/adapters.js.map +1 -0
  7. package/dist/auth.d.ts +45 -0
  8. package/dist/auth.d.ts.map +1 -0
  9. package/dist/auth.js +196 -0
  10. package/dist/auth.js.map +1 -0
  11. package/dist/direct-conversations-http.d.ts +40 -0
  12. package/dist/direct-conversations-http.d.ts.map +1 -0
  13. package/dist/direct-conversations-http.js +140 -0
  14. package/dist/direct-conversations-http.js.map +1 -0
  15. package/dist/http-io.d.ts +20 -0
  16. package/dist/http-io.d.ts.map +1 -0
  17. package/dist/http-io.js +101 -0
  18. package/dist/http-io.js.map +1 -0
  19. package/dist/index.d.ts +49 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +750 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/lifecycle.d.ts +56 -0
  24. package/dist/lifecycle.d.ts.map +1 -0
  25. package/dist/lifecycle.js +112 -0
  26. package/dist/lifecycle.js.map +1 -0
  27. package/dist/multipart.d.ts +3 -0
  28. package/dist/multipart.d.ts.map +1 -0
  29. package/dist/multipart.js +125 -0
  30. package/dist/multipart.js.map +1 -0
  31. package/dist/provider-resolver.d.ts +17 -0
  32. package/dist/provider-resolver.d.ts.map +1 -0
  33. package/dist/provider-resolver.js +94 -0
  34. package/dist/provider-resolver.js.map +1 -0
  35. package/dist/rate-limit.d.ts +60 -0
  36. package/dist/rate-limit.d.ts.map +1 -0
  37. package/dist/rate-limit.js +98 -0
  38. package/dist/rate-limit.js.map +1 -0
  39. package/dist/route-match.d.ts +21 -0
  40. package/dist/route-match.d.ts.map +1 -0
  41. package/dist/route-match.js +89 -0
  42. package/dist/route-match.js.map +1 -0
  43. package/dist/run-stream-http.d.ts +13 -0
  44. package/dist/run-stream-http.d.ts.map +1 -0
  45. package/dist/run-stream-http.js +145 -0
  46. package/dist/run-stream-http.js.map +1 -0
  47. package/dist/usage-http.d.ts +9 -0
  48. package/dist/usage-http.d.ts.map +1 -0
  49. package/dist/usage-http.js +184 -0
  50. package/dist/usage-http.js.map +1 -0
  51. package/package.json +96 -0
package/dist/index.js ADDED
@@ -0,0 +1,750 @@
1
+ import { createServer } from "node:http";
2
+ import { resolve } from "node:path";
3
+ import { codexAgentHarness } from "@assemblyline-agents/codex";
4
+ import { INTERNAL_ROUTE_PREFIX, LEGACY_INTERNAL_ROUTE_PREFIX, adapter, applyEnvAliases, mirrorEnvAliasesInPlace } from "@assemblyline-agents/core";
5
+ import { OpenEveRuntime, RunCapacityError, loadRuntimeBundle, queryAgentRuns, retryAfterSeconds, validateEvalRunOptions } from "@assemblyline-agents/runtime";
6
+ import { createBlobAdapter, createConnectionStores, createSandboxAdapter, createSelfImprovementStores, createStateAdapter, loadRunTimeline } from "./adapters.js";
7
+ import { apiRunCreationEnabled, evalRunsEnabled, authorizeOrSend, logHttpRequest, numberBodyValue, numberQueryValue, rateLimitOrSend, stringBodyValue, validateRuntimeHttpSecurity } from "./auth.js";
8
+ import { HttpRequestError, firstHeaderValue, maxRequestBodyBytes, normalizeHeaders, readBody, send, summarizeRun } from "./http-io.js";
9
+ import { serveRunEventStream, serveStreamingRun } from "./run-stream-http.js";
10
+ import { matchRoute, parseAgentControlRoute, parseRunResumeRoute, parseRunRoute } from "./route-match.js";
11
+ import { resolveIngressRateLimit } from "./rate-limit.js";
12
+ import { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
13
+ import { DIRECT_CHANNEL, conversationBelongsToAgent, decodeConversationCursor, decodeMessageCursor, directAgentScope, directConversationPatch, directTurnFromBody, encodeConversationCursor, encodeMessageCursor, parseConversationHttpRoute } from "./direct-conversations-http.js";
14
+ import { usageHttpResult, usageReconciliationHttpResult } from "./usage-http.js";
15
+ export { resolveProvider } from "./provider-resolver.js";
16
+ export { createMemoryRateLimiterStore, parseRateLimitSpec, resolveIngressRateLimit } from "./rate-limit.js";
17
+ export { createNodeRuntimeHandle, installSignalHandlers } from "./lifecycle.js";
18
+ export function nodeAdapter(options = {}) {
19
+ return {
20
+ ...adapter("node", options),
21
+ kind: "node",
22
+ capabilities: ["http-runtime", "hosted-container-runtime", "local-server"]
23
+ };
24
+ }
25
+ /** Install Node-hosted model harnesses without changing Pi's provider defaults. */
26
+ export function withNodeModelHarnesses(options) {
27
+ return {
28
+ ...options,
29
+ modelHarnesses: {
30
+ "openai-codex": codexAgentHarness(),
31
+ ...(options.modelHarnesses ?? {})
32
+ }
33
+ };
34
+ }
35
+ export async function createProductionRuntimeOptions(options) {
36
+ const artifactRoot = resolve(options.artifactRoot);
37
+ const env = options.env ?? process.env;
38
+ const runtimeOptions = loadRuntimeBundle(artifactRoot);
39
+ runtimeOptions.devMode = options.devMode ?? false;
40
+ const state = await createStateAdapter(runtimeOptions, artifactRoot, env);
41
+ const blob = await createBlobAdapter(runtimeOptions, artifactRoot, env);
42
+ const connectionStores = createConnectionStores(runtimeOptions, artifactRoot, env, state, options.devMode ?? false);
43
+ const selfImprovementStores = createSelfImprovementStores(artifactRoot, env, state);
44
+ const sandbox = await createSandboxAdapter(runtimeOptions, artifactRoot, env);
45
+ const out = {
46
+ ...runtimeOptions,
47
+ state,
48
+ blob,
49
+ sandbox,
50
+ connectionGrantStore: connectionStores.grantStore,
51
+ connectionAuthorizationSessionStore: connectionStores.authorizationSessionStore,
52
+ skillStore: selfImprovementStores.skillStore,
53
+ scheduleStore: selfImprovementStores.scheduleStore,
54
+ connectionDefinitionStore: selfImprovementStores.connectionDefinitionStore,
55
+ env,
56
+ devMode: options.devMode ?? false
57
+ };
58
+ const callbackBaseUrl = env.OPENEVE_CONNECTION_CALLBACK_BASE_URL ?? env.OPENEVE_PUBLIC_URL;
59
+ if (callbackBaseUrl)
60
+ out.connectionCallbackBaseUrl = callbackBaseUrl;
61
+ // Production default: bound run concurrency unless the operator chose a limit.
62
+ if (!(options.devMode ?? false) && env.OPENEVE_MAX_CONCURRENT_RUNS === undefined) {
63
+ out.maxConcurrentRuns = 16;
64
+ }
65
+ return withNodeModelHarnesses(out);
66
+ }
67
+ export function createNodeRuntimeServer(options) {
68
+ const runtime = options.runtime ?? new OpenEveRuntime(withNodeModelHarnesses(options));
69
+ runtime.boot();
70
+ validateRuntimeHttpSecurity(runtime, options);
71
+ return createNodeRuntimeServerForRuntime(runtime, options);
72
+ }
73
+ function createNodeRuntimeServerForRuntime(runtime, options, drain = { draining: false }) {
74
+ const rateLimit = resolveIngressRateLimit(options.rateLimit, options.env ?? runtime.env ?? process.env);
75
+ return createServer(async (request, response) => {
76
+ const startedAt = Date.now();
77
+ let channel;
78
+ response.on("finish", () => {
79
+ logHttpRequest(runtime, request, response, startedAt, channel);
80
+ });
81
+ try {
82
+ if (!request.url) {
83
+ send(response, 400, { error: "Missing URL." });
84
+ return;
85
+ }
86
+ const url = new URL(request.url, "http://localhost");
87
+ // Rename compatibility: /assembly-line/* aliases the legacy /openeve/*
88
+ // internal route family. Normalizing before dispatch gives every branch
89
+ // identical auth/rate-limit behavior and one shared rate-limit bucket.
90
+ if (url.pathname === INTERNAL_ROUTE_PREFIX || url.pathname.startsWith(`${INTERNAL_ROUTE_PREFIX}/`)) {
91
+ url.pathname = LEGACY_INTERNAL_ROUTE_PREFIX + url.pathname.slice(INTERNAL_ROUTE_PREFIX.length);
92
+ }
93
+ if (request.method === "GET" && url.pathname === "/health") {
94
+ if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
95
+ return;
96
+ send(response, 200, { ok: true, agentRevision: options.manifest.agentRevision });
97
+ return;
98
+ }
99
+ if (request.method === "GET" && url.pathname === "/healthz") {
100
+ if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
101
+ return;
102
+ send(response, 200, {
103
+ ok: true,
104
+ agentRevision: options.manifest.agentRevision,
105
+ capabilities: evalRunsEnabled(runtime, options) ? ["eval-runs"] : []
106
+ });
107
+ return;
108
+ }
109
+ if (request.method === "GET" && url.pathname === "/readyz") {
110
+ // Readiness (vs /health liveness): 503 while draining so load
111
+ // balancers stop routing new traffic during graceful shutdown.
112
+ if (!await authorizeOrSend(request, response, runtime, options, "public-health", url.pathname))
113
+ return;
114
+ if (drain.draining) {
115
+ send(response, 503, { ok: false, draining: true });
116
+ return;
117
+ }
118
+ send(response, 200, { ok: true, agentRevision: options.manifest.agentRevision });
119
+ return;
120
+ }
121
+ if (request.method === "GET" && url.pathname === "/manifest") {
122
+ if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
123
+ return;
124
+ send(response, 200, options.manifest);
125
+ return;
126
+ }
127
+ if (request.method === "GET" && url.pathname === "/routes") {
128
+ if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
129
+ return;
130
+ send(response, 200, options.manifest.routeTable);
131
+ return;
132
+ }
133
+ const agentControlRoute = parseAgentControlRoute(url.pathname);
134
+ if (agentControlRoute && request.method === "GET" && agentControlRoute.kind === "control") {
135
+ if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
136
+ return;
137
+ send(response, 200, await runtime.getAgentControl());
138
+ return;
139
+ }
140
+ if (agentControlRoute && request.method === "POST" && agentControlRoute.kind !== "control") {
141
+ if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
142
+ return;
143
+ if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
144
+ return;
145
+ const requestId = firstHeaderValue(request.headers["x-request-id"] ?? request.headers["x-railway-request-id"]);
146
+ const metadata = {
147
+ source: "http",
148
+ path: url.pathname,
149
+ ...(requestId ? { requestId } : {})
150
+ };
151
+ const result = agentControlRoute.kind === "quiesce" || agentControlRoute.kind === "resume"
152
+ ? await runtime.setAgentMaintenance(agentControlRoute.kind === "quiesce", metadata)
153
+ : await runtime.setAgentEnabled(agentControlRoute.kind === "enable", metadata);
154
+ send(response, 200, result);
155
+ return;
156
+ }
157
+ const conversationRoute = parseConversationHttpRoute(url.pathname);
158
+ if (conversationRoute?.kind === "collection" && request.method === "GET") {
159
+ if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
160
+ return;
161
+ if (!runtime.state.listConversations) {
162
+ send(response, 501, { error: "The configured state adapter does not support conversation listing." });
163
+ return;
164
+ }
165
+ const cursorValue = url.searchParams.get("cursor");
166
+ const before = decodeConversationCursor(cursorValue);
167
+ if (cursorValue && !before) {
168
+ send(response, 400, { error: "Invalid conversation cursor." });
169
+ return;
170
+ }
171
+ const limitParam = Number(url.searchParams.get("limit"));
172
+ const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(Math.floor(limitParam), 100) : 40;
173
+ const archivedValue = url.searchParams.get("archived");
174
+ const archived = archivedValue === "true" ? true : archivedValue === "all" ? undefined : false;
175
+ const listed = await runtime.state.listConversations({
176
+ agentScope: directAgentScope(options.manifest),
177
+ channel: url.searchParams.get("channel") || DIRECT_CHANNEL,
178
+ ...(url.searchParams.get("subject") ? { subject: url.searchParams.get("subject") } : {}),
179
+ ...(archived !== undefined ? { archived } : {}),
180
+ ...(before ? { before } : {}),
181
+ limit: limit + 1
182
+ });
183
+ const hasMore = listed.length > limit;
184
+ const conversations = listed.slice(0, limit);
185
+ send(response, 200, {
186
+ conversations,
187
+ nextCursor: hasMore && conversations.length > 0
188
+ ? encodeConversationCursor(conversations[conversations.length - 1])
189
+ : null
190
+ });
191
+ return;
192
+ }
193
+ if (conversationRoute?.kind === "messages" && request.method === "GET") {
194
+ if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
195
+ return;
196
+ const conversation = await runtime.state.getConversation(conversationRoute.conversationId);
197
+ if (!conversation || !conversationBelongsToAgent(conversation, directAgentScope(options.manifest))) {
198
+ send(response, 404, { error: "Conversation not found." });
199
+ return;
200
+ }
201
+ const cursorValue = url.searchParams.get("before");
202
+ const before = decodeMessageCursor(cursorValue);
203
+ if (cursorValue && !before) {
204
+ send(response, 400, { error: "Invalid message cursor." });
205
+ return;
206
+ }
207
+ const limitParam = Number(url.searchParams.get("limit"));
208
+ const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(Math.floor(limitParam), 100) : 50;
209
+ const listed = runtime.state.listMessages
210
+ ? await runtime.state.listMessages({
211
+ conversationId: conversation.id,
212
+ ...(before ? { before } : {}),
213
+ limit: limit + 1
214
+ })
215
+ : before
216
+ ? []
217
+ : await runtime.state.listRecentMessages(conversation.id, limit + 1);
218
+ if (before && !runtime.state.listMessages) {
219
+ send(response, 501, { error: "The configured state adapter does not support message pagination." });
220
+ return;
221
+ }
222
+ const hasMore = listed.length > limit;
223
+ const messages = hasMore ? listed.slice(1) : listed;
224
+ send(response, 200, {
225
+ messages,
226
+ nextCursor: hasMore && messages.length > 0 ? encodeMessageCursor(messages[0]) : null
227
+ });
228
+ return;
229
+ }
230
+ if (conversationRoute?.kind === "record" && request.method === "PATCH") {
231
+ if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
232
+ return;
233
+ if (!runtime.state.updateConversation) {
234
+ send(response, 501, { error: "The configured state adapter does not support conversation updates." });
235
+ return;
236
+ }
237
+ const existing = await runtime.state.getConversation(conversationRoute.conversationId);
238
+ if (!existing || !conversationBelongsToAgent(existing, directAgentScope(options.manifest))) {
239
+ send(response, 404, { error: "Conversation not found." });
240
+ return;
241
+ }
242
+ const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
243
+ const patch = directConversationPatch(body);
244
+ if (Object.keys(patch).length === 0) {
245
+ send(response, 400, { error: "Provide a title, archived flag, or metadata patch." });
246
+ return;
247
+ }
248
+ send(response, 200, await runtime.state.updateConversation(existing.id, patch));
249
+ return;
250
+ }
251
+ if (conversationRoute?.kind === "turns" && request.method === "POST") {
252
+ channel = DIRECT_CHANNEL;
253
+ if (!apiRunCreationEnabled(runtime, options)) {
254
+ send(response, 403, { error: "API run creation is disabled." });
255
+ return;
256
+ }
257
+ if (!await authorizeOrSend(request, response, runtime, options, "run-create", url.pathname))
258
+ return;
259
+ if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname, DIRECT_CHANNEL))
260
+ return;
261
+ const existing = await runtime.state.getConversation(conversationRoute.conversationId);
262
+ if (existing && !conversationBelongsToAgent(existing, directAgentScope(options.manifest))) {
263
+ send(response, 404, { error: "Conversation not found." });
264
+ return;
265
+ }
266
+ if (existing?.channel && existing.channel !== DIRECT_CHANNEL) {
267
+ send(response, 409, { error: `Conversation belongs to channel ${existing.channel}.` });
268
+ return;
269
+ }
270
+ if (existing?.archivedAt) {
271
+ send(response, 409, { error: "Archived conversations cannot accept new turns." });
272
+ return;
273
+ }
274
+ const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
275
+ const idempotencyKey = firstHeaderValue(request.headers["idempotency-key"]);
276
+ const direct = directTurnFromBody({
277
+ conversationId: conversationRoute.conversationId,
278
+ body,
279
+ ...(idempotencyKey ? { idempotencyKey } : {})
280
+ });
281
+ if (!direct.turn.message && !(direct.turn.attachments?.length)) {
282
+ send(response, 400, { error: "A message or attachment is required." });
283
+ return;
284
+ }
285
+ const runOptions = {
286
+ message: direct.turn.message,
287
+ channel: DIRECT_CHANNEL,
288
+ approve: direct.approve,
289
+ turn: direct.turn
290
+ };
291
+ if (direct.turn.model)
292
+ Object.assign(runOptions, { model: direct.turn.model });
293
+ try {
294
+ if (direct.stream) {
295
+ await serveStreamingRun(runtime, response, (onRunCreated) => runtime.run({ ...runOptions, onRunCreated }), (result) => summarizeRun(result));
296
+ return;
297
+ }
298
+ const result = await runtime.run(runOptions);
299
+ send(response, 200, summarizeRun(result));
300
+ }
301
+ catch (error) {
302
+ if (error instanceof RunCapacityError) {
303
+ send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
304
+ "retry-after": String(retryAfterSeconds(error.retryAfterMs))
305
+ });
306
+ return;
307
+ }
308
+ throw error;
309
+ }
310
+ return;
311
+ }
312
+ if (request.method === "GET" && url.pathname === "/runs") {
313
+ if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
314
+ return;
315
+ // Light list: no per-run detail (avoids the 1+4N fan-out that hangs on
316
+ // busy agents). Bounded to the most recent runs; ?limit overrides.
317
+ const limitParam = Number(url.searchParams.get("limit"));
318
+ const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(limitParam, 1000) : 200;
319
+ send(response, 200, await queryAgentRuns(runtime.state, options.manifest, { includeDetails: false, limit }));
320
+ return;
321
+ }
322
+ if (request.method === "GET" && url.pathname === "/usage") {
323
+ if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
324
+ return;
325
+ try {
326
+ send(response, 200, await usageHttpResult(runtime.state, url));
327
+ }
328
+ catch (error) {
329
+ send(response, 400, { error: error instanceof Error ? error.message : String(error) });
330
+ }
331
+ return;
332
+ }
333
+ if (request.method === "POST" && url.pathname === "/usage/reconcile") {
334
+ if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
335
+ return;
336
+ try {
337
+ const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
338
+ send(response, 200, await usageReconciliationHttpResult(runtime.state, runtime.env, body));
339
+ }
340
+ catch (error) {
341
+ send(response, 400, { error: error instanceof Error ? error.message : String(error) });
342
+ }
343
+ return;
344
+ }
345
+ if ((request.method === "GET" || request.method === "POST")
346
+ && (url.pathname === "/openeve/automations/tick" || url.pathname === "/openeve/scheduler/tick")) {
347
+ if (!await authorizeOrSend(request, response, runtime, options, "scheduler", url.pathname))
348
+ return;
349
+ if (!await rateLimitOrSend(request, response, rateLimit, "scheduler", url.pathname))
350
+ return;
351
+ const body = request.method === "POST" ? (await readBody(request, maxRequestBodyBytes(runtime, options))).body : {};
352
+ const nowValue = stringBodyValue(body.now) ?? url.searchParams.get("now") ?? undefined;
353
+ const lookbackValue = numberBodyValue(body.lookbackMs) ?? numberQueryValue(url.searchParams.get("lookbackMs"));
354
+ const tickOptions = {};
355
+ if (nowValue)
356
+ tickOptions.now = new Date(nowValue);
357
+ if (lookbackValue !== undefined)
358
+ tickOptions.lookbackMs = lookbackValue;
359
+ if (tickOptions.now && Number.isNaN(tickOptions.now.getTime())) {
360
+ send(response, 400, { error: "Invalid scheduler now timestamp." });
361
+ return;
362
+ }
363
+ const results = await runtime.runDueAutomations(tickOptions);
364
+ send(response, 200, { ok: true, results });
365
+ return;
366
+ }
367
+ if (request.method === "POST" && url.pathname === "/openeve/automations/events") {
368
+ if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
369
+ return;
370
+ if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
371
+ return;
372
+ const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
373
+ const source = stringBodyValue(body.source);
374
+ const event = stringBodyValue(body.event);
375
+ const eventId = stringBodyValue(body.eventId);
376
+ const payload = body.payload;
377
+ if (!source || !event || !eventId || !payload || typeof payload !== "object" || Array.isArray(payload)) {
378
+ send(response, 400, { error: "Automation events require source, event, eventId, and an object payload." });
379
+ return;
380
+ }
381
+ const automationEvent = {
382
+ source,
383
+ event,
384
+ eventId,
385
+ payload: payload
386
+ };
387
+ const occurredAt = stringBodyValue(body.occurredAt);
388
+ const userId = stringBodyValue(body.userId);
389
+ const conversationId = stringBodyValue(body.conversationId);
390
+ const projectId = stringBodyValue(body.projectId);
391
+ if (occurredAt && Number.isNaN(new Date(occurredAt).getTime())) {
392
+ send(response, 400, { error: "Automation event occurredAt must be a valid ISO timestamp." });
393
+ return;
394
+ }
395
+ if (occurredAt)
396
+ automationEvent.occurredAt = occurredAt;
397
+ if (userId)
398
+ automationEvent.userId = userId;
399
+ if (conversationId)
400
+ automationEvent.conversationId = conversationId;
401
+ if (projectId)
402
+ automationEvent.projectId = projectId;
403
+ if (body.delivery && typeof body.delivery === "object" && !Array.isArray(body.delivery)) {
404
+ automationEvent.delivery = body.delivery;
405
+ }
406
+ if (body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata)) {
407
+ automationEvent.metadata = body.metadata;
408
+ }
409
+ const results = await runtime.dispatchAutomationEvent(automationEvent);
410
+ send(response, 200, { ok: true, matched: results.length, results });
411
+ return;
412
+ }
413
+ if (request.method === "GET" && url.pathname.startsWith("/runs/")) {
414
+ if (!await authorizeOrSend(request, response, runtime, options, "admin-read", url.pathname))
415
+ return;
416
+ const runRoute = parseRunRoute(url.pathname);
417
+ if (!runRoute) {
418
+ send(response, 404, { error: "Not found." });
419
+ return;
420
+ }
421
+ if (runRoute.kind === "stream") {
422
+ if (!await runtime.state.getRun(runRoute.runId)) {
423
+ send(response, 404, { error: `Run not found: ${runRoute.runId}` });
424
+ return;
425
+ }
426
+ const lastEventId = firstHeaderValue(request.headers["last-event-id"]) ?? url.searchParams.get("lastEventId") ?? undefined;
427
+ await serveRunEventStream(runtime, response, runRoute.runId, lastEventId);
428
+ return;
429
+ }
430
+ const timeline = await loadRunTimeline(runtime, runRoute.runId);
431
+ if (!timeline) {
432
+ send(response, 404, { error: `Run not found: ${runRoute.runId}` });
433
+ return;
434
+ }
435
+ if (runRoute.kind === "events") {
436
+ send(response, 200, { runId: runRoute.runId, events: timeline.events });
437
+ return;
438
+ }
439
+ if (runRoute.kind === "timeline") {
440
+ send(response, 200, timeline);
441
+ return;
442
+ }
443
+ send(response, 200, timeline);
444
+ return;
445
+ }
446
+ if (request.method === "POST" && url.pathname.startsWith("/runs/")) {
447
+ const resumeRoute = parseRunResumeRoute(url.pathname);
448
+ if (resumeRoute) {
449
+ const isHitlResume = resumeRoute.kind === "approve" || resumeRoute.kind === "answer";
450
+ const routeClass = isHitlResume || resumeRoute.kind === "resume" ? "run-resume" : "run-control";
451
+ if (!await authorizeOrSend(request, response, runtime, options, routeClass, url.pathname))
452
+ return;
453
+ if (!await rateLimitOrSend(request, response, rateLimit, routeClass, url.pathname))
454
+ return;
455
+ const run = await runtime.state.getRun(resumeRoute.runId);
456
+ if (!run) {
457
+ send(response, 404, { error: `Run not found: ${resumeRoute.runId}` });
458
+ return;
459
+ }
460
+ const expected = resumeRoute.kind === "approve"
461
+ ? "waiting_for_approval"
462
+ : resumeRoute.kind === "answer"
463
+ ? "waiting_for_input"
464
+ : resumeRoute.kind === "suspend"
465
+ ? "running"
466
+ : resumeRoute.kind === "resume"
467
+ ? "suspended"
468
+ : undefined;
469
+ if (expected && run.status !== expected) {
470
+ send(response, 409, { error: `Run ${resumeRoute.runId} is not ${expected} (status: ${run.status}).` });
471
+ return;
472
+ }
473
+ if (resumeRoute.kind === "cancel" && ["completed", "failed", "cancelled"].includes(run.status)) {
474
+ send(response, 409, { error: `Run ${resumeRoute.runId} is already terminal (status: ${run.status}).` });
475
+ return;
476
+ }
477
+ let answer = "";
478
+ if (resumeRoute.kind === "answer") {
479
+ const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
480
+ answer = typeof body.answer === "string" ? body.answer : "";
481
+ if (!answer) {
482
+ send(response, 400, { error: "An 'answer' string is required to resume a waiting run." });
483
+ return;
484
+ }
485
+ }
486
+ try {
487
+ if (resumeRoute.kind === "cancel" || resumeRoute.kind === "suspend") {
488
+ const controlled = resumeRoute.kind === "cancel"
489
+ ? await runtime.cancelRun(resumeRoute.runId)
490
+ : await runtime.suspendRun(resumeRoute.runId);
491
+ send(response, controlled.status === "running" ? 202 : 200, {
492
+ runId: controlled.id,
493
+ status: controlled.status,
494
+ controlIntent: controlled.controlIntent ?? null
495
+ });
496
+ return;
497
+ }
498
+ const result = resumeRoute.kind === "approve"
499
+ ? await runtime.resumeApproval(resumeRoute.runId)
500
+ : resumeRoute.kind === "answer"
501
+ ? await runtime.resumeInput(resumeRoute.runId, answer)
502
+ : await runtime.resumeSuspended(resumeRoute.runId);
503
+ send(response, 200, summarizeRun(result));
504
+ }
505
+ catch (error) {
506
+ if (error instanceof RunCapacityError) {
507
+ send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
508
+ "retry-after": String(retryAfterSeconds(error.retryAfterMs))
509
+ });
510
+ return;
511
+ }
512
+ // The resume idempotency claim rejects a concurrent/replayed resume.
513
+ const message = error instanceof Error ? error.message : String(error);
514
+ if (/already in progress|not waiting|not suspended|not running|already terminal/i.test(message)) {
515
+ send(response, 409, { error: message });
516
+ return;
517
+ }
518
+ throw error;
519
+ }
520
+ return;
521
+ }
522
+ // Fall through: unknown POST /runs/... path.
523
+ }
524
+ if (request.method === "GET" && url.pathname === "/openeve/connections/authorize") {
525
+ // Authenticated JSON, not a public 302: the operator surface fetches
526
+ // the consent URL and opens the browser itself, so unauthenticated
527
+ // callers can neither trigger consent flows nor enumerate connection
528
+ // names. The provider's redirect lands on the public callback below.
529
+ if (!await authorizeOrSend(request, response, runtime, options, "agent-control", url.pathname))
530
+ return;
531
+ if (!await rateLimitOrSend(request, response, rateLimit, "agent-control", url.pathname))
532
+ return;
533
+ const connectionName = url.searchParams.get("connection") ?? "";
534
+ if (!connectionName) {
535
+ send(response, 400, { error: "A 'connection' query parameter is required." });
536
+ return;
537
+ }
538
+ const session = {};
539
+ const sessionChannel = url.searchParams.get("channel");
540
+ const sessionUserId = url.searchParams.get("userId");
541
+ const sessionConversationId = url.searchParams.get("conversationId");
542
+ if (sessionChannel)
543
+ session.channel = sessionChannel;
544
+ if (sessionUserId)
545
+ session.userId = sessionUserId;
546
+ if (sessionConversationId)
547
+ session.conversationId = sessionConversationId;
548
+ const result = await runtime.beginConnectionAuthorization(connectionName, session);
549
+ send(response, result.status === "unavailable" ? 409 : 200, result);
550
+ return;
551
+ }
552
+ if ((request.method === "GET" || request.method === "POST") && url.pathname === "/openeve/connections/callback") {
553
+ if (!await authorizeOrSend(request, response, runtime, options, "connection-callback", url.pathname))
554
+ return;
555
+ const { rawBody, body } = request.method === "POST" ? await readBody(request, maxRequestBodyBytes(runtime, options)) : { rawBody: "", body: {} };
556
+ const params = Object.fromEntries(url.searchParams.entries());
557
+ if (Object.keys(body).length > 0) {
558
+ for (const [key, value] of Object.entries(body)) {
559
+ if (typeof value === "string")
560
+ params[key] = value;
561
+ }
562
+ }
563
+ const callbackInput = {
564
+ params,
565
+ method: request.method
566
+ };
567
+ if (rawBody)
568
+ callbackInput.body = rawBody;
569
+ const result = await runtime.completeConnectionAuthorizationCallback(callbackInput);
570
+ send(response, result.status, result.body);
571
+ return;
572
+ }
573
+ if (request.method === "POST" && url.pathname === "/runs") {
574
+ if (!apiRunCreationEnabled(runtime, options)) {
575
+ send(response, 403, { error: "API run creation is disabled." });
576
+ return;
577
+ }
578
+ if (!await authorizeOrSend(request, response, runtime, options, "run-create", url.pathname))
579
+ return;
580
+ if (!await rateLimitOrSend(request, response, rateLimit, "run-create", url.pathname))
581
+ return;
582
+ const { body } = await readBody(request, maxRequestBodyBytes(runtime, options));
583
+ const runOptions = {
584
+ message: typeof body.message === "string" ? body.message : "",
585
+ approve: body.approve === true,
586
+ channel: typeof body.channel === "string" ? body.channel : "http"
587
+ };
588
+ if (typeof body.model === "string")
589
+ Object.assign(runOptions, { model: body.model });
590
+ if (typeof body.toolName === "string")
591
+ Object.assign(runOptions, { toolName: body.toolName });
592
+ if (body.input !== undefined)
593
+ Object.assign(runOptions, { input: body.input });
594
+ if (body.eval !== undefined) {
595
+ if (!evalRunsEnabled(runtime, options)) {
596
+ send(response, 403, { error: "Eval run options are disabled. Set OPENEVE_ENABLE_EVAL_RUNS=true or allowEvalRuns." });
597
+ return;
598
+ }
599
+ try {
600
+ Object.assign(runOptions, { eval: validateEvalRunOptions(body.eval) });
601
+ }
602
+ catch (error) {
603
+ send(response, 400, { error: error instanceof Error ? error.message : String(error) });
604
+ return;
605
+ }
606
+ }
607
+ try {
608
+ if (body.stream === true) {
609
+ await serveStreamingRun(runtime, response, (onRunCreated) => runtime.run({ ...runOptions, onRunCreated }), (result) => summarizeRun(result));
610
+ return;
611
+ }
612
+ const result = await runtime.run(runOptions);
613
+ send(response, 200, summarizeRun(result));
614
+ }
615
+ catch (error) {
616
+ if (error instanceof RunCapacityError) {
617
+ send(response, 429, { error: "Run capacity exhausted. Retry later.", retryAfterMs: error.retryAfterMs }, {
618
+ "retry-after": String(retryAfterSeconds(error.retryAfterMs))
619
+ });
620
+ return;
621
+ }
622
+ throw error;
623
+ }
624
+ return;
625
+ }
626
+ const requestMethod = request.method;
627
+ const routeMatch = requestMethod ? matchRoute(options.manifest.routeTable, requestMethod, url.pathname) : undefined;
628
+ if (routeMatch && requestMethod) {
629
+ channel = routeMatch.route.channel;
630
+ const authorization = await authorizeOrSend(request, response, runtime, options, "provider-channel", url.pathname, channel);
631
+ if (!authorization)
632
+ return;
633
+ if (!await rateLimitOrSend(request, response, rateLimit, "provider-channel", url.pathname, channel))
634
+ return;
635
+ const { rawBody, rawBodyBytes, body } = await readBody(request, maxRequestBodyBytes(runtime, options));
636
+ const result = await runtime.dispatchHttpChannel(routeMatch.route.channel, {
637
+ method: requestMethod,
638
+ url: request.url,
639
+ path: url.pathname,
640
+ params: routeMatch.params,
641
+ headers: normalizeHeaders(request.headers),
642
+ rawBody,
643
+ rawBodyBytes,
644
+ body,
645
+ trusted: authorization.trusted === true
646
+ });
647
+ send(response, result.status, result.body, result.headers);
648
+ return;
649
+ }
650
+ send(response, 404, { error: "Not found." });
651
+ }
652
+ catch (error) {
653
+ if (error instanceof HttpRequestError) {
654
+ send(response, error.status, { error: error.message });
655
+ return;
656
+ }
657
+ // Log the real cause server-side but return a generic message: raw
658
+ // exception text (DB errors, file paths, driver internals) must not leak
659
+ // to callers.
660
+ runtime.logger.error("unhandled runtime http error", {
661
+ error: error instanceof Error ? error.message : String(error),
662
+ ...(error instanceof Error && error.stack ? { stack: error.stack } : {})
663
+ });
664
+ send(response, 500, { error: "Internal server error." });
665
+ }
666
+ });
667
+ }
668
+ export async function listenNodeRuntime(rawOptions) {
669
+ // Rename compatibility: make every branded env var readable under both its
670
+ // ASSEMBLY_LINE_* and OPENEVE_* spellings for all downstream readers.
671
+ const options = rawOptions.env
672
+ ? { ...rawOptions, env: applyEnvAliases(rawOptions.env).env }
673
+ : rawOptions;
674
+ if (!rawOptions.env)
675
+ mirrorEnvAliasesInPlace(process.env);
676
+ const runtime = new OpenEveRuntime(withNodeModelHarnesses(options));
677
+ runtime.boot();
678
+ validateRuntimeHttpSecurity(runtime, options);
679
+ const drain = { draining: false };
680
+ const server = createNodeRuntimeServerForRuntime(runtime, options, drain);
681
+ const port = options.port ?? 0;
682
+ const host = options.host ?? "127.0.0.1";
683
+ await new Promise((resolve, reject) => {
684
+ server.once("error", reject);
685
+ server.listen(port, host, () => resolve());
686
+ });
687
+ const address = server.address();
688
+ const boundPort = typeof address === "object" && address ? address.port : port;
689
+ const scheduler = await runtime.startSchedulerLoop();
690
+ const stopScheduler = once(() => scheduler?.stop());
691
+ if (scheduler)
692
+ server.once("close", stopScheduler);
693
+ const env = options.env ?? runtime.env ?? process.env;
694
+ if (env.OPENEVE_RUN_RECOVERY !== "false" && env.OPENEVE_RUN_RECOVERY !== "0") {
695
+ // Repair runs orphaned by a previous process before accepting new traffic.
696
+ await runtime.recoverIncompleteRuns().catch((error) => {
697
+ runtime.logger.warn("boot run recovery failed", {
698
+ error: error instanceof Error ? error.message : String(error)
699
+ });
700
+ });
701
+ }
702
+ const ingress = await runtime.startChannelIngress();
703
+ const stopIngress = onceAsync(async () => {
704
+ await ingress?.stop();
705
+ });
706
+ if (ingress)
707
+ server.once("close", () => void stopIngress());
708
+ const workers = runtime.startBackgroundWorkers();
709
+ const stopWorkers = once(() => workers.stop());
710
+ server.once("close", stopWorkers);
711
+ const handleInput = {
712
+ server,
713
+ url: `http://${host}:${boundPort}`,
714
+ runtime,
715
+ drain,
716
+ env,
717
+ stopIngress,
718
+ stopScheduler,
719
+ stopWorkers
720
+ };
721
+ if (scheduler)
722
+ handleInput.scheduler = scheduler;
723
+ if (ingress)
724
+ handleInput.ingress = ingress;
725
+ const handle = createNodeRuntimeHandle(handleInput);
726
+ // Hosts like Railway send SIGTERM on every redeploy/restart. With no handler
727
+ // installed, Node terminates immediately and the graceful drain never runs, so
728
+ // in-flight runs are orphaned. Install the handlers by default (opt-out via
729
+ // option or OPENEVE_SIGNAL_HANDLERS) so redeploys drain instead of hard-kill.
730
+ if (options.installSignalHandlers !== false &&
731
+ env.OPENEVE_SIGNAL_HANDLERS !== "false" &&
732
+ env.OPENEVE_SIGNAL_HANDLERS !== "0") {
733
+ installSignalHandlers(handle);
734
+ }
735
+ return handle;
736
+ }
737
+ function once(fn) {
738
+ let done = false;
739
+ return () => {
740
+ if (done)
741
+ return;
742
+ done = true;
743
+ fn();
744
+ };
745
+ }
746
+ function onceAsync(fn) {
747
+ let pending;
748
+ return () => (pending ??= fn());
749
+ }
750
+ //# sourceMappingURL=index.js.map